From 3f25fb7d5f973e09ce294d10767ef3ad786b6449 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Tue, 24 Jul 2018 16:11:23 -0400 Subject: [PATCH] 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 --- build.gradle | 8 +- res/layout-v16/video_player.xml | 2 +- .../securesms/audio/AudioSlidePlayer.java | 137 +++++++++--------- .../securesms/video/VideoPlayer.java | 3 +- .../video/exo/AttachmentDataSource.java | 13 ++ .../exo/AttachmentDataSourceFactory.java | 6 +- .../securesms/video/exo/PartDataSource.java | 22 ++- 7 files changed, 110 insertions(+), 81 deletions(-) diff --git a/build.gradle b/build.gradle index 5d3e3b461d..d9e355b023 100644 --- a/build.gradle +++ b/build.gradle @@ -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', diff --git a/res/layout-v16/video_player.xml b/res/layout-v16/video_player.xml index 9d8b075204..ba3075f06f 100644 --- a/res/layout-v16/video_player.xml +++ b/res/layout-v16/video_player.xml @@ -4,7 +4,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - 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; - } - } } diff --git a/src/org/thoughtcrime/securesms/video/VideoPlayer.java b/src/org/thoughtcrime/securesms/video/VideoPlayer.java index 0a965aa61c..b309b3e2f2 100644 --- a/src/org/thoughtcrime/securesms/video/VideoPlayer.java +++ b/src/org/thoughtcrime/securesms/video/VideoPlayer.java @@ -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; diff --git a/src/org/thoughtcrime/securesms/video/exo/AttachmentDataSource.java b/src/org/thoughtcrime/securesms/video/exo/AttachmentDataSource.java index d3cf6f2b82..2989ff35c2 100644 --- a/src/org/thoughtcrime/securesms/video/exo/AttachmentDataSource.java +++ b/src/org/thoughtcrime/securesms/video/exo/AttachmentDataSource.java @@ -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> getResponseHeaders() { + return Collections.emptyMap(); + } + @Override public void close() throws IOException { dataSource.close(); diff --git a/src/org/thoughtcrime/securesms/video/exo/AttachmentDataSourceFactory.java b/src/org/thoughtcrime/securesms/video/exo/AttachmentDataSourceFactory.java index ec216148cb..0d9410f7d0 100644 --- a/src/org/thoughtcrime/securesms/video/exo/AttachmentDataSourceFactory.java +++ b/src/org/thoughtcrime/securesms/video/exo/AttachmentDataSourceFactory.java @@ -13,12 +13,12 @@ public class AttachmentDataSourceFactory implements DataSource.Factory { private final Context context; - private final DefaultDataSourceFactory defaultDataSourceFactory; - private final TransferListener listener; + private final DefaultDataSourceFactory defaultDataSourceFactory; + private final TransferListener listener; public AttachmentDataSourceFactory(@NonNull Context context, @NonNull DefaultDataSourceFactory defaultDataSourceFactory, - @Nullable TransferListener listener) + @Nullable TransferListener listener) { this.context = context; this.defaultDataSourceFactory = defaultDataSourceFactory; diff --git a/src/org/thoughtcrime/securesms/video/exo/PartDataSource.java b/src/org/thoughtcrime/securesms/video/exo/PartDataSource.java index 220c733d83..78116e5992 100644 --- a/src/org/thoughtcrime/securesms/video/exo/PartDataSource.java +++ b/src/org/thoughtcrime/securesms/video/exo/PartDataSource.java @@ -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 listener; + private final @NonNull Context context; + private final @Nullable TransferListener listener; private Uri uri; private InputStream inputSteam; - PartDataSource(@NonNull Context context, @Nullable TransferListener 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> getResponseHeaders() { + return Collections.emptyMap(); + } + @Override public void close() throws IOException { inputSteam.close();