美文网首页
2019-11-02 yjPlay使用记录

2019-11-02 yjPlay使用记录

作者: 兣甅 | 来源:发表于2019-11-02 19:18 被阅读0次

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
  }
}

2.文件压缩命令,需要使用RxFFmpeg

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.使用代码

具体使用

相关文章

  • 2019-11-02 yjPlay使用记录

    1.自定义View解放外部调用 2.文件压缩命令,需要使用RxFFmpeg 3.layout_video布局 4....

  • 群文阅读,文本特质

    青春语文一群@语文湿地 微信群上的聊天记录如下,请查收。 ————— 2019-11-02 ————— 孙秋备 1...

  • 周末一画

    周末小画,愉快的度过一天。2019-11-02

  • Jenkins中无法启动docker daemon的解决和思考

    changelog:[2019-04-23] 更新push镜像失败[2019-11-02] 追加遇到资源隔离问题 ...

  • 文先森的日常--剩44天

    日精进打卡第447天 姓名:李文杰 (四爷); 公司:中国太平人寿; 日期:2019-11-02 【知~学习】 《...

  • 2019-11-03

    2019-11-02 【日精进打卡第 588 天 【知~学习】 《六项精进》大纲 4 遍共 2208 遍 《大学》...

  • 幸福快乐的阶梯

    【成长日志】:2019-11-02 星期六 【学习方法】:系统读经第1085天。 雨宝:147累积法; 阳宝:1...

  • 2019-11-04

    2019-11-03 2019-11-02 2019-11-03 日精进打卡 姓名:彭新 宁波蓝天白云供应链管理有...

  • 2019-11-03

    2019-11-02 2019-11-03 日精进打卡 姓名:彭新 宁波蓝天白云供应链管理有限公司 【日精进打卡第...

  • 2019-11-02

    2019-11-02 日精进打卡 姓名:彭新 宁波蓝天白云供应链管理有限公司 【日精进打卡第560天】 【知~学习...

网友评论

      本文标题:2019-11-02 yjPlay使用记录

      本文链接:https://www.haomeiwen.com/subject/mqshbctx.html