我们经常在使用视频录制时,动态添加像监控画面一样的精确到秒的时间信息,需要记录当前时间到视频中去,这样的需求很常见。今天使用Java代码来实现,通常来说这种用C/C++更高效。如使用FFmpeg的filter功能可以很快实现。下图是网上找的一张监控视频画面。那么我们在录制视频时实现类似功能:
在使用Java代码实现时,需要使用视频录制(MediaRecorder)类,状态周期图如下:
最终效果视频:
下面是实现的一些步骤
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、源码点击【阅读原文】进行下载。