This commit is contained in:
litingting 2025-07-01 17:37:23 +08:00
parent 973e0f1d30
commit f3c2544cd4
25 changed files with 226 additions and 1257 deletions

BIN
app/RecordScreen.jks Normal file

Binary file not shown.

View File

@ -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
View 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

View File

@ -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"

View File

@ -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()
}
}

View File

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

View File

@ -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?) {
}
}

View File

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

View File

@ -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 {

View File

@ -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()
}
}
}
}

View File

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

View File

@ -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()
}
}
}
}

View File

@ -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()
}
}
}
}

View File

@ -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()
}
}

View File

@ -108,6 +108,7 @@ class RecordingFragment : BaseFragment<FragmentRecordingBinding>(), FloatingCall
override fun onUpdateRecordTime(time: String) {
super.onUpdateRecordTime(time)
Common.showLog("=======onUpdateRecordTime 时间更新")
binding.tvTimer.text = time
}

View File

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

View File

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

View File

@ -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}")
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 668 KiB

View File

@ -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"

View File

@ -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"

View File

@ -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">

View File

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