参考:
视音频编解码技术零基础学习方法
Android 集成 FFmpeg (一) 基础知识及简单调用
从零开始仿写一个抖音App——开始
【Android 进阶】仿抖音系列之翻页上下滑切换视频(一)
自定义视频选择器:
Android简单实现本地图片和视频选择器功能

视频播放库:
JiaoZiVideoPlayer -- 视频播放器,自定义更好
GSYVideoPlayer -- 视频播放器,功能完善,更强大(本项目所用)
ijkplayer -- Android/iOS video player based on FFmpeg n3.4, with MediaCodec, VideoToolbox support.

压缩库相关:
VideoProcessor -- 视频压缩,体积小,速度快
VideoCompressor -- 比VideoProcessor还快,但是没有进度回调
FFmpeg -- 视频压缩 体积大,压缩时间长,功能完善强大
FFmpegAndroid -- android端基于FFmpeg
FFMPEG-AAC-264-Android-32-64 -- 编译好的ffmpeg压缩aar
FFmpegDemo --lastYear使用FFmpeg压缩的Demo
SiliCompressor -- 保证质量,但只能压缩,不能控制码率和进度
android视频压缩七牛sdk -- 要收费,废弃
small-video-record -- 采用FFmpeg,3.1k 的star

  • 1.获取本地视频:

Android 从系统媒体库中选择视频
权限获取后选择视频

AndPermission.with(this)                    .runtime()                    .permission(Permission.Group.STORAGE)                    .onGranted(permissions -> {                        Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Video.Media.EXTERNAL_CONTENT_URI);                        startActivityForResult(intent, SELECT_VIDEO_REQUEST_CODE);                    })                    .onDenied(permissions -> {                        ToastUtil.showLong("你取消了,需要同意权限方可读取视频文件!");                    })                    .start();

拿到视频路径后传递给需要用到的页面

    @Override    public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {        super.onActivityResult(requestCode, resultCode, data);        if (requestCode == SELECT_VIDEO_REQUEST_CODE&& resultCode == RESULT_OK && null != data) {            Uri selectedVideo = data.getData();            String[] filePathColumn = { MediaStore.Video.Media.DATA };            Cursor cursor = _mActivity.getContentResolver().query(selectedVideo , filePathColumn, null, null, null);            cursor.moveToFirst();            int columnIndex = cursor.getColumnIndex(filePathColumn[0]);            String videoPath = cursor.getString(columnIndex);            cursor.close();            start(UpVideoFragment.newInstance(videoPath));        }    }
  • 2.显示视频第一帧:

获取视频的第一帧,网络视频,直接用Glide加载就好,本地视频:
android 获取视频第一帧作为缩略图
获取第一帧视频异常
Uri的获取需要使用FileProvider的方式

  Uri videoUri = FileProvider.getUriForFile(_mActivity, AppUtils.getAppPackageName() + ".fileprovider", new File(videoPath));

然后把此uri进行获取第一帧

    private  Bitmap getVideoThumb(Context context, Uri uri) {        MediaMetadataRetriever media = new MediaMetadataRetriever();        media.setDataSource(context,uri);        return  media.getFrameAtTime();    }

或者:

 Bitmap videoThumbnail = ThumbnailUtils.createVideoThumbnail(videoPath, MediaStore.Video.Thumbnails.MICRO_KIND);
  • 3.获取视频大小:

就是获取文件的大小

    private static long getFileSize(File file) throws Exception {        long size = 0;        if (file.exists()) {            FileInputStream fis = new FileInputStream(file);            size = fis.available();        } else {            ToastUtil.showShort("文件不存在!");        }        return size;    }
  • 4.获取视频时长:

Android获取视频音频的时长的方法

    //获取视频时长,这里获取的是毫秒    private int getVideoTime(Context context, Uri uri){        try {            MediaPlayer mediaPlayer = new MediaPlayer();            mediaPlayer.setDataSource(context,uri);            mediaPlayer.prepare();            int duration = mediaPlayer.getDuration();            return duration;        } catch (IOException e) {            e.printStackTrace();        }        return 0;    }
  • 4.1获取视频的宽高和比特率,本地路径视频
MediaMetadataRetriever retriever = new MediaMetadataRetriever();retriever.setDataSource(videoPath);int originWidth = Integer.parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));int originHeight = Integer.parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT));int bitrate = Integer.parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE));
  • 4.2MediaInfo获取视频信息(帧率,时长,大小等)

https://www.jianshu.com/p/069bcef954f4

  • 5.自定义视频选择器

参考:
Android简单实现本地图片和视频选择器功能
Android 多媒体:MediaProvider、MediaStore
ContentResolver query 参数详解
Android利用ContentResolver查询的三种方式
Android_优化查询加载大数量的本地相册图片

主要是通过ContentResolver.query来查询本地视频:

    //获取本地视频数据,查询出本地mp4,以时间倒序排列    private List getLocalVideo(int limit) {        List videos = new ArrayList<>();        String[] projection = new String[]{                MediaStore.Video.Media.DATA,                MediaStore.Video.Media.DURATION,                MediaStore.Video.Media._ID,                MediaStore.Video.Media.DISPLAY_NAME,                MediaStore.Video.Media.SIZE,                MediaStore.Video.Media.DATE_MODIFIED};        ContentResolver resolver = _mActivity.getContentResolver();        Cursor cursor = resolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, projection,                MediaStore.Video.Media.MIME_TYPE + "=?", new String[]{"video/mp4"}, MediaStore.Video.Media.DATE_MODIFIED+" DESC limit "+limit);       while (cursor.moveToNext()){           String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA));           long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID));           long duration = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION));           String name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME));           long size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE));           long date = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_MODIFIED));           LocalVideo localVideo = new LocalVideo.Builder()                   .path(path)                   .id(id)                   .duration(duration)                   .name(name)                   .size(size)                   .date(date).build();           videos.add(localVideo);       }        for (LocalVideo video : videos) {           L.e(video.toString());        }        return videos;    }

优化一:异步的方式查询:

        //异步查询,加载第一页        QueryHandler mQueryHandler = new QueryHandler(_mActivity.getContentResolver());        mQueryHandler.startQuery(0,null,MediaStore.Video.Media.EXTERNAL_CONTENT_URI, null,                MediaStore.Video.Media.MIME_TYPE + "=?", new String[]{"video/mp4"},                 MediaStore.Video.Media.DATE_MODIFIED+" DESC limit "+limit);

优化二:进一步优化,采用分页查询

    private void queryLocalVideo() {        mQueryHandler.startQuery(0,null, MediaStore.Video.Media.EXTERNAL_CONTENT_URI,                null,                MediaStore.Video.Media.MIME_TYPE + "=?", new String[]{"video/mp4"},                MediaStore.Video.Media.DATE_MODIFIED+" DESC limit "+limit+" offset "+(page-1)*limit);    }

优化三:查询条件过滤,只查询15秒以内的视频文件

    private void queryLocalVideo() {        mQueryHandler.startQuery(0,null, MediaStore.Video.Media.EXTERNAL_CONTENT_URI,                null,               MediaStore.Video.Media.MIME_TYPE + "=? and " + MediaStore.Video.Media.DURATION+" < ?", new String[]{"video/mp4","16000"},                MediaStore.Video.Media.DATE_MODIFIED+" DESC limit "+limit+" offset "+(page-1)*limit);    }

最后查询结果的回调处理:

    //写一个异步查询类    private final class QueryHandler extends AsyncQueryHandler {        public QueryHandler(ContentResolver cr) {            super(cr);        }        @Override        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {            super.onQueryComplete(token, cookie, cursor);            if (cursor==null)return;            List videos = new ArrayList<>();            while (cursor.moveToNext()){                String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA));                long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID));                long duration = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION));                String name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME));                long size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE));                long date = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_MODIFIED));                LocalVideo localVideo = new LocalVideo.Builder()                        .path(path)                        .id(id)                        .duration(duration)                        .name(name)                        .size(size)                        .date(date).build();                videos.add(localVideo);            }            mAdapter.addData(videos);            mAdapter.loadMoreComplete();        }    }
  • 6.视频的压缩

我采用的VideoProcessor压缩工具:
VideoProcessor -- 视频压缩,体积小,速度快
FFmpeg -- 视频压缩 体积大,功能完善强大

参考:
Android本地视频压缩方案 --使用的ffmpeg-android-java
码率(Bitrate)、帧率(FPS)、分辨率和清晰度的联系与区别

坑:
a.用VideoProcessor压缩时输出路径对应的文件夹不存在的话,不报错也没有任何反应。所以要确定videoOutCompressPath这个路径上的文件夹确实存在。
b.如果不配置宽高和码率(Bitrate)的话,有的小文件越压缩越大
c.要开启一个子线程来压缩这个视频

    //压缩视频    private void compressVideo(String videoPath){        mBinding.progressBar.setVisibility(View.VISIBLE);        new Thread(new Runnable() {            @Override            public void run() {                try {                    MediaMetadataRetriever retriever = new MediaMetadataRetriever();                    retriever.setDataSource(videoPath);                    int originWidth = Integer.parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));                    int originHeight = Integer.parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT));                    int bitrate = Integer.parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE));                    L.e("originWidth="+originWidth+" originHeight=="+originHeight+" bitrate=="+bitrate);                    String videoOutCompressPath = getVideoOutCompressPath(videoPath);                    VideoProcessor.processor(_mActivity)                            .input(videoPath)                            .bitrate(bitrate / 2)                            .output(videoOutCompressPath)                            .progressListener(new VideoProgressListener() {                                @Override                                public void onProgress(float progress) {                                    int intProgress = (int) (progress * 100);                                    Message message = mHandler.obtainMessage();                                    message.what=0;                                    message.arg1 = intProgress;                                    mHandler.sendMessage(message);                                    if (intProgress==100){                                        message.what=1;                                        message.obj = videoOutCompressPath;                                        mHandler.sendMessage(message);                                    }                                }                            })                            .process();                } catch (Exception e) {                    e.printStackTrace();                }            }        }).start();    }    private Handler mHandler = new Handler(new Handler.Callback() {        @Override        public boolean handleMessage(Message msg) {            switch (msg.what) {                case 0:                    mBinding.progressBar.setProgress(msg.arg1);                    break;                case 1:                    mBinding.progressBar.setVisibility(View.INVISIBLE);                    ToastUtil.showLong("压缩完成!");                    String videoOutCompressPath  = (String) msg.obj;                    L.e("压缩后大小=="+ FormatUtils.formatSize(VideoUtils.getFileSize(new File(videoOutCompressPath))));                    break;            }            return false;        }    });

测试:
录制5分钟4k高清视频:

fileSize==1.58 GBvideoTime==300秒originWidth=3840 originHeight==2160 bitrate==42201919压缩后大小==796 MB
  • 7.视频的录制

Android自定义视频录制
Android 使用系统相机录制视频查看视频

首先要申请权限

  //开始录像    private void startVideoTape() {        AndPermission.with(this)                .runtime()                .permission(Permission.Group.CAMERA)                .onGranted(permissions ->  startSystemRecord())                .onDenied(permissions -> ToastUtil.showLong(getString(R.string.need_record_permission)))                .start();    }

然后开始录制,并限制时长5分钟

   //调用系统的录制视频    private void startSystemRecord(){        Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);        //限制时长s        intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, 5*60);        //限制大小        intent.putExtra(MediaStore.EXTRA_SIZE_LIMIT, 30*1024*1024);        //设置质量        intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1);        //设置输出位置        intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.parse(SdUtils.getCameraPath() +"antvideo"+File.separator+System.currentTimeMillis()+".mp4"));        startActivityForResult(intent, 1);    }
  • 8.视频上传

使用Retrofit框架进行上传,那么请求体就是关键,一般使用POST请求方式
首先定义一个接口

    //视频发布接口    @POST(VIDEO_POST_VIDEO)    Observable> postVideo(@Body RequestBody request);

然后对其进行实现

@Override    public Observable> postVideo(RequestBody request) {        return bindIoUI(videoApi.postVideo(request));    }

最后上传的请求体是需要自己组装的,包含了上传视频的相关参数,视频的缩量图,和视频本身

    /**     *     发布视频     *     private String title;//视频标题     *     private String cat_id;//视频分类     *     private String track_id;//视频所属赛道id     *     private File image_url;//视频缩略图     *     private File path;//视频地址     *     private int width;//视频宽度     *     private int height;//视频高度     *     private int duration;//视频时长     */    private void upVideo() {        dialogProgress.show();        //其他参数键值对的组装        String title = mBinding.etTitle.getText().toString();        Map map = new HashMap<>();        map.put("title",title);        map.put("cat_id",cat_id+"");        if (track_id != -1)map.put("track_id",track_id+"");        map.put("width",videoWidth+"");        map.put("height",videoHeight+"");        map.put("duration",videoDuration+"");        //视频各个参数        MultipartBody.Builder builder = new MultipartBody.Builder();        builder.setType(MultipartBody.FORM);        for (String key:map.keySet()) builder.addFormDataPart(key,map.get(key));        //图片流和视频流        builder.addFormDataPart("image_url",getFileName(videoImgPath), RequestBody.create(MediaType.parse("application/octet-stream"),new File(videoImgPath)));        builder.addFormDataPart("path",getFileName(videoUploadPath), RequestBody.create(MediaType.parse("application/octet-stream"),new File(videoUploadPath)));        //用FileRequestBody进行包装,以监听上传进度        FileRequestBody body = new FileRequestBody(builder.build(), (currentLength, contentLength) -> {            int progress = FormatUtils.getProgress(currentLength, contentLength);            Message message = mHandler.obtainMessage();            message.what = 2;            message.arg1 = progress;            mHandler.sendMessage(message);        });        dataProvider.video.postVideo(body).subscribe(new OnSuccessAndFailListener>() {            @Override            protected void onSuccess(BaseResponse baseResponse) {                BaseErrResponse data = baseResponse.getData();                ToastUtil.showLong(data.getMessage());                pop();            }        });    }

其中FileRequestBody是对RequestBody的一层封装,主要是为了监听上传的进度进行回调

/** * MyApplication --  com.smallcake.okhttp * Created by Small Cake on  2017/9/8 17:52. */public class FileRequestBody extends RequestBody {    private RequestBody mRequestBody;    private LoadingListener mLoadingListener;    private long mContentLength;    public FileRequestBody(RequestBody requestBody, LoadingListener loadingListener) {        mRequestBody = requestBody;        mLoadingListener = loadingListener;    }    //total length    @Override    public long contentLength() {        try {            if (mContentLength == 0)                mContentLength = mRequestBody.contentLength();            return mContentLength;        } catch (IOException e) {            e.printStackTrace();        }        return -1;    }    @Override    public MediaType contentType() {        return mRequestBody.contentType();    }    @Override    public void writeTo(BufferedSink sink) throws IOException {        ByteSink byteSink = new ByteSink(sink);        BufferedSink mBufferedSink = Okio.buffer(byteSink);        mRequestBody.writeTo(mBufferedSink);        mBufferedSink.flush();    }    private final class ByteSink extends ForwardingSink {        private long mByteLength = 0L;        ByteSink(Sink delegate) {            super(delegate);        }        @Override        public void write(Buffer source, long byteCount) throws IOException {            super.write(source, byteCount);            mByteLength += byteCount;            mLoadingListener.onProgress(mByteLength, contentLength());        }    }    public interface LoadingListener {        void onProgress(long currentLength, long contentLength);    }}
  • 9.点赞打Call特效

参考:
第三方控件:
SVGAPlayer-Android
SVGAPlayer 是一个轻量的动画渲染库

  • 10.自定义渲染层,然后实现自己的 MeasureHepler,来达到实现单个播放器,单独设置的目的。

https://github.com/CarGuo/GSYVideoPlayer/blob/master/app/src/main/java/com/example/gsyvideoplayer/view/CustomRenderView.java 然后实现自己的 MeasureHepler

  • 11.视频优化项:

a.视频播放前会闪烁一下:
参考:https://github.com/CarGuo/GSYVideoPlayer/issues/2046

  • 12.视频格式:

m3u8 文件格式详解

  • 13.异常:使用GSYVideoPlayer个别视频被拉伸显示

现象:在播放的时候发现个别视频明明设置的全屏裁剪GSYVideoType.setShowType(GSYVideoType.SCREEN_TYPE_FULL);但是视频却被拉伸了。
解决:原来需要设置视频播放器StandardGSYVideoPlayer的布局控件,也就是video_layout_standard.xml中的布局文件中的id为 android:id="@+id/surface_container"RelativeLayout改为FrameLayout,不知道为什么 GSYVideoPlayer为什么不直接就写成FrameLayout

  • 14.异常:当弹出Toast时候,视频进入changeUiToNormal状态,导致视频变相暂停。

原因:是因为做了更新notifyItemChanged的操作,而不是Toast引起的,也不是播放器因为屏幕焦点被获取而导致暂停。

14.原生播放器播放:
https://blog.csdn.net/lvxiaobo1994/article/details/81060887

更多相关文章

  1. ffmpag总结_android_to_ios视频转换
  2. 传智播客Android视频教程——第八天
  3. Android自适应不同分辨率或不同屏幕大小
  4. android拍照上传
  5. android 视频播放 Google exoplayer

随机推荐

  1. Android WebView JavaScript交互
  2. Android四种布局
  3. android系统权限大全
  4. Android基础之CursorAdapter 的用法与获
  5. Android MediaProvider详解(基础篇)
  6. android QQ好友分享
  7. Android FTP客户端使用,快速上传文件
  8. Cocos2dx setup Eclipse environment for
  9. Android 之shape
  10. Intent在Android中的几种用法