欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

安卓视频播放、选择、压缩和上传

最编程 2024-03-08 13:59:45
...

参考:
视音频编解码技术零基础学习方法
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 文件格式详解

  • 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引起的,也不是播放器因为屏幕焦点被获取而导致暂停。

  • 15.原生播放器播放:

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

推荐阅读