1.自定义View解放外部调用
import android.content.Context
import android.content.res.Configuration
import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.util.AttributeSet
import android.util.Log
import android.view.View
import androidx.lifecycle.*
import cc.ab.base.ext.*
import cc.ab.base.widget.roundlayout.widget.GeneralRoundFrameLayout
import cc.abase.demo.R
import cc.abase.demo.utils.VideoUtils
import chuangyuan.ycj.videolibrary.video.ExoUserPlayer
import chuangyuan.ycj.videolibrary.video.VideoPlayerManager
import chuangyuan.ycj.videolibrary.widget.VideoPlayerView
import me.panpf.sketch.SketchImageView
import java.io.File
/**
* Description:
* @author: caiyoufei
* @date: 2019/11/2 11:52
*/
class SimpleVideoView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : FrameLayout(context, attrs, defStyleAttr, defStyleRes), LifecycleObserver {
//真实播放控件
var player: VideoPlayerView = context.inflate(R.layout.layout_video) as VideoPlayerView
//播放管理器
var manager: ExoUserPlayer? = null
init {
removeAllViews()
addView(player, LayoutParams(-1, -1))
}
//是否要重新播放
private var needResumePlay = false
private var mLifecycle: Lifecycle? = null
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun onResumeVideo() {
if (needResumePlay) {
needResumePlay = false
manager?.onResume()
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun onPauseVideo() {
if (manager?.isPlaying == true) {
manager?.onPause()
needResumePlay = true
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroyVideo() {
manager?.onDestroy()
}
override fun onConfigurationChanged(newConfig: Configuration?) {
manager?.onConfigurationChanged(newConfig)
super.onConfigurationChanged(newConfig)
}
private fun setPlayDataSource(
url: String,
loop: Boolean
) {
manager?.onDestroy()
val builder = VideoPlayerManager.Builder(VideoPlayerManager.TYPE_PLAY_USER, player)
.setPlayUri(url)
if (loop) builder.setLoopingMediaSource(Int.MAX_VALUE, Uri.parse(url))
manager = builder.create()
player.previewImage.click { manager?.startPlayer() }
}
//判断文件是否是图片
private fun isImageFile(filePath: String): Boolean {
if (!File(filePath).exists()) return false
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(filePath, options)
return options.outWidth != -1
}
//判断文件是否是视频
private fun isVideoFile(filePath: String): Boolean {
if (!File(filePath).exists()) return false
return try {
val mmr = MediaMetadataRetriever()
mmr.setDataSource(filePath)
val mimeType = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
mimeType?.contains("video", true) == true
} catch (e: Exception) {
e.printStackTrace()
false
}
}
//================================外部设置================================//
//设置封面
fun setCover(cover: String?): SimpleVideoView {
val iv = player.previewImage
if (cover.isNullOrBlank()) {//传空清除封面
iv.setImageDrawable(null)
return this
}
if (iv !is SketchImageView) {
Log.e("CASE", "封面控件不是SketchImageView")
return this
}
when {
cover.startsWith("http", true) -> iv.load(cover)//加载网络封面
File(cover).exists() -> {
when {
isImageFile(cover) -> iv.load(cover)
//直接加载本地图片
isVideoFile(cover) -> VideoUtils.instance.getFirstFrame(File(cover)) { suc, info ->
if (suc) {
iv.load(info)
} else {
Log.e("CASE", "视频封面获取失败")
}
}
else -> Log.e("CASE", "非图片,非视频,无法加载封面")
}
}
else -> Log.e("CASE", "封面文件不存在:$cover")
}
return this
}
//设置播放地址
fun setPlayUrl(videoUrl: String): SimpleVideoView {
setPlayDataSource(videoUrl, false)
return this
}
//设置是否循环播放
fun setPlayUrlWithLoop(videoUrl: String): SimpleVideoView {
setPlayDataSource(videoUrl, true)
return this
}
//设置是否显示返回按钮
fun setBackShow(show: Boolean): SimpleVideoView {
player.findViewById<View>(chuangyuan.ycj.videolibrary.R.id.exo_controls_back)
.gone()
player.isShowBack = show
return this
}
//设置播放生命周期
fun setLifecycleOwner(owner: LifecycleOwner): SimpleVideoView {
mLifecycle?.removeObserver(this)
mLifecycle = owner.lifecycle
mLifecycle?.addObserver(this)
return this
}
//是否可以调用外部返回
fun getCanBackPressed(): Boolean {
return manager == null || manager?.onBackPressed() == true
}
}
import android.Manifest
import android.media.MediaMetadataRetriever
import android.util.Log
import cc.abase.demo.R
import com.blankj.utilcode.util.*
import io.microshow.rxffmpeg.RxFFmpegInvoke
import io.microshow.rxffmpeg.RxFFmpegSubscriber
import java.io.File
import kotlin.math.min
/**
* Description:
* @author: caiyoufei
* @date: 2019/10/28 11:24
*/
class VideoUtils private constructor() {
//输出文件目录
private val outParentVideo = PathUtils.getExternalAppDataPath() + File.separator + "video"
//产生的封面保存地址
private val outParentImgs = PathUtils.getExternalAppDataPath() + File.separator + "temp"
private object SingletonHolder {
val holder = VideoUtils()
}
//创建文件夹
init {
if (PermissionUtils.isGranted(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
) {
if (!File(outParentVideo).exists())
Log.e("CASE", "创建Video文件夹:${File(outParentVideo).mkdirs()}")
if (!File(outParentImgs).exists())
Log.e("CASE", "创建Temp文件夹:${File(outParentImgs).mkdirs()}")
}
}
companion object {
val instance = SingletonHolder.holder
}
//视频压缩进度
private var compressPro = -1
//开始压缩
fun startCompressed(
originFile: File,
result: ((suc: Boolean, info: String) -> Unit)? = null,
pro: ((progress: Int) -> Unit)? = null
) {
val outFile = File(outParentVideo, EncryptUtils.encryptMD5File2String(originFile) + ".mp4")
if (outFile.exists() && outFile.length() > 1024 * 1024) {//大于1M
//视频已压缩过,不再压缩,直接返回
result?.invoke(true, outFile.path)
return
}
Log.e("CASE", "视频压缩前大小:${FileUtils.getFileSize(originFile)}")
val command = getCommandCompress(originFile.path, outFile.path)
//码率太低不进行压缩,直接拷贝原文件并返回
if (command.isNullOrBlank()) {
FileUtils.copyFile(originFile, outFile)
Log.e("CASE", "视频码率太小,不用压缩,直接拷贝上传")
result?.invoke(true, outFile.path)
return
}
Log.e("CASE", "执行的压缩命令:$command")
RxFFmpegInvoke.getInstance()
.runCommandRxJava(command.split(" ").toTypedArray())
.subscribe(object : RxFFmpegSubscriber() {
override fun onFinish() {
Log.e("CASE", "视频压缩成功后大小:${FileUtils.getFileSize(outFile)}")
result?.invoke(outFile.length() > 1024 * 1024, outFile.path)
}
override fun onCancel() {
result?.invoke(false, StringUtils.getString(R.string.compress_video_fail))
}
override fun onProgress(
progress: Int,
progressTime: Long
) {
if (progress >= 0 && progress != compressPro) {
compressPro = progress
Log.e("CASE", "视频压缩进度:$progress")
pro?.invoke(progress)
}
}
override fun onError(message: String?) {
Log.e("CASE", "视频压缩失败")
result?.invoke(false, StringUtils.getString(R.string.compress_video_fail))
}
})
}
//获取视频封面第一帧
fun getFirstFrame(
originFile: File,
call: ((suc: Boolean, info: String) -> Unit)?
) {
val destImg = File(outParentImgs, EncryptUtils.encryptMD5File2String(originFile) + ".jpg")
val command = getCommandFirstFrame(originFile.path, destImg.path)
RxFFmpegInvoke.getInstance()
.runCommandRxJava(command.split(" ").toTypedArray())
.subscribe(object : RxFFmpegSubscriber() {
override fun onFinish() {
if (destImg.exists() && destImg.length() > 0) {
call?.invoke(true, destImg.path)
} else {
call?.invoke(false, StringUtils.getString(R.string.pic_first_frame_fail))
}
}
override fun onCancel() {
call?.invoke(false, StringUtils.getString(R.string.pic_first_frame_fail))
}
override fun onProgress(
progress: Int,
progressTime: Long
) {
}
override fun onError(message: String?) {
call?.invoke(false, message ?: StringUtils.getString(R.string.pic_first_frame_fail))
}
})
}
//清理压缩的视频文件(保留发布失败的视频)
fun clearCompressVideos(keepFile: File? = null) {
if (keepFile == null) {
FileUtils.deleteAllInDir(outParentVideo)
} else {
File(outParentVideo).listFiles()
?.let { files ->
for (file in files) {
if (file == keepFile) {
continue
}
FileUtils.delete(file)
}
}
}
}
//清理封面
fun clearFirstFrame() {
FileUtils.deleteAllInDir(outParentImgs)
}
//封面获取命令
@Throws
private fun getCommandFirstFrame(
originPath: String,
outPath: String
): String {
//读取图片尺寸和旋转角度
val mMetadataRetriever = MediaMetadataRetriever()
mMetadataRetriever.setDataSource(originPath)
val videoRotation =
mMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
val videoHeight =
mMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
val videoWidth =
mMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
val width: Int
val height: Int
if (Integer.parseInt(videoRotation) == 90 || Integer.parseInt(videoRotation) == 270) {
//角度不对需要宽高调换
width = videoHeight.toInt()
height = videoWidth.toInt()
} else {
width = videoWidth.toInt()
height = videoHeight.toInt()
}
return String.format(
"ffmpeg -y -i %1\$s -y -f image2 -t 0.001 -s %2\$sx%3\$s %4\$s",
originPath, width, height, outPath
)
}
//文件压缩命令
private fun getCommandCompress(
originPath: String,
outPath: String
): String? {
//https://blog.csdn.net/qq_31332467/article/details/79166945
//4K视频可能会闪退,所以需要添加尺寸压缩
val mMetadataRetriever = MediaMetadataRetriever()
mMetadataRetriever.setDataSource(originPath)
val videoRotation =
mMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
val videoHeight =
mMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
val videoWidth =
mMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
val bitrate = mMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)
//码率低于400不进行压缩
if (bitrate.toInt() < 400 * 1000) return null
val newBitrate = if (bitrate.toInt() > 3000 * 1000) {
3000
} else {
(bitrate.toInt() * 0.8f / 1000f).toInt()
}
val width: Int
val height: Int
if (Integer.parseInt(videoRotation) == 90 || Integer.parseInt(videoRotation) == 270) {
//角度不对需要宽高调换
width = videoHeight.toInt()
height = videoWidth.toInt()
} else {
width = videoWidth.toInt()
height = videoHeight.toInt()
}
//需要根据视频大小和视频时长计算得到需要压缩的码率,不然会导致高清视频压缩后变模糊,非高清视频压缩后文件变大
//https://blog.csdn.net/zhezhebie/article/details/79263492
return if (min(width, height) > 1080) {
//大于1080p
String.format(
"ffmpeg -y -i %1\$s -b ${newBitrate}k -r 30 -vcodec libx264 -vf scale=%2\$s -preset superfast %3\$s",
originPath, if (width > height) "1080:-1" else "-1:1080", outPath
)
} else {
//小于1080p
String.format(
"ffmpeg -y -i %1\$s -b ${newBitrate}k -r 30 -vcodec libx264 -preset superfast %2\$s",
originPath, outPath
)
}
}
}
3.layout_video布局
<?xml version="1.0" encoding="utf-8"?>
<chuangyuan.ycj.videolibrary.widget.VideoPlayerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:player_preview_layout_id="@layout/layout_video_cover"
/>
4.layout_video_cover布局
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
android:orientation="vertical"
>
<me.panpf.sketch.SketchImageView
android:id="@id/exo_preview_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter"
/>
<ImageView
android:id="@id/exo_preview_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:contentDescription="@null"
android:scaleType="centerInside"
android:src="@drawable/ic_exo_start"
/>
</FrameLayout>
5.使用代码

具体使用
网友评论