Android 13(API33)屏幕录制视频保存至外部存储解决方案
Android屏幕录制API 32+保存外部存储失效的解决步骤
我开发的屏幕录制功能,需要将MP4视频保存到外部存储。该功能在API 29及以下版本正常运行,但在API 32及以上版本失效。
核心问题分析
API 30+引入的Scoped Storage限制、原代码权限检查逻辑错误、废弃文件路径API,是导致功能失效的主要原因。
具体解决步骤
1. 修复权限检查与申请逻辑
原代码onRequestPermissionsResult中直接用if(true)判断权限授予,属于无效逻辑,需替换为实际权限校验;同时要区分API版本处理权限:
- API 29及以下:申请
WRITE_EXTERNAL_STORAGE和RECORD_AUDIO - API 30+:若需写入公共目录,申请
MANAGE_EXTERNAL_STORAGE和RECORD_AUDIO;或改用无需额外权限的应用专属外部存储
2. 适配API 29+的文件存储路径
Environment.getExternalStoragePublicDirectory在API 29已被废弃,API 30+强制限制直接访问公共目录,推荐两种适配方式:
- MediaStore写入公共Downloads目录:无需
MANAGE_EXTERNAL_STORAGE权限,通过媒体库插入文件获取可写入Uri - 应用专属外部存储:路径为
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),文件仅本应用可见,卸载时自动删除
3. 修正MediaRecorder配置
原代码输出格式使用THREE_GPP但文件后缀为.mp4,格式不匹配可能导致录制失败,需改为MPEG_4格式。
修改后的代码示例
MainActivity.kt
class MainActivity : AppCompatActivity() { private var mScreenDensity = 0 private var mProjectionManager: MediaProjectionManager? = null private var mMediaProjection: MediaProjection? = null private var mVirtualDisplay: VirtualDisplay? = null private var mMediaProjectionCallback: MediaProjectionCallback? = null private var mMediaRecorder: MediaRecorder? = null private lateinit var binding: ActivityMainBinding private var outputUri: Uri? = null public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) val metrics = DisplayMetrics() windowManager.defaultDisplay.getMetrics(metrics) mScreenDensity = metrics.densityDpi mMediaRecorder = MediaRecorder() mProjectionManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager binding.toggle.setOnClickListener { v: View? -> val hasAudioPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED val hasStoragePermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { Environment.isExternalStorageManager() } else { ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED } if (!hasAudioPermission || !hasStoragePermission) { requestNecessaryPermissions() } else { onToggleScreenShare(v) } } } private fun requestNecessaryPermissions() { val permissions = mutableListOf<String>() if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { permissions.add(Manifest.permission.RECORD_AUDIO) } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (!Environment.isExternalStorageManager()) { val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) intent.data = Uri.parse("package:$packageName") startActivity(intent) return } } else { if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) } } if (permissions.isNotEmpty()) { ActivityCompat.requestPermissions(this, permissions.toTypedArray(), REQUEST_PERMISSIONS) } } public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode != REQUEST_CODE) { Log.e(TAG, "Unknown request code: $requestCode") return } if (resultCode != RESULT_OK) { Toast.makeText(this, "屏幕录制权限被拒绝", Toast.LENGTH_SHORT).show() binding.toggle.isChecked = false return } mMediaProjectionCallback = MediaProjectionCallback() mMediaProjection = mProjectionManager!!.getMediaProjection(resultCode, data!!) mMediaProjection?.registerCallback(mMediaProjectionCallback, null) mVirtualDisplay = createVirtualDisplay() mMediaRecorder!!.start() } fun onToggleScreenShare(view: View?) { val toggleButton = view as ToggleButton if (toggleButton.isChecked) { if (initRecorder()) { shareScreen() } else { toggleButton.isChecked = false Toast.makeText(this, "初始化录制器失败", Toast.LENGTH_SHORT).show() } } else { stopRecording() } } private fun shareScreen() { if (mMediaProjection == null) { startActivityForResult(mProjectionManager!!.createScreenCaptureIntent(), REQUEST_CODE) return } mVirtualDisplay = createVirtualDisplay() mMediaRecorder!!.start() } private fun createVirtualDisplay(): VirtualDisplay { return mMediaProjection!!.createVirtualDisplay( "ScreenRecorder", DISPLAY_WIDTH, DISPLAY_HEIGHT, mScreenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mMediaRecorder!!.surface, null, null ) } @SuppressLint("SimpleDateFormat") private fun initRecorder(): Boolean { return try { mMediaRecorder!!.reset() mMediaRecorder!!.setAudioSource(MediaRecorder.AudioSource.MIC) mMediaRecorder!!.setVideoSource(MediaRecorder.VideoSource.SURFACE) mMediaRecorder!!.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) mMediaRecorder!!.setVideoSize(DISPLAY_WIDTH, DISPLAY_HEIGHT) mMediaRecorder!!.setVideoEncoder(MediaRecorder.VideoEncoder.H264) mMediaRecorder!!.setAudioEncoder(MediaRecorder.AudioEncoder.AAC) mMediaRecorder!!.setVideoEncodingBitRate(512 * 1000) mMediaRecorder!!.setVideoFrameRate(30) val rotation = windowManager.defaultDisplay.rotation val orientation = ORIENTATIONS[rotation] mMediaRecorder!!.setOrientationHint(orientation) val fileName = "Prank_Record_${SimpleDateFormat("dd-MM-yyyy-hh_mm_ss").format(Date())}.mp4" outputUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val contentValues = ContentValues().apply { put(MediaStore.Video.Media.DISPLAY_NAME, fileName) put(MediaStore.Video.Media.MIME_TYPE, "video/mp4") put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) } contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues) } else { val file = File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName ) Uri.fromFile(file) } outputUri?.let { uri -> val outputStream = contentResolver.openOutputStream(uri) mMediaRecorder!!.setOutputFile(outputStream?.fd) } ?: throw IOException("无法获取输出文件Uri") mMediaRecorder!!.prepare() true } catch (e: IOException) { e.printStackTrace() false } } private fun stopRecording() { try { mMediaRecorder!!.stop() mMediaRecorder!!.reset() outputUri?.let { sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, it)) Toast.makeText(this, "录制已保存到下载目录", Toast.LENGTH_SHORT).show() } } catch (e: IllegalStateException) { e.printStackTrace() } finally { stopScreenSharing() } } private inner class MediaProjectionCallback : MediaProjection.Callback() { override fun onStop() { if (binding.toggle.isChecked) { binding.toggle.isChecked = false stopRecording() } mMediaProjection = null stopScreenSharing() } } private fun stopScreenSharing() { mVirtualDisplay?.release() mVirtualDisplay = null destroyMediaProjection() } public override fun onDestroy() { super.onDestroy() destroyMediaProjection() mMediaRecorder?.release() mMediaRecorder = null } private fun destroyMediaProjection() { mMediaProjection?.let { it.unregisterCallback(mMediaProjectionCallback) it.stop() mMediaProjection = null } Log.i(TAG, "MediaProjection已停止") } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<String>, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) when (requestCode) { REQUEST_PERMISSIONS -> { val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED } if (allGranted) { onToggleScreenShare(binding.toggle) } else { binding.toggle.isChecked = false Snackbar.make( findViewById(android.R.id.content), "需要授予必要权限才能录制", Snackbar.LENGTH_INDEFINITE ).setAction("去设置") { requestNecessaryPermissions() }.show() } } } } companion object { private const val TAG = "ScreenRecorder" private const val REQUEST_CODE = 1000 private const val DISPLAY_WIDTH = 720 private const val DISPLAY_HEIGHT = 1280 private val ORIENTATIONS = SparseIntArray() private const val REQUEST_PERMISSIONS = 10 init { ORIENTATIONS.append(Surface.ROTATION_0, 90) ORIENTATIONS.append(Surface.ROTATION_90, 0) ORIENTATIONS.append(Surface.ROTATION_180, 270) ORIENTATIONS.append(Surface.ROTATION_270, 180) } } }
AndroidManifest.xml
<uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29"/> <!-- API 30+若需写入公共目录则保留,否则可移除 --> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
内容的提问来源于stack exchange,提问作者WilliamVietnam
相关产品推荐
相关产品推荐

