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 { plugins {
id("com.android.application") id("com.android.application")
id("org.jetbrains.kotlin.android") id("org.jetbrains.kotlin.android")
} }
val timestamp = SimpleDateFormat("MM_dd_HH_mm").format(Date())
android { android {
namespace = "com.audio.record.screen.test" namespace = "com.audio.record.screen.test"
compileSdk = 35 compileSdk = 35
defaultConfig { defaultConfig {
applicationId = "com.audio.record.screen.test" applicationId = "com.audio.record.screen"
minSdk = 24 minSdk = 24
targetSdk = 35 targetSdk = 35
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
setProperty(
"archivesBaseName",
"RecordScreen_V" + versionName + "(${versionCode})_$timestamp"
)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {
release { release {
isMinifyEnabled = false isMinifyEnabled = true
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro"
@ -79,8 +84,8 @@ dependencies {
implementation("com.github.bumptech.glide:glide:4.16.0") implementation("com.github.bumptech.glide:glide:4.16.0")
implementation(files("libs/jetified-ffmpeg-kit-full-6.0.aar")) // 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-common-0.2.1.jar"))
implementation(files("libs/smart-exception-java-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" /> <!-- 通知 --> android:required="false" /> <!-- 通知 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- 前台服务 --> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- 前台服务 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <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.RECORD_AUDIO" /> <!-- 录音 -->
<uses-permission android:name="android.permission.CAMERA" /> <!-- 悬浮窗 --> <uses-permission android:name="android.permission.CAMERA" /> <!-- 相机 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /><!-- 悬浮窗 -->
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" android:maxSdkVersion="32"
@ -19,6 +21,14 @@
android:name="android.permission.READ_EXTERNAL_STORAGE" android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> 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 <application
android:name=".App" android:name=".App"
android:allowBackup="true" android:allowBackup="true"
@ -32,8 +42,8 @@
tools:targetApi="31"> tools:targetApi="31">
<activity <activity
android:name=".activity.WelcomeActivity" android:name=".activity.WelcomeActivity"
android:screenOrientation="portrait" android:exported="true"
android:exported="true" > android:screenOrientation="portrait">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -48,28 +58,16 @@
android:name=".activity.ImageViewActivity" android:name=".activity.ImageViewActivity"
android:exported="false" android:exported="false"
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity
android:name=".activity.PreviewActivity"
android:exported="false"
android:screenOrientation="portrait" />
<activity <activity
android:name=".activity.MainActivity1" android:name=".activity.MainActivity1"
android:exported="false" android:exported="false"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait"> android:screenOrientation="portrait"/>
</activity>
<activity
android:name=".activity.MainActivity"
android:exported="false">
<!-- <intent-filter> -->
<!-- <action android:name="android.intent.action.MAIN" /> -->
<!-- <category android:name="android.intent.category.LAUNCHER" /> -->
<!-- </intent-filter> -->
</activity>
<activity <activity
android:name=".activity.ScreenPermissionActivity" android:name=".activity.ScreenPermissionActivity"
android:excludeFromRecents="true" 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 package com.audio.record.screen.test.activity
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
@ -13,24 +12,18 @@ import android.widget.ImageView
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.marginBottom
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.audio.record.screen.test.R import com.audio.record.screen.test.R
import com.audio.record.screen.test.adapter.ViewPager2Adapter import com.audio.record.screen.test.adapter.ViewPager2Adapter
import com.audio.record.screen.test.base.BaseActivity import com.audio.record.screen.test.base.BaseActivity
import com.audio.record.screen.test.databinding.ActivityMain1Binding import com.audio.record.screen.test.databinding.ActivityMain1Binding
import com.audio.record.screen.test.dialog.DialogPermission 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.ConnectionListener
import com.audio.record.screen.test.service.FloatingCallback import com.audio.record.screen.test.service.FloatingCallback
import com.audio.record.screen.test.service.FloatingWindowBridge import com.audio.record.screen.test.service.FloatingWindowBridge
import com.audio.record.screen.test.tool.Common 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.Permission
import com.audio.record.screen.test.tool.ScreenCaptureHelper
import com.audio.record.screen.test.viewmodel.MainViewModel 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
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
@ -64,6 +57,8 @@ class MainActivity1 : BaseActivity<ActivityMain1Binding>(), ConnectionListener,F
initLauncher() initLauncher()
firstCheck() firstCheck()
checkStoragePermissionAndDoSomething() checkStoragePermissionAndDoSomething()
} }
private fun initLauncher() { private fun initLauncher() {
@ -73,6 +68,7 @@ class MainActivity1 : BaseActivity<ActivityMain1Binding>(), ConnectionListener,F
if (isGranted) { if (isGranted) {
// 权限已授予 // 权限已授予
Common.showLog("已获取存储权限") Common.showLog("已获取存储权限")
showPermissionDialog() showPermissionDialog()
// 执行写入文件操作 // 执行写入文件操作
} else { } else {
@ -83,14 +79,21 @@ class MainActivity1 : BaseActivity<ActivityMain1Binding>(), ConnectionListener,F
requestNotificationLauncher = requestNotificationLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
if (isGranted) { if (isGranted) {
startForegroundService()
Common.showLog("权限授予") Common.showLog("权限授予")
} else { } else {
Common.showLog("权限拒绝") 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( // screenCaptureLauncher = registerForActivityResult(
// ActivityResultContracts.StartActivityForResult() // ActivityResultContracts.StartActivityForResult()
// ) { result -> // ) { result ->
@ -161,9 +164,10 @@ class MainActivity1 : BaseActivity<ActivityMain1Binding>(), ConnectionListener,F
private fun showPermissionDialog() { private fun showPermissionDialog() {
if (isNotification && isOverlay) { if (isNotification && isOverlay) {
requestScreenPermission()
return return
} }
mPermissionDialog = mPermissionDialog ?: DialogPermission { mPermissionDialog = mPermissionDialog ?: DialogPermission ({
when (it) { when (it) {
DialogPermission.type_ball -> { DialogPermission.type_ball -> {
intentSysWindow(this@MainActivity1) intentSysWindow(this@MainActivity1)
@ -173,11 +177,19 @@ class MainActivity1 : BaseActivity<ActivityMain1Binding>(), ConnectionListener,F
requestNotificationLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS) requestNotificationLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS)
} }
} }
}){
//消失
requestScreenPermission()
} }
mPermissionDialog?.show(supportFragmentManager, "") 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) { private fun intentSysWindow(context: Context) {
val intent = Intent( val intent = Intent(
@ -191,9 +203,7 @@ class MainActivity1 : BaseActivity<ActivityMain1Binding>(), ConnectionListener,F
private fun firstCheck() { private fun firstCheck() {
Permission.checkNotification(this@MainActivity1) { Permission.checkNotification(this@MainActivity1) {
isNotification = it isNotification = it
if (it) {
startForegroundService()
}
} }
Permission.checkOvalApp(this@MainActivity1) { Permission.checkOvalApp(this@MainActivity1) {
isOverlay = it 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.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.media.projection.MediaProjectionManager import android.media.projection.MediaProjectionManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.PersistableBundle 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.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import com.audio.record.screen.test.base.BaseActivity import com.audio.record.screen.test.base.BaseActivity
import com.audio.record.screen.test.databinding.ActivityPlayBinding import com.audio.record.screen.test.databinding.ActivityPlayBinding
import com.audio.record.screen.test.service.FloatingWindowBridge import com.audio.record.screen.test.service.FloatingWindowBridge
@ -29,14 +35,46 @@ class ScreenPermissionActivity : AppCompatActivity() {
} }
private var withAudio = false 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) 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) withAudio = intent.getBooleanExtra(key_with_audio, false)
screenCaptureLauncher = registerForActivityResult( screenCaptureLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult() ActivityResultContracts.StartActivityForResult()
) { result -> ) { result ->
Common.showLog("--------------screenCaptureLauncher")
if (result.resultCode == RESULT_OK && result.data != null) { if (result.resultCode == RESULT_OK && result.data != null) {
val data: Intent? = result.data val data: Intent? = result.data
FloatingWindowBridge.updateMediaProjection(result.resultCode, data!!) FloatingWindowBridge.updateMediaProjection(result.resultCode, data!!)
@ -63,7 +101,7 @@ class ScreenPermissionActivity : AppCompatActivity() {
private fun requestScreenPermission() { private fun requestScreenPermission() {
val mediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager Common.showLog("--------------requestScreenPermission")
val intent = mediaProjectionManager.createScreenCaptureIntent() val intent = mediaProjectionManager.createScreenCaptureIntent()
screenCaptureLauncher.launch(intent) screenCaptureLauncher.launch(intent)
} }

View File

@ -1,5 +1,6 @@
package com.audio.record.screen.test.dialog package com.audio.record.screen.test.dialog
import android.content.DialogInterface
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Bundle 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.audio.record.screen.test.tool.Permission
import com.google.android.material.bottomsheet.BottomSheetDialogFragment 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 private lateinit var vb: DialogPermissionBinding
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -28,6 +29,7 @@ class DialogPermission(private var mClickType: (type: Int) -> Unit) : BottomShee
val dialog = dialog val dialog = dialog
if (dialog != null) { if (dialog != null) {
dialog.setCanceledOnTouchOutside(false) dialog.setCanceledOnTouchOutside(false)
dialog.setCancelable(false)
val window = dialog.window val window = dialog.window
if (window != null) { if (window != null) {
window.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) 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() { private fun init() {
vb.run { 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) { private fun setScreenshot(boolean: Boolean) {
Common.showLog("----------setScreenshot=${boolean}")
if (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)
} // }
FloatingWindowBridge.sendCommand(FloatingWindowBridge.COMMEND_show_screenshot)
} else { } else {
FloatingWindowBridge.sendCommand(FloatingWindowBridge.COMMEND_hide_screenshot) FloatingWindowBridge.sendCommand(FloatingWindowBridge.COMMEND_hide_screenshot)
@ -217,7 +219,7 @@ class RecordNormalFragment : BaseFragment<FragmentRecordNormalBinding>(), Floati
private fun startRecorder() { private fun startRecorder() {
requestRecordPermission( ConstValue.type_record){ requestRecordPermission( ConstValue.type_record){
startCountDown() // startCountDown()
} }
} }
@ -227,13 +229,9 @@ class RecordNormalFragment : BaseFragment<FragmentRecordNormalBinding>(), Floati
*/ */
private fun requestRecordPermission(type:Int,check:(()->Unit)? = null){ private fun requestRecordPermission(type:Int,check:(()->Unit)? = null){
// TODO: requestRecordPermission // TODO: requestRecordPermission
if (FloatingWindowBridge.getMediaProjection() == null) { val captureIntent = mediaProjectionManager.createScreenCaptureIntent()
val captureIntent = mediaProjectionManager.createScreenCaptureIntent() pendingType = type
pendingType = type recorderLauncher.launch(captureIntent)
recorderLauncher.launch(captureIntent)
}else{
check?.invoke()
}
} }

View File

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

View File

@ -146,7 +146,7 @@ class ScreenRecordService : Service() {
private val timeUpdateRunnable = object : Runnable { private val timeUpdateRunnable = object : Runnable {
override fun run() { override fun run() {
Common.showLog("=======timeUpdateRunnable 时间更新 isPause=${isPause}")
val currentRecordingTimeInMillis = val currentRecordingTimeInMillis =
System.currentTimeMillis() - recordingStartTime - totalPausedTime System.currentTimeMillis() - recordingStartTime - totalPausedTime
val elapsed = currentRecordingTimeInMillis / 1000 val elapsed = currentRecordingTimeInMillis / 1000
@ -156,8 +156,10 @@ class ScreenRecordService : Service() {
callbacks.forEach { callbacks.forEach {
it.get()?.onUpdateRecordTime(onRecordingTimeChanged) it.get()?.onUpdateRecordTime(onRecordingTimeChanged)
} }
if (!isPause) if (!isPause){
mRecorderHandler.postDelayed(this, updateInterval) mRecorderHandler.postDelayed(this, updateInterval)
}
} }
} }
@ -277,8 +279,9 @@ class ScreenRecordService : Service() {
*/ */
private fun startScreenshot(showView: Boolean) { private fun startScreenshot(showView: Boolean) {
// TODO: 开始截屏 // TODO: 开始截屏
mIntent?.let { intent ->
mediaProjectionManager.getMediaProjection(mCode, intent)?.let { mediaProjection -> createMediaProject {
it?.let { mediaProjection->
hideScreenshot() hideScreenshot()
val intent = Intent(this, ScreenshotAnimActivity::class.java) val intent = Intent(this, ScreenshotAnimActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
@ -291,8 +294,10 @@ class ScreenRecordService : Service() {
if (showView) if (showView)
showScreenshot() showScreenshot()
} }
} }
} }
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
@ -384,7 +389,7 @@ class ScreenRecordService : Service() {
} }
Common.showLog("-------layoutParamsBall.x=${layoutParamsBall.x} layoutParamsBall.y=${layoutParamsBall.y}") Common.showLog("-------layoutParamsBall.x=${layoutParamsBall.x} layoutParamsBall.y=${layoutParamsBall.y}")
} }
if (!isBallViewAdded&&!isBallExpandViewAdded) { if (!isBallViewAdded && !isBallExpandViewAdded) {
windowManager.addView(ballView, layoutParamsBall) windowManager.addView(ballView, layoutParamsBall)
isBallViewAdded = true isBallViewAdded = true
} }
@ -482,6 +487,7 @@ class ScreenRecordService : Service() {
} }
layoutParamsBallExpand.y = aY layoutParamsBallExpand.y = aY
updateBall()
windowManager.addView(ballViewExpand, layoutParamsBallExpand) windowManager.addView(ballViewExpand, layoutParamsBallExpand)
isBallExpandViewAdded = true isBallExpandViewAdded = true
delayRemove { delayRemove {
@ -529,7 +535,8 @@ class ScreenRecordService : Service() {
windowManager, windowManager,
true true
) { ) {
startScreenshot(true) ballType = ConstValue.type_screenshot
intentPermission(false)
} }
} }
if (!isScreenshotViewAdded) { if (!isScreenshotViewAdded) {
@ -662,23 +669,27 @@ class ScreenRecordService : Service() {
//倒计时结束后,开启录制 //倒计时结束后,开启录制
Common.showLog("Service--- 倒计时结束") Common.showLog("Service--- 倒计时结束")
hideCountDown() hideCountDown()
mIntent?.let { intent -> startRecording()
mediaProjectionManager.getMediaProjection(mCode, intent) callbacks.forEach {
?.let { media -> it.get()?.onStartRecording()
mediaProjection = media
startRecording()
callbacks.forEach {
it.get()?.onStartRecording()
}
}
} }
} }
} }
} }
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( private fun animateNext(
countdownValues: Array<String>, countdownValues: Array<String>,
tvCountdown: TextView, tvCountdown: TextView,
@ -741,37 +752,40 @@ class ScreenRecordService : Service() {
totalPausedTime = 0L totalPausedTime = 0L
mRecorderHandler.post(timeUpdateRunnable) mRecorderHandler.post(timeUpdateRunnable)
mediaProjection?.let { createMediaProject{ curmed->
it.registerCallback(object : MediaProjection.Callback() { mediaProjection?.let {
override fun onStop() { it.registerCallback(object : MediaProjection.Callback() {
Common.showLog("MediaProjection 被系统或用户停止") override fun onStop() {
// 这里应该释放 MediaRecorder 和 VirtualDisplay 等 Common.showLog("MediaProjection 被系统或用户停止")
stopRecording() // 这里应该释放 MediaRecorder 和 VirtualDisplay 等
stopRecording()
}
}, Handler(Looper.getMainLooper()))
virtualDisplay = it.createVirtualDisplay(
"ScreenRecord",
width, height, resources.displayMetrics.densityDpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
mediaRecorder.surface, object : VirtualDisplay.Callback() {
override fun onPaused() {
super.onPaused()
Common.showLog("--VirtualDisplay.Callback..onPaused...")
} }
}, Handler(Looper.getMainLooper()))
virtualDisplay = it.createVirtualDisplay(
"ScreenRecord",
width, height, resources.displayMetrics.densityDpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
mediaRecorder.surface, object : VirtualDisplay.Callback() {
override fun onPaused() {
super.onPaused()
Common.showLog("--VirtualDisplay.Callback..onPaused...")
}
override fun onResumed() { override fun onResumed() {
super.onResumed() super.onResumed()
Common.showLog("--VirtualDisplay.Callback..onResumed...") Common.showLog("--VirtualDisplay.Callback..onResumed...")
} }
override fun onStopped() { override fun onStopped() {
super.onStopped() super.onStopped()
Common.showLog("--VirtualDisplay.Callback..onStopped...") Common.showLog("--VirtualDisplay.Callback..onStopped...")
} }
}, null }, null
) )
}
} }
} }
@ -922,7 +936,11 @@ class ScreenRecordService : Service() {
fun getViewStatus() { fun getViewStatus() {
callbacks.forEach { 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 package com.audio.record.screen.test.tool
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -17,6 +18,8 @@ import android.view.KeyCharacterMap
import android.view.KeyEvent import android.view.KeyEvent
import android.view.View import android.view.View
import android.view.ViewConfiguration import android.view.ViewConfiguration
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.view.WindowManager import android.view.WindowManager
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
@ -40,26 +43,6 @@ object Common {
} }
fun setStatusBarTextColor(activity: Activity, dark: Boolean) { 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 window = activity.window
val decor = window.decorView 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:layout_height="wrap_content"
android:background="@drawable/play_message_background" android:background="@drawable/play_message_background"
android:orientation="horizontal" android:orientation="horizontal"
android:paddingTop="10dp" android:paddingTop="35dp"
android:paddingBottom="10dp"> android:paddingBottom="15dp">
<ImageView <ImageView
android:id="@+id/back" android:id="@+id/back"

View File

@ -33,10 +33,10 @@
<ImageView <ImageView
android:id="@+id/icon_ball" android:id="@+id/icon_ball"
android:layout_width="wrap_content" android:layout_width="35dp"
android:layout_height="wrap_content" android:layout_height="35dp"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:src="@drawable/tab1_normal" /> android:src="@drawable/logo1" />
<TextView <TextView
android:id="@+id/enable_tv_ball" android:id="@+id/enable_tv_ball"
@ -98,10 +98,10 @@
<ImageView <ImageView
android:id="@+id/icon_notification" android:id="@+id/icon_notification"
android:layout_width="wrap_content" android:layout_width="35dp"
android:layout_height="wrap_content" android:layout_height="35dp"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:src="@drawable/tab1_normal" /> android:src="@drawable/logo2" />
<TextView <TextView
android:id="@+id/enable_tv_notification" android:id="@+id/enable_tv_notification"

View File

@ -12,6 +12,7 @@
android:layout_marginEnd="25dp" android:layout_marginEnd="25dp"
app:cardCornerRadius="12dp" app:cardCornerRadius="12dp"
app:cardElevation="0dp" app:cardElevation="0dp"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<ImageView <ImageView
@ -82,10 +83,10 @@
<LinearLayout <LinearLayout
android:id="@+id/layout_empty" android:id="@+id/layout_empty"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_centerInParent="true"
android:gravity="center" android:gravity="center"
android:layout_below="@id/layout_recent"
android:orientation="vertical" android:orientation="vertical"
android:visibility="gone"> android:visibility="gone">

View File

@ -9,10 +9,12 @@
<style name="Theme.Transparent" parent="Theme.AppCompat.NoActionBar"> <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:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowNoTitle">true</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> </style>
</resources> </resources>