You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用MediaProjectionManager实现前台截图遇问题求助

解决MediaProjectionManager截图黑屏/崩溃问题及语音触发截图实现

一、现有代码的核心问题

  • Intent参数引用错误:startMediaProjection()中直接使用onStartCommand的局部intent变量,服务重启或延迟调用时会导致空指针,需将授权参数持久化存储。
  • Image转Bitmap逻辑缺陷:未正确处理图像缓冲区的行间距(rowPadding),导致生成的Bitmap包含无效空白区域或直接黑屏。
  • 虚拟显示无状态回调:createVirtualDisplay未设置回调,无法确认虚拟显示是否创建成功,可能导致ImageReader无法接收画面。
  • 前台服务未合规实现:Android 8.0+后台服务无法稳定持有MediaProjection,必须启动前台服务并显示通知。
  • 触发逻辑不符合需求:当前代码持续监听截图,而非语音指令触发单次截图。

二、修正后的完整代码

ScreenshotService.kt

class ScreenshotService : Service() {
    private lateinit var mediaProjectionManager: MediaProjectionManager
    private var mediaProjection: MediaProjection? = null
    private var imageReader: ImageReader? = null
    private var speechRecognizer: SpeechRecognizer? = null
    private var resultCode: Int = -1
    private var data: Intent? = null
    private val notificationId = 1001

    override fun onBind(intent: Intent?): IBinder? = null

    override fun onCreate() {
        super.onCreate()
        mediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
        // 启动前台服务,符合Android 8.0+权限要求
        startForeground(notificationId, createForegroundNotification())
        // 初始化语音识别
        initSpeechRecognizer()
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // 保存MediaProjection授权参数
        intent?.let {
            resultCode = it.getIntExtra("resultCode", -1)
            data = it.getParcelableExtra("data")
        }
        return START_STICKY
    }

    private fun createForegroundNotification(): Notification {
        val channelId = "screenshot_service_channel"
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(channelId, "截图服务", NotificationManager.IMPORTANCE_LOW)
            val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            notificationManager.createNotificationChannel(channel)
        }
        return NotificationCompat.Builder(this, channelId)
            .setContentTitle("语音截图服务")
            .setContentText("说出\"screenshot\"触发截图")
            .setSmallIcon(R.drawable.ic_notification)
            .build()
    }

    private fun initSpeechRecognizer() {
        if (!SpeechRecognizer.isRecognitionAvailable(this)) {
            Toast.makeText(this, "当前设备不支持语音识别", Toast.LENGTH_SHORT).show()
            stopSelf()
            return
        }
        speechRecognizer = SpeechRecognizer.createSpeechRecognizer(this)
        val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
        intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
        intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.ENGLISH)
        speechRecognizer?.setRecognitionListener(object : RecognitionListener {
            override fun onReadyForSpeech(params: Bundle?) {}
            override fun onBeginningOfSpeech() {}
            override fun onRmsChanged(rmsdB: Float) {}
            override fun onBufferReceived(buffer: ByteArray?) {}
            override fun onEndOfSpeech() {}
            override fun onError(error: Int) {}
            override fun onResults(results: Bundle?) {
                results?.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION)?.let { matches ->
                    if (matches.any { it.equals("screenshot", ignoreCase = true) }) {
                        triggerScreenshot()
                    }
                    // 继续监听下一次语音指令
                    speechRecognizer?.startListening(intent)
                }
            }
            override fun onPartialResults(partialResults: Bundle?) {}
            override fun onEvent(eventType: Int, params: Bundle?) {}
        })
        // 开始监听语音
        speechRecognizer?.startListening(intent)
    }

    private fun triggerScreenshot() {
        if (resultCode == -1 || data == null) {
            Toast.makeText(this, "请先授权屏幕录制权限", Toast.LENGTH_SHORT).show()
            return
        }
        // 初始化MediaProjection(每次截图前确保实例有效)
        mediaProjection?.stop()
        mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data!!)
        val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
        val metrics = DisplayMetrics()
        windowManager.defaultDisplay.getRealMetrics(metrics) // 使用getRealMetrics获取真实屏幕尺寸
        val width = metrics.widthPixels
        val height = metrics.heightPixels
        val densityDpi = metrics.densityDpi
        // 创建ImageReader,格式使用PRIV格式更可靠
        imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 1)
        mediaProjection?.createVirtualDisplay(
            "ScreenshotVirtualDisplay",
            width, height, densityDpi,
            DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
            imageReader?.surface,
            object : VirtualDisplay.Callback() {
                override fun onDisplayPaused() {}
                override fun onDisplayResumed() {}
                override fun onDisplayStopped() {}
            },
            Handler(Looper.getMainLooper())
        )
        // 延迟获取图像,确保虚拟显示已渲染完成
        Handler(Looper.getMainLooper()).postDelayed({
            captureSingleScreenshot()
        }, 200)
    }

    private fun captureSingleScreenshot() {
        val image = imageReader?.acquireLatestImage() ?: return
        val bitmap = imageToBitmap(image)
        saveBitmapToFile(bitmap)
        // 释放资源
        image.close()
        imageReader?.close()
        mediaProjection?.stop()
        Toast.makeText(this, "截图已保存", Toast.LENGTH_SHORT).show()
    }

    private fun imageToBitmap(image: Image): Bitmap {
        val planes = image.planes
        val buffer = planes[0].buffer
        val pixelStride = planes[0].pixelStride
        val rowStride = planes[0].rowStride
        val rowPadding = rowStride - pixelStride * image.width
        // 创建正确尺寸的Bitmap
        val bitmap = Bitmap.createBitmap(
            image.width + rowPadding / pixelStride,
            image.height,
            Bitmap.Config.ARGB_8888
        )
        bitmap.copyPixelsFromBuffer(buffer)
        // 裁剪掉多余的行padding
        return Bitmap.createBitmap(bitmap, 0, 0, image.width, image.height)
    }

    private fun saveBitmapToFile(bitmap: Bitmap) {
        val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
        val file = File(storageDir, "screenshot_${System.currentTimeMillis()}.png")
        try {
            FileOutputStream(file).use { fos ->
                bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
            }
        } catch (e: IOException) {
            e.printStackTrace()
            Toast.makeText(this, "保存截图失败", Toast.LENGTH_SHORT).show()
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        speechRecognizer?.destroy()
        mediaProjection?.stop()
        imageReader?.close()
    }
}

必要的权限声明(AndroidManifest.xml)

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" android:minSdkVersion="33" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" android:minSdkVersion="33" />

<application ...>
    <service android:name=".ScreenshotService" android:foregroundServiceType="mediaProjection" />
</application>

启动服务的Activity代码示例

class MainActivity : AppCompatActivity() {
    private val REQUEST_CODE_SCREEN_CAPTURE = 100

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<Button>(R.id.btn_start_service).setOnClickListener {
            val mediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
            startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), REQUEST_CODE_SCREEN_CAPTURE)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_CODE_SCREEN_CAPTURE && resultCode == RESULT_OK) {
            val intent = Intent(this, ScreenshotService::class.java)
            intent.putExtra("resultCode", resultCode)
            intent.putExtra("data", data)
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                startForegroundService(intent)
            } else {
                startService(intent)
            }
        }
    }
}

三、关键修改说明

  1. 授权参数持久化:在onStartCommand中保存resultCode和data,避免服务重启后参数丢失。
  2. 修复Bitmap转换:添加裁剪步骤去除行padding,使用getRealMetrics获取真实屏幕尺寸,避免截图区域不全。
  3. 前台服务合规:创建通知渠道并启动前台服务,符合Android 8.0+后台服务限制。
  4. 语音触发逻辑:集成SpeechRecognizer监听"screenshot"指令,触发单次截图后立即释放MediaProjection资源,避免内存泄漏。
  5. 资源生命周期管理:在onDestroy和截图完成后及时释放MediaProjection、ImageReader等资源,防止崩溃和内存泄漏。
  6. 虚拟显示回调:添加VirtualDisplay.Callback,可扩展处理显示状态变化的逻辑。

内容的提问来源于stack exchange,提问作者Droid Automations

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 07:59:57