kopia lustrzana https://github.com/ryukoposting/Signal-Android
Use MediaRecorder for voice notes on capable devices.
Co-authored-by: Greyson Parrelli <greyson@signal.org>fork-5.53.8
rodzic
e6451db888
commit
b04ae3a8b3
|
@ -1,13 +1,12 @@
|
||||||
package org.thoughtcrime.securesms.audio;
|
package org.thoughtcrime.securesms.audio;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.media.AudioFormat;
|
import android.media.AudioFormat;
|
||||||
import android.media.AudioRecord;
|
import android.media.AudioRecord;
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.media.MediaCodecInfo;
|
import android.media.MediaCodecInfo;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
import android.media.MediaRecorder;
|
import android.media.MediaRecorder;
|
||||||
import android.os.Build;
|
import android.os.ParcelFileDescriptor;
|
||||||
|
|
||||||
import org.signal.core.util.StreamUtil;
|
import org.signal.core.util.StreamUtil;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
|
@ -17,8 +16,7 @@ import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
public class AudioCodec implements Recorder {
|
||||||
public class AudioCodec {
|
|
||||||
|
|
||||||
private static final String TAG = Log.tag(AudioCodec.class);
|
private static final String TAG = Log.tag(AudioCodec.class);
|
||||||
|
|
||||||
|
@ -51,12 +49,19 @@ public class AudioCodec {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start(ParcelFileDescriptor fileDescriptor) {
|
||||||
|
Log.i(TAG, "Recording voice note using AudioCodec.");
|
||||||
|
start(new ParcelFileDescriptor.AutoCloseOutputStream(fileDescriptor));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public synchronized void stop() {
|
public synchronized void stop() {
|
||||||
running = false;
|
running = false;
|
||||||
while (!finished) Util.wait(this, 0);
|
while (!finished) Util.wait(this, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void start(final OutputStream outputStream) {
|
private void start(final OutputStream outputStream) {
|
||||||
new Thread(new Runnable() {
|
new Thread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package org.thoughtcrime.securesms.audio;
|
package org.thoughtcrime.securesms.audio;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
@ -13,6 +12,7 @@ import org.signal.core.util.concurrent.SignalExecutors;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
|
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||||
|
@ -20,7 +20,6 @@ import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
|
||||||
public class AudioRecorder {
|
public class AudioRecorder {
|
||||||
|
|
||||||
private static final String TAG = Log.tag(AudioRecorder.class);
|
private static final String TAG = Log.tag(AudioRecorder.class);
|
||||||
|
@ -29,8 +28,8 @@ public class AudioRecorder {
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
||||||
private AudioCodec audioCodec;
|
private Recorder recorder;
|
||||||
private Uri captureUri;
|
private Uri captureUri;
|
||||||
|
|
||||||
public AudioRecorder(@NonNull Context context) {
|
public AudioRecorder(@NonNull Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
@ -42,7 +41,7 @@ public class AudioRecorder {
|
||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
Log.i(TAG, "Running startRecording() + " + Thread.currentThread().getId());
|
Log.i(TAG, "Running startRecording() + " + Thread.currentThread().getId());
|
||||||
try {
|
try {
|
||||||
if (audioCodec != null) {
|
if (recorder != null) {
|
||||||
throw new AssertionError("We can only record once at a time.");
|
throw new AssertionError("We can only record once at a time.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,9 +51,9 @@ public class AudioRecorder {
|
||||||
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
|
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
|
||||||
.withMimeType(MediaUtil.AUDIO_AAC)
|
.withMimeType(MediaUtil.AUDIO_AAC)
|
||||||
.createForDraftAttachmentAsync(context, () -> Log.i(TAG, "Write successful."), e -> Log.w(TAG, "Error during recording", e));
|
.createForDraftAttachmentAsync(context, () -> Log.i(TAG, "Write successful."), e -> Log.w(TAG, "Error during recording", e));
|
||||||
audioCodec = new AudioCodec();
|
|
||||||
|
|
||||||
audioCodec.start(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1]));
|
recorder = Build.VERSION.SDK_INT >= 26 && FeatureFlags.voiceNoteRecordingV2() ? new MediaRecorderWrapper() : new AudioCodec();
|
||||||
|
recorder.start(fds[1]);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
}
|
}
|
||||||
|
@ -67,12 +66,12 @@ public class AudioRecorder {
|
||||||
final SettableFuture<VoiceNoteDraft> future = new SettableFuture<>();
|
final SettableFuture<VoiceNoteDraft> future = new SettableFuture<>();
|
||||||
|
|
||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
if (audioCodec == null) {
|
if (recorder == null) {
|
||||||
sendToFuture(future, new IOException("MediaRecorder was never initialized successfully!"));
|
sendToFuture(future, new IOException("MediaRecorder was never initialized successfully!"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
audioCodec.stop();
|
recorder.stop();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
long size = MediaUtil.getMediaSize(context, captureUri);
|
long size = MediaUtil.getMediaSize(context, captureUri);
|
||||||
|
@ -82,7 +81,7 @@ public class AudioRecorder {
|
||||||
sendToFuture(future, ioe);
|
sendToFuture(future, ioe);
|
||||||
}
|
}
|
||||||
|
|
||||||
audioCodec = null;
|
recorder = null;
|
||||||
captureUri = null;
|
captureUri = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
package org.thoughtcrime.securesms.audio;
|
||||||
|
|
||||||
|
import android.media.MediaRecorder;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
|
||||||
|
import org.signal.core.util.logging.Log;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap Android's {@link MediaRecorder} for use with voice notes.
|
||||||
|
*/
|
||||||
|
public class MediaRecorderWrapper implements Recorder {
|
||||||
|
|
||||||
|
private static final String TAG = Log.tag(MediaRecorderWrapper.class);
|
||||||
|
|
||||||
|
private static final int SAMPLE_RATE = 44100;
|
||||||
|
private static final int CHANNELS = 1;
|
||||||
|
private static final int BIT_RATE = 32000;
|
||||||
|
|
||||||
|
private MediaRecorder recorder = null;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start(ParcelFileDescriptor fileDescriptor) throws IOException {
|
||||||
|
Log.i(TAG, "Recording voice note using MediaRecorderWrapper.");
|
||||||
|
recorder = new MediaRecorder();
|
||||||
|
recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
|
||||||
|
recorder.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS);
|
||||||
|
recorder.setOutputFile(fileDescriptor.getFileDescriptor());
|
||||||
|
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
|
||||||
|
recorder.setAudioSamplingRate(SAMPLE_RATE);
|
||||||
|
recorder.setAudioEncodingBitRate(BIT_RATE);
|
||||||
|
recorder.setAudioChannels(CHANNELS);
|
||||||
|
recorder.prepare();
|
||||||
|
recorder.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {
|
||||||
|
recorder.stop();
|
||||||
|
recorder.release();
|
||||||
|
recorder = null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package org.thoughtcrime.securesms.audio;
|
||||||
|
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple abstraction of the interface for the original voice note recording and the new.
|
||||||
|
*/
|
||||||
|
public interface Recorder {
|
||||||
|
void start(ParcelFileDescriptor fileDescriptor) throws IOException;
|
||||||
|
void stop();
|
||||||
|
}
|
|
@ -87,6 +87,7 @@ public final class FeatureFlags {
|
||||||
private static final String DONOR_BADGES = "android.donorBadges.6";
|
private static final String DONOR_BADGES = "android.donorBadges.6";
|
||||||
private static final String DONOR_BADGES_DISPLAY = "android.donorBadges.display.4";
|
private static final String DONOR_BADGES_DISPLAY = "android.donorBadges.display.4";
|
||||||
private static final String CDSH = "android.cdsh";
|
private static final String CDSH = "android.cdsh";
|
||||||
|
private static final String VOICE_NOTE_RECORDING_V2 = "android.voiceNoteRecordingV2";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||||
|
@ -127,7 +128,8 @@ public final class FeatureFlags {
|
||||||
SENDER_KEY_MAX_AGE,
|
SENDER_KEY_MAX_AGE,
|
||||||
DONOR_BADGES,
|
DONOR_BADGES,
|
||||||
DONOR_BADGES_DISPLAY,
|
DONOR_BADGES_DISPLAY,
|
||||||
CHANGE_NUMBER_ENABLED
|
CHANGE_NUMBER_ENABLED,
|
||||||
|
VOICE_NOTE_RECORDING_V2
|
||||||
);
|
);
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
@ -181,7 +183,8 @@ public final class FeatureFlags {
|
||||||
CDSH,
|
CDSH,
|
||||||
SENDER_KEY_MAX_AGE,
|
SENDER_KEY_MAX_AGE,
|
||||||
DONOR_BADGES_DISPLAY,
|
DONOR_BADGES_DISPLAY,
|
||||||
DONATE_MEGAPHONE
|
DONATE_MEGAPHONE,
|
||||||
|
VOICE_NOTE_RECORDING_V2
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -427,6 +430,11 @@ public final class FeatureFlags {
|
||||||
return Environment.IS_STAGING && getBoolean(CDSH, false);
|
return Environment.IS_STAGING && getBoolean(CDSH, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether or not to use the new voice note recorder backed by MediaRecorder. */
|
||||||
|
public static boolean voiceNoteRecordingV2() {
|
||||||
|
return getBoolean(VOICE_NOTE_RECORDING_V2, true);
|
||||||
|
}
|
||||||
|
|
||||||
/** Only for rendering debug info. */
|
/** Only for rendering debug info. */
|
||||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||||
return new TreeMap<>(REMOTE_VALUES);
|
return new TreeMap<>(REMOTE_VALUES);
|
||||||
|
|
Ładowanie…
Reference in New Issue