Android视频播放,选择,压缩,上传

参考:
视音频编解码技术零基础学习方法
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_优化查询加载大数量的本地相册图片

访问MediaStore需要Permission.READ_EXTERNAL_STORAGE权限,主要是通过ContentResolver.query来查询本地视频:

    //获取本地视频数据,查询出本地mp4,以时间倒序排列
    private List<LocalVideo> getLocalVideo(int limit) {
        List<LocalVideo> 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));
           Uri uri = Uri.withAppendedPath(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, 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()
                    .uri(uri)
                   .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<LocalVideo> videos = new ArrayList<>();
            while (cursor.moveToNext()){
                LocalVideo localVideo = getLocalVideo(cursor);
                videos.add(localVideo);
            }
            cursor.close();
            if (videos.size()>0){
                mAdapter.addData(videos);
                mAdapter.loadMoreComplete();
            }else mAdapter.loadMoreEnd();

        }
    }
  • 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 GB
videoTime==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<BaseResponse<BaseErrResponse>> postVideo(@Body RequestBody request);

然后对其进行实现

@Override
    public Observable<BaseResponse<BaseErrResponse>> 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<String,String> 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<BaseResponse<BaseErrResponse>>() {
            @Override
            protected void onSuccess(BaseResponse<BaseErrResponse> 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 文件格式详解

现象:在播放的时候发现个别视频明明设置的全屏裁剪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引起的,也不是播放器因为屏幕焦点被获取而导致暂停。

  • 15.原生播放器播放:

Android 原生视频播放VideoView的使用
Android VideoView 视频播放完成例子(进度条,播放时间,暂停,拖动)
VideoView及其相关组件总结
android VideoView屏幕旋转为竖屏固定高度,旋转为横屏全屏播放实现
android代码设置RelativeLayout子控件位置(addRule)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 原文链接http://www.cnblogs.com/kenshincui/p/4186022.html 音频在i...
    Hyman0819阅读 21,660评论 4 74
  • 一、简历准备 1、个人技能 (1)自定义控件、UI设计、常用动画特效 自定义控件 ①为什么要自定义控件? Andr...
    lucas777阅读 5,186评论 2 54
  • 这两天看了面包树上的女人,看完之后只感悲凉,甚至让我联想到正在追我的男生,如果我答应了,是否也会有程韵和林方文的...
    陈陈陈大疯阅读 1,030评论 1 2
  • 又要过年了,新年对于我已经失去了意义,怎么也找不到快乐的感觉,每天对着手机诉说心里话。 我注定孤独,没有人懂我心中...
    刘芳同学阅读 224评论 0 1
  • 新精英咨询训练营成长报告——迦缤 初识新精英: 2018年国庆节,放弃与家人团聚的时间,放弃21周年结婚纪念日的温...
    迦缤419阅读 248评论 0 0