V1.0(1)
This commit is contained in:
parent
973e0f1d30
commit
f3c2544cd4
BIN
app/RecordScreen.jks
Normal file
BIN
app/RecordScreen.jks
Normal file
Binary file not shown.
@ -1,25 +1,30 @@
|
||||
import java.util.Date
|
||||
import java.text.SimpleDateFormat
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
val timestamp = SimpleDateFormat("MM_dd_HH_mm").format(Date())
|
||||
android {
|
||||
namespace = "com.audio.record.screen.test"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.audio.record.screen.test"
|
||||
applicationId = "com.audio.record.screen"
|
||||
minSdk = 24
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
setProperty(
|
||||
"archivesBaseName",
|
||||
"RecordScreen_V" + versionName + "(${versionCode})_$timestamp"
|
||||
)
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
@ -79,8 +84,8 @@ dependencies {
|
||||
|
||||
implementation("com.github.bumptech.glide:glide:4.16.0")
|
||||
|
||||
implementation(files("libs/jetified-ffmpeg-kit-full-6.0.aar"))
|
||||
implementation(files("libs/smart-exception-common-0.2.1.jar"))
|
||||
implementation(files("libs/smart-exception-java-0.2.1.jar"))
|
||||
// implementation(files("libs/jetified-ffmpeg-kit-full-6.0.aar"))
|
||||
// implementation(files("libs/smart-exception-common-0.2.1.jar"))
|
||||
// implementation(files("libs/smart-exception-java-0.2.1.jar"))
|
||||
|
||||
}
|
||||
6
app/keystore.properties
Normal file
6
app/keystore.properties
Normal file
@ -0,0 +1,6 @@
|
||||
app_name=RecordScreen
|
||||
package_name=com.audio.record.screen
|
||||
keystoreFile=app/RecordScreen.jks
|
||||
key_alias=RecordScreenkey0
|
||||
key_store_password=RecordScreen
|
||||
key_password=RecordScreen
|
||||
@ -7,10 +7,12 @@
|
||||
android:required="false" /> <!-- 通知 -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- 前台服务 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" /> <!-- 录音 -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <!-- 相机 -->
|
||||
<uses-permission android:name="android.permission.CAMERA" /> <!-- 悬浮窗 -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <!-- 录音 -->
|
||||
<uses-permission android:name="android.permission.CAMERA" /> <!-- 相机 -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /><!-- 悬浮窗 -->
|
||||
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32"
|
||||
@ -19,6 +21,14 @@
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
|
||||
|
||||
<!-- API34+ -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
|
||||
<uses-permission
|
||||
android:name="android.permission.CAPTURE_VIDEO_OUTPUT"
|
||||
tools:ignore="ProtectedPermissions" />
|
||||
|
||||
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
@ -32,8 +42,8 @@
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".activity.WelcomeActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:exported="true" >
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@ -48,28 +58,16 @@
|
||||
android:name=".activity.ImageViewActivity"
|
||||
android:exported="false"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name=".activity.PreviewActivity"
|
||||
android:exported="false"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".activity.MainActivity1"
|
||||
android:exported="false"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="portrait">
|
||||
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.MainActivity"
|
||||
android:exported="false">
|
||||
|
||||
<!-- <intent-filter> -->
|
||||
<!-- <action android:name="android.intent.action.MAIN" /> -->
|
||||
android:screenOrientation="portrait"/>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- <category android:name="android.intent.category.LAUNCHER" /> -->
|
||||
<!-- </intent-filter> -->
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.ScreenPermissionActivity"
|
||||
android:excludeFromRecents="true"
|
||||
|
||||
@ -1,286 +0,0 @@
|
||||
package com.audio.record.screen.test.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.media.MediaRecorder
|
||||
import android.media.projection.MediaProjection
|
||||
import android.media.projection.MediaProjectionManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.audio.record.screen.test.base.BaseActivity
|
||||
import com.audio.record.screen.test.databinding.ActivityMainBinding
|
||||
import com.audio.record.screen.test.service.FloatingWindowBridge
|
||||
import com.audio.record.screen.test.tool.Common
|
||||
import com.audio.record.screen.test.tool.ScreenCaptureHelper
|
||||
import com.audio.record.screen.test.tool.VideoFileHelper
|
||||
|
||||
class MainActivity : BaseActivity<ActivityMainBinding>() {
|
||||
val NOTIFICATION_PERMISSION_REQUEST_CODE = 123
|
||||
val SCREEN_CAPTURE_REQUEST_CODE = 124
|
||||
val REQUEST_SCREENSHOT = 125
|
||||
|
||||
lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
|
||||
lateinit var micPermissionLauncher: ActivityResultLauncher<String>
|
||||
lateinit var cameraPermissionLauncher: ActivityResultLauncher<String>
|
||||
lateinit var requestStoragePermission: ActivityResultLauncher<String>
|
||||
|
||||
lateinit var mediaProjectionManager: MediaProjectionManager
|
||||
// private var floatingService: ScreenRecordService? = null
|
||||
|
||||
private lateinit var tmpVideoUri:Uri
|
||||
|
||||
// private val connection = object : ServiceConnection {
|
||||
// override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||
// val binder = service as ScreenRecordService.FloatingBinder
|
||||
// floatingService = binder.getService()
|
||||
// binder.setCallback(object : FloatingCallback {
|
||||
// override fun onFloatingButtonClicked(action: String) {
|
||||
// // 处理来自悬浮窗的点击
|
||||
// when (action) {
|
||||
// "stop_clicked" -> {
|
||||
//
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// override fun onServiceDisconnected(name: ComponentName?) {
|
||||
// floatingService = null
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
override fun initBinding(): ActivityMainBinding = ActivityMainBinding.inflate(layoutInflater)
|
||||
|
||||
override fun getFullColor(): Boolean? = true
|
||||
|
||||
override fun onCreateInit() {
|
||||
initPermissionLauncher()
|
||||
// checkSyswindow(this@MainActivity)
|
||||
checkStoragePermissionAndDoSomething()
|
||||
|
||||
|
||||
binding.btn1.setOnClickListener {
|
||||
requestNotification()
|
||||
}
|
||||
binding.btn2.setOnClickListener {
|
||||
checkStoragePermissionAndDoSomething()
|
||||
}
|
||||
binding.btn3.setOnClickListener {
|
||||
micPermissionLauncher.launch(android.Manifest.permission.RECORD_AUDIO)
|
||||
}
|
||||
binding.btn4.setOnClickListener {
|
||||
checkCamera()
|
||||
}
|
||||
binding.btnShowcamera.setOnClickListener {
|
||||
it.isSelected = !it.isSelected
|
||||
if(it.isSelected){
|
||||
FloatingWindowBridge.sendCommand("show")
|
||||
}else{
|
||||
FloatingWindowBridge.sendCommand("hide")
|
||||
}
|
||||
|
||||
}
|
||||
binding.btn7.setOnClickListener {
|
||||
startActivity(Intent(this,PreviewActivity::class.java))
|
||||
|
||||
}
|
||||
|
||||
mediaProjectionManager =
|
||||
getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||
binding.btn5.setOnClickListener {
|
||||
startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), SCREEN_CAPTURE_REQUEST_CODE)
|
||||
}
|
||||
binding.btn6.setOnClickListener {
|
||||
stopRecording()
|
||||
}
|
||||
|
||||
binding.btnScreenshot.setOnClickListener {
|
||||
|
||||
startActivityForResult( mediaProjectionManager.createScreenCaptureIntent(), REQUEST_SCREENSHOT)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInitPadding(): Boolean = true
|
||||
|
||||
|
||||
private fun initPermissionLauncher() {
|
||||
requestPermissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||
if (isGranted) {
|
||||
startForegroundService()
|
||||
Common.showLog("权限授予")
|
||||
} else {
|
||||
Common.showLog("权限拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
micPermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted ->
|
||||
if (isGranted) {
|
||||
Common.showLog("mic 权限授予")
|
||||
} else {
|
||||
Common.showLog("mic 权限拒绝")
|
||||
}
|
||||
}
|
||||
cameraPermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted ->
|
||||
if (isGranted) {
|
||||
Common.showLog("CAMERA 权限授予")
|
||||
} else {
|
||||
Common.showLog("CAMERA 权限拒绝")
|
||||
}
|
||||
}
|
||||
requestStoragePermission = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted ->
|
||||
if (isGranted) {
|
||||
// 权限已授予
|
||||
Common.showLog("已获取存储权限")
|
||||
// 执行写入文件操作
|
||||
} else {
|
||||
Common.showLog( "存储权限被拒绝" )
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
fun checkStoragePermissionAndDoSomething() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q &&
|
||||
ContextCompat.checkSelfPermission(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
!= PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
requestStoragePermission.launch(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
} else {
|
||||
// Android 10+ 不需要写权限,或者权限已获取
|
||||
// 执行写入文件操作
|
||||
Common.showLog("已获取存储权限")
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkSyswindow(context: Context) {
|
||||
if (!Settings.canDrawOverlays(context)) {
|
||||
val intent = Intent(
|
||||
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||
Uri.parse("package:${context.packageName}")
|
||||
)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun requestNotification() {
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
android.Manifest.permission.POST_NOTIFICATIONS
|
||||
)
|
||||
!= PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
requestPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
|
||||
} else {
|
||||
// 权限已授予,可以发送通知
|
||||
Common.showLog("权限已授予,可以发送通知")
|
||||
startForegroundService()
|
||||
}
|
||||
} else {
|
||||
// Android 12 及以下,不需要请求权限
|
||||
Common.showLog("不需要请求权限")
|
||||
startForegroundService()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun checkCamera() {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
android.Manifest.permission.CAMERA
|
||||
)
|
||||
!= PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
cameraPermissionLauncher.launch(android.Manifest.permission.CAMERA)
|
||||
} else {
|
||||
Common.showLog("权限已授予 CAMERA")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
val mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data!!)
|
||||
if (requestCode == SCREEN_CAPTURE_REQUEST_CODE && resultCode == RESULT_OK) {
|
||||
|
||||
startRecording(mediaProjection)
|
||||
}else if (requestCode == REQUEST_SCREENSHOT && resultCode == RESULT_OK && data != null) {
|
||||
// ScreenCaptureHelper.startScreenCapture(this,mediaProjection)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startForegroundService() {
|
||||
// FloatingWindowBridge.startAndBindService(this)
|
||||
}
|
||||
|
||||
fun startRecording(mediaProjection: MediaProjection) {
|
||||
val screen = VideoFileHelper.getScreenInfo(this@MainActivity)
|
||||
val width = VideoFileHelper.alignTo16(screen.width)
|
||||
val height = VideoFileHelper.alignTo16(screen.height)
|
||||
initRecorder(width, height)
|
||||
mediaRecorder.start()
|
||||
|
||||
val virtualDisplay = mediaProjection.createVirtualDisplay(
|
||||
"ScreenRecord",
|
||||
width, height, resources.displayMetrics.densityDpi,
|
||||
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||
mediaRecorder.surface, null, null
|
||||
)
|
||||
}
|
||||
|
||||
fun stopRecording() {
|
||||
Common.showLog("-------录屏完成.....")
|
||||
mediaRecorder.stop()
|
||||
mediaRecorder.reset()
|
||||
}
|
||||
|
||||
|
||||
private lateinit var mediaRecorder: MediaRecorder
|
||||
|
||||
fun initRecorder(width: Int, height: Int) {
|
||||
|
||||
val (videoUri, pfd) = VideoFileHelper.createVideoFile(this, Common.folderName)
|
||||
tmpVideoUri = videoUri
|
||||
|
||||
mediaRecorder = MediaRecorder().apply {
|
||||
setAudioSource(MediaRecorder.AudioSource.MIC)
|
||||
setVideoSource(MediaRecorder.VideoSource.SURFACE)
|
||||
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
|
||||
setOutputFile(pfd?.fileDescriptor)
|
||||
setVideoSize(width, height)
|
||||
setVideoEncoder(MediaRecorder.VideoEncoder.H264)
|
||||
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
|
||||
setVideoEncodingBitRate(8 * 1000 * 1000)
|
||||
setVideoFrameRate(30)
|
||||
prepare()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
package com.audio.record.screen.test.activity
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
@ -13,24 +12,18 @@ import android.widget.ImageView
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.audio.record.screen.test.R
|
||||
import com.audio.record.screen.test.adapter.ViewPager2Adapter
|
||||
import com.audio.record.screen.test.base.BaseActivity
|
||||
import com.audio.record.screen.test.databinding.ActivityMain1Binding
|
||||
import com.audio.record.screen.test.dialog.DialogPermission
|
||||
import com.audio.record.screen.test.fragment.MainFragment
|
||||
import com.audio.record.screen.test.service.ConnectionListener
|
||||
import com.audio.record.screen.test.service.FloatingCallback
|
||||
import com.audio.record.screen.test.service.FloatingWindowBridge
|
||||
import com.audio.record.screen.test.tool.Common
|
||||
import com.audio.record.screen.test.tool.Extend.setMarginBottom
|
||||
import com.audio.record.screen.test.tool.Permission
|
||||
import com.audio.record.screen.test.tool.ScreenCaptureHelper
|
||||
import com.audio.record.screen.test.viewmodel.MainViewModel
|
||||
import com.audio.record.screen.test.viewmodel.PreviewViewModel
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
@ -64,6 +57,8 @@ class MainActivity1 : BaseActivity<ActivityMain1Binding>(), ConnectionListener,F
|
||||
initLauncher()
|
||||
firstCheck()
|
||||
checkStoragePermissionAndDoSomething()
|
||||
|
||||
|
||||
}
|
||||
|
||||
private fun initLauncher() {
|
||||
@ -73,6 +68,7 @@ class MainActivity1 : BaseActivity<ActivityMain1Binding>(), ConnectionListener,F
|
||||
if (isGranted) {
|
||||
// 权限已授予
|
||||
Common.showLog("已获取存储权限")
|
||||
|
||||
showPermissionDialog()
|
||||
// 执行写入文件操作
|
||||
} else {
|
||||
@ -83,14 +79,21 @@ class MainActivity1 : BaseActivity<ActivityMain1Binding>(), ConnectionListener,F
|
||||
requestNotificationLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
|
||||
if (isGranted) {
|
||||
startForegroundService()
|
||||
Common.showLog("权限授予")
|
||||
} else {
|
||||
Common.showLog("权限拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
screenCaptureLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == RESULT_OK && result.data != null) {
|
||||
val data: Intent? = result.data
|
||||
FloatingWindowBridge.updateMediaProjection(result.resultCode, data!!)
|
||||
startForegroundService()
|
||||
}
|
||||
}
|
||||
// screenCaptureLauncher = registerForActivityResult(
|
||||
// ActivityResultContracts.StartActivityForResult()
|
||||
// ) { result ->
|
||||
@ -161,9 +164,10 @@ class MainActivity1 : BaseActivity<ActivityMain1Binding>(), ConnectionListener,F
|
||||
|
||||
private fun showPermissionDialog() {
|
||||
if (isNotification && isOverlay) {
|
||||
requestScreenPermission()
|
||||
return
|
||||
}
|
||||
mPermissionDialog = mPermissionDialog ?: DialogPermission {
|
||||
mPermissionDialog = mPermissionDialog ?: DialogPermission ({
|
||||
when (it) {
|
||||
DialogPermission.type_ball -> {
|
||||
intentSysWindow(this@MainActivity1)
|
||||
@ -173,11 +177,19 @@ class MainActivity1 : BaseActivity<ActivityMain1Binding>(), ConnectionListener,F
|
||||
requestNotificationLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
}
|
||||
}){
|
||||
//消失
|
||||
requestScreenPermission()
|
||||
}
|
||||
|
||||
mPermissionDialog?.show(supportFragmentManager, "")
|
||||
}
|
||||
|
||||
|
||||
private fun requestScreenPermission() {
|
||||
val mediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||
val intent = mediaProjectionManager.createScreenCaptureIntent()
|
||||
screenCaptureLauncher.launch(intent)
|
||||
}
|
||||
//悬浮窗
|
||||
private fun intentSysWindow(context: Context) {
|
||||
val intent = Intent(
|
||||
@ -191,9 +203,7 @@ class MainActivity1 : BaseActivity<ActivityMain1Binding>(), ConnectionListener,F
|
||||
private fun firstCheck() {
|
||||
Permission.checkNotification(this@MainActivity1) {
|
||||
isNotification = it
|
||||
if (it) {
|
||||
startForegroundService()
|
||||
}
|
||||
|
||||
}
|
||||
Permission.checkOvalApp(this@MainActivity1) {
|
||||
isOverlay = it
|
||||
|
||||
@ -1,183 +0,0 @@
|
||||
package com.audio.record.screen.test.activity
|
||||
|
||||
|
||||
import android.graphics.RectF
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.PlaybackParameters
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import com.audio.record.screen.test.App
|
||||
import com.audio.record.screen.test.R
|
||||
import com.audio.record.screen.test.base.BaseActivity
|
||||
import com.audio.record.screen.test.databinding.ActivityPreviewBinding
|
||||
import com.audio.record.screen.test.tool.Common
|
||||
import com.audio.record.screen.test.tool.FFmpegKitTool
|
||||
import com.audio.record.screen.test.viewmodel.PreviewViewModel
|
||||
import java.io.File
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
|
||||
class PreviewActivity : BaseActivity<ActivityPreviewBinding>(), View.OnClickListener {
|
||||
|
||||
private lateinit var exoPlayer: ExoPlayer
|
||||
|
||||
//将当前需要处理的原视频复制到内部存储,方便操作
|
||||
private lateinit var copyFile: File
|
||||
|
||||
private var copyResult by Delegates.notNull<Boolean>()
|
||||
private lateinit var viewModel: PreviewViewModel
|
||||
override fun initBinding(): ActivityPreviewBinding =
|
||||
ActivityPreviewBinding.inflate(layoutInflater)
|
||||
|
||||
|
||||
override fun getFullColor(): Boolean = true
|
||||
override fun onInitPadding(): Boolean = true
|
||||
override fun onCreateInit() {
|
||||
initPlay()
|
||||
viewModel = ViewModelProvider(this)[PreviewViewModel::class.java]
|
||||
binding.imPlay.setOnClickListener {
|
||||
if (!binding.imPlay.isSelected) {
|
||||
exoPlayer.play()
|
||||
} else {
|
||||
exoPlayer.pause()
|
||||
}
|
||||
binding.imPlay.isSelected = !binding.imPlay.isSelected
|
||||
}
|
||||
|
||||
val thumbDir = File(App.instanceApp.cacheDir, "thumb").apply {
|
||||
if (!exists()) {
|
||||
mkdir()
|
||||
} else {
|
||||
Common.deleteAllFilesInDirectory(this.absolutePath)
|
||||
}
|
||||
}
|
||||
// val thumbAdapter = ThumbAdapter(this@PreviewActivity)
|
||||
// binding.thumbRecycler.apply {
|
||||
// layoutManager =
|
||||
// LinearLayoutManager(this@PreviewActivity, RecyclerView.HORIZONTAL, false)
|
||||
// adapter = thumbAdapter
|
||||
// }
|
||||
val open = App.instanceApp.assets.open("record_1748398994963.mp4")
|
||||
copyFile = File(App.instanceApp.cacheDir, "temp_video.mp4")
|
||||
//原视频复制到内部存储
|
||||
FFmpegKitTool.copy(copyFile, open, thumbDir.absolutePath) {
|
||||
Common.showLog("--------copy success")
|
||||
copyResult = it
|
||||
}
|
||||
|
||||
|
||||
initListener()
|
||||
viewModel.cropRatioText.observe(this) {
|
||||
if (it.equals("原始")) {
|
||||
val videoRatio = Common.getVideoRatio(copyFile.absolutePath)
|
||||
binding.cropView.setAspectRatio(videoRatio)
|
||||
} else {
|
||||
val split = it.split(":")
|
||||
binding.cropView.setAspectRatio(split[0].toFloat() / split[1].toFloat())
|
||||
}
|
||||
}
|
||||
viewModel.saveCrop.observe(this){
|
||||
|
||||
val cropFile = File(App.instanceApp.cacheDir, "crop_video_${System.currentTimeMillis()}.mp4")
|
||||
val videoWH = Common.getVideoWH(copyFile.absolutePath)
|
||||
|
||||
val rawCropRect = binding.cropView.getCropRectInVideoCoords(videoWH.first ,
|
||||
videoWH.second
|
||||
)
|
||||
|
||||
val x = rawCropRect?.left?.toInt() ?: 0
|
||||
val y = rawCropRect?.top?.toInt() ?: 0
|
||||
val w = rawCropRect?.width()?.toInt() ?: 0
|
||||
val h = rawCropRect?.height()?.toInt() ?: 0
|
||||
|
||||
Common.showLog("-------videoWH w=${videoWH.first} h=${videoWH.second} x=${x} y = $y w= $w h= $h")
|
||||
FFmpegKitTool.cropVideo(copyFile.absolutePath,cropFile.absolutePath,x,y,w,h)
|
||||
|
||||
|
||||
}
|
||||
viewModel.changeSpeed.observe(this){
|
||||
exoPlayer.playbackParameters = PlaybackParameters(it, it)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun initListener() {
|
||||
val navHostFragment = supportFragmentManager
|
||||
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
||||
val navController = navHostFragment.navController
|
||||
navController.addOnDestinationChangedListener { controller, destination, arguments ->
|
||||
Common.showLog("NavControllerListener 当前的目的地: id=${destination.id} name=${destination.displayName} label=${destination.label}")
|
||||
binding.cropView.isVisible = false
|
||||
when (destination.id) {
|
||||
R.id.fragmentCut -> {
|
||||
viewModel.updateCopyResult(Pair(copyResult, copyFile.absolutePath))
|
||||
Common.showLog("----fragmentCut")
|
||||
}
|
||||
|
||||
R.id.fragmentCropping -> {
|
||||
Common.showLog("----fragmentCropping")
|
||||
initCropView()
|
||||
|
||||
}
|
||||
|
||||
R.id.fragmentVolume -> {
|
||||
Common.showLog("----fragmentVolume")
|
||||
viewModel.updateCopyResult(Pair(copyResult, copyFile.absolutePath))
|
||||
}
|
||||
|
||||
R.id.fragmentSpeed -> {
|
||||
Common.showLog("----fragmentSpeed")
|
||||
viewModel.updateCopyResult(Pair(copyResult, copyFile.absolutePath))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun initCropView() {
|
||||
binding.cropView.isVisible = true
|
||||
// 延迟执行以确保布局完成
|
||||
binding.playerView.post {
|
||||
val contentFrame: View =
|
||||
binding.playerView.findViewById(androidx.media3.ui.R.id.exo_content_frame)
|
||||
if (contentFrame != null) {
|
||||
// 获取在 cropView 坐标系中的显示区域
|
||||
val videoRect = RectF()
|
||||
val contentLoc = IntArray(2)
|
||||
val cropLoc = IntArray(2)
|
||||
contentFrame.getLocationOnScreen(contentLoc)
|
||||
binding.cropView.getLocationOnScreen(cropLoc)
|
||||
val offsetX = (contentLoc[0] - cropLoc[0]).toFloat()
|
||||
val offsetY = (contentLoc[1] - cropLoc[1]).toFloat()
|
||||
videoRect.left = offsetX
|
||||
videoRect.top = offsetY
|
||||
videoRect.right = offsetX + contentFrame.width
|
||||
videoRect.bottom = offsetY + contentFrame.height
|
||||
// 设置裁剪区域限制边界
|
||||
binding.cropView.setVideoDisplayBounds(videoRect)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun initPlay() {
|
||||
val uri = Uri.parse("asset:///record_1748398994963.mp4")
|
||||
exoPlayer = ExoPlayer.Builder(this).build()
|
||||
|
||||
binding.playerView.player = exoPlayer
|
||||
val mediaItem = MediaItem.fromUri(uri)
|
||||
exoPlayer.setMediaItem(mediaItem)
|
||||
exoPlayer.prepare()
|
||||
// exoPlayer.playWhenReady = true
|
||||
}
|
||||
|
||||
override fun onClick(p0: View?) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -2,12 +2,18 @@ package com.audio.record.screen.test.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.media.projection.MediaProjectionManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.PersistableBundle
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsController
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.audio.record.screen.test.base.BaseActivity
|
||||
import com.audio.record.screen.test.databinding.ActivityPlayBinding
|
||||
import com.audio.record.screen.test.service.FloatingWindowBridge
|
||||
@ -29,14 +35,46 @@ class ScreenPermissionActivity : AppCompatActivity() {
|
||||
}
|
||||
private var withAudio = false
|
||||
|
||||
private lateinit var mediaProjectionManager:MediaProjectionManager
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
if (hasFocus) {
|
||||
// Common.hideSystemBars(this)
|
||||
// Common.setStatusBarTextColor(this, false)
|
||||
setFullScreenTransparent()
|
||||
}
|
||||
}
|
||||
private fun setFullScreenTransparent() {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.insetsController?.let {
|
||||
it.hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
|
||||
it.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
window.decorView.systemUiVisibility =
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
}
|
||||
|
||||
window.statusBarColor = Color.TRANSPARENT
|
||||
window.navigationBarColor = Color.TRANSPARENT
|
||||
}
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Common.setStatusBarTextColor(this, true)
|
||||
// Common.setStatusBarTextColor(this, true)
|
||||
mediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||
withAudio = intent.getBooleanExtra(key_with_audio, false)
|
||||
screenCaptureLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
Common.showLog("--------------screenCaptureLauncher")
|
||||
if (result.resultCode == RESULT_OK && result.data != null) {
|
||||
val data: Intent? = result.data
|
||||
FloatingWindowBridge.updateMediaProjection(result.resultCode, data!!)
|
||||
@ -63,7 +101,7 @@ class ScreenPermissionActivity : AppCompatActivity() {
|
||||
|
||||
|
||||
private fun requestScreenPermission() {
|
||||
val mediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||
Common.showLog("--------------requestScreenPermission")
|
||||
val intent = mediaProjectionManager.createScreenCaptureIntent()
|
||||
screenCaptureLauncher.launch(intent)
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.audio.record.screen.test.dialog
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Bundle
|
||||
@ -11,7 +12,7 @@ import com.audio.record.screen.test.databinding.DialogPermissionBinding
|
||||
import com.audio.record.screen.test.tool.Permission
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
|
||||
class DialogPermission(private var mClickType: (type: Int) -> Unit) : BottomSheetDialogFragment() {
|
||||
class DialogPermission(private var mClickType: (type: Int) -> Unit,private var onDismiss:()->Unit) : BottomSheetDialogFragment() {
|
||||
private lateinit var vb: DialogPermissionBinding
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@ -28,6 +29,7 @@ class DialogPermission(private var mClickType: (type: Int) -> Unit) : BottomShee
|
||||
val dialog = dialog
|
||||
if (dialog != null) {
|
||||
dialog.setCanceledOnTouchOutside(false)
|
||||
dialog.setCancelable(false)
|
||||
val window = dialog.window
|
||||
if (window != null) {
|
||||
window.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
@ -39,6 +41,11 @@ class DialogPermission(private var mClickType: (type: Int) -> Unit) : BottomShee
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
onDismiss.invoke()
|
||||
}
|
||||
|
||||
private fun init() {
|
||||
vb.run {
|
||||
|
||||
|
||||
@ -1,76 +0,0 @@
|
||||
package com.audio.record.screen.test.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.audio.record.screen.test.R
|
||||
import com.audio.record.screen.test.adapter.CropAdapter
|
||||
import com.audio.record.screen.test.base.BaseFragment
|
||||
import com.audio.record.screen.test.databinding.FragmentCropBinding
|
||||
import com.audio.record.screen.test.viewmodel.PreviewViewModel
|
||||
|
||||
class CropFragment : BaseFragment<FragmentCropBinding>(), View.OnClickListener {
|
||||
|
||||
|
||||
private lateinit var navController: NavController
|
||||
private lateinit var viewModel: PreviewViewModel
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance() =
|
||||
CropFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun initBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentCropBinding =
|
||||
FragmentCropBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
navController = findNavController()
|
||||
|
||||
viewModel = ViewModelProvider(requireActivity())[PreviewViewModel::class.java]
|
||||
binding.recycler.run {
|
||||
val stringList = resources.getStringArray(R.array.crop_text).toList()
|
||||
adapter = CropAdapter(requireContext()) {
|
||||
viewModel.updateCropText(it)
|
||||
|
||||
}.apply { updateData(stringList) }
|
||||
layoutManager = LinearLayoutManager(requireContext(), RecyclerView.HORIZONTAL, false)
|
||||
}
|
||||
initClick()
|
||||
}
|
||||
|
||||
private fun initClick() {
|
||||
binding.imClose.setOnClickListener(this)
|
||||
binding.imSave.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onClick(v: View?) {
|
||||
v?.let {
|
||||
if (it == binding.imSave) {
|
||||
viewModel.updateClickCropSave(true)
|
||||
} else if (it == binding.imClose) {
|
||||
navController.navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,148 +0,0 @@
|
||||
package com.audio.record.screen.test.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.audio.record.screen.test.App
|
||||
import com.audio.record.screen.test.adapter.ThumbAdapter
|
||||
import com.audio.record.screen.test.base.BaseFragment
|
||||
import com.audio.record.screen.test.databinding.FragmentCutBinding
|
||||
import com.audio.record.screen.test.tool.Common
|
||||
import com.audio.record.screen.test.tool.FFmpegKitTool
|
||||
import com.audio.record.screen.test.viewmodel.PreviewViewModel
|
||||
import com.jaygoo.widget.OnRangeChangedListener
|
||||
import com.jaygoo.widget.RangeSeekBar
|
||||
import java.io.File
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class CutFragment : BaseFragment<FragmentCutBinding>() ,View.OnClickListener{
|
||||
//毫秒单位
|
||||
private var leftV by Delegates.notNull<Float>()
|
||||
private lateinit var viewModel: PreviewViewModel
|
||||
private var rightV by Delegates.notNull<Float>()
|
||||
private var param1: String? = null
|
||||
private var param2: String? = null
|
||||
private lateinit var copyFilePath: String
|
||||
private lateinit var navController:NavController
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance() =
|
||||
CutFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
// putString(ARG_PARAM1, param1)
|
||||
// putString(ARG_PARAM2, param2)
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
// param1 = it.getString(ARG_PARAM1)
|
||||
// param2 = it.getString(ARG_PARAM2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun initBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentCutBinding =
|
||||
FragmentCutBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
navController = findNavController()
|
||||
viewModel = ViewModelProvider(requireActivity())[PreviewViewModel::class.java]
|
||||
initThumb()
|
||||
initClick()
|
||||
}
|
||||
|
||||
|
||||
private fun initThumb(){
|
||||
val thumbDir = File(App.instanceApp.cacheDir, "thumb")
|
||||
val thumbAdapter = ThumbAdapter(requireContext())
|
||||
binding.thumbRecycler.apply {
|
||||
layoutManager =
|
||||
LinearLayoutManager(requireContext(), RecyclerView.HORIZONTAL, false)
|
||||
adapter = thumbAdapter
|
||||
}
|
||||
// val open = App.instanceApp.assets.open("temp_video.mp4")
|
||||
// tempFile = File(App.instanceApp.cacheDir, "temp_video.mp4")
|
||||
viewModel.copySuccess.observe(requireActivity()){
|
||||
if (it.first) {
|
||||
Common.showLog("-------11111111111111")
|
||||
copyFilePath = it.second
|
||||
val allImagePaths = Common.getNaturallySortedThumbFiles(thumbDir.absolutePath)
|
||||
thumbAdapter.updateData(allImagePaths)
|
||||
initSeekBar()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
private fun initClick() {
|
||||
binding.imClose.setOnClickListener(this)
|
||||
binding.imSave.setOnClickListener(this)
|
||||
}
|
||||
private fun initSeekBar() {
|
||||
val durationMs = Common.getVideoDurationMs(copyFilePath)
|
||||
|
||||
// val millisToSeconds = Common.millisToSeconds(durationMs)
|
||||
Common.showLog("----durationMs=${durationMs} ")
|
||||
binding.rangeSlider.run {
|
||||
durationMs.toFloat().let {
|
||||
setRange(0f, it)
|
||||
leftSeekBar?.setIndicatorText(Common.formatSeconds(0f))
|
||||
rightSeekBar?.setIndicatorText(Common.formatSeconds(it))
|
||||
setProgress(0f, it)
|
||||
|
||||
}
|
||||
|
||||
|
||||
setOnRangeChangedListener(object : OnRangeChangedListener {
|
||||
override fun onRangeChanged(
|
||||
view: RangeSeekBar?,
|
||||
leftValue: Float,
|
||||
rightValue: Float,
|
||||
isFromUser: Boolean
|
||||
) {
|
||||
view?.leftSeekBar?.setIndicatorText(Common.formatSeconds(leftValue))
|
||||
view?.rightSeekBar?.setIndicatorText(Common.formatSeconds(rightValue))
|
||||
leftV = leftValue
|
||||
rightV = rightValue
|
||||
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(view: RangeSeekBar?, isLeft: Boolean) {
|
||||
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(view: RangeSeekBar?, isLeft: Boolean) {
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(p0: View?) {
|
||||
p0?.let {
|
||||
if (it == binding.imSave) {
|
||||
trimVideoFile()
|
||||
}else if(it == binding.imClose){
|
||||
navController.navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
fun trimVideoFile() {
|
||||
val resultFile = File(requireContext().cacheDir, "test_${System.currentTimeMillis()}.mp4")
|
||||
val left = leftV.toString()
|
||||
val right = rightV.toString()
|
||||
Common.showLog("------left=${left} right=${right}")
|
||||
// FFmpegKitTool.trimVideo(tempFile.absolutePath, resultFile.absolutePath, left, right)
|
||||
FFmpegKitTool.cropVideoWithFFmpeg(copyFilePath, resultFile.absolutePath,leftV,rightV)
|
||||
}
|
||||
}
|
||||
@ -1,105 +0,0 @@
|
||||
package com.audio.record.screen.test.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.SeekBar
|
||||
import android.widget.SeekBar.OnSeekBarChangeListener
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.audio.record.screen.test.App
|
||||
import com.audio.record.screen.test.base.BaseFragment
|
||||
import com.audio.record.screen.test.databinding.FragmentSpeedBinding
|
||||
import com.audio.record.screen.test.tool.Common
|
||||
import com.audio.record.screen.test.tool.FFmpegKitTool
|
||||
import com.audio.record.screen.test.viewmodel.PreviewViewModel
|
||||
import java.io.File
|
||||
|
||||
class SpeedFragment : BaseFragment<FragmentSpeedBinding>(), View.OnClickListener {
|
||||
|
||||
|
||||
private lateinit var viewModel: PreviewViewModel
|
||||
private lateinit var navController: NavController
|
||||
private lateinit var copyFilePath: String
|
||||
private var speed = 1f
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance() =
|
||||
SpeedFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
// putString(ARG_PARAM1, param1)
|
||||
// putString(ARG_PARAM2, param2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
// param1 = it.getString(ARG_PARAM1)
|
||||
// param2 = it.getString(ARG_PARAM2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun initBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
): FragmentSpeedBinding =
|
||||
FragmentSpeedBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel = ViewModelProvider(requireActivity())[PreviewViewModel::class.java]
|
||||
navController = findNavController()
|
||||
|
||||
initClick()
|
||||
viewModel.copySuccess.observe(requireActivity()){
|
||||
if (it.first) {
|
||||
Common.showLog("-------11111111111111")
|
||||
copyFilePath = it.second
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun initClick() {
|
||||
binding.imClose.setOnClickListener(this)
|
||||
binding.imSave.setOnClickListener(this)
|
||||
binding.seekbar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||
speed = 0.5f + (progress / 150f) * 1.5f
|
||||
binding.progressText.text = "${speed}X"
|
||||
viewModel.updateSpeed(speed)
|
||||
Common.showLog("--------progress=${progress} speed=${speed}")
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar?) {
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
override fun onClick(p0: View?) {
|
||||
p0?.let {
|
||||
if (it == binding.imSave) {
|
||||
val volumeFile = File(App.instanceApp.cacheDir, "speed_video_${System.currentTimeMillis()}.mp4")
|
||||
FFmpegKitTool.buildSpeedCommand(copyFilePath,volumeFile.absolutePath,speed)
|
||||
} else if (it == binding.imClose) {
|
||||
navController.navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,102 +0,0 @@
|
||||
package com.audio.record.screen.test.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.SeekBar
|
||||
import android.widget.SeekBar.OnSeekBarChangeListener
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.audio.record.screen.test.App
|
||||
import com.audio.record.screen.test.base.BaseFragment
|
||||
import com.audio.record.screen.test.databinding.FragmentVolumeBinding
|
||||
import com.audio.record.screen.test.tool.Common
|
||||
import com.audio.record.screen.test.tool.FFmpegKitTool
|
||||
import com.audio.record.screen.test.viewmodel.PreviewViewModel
|
||||
import java.io.File
|
||||
|
||||
class VolumeFragment : BaseFragment<FragmentVolumeBinding>(), View.OnClickListener {
|
||||
|
||||
|
||||
private lateinit var viewModel: PreviewViewModel
|
||||
private lateinit var navController: NavController
|
||||
private lateinit var copyFilePath: String
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance() =
|
||||
VolumeFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
// putString(ARG_PARAM1, param1)
|
||||
// putString(ARG_PARAM2, param2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
arguments?.let {
|
||||
// param1 = it.getString(ARG_PARAM1)
|
||||
// param2 = it.getString(ARG_PARAM2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun initBinding(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?
|
||||
): FragmentVolumeBinding =
|
||||
FragmentVolumeBinding.inflate(inflater, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel = ViewModelProvider(requireActivity())[PreviewViewModel::class.java]
|
||||
navController = findNavController()
|
||||
|
||||
initClick()
|
||||
viewModel.copySuccess.observe(requireActivity()){
|
||||
if (it.first) {
|
||||
Common.showLog("-------11111111111111")
|
||||
copyFilePath = it.second
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun initClick() {
|
||||
binding.imClose.setOnClickListener(this)
|
||||
binding.imSave.setOnClickListener(this)
|
||||
binding.seekbar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||
binding.progressText.text = "${progress}%"
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar?) {
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
override fun onClick(p0: View?) {
|
||||
p0?.let {
|
||||
if (it == binding.imSave) {
|
||||
val volumeFile = File(App.instanceApp.cacheDir, "volume_video_${System.currentTimeMillis()}.mp4")
|
||||
val volume = (binding.seekbar.progress.coerceIn(0, 200)) / 100.0f
|
||||
FFmpegKitTool.setVideVolume(copyFilePath,volumeFile.absolutePath,volume)
|
||||
|
||||
} else if (it == binding.imClose) {
|
||||
navController.navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -197,10 +197,12 @@ class RecordNormalFragment : BaseFragment<FragmentRecordNormalBinding>(), Floati
|
||||
}
|
||||
|
||||
private fun setScreenshot(boolean: Boolean) {
|
||||
Common.showLog("----------setScreenshot=${boolean}")
|
||||
if (boolean) {
|
||||
requestRecordPermission(ConstValue.type_show_screenshot){
|
||||
// requestRecordPermission(ConstValue.type_show_screenshot){
|
||||
//// FloatingWindowBridge.sendCommand(FloatingWindowBridge.COMMEND_show_screenshot)
|
||||
// }
|
||||
FloatingWindowBridge.sendCommand(FloatingWindowBridge.COMMEND_show_screenshot)
|
||||
}
|
||||
|
||||
} else {
|
||||
FloatingWindowBridge.sendCommand(FloatingWindowBridge.COMMEND_hide_screenshot)
|
||||
@ -217,7 +219,7 @@ class RecordNormalFragment : BaseFragment<FragmentRecordNormalBinding>(), Floati
|
||||
|
||||
private fun startRecorder() {
|
||||
requestRecordPermission( ConstValue.type_record){
|
||||
startCountDown()
|
||||
// startCountDown()
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,13 +229,9 @@ class RecordNormalFragment : BaseFragment<FragmentRecordNormalBinding>(), Floati
|
||||
*/
|
||||
private fun requestRecordPermission(type:Int,check:(()->Unit)? = null){
|
||||
// TODO: requestRecordPermission
|
||||
if (FloatingWindowBridge.getMediaProjection() == null) {
|
||||
val captureIntent = mediaProjectionManager.createScreenCaptureIntent()
|
||||
pendingType = type
|
||||
recorderLauncher.launch(captureIntent)
|
||||
}else{
|
||||
check?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -108,6 +108,7 @@ class RecordingFragment : BaseFragment<FragmentRecordingBinding>(), FloatingCall
|
||||
|
||||
override fun onUpdateRecordTime(time: String) {
|
||||
super.onUpdateRecordTime(time)
|
||||
Common.showLog("=======onUpdateRecordTime 时间更新")
|
||||
binding.tvTimer.text = time
|
||||
}
|
||||
|
||||
|
||||
@ -146,7 +146,7 @@ class ScreenRecordService : Service() {
|
||||
|
||||
private val timeUpdateRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
|
||||
Common.showLog("=======timeUpdateRunnable 时间更新 isPause=${isPause}")
|
||||
val currentRecordingTimeInMillis =
|
||||
System.currentTimeMillis() - recordingStartTime - totalPausedTime
|
||||
val elapsed = currentRecordingTimeInMillis / 1000
|
||||
@ -156,9 +156,11 @@ class ScreenRecordService : Service() {
|
||||
callbacks.forEach {
|
||||
it.get()?.onUpdateRecordTime(onRecordingTimeChanged)
|
||||
}
|
||||
if (!isPause)
|
||||
if (!isPause){
|
||||
mRecorderHandler.postDelayed(this, updateInterval)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
inner class FloatingBinder : Binder() {
|
||||
@ -277,8 +279,9 @@ class ScreenRecordService : Service() {
|
||||
*/
|
||||
private fun startScreenshot(showView: Boolean) {
|
||||
// TODO: 开始截屏
|
||||
mIntent?.let { intent ->
|
||||
mediaProjectionManager.getMediaProjection(mCode, intent)?.let { mediaProjection ->
|
||||
|
||||
createMediaProject {
|
||||
it?.let { mediaProjection->
|
||||
hideScreenshot()
|
||||
val intent = Intent(this, ScreenshotAnimActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
@ -291,8 +294,10 @@ class ScreenRecordService : Service() {
|
||||
if (showView)
|
||||
showScreenshot()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@ -482,6 +487,7 @@ class ScreenRecordService : Service() {
|
||||
|
||||
}
|
||||
layoutParamsBallExpand.y = aY
|
||||
updateBall()
|
||||
windowManager.addView(ballViewExpand, layoutParamsBallExpand)
|
||||
isBallExpandViewAdded = true
|
||||
delayRemove {
|
||||
@ -529,7 +535,8 @@ class ScreenRecordService : Service() {
|
||||
windowManager,
|
||||
true
|
||||
) {
|
||||
startScreenshot(true)
|
||||
ballType = ConstValue.type_screenshot
|
||||
intentPermission(false)
|
||||
}
|
||||
}
|
||||
if (!isScreenshotViewAdded) {
|
||||
@ -662,10 +669,6 @@ class ScreenRecordService : Service() {
|
||||
//倒计时结束后,开启录制
|
||||
Common.showLog("Service--- 倒计时结束")
|
||||
hideCountDown()
|
||||
mIntent?.let { intent ->
|
||||
mediaProjectionManager.getMediaProjection(mCode, intent)
|
||||
?.let { media ->
|
||||
mediaProjection = media
|
||||
startRecording()
|
||||
callbacks.forEach {
|
||||
it.get()?.onStartRecording()
|
||||
@ -673,10 +676,18 @@ class ScreenRecordService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun createMediaProject(onAction: (med: MediaProjection?) -> Unit) {
|
||||
mIntent?.let { intent ->
|
||||
mediaProjectionManager.getMediaProjection(mCode, intent)
|
||||
?.let { media ->
|
||||
mediaProjection = media
|
||||
onAction.invoke(media)
|
||||
} ?: onAction.invoke(null)
|
||||
} ?: onAction.invoke(null)
|
||||
}
|
||||
|
||||
private fun animateNext(
|
||||
@ -741,6 +752,7 @@ class ScreenRecordService : Service() {
|
||||
totalPausedTime = 0L
|
||||
mRecorderHandler.post(timeUpdateRunnable)
|
||||
|
||||
createMediaProject{ curmed->
|
||||
mediaProjection?.let {
|
||||
it.registerCallback(object : MediaProjection.Callback() {
|
||||
override fun onStop() {
|
||||
@ -774,6 +786,8 @@ class ScreenRecordService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private fun releaseAll() {
|
||||
if (::mediaRecorder.isInitialized) {
|
||||
@ -922,7 +936,11 @@ class ScreenRecordService : Service() {
|
||||
|
||||
fun getViewStatus() {
|
||||
callbacks.forEach {
|
||||
it.get()?.onRefreshViewShow(isWebcamViewAdded, isScreenshotViewAdded, isBallViewAdded||isBallExpandViewAdded)
|
||||
it.get()?.onRefreshViewShow(
|
||||
isWebcamViewAdded,
|
||||
isScreenshotViewAdded,
|
||||
isBallViewAdded || isBallExpandViewAdded
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.audio.record.screen.test.tool
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@ -17,6 +18,8 @@ import android.view.KeyCharacterMap
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.ViewConfiguration
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsController
|
||||
import android.view.WindowManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
@ -40,26 +43,6 @@ object Common {
|
||||
}
|
||||
|
||||
fun setStatusBarTextColor(activity: Activity, dark: Boolean) {
|
||||
// val window = activity.window
|
||||
// val decor = window.decorView
|
||||
//
|
||||
// // 设置状态栏图标颜色(深色图标表示浅色背景)
|
||||
// var flags = decor.systemUiVisibility
|
||||
// flags = if (dark) {
|
||||
// flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
|
||||
// } else {
|
||||
// flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
|
||||
// }
|
||||
// // 保持布局全屏
|
||||
// flags = flags or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
// decor.systemUiVisibility = flags
|
||||
//
|
||||
// // 去除 TRANSLUCENT_STATUS,使用透明背景更现代
|
||||
// window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
|
||||
// window.statusBarColor = Color.TRANSPARENT
|
||||
|
||||
|
||||
|
||||
val window = activity.window
|
||||
val decor = window.decorView
|
||||
|
||||
@ -428,4 +411,30 @@ object Common {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
fun hideSystemBars(activity: Activity) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
// Android 11+
|
||||
activity.window.setDecorFitsSystemWindows(false)
|
||||
val controller = activity.window.insetsController
|
||||
controller?.let {
|
||||
it.hide(WindowInsets.Type.navigationBars() or WindowInsets.Type.statusBars())
|
||||
it.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
} else {
|
||||
// Android 7 - 10
|
||||
@Suppress("DEPRECATION")
|
||||
activity.window.decorView.systemUiVisibility = (
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -1,224 +0,0 @@
|
||||
package com.audio.record.screen.test.tool
|
||||
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import com.arthenica.ffmpegkit.FFmpegKit
|
||||
import com.arthenica.ffmpegkit.ReturnCode
|
||||
import com.audio.record.screen.test.App
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object FFmpegKitTool {
|
||||
|
||||
val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
fun copy(
|
||||
tempFile: File,
|
||||
inputStream: InputStream,
|
||||
thumbDir: String,
|
||||
result: (ok: Boolean) -> Unit
|
||||
) {
|
||||
|
||||
FileOutputStream(tempFile).use { outputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
outputStream.flush()
|
||||
extractThumbnailsFFmpeg(tempFile.absolutePath, thumbDir, result)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun createThumb(inputPath: String, outputDir: String, result: (ok: Boolean) -> Unit) {
|
||||
// val inputPath = "/storage/emulated/0/Movies/sample.mp4"
|
||||
// val outputDir = "/storage/emulated/0/Movies/thumbs" // 请确保已创建
|
||||
val cmd = "-i $inputPath -vf fps=10 $outputDir/thumb_%04d.jpg"
|
||||
|
||||
FFmpegKit.executeAsync(cmd) { session ->
|
||||
val returnCode = session.returnCode
|
||||
if (ReturnCode.isSuccess(returnCode)) {
|
||||
Common.showLog("FFmpegKit 缩略图生成成功 ${Thread.currentThread().name}")
|
||||
result.invoke(true)
|
||||
} else {
|
||||
result.invoke(false)
|
||||
Common.showLog("FFmpegKit 缩略图生成失败:${session.failStackTrace}")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 截取视频缩略图,均分6张图
|
||||
*/
|
||||
fun extractThumbnailsFFmpeg(
|
||||
videoPath: String,
|
||||
outputDir: String,
|
||||
result: (ok: Boolean) -> Unit
|
||||
) {
|
||||
var count = 0
|
||||
val durationMs = Common.getVideoDurationMs(videoPath)
|
||||
val durationSec = durationMs / 1000.0
|
||||
val step = durationSec / 7 // 均分6帧,跳过首尾
|
||||
Common.showLog("durationSec $durationSec step $step")
|
||||
for (i in 1..6) {
|
||||
val timestamp = i * step
|
||||
val outputPath = "$outputDir/thumb_$i.jpg"
|
||||
Common.showLog("i = $i timestamp $timestamp ")
|
||||
val cmd = "-ss $timestamp -i \"$videoPath\" -frames:v 1 -q:v 2 \"$outputPath\""
|
||||
FFmpegKit.executeAsync(cmd) { session ->
|
||||
val returnCode = session.returnCode
|
||||
count++
|
||||
if (count == 6) {
|
||||
mainHandler.post {
|
||||
if (returnCode.isValueSuccess) {
|
||||
result.invoke(true)
|
||||
Common.showLog("FFmpeg Frame $i saved to $outputPath")
|
||||
} else {
|
||||
result.invoke(false)
|
||||
Common.showLog("FFmpeg Failed to extract frame $i")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 剪切视频时长
|
||||
* @param startTime
|
||||
*/
|
||||
fun cropVideoWithFFmpeg(
|
||||
inputPath: String,
|
||||
outputPath: String,
|
||||
startMs: Float,
|
||||
endMs: Float
|
||||
) {
|
||||
val durationMs = endMs - startMs
|
||||
if (durationMs <= 0) {
|
||||
println("结束时间必须大于开始时间")
|
||||
return
|
||||
}
|
||||
val formatFloatTime = formatFloatTime(durationMs)
|
||||
val startTime = formatFloatTime(startMs)
|
||||
val endTime = formatFloatTime(endMs)
|
||||
val command =
|
||||
"-ss $startTime -i \"$inputPath\" -to $formatFloatTime -c:v copy -c:a copy \"$outputPath\""
|
||||
Common.showLog("--command=${command}")
|
||||
|
||||
FFmpegKit.executeAsync(command) { session ->
|
||||
val returnCode = session.returnCode
|
||||
if (returnCode.isValueSuccess) {
|
||||
Common.showLog("裁剪成功: $outputPath")
|
||||
} else {
|
||||
Common.showLog("裁剪失败: ${session.failStackTrace}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 毫秒转为 ffmpeg 时间格式:hh:mm:ss.S
|
||||
fun formatFloatTime(ms: Float): String {
|
||||
val totalSeconds = ms / 1000f
|
||||
val hours = (totalSeconds / 3600).toInt()
|
||||
val minutes = ((totalSeconds % 3600) / 60).toInt()
|
||||
val seconds = (totalSeconds % 60).toInt()
|
||||
val tenths = ((totalSeconds % 1) * 10).toInt() // 保留一位小数
|
||||
|
||||
return String.format("%02d:%02d:%02d.%01d", hours, minutes, seconds, tenths)
|
||||
}
|
||||
//
|
||||
// fun trimVideo(inputPath: String, outputPath: String, startTime: String, endTime: String) {
|
||||
// val command = "-ss $startTime -i $inputPath -to $endTime -c copy $outputPath"
|
||||
//
|
||||
// Common.showLog("--command=${command}")
|
||||
//
|
||||
// FFmpegKit.executeAsync(command) { session ->
|
||||
// when {
|
||||
// ReturnCode.isSuccess(session.returnCode) -> {
|
||||
// Log.d("FFmpegKit", "剪切成功!输出路径: $outputPath")
|
||||
// }
|
||||
//
|
||||
// session.returnCode.value == ReturnCode.CANCEL -> {
|
||||
// Log.w("FFmpegKit", "用户取消操作")
|
||||
// }
|
||||
//
|
||||
// else -> {
|
||||
// Log.e("FFmpegKit", "剪切失败. 错误日志: ${session.output}")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
/**
|
||||
* 裁切视频显示内容尺寸
|
||||
*/
|
||||
fun cropVideo(
|
||||
inputPath: String,
|
||||
outputPath: String,
|
||||
cropX: Int,
|
||||
cropY: Int,
|
||||
cropWidth: Int,
|
||||
cropHeight: Int
|
||||
) {
|
||||
// val cmd = "-i $inputPath -vf crop=$cropWidth:$cropHeight:$cropX:$cropY -c:a copy $outputPath"
|
||||
val cmd =
|
||||
"-i $inputPath -vf crop=$cropWidth:$cropHeight:$cropX:$cropY -c:v mpeg4 -qscale:v 2 -c:a copy $outputPath"
|
||||
|
||||
Common.showLog("FFmpeg Crop 比例裁剪 $cmd")
|
||||
FFmpegKit.executeAsync(cmd) {
|
||||
if (ReturnCode.isSuccess(it.returnCode)) {
|
||||
Common.showLog("FFmpeg Crop 比例裁剪成功 $outputPath")
|
||||
} else {
|
||||
Common.showLog("FFmpeg Crop 比例裁剪失败: $outputPath ${it.failStackTrace}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 调整视频音量
|
||||
*/
|
||||
fun setVideVolume(
|
||||
inputPath: String,
|
||||
outputPath: String, volumeLevel: Float
|
||||
) {
|
||||
// val volumeLevel = 1.5 // 音量倍数(例如 1.5 表示音量加大 50%)
|
||||
val cmd = "-y -i $inputPath -filter:a volume=$volumeLevel -c:v copy $outputPath"
|
||||
Common.showLog("FFmpeg 音量 $cmd")
|
||||
FFmpegKit.executeAsync(cmd) { session ->
|
||||
val returnCode = session.returnCode
|
||||
if (ReturnCode.isSuccess(returnCode)) {
|
||||
Common.showLog("FFmpeg 音量调节成功")
|
||||
} else {
|
||||
Common.showLog("FFmpeg 音量调节失败: ${session.failStackTrace}")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整视频播放速度
|
||||
*/
|
||||
fun buildSpeedCommand(inputPath: String, outputPath: String, speed: Float) {
|
||||
val videoFilter = "setpts=${1 / speed}*PTS"
|
||||
val audioFilter = "atempo=$speed"
|
||||
val cmd = "-y -i $inputPath -filter_complex \"[0:v]$videoFilter[v];[0:a]$audioFilter[a]\" -map \"[v]\" -map \"[a]\" -preset ultrafast $outputPath"
|
||||
Common.showLog("FFmpeg 播放速度调整 $cmd")
|
||||
FFmpegKit.executeAsync(cmd) { session ->
|
||||
if (ReturnCode.isSuccess(session.returnCode)) {
|
||||
Common.showLog("FFmpeg 播放速度调整成功")
|
||||
} else {
|
||||
Common.showLog("FFmpeg 播放速度调整失败: ${session.failStackTrace}")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
BIN
app/src/main/res/drawable/logo1.png
Normal file
BIN
app/src/main/res/drawable/logo1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
BIN
app/src/main/res/drawable/logo2.png
Normal file
BIN
app/src/main/res/drawable/logo2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 668 KiB |
@ -11,8 +11,8 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/play_message_background"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp">
|
||||
android:paddingTop="35dp"
|
||||
android:paddingBottom="15dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/back"
|
||||
|
||||
@ -33,10 +33,10 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon_ball"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="35dp"
|
||||
android:layout_height="35dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:src="@drawable/tab1_normal" />
|
||||
android:src="@drawable/logo1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/enable_tv_ball"
|
||||
@ -98,10 +98,10 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon_notification"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="35dp"
|
||||
android:layout_height="35dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:src="@drawable/tab1_normal" />
|
||||
android:src="@drawable/logo2" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/enable_tv_notification"
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
android:layout_marginEnd="25dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageView
|
||||
@ -82,10 +83,10 @@
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_empty"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:layout_below="@id/layout_recent"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
|
||||
@ -9,10 +9,12 @@
|
||||
|
||||
|
||||
<style name="Theme.Transparent" parent="Theme.AppCompat.NoActionBar">
|
||||
<item name="android:windowBackground">@color/color_transparent</item>
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<item name="android:backgroundDimEnabled">false</item>
|
||||
<item name="android:colorBackgroundCacheHint">@null</item>
|
||||
<item name="android:windowIsFloating">false</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
Loading…
Reference in New Issue
Block a user