You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
481 lines
20 KiB
481 lines
20 KiB
package com.frank.ffmpeg.activity
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.graphics.Color
|
|
import android.media.MediaMetadataRetriever
|
|
import android.os.Bundle
|
|
import android.os.Environment
|
|
import android.os.Handler
|
|
import android.os.Message
|
|
import android.util.Log
|
|
import android.view.View
|
|
import android.widget.LinearLayout
|
|
import android.widget.TextView
|
|
import androidx.recyclerview.widget.RecyclerView
|
|
import androidx.recyclerview.widget.StaggeredGridLayoutManager
|
|
|
|
import com.frank.ffmpeg.FFmpegCmd
|
|
import com.frank.ffmpeg.R
|
|
import com.frank.ffmpeg.adapter.WaterfallAdapter
|
|
import com.frank.ffmpeg.format.VideoLayout
|
|
import com.frank.ffmpeg.gif.HighQualityGif
|
|
import com.frank.ffmpeg.handler.FFmpegHandler
|
|
import com.frank.ffmpeg.tool.JsonParseTool
|
|
import com.frank.ffmpeg.util.BitmapUtil
|
|
import com.frank.ffmpeg.util.FFmpegUtil
|
|
import com.frank.ffmpeg.util.FileUtil
|
|
|
|
import java.io.File
|
|
import java.util.ArrayList
|
|
|
|
import com.frank.ffmpeg.handler.FFmpegHandler.MSG_BEGIN
|
|
import com.frank.ffmpeg.handler.FFmpegHandler.MSG_FINISH
|
|
import com.frank.ffmpeg.handler.FFmpegHandler.MSG_PROGRESS
|
|
import com.frank.ffmpeg.listener.OnItemClickListener
|
|
|
|
/**
|
|
* video process by FFmpeg command
|
|
* Created by frank on 2018/1/25.
|
|
*/
|
|
class VideoHandleActivity : BaseActivity() {
|
|
|
|
private var layoutVideoHandle: RecyclerView? = null
|
|
private var layoutProgress: LinearLayout? = null
|
|
private var txtProgress: TextView? = null
|
|
private var currentPosition: Int = 0
|
|
private var ffmpegHandler: FFmpegHandler? = null
|
|
|
|
private val appendPath = PATH + File.separator + "snow.mp4"
|
|
private val outputPath1 = PATH + File.separator + "output1.ts"
|
|
private val outputPath2 = PATH + File.separator + "output2.ts"
|
|
private val listPath = PATH + File.separator + "listFile.txt"
|
|
|
|
private var list :List<String> ?= null
|
|
private var isJointing = false
|
|
|
|
@SuppressLint("HandlerLeak")
|
|
private val mHandler = object : Handler() {
|
|
override fun handleMessage(msg: Message) {
|
|
super.handleMessage(msg)
|
|
when (msg.what) {
|
|
MSG_BEGIN -> {
|
|
layoutProgress!!.visibility = View.VISIBLE
|
|
layoutVideoHandle!!.visibility = View.GONE
|
|
}
|
|
MSG_FINISH -> {
|
|
layoutProgress!!.visibility = View.GONE
|
|
layoutVideoHandle!!.visibility = View.VISIBLE
|
|
if (isJointing) {
|
|
isJointing = false
|
|
FileUtil.deleteFile(outputPath1)
|
|
FileUtil.deleteFile(outputPath2)
|
|
FileUtil.deleteFile(listPath)
|
|
}
|
|
if (!outputPath.isNullOrEmpty() && !this@VideoHandleActivity.isDestroyed) {
|
|
showToast("Save to:$outputPath")
|
|
outputPath = ""
|
|
}
|
|
}
|
|
MSG_PROGRESS -> {
|
|
val progress = msg.arg1
|
|
val duration = msg.arg2
|
|
if (progress > 0) {
|
|
txtProgress!!.visibility = View.VISIBLE
|
|
val percent = if (duration > 0) "%" else ""
|
|
val strProgress = progress.toString() + percent
|
|
txtProgress!!.text = strProgress
|
|
} else {
|
|
txtProgress!!.visibility = View.INVISIBLE
|
|
}
|
|
}
|
|
else -> {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override val layoutId: Int
|
|
get() = R.layout.activity_video_handle
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
|
|
// hideActionBar()
|
|
intView()
|
|
ffmpegHandler = FFmpegHandler(mHandler)
|
|
}
|
|
|
|
private fun intView() {
|
|
layoutProgress = getView(R.id.layout_progress)
|
|
txtProgress = getView(R.id.txt_progress)
|
|
list = listOf(
|
|
getString(R.string.video_transform),
|
|
getString(R.string.video_cut),
|
|
getString(R.string.video_concat),
|
|
getString(R.string.video_screen_shot),
|
|
getString(R.string.video_water_mark),
|
|
getString(R.string.video_remove_logo),
|
|
getString(R.string.video_from_photo),
|
|
getString(R.string.video_to_gif),
|
|
getString(R.string.video_multi),
|
|
getString(R.string.video_reverse),
|
|
getString(R.string.video_denoise),
|
|
getString(R.string.video_image),
|
|
getString(R.string.video_pip),
|
|
getString(R.string.video_speed),
|
|
getString(R.string.video_thumbnail),
|
|
getString(R.string.video_subtitle),
|
|
getString(R.string.video_rotate),
|
|
getString(R.string.video_gray),
|
|
getString(R.string.video_zoom))
|
|
|
|
layoutVideoHandle = findViewById(R.id.list_video_item)
|
|
val layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
|
|
layoutVideoHandle?.layoutManager = layoutManager
|
|
|
|
val adapter = WaterfallAdapter(list)
|
|
adapter.setOnItemClickListener(object : OnItemClickListener {
|
|
override fun onItemClick(position: Int) {
|
|
currentPosition = position
|
|
if (getString(R.string.video_from_photo) == (list as List<String>)[position]) {
|
|
handlePhoto()
|
|
} else {
|
|
selectFile()
|
|
}
|
|
}
|
|
})
|
|
layoutVideoHandle?.adapter = adapter
|
|
}
|
|
|
|
override fun onViewClick(view: View) {
|
|
|
|
}
|
|
|
|
override fun onSelectedFile(filePath: String) {
|
|
doHandleVideo(filePath)
|
|
}
|
|
|
|
/**
|
|
* Using FFmpeg cmd to handle video
|
|
*
|
|
* @param srcFile srcFile
|
|
*/
|
|
private fun doHandleVideo(srcFile: String) {
|
|
var commandLine: Array<String>? = null
|
|
if (!FileUtil.checkFileExist(srcFile)) {
|
|
return
|
|
}
|
|
if (!FileUtil.isVideo(srcFile)
|
|
&& (list as List<String>)[currentPosition] != getString(R.string.video_zoom)) {
|
|
showToast(getString(R.string.wrong_video_format))
|
|
return
|
|
}
|
|
val suffix = FileUtil.getFileSuffix(srcFile)
|
|
if (suffix == null || suffix.isEmpty()) {
|
|
return
|
|
}
|
|
when (currentPosition) {
|
|
0 //transform format
|
|
-> {
|
|
outputPath = PATH + File.separator + "transformVideo.mp4"
|
|
commandLine = FFmpegUtil.transformVideo(srcFile, outputPath)
|
|
}
|
|
1 //cut video
|
|
-> {
|
|
outputPath = PATH + File.separator + "cutVideo" + suffix
|
|
val startTime = 5.5f
|
|
val duration = 20.0f
|
|
commandLine = FFmpegUtil.cutVideo(srcFile, startTime, duration, outputPath)
|
|
}
|
|
2 //concat video together
|
|
-> concatVideo(srcFile)
|
|
3 //video snapshot
|
|
-> {
|
|
outputPath = PATH + File.separator + "screenShot.jpg"
|
|
val time = 10.5f
|
|
commandLine = FFmpegUtil.screenShot(srcFile, time, outputPath)
|
|
}
|
|
4 //add watermark to video
|
|
-> {
|
|
// the unit of bitRate is kb
|
|
var bitRate = 500
|
|
val retriever = MediaMetadataRetriever()
|
|
retriever.setDataSource(srcFile)
|
|
val mBitRate = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)
|
|
if (mBitRate != null && mBitRate.isNotEmpty()) {
|
|
val probeBitrate = Integer.valueOf(mBitRate)
|
|
bitRate = probeBitrate / 1000 / 100 * 100
|
|
}
|
|
//1:top left 2:top right 3:bottom left 4:bottom right
|
|
val location = 1
|
|
val offsetXY = 10
|
|
when (waterMarkType) {
|
|
TYPE_IMAGE// image
|
|
-> {
|
|
val photo = PATH + File.separator + "hello.png"
|
|
outputPath = PATH + File.separator + "photoMark.mp4"
|
|
commandLine = FFmpegUtil.addWaterMarkImg(srcFile, photo, location, bitRate, offsetXY, outputPath)
|
|
}
|
|
TYPE_GIF// gif
|
|
-> {
|
|
val gifPath = PATH + File.separator + "ok.gif"
|
|
outputPath = PATH + File.separator + "gifWaterMark.mp4"
|
|
commandLine = FFmpegUtil.addWaterMarkGif(srcFile, gifPath, location, bitRate, offsetXY, outputPath)
|
|
}
|
|
TYPE_TEXT// text
|
|
-> {
|
|
val text = "Hello,FFmpeg"
|
|
val textPath = PATH + File.separator + "text.png"
|
|
val result = BitmapUtil.textToPicture(textPath, text, Color.BLUE, 20)
|
|
outputPath = PATH + File.separator + "textMark.mp4"
|
|
commandLine = FFmpegUtil.addWaterMarkImg(srcFile, textPath, location, bitRate, offsetXY, outputPath)
|
|
}
|
|
else -> {
|
|
}
|
|
}
|
|
}
|
|
5 //Remove logo from video, or use to mosaic video
|
|
-> {
|
|
outputPath = PATH + File.separator + "removeLogo" + suffix
|
|
val widthL = 64
|
|
val heightL = 40
|
|
commandLine = FFmpegUtil.removeLogo(srcFile, 10, 10, widthL, heightL, outputPath)
|
|
}
|
|
7 //convert video into gif
|
|
-> {
|
|
outputPath = PATH + File.separator + "video2Gif.gif"
|
|
val gifStart = 10
|
|
val gifDuration = 3
|
|
val width = 320
|
|
val frameRate = 10
|
|
|
|
if (convertGifWithFFmpeg) {
|
|
val palettePath = "$PATH/palette.png"
|
|
FileUtil.deleteFile(palettePath)
|
|
val paletteCmd = FFmpegUtil.generatePalette(srcFile, gifStart, gifDuration,
|
|
frameRate, width, palettePath)
|
|
val gifCmd = FFmpegUtil.generateGifByPalette(srcFile, palettePath, gifStart, gifDuration,
|
|
frameRate, width, outputPath)
|
|
val cmdList = ArrayList<Array<String>>()
|
|
cmdList.add(paletteCmd)
|
|
cmdList.add(gifCmd)
|
|
ffmpegHandler!!.executeFFmpegCmds(cmdList)
|
|
} else {
|
|
convertGifInHighQuality(outputPath!!, srcFile, gifStart, gifDuration, frameRate)
|
|
}
|
|
}
|
|
8 //combine video which layout could be horizontal of vertical
|
|
-> {
|
|
val input1 = PATH + File.separator + "input1.mp4"
|
|
val input2 = PATH + File.separator + "input2.mp4"
|
|
outputPath = PATH + File.separator + "multi.mp4"
|
|
if (!FileUtil.checkFileExist(input1) || !FileUtil.checkFileExist(input2)) {
|
|
return
|
|
}
|
|
commandLine = FFmpegUtil.multiVideo(input1, input2, outputPath, VideoLayout.LAYOUT_HORIZONTAL)
|
|
}
|
|
9 //video reverse
|
|
-> {
|
|
outputPath = PATH + File.separator + "reverse.mp4"
|
|
commandLine = FFmpegUtil.reverseVideo(srcFile, outputPath)
|
|
}
|
|
10 //noise reduction of video
|
|
-> {
|
|
outputPath = PATH + File.separator + "denoise.mp4"
|
|
commandLine = FFmpegUtil.denoiseVideo(srcFile, outputPath)
|
|
}
|
|
11 //convert video to picture
|
|
-> {
|
|
outputPath = PATH + File.separator + "Video2Image/"
|
|
val imageFile = File(outputPath)
|
|
if (!imageFile.exists()) {
|
|
if (!imageFile.mkdir()) {
|
|
return
|
|
}
|
|
}
|
|
val mStartTime = 10//start time
|
|
val mDuration = 5//duration
|
|
val mFrameRate = 10//frameRate
|
|
commandLine = FFmpegUtil.videoToImage(srcFile, mStartTime, mDuration, mFrameRate, outputPath)
|
|
}
|
|
12 //combine into picture-in-picture video
|
|
-> {
|
|
val inputFile1 = PATH + File.separator + "beyond.mp4"
|
|
val inputFile2 = PATH + File.separator + "small_girl.mp4"
|
|
if (!FileUtil.checkFileExist(inputFile1) && !FileUtil.checkFileExist(inputFile2)) {
|
|
return
|
|
}
|
|
//x and y coordinate points need to be calculated according to the size of full video and small video
|
|
//For example: full video is 320x240, small video is 120x90, so x=200 y=150
|
|
val x = 200
|
|
val y = 150
|
|
outputPath = PATH + File.separator + "PicInPic.mp4"
|
|
commandLine = FFmpegUtil.picInPicVideo(inputFile1, inputFile2, x, y, outputPath)
|
|
}
|
|
13 //playing speed of video
|
|
-> {
|
|
outputPath = PATH + File.separator + "speed.mp4"
|
|
commandLine = FFmpegUtil.changeSpeed(srcFile, outputPath, 2f, false)
|
|
}
|
|
14 // insert thumbnail into video
|
|
-> {
|
|
val thumbnailPath = PATH + File.separator + "thumb.jpg"
|
|
outputPath = PATH + File.separator + "thumbnailVideo" + suffix
|
|
commandLine = FFmpegUtil.insertPicIntoVideo(srcFile, thumbnailPath, outputPath)
|
|
}
|
|
15 //add subtitle into video
|
|
-> {
|
|
val subtitlePath = PATH + File.separator + "test.ass"
|
|
outputPath = PATH + File.separator + "subtitle.mkv"
|
|
commandLine = FFmpegUtil.addSubtitleIntoVideo(srcFile, subtitlePath, outputPath)
|
|
}
|
|
16 // set the rotate degree of video
|
|
-> {
|
|
val rotateDegree = 90
|
|
outputPath = PATH + File.separator + "rotate" + rotateDegree + suffix
|
|
commandLine = FFmpegUtil.rotateVideo(srcFile, rotateDegree, outputPath)
|
|
}
|
|
17 // change video from RGB to gray
|
|
-> {
|
|
outputPath = PATH + File.separator + "gray" + suffix
|
|
commandLine = FFmpegUtil.toGrayVideo(srcFile, outputPath)
|
|
}
|
|
18 // zoom photo to video
|
|
-> {
|
|
outputPath = PATH + File.separator + "zoom.mp4"
|
|
commandLine = FFmpegUtil.photoZoomToVideo(srcFile, outputPath)
|
|
}
|
|
else -> {
|
|
}
|
|
}
|
|
if (ffmpegHandler != null && commandLine != null) {
|
|
ffmpegHandler!!.executeFFmpegCmd(commandLine)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* concat/joint two videos together
|
|
* It's recommended to convert to the same resolution and encoding
|
|
* @param selectedPath the path which is selected
|
|
*/
|
|
private fun concatVideo(selectedPath: String) {
|
|
if (ffmpegHandler == null || selectedPath.isEmpty()) {
|
|
return
|
|
}
|
|
isJointing = true
|
|
val targetPath = PATH + File.separator + "jointVideo.mp4"
|
|
val transformCmd1 = FFmpegUtil.transformVideoWithEncode(selectedPath, outputPath1)
|
|
var width = 0
|
|
var height = 0
|
|
//probe width and height of the selected video
|
|
val probeResult = FFmpegCmd.executeProbeSynchronize(FFmpegUtil.probeFormat(selectedPath))
|
|
val mediaBean = JsonParseTool.parseMediaFormat(probeResult)
|
|
if (mediaBean?.videoBean != null) {
|
|
width = mediaBean.videoBean!!.width
|
|
height = mediaBean.videoBean!!.height
|
|
Log.e(TAG, "width=$width--height=$height")
|
|
}
|
|
val transformCmd2 = FFmpegUtil.transformVideoWithEncode(appendPath, width, height, outputPath2)
|
|
val fileList = ArrayList<String>()
|
|
fileList.add(outputPath1)
|
|
fileList.add(outputPath2)
|
|
FileUtil.createListFile(listPath, fileList)
|
|
val jointVideoCmd = FFmpegUtil.jointVideo(listPath, targetPath)
|
|
val commandList = ArrayList<Array<String>>()
|
|
commandList.add(transformCmd1)
|
|
commandList.add(transformCmd2)
|
|
commandList.add(jointVideoCmd)
|
|
ffmpegHandler!!.executeFFmpegCmds(commandList)
|
|
}
|
|
|
|
private fun moov(srcFile: String) {
|
|
if (!srcFile.endsWith(FileUtil.TYPE_MP4)) {
|
|
showToast(getString(R.string.tip_not_mp4_video))
|
|
return
|
|
}
|
|
val filePath = FileUtil.getFilePath(srcFile)
|
|
var fileName = FileUtil.getFileName(srcFile)
|
|
fileName = "moov_" + fileName!!
|
|
outputPath = filePath + File.separator + fileName
|
|
val commandLine = FFmpegUtil.moveMoovAhead(srcFile, outputPath)
|
|
ffmpegHandler!!.executeFFmpegCmd(commandLine)
|
|
}
|
|
|
|
/**
|
|
* Combine pictures into video
|
|
*/
|
|
private fun handlePhoto() {
|
|
// The path of pictures, naming format: img+number.jpg
|
|
val picturePath = "$PATH/img/"
|
|
if (!FileUtil.checkFileExist(picturePath)) {
|
|
return
|
|
}
|
|
val tempPath = "$PATH/temp/"
|
|
FileUtil.deleteFolder(tempPath)
|
|
val photoFile = File(picturePath)
|
|
val files = photoFile.listFiles()
|
|
val cmdList = ArrayList<Array<String>>()
|
|
//the resolution of photo which you want to convert
|
|
val resolution = "640x320"
|
|
for (file in files) {
|
|
val inputPath = file.absolutePath
|
|
val outputPath = tempPath + file.name
|
|
val convertCmd = FFmpegUtil.convertResolution(inputPath, resolution, outputPath)
|
|
cmdList.add(convertCmd)
|
|
}
|
|
val combineVideo = PATH + File.separator + "combineVideo.mp4"
|
|
val frameRate = 2// suggested synthetic frameRate:1-10
|
|
val commandLine = FFmpegUtil.pictureToVideo(tempPath, frameRate, combineVideo)
|
|
cmdList.add(commandLine)
|
|
if (ffmpegHandler != null) {
|
|
ffmpegHandler!!.executeFFmpegCmds(cmdList)
|
|
}
|
|
}
|
|
|
|
private fun convertGifInHighQuality(gifPath: String, videoPath: String, startTime: Int, duration: Int, frameRate: Int) {
|
|
Thread {
|
|
mHandler.sendEmptyMessage(MSG_BEGIN)
|
|
var width = 0
|
|
var height = 0
|
|
var rotateDegree = 0
|
|
try {
|
|
val retriever = MediaMetadataRetriever()
|
|
retriever.setDataSource(videoPath)
|
|
val mWidth = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
|
|
val mHeight = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
|
|
width = Integer.valueOf(mWidth)
|
|
height = Integer.valueOf(mHeight)
|
|
val rotate = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)
|
|
rotateDegree = Integer.valueOf(rotate)
|
|
retriever.release()
|
|
Log.e(TAG, "retrieve width=$width--height=$height--rotate=$rotate")
|
|
} catch (e: Exception) {
|
|
Log.e(TAG, "retrieve error=$e")
|
|
}
|
|
|
|
val start = System.currentTimeMillis()
|
|
val highQualityGif = HighQualityGif(width, height, rotateDegree)
|
|
val result = highQualityGif.convertGIF(gifPath, videoPath, startTime, duration, frameRate)
|
|
Log.e(TAG, "convert gif result=" + result + "--time=" + (System.currentTimeMillis() - start))
|
|
mHandler.sendEmptyMessage(MSG_FINISH)
|
|
}.start()
|
|
}
|
|
|
|
override fun onDestroy() {
|
|
super.onDestroy()
|
|
mHandler.removeCallbacksAndMessages(null)
|
|
}
|
|
|
|
companion object {
|
|
|
|
private val TAG = VideoHandleActivity::class.java.simpleName
|
|
private val PATH = Environment.getExternalStorageDirectory().path
|
|
private var outputPath :String ?= null
|
|
|
|
private const val TYPE_IMAGE = 1
|
|
private const val TYPE_GIF = 2
|
|
private const val TYPE_TEXT = 3
|
|
private const val waterMarkType = TYPE_IMAGE
|
|
private const val convertGifWithFFmpeg = false
|
|
}
|
|
}
|
|
|