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-maps:9.6.1'
compile 'com.google.android.gms:play-services-places: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-core:2.9.1'
compile 'com.google.android.exoplayer:exoplayer-ui:2.8.4' compile 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
compile 'org.whispersystems:signal-service-android:2.12.2' compile 'org.whispersystems:signal-service-android:2.12.2'
compile 'org.whispersystems:webrtc-android:M69' 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-gcm:312e61253a236f2d9b750b9c04fc92fd190d23b0b2755c99de6ce4a28b259dae',
'com.google.android.gms:play-services-places:abf3a4a3b146ec7e6e753be62775e512868cf37d6f88ffe2d81167b33b57132b', 'com.google.android.gms:play-services-places:abf3a4a3b146ec7e6e753be62775e512868cf37d6f88ffe2d81167b33b57132b',
'com.google.android.gms:play-services-maps:45e8021e7ddac4a44a82a0e9698991389ded3023d35c58f38dbd86d54211ec0e', 'com.google.android.gms:play-services-maps:45e8021e7ddac4a44a82a0e9698991389ded3023d35c58f38dbd86d54211ec0e',
'com.google.android.exoplayer:exoplayer-ui:027557b2d69b15e1852a2530b36971f0dcc177abae240ee35e05f63502cdb0a7', 'com.google.android.exoplayer:exoplayer-ui:7a942afcc402ff01e9bf48e8d3942850986710f06562d50a1408aaf04a683151',
'com.google.android.exoplayer:exoplayer-core:e69b409e11887c955deb373357c30eeabf183395db0092b4817e0f80bb467d5b', 'com.google.android.exoplayer:exoplayer-core:b6ab34abac36bc2bc6934b7a50008162feca2c0fde91aaf1e8c1c22f2c16e2c0',
'org.whispersystems:signal-service-android:26639df2a9c31b6f31f82034091a4ea3002ca6b1088e7fe6d30428a8290dcf2a', 'org.whispersystems:signal-service-android:26639df2a9c31b6f31f82034091a4ea3002ca6b1088e7fe6d30428a8290dcf2a',
'org.whispersystems:webrtc-android:5493c92141ce884fc5ce8240d783232f4fe14bd17a8d0d7d1bd4944d0bd1682f', 'org.whispersystems:webrtc-android:5493c92141ce884fc5ce8240d783232f4fe14bd17a8d0d7d1bd4944d0bd1682f',
'me.leolin:ShortcutBadger:e3cb3e7625892129b0c92dd5e4bc649faffdd526d5af26d9c45ee31ff8851774', 'me.leolin:ShortcutBadger:e3cb3e7625892129b0c92dd5e4bc649faffdd526d5af26d9c45ee31ff8851774',

View File

@ -4,7 +4,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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:id="@+id/video_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@ -6,7 +6,7 @@ import android.hardware.SensorEvent;
import android.hardware.SensorEventListener; import android.hardware.SensorEventListener;
import android.hardware.SensorManager; import android.hardware.SensorManager;
import android.media.AudioManager; import android.media.AudioManager;
import android.media.MediaPlayer; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Handler; import android.os.Handler;
import android.os.Message; import android.os.Message;
@ -17,6 +17,21 @@ import android.support.annotation.Nullable;
import android.util.Pair; import android.util.Pair;
import android.widget.Toast; 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.R;
import org.thoughtcrime.securesms.attachments.AttachmentServer; import org.thoughtcrime.securesms.attachments.AttachmentServer;
import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.logging.Log;
@ -43,7 +58,7 @@ public class AudioSlidePlayer implements SensorEventListener {
private final @Nullable WakeLock wakeLock; private final @Nullable WakeLock wakeLock;
private @NonNull WeakReference<Listener> listener; private @NonNull WeakReference<Listener> listener;
private @Nullable MediaPlayerWrapper mediaPlayer; private @Nullable SimpleExoPlayer mediaPlayer;
private @Nullable AttachmentServer audioAttachmentServer; private @Nullable AttachmentServer audioAttachmentServer;
private long startTime; private long startTime;
@ -85,40 +100,41 @@ public class AudioSlidePlayer implements SensorEventListener {
private void play(final double progress, boolean earpiece) throws IOException { private void play(final double progress, boolean earpiece) throws IOException {
if (this.mediaPlayer != null) return; 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.audioAttachmentServer = new AttachmentServer(context, slide.asAttachment());
this.startTime = System.currentTimeMillis(); this.startTime = System.currentTimeMillis();
audioAttachmentServer.start(); 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.setAudioStreamType(earpiece ? AudioManager.STREAM_VOICE_CALL : AudioManager.STREAM_MUSIC);
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { mediaPlayer.addListener(new Player.DefaultEventListener() {
@Override @Override
public void onPrepared(MediaPlayer mp) { public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
Log.i(TAG, "onPrepared"); switch (playbackState) {
case Player.STATE_READY:
Log.w(TAG, "onPrepared");
synchronized (AudioSlidePlayer.this) { synchronized (AudioSlidePlayer.this) {
if (mediaPlayer == null) return; if (mediaPlayer == null) return;
if (progress > 0) { if (progress > 0) {
mediaPlayer.seekTo((int) (mediaPlayer.getDuration() * progress)); mediaPlayer.seekTo((long) (mediaPlayer.getDuration() * progress));
} }
sensorManager.registerListener(AudioSlidePlayer.this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL); sensorManager.registerListener(AudioSlidePlayer.this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
mediaPlayer.start();
setPlaying(AudioSlidePlayer.this); setPlaying(AudioSlidePlayer.this);
} }
notifyOnStart(); notifyOnStart();
progressEventHandler.sendEmptyMessage(0); progressEventHandler.sendEmptyMessage(0);
} break;
});
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { case Player.STATE_ENDED:
@Override Log.w(TAG, "onComplete");
public void onCompletion(MediaPlayer mp) {
Log.i(TAG, "onComplete");
synchronized (AudioSlidePlayer.this) { synchronized (AudioSlidePlayer.this) {
mediaPlayer = null; mediaPlayer = null;
@ -137,12 +153,11 @@ public class AudioSlidePlayer implements SensorEventListener {
notifyOnStop(); notifyOnStop();
progressEventHandler.removeMessages(0); progressEventHandler.removeMessages(0);
} }
}); }
mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override @Override
public boolean onError(MediaPlayer mp, int what, int extra) { public void onPlayerError(ExoPlaybackException error) {
Log.w(TAG, "MediaPlayer Error: " + what + " , " + extra); Log.w(TAG, "MediaPlayer Error: " + error);
Toast.makeText(context, R.string.AudioSlidePlayer_error_playing_audio, Toast.LENGTH_SHORT).show(); Toast.makeText(context, R.string.AudioSlidePlayer_error_playing_audio, Toast.LENGTH_SHORT).show();
@ -163,11 +178,14 @@ public class AudioSlidePlayer implements SensorEventListener {
notifyOnStop(); notifyOnStop();
progressEventHandler.removeMessages(0); 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() { public synchronized void stop() {
@ -199,7 +217,7 @@ public class AudioSlidePlayer implements SensorEventListener {
public void setListener(@NonNull Listener listener) { public void setListener(@NonNull Listener listener) {
this.listener = new WeakReference<>(listener); this.listener = new WeakReference<>(listener);
if (this.mediaPlayer != null && this.mediaPlayer.isPlaying()) { if (this.mediaPlayer != null && this.mediaPlayer.getPlaybackState() == Player.STATE_READY) {
notifyOnStart(); notifyOnStart();
} }
} }
@ -214,7 +232,7 @@ public class AudioSlidePlayer implements SensorEventListener {
return new Pair<>(0D, 0); return new Pair<>(0D, 0);
} else { } else {
return new Pair<>((double) mediaPlayer.getCurrentPosition() / (double) mediaPlayer.getDuration(), return new Pair<>((double) mediaPlayer.getCurrentPosition() / (double) mediaPlayer.getDuration(),
mediaPlayer.getCurrentPosition()); (int) mediaPlayer.getCurrentPosition());
} }
} }
@ -277,7 +295,7 @@ public class AudioSlidePlayer implements SensorEventListener {
@Override @Override
public void onSensorChanged(SensorEvent event) { public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() != Sensor.TYPE_PROXIMITY) return; 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; int streamType;
@ -335,7 +353,7 @@ public class AudioSlidePlayer implements SensorEventListener {
public void handleMessage(Message msg) { public void handleMessage(Message msg) {
AudioSlidePlayer player = playerReference.get(); 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; return;
} }
@ -344,19 +362,4 @@ public class AudioSlidePlayer implements SensorEventListener {
sendEmptyMessageDelayed(0, 50); 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.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector; 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.ui.SimpleExoPlayerView;
import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
@ -66,7 +67,7 @@ public class VideoPlayer extends FrameLayout {
private static final String TAG = VideoPlayer.class.getName(); private static final String TAG = VideoPlayer.class.getName();
@Nullable private final VideoView videoView; @Nullable private final VideoView videoView;
@Nullable private final SimpleExoPlayerView exoView; @Nullable private final PlayerView exoView;
@Nullable private SimpleExoPlayer exoPlayer; @Nullable private SimpleExoPlayer exoPlayer;
@Nullable private AttachmentServer attachmentServer; @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.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DefaultDataSource; import com.google.android.exoplayer2.upstream.DefaultDataSource;
import com.google.android.exoplayer2.upstream.TransferListener;
import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.PartAuthority;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class AttachmentDataSource implements DataSource { public class AttachmentDataSource implements DataSource {
@ -23,6 +27,10 @@ public class AttachmentDataSource implements DataSource {
this.partDataSource = partDataSource; this.partDataSource = partDataSource;
} }
@Override
public void addTransferListener(TransferListener transferListener) {
}
@Override @Override
public long open(DataSpec dataSpec) throws IOException { public long open(DataSpec dataSpec) throws IOException {
if (PartAuthority.isLocalUri(dataSpec.uri)) dataSource = partDataSource; if (PartAuthority.isLocalUri(dataSpec.uri)) dataSource = partDataSource;
@ -41,6 +49,11 @@ public class AttachmentDataSource implements DataSource {
return dataSource.getUri(); return dataSource.getUri();
} }
@Override
public Map<String, List<String>> getResponseHeaders() {
return Collections.emptyMap();
}
@Override @Override
public void close() throws IOException { public void close() throws IOException {
dataSource.close(); dataSource.close();

View File

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

View File

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