本文主要详细介绍Android视频处理的动态时间水印效果。本文中的示例代码非常详细,具有一定的参考价值。感兴趣的朋友可以参考一下。
在最近的项目中,我遇到了一个非常头疼的问题。在Android上录制视频时,我动态添加了和监视器屏幕一样精确的时间信息。关键是,仅仅把时间显示在播放器的界面上是不够的,还要录进视频里。这个MP4在电脑上播放的时候还能看到每个屏幕的时间。
最后一个想法是录制完之后再处理这个视频。
期间查阅了很多资料,比较有用的大概是ffmpeg和比较新的Api mediaCodec系列。由于ffmpeg是用C实现的,并且涉及到很多NDK,所以我不太了解,所以我把重点放在了MediaCodec系列上。
这篇博文,参考逻辑流程图就清楚了。
MEDIC进行编解码的一般逻辑如下(转载):
主要函数的调用逻辑如下:
三个API,Media Extractor,MediaCodec和MediaMuxer,已经可以完成大量的多媒体处理工作。例如,MediaExtractor MediaMuxer可用于音频和视频编辑,MediaCodec MediaMuxer可用于自定义录像机。配合使用的时候可以做特效剪辑,滤镜之类的。
添加时间水印效果
关键是获取的数据帧是YUV格式的,根据拍摄时选择的不同而不同。我用的NV21格式,也就是YUV420sp,我拿到NV21格式的帧后,转换成RGB渲染,再转回NV21,交给编码器。看起来很繁琐,很费时间,但是我还没有找到更好的方法。
私有位图优先;
private void handleFrameData(byte[]data,MediaCodec。缓冲信息){
//YUV420sp转RGB数据5-60ms
ByteArrayOutputStream out=new ByteArrayOutputStream();
yuvi image yuvi image=new yuvi image(data,ImageFormat。NV21,srcWidth,srcHeight,null);
yuvi image . compresstojpeg(new Rect(0,0,srcWidth,srcHeight),100,out);
byte[]image bytes=out . tobytearray();
//旋转图像,顺便解决电脑上播放旋转90度20-50ms的问题。
bitmap image=bitmapfactory . decodebytearray(image bytes,0,image bytes . length);
bitmap bitmap=rotating image view(video rotation,image);
image . recycle();
//渲染文本0-1毫秒
Canvas canvas=新画布(位图);
canvas . drawtext(video time format . format(video first time info . presentation time us/1000),10,30,paint);
//预览处理后的帧0-5ms
first=位图;
handler . senemptymessage((int)(info . presentation time us/1000));
synchronized(media codec . class){//记得锁定
timeDataContainer.add(新帧(信息,位图));
}
}
/*
*旋转图片
* @参数角度
* @param位图
* @返回位图
*/
公共位图旋转图像视图(int angle,Bitmap bitmap) {
//旋转图片动作
Matrix Matrix=new Matrix();
matrix.postRotate(角度);
//创建新图片
返回Bitmap.createBitmap(bitmap,0,0,bitmap.getWidth(),bitmap.getHeight(),matrix,true);
}
然后转回NV21。
/**
*获取时间戳的数据。
*
* @返回
*/
私有帧getFrameData() {
synchronized(media codec . class){//记得锁定
if(time data container . isempty()){
返回null
}
//从队列中获取数据
frame frame=time data container . remove(0);////取完这些数据,既能保证PCM数据块的取顺序,又能及时释放内存。
//转回YUV420sp 120-160ms
frame.data=getNV21(dstWidth,dstHeight,frame . bitmap);
返回框架;
}
}
public static byte[]getnv 21(int width,int height,Bitmap scaled) {
int[]argb=new int[width * height];
scaled.getPixels(argb,0,width,0,0,width,height);
byte[] yuv=新字节[宽度*高度* 3/2];
encodeYUV420SP(yuv,argb,width,height);
scaled . recycle();
返回yuv
}
/**
*将位图中获取的argb数据转换为yuv420sp格式。
*此yuv420sp数据可以直接传输到MediaCodec,由AvcEncoder间接编码。
*
* @param yuv420sp用于存储yuv420sp数据。
* @param argb传入argb数据
* @param宽度图片宽度
* @param高度图片高度
*/
public static void encode YUV 420 sp(byte[]YUV 420 sp,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身高;j ) {
for(int I=0;我宽度;i ) {
//a=(argb[index]0x ff 000000)24;//显然没有使用a
r=(argb[index]0x ff 0000)16;
g=(argb[index]0x ff 00)8;
b=(argb[index]0x ff)0;
//众所周知的RGB转YUV算法
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有一个Y平面和瓦努阿图(Vanuatu)的交错平面,每个平面都以因子2采样
//这意味着每4岁个像素有1伏和1 U .注意,采样是每隔一个进行的
//像素和每隔一条扫描线。
yuv420sp[yIndex ]=(字节)((Y 0)?0 : ((Y 255)?255:Y));
if (j % 2==0 index % 2==0) {
yuv420sp[uvIndex ]=(字节)((V 0)?0 : ((V 255)?255:V));
yuv420sp[uvIndex ]=(字节)((U 0)?0 : ((U 255)?255:U));
}
指数;
}
}
}
看到上面的代码执行耗时,根本不可能实时录制时处理,就算后台服务处理,3秒钟的720*480视频得花费约20秒.
解码与编码的地方也有很多,比如编码器在某些手机不支持颜色格式,为了适配更多机型,颜色格式的设置以后需要更换。
/**
* 初始化编码器
*/
私有void initMediaEncode(字符串mime) {
尝试{
媒体格式格式=媒体格式。创建视频格式(媒体格式.MIMETYPE_VIDEO_AVC,dstWidth,dst height);
format.setInteger(MediaFormat .KEY_BIT_RATE,1024 * 512);
format.setInteger(MediaFormat .关键帧速率,27);
format.setInteger(MediaFormat .KEY_COLOR_FORMAT,MediaCodecInfo .编解码器功能。COLOR _ format YUV 420灵活);
//格式。设置整数(媒体格式.KEY_COLOR_FORMAT,MediaCodecInfo .编解码器功能。COLOR _ format YUV 420平面);
format.setInteger(MediaFormat .KEY _ I _帧_间隔,1);
媒体编码=媒体编解码器。createencoderbytype(媒体格式.mime type _ VIDEO _ AVC);
mediaEncode.configure(格式,null,null,MediaCodec .配置_标志_编码);
} catch (IOException e) {
e。printstacktrace();
}
if (mediaEncode==null) {
JLog.e(标签,'创建媒体编码失败');
返回;
}
媒体编码。start();
}
补充:匹配大部分手机的颜色模式应该是MediaCodecInfo .编解码器功能。COLOR _ format YUV 420半平面=21,这个颜色格式是在译解码首缓冲器的时候得到的,但这个格式居然没有码率,关键帧间隔,以及英国制药学会会员等,这些只能根据自己情况设
为什么我之前用了YUV 420灵活,安卓源码里面说的YUV 420半平面器弃用
@不推荐使用{ @ link # COLOR _ format YUV 420 flexible }。
public static final int COLOR _ format YUV 420 semi planar=21;
不过现在可以从源文件首缓冲器里面解码读取出来
案例媒体编解码器。信息_输出_格式_更改:
媒体格式格式=媒体解码。获取输出格式();
Log.d(标签,'新格式'格式);
如果(格式!=空格式。包含密钥(媒体格式.KEY_COLOR_FORMAT)) {
videoColorFormat=格式。getinteger(媒体格式.KEY _ COLOR _ FORMAT);
Log.d(标签,' decode extract get videoColorFormat=' videoColorFormat);//解码得到视频颜色格式
}
initMediaEncode(videoColorFormat);//根据颜色格式初始化编码器
打破;
源码:
导入安卓。注释。目标API
导入安卓。app。服务;
导入安卓。内容。意图;
导入安卓。图形。位图;
导入安卓。图形。bitmapfactory
导入安卓。图形。画布;
导入安卓。图形。颜色;
导入安卓。图形。图像格式;
导入安卓。图形。矩阵;
导入安卓。图形。油漆;
导入安卓。图形。rect
导入安卓。图形。yuvi图像;
导入安卓。媒体。媒体编解码器;
导入安卓。媒体。mediacodecinfo
导入安卓。媒体。媒体提取器;
导入安卓。媒体。媒体格式;
导入安卓。媒体。mediametadataretriever
导入安卓。媒体。媒体复用器;
导入安卓。OS。粘合剂;
导入安卓。OS。建造;
导入安卓。OS。I活页夹;
导入安卓。OS。消息;
导入安卓。支持。注释。可空;
导入安卓。小部件。敬酒;
导入Java。io。bytearrayoutputstream
导入Java。io。文件;
导入Java。io。io异常;
导入Java。郎。参考文献。弱引用;
导入Java。nio。字节缓冲区;
导入Java。文字。简单的日期格式;
导入Java。util。ArrayList
导入Java。util。列表;
/**
*由用户在2016/8/13创建。
*/
@TargetApi(Build .版本代码.果冻_豆子_MR2)
公共类TestCodecService扩展服务{
私有媒体提取器提取器;
私人媒体多路复用器;
私人决赛静态字符串标记=' px
私有最终字符串标记=this。getclass().get simplename();
私有媒体格式格式;
private int videoMaxInputSize=0,视频旋转=0;
私人长视频时长;
private boolean decodeOver=false,encoding=false,mCancel,mDelete
//视频流在数据流中的序号
private int视频轨道索引=-1;
私有媒体编解码器媒体解码,媒体编码
私有字节缓冲区[]decodeInputBuffers,decodeOutputBuffers
私有数组列表帧时间数据容器;//数据块容器
专用媒体编解码器BufferInfo decodeBufferInfo
private int srcWidth,srcHeight,dstWidth,dstHeight
私人简单日期格式视频时间格式;
私有图像,mMax
私有视频编码道
//绘制时间戳的画笔
私漆油漆=新漆(油漆ANTI _ ALIAS _ FLAG);
@覆盖
public void onCreate() {
超级棒。oncreate();
JLog.d(标签,' onCreate ');
//视频时间戳显示格式
视频时间格式=新的简单日期格式(' yyyy/MM/DD HH:MM:ss ');
time数据容器=new ArrayList();
//初始化画笔工具
paint.setColor(颜色。白色);
画画。settextsize(20);
编解码器Dao=videocodecdao。getinstance(jingruiapp。getjrapplicationcontext());
}
@覆盖
public void onDestroy() {
超级棒。on destroy();
JLog.d(标签,‘on destroy’);
decodeOver=true
编码=假;
}
私有void init(String srcPath,String dstpath) {
MediaMetadataRetriever MMR=new MediaMetadataRetriever();
MMR。设置数据源(src路径);
尝试{
src宽度=整数。解析int(MMR。提取元数据(MediaMetadataRetriever .元数据_关键字_视频_宽度));
src高度=整数。解析int(MMR。提取元数据(MediaMetadataRetriever .元数据_关键_视频_高度));
} catch(IllegalArgumentException e){
e。printstacktrace();
} catch(IllegalStateException e){
e。printstacktrace();
}
尝试{
extractor=新媒体提取器();
提取器。设置数据源(src路径);
字符串mime=null
for(int I=0;我是提取者。gettrackcount();i ) {
//获取码流的详细格式/配置信息
媒体格式格式=提取器。gettrackformat(I);
mime=格式。getstring(媒体格式.KEY _ MIME);
如果(哑剧。以(' video/')开头{
videoTrackIndex=I;
this.format=格式
} else if(哑剧。以(' audio/')开头{
继续;
}否则{
继续;
}
}
提取器。选择轨道(videoTrackIndex);//选择读取视频数据
//创建合成器
src宽度=格式。getinteger(媒体格式.KEY _ WIDTH);
dst高度=格式。getinteger(媒体格式.KEY _ HEIGHT);
videoMaxInputSize=format。getinteger(媒体格式.KEY _ MAX _ INPUT _ SIZE);
视频时长=格式。getlong(媒体格式.KEY _ DURATION);
//视频旋转=格式。getinteger(媒体格式.KEY _ ROTATION);
videoRotation=90//低版本不支持获取旋转,手动写入了
if (videoRotation==90) {
dstWidth=srcHeight
dstHeight=srcWidth
} else if (videoRotation==0) {
dstWidth=srcWidth
dstHeight=srcHeight
}
mMax=(int)(视频时长/1000);
//int bit=this。格式。getinteger(媒体格式.KEY _ BIT _ RATE);
JLog.d(tag,' videoWidth=' srcWidth ',videoHeight=' srcHeight ',videoMaxInputSize=' videoMaxInputSize ',videoDuration=' videoDuration ',video rotation=' video rotation);
//写入文件的合成器
多路复用器=新的媒体多路复用器(dstpath,媒体多路复用器).输出格式。MUXER _ OUTPUT _ MPEG _ 4);
//向合成器添加视频轨
//videoTrackIndex=muxer。添加音轨(格式);
媒体编解码器BufferInfo videoInfo=新媒体编解码器缓冲区信息();
视频信息。演示时间us=0;
initMediaDecode(mime);
initMediaEncode(mime);
} catch (IOException e) {
e。printstacktrace();
}
}
//抽出每一帧
@TargetApi(Build .版本代码。棒棒糖)
私有空的提取(){
int输入索引=媒体解码。输入缓冲区出队(-1);//获取可用的输入缓冲器-1代表一直等待,0表示不等待建议-1,避免丢帧
if (inputIndex 0) {
JLog.d('px ','=========code over=======');
返回;
}
字节缓冲区输入缓冲区=decodeInputBuffers[输入索引];//拿到输入缓冲器
输入缓冲器。clear();
int length=提取器。readsampledata(输入缓冲区,0);//读取一帧数据,放到解码队列
如果(长度为0) {
JLog.d('px ',' extract Over ');
decodeOver=true
返回;
}否则{
//获取时间戳
长演示时间用户=提取器。getsampletime();
媒体编解码器BufferInfo videoInfo=新媒体编解码器缓冲区信息();
视频信息。偏移=0;
videoInfo.size=length
//获取帧类型,只能识别是否为我帧
视频信息。标志=提取器。getsampleflags();
视频信息。演示时间us=提取器。getsampletime();
//解码视频
解码(videoInfo,输入索引);
提取器。advance();//移动到下一帧
}
}
private void handleFrameData(byte[]data,MediaCodec .缓冲信息){
//YUV420sp转RGB数据5-60毫秒
ByteArrayOutputStream out=new ByteArrayOutputStream();
yuvi image yuvi image=新的yuvi图像(数据,图像格式.NV21,srcWidth,srcHeight,null);
yuvi图像。compresstojpge(new Rect(0,0,srcWidth,srcHeight),100,out);
byte[]图像字节=输出。tobytearray();
//旋转图像20-50毫秒
位图图像=bitmapfactory。decodebytearray(图像字节,0,图像字节。长度);
位图bitmap=旋转图像视图(视频旋转,图像);
形象。recycle();
//渲染文字0-1毫秒
画布canvas=新画布(位图);
画布。drawtext(视频时间格式。格式(mv视频。视频创建时间信息。演示时间us/1000),10,30,画图);
//通知进度0-5毫秒
mProgress=(int)(info。演示时间us/1000);
if (mListener!=null) {
mListener.onProgress(mProgress,mMax);
}
同步(媒体编解码器。class){//记得加锁
timeDataContainer.add(新帧(信息,位图));
}
}
public static byte[]getnv 21(int width,int height,Bitmap scaled) {
int[]argb=new int[width * height];
scaled.getPixels(argb,0,width,0,0,width,height);
byte[] yuv=新字节[宽度*高度* 3/2];
encodeYUV420SP(yuv,argb,width,height);
缩放。recycle();
返回yuv
}
/**
* 将位图里得到的argb数据转成yuv420sp格式
* 这个yuv420sp数据就可以直接传给媒体编解码器,通过AvcEncoder间接进行编码
*
* @param yuv420sp用来存放yuv420sp数据
* @param argb传入argb数据
* @param宽度图片宽度
* @param高度图片高度
*/
public static void encode YUV 420 sp(byte[]YUV 420 sp,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身高;j ) {
for(int I=0;我宽度;i ) {
//a=(argb[index]0x ff 000000)24;//显然没有使用a
r=(argb[index]0x ff 0000)16;
g=(argb[index]0x ff 00)8;
b=(argb[index]0x ff)0;
//众所周知的RGB转YUV算法
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有一个Y平面和瓦努阿图(Vanuatu)的交错平面,每个平面都以因子2采样
//这意味着每4岁个像素有1伏和1 U .注意,采样是每隔一个进行的
//像素和每隔一条扫描线。
yuv420sp[yIndex ]=(字节)((Y 0)?0 : ((Y 255)?255:Y));
if (j % 2==0 index % 2==0) {
yuv420sp[uvIndex ]=(字节)((V 0)?0 : ((V 255)?255:V));
yuv420sp[uvIndex ]=(字节)((U 0)?0 : ((U 255)?255:U));
}
指数;
}
}
}
/**
* 获取夹了时间戳的的数据
*
* @返回
*/
私有帧getFrameData() {
同步(媒体编解码器。class){//记得加锁
if(时间数据容器。isempty()){
返回空
}
//从队列中获取数据
帧帧=时间数据容器。删除(0);////取出后将此数据去除掉既能保证脉冲编码调制数据块的取出顺序又能及时释放内存
//转回YUV 420 sp 120-160毫秒
frame.data=getNV21(dstWidth,dstHeight,frame。位图);
返回框架;
}
}
/*
* 旋转图片
* @参数角度
* @param位图
* @返回位图
*/
公共位图旋转图像视图(int angle,Bitmap bitmap) {
//旋转图片动作
Matrix Matrix=new Matrix();
matrix.postRotate(角度);
//创建新的图片
返回Bitmap.createBitmap(bitmap,0,0,bitmap.getWidth()、bitmap.getHeight()、matrix,true);
}
/**
* 初始化解码器
*/
私有void initMediaDecode(字符串mime) {
尝试{
//创建解码器
媒体解码=媒体编解码器。createdecoderbytype(mime);
mediaDecode.configure(格式,null,null,0);
} catch (IOException e) {
e。printstacktrace();
}
if (mediaDecode==null) {
JLog.e(标签,'创建媒体解码失败');
返回;
}
媒体解码。start();
decodeInputBuffers=媒体解码。getinputbuffers();
decodeOutputBuffers=媒体解码。获取输出缓冲区();
decodeBufferInfo=新媒体编解码器缓冲区信息();//用于描述解码得到的字节[]数据的相关信息
}
/**
* 初始化编码器
*/
私有void initMediaEncode(字符串mime) {
尝试{
媒体格式格式=媒体格式。创建视频格式(媒体格式.MIMETYPE_VIDEO_AVC,dstWidth,dst height);
format.setInteger(MediaFormat .KEY_BIT_RATE,1024 * 512);
format.setInteger(MediaFormat .关键帧速率,27);
format.setInteger(MediaFormat .KEY_COLOR_FORMAT,MediaCodecInfo .编解码器功能。COLOR _ format YUV 420灵活);
//格式。设置整数(媒体格式.KEY_COLOR_FORMAT,MediaCodecInfo .编解码器功能。COLOR _ format YUV 420平面);
format.setInteger(MediaFormat .KEY _ I _帧_间隔,1);
媒体编码=媒体编解码器。createencoderbytype(媒体格式.mime type _ VIDEO _ AVC);
mediaEncode.configure(格式,null,null,MediaCodec .配置_标志_编码);
} catch (IOException e) {
e。printstacktrace();
}
if (mediaEncode==null) {
JLog.e(标签,'创建媒体编码失败');
返回;
}
媒体编码。start();
}
@TargetApi(Build .版本代码。棒棒糖)
私有空解码(媒体编解码器.BufferInfo videoInfo,int inputIndex) {
媒体解码。queueinputbuffer(输入索引,0,videoInfo.size,videoInfo.presentationTimeUs,videoInfo。旗帜);//通知媒体解码解码刚刚传入的数据
//获取解码得到的字节[]数据参数BufferInfo上面已介绍10000同样为等待时间同上-1代表一直等待,0代表不等待。此处单位为微秒
//此处建议不要填-1 有些时候并没有数据输出,那么他就会一直卡在这等待
媒体编解码器BufferInfo bufferInfo=新媒体编解码器缓冲区信息();
int输出索引=媒体解码。输出缓冲区出队(缓冲区信息,50000);
开关(输出索引){
案例媒体编解码器。信息输出缓冲区已更改:
JLog.d(标签,' INFO _ OUTPUT _ BUFFERS _ CHANGED ');
decodeOutputBuffers=媒体解码。获取输出缓冲区();
打破;
案例媒体编解码器。信息_输出_格式_更改:
JLog.d(标签,'新格式媒体解码。获取输出格式());
打破;
案例媒体编解码器。信息_重试_稍后:
JLog.d(标记,' dequeueOutputBuffer超时!');
打破;
默认值:
ByteBuffer输出缓冲器
字节[]帧;
while (outputIndex=0) {//每次解码完成的数据不一定能一次吐出所以用在…期间循环,保证解码器吐出所有数据
output buffer=decodeOutputBuffers[输出索引];//拿到用于存放脉冲编码调制数据的缓冲器
框架=新字节[缓冲区信息。size];//BufferInfo内定义了此数据块的大小
输出缓冲器。get(帧);//将缓冲器内的数据取出到字节数组中
输出缓冲器。clear();//数据取出后一定记得清空此缓冲媒体编解码器是循环使用这些缓冲器的,不清空下次会得到同样的数据
handleFrameData(帧,视频信息);//自己定义的方法,供编码器所在的线程获取数据,下面会贴出代码
媒体解码。释放输出缓冲区(输出索引,假);//此操作一定要做,不然媒体编解码器用完所有的缓冲器后将不能向外输出数据
输出索引=媒体解码。输出缓冲区出队(解码缓冲区信息,50000);//再次获取数据,如果没有数据输出则输出索引=-1循环结束
}
打破;
}
}
/**
* 编码
*/
私有空的编码(){
//获取解码器所在线程输出的数据
byte[]区块时间;
frame frame=getFrameData();
if (frame==null) {
返回;
}
chunkTime=frame.data
int输入索引=媒体编码。输入缓冲区出队(-1);//同解码器
if (inputIndex 0) {
JLog.d('px ','出列输入缓冲区返回输入索引'输入索引',然后break));
媒体编码。signalendofinputstream();
}
字节缓冲输入缓冲=媒体编码。getinputbuffers()[输入索引];//同解码器
输入缓冲器。clear();//同解码器
输入缓冲器。put(组块时间);//PCM数据填充给输入缓冲器
输入缓冲器。极限(框架。视频信息。尺寸);
媒体编码。queueinputbuffer(输入索引,0,chunkTime.length,frame。视频信息。演示时间美国,帧。视频信息。旗帜);//通知编码器编码
媒体编解码器BufferInfo bufferInfo=新媒体编解码器缓冲区信息();
int输出索引=媒体编码。输出缓冲区出队(缓冲区信息,50000);//同解码器
开关(输出索引){
案例媒体编解码器。信息输出缓冲区已更改:
JLog.d(标签,' INFO _ OUTPUT _ BUFFERS _ CHANGED ');
打破;
案例媒体编解码器。信息_输出_格式_更改:
媒体格式输出格式=媒体编码。获取输出格式();
输出格式。设置整数(媒体格式.KEY_ROTATION,视频旋转);
JLog.d(标签、‘媒体编码寻找新格式’输出格式);
//向合成器添加视频轨
视频跟踪索引=多路复用器。添加轨道(输出格式);
muxer。start();
打破;
案例媒体编解码器。信息_重试_稍后:
JLog.d(标记,' dequeueOutputBuffer超时!');
打破;
默认值:
ByteBuffer输出缓冲器
while (outputIndex=0) {//同解码器
输出缓冲=媒体编码。get output buffers()[输出索引];//拿到输出缓冲器
muxer。writesampledata(videoTrackIndex,outputBuffer,buffer info);
//JLog.d('px ',' write sampledata:' buffer info。大小’);
媒体编码。释放输出缓冲区(输出索引,假);
输出索引=媒体编码。输出缓冲区出队(缓冲区信息,50000);
}
打破;
}
}
私有空的释放(){
//全部写完后释放媒体复用器和介质提取器
提取器。发布();
媒体解码。发布();
媒体编码。发布();
muxer。stop();
muxer。发布();
}
私有解码器runnable解码器runnable;
私有编码器runnable编码器runnable;
/**
* 解码线程
*/
私有类解码器可运行扩展线程{
@覆盖
公共无效运行(){
decodeOver=false
而(!解码器){
尝试{
提取();
} catch(异常e) {
//抓住删除文件造成的异常
JLog.e("px", e.toString()); } synchronized (encodeRunnable) { encodeRunnable.notify(); } } } } /** * 编码线程 */ private class EncodeRunnable extends Thread { @Override public void run() { encoding = true; while (encoding) { if (timeDataContainer.isEmpty()) { if (decodeOver) {//解码完成,缓存也清空了 break; } try { synchronized (encodeRunnable) { wait(); } } catch (InterruptedException e) { e.printStackTrace(); } } else { encode(); } } release(); encoding = false; handler.sendEmptyMessage(-2);//发送消息完成任务 } } android.os.Handler handler = new android.os.Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case -2: onComplete(); break; default: break; } } }; public void onComplete() { if (mDelete) {//delete请求,是在cancel事件前提下 mDelete = false; new File(mVideo.srcPath).delete();//主动请求删除,删源文件,和数据库 codecDao.deleteItem(mVideo); JLog.d("px", "delete file " + mVideo.srcPath); } else { mVideo.finish = mCancel ? 0 : 100; codecDao.createOrUpdate(mVideo);//更新数据库状态为已完成,或闲置中 } if (mCancel) {//中途取消 mCancel = false; new File(mVideo.dstPath).delete();//取消,删除目标文件 JLog.d("px", "delete file " + mVideo.dstPath); } else {//顺利完成 new File(mVideo.srcPath).delete();//成功,删除源文件 JLog.d("px", "delete file " + mVideo.srcPath); } if (mListener != null) { mListener.onCodecFinish(mVideo); } if (!videos.isEmpty()) { VideoCodecModel video = videos.remove(0); start(video); } } class Frame { MediaCodec.BufferInfo videoInfo; byte[] data; Bitmap bitmap; public Frame(MediaCodec.BufferInfo videoInfo, Bitmap bitmap) { this.videoInfo = videoInfo; this.bitmap = bitmap; } } private long getInterval() { //用第一二帧获取帧间隔 long videoSampleTime; ByteBuffer buffer = ByteBuffer.allocate(1024 * 512); //获取源视频相邻帧之间的时间间隔。(1) extractor.readSampleData(buffer, 0); //skip first I frame if (extractor.getSampleFlags() == MediaExtractor.SAMPLE_FLAG_SYNC) extractor.advance(); extractor.readSampleData(buffer, 0); long firstVideoPTS = extractor.getSampleTime(); extractor.advance(); extractor.readSampleData(buffer, 0); long SecondVideoPTS = extractor.getSampleTime(); videoSampleTime = Math.abs(SecondVideoPTS - firstVideoPTS); JLog.d(tag, "videoSampleTime is " + videoSampleTime); return videoSampleTime; } @Override public int onStartCommand(Intent intent, int flags, int startId) { JLog.d(TAG, "onStartCommand"); super.onStartCommand(intent, flags, startId); if (intent == null) { return START_NOT_STICKY; } int action = intent.getIntExtra("action", 0); if (action == REQUEST_CODEC) { VideoCodecModel video = (VideoCodecModel) intent.getSerializableExtra("video"); video = codecDao.addItem(video); if (!encoding) { start(video); } else { videos.add(video); } } else if (action == REQUEST_CODEC_CANCEL) { VideoCodecModel video = (VideoCodecModel) intent.getSerializableExtra("video"); mDelete = intent.getBooleanExtra("delete", false);//是否删除旧文件 JLog.d("px", "----- onStartCommand action " + action + " is delete?" + mDelete); mBinder.cancel(video); } return START_NOT_STICKY; } @Nullable @Override public IBinder onBind(Intent intent) { JLog.d(TAG, "onBind"); return mBinder; } private CodecBinder mBinder = new CodecBinder(); private VideoCodecModel mVideo; //video下载的任务队列 private List<VideoCodecModel> videos = new ArrayList<>(); public static final int REQUEST_CODEC = 0x183; public static final int REQUEST_CODEC_CANCEL = 0x184; public class CodecBinder extends Binder { /** * @param video * @return 是否可以执行, 或等待执行 */ public boolean start(VideoCodecModel video) { video = codecDao.addItem(video); if (!encoding) { TestCodecService.this.start(video); } else { videos.add(video); } return !encoding; } public void setOnProgressChangeListener(OnProgressChangeListener l) { mListener = l; } public VideoCodecModel getCurrentVideo() { return mVideo; } public void cancel(VideoCodecModel video) { if (mVideo.equals(video)) {//正在处理 decodeOver = true;//控制解码线程结束 encoding = false;//控制编码线程结束 mCancel = true;//控制结束后删除文件等 } else {//视频没有正在处理 boolean flag = videos.remove(video); if (flag) { JLog.d("px", "cancel render task sucess"); } else { //并没有这个任务 JLog.d("px", "cancel render task fail,seems this video not in renderring queen"); } //删除源文件 if (mDelete) { mDelete = false; new File(video.srcPath).delete(); codecDao.deleteItem(video); } } } public List<VideoCodecModel> getVideoList() { return videos; } public void removeListener() { mListener = null; } } private void start(VideoCodecModel video) { if (video == null) { return; } if (!new File(video.srcPath).exists()) { Toast.makeText(this, "该视频缓存文件可能已经被删除", Toast.LENGTH_SHORT).show(); video.finish = -100; codecDao.createOrUpdate(video); return; } mVideo = video; if (mListener != null) { mListener.onCodecStart(mVideo); } mVideo.finish = 50;//改成处理中 codecDao.createOrUpdate(mVideo); Runnable runnable = new Runnable() { @Override public void run() { init(mVideo.srcPath, mVideo.dstPath); decodeRunnable = new DecodeRunnable(); decodeRunnable.start(); encodeRunnable = new EncodeRunnable(); encodeRunnable.start(); } }; AsyncTaskExecutor.getExecutor().execute(runnable); } private OnProgressChangeListener mListener; public interface OnProgressChangeListener { void onProgress(int progress, int max); void onCodecStart(VideoCodecModel video); void onCodecFinish(VideoCodecModel video); } } //这是模型类 import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; import java.io.Serializable; /** * Created by user on 2016/8/29. */ @DatabaseTable(tableName = "video_codec_task") public class VideoCodecModel implements Serializable { private static final long serialVersionUID = -1307249622002520298L; @DatabaseField public String srcPath; @DatabaseField public String dstPath; @DatabaseField public long videoCreateTime; @DatabaseField(generatedId = true) public int id; //0为被限制的状态,50为渲染中,或渲染队列中,100为已完成,-100为已删除, @DatabaseField public int finish = 0; @DatabaseField public String serno; //操作是用到,不需要存数据库 public boolean select; public VideoCodecModel(String srcPath, String dstPath, long videoCreateTime) { this.srcPath = srcPath; this.videoCreateTime = videoCreateTime; this.dstPath = dstPath; } public VideoCodecModel() { } public String getSrcPath() { return srcPath; } public void setSrcPath(String srcPath) { this.srcPath = srcPath; } public String getDstPath() { return dstPath; } public void setDstPath(String dstPath) { this.dstPath = dstPath; } public long getVideoCreateTime() { return videoCreateTime; } public void setVideoCreateTime(long videoCreateTime) { this.videoCreateTime = videoCreateTime; } public boolean isSelect() { return select; } public void setSelect(boolean select) { this.select = select; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof VideoCodecModel)) return false; VideoCodecModel that = (VideoCodecModel) o; if (videoCreateTime != that.videoCreateTime) return false; if (!srcPath.equals(that.srcPath)) return false; return dstPath.equals(that.dstPath); } } //用来查看水印任务完成状态,和监控Service运行的界面Activity,,Activity的打开与否,不影响服务的运行 import android.annotation.TargetApi; import android.app.ProgressDialog; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.ServiceConnection; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.Message; import android.support.annotation.Nullable; import android.util.Log; import android.view.Gravity; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.PopupMenu; import android.widget.ProgressBar; import android.widget.TextView; import ... import java.io.File; import java.lang.ref.WeakReference; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; /** * Created by user on 2016/8/29. */ public class ShowCodecActivity extends BaseActivity implements TestCodecService.OnProgressChangeListener, View.OnClickListener { private TextView noneTipsView; private List<VideoCodecModel> videos = new ArrayList<>(), cordingVideos; private ListView listView; private BaseAdapter adapter; private View firstTips; @Nullable VideoCodecModel curShowVideo, curRenderVideo; TestCodecService.CodecBinder binder; private ProgressBar progressBar; ServiceConnection connection; VideoCodecDao codecDao; private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); private boolean mEditMode = false; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_show_codec); setTitle("签约视频列表"); initView(); if (getIntent() != null) { curShowVideo = (VideoCodecModel) getIntent().getSerializableExtra("video"); } codecDao = VideoCodecDao.getInstance(this); final Intent intent = new Intent(this, TestCodecService.class); connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { Log.d("px", "onServiceConnected"); binder = (TestCodecService.CodecBinder) service; binder.setOnProgressChangeListener(ShowCodecActivity.this); videos.clear(); curRenderVideo = binder.getCurrentVideo(); cordingVideos = binder.getVideoList(); videos.addAll(codecDao.queryAll()); notifyChange(); } @Override public void onServiceDisconnected(ComponentName name) { } }; bindService(intent, connection, Context.BIND_AUTO_CREATE); } private void notifyChange() { if (adapter == null) { adapter = new BaseAdapter() { @Override public int getCount() { return videos.size(); } @Override public VideoCodecModel getItem(int position) { return videos.get(position); } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { final Holder holder; if (convertView == null) { convertView = View.inflate(ShowCodecActivity.this, R.layout.item_show_codec, null); holder = new Holder(); holder.bar = (ProgressBar) convertView.findViewById(R.id.pb_codec); holder.status = (TextView) convertView.findViewById(R.id.status); holder.serno = (TextView) convertView.findViewById(R.id.serno); holder.select = convertView.findViewById(R.id.select); holder.time = (TextView) convertView.findViewById(R.id.time); holder.operate = (TextView) convertView.findViewById(R.id.operate); holder.checkBox = (CheckBox) convertView.findViewById(R.id.cb_select); convertView.setTag(holder); } else { holder = (Holder) convertView.getTag(); } final VideoCodecModel video = getItem(position); if (video.finish == 100) { holder.status.setText("已完成"); holder.operate.setVisibility(View.VISIBLE); holder.operate.setText("操作"); } else if (video.finish == -100) { holder.status.setText("已删除"); holder.operate.setVisibility(View.INVISIBLE); } else if (video.equals(curRenderVideo)) { progressBar = holder.bar; holder.status.setText("处理中"); holder.operate.setVisibility(View.INVISIBLE); } else if (cordingVideos.contains(video)) { holder.status.setText("等待中"); holder.operate.setVisibility(View.VISIBLE); holder.operate.setText("取消"); } else { holder.status.setText("未处理"); holder.operate.setVisibility(View.VISIBLE); holder.operate.setText("开始"); } holder.operate.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (video.finish == 100) { operate(holder.status, video); } else if (video.finish == -100) { return; } else if (video.equals(curRenderVideo)) {//已经在编码中,不可操作 return; } else if (cordingVideos.contains(video)) {//已经在编码队列中,可取消 binder.cancel(video); holder.status.setText("未处理"); holder.operate.setVisibility(View.VISIBLE); holder.operate.setText("开始"); } else { boolean immedia = binder.start(video); if (immedia) { holder.status.setText("处理中"); holder.operate.setVisibility(View.INVISIBLE); } else { holder.status.setText("等待中"); holder.operate.setVisibility(View.VISIBLE); holder.operate.setText("取消"); } } } }); holder.select.setVisibility(video.equals(curShowVideo) ? View.VISIBLE : View.GONE); holder.serno.setText(video.serno); holder.time.setText(dateFormat.format(new Date(video.videoCreateTime))); holder.checkBox.setVisibility(mEditMode ? View.VISIBLE : View.GONE); holder.checkBox.setChecked(video.isSelect()); holder.checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { video.setSelect(isChecked); } }); return convertView; } }; listView.setAdapter(adapter); } else { adapter.notifyDataSetChanged(); } noneTipsView.setVisibility(videos.isEmpty() ? View.VISIBLE : View.GONE); more.setVisibility(mEditMode ? View.VISIBLE : View.GONE); back.setVisibility(mEditMode ? View.INVISIBLE : View.VISIBLE); checkBox.setVisibility(mEditMode ? View.VISIBLE : View.GONE); } class Holder { ProgressBar bar; TextView status, serno, time, operate; View select; CheckBox checkBox; } private void initView() { listView = (ListView) findViewById(R.id.lv_codec); noneTipsView = (TextView) findViewById(R.id.tv_none); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { VideoCodecModel video = videos.get(position); operate(view, video); } }); listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { if (mEditMode) { return false; } mEditMode = true; //启动编辑模式不记住从前的选中状态 for (VideoCodecModel video : videos) { if (video.select) video.select = false; } checkBox.setChecked(false); notifyChange(); return true; } }); firstTips = findViewById(R.id.ll_tips); boolean visable = Preferences.getBoolean("firstShowCodec", true); firstTips.setVisibility(visable ? View.VISIBLE : View.GONE); if (visable) findViewById(R.id.btn_noshow).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Preferences.put("firstShowCodec", false); firstTips.setVisibility(View.GONE); } }); checkBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { for (VideoCodecModel model : videos) { model.setSelect(isChecked); } notifyChange(); } }); more.setText("操作"); more.setOnClickListener(this); } private void operate(View view, final VideoCodecModel video) { if (video.finish != 100) { return; } PopupMenu popupMenu = new PopupMenu(ShowCodecActivity.this, view); popupMenu.getMenu().add(1, 0, 0, "预览或发送"); popupMenu.getMenu().add(1, 1, 1, "删除"); popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { case 0: previewVideo(video.dstPath); break; case 1: File file = new File(video.dstPath); if (file.exists()) { file.delete(); } codecDao.deleteItem(video); videos.remove(video); if (cordingVideos.contains(video)) { binder.cancel(video); } notifyChange(); break; } return true; } }); popupMenu.show(); } @Override public void onProgress(int progress, int max) { if (progressBar != null) { progressBar.setMax(max); progressBar.setProgress(progress); } } @Override public void onCodecStart(VideoCodecModel video) { JLog.d("px", "onCodecStart"); curRenderVideo = video; int index = videos.indexOf(video); if (index >= 0) { View child = listView.getChildAt(index); Holder holder = (Holder) child.getTag(); holder.status.setText("处理中"); holder.operate.setVisibility(View.INVISIBLE); progressBar = holder.bar; } } @Override public void onCodecFinish(VideoCodecModel video) { JLog.d("px", "onCodecFinish"); if (progressBar != null) { progressBar.setProgress(0); } int index = videos.indexOf(video); videos.get(index).finish = 100; if (index >= 0) { View child = listView.getChildAt(index); Holder holder = (Holder) child.getTag(); holder.status.setText("已完成"); holder.operate.setVisibility(View.VISIBLE); holder.operate.setText("操作"); progressBar = null; } } @Override protected void onDestroy() { if (binder != null) binder.removeListener(); unbindService(connection); super.onDestroy(); } private void previewVideo(String filePath) { //预览录像 Intent intent = new Intent(Intent.ACTION_VIEW); String type = "video/mp4"; Uri uri = Uri.parse("file://" + filePath); intent.setDataAndType(uri, type); startActivity(intent); } @Override public void onBackPressed() { if (mEditMode) { mEditMode = false; notifyChange(); return; } super.onBackPressed(); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.more: PopupMenu menu = new PopupMenu(this, v); // menu.getMenu().add(1, 0, 0, "发送"); menu.getMenu().add(1, 1, 1, "删除"); menu.getMenu().add(1, 2, 2, "取消"); menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { case 0: break; case 1: deleteSelect(); break; case 2: mEditMode = false; notifyChange(); break; } return true; } }); menu.show(); break; } } //删除所选 private void deleteSelect() { final ProgressDialog dialog = ProgressDialog.show(this, null, null); AsyncTask<String, String, Boolean> task = new AsyncTask<String, String, Boolean>() { @Override protected Boolean doInBackground(String... params) { boolean has = false;//是否选到可以删除的,有可能并未有任何选择 for (VideoCodecModel video : videos) { if (video.select) { File file; if (video.finish == 100) { file = new File(video.dstPath); } else { file = new File(video.srcPath); } if (file.exists()) { file.delete(); } codecDao.deleteItem(video); if (!has) { has = true; } } } if (has) { videos.clear(); videos.addAll(codecDao.queryAll()); } return has; } @Override protected void onPostExecute(Boolean s) { mEditMode = false; notifyChange(); dialog.dismiss(); } }; task.executeOnExecutor(AsyncTaskExecutor.getExecutor()); } }以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。