blob: df03c3e08f087525e8c33bfe524b3c5458255599 [file] [log] [blame]
Jay Aliomer0bd491a2020-03-16 14:34:10 -04001/*
2 * Copyright (C) 2020 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui.screenrecord;
18
Jay Aliomer0bd491a2020-03-16 14:34:10 -040019import android.media.AudioAttributes;
20import android.media.AudioFormat;
21import android.media.AudioPlaybackCaptureConfiguration;
22import android.media.AudioRecord;
23import android.media.MediaCodec;
24import android.media.MediaCodecInfo;
25import android.media.MediaFormat;
26import android.media.MediaMuxer;
27import android.media.MediaRecorder;
28import android.media.projection.MediaProjection;
29import android.util.Log;
30
31import java.io.IOException;
32import java.nio.ByteBuffer;
33
34/**
35 * Recording internal audio
36 */
37public class ScreenInternalAudioRecorder {
38 private static String TAG = "ScreenAudioRecorder";
39 private static final int TIMEOUT = 500;
Jay Aliomer246a25e2020-05-26 22:57:34 -040040 private static final float MIC_VOLUME_SCALE = 1.4f;
Jay Aliomer0bd491a2020-03-16 14:34:10 -040041 private AudioRecord mAudioRecord;
42 private AudioRecord mAudioRecordMic;
43 private Config mConfig = new Config();
44 private Thread mThread;
45 private MediaProjection mMediaProjection;
46 private MediaCodec mCodec;
47 private long mPresentationTime;
48 private long mTotalBytes;
49 private MediaMuxer mMuxer;
Jay Aliomer0bd491a2020-03-16 14:34:10 -040050 private boolean mMic;
51
52 private int mTrackId = -1;
53
Beth Thibodeau231ac9b2020-06-17 22:34:42 -040054 public ScreenInternalAudioRecorder(String outFile, MediaProjection mp, boolean includeMicInput)
55 throws IOException {
Jay Aliomer0bd491a2020-03-16 14:34:10 -040056 mMic = includeMicInput;
Jay Aliomer0bd491a2020-03-16 14:34:10 -040057 mMuxer = new MediaMuxer(outFile, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
Jay Aliomer0bd491a2020-03-16 14:34:10 -040058 mMediaProjection = mp;
59 Log.d(TAG, "creating audio file " + outFile);
60 setupSimple();
61 }
62 /**
63 * Audio recoding configuration
64 */
65 public static class Config {
66 public int channelOutMask = AudioFormat.CHANNEL_OUT_MONO;
67 public int channelInMask = AudioFormat.CHANNEL_IN_MONO;
68 public int encoding = AudioFormat.ENCODING_PCM_16BIT;
69 public int sampleRate = 44100;
70 public int bitRate = 196000;
71 public int bufferSizeBytes = 1 << 17;
72 public boolean privileged = true;
73 public boolean legacy_app_looback = false;
74
75 @Override
76 public String toString() {
77 return "channelMask=" + channelOutMask
78 + "\n encoding=" + encoding
79 + "\n sampleRate=" + sampleRate
80 + "\n bufferSize=" + bufferSizeBytes
81 + "\n privileged=" + privileged
82 + "\n legacy app looback=" + legacy_app_looback;
83 }
84
85 }
86
87 private void setupSimple() throws IOException {
88 int size = AudioRecord.getMinBufferSize(
89 mConfig.sampleRate, mConfig.channelInMask,
90 mConfig.encoding) * 2;
91
92 Log.d(TAG, "audio buffer size: " + size);
93
94 AudioFormat format = new AudioFormat.Builder()
95 .setEncoding(mConfig.encoding)
96 .setSampleRate(mConfig.sampleRate)
97 .setChannelMask(mConfig.channelOutMask)
98 .build();
99
100 AudioPlaybackCaptureConfiguration playbackConfig =
101 new AudioPlaybackCaptureConfiguration.Builder(mMediaProjection)
102 .addMatchingUsage(AudioAttributes.USAGE_MEDIA)
103 .addMatchingUsage(AudioAttributes.USAGE_UNKNOWN)
104 .addMatchingUsage(AudioAttributes.USAGE_GAME)
105 .build();
106
107 mAudioRecord = new AudioRecord.Builder()
108 .setAudioFormat(format)
109 .setAudioPlaybackCaptureConfig(playbackConfig)
110 .build();
111
112 if (mMic) {
113 mAudioRecordMic = new AudioRecord(MediaRecorder.AudioSource.VOICE_COMMUNICATION,
114 mConfig.sampleRate, AudioFormat.CHANNEL_IN_MONO, mConfig.encoding, size);
115 }
116
117 mCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
118 MediaFormat medFormat = MediaFormat.createAudioFormat(
119 MediaFormat.MIMETYPE_AUDIO_AAC, mConfig.sampleRate, 1);
120 medFormat.setInteger(MediaFormat.KEY_AAC_PROFILE,
121 MediaCodecInfo.CodecProfileLevel.AACObjectLC);
122 medFormat.setInteger(MediaFormat.KEY_BIT_RATE, mConfig.bitRate);
123 medFormat.setInteger(MediaFormat.KEY_PCM_ENCODING, mConfig.encoding);
124 mCodec.configure(medFormat,
125 null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
126
127 mThread = new Thread(() -> {
128 short[] bufferInternal = null;
129 short[] bufferMic = null;
130 byte[] buffer = null;
131
132 if (mMic) {
133 bufferInternal = new short[size / 2];
134 bufferMic = new short[size / 2];
135 } else {
136 buffer = new byte[size];
137 }
138
139 while (true) {
140 int readBytes = 0;
141 int readShortsInternal = 0;
142 int readShortsMic = 0;
143 if (mMic) {
144 readShortsInternal = mAudioRecord.read(bufferInternal, 0,
145 bufferInternal.length);
146 readShortsMic = mAudioRecordMic.read(bufferMic, 0, bufferMic.length);
Jay Aliomer246a25e2020-05-26 22:57:34 -0400147
148 // modify the volume
149 bufferMic = scaleValues(bufferMic,
150 readShortsMic, MIC_VOLUME_SCALE);
Jay Aliomer0bd491a2020-03-16 14:34:10 -0400151 readBytes = Math.min(readShortsInternal, readShortsMic) * 2;
152 buffer = addAndConvertBuffers(bufferInternal, readShortsInternal, bufferMic,
153 readShortsMic);
154 } else {
155 readBytes = mAudioRecord.read(buffer, 0, buffer.length);
156 }
157
158 //exit the loop when at end of stream
159 if (readBytes < 0) {
160 Log.e(TAG, "read error " + readBytes +
161 ", shorts internal: " + readShortsInternal +
162 ", shorts mic: " + readShortsMic);
163 break;
164 }
165 encode(buffer, readBytes);
166 }
167 endStream();
168 });
169 }
170
Jay Aliomer246a25e2020-05-26 22:57:34 -0400171 private short[] scaleValues(short[] buff, int len, float scale) {
172 for (int i = 0; i < len; i++) {
173 int oldValue = buff[i];
174 int newValue = (int) (buff[i] * scale);
175 if (newValue > Short.MAX_VALUE) {
176 newValue = Short.MAX_VALUE;
177 } else if (newValue < Short.MIN_VALUE) {
178 newValue = Short.MIN_VALUE;
179 }
180 buff[i] = (short) (newValue);
181 }
182 return buff;
183 }
Jay Aliomer0bd491a2020-03-16 14:34:10 -0400184 private byte[] addAndConvertBuffers(short[] a1, int a1Limit, short[] a2, int a2Limit) {
185 int size = Math.max(a1Limit, a2Limit);
186 if (size < 0) return new byte[0];
187 byte[] buff = new byte[size * 2];
188 for (int i = 0; i < size; i++) {
189 int sum;
190 if (i > a1Limit) {
191 sum = a2[i];
192 } else if (i > a2Limit) {
193 sum = a1[i];
194 } else {
195 sum = (int) a1[i] + (int) a2[i];
196 }
197
198 if (sum > Short.MAX_VALUE) sum = Short.MAX_VALUE;
199 if (sum < Short.MIN_VALUE) sum = Short.MIN_VALUE;
200 int byteIndex = i * 2;
201 buff[byteIndex] = (byte) (sum & 0xff);
202 buff[byteIndex + 1] = (byte) ((sum >> 8) & 0xff);
203 }
204 return buff;
205 }
206
207 private void encode(byte[] buffer, int readBytes) {
208 int offset = 0;
209 while (readBytes > 0) {
210 int totalBytesRead = 0;
211 int bufferIndex = mCodec.dequeueInputBuffer(TIMEOUT);
212 if (bufferIndex < 0) {
213 writeOutput();
214 return;
215 }
216 ByteBuffer buff = mCodec.getInputBuffer(bufferIndex);
217 buff.clear();
218 int bufferSize = buff.capacity();
219 int bytesToRead = readBytes > bufferSize ? bufferSize : readBytes;
220 totalBytesRead += bytesToRead;
221 readBytes -= bytesToRead;
222 buff.put(buffer, offset, bytesToRead);
223 offset += bytesToRead;
224 mCodec.queueInputBuffer(bufferIndex, 0, bytesToRead, mPresentationTime, 0);
225 mTotalBytes += totalBytesRead;
226 mPresentationTime = 1000000L * (mTotalBytes / 2) / mConfig.sampleRate;
227
228 writeOutput();
229 }
230 }
231
232 private void endStream() {
233 int bufferIndex = mCodec.dequeueInputBuffer(TIMEOUT);
234 mCodec.queueInputBuffer(bufferIndex, 0, 0, mPresentationTime,
235 MediaCodec.BUFFER_FLAG_END_OF_STREAM);
236 writeOutput();
237 }
238
239 private void writeOutput() {
240 while (true) {
241 MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
242 int bufferIndex = mCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT);
243 if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
244 mTrackId = mMuxer.addTrack(mCodec.getOutputFormat());
245 mMuxer.start();
246 continue;
247 }
248 if (bufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
249 break;
250 }
251 if (mTrackId < 0) return;
252 ByteBuffer buff = mCodec.getOutputBuffer(bufferIndex);
253
254 if (!((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0
255 && bufferInfo.size != 0)) {
256 mMuxer.writeSampleData(mTrackId, buff, bufferInfo);
257 }
258 mCodec.releaseOutputBuffer(bufferIndex, false);
259 }
260 }
261
262 /**
263 * start recording
Beth Thibodeau231ac9b2020-06-17 22:34:42 -0400264 * @throws IllegalStateException if recording fails to initialize
Jay Aliomer0bd491a2020-03-16 14:34:10 -0400265 */
Beth Thibodeau231ac9b2020-06-17 22:34:42 -0400266 public void start() throws IllegalStateException {
Jay Aliomer0bd491a2020-03-16 14:34:10 -0400267 if (mThread != null) {
268 Log.e(TAG, "a recording is being done in parallel or stop is not called");
269 }
270 mAudioRecord.startRecording();
271 if (mMic) mAudioRecordMic.startRecording();
272 Log.d(TAG, "channel count " + mAudioRecord.getChannelCount());
273 mCodec.start();
274 if (mAudioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
Beth Thibodeau231ac9b2020-06-17 22:34:42 -0400275 throw new IllegalStateException("Audio recording failed to start");
Jay Aliomer0bd491a2020-03-16 14:34:10 -0400276 }
277 mThread.start();
278 }
279
280 /**
281 * end recording
282 */
283 public void end() {
284 mAudioRecord.stop();
285 if (mMic) {
286 mAudioRecordMic.stop();
287 }
288 mAudioRecord.release();
289 if (mMic) {
290 mAudioRecordMic.release();
291 }
292 try {
293 mThread.join();
294 } catch (InterruptedException e) {
295 e.printStackTrace();
296 }
297 mCodec.stop();
298 mCodec.release();
299 mMuxer.stop();
300 mMuxer.release();
301 mThread = null;
302 }
303}