我们经常在使用视频录制时,动态添加像监控画面一样的精确到秒的时间信息,需要记录当前时间到视频中去,这样的需求很常见。今天使用Java代码来实现,通常来说这种用C/C++更高效。如使用FFmpeg的filter功能可以很快实现。下图是网上找的一张监控视频画面。那么我们在录制视频时实现类似功能:
在使用Java代码实现时,需要使用视频录制(MediaRecorder)类,状态周期图如下:
最终效果视频:
class="video_iframe" data-vidtype="2" allowfullscreen="" frameborder="0" data-ratio="0.5666666666666667" data-w="272" data-src="http://v.qq.com/iframe/player.html?vid=z1334nqtq77&width=638&height=478.5&auto=0" style="display: none; width: 638px !important; height: 478.5px !important;" width="638" height="478.5" data-vh="478.5" data-vw="638"/>
下面是实现的一些步骤
1、使用MediaRecord录制一段视频。
private void startRecorder() {
if (mState == State.RECORDE) {
return;
}
if (mState == State.COMPLETE) {
mCamera.startPreview();//重拍启动预览,这里主要启动对焦程序,如果不启动,则manager不知道已经启动,在stop的时候不会关闭预览
}
// 关闭预览并释放资源
Camera c = mCamera;
c.unlock();
mRecorder = new MediaRecorder();
mRecorder.reset();
mRecorder.setCamera(c);
mRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
mRecorder.setProfile(CamcorderProfile.get(mQuality));
//设置选择角度,顺时针方向,因为默认是逆向度的,这样图像就是正常显示了,这里设置的是观看保存后的视频的角度
mRecorder.setOrientationHint(90);
videoCreateTime = System.currentTimeMillis();
Log.d(TAG, "video cache path:" + fileCachePath);
try {
File file = new File(fileCachePath);
if (!file.getParentFile().exists()) file.getParentFile().mkdirs();
if (file.exists()) file.delete();
file.createNewFile();
mRecorder.setOutputFile(file.getAbsolutePath());
} catch (IOException e) {
e.printStackTrace();
}
try {
mRecorder.prepare();
mRecorder.start();
} catch (Exception e) {
e.printStackTrace();
}
mState = State.RECORDE;
unRecordButton.setVisibility(View.VISIBLE);
recordButton.setVisibility(View.GONE);
previewButton.setVisibility(View.GONE);
unRecordButton.setText("停止");
}
2、使用MediaExtractor分离出音视频数据,使用MediaMuxer进行合成。
private void init(String srcPath, String dstPath) {
MediaMetadataRetriever mmr = new MediaMetadataRetriever();
mmr.setDataSource(srcPath);
try {
mSrcWidth = Integer.parseInt(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));
mSrcHeight = Integer.parseInt(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT));
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
}
try {
mExtractor = new MediaExtractor();
mExtractor.setDataSource(srcPath);
String mime = null;
for (int i = 0; i < mExtractor.getTrackCount(); i++) {
//获取码流的详细格式/配置信息
MediaFormat format = mExtractor.getTrackFormat(i);
mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("video/")) {
mVideoTrackIndex = i;
mMediaFormat = format;
} else if (mime.startsWith("audio/")) {
continue;
} else {
continue;
}
}
mExtractor.selectTrack(mVideoTrackIndex); //选择读取视频数据
//创建合成器
mSrcWidth = mMediaFormat.getInteger(MediaFormat.KEY_WIDTH);
mSrcHeight = mMediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
mVideoMaxInputSize = mMediaFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);
mVideoDuration = mMediaFormat.getLong(MediaFormat.KEY_DURATION);
mVideoRotation = 90;//低版本不支持获取旋转,手动写入了
if (mVideoRotation == 90) {
mDstWidth = mSrcHeight;
mDstHeight = mSrcWidth;
} else if (mVideoRotation == 0) {
mDstWidth = mSrcWidth;
mDstHeight = mSrcHeight;
}
mMax = (int) (mVideoDuration / 1000);
Log.d(TAG, "videoWidth=" + mSrcWidth + ",videoHeight=" + mSrcHeight + ",mVideoMaxInputSize=" + mVideoMaxInputSize + ",mVideoDuration=" + mVideoDuration + ",mVideoRotation=" + mVideoRotation);
//写入文件的合成器
mMediaMuxer = new MediaMuxer(dstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
MediaCodec.BufferInfo videoInfo = new MediaCodec.BufferInfo();
videoInfo.presentationTimeUs = 0;
initMediaDecode(mime);
} catch (IOException e) {
e.printStackTrace();
}
}
3、借用MediaCodec解码出每一帧,进行处理。实际就是Codec出的outputBuffer数据。再用Canvas把时间画上去。时间获取是视频录制时创建文件的时间,也就是我们在录制那个时间。这样就能匹配上。
private void decode(MediaCodec.BufferInfo videoInfo, int inputIndex) {
mMediaDecode.queueInputBuffer(inputIndex, 0, videoInfo.size, videoInfo.presentationTimeUs, videoInfo.flags);//通知MediaDecode解码刚刚传入的数据
//获取解码得到的byte[]数据 参数BufferInfo上面已介绍 10000同样为等待时间 同上-1代表一直等待,0代表不等待。此处单位为微秒
//此处建议不要填-1 有些时候并没有数据输出,那么他就会一直卡在这 等待
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputIndex = mMediaDecode.dequeueOutputBuffer(bufferInfo, 50000);
switch (outputIndex) {
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
Log.d(TAG, "INFO_OUTPUT_BUFFERS_CHANGED");
mDecodeOutputBuffers = mMediaDecode.getOutputBuffers();
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
MediaFormat format = mMediaDecode.getOutputFormat();
Log.d(TAG, "New mMediaFormat " + format);
if (format != null && format.containsKey(MediaFormat.KEY_COLOR_FORMAT)) {
mVideoDecodeColor = format.getInteger(MediaFormat.KEY_COLOR_FORMAT);
Log.d(TAG, "decode extract get mVideoDecodeColor =" + mVideoDecodeColor);//解码得到视频颜色格式
}
initMediaEncode();//根据颜色格式初始化编码器
break;
case MediaCodec.INFO_TRY_AGAIN_LATER:
Log.d(TAG, "dequeueOutputBuffer timed out!");
break;
default:
ByteBuffer outputBuffer;
byte[] frame;
while (outputIndex >= 0) {//每次解码完成的数据不一定能一次吐出 所以用while循环,保证解码器吐出所有数据
outputBuffer = mDecodeOutputBuffers[outputIndex];//拿到用于存放PCM数据的Buffer
frame = new byte[bufferInfo.size];//BufferInfo内定义了此数据块的大小
outputBuffer.get(frame);//将Buffer内的数据取出到字节数组中
outputBuffer.clear();//数据取出后一定记得清空此Buffer MediaCodec是循环使用这些Buffer的,不清空下次会得到同样的数据
handleFrameData(frame, videoInfo);//自己定义的方法,供编码器所在的线程获取数据,下面会贴出代码
mMediaDecode.releaseOutputBuffer(outputIndex, false);//此操作一定要做,不然MediaCodec用完所有的Buffer后 将不能向外输出数据
outputIndex = mMediaDecode.dequeueOutputBuffer(mDecodeBufferInfo, 50000);//再次获取数据,如果没有数据输出则outputIndex=-1 循环结束
}
break;
}
}
4、关键在于取到的数据帧,是YUV格式的,根据拍摄时选取的不同还不一样,Camera获取数据是NV21格式,也就是YUV420sp,拿到NV21格式的帧以后,转成RGB渲染,然后又转回NV21交给编码器。
private void handleFrameData(byte[] data, MediaCodec.BufferInfo info) {
//转换Yuv数据成RGB格式的bitmap
Bitmap image = changeYUV2Bitmap(data);
//旋转图像
Bitmap bitmap = rotaingImage(mVideoRotation, image);
image.recycle();
//渲染文字及背景 0-1ms
Canvas canvas = new Canvas(bitmap);
canvas.drawText(mVideoTimeFormat.format(mVideo.videoCreateTime + info.presentationTimeUs / 1000), 20, 60, mTextPaint);
//通知进度 0-5ms
mProgress = (int) (info.presentationTimeUs / 1000);
mProgressHandler.obtainMessage(PROGRESS, mProgress, mMax, mVideo).sendToTarget();
synchronized (MediaCodec.class) {//加锁
mTimeDataContainer.add(new Frame(info, bitmap));
}
}
5、源码点击【阅读原文】进行下载。