一、声明必要的权限

在 Android 项目中,需要在AndroidManifest.xml文件中添加权限:

1
2
3
4
5
6
7
8
9
	<!--文件存储权限-->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    
    <!--高版本录制屏幕需要在前台服务中-->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
	

二、声明应用允许录制播放的声音

需要在 AndroidManifest.xml文件 application 标签中添加 android:allowAudioPlaybackCapture="true"

三、定义前台服务

AndroidManifest.xml文件中定义前台服务:

1
2
3
4
    <service
    android:name=".MediaProjectionService"
    android:foregroundServiceType="mediaProjection"
    />

四、开始录制

Activity中定义开始录制的方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    private void startRecord() {
        MediaProjectionManager mpm = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
        Intent intent = mpm.createScreenCaptureIntent();
        startActivityForResult(intent, REQUEST_PROJECTION);
    }
	
	@Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_PROJECTION && resultCode == RESULT_OK) {
			//调起前台录制服务,在服务中获取录制的数据
            Intent intent = new Intent(this, MediaProjectionService.class);
            intent.putExtra("code", resultCode);
            intent.putExtra("data", data);
            intent.putExtra("save_uri", mDirUri);
            startService(intent);
        }
    }
	

五、在前台服务中获取录制的数据

show codes:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
package com.lixb.app.rec.outaudio;

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.Context;
import android.content.Intent;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioPlaybackCaptureConfiguration;
import android.media.AudioRecord;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.net.Uri;
import android.os.Build;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.util.Log;

import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * @ClassName MediaProjectionService
 * @Description
 * @Author lixb
 * @Date 2025/2/18 14:36
 * @Version 1.0
 */
public class MediaProjectionService extends Service {
    public static final int NOTIFICATION_ID = 111;
    public static final String CHANNEL_ID = "Media Projection Channel";
    public static final String TAG = "MediaProjectionService";
    private Recorder mRecorder;
    private Uri mSaveUri;

    @Override
    public void onCreate() {
        super.onCreate();
        createNotificationChannel();
        startForeground(NOTIFICATION_ID,createNotification());
    }

    private void createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(
                    CHANNEL_ID,
                    "Media Projection Channel",
                    NotificationManager.IMPORTANCE_DEFAULT
            );
            NotificationManager manager = getSystemService(NotificationManager.class);
            manager.createNotificationChannel(channel);
        }
    }

    private Notification createNotification() {
        Intent notificationIntent = new Intent(this, MainActivity.class);
        int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0;
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, flags);

        return new NotificationCompat.Builder(this, CHANNEL_ID)
                .setContentTitle("Media Projection Service")
                .setContentText("Screen recording is in progress.")
                .setSmallIcon(R.drawable.ic_launcher_foreground)
                .setContentIntent(pendingIntent)
                .build();
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @SuppressLint("MissingPermission")
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        int resultCode = intent.getIntExtra("code", -1);
        Intent data = intent.getParcelableExtra("data");
        mSaveUri = intent.getParcelableExtra("save_uri");
        MediaProjectionManager mpm = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
        MediaProjection mp = mpm.getMediaProjection(resultCode, data);

        Log.e(TAG, "onActivityResult config record >>>" );
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            AudioPlaybackCaptureConfiguration config = new AudioPlaybackCaptureConfiguration.Builder(mp)
                    .addMatchingUsage(AudioAttributes.USAGE_MEDIA) //少了这句会报错
                    .build();

            AudioFormat format = new AudioFormat.Builder()
                    .setSampleRate(44100)
                    .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                    .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
                    .build();
             AudioRecord audioRecord = new AudioRecord.Builder().
                     setAudioFormat(format).setAudioPlaybackCaptureConfig(config).build();
            mRecorder = new Recorder(audioRecord);
            mRecorder.start();
        }

        return START_STICKY;
    }

    private class Recorder extends Thread {
        private AudioRecord record;

        public Recorder(AudioRecord record) {
            this.record=record;
        }

        @Override
        public void run() {
            Log.e(TAG, "record thread start >>>" );
            int minBufferSize = AudioRecord.getMinBufferSize(44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
            byte[] buffer = new byte[minBufferSize];
            FileOutputStream os = null;
            try {
                ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(mSaveUri, "w");
                os = new FileOutputStream(pfd.getFileDescriptor());
                os.write(new byte[44]); //wav预先填充
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }

            int audioLen = 0;
            if (record.getState() == AudioRecord.STATE_INITIALIZED) {
                record.startRecording();
                while (record.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
                    int read = record.read(buffer, 0, buffer.length);
                    Log.e(TAG, "record audio data "+ read );
                    if (read > 0) {
                        // 处理录音数据
                        if (os != null) {
                            try {
                                os.write(buffer, 0, read);
                                audioLen+=read;
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
            Log.e(TAG, "audio len = " + audioLen);
            if (null != os) {
                try {
                    os.close();
                    ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(mSaveUri, "w");
                    os = new FileOutputStream(pfd.getFileDescriptor());
                    writeWaveFileHeader(os, audioLen, audioLen + 36, 44100, 1, 44100 * 2);
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            Log.e(TAG, "record thread over <<<<<" );
        }

        public void stopRecord() {
            if (null != record) {
                record.stop();
                record.release();
                record = null;
            }
        }
    }


    @Override
    public void onDestroy() {
        super.onDestroy();
        stopRecord();
        stopForeground(true);
    }

    private void stopRecord() {
        if (null != mRecorder) {
            mRecorder.stopRecord();
        }
    }
}

六、结束录制

直接在Activity中停止前台服务

1
2
3
    private void stopRecord() {
        stopService(new Intent(this, MediaProjectionService.class));
    }

七、封装WAV-PCM参考

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
 /**
     * 写入 WAV 文件头
     *
     * @param out           文件输出流
     * @param totalAudioLen 音频数据长度
     * @param totalDataLen  总数据长度
     * @param longSampleRate 采样率
     * @param channels      声道数
     * @param byteRate      字节率
     * @throws IOException 写入文件时可能抛出的异常
     */
    public static void writeWaveFileHeader(FileOutputStream out, long totalAudioLen,
                                            long totalDataLen, long longSampleRate, int channels,
                                            long byteRate) throws IOException {
        byte[] header = new byte[44];
        // RIFF/WAVE header
        header[0] = 'R';
        header[1] = 'I';
        header[2] = 'F';
        header[3] = 'F';
        header[4] = (byte) (totalDataLen & 0xff);
        header[5] = (byte) ((totalDataLen >> 8) & 0xff);
        header[6] = (byte) ((totalDataLen >> 16) & 0xff);
        header[7] = (byte) ((totalDataLen >> 24) & 0xff);
        // WAVE
        header[8] = 'W';
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        // 'fmt ' chunk
        header[12] = 'f';
        header[13] = 'm';
        header[14] = 't';
        header[15] = ' ';
        // 4 bytes: size of 'fmt ' chunk
        header[16] = 16;
        header[17] = 0;
        header[18] = 0;
        header[19] = 0;
        // format = 1
        header[20] = 1;
        header[21] = 0;
        header[22] = (byte) channels;
        header[23] = 0;
        header[24] = (byte) (longSampleRate & 0xff);
        header[25] = (byte) ((longSampleRate >> 8) & 0xff);
        header[26] = (byte) ((longSampleRate >> 16) & 0xff);
        header[27] = (byte) ((longSampleRate >> 24) & 0xff);
        header[28] = (byte) (byteRate & 0xff);
        header[29] = (byte) ((byteRate >> 8) & 0xff);
        header[30] = (byte) ((byteRate >> 16) & 0xff);
        header[31] = (byte) ((byteRate >> 24) & 0xff);
        // block align
        header[32] = (byte) (channels * 16 / 8);
        header[33] = 0;
        // bits per sample
        header[34] = 16;
        header[35] = 0;
        // data
        header[36] = 'd';
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        header[40] = (byte) (totalAudioLen & 0xff);
        header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
        header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
        header[43] = (byte) ((totalAudioLen >> 24) & 0xff);

        out.write(header, 0, 44);
    }

八、总结

在Android12机器上实测OK,录音效果也可以,主要需要注意权限的申请,前台服务的实现。 以后可以随时录制其他APP播放的音频了。