赞
踩
需求很简单,在安卓手机上进行视频裁剪,只要裁短,不要求拼接,也不要求裁剪画面。编码形式直接复制原本的,分辨率码率帧率都直接照搬原本的。
尽量不要重复造轮子,有现成的直接找现成的。这里找了一个ffmpeg实现的轮子来直接用,唯一问题是项目是5年前的,要做些适配。
VideoCrop
5年前的轮子。。。试试在5年前的系统上跑一下。Android 9能正常运行,到了Android 10就报找不到文件了。在各个版本跑了一遍以后,发现以下问题等待解决:
我们需要读取外部存储里的媒体文件,其实读相册和下载是不需要权限的,但是保不齐用户的媒体文件会存在什么奇奇怪怪的地方,比如说微信和QQ就存在他们自己的公共目录里。所以这个读取权限还是有必要的。
在Manifest声明需要的权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
动态申请权限,写在第一个Activity里就好
private val REQUEST_CODE_PERMISSIONS = 10//数字随意 private val REQUIRED_PERMISSIONS = arrayOf( Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)//要申请更多的权限都在这里拼接 //查询权限是否获取成功 private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED } //获取权限弹窗操作完成回调 override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == REQUEST_CODE_PERMISSIONS) { if (allPermissionsGranted()) { selectFile()//去选择文件 } else { Toast.makeText(this, "需要授予权限才能使用该功能", Toast.LENGTH_SHORT).show()//给用户提示 } } }
直接用intent唤起系统相册即可
private fun selectFile() { val i = Intent(Intent.ACTION_PICK, MediaStore.Video.Media.EXTERNAL_CONTENT_URI) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { someActivityResultLauncher.launch(i)//Android 10以上的写法,targetAPI在31以上才这么写 } else { startActivityForResult(i, REQUEST_SELECT_FILE)//Android 10以下的写法 } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) when (requestCode) { REQUEST_SELECT_FILE -> { if (resultCode == Activity.RESULT_OK && data != null && data.data != null) { toCrop(data.data!!)//选完文件以后跳转到裁剪界面 } } } } var someActivityResultLauncher = registerForActivityResult( StartActivityForResult(), ActivityResultCallback { result -> if (result.resultCode == Activity.RESULT_OK && result.data != null && result.data!!.data != null) { toCrop(result.data!!.data!!)//选完文件以后跳转到裁剪界面 } }) /** * 跳转裁剪页面 */ private fun toCrop(data: Uri) { val filePathColumn = arrayOf(MediaStore.Video.Media.DATA) try { val cursor: Cursor? = contentResolver.query(data, filePathColumn, null, null, null) if (cursor != null) { cursor.moveToFirst() val columnIndex: Int = cursor.getColumnIndex(filePathColumn[0]) val videoPath: String = cursor.getString(columnIndex) cursor.close() val intent = Intent(this, VideoCropPreActivity::class.java) intent.putExtra("src", videoPath)//在裁剪界面接收这个参数即可 startActivity(intent) } } catch (e: Exception) { Log.e(TAG, "读取文件出错", e) Toast.makeText(this, resources.getString(R.string.readVideoFileError), Toast.LENGTH_LONG).show() } }
注意,如果使用模拟器测试会触发一个闪退bug
模拟器镜像的谷歌相册是老版本,会抛出异常java.lang.IllegalArgumentException: Invalid column latitude
这个是谷歌相册老版本的bug,更新相册或者使用别的相册APP选择图片即可解决。谷歌的锅我们不帮他修,不管这个问题。实机不会出现这个问题,我用Pixel 3 XL在Android 12实测过不会报错。
从Android 10开始,不允许直接运行data/data/packagename/
目录下的二进制文件。旧的写法正是把ffmpeg放到这里运行的,所以到了Android 10需要改变写法。
如果你直接用老的写法的话,会报文件不存在或者权限被拒绝,因为安卓10以上根本不会把你Assert里的二进制文件放到这个目录里。那么解决的思路就很简单了,我们把ffmpeg伪装成别的文件,骗系统把我们放进去不就可以了吗?
说干就干,伪装成so库文件。先把ffmpeg
改名成ffmpeg.so
,然后放进jniLibs
里面去,你想支持哪些架构就放到哪些架构的目录里,反正这玩意不挑架构。
修改manifest
,在application
标签里加入这个参数
android:extractNativeLibs="true"
在原本的代码中搜索data/data
,找到这一段
"/data/data/" + context.getPackageName() + "/ffmpeg"
把它改成
context.getApplicationInfo().nativeLibraryDir + "/ffmpeg.so"
有两处,都改掉以后就大功告成了~
原本的轮子是有裁剪画面的功能的,但是我没有这个需求,所以要在最后的ffmpeg指令中把裁剪的指令干掉。因为不需要重新编码,直接复制视频流和音频流最快,所以加个-c copy
测试时发现一个小bug,如果文件名中有空格,会当成指令的分隔符,所以我们添加引号把文件名包起来,解决这个问题。
//-ss 开始时间(秒) -t 结束时间(秒) 时间裁切
//-strict -2 -vf crop=500:500:0:100 尺寸裁切
String cmd = context.getApplicationInfo().nativeLibraryDir + "/ffmpeg.so" + " -y -i "//-y是覆盖同名文件
+ """ + srcVideoPath + """//加引号避免名字有空格无法识别
+ " -ss " + start + " -t " + duration
// + " -strict -2 -vf crop=" + width + ":" + height + ":" + x + ":" + y + " -preset fast "//裁剪尺寸
+ " -c copy "//直接复制流,跳过编码节约时间
+ """ + destPath + """;//加引号避免名字有空格无法识别
task.execute(cmd);
原本的轮子渲染底部时间轴的图片速度很慢,通过修改MediaMetadataRetriever
的参数可以大幅提升渲染速度,找到这一行
Bitmap bitmap = mmr.getFrameAtTime(i, MediaMetadataRetriever.OPTION_CLOSEST);
把它改成这样即可
Bitmap bitmap = mmr.getFrameAtTime(i, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
尽管加了引号包裹文件名,但在部分机型上还是存在带空格的文件路径和文件名找不到的情况,建议在选文件的阶段直接给用户提示。
把它改成这样即可
Bitmap bitmap = mmr.getFrameAtTime(i, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
尽管加了引号包裹文件名,但在部分机型上还是存在带空格的文件路径和文件名找不到的情况,建议在选文件的阶段直接给用户提示。
文末放一个小福利给大家,扫描下方二维码:
群内有许多技术大牛,有任何问题,欢迎广大网友一起来交流,群内还不定期免费分享高阶Android学习视频资料和面试资料包~
偷偷说一句:群里高手如云,欢迎大家加群和大佬们一起交流讨论啊!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。