android视频处理之动态时间水印效果
2016-09-01 16:46
661 查看
最近的项目中遇到一个非常头痛的需求,在android端录制视频的时候动态添加像监控画面一样的精确到秒的时间信息,关键是,并不是说只在播放器的界面显示时间就可以了,而是录制到视频里面去,这个MP4在电脑上播放也能看到每个画面的时间。
最后想到的办法是在录制完成以后去处理这个视频。
期间参考了很多资料,比较有用的大概是ffmpeg和比较新的Api
mediaCodec系列了。介于ffmpeg都是C实现,和一大堆NDK相关,本人不是太懂,就重点关注了MediaCodec系列。
参考逻辑流程图一目了然的这篇博文
http://blog.csdn.net/xipiaoyouzi/article/details/37599759
MediaCodec进行编解码的大体逻辑是这样的(转载):
MediaExtractor,MediaCodec,MediaMuxer这三个Api已经可以很多多媒体处理工作了,比如用MediaExtractor+MediaMuxer就可以做音视频剪辑,MediaCodec+MediaMuxer就可以做自定义的录像机,一起用就可以做特效编辑,滤镜之类的了。
添加时间水印效果
关键在于取到的数据帧,是YUV格式的,根据拍摄时选取的不同还不一样,我用到的NV21格式,也就是YUV420sp,拿到NV21格式的帧以后,转成RGB渲染,然后又转回NV21交给encoder,看起来好笨重,也非常地耗时,但我还没找到更好的办法。
然后是转回NV21
看到上面的代码执行耗时,根本不可能实时录制时处理,就算后台服务处理,3秒钟的720*480视频得花费约20秒..但把encodeYUV420SP等换成JNI实现后,速度加快了很多。
初始化编码器,设置编码后的视频格式
在Service中管理这任务,例如
打开Acitivity时,绑定服务,可以查看服务的进行状态
代码片段
简单deomo的git地址
最后想到的办法是在录制完成以后去处理这个视频。
期间参考了很多资料,比较有用的大概是ffmpeg和比较新的Api
mediaCodec系列了。介于ffmpeg都是C实现,和一大堆NDK相关,本人不是太懂,就重点关注了MediaCodec系列。
参考逻辑流程图一目了然的这篇博文
http://blog.csdn.net/xipiaoyouzi/article/details/37599759
MediaCodec进行编解码的大体逻辑是这样的(转载):
主要函数的调用逻辑如下:
MediaExtractor,MediaCodec,MediaMuxer这三个Api已经可以很多多媒体处理工作了,比如用MediaExtractor+MediaMuxer就可以做音视频剪辑,MediaCodec+MediaMuxer就可以做自定义的录像机,一起用就可以做特效编辑,滤镜之类的了。
添加时间水印效果
关键在于取到的数据帧,是YUV格式的,根据拍摄时选取的不同还不一样,我用到的NV21格式,也就是YUV420sp,拿到NV21格式的帧以后,转成RGB渲染,然后又转回NV21交给encoder,看起来好笨重,也非常地耗时,但我还没找到更好的办法。
private Bitmap first; private void handleFrameData(byte[] data, MediaCodec.BufferInfo info) { //YUV420sp转RGB数据 5-60ms ByteArrayOutputStream out = new ByteArrayOutputStream(); YuvImage yuvImage = new YuvImage(data, ImageFormat.NV21, srcWidth, srcHeight, null); yuvImage.compressToJpeg(new Rect(0, 0, srcWidth, srcHeight), 100, out); byte[] imageBytes = out.toByteArray(); //旋转图像,顺便解决电脑上播放被旋转90度的问题 20-50ms Bitmap image = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length); Bitmap bitmap = rotaingImageView(videoRotation, image); image.recycle(); //渲染文字 0-1ms Canvas canvas = new Canvas(bitmap); canvas.drawText(videoTimeFormat.format(videoFirstTime + info.presentationTimeUs / 1000), 10, 30, paint); //预览处理帧 0-5ms first = bitmap; handler.sendEmptyMessage((int) (info.presentationTimeUs / 1000)); synchronized (MediaCodec.class) {//记得加锁 timeDataContainer.add(new Frame(info, bitmap)); } } /* * 旋转图片 * @param angle * @param bitmap * @return Bitmap */ public Bitmap rotaingImageView(int angle, Bitmap bitmap) { //旋转图片 动作 Matrix matrix = new Matrix(); matrix.postRotate(angle); // 创建新的图片 return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); }
然后是转回NV21
/** * 获取夹了时间戳的的数据 * * @return */ private Frame getFrameData() { synchronized (MediaCodec.class) {//记得加锁 if (timeDataContainer.isEmpty()) { return null; } //从队列中获取数据 Frame frame = timeDataContainer.remove(0);////取出后将此数据remove掉 既能保证PCM数据块的取出顺序 又能及时释放内存 //转回YUV420sp 120-160ms frame.data = getNV21(dstWidth, dstHeight, frame.bitmap); return frame; } } public static byte[] getNV21(int width, int height, Bitmap scaled) { int[] argb = new int[width * height]; scaled.getPixels(argb, 0, width, 0, 0, width, height); byte[] yuv = new byte[width * height * 3 / 2]; encodeYUV420SP(yuv, argb, width, height); scaled.recycle(); return yuv; } /** * 将bitmap里得到的argb数据转成yuv420sp格式 * 这个yuv420sp数据就可以直接传给MediaCodec,通过AvcEncoder间接进行编码 * * @param yuv420sp 用来存放yuv420sp数据 * @param argb 传入argb数据 * @param width 图片width * @param height 图片height */ public static void encodeYUV420SP(byte[] yuv420sp, int[] argb, int width, int height) { final int frameSize = width * height; int yIndex = 0; int uvIndex = frameSize; int a, R, G, B, Y, U, V; int index = 0; for (int j = 0; j < height; j++) { for (int i = 0; i < width; i++) { // a = (argb[index] & 0xff000000) >> 24; // a is not used obviously R = (argb[index] & 0xff0000) >> 16; G = (argb[index] & 0xff00) >> 8; B = (argb[index] & 0xff) >> 0; // well known RGB to YUV algorithm Y = ((66 * R + 129 * G + 25 * B + 128) >> 8) + 16; U = ((-38 * R - 74 * G + 112 * B + 128) >> 8) + 128; V = ((112 * R - 94 * G - 18 * B + 128) >> 8) + 128; // NV21 has a plane of Y and interleaved planes of VU each sampled by a factor of 2 // meaning for every 4 Y pixels there are 1 V and 1 U. Note the sampling is every other // pixel AND every other scanline. yuv420sp[yIndex++] = (byte) ((Y < 0) ? 0 : ((Y > 255) ? 255 : Y)); if (j % 2 == 0 && index % 2 == 0) { yuv420sp[uvIndex++] = (byte) ((V < 0) ? 0 : ((V > 255) ? 255 : V)); yuv420sp[uvIndex++] = (byte) ((U < 0) ? 0 : ((U > 255) ? 255 : U)); } index++; } } }
看到上面的代码执行耗时,根本不可能实时录制时处理,就算后台服务处理,3秒钟的720*480视频得花费约20秒..但把encodeYUV420SP等换成JNI实现后,速度加快了很多。
初始化编码器,设置编码后的视频格式
/** * 初始化编码器 */ private void initMediaEncode() { try { MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, dstWidth, dstHeight); format.setInteger(MediaFormat.KEY_BIT_RATE, 1024 * 512); format.setInteger(MediaFormat.KEY_FRAME_RATE, 27); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, getVideoColorFormat()); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); mediaEncode = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); mediaEncode.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); } catch (IOException e) { e.printStackTrace(); } mediaEncode.start(); } /** * 获取颜色格式 */ private int getVideoColorFormat() { String model = android.os.Build.MODEL; JLog.d("px", "phone model string is " + model); if (model.startsWith("MI")) {//小米 return MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar; } else if (videoColorFormat == MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar) { return videoColorFormat; } else { return MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar; } }
在Service中管理这任务,例如
@Override public int onStartCommand(Intent intent, int flags, int startId) { super.onStartCommand(intent, flags, startId); int action = intent.getIntExtra("action", 0); if (action == REQUEST_CODEC) { VideoCodecModel video = (VideoCodecModel) intent.getSerializableExtra("video"); video = codecDao.addItem(video); if (mVideo == null) {//空闲 start(video); } else {//排队 videos.add(video); } } return START_NOT_STICKY; } private void start(VideoCodecModel video) { mTask = new VideoCodecTask(video); mTask.setProgressHandler(handler); mTask.start(); }
打开Acitivity时,绑定服务,可以查看服务的进行状态
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_show_codec); VideoCodecDao codecDao = VideoCodecDao.getInstance(this); final Intent intent = new Intent(this, WaterMarkService.class); connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { Log.d("px", "onServiceConnected"); //这里利用一个回调方法去监听服务的运行状态. binder.setOnProgressChangeListener(ShowCodecActivity.this); } @Override public void onServiceDisconnected(ComponentName name) { } }; bindService(intent, connection, Context.BIND_AUTO_CREATE); } @Override public void onProgress(int progress, int max) {} @Override public void onCodecStart(VideoCodecModel video) {} @Override public void onCodecFinish(VideoCodecModel video) {} @Override public void onCodecError(VideoCodecModel video, String msg) {} @Override public void onCodecCancel(VideoCodecModel video, boolean delete) {}
代码片段
简单deomo的git地址
相关文章推荐
- Android进阶系列8-编译时注解框架ButterKnife浅析
- xUtils框架
- Android lame库在Windows下编译
- android6.0 Activity(一) Activity创建 初始化
- Android开发:shape和selector和layer-list的(详细说明)
- ANDROID中自定义属性格式详解
- ubunbu下编译android源码
- android中selector在java代码中使用无效的解决方案
- Android过度绘制深度优化---View提前绘制
- Android Studio下NDN-JNI技术初学
- Android -- 固定在ScrollView顶部的View,类似于新浪微博的评论列表的顶部
- 获取Android Contacts联系人信息
- android studio笔记之编译运行错误
- Android之Fragment的切换不对Fragment进行重新加载
- [置顶] Android蓝牙自动配对Demo,亲测好使!!!
- 事件分发机制源码分析
- Android蓝牙自动配对Demo,亲测好使!!!
- Android入门.第二课
- Android的普通广播和有序广播
- Android实现Activity页面跳转切换动画特效