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

Android使用MediaProjection截图报错:需媒体投影类型前台服务

问题描述

我是Android开发新手,在Android Studio 2024中用Kotlin开发跨应用截图应用,采用MediaProjection方案。应用包含MainActivity与CaptureService,但执行projection = mediaProjectionManager.getMediaProjection(resultCode, data!!)时,触发错误:

Media projections require a foreground service of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION

导致应用崩溃。已在AndroidManifest中添加相关权限,服务中也配置了通知,但问题仍未解决。


相关代码

MainActivity

package com.example.screenshot

import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
import android.graphics.Color
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.util.Log
import android.widget.Toast
import androidx.core.app.ServiceCompat
import java.util.*
import com.example.screenshot.Capture

class MainActivity : Activity() {

    companion object {
        private const val REQUEST_CAPTURE = 1

        var projection: MediaProjection? = null
    }
    private lateinit var mediaProjectionManager: MediaProjectionManager
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val startServiceButton: Button = findViewById(R.id.start_service_button)
        val stopServiceButton: Button = findViewById(R.id.stop_service_button)

        mediaProjectionManager = getSystemService(Service.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
        startActivityForResult(mediaProjectionManager.createScreenCaptureIntent() , REQUEST_CAPTURE)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        val intent = Intent(this, CaptureService::class.java)
        if (requestCode == REQUEST_CAPTURE) {
            if (resultCode == RESULT_OK) {
                .setAction(CaptureService.ACTION_ENABLE_CAPTURE)
                startForegroundService(intent)
                Thread.sleep(100)
            projection = mediaProjectionManager.getMediaProjection(resultCode, data!!)
                Toast.makeText(this, "All OK", Toast.LENGTH_SHORT).show()                
            } else {
                projection = null
                Toast.makeText(this, "Error", Toast.LENGTH_SHORT).show()
            }
        }
            //finish()
    }
}

CaptureService

package com.example.screenshot

import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
import android.graphics.Color
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.util.Log
import android.widget.Toast
import androidx.core.app.ServiceCompat
import java.util.*
import com.example.screenshot.Capture

class CaptureService : Service() {

    companion object {
        private val TAG = CaptureService::class.qualifiedName
        val ACTION_ENABLE_CAPTURE = "enable_capture"
    }

    private val notificationId = Random().nextInt()

    private val capture = Capture(this)
    
    override fun onBind(intent: Intent?): IBinder? {
        return null
    }
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        if (intent != null) {

            when (intent.action) {
                ACTION_ENABLE_CAPTURE -> onEnableCapture()
            }
        }

            val channelId = "001"
            val channelName = "myChannel"
            val channel =
                NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_NONE)
            channel.lightColor = Color.BLUE
            channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
            val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
            manager.createNotificationChannel(channel)
            val notification: Notification =
                Notification.Builder(applicationContext, channelId).setOngoing(true)
                    .setSmallIcon(R.mipmap.ic_launcher).setCategory(
                        Notification.CATEGORY_SERVICE
                    ).build()
        ServiceCompat.startForeground(this,1,notification, FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)
        return Service.START_STICKY
    }

    private fun enableCapture() {
        if (MainActivity.projection == null) {
            println("Phase1")
            Log.d(TAG, "startActivity(CaptureActivity)")
            val intent = Intent(this, MainActivity::class.java)
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            startActivity(intent)
        } else {
            onEnableCapture()
        }
    }

    private fun onEnableCapture() {
       MainActivity.projection?.run {
            capture.run(this) {
                capture.stop()
                // save bitmap
            }
        }
    }

    private fun disableCapture() {
        capture.stop()        
        MainActivity.projection = null
    }

    override fun onDestroy() {
        super.onDestroy()
        disableCapture()        
    }
}

Capture类

package com.example.screenshot

import android.content.Context
import android.graphics.Bitmap
import android.graphics.PixelFormat
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.media.ImageReader
import android.media.projection.MediaProjection
import android.util.Log
import android.widget.Toast

class Capture(val context: Context) : ImageReader.OnImageAvailableListener {

    companion object {
        val TAG = Capture::class.qualifiedName
    }

    private var display: VirtualDisplay? = null
    private var onCaptureListener: ((Bitmap) -> Unit)? = null

    fun run(mediaProjection: MediaProjection, onCaptureListener: (Bitmap) -> Unit) {
        this.onCaptureListener = onCaptureListener
        if (display == null) {
            display = createDisplay(mediaProjection)
        }
    }

    private fun createDisplay(mediaProjection: MediaProjection): VirtualDisplay {
        context.resources.displayMetrics.run {
            val maxImages = 2
            val reader = ImageReader.newInstance(
                widthPixels, heightPixels, PixelFormat.RGBA_8888, maxImages)
            reader.setOnImageAvailableListener(this@Capture, null)
            val display = mediaProjection.createVirtualDisplay(
                "Capture Display", widthPixels, heightPixels, densityDpi,
                DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                reader.surface, null, null)
            Log.d(TAG, "createVirtualDisplay")
            return display
        }
    }

    override fun onImageAvailable(reader: ImageReader) {
        if (display != null) {
            onCaptureListener?.invoke(captureImage(reader))
        }
    }

    private fun captureImage(reader: ImageReader): Bitmap {
        val image = reader.acquireLatestImage()
        context.resources.displayMetrics.run {
            image.planes[0].run {
                val bitmap = Bitmap.createBitmap(
                    rowStride / pixelStride, heightPixels, Bitmap.Config.ARGB_8888)
                bitmap.copyPixelsFromBuffer(buffer)
                image.close()                
                return bitmap
            }
        }
    }

    fun stop() {
        display?.release()
        display = null
        onCaptureListener = null
    }
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.ScreenShot"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.ScreenShot">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

        </activity>
        <service
            android:name=".CaptureService"
            android:enabled="true"
            android:exported="true"
            android:stopWithTask="true"
            android:foregroundServiceType="mediaProjection" />
            />
    </application>
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
</manifest>

解决方案

核心问题

从Android 12(API 31)开始,调用MediaProjectionManager.getMediaProjection()必须由已启动的FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION类型前台服务发起,你的代码在Activity中调用该方法时,服务还未完成前台状态初始化,导致系统权限校验失败。同时代码存在语法错误和逻辑漏洞,需要一并修复。

具体修复步骤

1. 重构MainActivity:将授权结果传递给服务

移除Activity中获取MediaProjection的逻辑,改为将授权结果传递给服务,由服务处理:

package com.example.screenshot

import android.app.Activity
import android.app.MediaProjectionManager
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import android.widget.Toast

class MainActivity : Activity() {

    companion object {
        private const val REQUEST_CAPTURE = 1
    }

    private lateinit var mediaProjectionManager: MediaProjectionManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val startServiceButton: Button = findViewById(R.id.start_service_button)
        val stopServiceButton: Button = findViewById(R.id.stop_service_button)

        mediaProjectionManager = getSystemService(MediaProjectionManager::class.java)

        startServiceButton.setOnClickListener {
            startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(), REQUEST_CAPTURE)
        }

        stopServiceButton.setOnClickListener {
            stopService(Intent(this, CaptureService::class.java))
            Toast.makeText(this, "服务已停止", Toast.LENGTH_SHORT).show()
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_CAPTURE) {
            if (resultCode == RESULT_OK && data != null) {
                val intent = Intent(this, CaptureService::class.java).apply {
                    action = CaptureService.ACTION_ENABLE_CAPTURE
                    putExtra("RESULT_CODE", resultCode)
                    putExtra("DATA", data)
                }
                startForegroundService(intent)
                Toast.makeText(this, "服务已启动", Toast.LENGTH_SHORT).show()
            } else {
                Toast.makeText(this, "截图授权失败", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

2. 修改CaptureService:在服务内部获取MediaProjection

确保服务先进入前台状态,再调用getMediaProjection():

package com.example.screenshot

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
import android.graphics.Color
import android.os.IBinder
import android.util.Log
import androidx.core.app.ServiceCompat

class CaptureService : Service() {

    companion object {
        private val TAG = CaptureService::class.qualifiedName
        const val ACTION_ENABLE_CAPTURE = "enable_capture"
    }

    private lateinit var capture: Capture
    private var mediaProjectionCallback = object : MediaProjectionCallback() {
        override fun onStop() {
            super.onStop()
            capture.stop()
            stopSelf()
        }
    }

    override fun onCreate() {
        super.onCreate()
        capture = Capture(this)
    }

    override fun onBind(intent: Intent?): IBinder? {
        return null
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        intent?.let {
            when (it.action) {
                ACTION_ENABLE_CAPTURE -> {
                    val resultCode = it.getIntExtra("RESULT_CODE", -1)
                    val data = it.getParcelableExtra<Intent>("DATA")
                    if (resultCode != -1 && data != null) {
                        startForegroundService()
                        initMediaProjection(resultCode, data)
                    }
                }
            }
        }
        return Service.START_STICKY
    }

    private fun startForegroundService() {
        val channelId = "media_projection_channel"
        val channelName = "截图服务"
        val channel = NotificationChannel(
            channelId,
            channelName,
            NotificationManager.IMPORTANCE_LOW
        ).apply {
            lightColor = Color.BLUE
            lockscreenVisibility = Notification.VISIBILITY_PRIVATE
        }
        val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
        manager.createNotificationChannel(channel)

        val notification = Notification.Builder(this, channelId)
            .setOngoing(true)
            .setSmallIcon(R.mipmap.ic_launcher)
            .setContentTitle("截图服务运行中")
            .setContentText("点击停止服务")
            .setCategory(Notification.CATEGORY_SERVICE)
            .build()

        ServiceCompat.startForeground(
            this,
            1001,
            notification,
            FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
        )
    }

    private fun initMediaProjection(resultCode: Int, data: Intent) {
        val mediaProjectionManager = getSystemService(MediaProjectionManager::class.java)
        val mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data)
        mediaProjection.registerCallback(mediaProjectionCallback, null)
        capture.run(mediaProjection) { bitmap ->
            Log.d(TAG, "截图成功,尺寸:${bitmap.width}x${bitmap.height}")
            // 在这里处理截图(如保存到本地)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        capture.stop()
    }

    private inner class MediaProjectionCallback : android.media.projection.MediaProjection.Callback() {
        override fun onStop() {
            capture.stop()
            stopSelf()
        }
    }
}

3. 修复Capture类的截图逻辑

修正Bitmap裁剪错误,避免截图出现拉伸或空白:

package com.example.screenshot

import android.content.Context
import android.graphics.Bitmap
import android.graphics.PixelFormat
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.media.Image
import android.media.ImageReader
import android.media.projection.MediaProjection
import android.util.Log
import android.view.WindowManager

class Capture(val context: Context) : ImageReader.OnImageAvailableListener {

    companion object {
        val TAG = Capture::class.qualifiedName
    }

    private var display: VirtualDisplay? = null
    private var onCaptureListener: ((Bitmap) -> Unit)? = null

    fun run(mediaProjection: MediaProjection, onCaptureListener: (Bitmap) -> Unit) {
        this.onCaptureListener = onCaptureListener
        if (display == null) {
            display = createDisplay(mediaProjection)
        }
    }

    private fun createDisplay(mediaProjection: MediaProjection): VirtualDisplay {
        val windowManager = context.getSystemService(WindowManager::class.java)
        val displayMetrics = context.resources.displayMetrics
        val width = displayMetrics.widthPixels
        val height = displayMetrics.heightPixels
        val density = displayMetrics.densityDpi

        val maxImages = 2
        val reader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, maxImages)
        reader.setOnImageAvailableListener(this, null)

        return mediaProjection.createVirtualDisplay(
            "Capture Display",
            width,
            height,
            density,
            DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
            reader.surface,
            null,
            null
        )
    }

    override fun onImageAvailable(reader: ImageReader) {
        val image = reader.acquireLatestImage() ?: return
        try {
            val bitmap = imageToBitmap(image)
            onCaptureListener?.invoke(bitmap)
        } catch (e: Exception) {
            Log.e(TAG, "截图失败:${e.message}", e)
        } finally {
            image.close()
        }
    }

    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

        val bitmap = Bitmap.createBitmap(
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 08:59:53