diff --git a/.gitignore b/.gitignore index 603b140..fb73966 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,7 @@ /captures .externalNativeBuild .cxx +.idea/shelf/Uncommitted_changes_before_Checkout_at_2023_11_9,_13_44_\[Changes\]/libffmpeg-org.so +.idea/shelf/Uncommitted_changes_before_Checkout_at_2023_11_9,_13_44_\[Changes\]/libffmpeg-org1.so +.idea/shelf/Uncommitted_changes_before_Checkout_at_2023_11_9,_13_44_\[Changes\]/shelved.patch +.idea/shelf/Uncommitted_changes_before_Checkout_at_2023_11_9__13_44__Changes_.xml diff --git a/README-CN.md b/README-CN.md index a773d66..275a150 100644 --- a/README-CN.md +++ b/README-CN.md @@ -23,7 +23,7 @@ 如果访问不了全部信息,请跳转[【国内镜像】](https://gitee.com/anjoiner/FFmpegCommand) ## 交叉编译 -* Macos 13.2 + GCC + Cmake + NDK 21 +* Macos 13.2 + Clang + Cmake + NDK 21 | 第三方库 | 版本 | 下载地址 | |------------|--------------------|------------------------------------------------------------------------------------------------------| diff --git a/README.md b/README.md index 83e3f90..010292a 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ In our development, audio and video related content is often used, generally we If you can’t access all the information, please go to[【国内镜像】](https://gitee.com/anjoiner/FFmpegCommand) ## Cross Compile -* Macos 13.2 + GCC + Cmake + NDK 21 (支持MediaCodec 编解码) +* Macos 13.2 + Clang + Cmake + NDK 21 (支持MediaCodec 编解码) | 第三方库 | 版本 | 下载地址 | |------------|--------------------|------------------------------------------------------------------------------------------------------| diff --git a/app/build.gradle b/app/build.gradle index 1aa0379..079a61c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,7 +23,7 @@ android { compileSdkVersion 31 defaultConfig { applicationId "com.coder.ffmpegtest" - minSdkVersion 15 + minSdkVersion 21 targetSdkVersion 31 versionCode 4 versionName "1.2.3" @@ -51,6 +51,10 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + buildFeatures{ + viewBinding true + } } dependencies { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 84c2524..56d4512 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -47,6 +47,9 @@ + + diff --git a/app/src/main/assets/fonts/PingFang Bold.ttf b/app/src/main/assets/fonts/PingFang Bold.ttf new file mode 100755 index 0000000..accaf1f Binary files /dev/null and b/app/src/main/assets/fonts/PingFang Bold.ttf differ diff --git a/app/src/main/assets/fonts/PingFang ExtraLight.ttf b/app/src/main/assets/fonts/PingFang ExtraLight.ttf new file mode 100755 index 0000000..d9b574b Binary files /dev/null and b/app/src/main/assets/fonts/PingFang ExtraLight.ttf differ diff --git a/app/src/main/assets/fonts/PingFang Heavy.ttf b/app/src/main/assets/fonts/PingFang Heavy.ttf new file mode 100755 index 0000000..f4cf788 Binary files /dev/null and b/app/src/main/assets/fonts/PingFang Heavy.ttf differ diff --git a/app/src/main/assets/fonts/PingFang Light.ttf b/app/src/main/assets/fonts/PingFang Light.ttf new file mode 100755 index 0000000..a513705 Binary files /dev/null and b/app/src/main/assets/fonts/PingFang Light.ttf differ diff --git a/app/src/main/assets/fonts/PingFang Medium.ttf b/app/src/main/assets/fonts/PingFang Medium.ttf new file mode 100755 index 0000000..982661b Binary files /dev/null and b/app/src/main/assets/fonts/PingFang Medium.ttf differ diff --git a/app/src/main/assets/fonts/PingFang Regular.ttf b/app/src/main/assets/fonts/PingFang Regular.ttf new file mode 100755 index 0000000..8790adb Binary files /dev/null and b/app/src/main/assets/fonts/PingFang Regular.ttf differ diff --git a/app/src/main/assets/mm.mp4 b/app/src/main/assets/mm.mp4 new file mode 100644 index 0000000..e99c1ba Binary files /dev/null and b/app/src/main/assets/mm.mp4 differ diff --git a/app/src/main/assets/srt/mm.srt b/app/src/main/assets/srt/mm.srt new file mode 100644 index 0000000..709a238 --- /dev/null +++ b/app/src/main/assets/srt/mm.srt @@ -0,0 +1,12 @@ +1 +00:00:00,200 --> 00:00:01,220 +自己不能待在这里 + +2 +00:00:01,460 --> 00:00:02,540 +得找个远离闹市 + +3 +00:00:02,720 --> 00:00:03,920 +但交通方便的地区 + diff --git a/app/src/main/java/com/coder/ffmpegtest/ui/KFFmpegFontActivity.kt b/app/src/main/java/com/coder/ffmpegtest/ui/KFFmpegFontActivity.kt new file mode 100644 index 0000000..957eb91 --- /dev/null +++ b/app/src/main/java/com/coder/ffmpegtest/ui/KFFmpegFontActivity.kt @@ -0,0 +1,252 @@ +package com.coder.ffmpegtest.ui + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.GridLayoutManager +import com.coder.ffmpeg.call.CommonCallBack +import com.coder.ffmpeg.jni.FFmpegCommand +import com.coder.ffmpeg.jni.FFmpegConfig +import com.coder.ffmpeg.utils.CommandParams +import com.coder.ffmpegtest.R +import com.coder.ffmpegtest.databinding.ActivityFfmpegFontBinding +import com.coder.ffmpegtest.model.CommandBean +import com.coder.ffmpegtest.ui.adapter.FFmpegCommandAdapter +import com.coder.ffmpegtest.ui.dialog.PromptDialog +import com.coder.ffmpegtest.utils.FileIOUtils +import com.coder.ffmpegtest.utils.FileUtils +import com.coder.ffmpegtest.utils.ToastUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import java.io.File +import java.util.ArrayList +import java.util.concurrent.ThreadLocalRandom + +/** + * 文字、字幕 + */ +class KFFmpegFontActivity : AppCompatActivity() { + private lateinit var binding: ActivityFfmpegFontBinding + + private lateinit var videoPath: String + private lateinit var srtPath: String + private lateinit var adapter: FFmpegCommandAdapter + private lateinit var fontDir: File + + private val fonts = arrayListOf( // 设置字体 + "PingFang ExtraLight.ttf", + "PingFang Light.ttf", + "PingFang Regular.ttf", + "PingFang Medium.ttf", + "PingFang Bold.ttf", + "PingFang Heavy.ttf" + ) + + // 存储在本地字体 + private val localFonts = mutableListOf() + + private var mErrorDialog: PromptDialog? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // 通过binding绑定视图 + binding = ActivityFfmpegFontBinding.inflate(layoutInflater) + setContentView(binding.root) + init() // 初始化 + } + + private fun init() { + initUI() + initData() + initClick() + } + + /** + * 初始化UI + */ + private fun initUI() { + if (mErrorDialog == null) { + mErrorDialog = PromptDialog.newInstance("进度", "完成", "", "停止") + mErrorDialog?.setHasNegativeButton(false) + mErrorDialog?.setOnPromptListener(object : PromptDialog.OnPromptListener { + override fun onPrompt(isPositive: Boolean) { + if (isPositive) FFmpegCommand.cancel() + } + }) + } + } + + private fun initData() { + if (FileUtils.copy2Memory(this, "mm.mp4")) { + videoPath = File(externalCacheDir, "mm.mp4").absolutePath + } + initList() + initFonts() + initSrt() + } + + private fun initList() { + val commands = this.resources.getStringArray(R.array.fonts) + val beans: MutableList = ArrayList() + for (i in commands.indices) { + beans.add(CommandBean(commands[i], i)) + } + binding.list.layoutManager = GridLayoutManager(this, 2) + adapter = FFmpegCommandAdapter(beans) + binding.list.adapter = adapter + } + + /** + * 初始化字体 + */ + private fun initFonts() { + fontDir = File(externalCacheDir, "fonts") + if (!fontDir.exists()) { + fontDir.mkdirs() + } + // 将字体循环写入本地 + for (font in fonts) { + val file = File(fontDir, font) + FileIOUtils.writeFileFromIS(file, assets.open("fonts/$font")) + localFonts.add(file) + } + } + + /** + * 初始化字幕文件 + */ + private fun initSrt() { + val srtDir = File(externalCacheDir, "srt") + if (!srtDir.exists()) { + srtDir.mkdirs() + } + val file = File(srtDir, "mm.srt") + FileIOUtils.writeFileFromIS(file, assets.open("srt/mm.srt")) + srtPath = file.absolutePath + } + + private fun initClick() { + adapter.setItemClickListener(object : FFmpegCommandAdapter.ItemClickListener { + override fun itemClick(id: Int) { + when (id) { + 0 -> { // 绘制文本 + drawText() + } + + 1 -> { // 添加字幕 + subtitles() + } + } + } + }) + } + + /** + * 绘制文本 + */ + private fun drawText() { + // 随机一个字体路径 + val index = ThreadLocalRandom.current().nextInt(localFonts.size) + val outputPath = File(externalCacheDir, "target-drawtext.mp4").absolutePath + val commands = CommandParams() + .append("-c:v") + .append("h264_mediacodec") + .append("-i") + .append(videoPath) + .append("-b") // 硬编码一般需要设置视频的比特率(bitrate) + .append("1500k") + .append("-vf") + .append("drawtext=fontfile=${localFonts[index]}:text=FFmpegCommand:x=(w-text_w)/2:y=(h-text_h)/2:fontsize=100:fontcolor=white") + .append("-c:v") + .append("h264_mediacodec") + .append(outputPath) + .get() + + MainScope().launch(Dispatchers.IO) { + FFmpegCommand.runCmd(commands, callback("绘制文本完成", outputPath)) + } + } + + /** + * 绘制文本 + */ + private fun subtitles() { + val mapping = HashMap() + mapping["PingFang-SC"] = "PingFang-SC" + FFmpegConfig.setFontDir(this, fontDir.absolutePath, mapping) + + val outputPath = File(externalCacheDir, "target-subtitles.mp4").absolutePath + val commands = CommandParams() + .append("-c:v") + .append("h264_mediacodec") + .append("-i") + .append(videoPath) + .append("-b") // 硬编码一般需要设置视频的比特率(bitrate) + .append("1500k") + .append("-vf") + .append("subtitles=${srtPath}:force_style='fontname=苹方 常规,fontsize=40,PrimaryColour=&H000000,OutlineColour=&HF0F0F0,Italic=0,BorderStyle=1,Alignment=2'") + .append("-c:v") + .append("h264_mediacodec") + .append(outputPath) + .get() + + MainScope().launch(Dispatchers.IO) { + FFmpegCommand.runCmd(commands, callback("绘制文本完成", outputPath)) + } + } + + + private fun callback(msg: String, targetPath: String?): CommonCallBack? { + return object : CommonCallBack() { + override fun onStart() { + Log.d("FFmpegCmd", "onStart") + runOnUiThread { + mErrorDialog?.show(supportFragmentManager, "Dialog") + } + } + + override fun onComplete() { + Log.d("FFmpegCmd", "onComplete") + runOnUiThread { + ToastUtils.show(msg) + mErrorDialog?.setContent(0) + mErrorDialog?.dismissAllowingStateLoss() + binding.contentText.text = targetPath + } + + } + + override fun onCancel() { + runOnUiThread { + ToastUtils.show("用户取消") + mErrorDialog?.setContent(0) + } + Log.d("FFmpegCmd", "Cancel") + } + + override fun onProgress(progress: Int, pts: Long) { + Log.d("FFmpegCmd", progress.toString()) + runOnUiThread { mErrorDialog?.setContent(progress) } + } + + override fun onError(errorCode: Int, errorMsg: String?) { + Log.d("FFmpegCmd", errorMsg ?: "") + runOnUiThread { + ToastUtils.show(errorMsg) + mErrorDialog?.setContent(0) + mErrorDialog?.dismissAllowingStateLoss() + } + } + } + } + + companion object { + fun start(context: Context) { + val intent = Intent(context, KFFmpegFontActivity::class.java) + context.startActivity(intent) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/coder/ffmpegtest/ui/MainActivity.kt b/app/src/main/java/com/coder/ffmpegtest/ui/MainActivity.kt index 8b7156e..d0234e9 100644 --- a/app/src/main/java/com/coder/ffmpegtest/ui/MainActivity.kt +++ b/app/src/main/java/com/coder/ffmpegtest/ui/MainActivity.kt @@ -9,44 +9,43 @@ import android.widget.Button import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import com.coder.ffmpegtest.R +import com.coder.ffmpegtest.databinding.ActivityFfmpegFontBinding +import com.coder.ffmpegtest.databinding.ActivityMainBinding import com.coder.ffmpegtest.service.FFmpegCommandService @SuppressLint("NonConstantResourceId") class MainActivity : AppCompatActivity(), View.OnClickListener { - var mCommandBtn: Button? = null - var mInfoBtn: Button? = null - var mFormatBtn: Button? = null - var mCodecBtn: Button? = null - var mAbiText: TextView? = null + private lateinit var binding: ActivityMainBinding + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - intiView() - initListener() + // 通过binding绑定视图 + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + // 初始化 + initUI() + initClick() } - private fun intiView() { - mCommandBtn = findViewById(R.id.btn_command) - mInfoBtn = findViewById(R.id.btn_info) - mFormatBtn = findViewById(R.id.btn_format) - mCodecBtn = findViewById(R.id.btn_codec) - mAbiText = findViewById(R.id.tv_abi) - mAbiText?.text = String.format("当前使用cpu-abi:%s", Build.CPU_ABI) + private fun initUI() { + binding.abiText.text = String.format("当前使用cpu-abi:%s", Build.CPU_ABI) } - private fun initListener() { - mCommandBtn!!.setOnClickListener(this) - mInfoBtn!!.setOnClickListener(this) - mFormatBtn!!.setOnClickListener(this) - mCodecBtn!!.setOnClickListener(this) + private fun initClick() { + binding.commandBtn.setOnClickListener(this) + binding.infoBtn.setOnClickListener(this) + binding.formatBtn.setOnClickListener(this) + binding.codecBtn.setOnClickListener(this) + binding.fontBtn.setOnClickListener(this) } override fun onClick(v: View) { when (v.id) { - R.id.btn_command -> KFFmpegCommandActivity.start(this) - R.id.btn_info -> KFFmpegInfoActivity.start(this) - R.id.btn_format -> KFFmppegFormatActivity.start(this) - R.id.btn_codec -> KFFmpegCodecActivity.start(this) + R.id.command_btn -> KFFmpegCommandActivity.start(this) + R.id.info_btn -> KFFmpegInfoActivity.start(this) + R.id.format_btn -> KFFmppegFormatActivity.start(this) + R.id.codec_btn -> KFFmpegCodecActivity.start(this) + R.id.font_btn -> KFFmpegFontActivity.start(this) } } } \ No newline at end of file diff --git a/app/src/main/java/com/coder/ffmpegtest/utils/FileIOUtils.kt b/app/src/main/java/com/coder/ffmpegtest/utils/FileIOUtils.kt new file mode 100644 index 0000000..76f5958 --- /dev/null +++ b/app/src/main/java/com/coder/ffmpegtest/utils/FileIOUtils.kt @@ -0,0 +1,455 @@ +package com.coder.ffmpegtest.utils + +import android.content.Context +import android.content.res.AssetManager +import android.util.Log +import java.io.* +import java.nio.ByteBuffer +import java.nio.MappedByteBuffer +import java.nio.channels.FileChannel +import java.nio.charset.Charset + + +/** + *Created by AnJoiner on 2021/10/2 09:51 + */ +object FileIOUtils { + + private const val sBufferSize: Int = 524288 + + /** + * Write file from input stream. + * + * @param filePath The path of file. + * @param is The input stream. + * @return `true`: success

`false`: fail + */ + fun writeFileFromIS(filePath: String?, inputStream: InputStream?): Boolean { + return writeFileFromIS(FileUtils.getFileByPath(filePath), inputStream, false, null) + } + + /** + * Write file from input stream. + * + * @param filePath The path of file. + * @param is The input stream. + * @param append True to append, false otherwise. + * @return `true`: success

`false`: fail + */ + fun writeFileFromIS( + filePath: String?, + inputStream: InputStream?, + append: Boolean + ): Boolean { + return writeFileFromIS(FileUtils.getFileByPath(filePath), inputStream, append, null) + } + + /** + * Write file from input stream. + * + * @param file The file. + * @param is The input stream. + * @return `true`: success

`false`: fail + */ + fun writeFileFromIS(file: File?, inputStream: InputStream?): Boolean { + return writeFileFromIS(file, inputStream, false, null) + } + + /** + * Write file from input stream. + * + * @param file The file. + * @param is The input stream. + * @param append True to append, false otherwise. + * @return `true`: success

`false`: fail + */ + fun writeFileFromIS( + file: File?, + inputStream: InputStream?, + append: Boolean + ): Boolean { + return writeFileFromIS(file, inputStream, append, null) + } + + /////////////////////////////////////////////////////////////////////////// + // writeFileFromIS with progress + /////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////// + // writeFileFromIS with progress + /////////////////////////////////////////////////////////////////////////// + /** + * Write file from input stream. + * + * @param filePath The path of file. + * @param is The input stream. + * @param listener The progress update listener. + * @return `true`: success

`false`: fail + */ + fun writeFileFromIS( + filePath: String?, + inputStream: InputStream?, + listener: OnProgressUpdateListener? + ): Boolean { + return writeFileFromIS(FileUtils.getFileByPath(filePath), inputStream, false, listener) + } + + /** + * Write file from input stream. + * + * @param filePath The path of file. + * @param is The input stream. + * @param append True to append, false otherwise. + * @param listener The progress update listener. + * @return `true`: success

`false`: fail + */ + fun writeFileFromIS( + filePath: String?, + inputStream: InputStream?, + append: Boolean, + listener: OnProgressUpdateListener? + ): Boolean { + return writeFileFromIS(FileUtils.getFileByPath(filePath), inputStream, append, listener) + } + + /** + * Write file from input stream. + * + * @param file The file. + * @param is The input stream. + * @param listener The progress update listener. + * @return `true`: success

`false`: fail + */ + fun writeFileFromIS( + file: File?, + inputStream: InputStream?, + listener: OnProgressUpdateListener? + ): Boolean { + return writeFileFromIS(file, inputStream, false, listener) + } + + /** + * Write file from input stream. + * + * @param file The file. + * @param is The input stream. + * @param append True to append, false otherwise. + * @param listener The progress update listener. + * @return `true`: success

`false`: fail + */ + fun writeFileFromIS( + file: File?, + inputStream: InputStream?, + append: Boolean, + listener: OnProgressUpdateListener? + ): Boolean { + if (inputStream == null || !FileUtils.createOrExistsFile(file)) { + Log.e("FileIOUtils", "create file <$file> failed.") + return false + } + var os: OutputStream? = null + return try { + os = BufferedOutputStream(FileOutputStream(file, append), sBufferSize) + if (listener == null) { + val data = ByteArray(sBufferSize) + var len: Int + while (inputStream.read(data).also { len = it } != -1) { + os.write(data, 0, len) + } + } else { + val totalSize: Double = inputStream.available().toDouble() + var curSize = 0 + listener.onProgressUpdate(0.0) + val data = ByteArray(sBufferSize) + var len: Int + while (inputStream.read(data).also { len = it } != -1) { + os.write(data, 0, len) + curSize += len + listener.onProgressUpdate(curSize / totalSize) + } + } + true + } catch (e: IOException) { + e.printStackTrace() + false + } finally { + try { + inputStream.close() + } catch (e: IOException) { + e.printStackTrace() + } + try { + os?.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + } + + + /** + * Return the string in file. + * + * @param filePath The path of file. + * @return the string in file + */ + fun readFile2String(filePath: String?): String? { + return readFile2String(FileUtils.getFileByPath(filePath), null) + } + + /** + * Return the string in file. + * + * @param filePath The path of file. + * @param charsetName The name of charset. + * @return the string in file + */ + fun readFile2String(filePath: String?, charsetName: String?): String? { + return readFile2String(FileUtils.getFileByPath(filePath), charsetName) + } + + /** + * Return the string in file. + * + * @param file The file. + * @return the string in file + */ + fun readFile2String(file: File?): String? { + return readFile2String(file, null) + } + + /** + * Return the string in file. + * + * @param file The file. + * @param charsetName The name of charset. + * @return the string in file + */ + fun readFile2String(file: File?, charsetName: String?): String? { + val bytes = readFile2BytesByStream(file) ?: return null + return if (charsetName.isNullOrEmpty()) { + String(bytes) + } else { + try { + String(bytes, Charset.forName(charsetName)) + } catch (e: UnsupportedEncodingException) { + e.printStackTrace() + "" + } + } + } + + /////////////////////////////////////////////////////////////////////////// + // readFile2BytesByStream without progress + /////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////// + // readFile2BytesByStream without progress + /////////////////////////////////////////////////////////////////////////// + /** + * Return the bytes in file by stream. + * + * @param filePath The path of file. + * @return the bytes in file + */ + fun readFile2BytesByStream(filePath: String?): ByteArray? { + return readFile2BytesByStream(FileUtils.getFileByPath(filePath), null) + } + + /** + * Return the bytes in file by stream. + * + * @param file The file. + * @return the bytes in file + */ + fun readFile2BytesByStream(file: File?): ByteArray? { + return readFile2BytesByStream(file, null) + } + + /////////////////////////////////////////////////////////////////////////// + // readFile2BytesByStream with progress + /////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////// + // readFile2BytesByStream with progress + /////////////////////////////////////////////////////////////////////////// + /** + * Return the bytes in file by stream. + * + * @param filePath The path of file. + * @param listener The progress update listener. + * @return the bytes in file + */ + fun readFile2BytesByStream( + filePath: String?, + listener: OnProgressUpdateListener? + ): ByteArray? { + return readFile2BytesByStream(FileUtils.getFileByPath(filePath), listener) + } + + /** + * Return the bytes in file by stream. + * + * @param file The file. + * @param listener The progress update listener. + * @return the bytes in file + */ + fun readFile2BytesByStream( + file: File?, + listener: OnProgressUpdateListener? + ): ByteArray? { + return if (file == null || !file.exists()) null else try { + var os: ByteArrayOutputStream? = null + val `is`: InputStream = BufferedInputStream(FileInputStream(file), sBufferSize) + try { + os = ByteArrayOutputStream() + val b = ByteArray(sBufferSize) + var len: Int + if (listener == null) { + while (`is`.read(b, 0, sBufferSize).also { len = it } != -1) { + os.write(b, 0, len) + } + } else { + val totalSize = `is`.available().toDouble() + var curSize = 0 + listener.onProgressUpdate(0.0) + while (`is`.read(b, 0, sBufferSize).also { len = it } != -1) { + os.write(b, 0, len) + curSize += len + listener.onProgressUpdate(curSize / totalSize) + } + } + os.toByteArray() + } catch (e: IOException) { + e.printStackTrace() + null + } finally { + try { + `is`.close() + } catch (e: IOException) { + e.printStackTrace() + } + try { + os?.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + } catch (e: FileNotFoundException) { + e.printStackTrace() + null + } + } + + /** + * Return the bytes in file by channel. + * + * @param filePath The path of file. + * @return the bytes in file + */ + fun readFile2BytesByChannel(filePath: String?): ByteArray? { + return readFile2BytesByChannel(FileUtils.getFileByPath(filePath)) + } + + /** + * Return the bytes in file by channel. + * + * @param file The file. + * @return the bytes in file + */ + fun readFile2BytesByChannel(file: File?): ByteArray? { + if (file == null || !file.exists()) return null + var fc: FileChannel? = null + return try { + fc = RandomAccessFile(file, "r").channel + if (fc == null) { + Log.e("FileIOUtils", "fc is null.") + return ByteArray(0) + } + val byteBuffer: ByteBuffer = ByteBuffer.allocate(fc.size().toInt()) + while (true) { + if (fc.read(byteBuffer) <= 0) break + } + byteBuffer.array() + } catch (e: IOException) { + e.printStackTrace() + null + } finally { + try { + fc?.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + } + + + /** + * Return the bytes in file by map. + * + * @param filePath The path of file. + * @return the bytes in file + */ + fun readFile2BytesByMap(filePath: String?): ByteArray? { + return readFile2BytesByMap(FileUtils.getFileByPath(filePath)) + } + + + /** + * Return the bytes in file by map. + * + * @param file The file. + * @return the bytes in file + */ + fun readFile2BytesByMap(file: File?): ByteArray? { + if (file == null || !file.exists()) return null + var fc: FileChannel? = null + return try { + fc = RandomAccessFile(file, "r").channel + if (fc == null) { + Log.e("FileIOUtils", "fc is null.") + return ByteArray(0) + } + val size = fc.size() as Int + val mbb: MappedByteBuffer = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size()).load() + val result = ByteArray(size) + mbb.get(result, 0, size) + result + } catch (e: IOException) { + e.printStackTrace() + null + } finally { + try { + fc?.close() + } catch (e: IOException) { + e.printStackTrace() + } + } + } + + fun readAssetFile(context: Context, fileName: String?): String { + //将json数据变成字符串 + val stringBuilder = StringBuilder() + try { + //获取assets资源管理器 + val assetManager: AssetManager = context.assets + //通过管理器打开文件并读取 + val bf = BufferedReader( + InputStreamReader( + assetManager.open(fileName!!) + ) + ) + var line: String? + while (bf.readLine().also { line = it } != null) { + stringBuilder.append(line) + } + } catch (e: IOException) { + e.printStackTrace() + } + return stringBuilder.toString() + } + + interface OnProgressUpdateListener { + fun onProgressUpdate(progress: Double) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/coder/ffmpegtest/utils/FileUtils.kt b/app/src/main/java/com/coder/ffmpegtest/utils/FileUtils.kt index c8144b5..16ee59c 100644 --- a/app/src/main/java/com/coder/ffmpegtest/utils/FileUtils.kt +++ b/app/src/main/java/com/coder/ffmpegtest/utils/FileUtils.kt @@ -3,15 +3,484 @@ package com.coder.ffmpegtest.utils import android.content.Context import android.util.Log import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException import java.io.FileOutputStream import java.io.IOException import java.io.RandomAccessFile +import java.net.URL +import javax.net.ssl.HttpsURLConnection /** * @author: AnJoiner * @datetime: 19-12-20 */ object FileUtils { + const val BYTE = 1 + const val KB = 1024 + const val MB = 1048576 + const val GB = 1073741824 + + /** + * Return the size. + * + * @param filePath The path of file. + * @return the size + */ + fun getSize(filePath: String?): String? { + return getSize(getFileByPath(filePath)) + } + + /** + * Return the size. + * + * @param file The directory. + * @return the size + */ + fun getSize(file: File?): String? { + if (file == null) return "" + return if (file.isDirectory) { + getDirSize(file) + } else getFileSize(file) + } + + /** + * Return the size of directory. + * + * @param dir The directory. + * @return the size of directory + */ + private fun getDirSize(dir: File): String? { + val len = getDirLength(dir) + return if (len == -1L) "" else byte2FitMemorySize(len) + } + + /** + * Return the size of file. + * + * @param file The file. + * @return the length of file + */ + private fun getFileSize(file: File): String? { + val len = getFileLength(file)!! + return if (len == -1L) "" else byte2FitMemorySize(len) + } + + /** + * Return the length. + * + * @param filePath The path of file. + * @return the length + */ + fun getLength(filePath: String?): Long { + return getLength(getFileByPath(filePath)) + } + + /** + * Return the length. + * + * @param file The file. + * @return the length + */ + fun getLength(file: File?): Long { + if (file == null) return 0 + return if (file.isDirectory) { + getDirLength(file) + } else getFileLength(file)!! + } + + /** + * Return the length of directory. + * + * @param dir The directory. + * @return the length of directory + */ + private fun getDirLength(dir: File): Long { + if (!isDir(dir)) return 0 + var len: Long = 0 + val files: Array = dir.listFiles() + if (files != null && files.size > 0) { + for (file in files) { + if (file.isDirectory()) { + len += getDirLength(file) + } else { + len += file.length() + } + } + } + return len + } + + /** + * Return the length of file. + * + * @param filePath The path of file. + * @return the length of file + */ + fun getFileLength(filePath: String): Long? { + + val regex = Regex("[a-zA-z]+://[^\\s]*") + val isURL = filePath.matches(regex) + if (isURL) { + try { + val conn: HttpsURLConnection = URL(filePath).openConnection() as HttpsURLConnection + conn.setRequestProperty("Accept-Encoding", "identity") + conn.connect() + return if (conn.responseCode == 200) { + conn.contentLength.toLong() + } else -1 + } catch (e: IOException) { + e.printStackTrace() + } + } + return getFileLength(getFileByPath(filePath)) + } + + /** + * Return the length of file. + * + * @param file The file. + * @return the length of file + */ + fun getFileLength(file: File?): Long? { + return if (!isFile(file)) -1 else file?.length() + } + + /** + * Return whether it is a directory. + * + * @param file The file. + * @return `true`: yes

`false`: no + */ + fun isDir(file: File?): Boolean { + return file != null && file.exists() && file.isDirectory + } + + /** + * Return whether it is a file. + * + * @param filePath The path of file. + * @return `true`: yes

`false`: no + */ + fun isFile(filePath: String?): Boolean { + return isFile(getFileByPath(filePath)) + } + + /** + * Return whether it is a file. + * + * @param file The file. + * @return `true`: yes

`false`: no + */ + fun isFile(file: File?): Boolean { + return file != null && file.exists() && file.isFile + } + + /** + * Return the file by path. + * + * @param filePath The path of file. + * @return the file + */ + fun getFileByPath(filePath: String?): File? { + return if (filePath.isNullOrEmpty()) null else File(filePath) + } + + private fun byte2FitMemorySize(byteSize: Long): String? { + return byte2FitMemorySize(byteSize, 3) + } + + private fun byte2FitMemorySize(byteSize: Long, precision: Int): String? { + require(precision >= 0) { "precision shouldn't be less than zero!" } + return when { + byteSize < 0 -> { + throw IllegalArgumentException("byteSize shouldn't be less than zero!") + } + byteSize < KB -> { + String.format("%." + precision + "fB", byteSize.toDouble()) + } + byteSize < MB -> { + java.lang.String.format( + "%." + precision + "fKB", + byteSize.toDouble() / KB + ) + } + byteSize < GB -> { + java.lang.String.format( + "%." + precision + "fMB", + byteSize.toDouble() / MB + ) + } + else -> { + java.lang.String.format( + "%." + precision + "fGB", + byteSize.toDouble() / GB + ) + } + } + } + + + /** + * Delete the directory. + * + * @param file The file. + * @return `true`: success

`false`: fail + */ + fun delete(file: File?): Boolean { + if (file == null) return false + return if (file.isDirectory) { + deleteDir(file) + } else deleteFile(file) + } + + /** + * Delete the file. + * + * @param file The file. + * @return `true`: success

`false`: fail + */ + private fun deleteFile(file: File?): Boolean { + return file != null && (!file.exists() || file.isFile && file.delete()) + } + + + /** + * Delete the directory. + * + * @param dir The directory. + * @return `true`: success

`false`: fail + */ + private fun deleteDir(dir: File?): Boolean { + if (dir == null) return false + // dir doesn't exist then return true + if (!dir.exists()) return true + // dir isn't a directory then return false + if (!dir.isDirectory) return false + val files = dir.listFiles() + if (files != null && files.size > 0) { + for (file in files) { + if (file.isFile) { + if (!file.delete()) return false + } else if (file.isDirectory) { + if (!deleteDir(file)) return false + } + } + } + return dir.delete() + } + + /** + * Create a file if it doesn't exist, otherwise do nothing. + * + * @param filePath The path of file. + * @return `true`: exists or creates successfully

`false`: otherwise + */ + fun createOrExistsFile(filePath: String?): Boolean { + return createOrExistsFile(getFileByPath(filePath)) + } + + /** + * Create a file if it doesn't exist, otherwise do nothing. + * + * @param file The file. + * @return `true`: exists or creates successfully

`false`: otherwise + */ + fun createOrExistsFile(file: File?): Boolean { + if (file == null) return false + if (file.exists()) return file.isFile + return if (!createOrExistsDir(file.parentFile)) false else try { + file.createNewFile() + } catch (e: IOException) { + e.printStackTrace() + false + } + } + + /** + * Create a directory if it doesn't exist, otherwise do nothing. + * + * @param dirPath The path of directory. + * @return `true`: exists or creates successfully

`false`: otherwise + */ + fun createOrExistsDir(dirPath: String?): Boolean { + return createOrExistsDir(getFileByPath(dirPath)) + } + + /** + * Create a directory if it doesn't exist, otherwise do nothing. + * + * @param file The file. + * @return `true`: exists or creates successfully

`false`: otherwise + */ + fun createOrExistsDir(file: File?): Boolean { + return file != null && if (file.exists()) file.isDirectory else file.mkdirs() + } + + /** + * Copy the directory or file. + * + * @param srcPath The path of source. + * @param destPath The path of destination. + * @return `true`: success

`false`: fail + */ + fun copy( + srcPath: String?, + destPath: String? + ): Boolean { + return copy(getFileByPath(srcPath), getFileByPath(destPath), null) + } + + /** + * Copy the directory or file. + * + * @param srcPath The path of source. + * @param destPath The path of destination. + * @param listener The replace listener. + * @return `true`: success

`false`: fail + */ + fun copy( + srcPath: String?, + destPath: String?, + listener: OnReplaceListener? + ): Boolean { + return copy(getFileByPath(srcPath), getFileByPath(destPath), listener) + } + + /** + * Copy the directory or file. + * + * @param src The source. + * @param dest The destination. + * @return `true`: success

`false`: fail + */ + fun copy( + src: File?, + dest: File? + ): Boolean { + return copy(src, dest, null) + } + + /** + * Copy the directory or file. + * + * @param src The source. + * @param dest The destination. + * @param listener The replace listener. + * @return `true`: success

`false`: fail + */ + fun copy( + src: File?, + dest: File?, + listener: OnReplaceListener? + ): Boolean { + if (src == null) return false + return if (src.isDirectory) { + copyDir(src, dest, listener) + } else copyFile(src, dest, listener) + } + + /** + * Copy the directory. + * + * @param srcDir The source directory. + * @param destDir The destination directory. + * @param listener The replace listener. + * @return `true`: success

`false`: fail + */ + private fun copyDir( + srcDir: File, + destDir: File?, + listener: OnReplaceListener? + ): Boolean { + return copyOrMoveDir(srcDir, destDir, listener!!, false) + } + + /** + * Copy the file. + * + * @param srcFile The source file. + * @param destFile The destination file. + * @param listener The replace listener. + * @return `true`: success

`false`: fail + */ + private fun copyFile( + srcFile: File, + destFile: File?, + listener: OnReplaceListener? + ): Boolean { + return copyOrMoveFile(srcFile, destFile, listener, false) + } + + private fun copyOrMoveDir( + srcDir: File?, + destDir: File?, + listener: OnReplaceListener, + isMove: Boolean + ): Boolean { + if (srcDir == null || destDir == null) return false + // destDir's path locate in srcDir's path then return false + val srcPath = srcDir.path + File.separator + val destPath = destDir.path + File.separator + if (destPath.contains(srcPath)) return false + if (!srcDir.exists() || !srcDir.isDirectory) return false + if (!createOrExistsDir(destDir)) return false + val files = srcDir.listFiles() + if (files != null && files.size > 0) { + for (file in files) { + val oneDestFile = File(destPath + file.name) + if (file.isFile) { + if (!copyOrMoveFile(file, oneDestFile, listener, isMove)) return false + } else if (file.isDirectory) { + if (!copyOrMoveDir(file, oneDestFile, listener, isMove)) return false + } + } + } + return !isMove || deleteDir(srcDir) + } + + private fun copyOrMoveFile( + srcFile: File?, + destFile: File?, + listener: OnReplaceListener?, + isMove: Boolean + ): Boolean { + if (srcFile == null || destFile == null) return false + // srcFile equals destFile then return false + if (srcFile == destFile) return false + // srcFile doesn't exist or isn't a file then return false + if (!srcFile.exists() || !srcFile.isFile) return false + if (destFile.exists()) { + if (listener == null || listener.onReplace( + srcFile, + destFile + ) + ) { // require delete the old file + if (!destFile.delete()) { // unsuccessfully delete then return false + return false + } + } else { + return true + } + } + return if (!createOrExistsDir(destFile.parentFile)) false else try { + (FileIOUtils.writeFileFromIS(destFile.absolutePath, FileInputStream(srcFile)) + && !(isMove && !deleteFile(srcFile))) + } catch (e: FileNotFoundException) { + e.printStackTrace() + false + } + } + + + fun getFileNameByUrl(url: String?):String { + return url?.substring(url.lastIndexOf('/') + 1)?:"" + } + + interface OnReplaceListener { + fun onReplace(srcFile: File?, destFile: File?): Boolean + } + /** * 将asset文件写入缓存 */ diff --git a/app/src/main/res/layout/activity_ffmpeg_font.xml b/app/src/main/res/layout/activity_ffmpeg_font.xml new file mode 100644 index 0000000..d663675 --- /dev/null +++ b/app/src/main/res/layout/activity_ffmpeg_font.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index eca73d5..16f45d4 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -10,64 +10,73 @@ tools:context=".ui.MainActivity"> - -