Handle voice note media playback with ExoPlayer.

There are several (popular) phone models out there that have bugs in
their MediaPlayer implementation that cause them to be unable to play
voice notes. By moving to ExoPlayer, an application-level media player,
we should avoid most of these headaches and stardardize playback.

Fixes #7748
This commit is contained in:
Greyson Parrelli 2018-07-24 16:11:23 -04:00
parent 053e6fc223
commit 3f25fb7d5f
7 changed files with 110 additions and 81 deletions

View File

@ -76,8 +76,8 @@ dependencies {
compile 'com.google.android.gms:play-services-maps:9.6.1'
compile 'com.google.android.gms:play-services-places:9.6.1'
compile 'com.google.android.exoplayer:exoplayer-core:2.8.4'
compile 'com.google.android.exoplayer:exoplayer-ui:2.8.4'
compile 'com.google.android.exoplayer:exoplayer-core:2.9.1'
compile 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
compile 'org.whispersystems:signal-service-android:2.12.2'
compile 'org.whispersystems:webrtc-android:M69'
@ -172,8 +172,8 @@ dependencyVerification {
'com.google.android.gms:play-services-gcm:312e61253a236f2d9b750b9c04fc92fd190d23b0b2755c99de6ce4a28b259dae',
'com.google.android.gms:play-services-places:abf3a4a3b146ec7e6e753be62775e512868cf37d6f88ffe2d81167b33b57132b',
'com.google.android.gms:play-services-maps:45e8021e7ddac4a44a82a0e9698991389ded3023d35c58f38dbd86d54211ec0e',
'com.google.android.exoplayer:exoplayer-ui:027557b2d69b15e1852a2530b36971f0dcc177abae240ee35e05f63502cdb0a7',
'com.google.android.exoplayer:exoplayer-core:e69b409e11887c955deb373357c30eeabf183395db0092b4817e0f80bb467d5b',
'com.google.android.exoplayer:exoplayer-ui:7a942afcc402ff01e9bf48e8d3942850986710f06562d50a1408aaf04a683151',
'com.google.android.exoplayer:exoplayer-core:b6ab34abac36bc2bc6934b7a50008162feca2c0fde91aaf1e8c1c22f2c16e2c0',
'org.whispersystems:signal-service-android:26639df2a9c31b6f31f82034091a4ea3002ca6b1088e7fe6d30428a8290dcf2a',
'org.whispersystems:webrtc-android:5493c92141ce884fc5ce8240d783232f4fe14bd17a8d0d7d1bd4944d0bd1682f',
'me.leolin:ShortcutBadger:e3cb3e7625892129b0c92dd5e4bc649faffdd526d5af26d9c45ee31ff8851774',

View File

@ -4,7 +4,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.exoplayer2.ui.SimpleExoPlayerView
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="match_parent"

View File

@ -6,7 +6,7 @@ import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
@ -17,6 +17,21 @@ import android.support.annotation.Nullable;
import android.util.Pair;
import android.widget.Toast;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.LoadControl;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.AttachmentServer;
import org.thoughtcrime.securesms.logging.Log;
@ -43,7 +58,7 @@ public class AudioSlidePlayer implements SensorEventListener {
private final @Nullable WakeLock wakeLock;
private @NonNull WeakReference<Listener> listener;
private @Nullable MediaPlayerWrapper mediaPlayer;
private @Nullable SimpleExoPlayer mediaPlayer;
private @Nullable AttachmentServer audioAttachmentServer;
private long startTime;
@ -85,64 +100,64 @@ public class AudioSlidePlayer implements SensorEventListener {
private void play(final double progress, boolean earpiece) throws IOException {
if (this.mediaPlayer != null) return;
this.mediaPlayer = new MediaPlayerWrapper();
LoadControl loadControl = new DefaultLoadControl.Builder().setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE).createDefaultLoadControl();
this.mediaPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultRenderersFactory(context), new DefaultTrackSelector(), loadControl);
this.audioAttachmentServer = new AttachmentServer(context, slide.asAttachment());
this.startTime = System.currentTimeMillis();
audioAttachmentServer.start();
mediaPlayer.setDataSource(context, audioAttachmentServer.getUri());
mediaPlayer.prepare(createMediaSource(audioAttachmentServer.getUri()));
mediaPlayer.setPlayWhenReady(true);
mediaPlayer.setAudioStreamType(earpiece ? AudioManager.STREAM_VOICE_CALL : AudioManager.STREAM_MUSIC);
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
mediaPlayer.addListener(new Player.DefaultEventListener() {
@Override
public void onPrepared(MediaPlayer mp) {
Log.i(TAG, "onPrepared");
synchronized (AudioSlidePlayer.this) {
if (mediaPlayer == null) return;
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
switch (playbackState) {
case Player.STATE_READY:
Log.w(TAG, "onPrepared");
synchronized (AudioSlidePlayer.this) {
if (mediaPlayer == null) return;
if (progress > 0) {
mediaPlayer.seekTo((int) (mediaPlayer.getDuration() * progress));
}
if (progress > 0) {
mediaPlayer.seekTo((long) (mediaPlayer.getDuration() * progress));
}
sensorManager.registerListener(AudioSlidePlayer.this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
mediaPlayer.start();
sensorManager.registerListener(AudioSlidePlayer.this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
setPlaying(AudioSlidePlayer.this);
setPlaying(AudioSlidePlayer.this);
}
notifyOnStart();
progressEventHandler.sendEmptyMessage(0);
break;
case Player.STATE_ENDED:
Log.w(TAG, "onComplete");
synchronized (AudioSlidePlayer.this) {
mediaPlayer = null;
if (audioAttachmentServer != null) {
audioAttachmentServer.stop();
audioAttachmentServer = null;
}
sensorManager.unregisterListener(AudioSlidePlayer.this);
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
}
}
notifyOnStop();
progressEventHandler.removeMessages(0);
}
notifyOnStart();
progressEventHandler.sendEmptyMessage(0);
}
});
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
Log.i(TAG, "onComplete");
synchronized (AudioSlidePlayer.this) {
mediaPlayer = null;
if (audioAttachmentServer != null) {
audioAttachmentServer.stop();
audioAttachmentServer = null;
}
sensorManager.unregisterListener(AudioSlidePlayer.this);
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
}
}
notifyOnStop();
progressEventHandler.removeMessages(0);
}
});
mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
Log.w(TAG, "MediaPlayer Error: " + what + " , " + extra);
public void onPlayerError(ExoPlaybackException error) {
Log.w(TAG, "MediaPlayer Error: " + error);
Toast.makeText(context, R.string.AudioSlidePlayer_error_playing_audio, Toast.LENGTH_SHORT).show();
@ -163,11 +178,14 @@ public class AudioSlidePlayer implements SensorEventListener {
notifyOnStop();
progressEventHandler.removeMessages(0);
return true;
}
});
}
mediaPlayer.prepareAsync();
private MediaSource createMediaSource(@NonNull Uri uri) {
return new ExtractorMediaSource.Factory(new DefaultDataSourceFactory(context, BuildConfig.USER_AGENT))
.setExtractorsFactory(new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true))
.createMediaSource(uri);
}
public synchronized void stop() {
@ -199,7 +217,7 @@ public class AudioSlidePlayer implements SensorEventListener {
public void setListener(@NonNull Listener listener) {
this.listener = new WeakReference<>(listener);
if (this.mediaPlayer != null && this.mediaPlayer.isPlaying()) {
if (this.mediaPlayer != null && this.mediaPlayer.getPlaybackState() == Player.STATE_READY) {
notifyOnStart();
}
}
@ -214,7 +232,7 @@ public class AudioSlidePlayer implements SensorEventListener {
return new Pair<>(0D, 0);
} else {
return new Pair<>((double) mediaPlayer.getCurrentPosition() / (double) mediaPlayer.getDuration(),
mediaPlayer.getCurrentPosition());
(int) mediaPlayer.getCurrentPosition());
}
}
@ -277,7 +295,7 @@ public class AudioSlidePlayer implements SensorEventListener {
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() != Sensor.TYPE_PROXIMITY) return;
if (mediaPlayer == null || !mediaPlayer.isPlaying()) return;
if (mediaPlayer == null || mediaPlayer.getPlaybackState() != Player.STATE_READY) return;
int streamType;
@ -335,7 +353,7 @@ public class AudioSlidePlayer implements SensorEventListener {
public void handleMessage(Message msg) {
AudioSlidePlayer player = playerReference.get();
if (player == null || player.mediaPlayer == null || !player.mediaPlayer.isPlaying()) {
if (player == null || player.mediaPlayer == null || player.mediaPlayer.getPlaybackState() != ExoPlayer.STATE_READY) {
return;
}
@ -344,19 +362,4 @@ public class AudioSlidePlayer implements SensorEventListener {
sendEmptyMessageDelayed(0, 50);
}
}
private static class MediaPlayerWrapper extends MediaPlayer {
private int streamType;
@Override
public void setAudioStreamType(int streamType) {
this.streamType = streamType;
super.setAudioStreamType(streamType);
}
public int getAudioStreamType() {
return streamType;
}
}
}

View File

@ -46,6 +46,7 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
@ -66,7 +67,7 @@ public class VideoPlayer extends FrameLayout {
private static final String TAG = VideoPlayer.class.getName();
@Nullable private final VideoView videoView;
@Nullable private final SimpleExoPlayerView exoView;
@Nullable private final PlayerView exoView;
@Nullable private SimpleExoPlayer exoPlayer;
@Nullable private AttachmentServer attachmentServer;

View File

@ -6,10 +6,14 @@ import android.net.Uri;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.upstream.TransferListener;
import org.thoughtcrime.securesms.mms.PartAuthority;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class AttachmentDataSource implements DataSource {
@ -23,6 +27,10 @@ public class AttachmentDataSource implements DataSource {
this.partDataSource = partDataSource;
}
@Override
public void addTransferListener(TransferListener transferListener) {
}
@Override
public long open(DataSpec dataSpec) throws IOException {
if (PartAuthority.isLocalUri(dataSpec.uri)) dataSource = partDataSource;
@ -41,6 +49,11 @@ public class AttachmentDataSource implements DataSource {
return dataSource.getUri();
}
@Override
public Map<String, List<String>> getResponseHeaders() {
return Collections.emptyMap();
}
@Override
public void close() throws IOException {
dataSource.close();

View File

@ -13,12 +13,12 @@ public class AttachmentDataSourceFactory implements DataSource.Factory {
private final Context context;
private final DefaultDataSourceFactory defaultDataSourceFactory;
private final TransferListener<? super DataSource> listener;
private final DefaultDataSourceFactory defaultDataSourceFactory;
private final TransferListener listener;
public AttachmentDataSourceFactory(@NonNull Context context,
@NonNull DefaultDataSourceFactory defaultDataSourceFactory,
@Nullable TransferListener<? super DataSource> listener)
@Nullable TransferListener listener)
{
this.context = context;
this.defaultDataSourceFactory = defaultDataSourceFactory;

View File

@ -18,20 +18,27 @@ import org.thoughtcrime.securesms.mms.PartUriParser;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class PartDataSource implements DataSource {
private final @NonNull Context context;
private final @Nullable TransferListener<? super PartDataSource> listener;
private final @NonNull Context context;
private final @Nullable TransferListener listener;
private Uri uri;
private InputStream inputSteam;
PartDataSource(@NonNull Context context, @Nullable TransferListener<? super PartDataSource> listener) {
PartDataSource(@NonNull Context context, @Nullable TransferListener listener) {
this.context = context.getApplicationContext();
this.listener = listener;
}
@Override
public void addTransferListener(TransferListener transferListener) {
}
@Override
public long open(DataSpec dataSpec) throws IOException {
this.uri = dataSpec.uri;
@ -45,7 +52,7 @@ public class PartDataSource implements DataSource {
this.inputSteam = attachmentDatabase.getAttachmentStream(partUri.getPartId(), dataSpec.position);
if (listener != null) {
listener.onTransferStart(this, dataSpec);
listener.onTransferStart(this, dataSpec, false);
}
if (attachment.getSize() - dataSpec.position <= 0) throw new EOFException("No more data");
@ -58,7 +65,7 @@ public class PartDataSource implements DataSource {
int read = inputSteam.read(buffer, offset, readLength);
if (read > 0 && listener != null) {
listener.onBytesTransferred(this, read);
listener.onBytesTransferred(this, null, false, read);
}
return read;
@ -69,6 +76,11 @@ public class PartDataSource implements DataSource {
return uri;
}
@Override
public Map<String, List<String>> getResponseHeaders() {
return Collections.emptyMap();
}
@Override
public void close() throws IOException {
inputSteam.close();