From 4fd41080acf56837596cc6cdf47877539206c2f9 Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Thu, 13 Apr 2017 14:15:06 -0700 Subject: [PATCH] Use exoplayer for playing video on API 16+ devices // FREEBIE --- build.gradle | 5 +- res/layout-v16/video_player.xml | 14 +++ .../securesms/video/VideoPlayer.java | 85 +++++++++++++++--- .../video/exo/AttachmentDataSource.java | 48 ++++++++++ .../exo/AttachmentDataSourceFactory.java | 37 ++++++++ .../securesms/video/exo/PartDataSource.java | 88 +++++++++++++++++++ 6 files changed, 264 insertions(+), 13 deletions(-) create mode 100644 res/layout-v16/video_player.xml create mode 100644 src/org/thoughtcrime/securesms/video/exo/AttachmentDataSource.java create mode 100644 src/org/thoughtcrime/securesms/video/exo/AttachmentDataSourceFactory.java create mode 100644 src/org/thoughtcrime/securesms/video/exo/PartDataSource.java diff --git a/build.gradle b/build.gradle index 016b4374b6..ea4287034e 100644 --- a/build.gradle +++ b/build.gradle @@ -57,6 +57,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:r2.3.1' + compile 'org.whispersystems:jobmanager:1.0.2' compile 'org.whispersystems:libpastelog:1.0.7' compile 'org.whispersystems:signal-service-android:2.5.5' @@ -127,6 +129,7 @@ dependencyVerification { 'com.google.android.gms:play-services-gcm:312e61253a236f2d9b750b9c04fc92fd190d23b0b2755c99de6ce4a28b259dae', 'com.google.android.gms:play-services-maps:45e8021e7ddac4a44a82a0e9698991389ded3023d35c58f38dbd86d54211ec0e', 'com.google.android.gms:play-services-places:abf3a4a3b146ec7e6e753be62775e512868cf37d6f88ffe2d81167b33b57132b', + 'com.google.android.exoplayer:exoplayer:955085aa611a8f7cf6c61b88ae03d1a392f4ad94c9bfbc153f3dedb9ffb14718', 'org.whispersystems:jobmanager:506f679fc2fcf7bb6d10f00f41d6f6ea0abf75c70dc95b913398661ad538a181', 'org.whispersystems:libpastelog:bb331d9a98240fc139101128ba836c1edec3c40e000597cdbb29ebf4cbf34d88', 'org.whispersystems:signal-service-android:3d7859b194e518fbaf5a082daf22ca345411705e825791f751eb388f149583c3', @@ -154,7 +157,6 @@ dependencyVerification { 'com.davemorrissey.labs:subsampling-scale-image-view:550c5baa07e0bb4ff0a18b705e96d34436d22619248bd8c08c08c730b1f55cfe', 'cn.carbswang.android:NumberPickerView:18b3c316d62c7c277978a8d4ed57a5b8f4e943762264960f579a8a549c756729', 'com.tomergoldst.android:tooltips:4c56697dd1ad64b8066535c61f961a6d901e7ae5d97ae27084ba40ad620349b6', - 'com.android.support:support-annotations:fb941680f43afbd70ce01ec3cc837a5037f0a774701b12a9fd3090bd4727cf15', 'com.android.support:support-v4:ed4cda7c752f51d33f9bbdfff3422b425b323d356cd1bdc9786aa413c912e594', 'com.android.support:support-vector-drawable:2697503d3e8e709023ae176ba5db7f98ca0aa0b4e6290aedcb3c371904806bf7', 'com.android.support:animated-vector-drawable:6d05cb63d1f68900220f85c56dfe1066a9bb19cb0ec1247cc68fc2ba32f6b4a7', @@ -180,6 +182,7 @@ dependencyVerification { 'com.fasterxml.jackson.core:jackson-annotations:0ca408c24202a7626ec8b861e99d85eca5e38b73311dd6dd12e3e9deecc3fe94', 'com.fasterxml.jackson.core:jackson-core:cbf4604784b4de226262845447a1ad3bb38a6728cebe86562e2c5afada8be2c0', 'com.squareup.okio:okio:8c5436cadfab36bbd97db5f5c43b7bfdb5bf2f5f894ec8709b1929f14bdd010c', + 'com.android.support:support-annotations:47a2a30eab487a490a8a8f16678007c3d2b6dcae1e09b0485a12bbf921200ec3', 'com.android.support:support-media-compat:8d6a1a5ba3d9eb1a25cb8f21bb312ac6280202e3d2900cb0b447d065d0d8a125', 'com.android.support:support-core-utils:a7649e18c04143dde40c218c5ce9a030e7ae674089cd7b18c6cf8ed2a22cf01a', 'com.android.support:support-fragment:1294500b357f52cf3779e2521c79f54ae7844f3b9a5f6727495dbbda7f231377', diff --git a/res/layout-v16/video_player.xml b/res/layout-v16/video_player.xml new file mode 100644 index 0000000000..9d8b075204 --- /dev/null +++ b/res/layout-v16/video_player.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/video/VideoPlayer.java b/src/org/thoughtcrime/securesms/video/VideoPlayer.java index bda1779fed..3e1f60226e 100644 --- a/src/org/thoughtcrime/securesms/video/VideoPlayer.java +++ b/src/org/thoughtcrime/securesms/video/VideoPlayer.java @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.video; import android.content.Context; +import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.AttributeSet; @@ -11,12 +12,30 @@ import android.widget.MediaController; import android.widget.Toast; import android.widget.VideoView; +import com.google.android.exoplayer2.DefaultLoadControl; +import com.google.android.exoplayer2.ExoPlayerFactory; +import com.google.android.exoplayer2.LoadControl; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.extractor.ExtractorsFactory; +import com.google.android.exoplayer2.source.ExtractorMediaSource; +import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelector; +import com.google.android.exoplayer2.ui.SimpleExoPlayerView; +import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; + import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.attachments.AttachmentServer; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.VideoSlide; import org.thoughtcrime.securesms.util.ViewUtil; +import org.thoughtcrime.securesms.video.exo.AttachmentDataSourceFactory; import java.io.IOException; @@ -24,8 +43,11 @@ public class VideoPlayer extends FrameLayout { private static final String TAG = VideoPlayer.class.getName(); - @NonNull private final VideoView videoView; - @Nullable private AttachmentServer attachmentServer; + @Nullable private final VideoView videoView; + @Nullable private final SimpleExoPlayerView exoView; + + @Nullable private SimpleExoPlayer exoPlayer; + @Nullable private AttachmentServer attachmentServer; public VideoPlayer(Context context) { this(context, null); @@ -40,12 +62,57 @@ public class VideoPlayer extends FrameLayout { inflate(context, R.layout.video_player, this); - this.videoView = ViewUtil.findById(this, R.id.video_view); - - initializeVideoViewControls(videoView); + if (Build.VERSION.SDK_INT >= 16) { + this.exoView = ViewUtil.findById(this, R.id.video_view); + this.videoView = null; + } else { + this.videoView = ViewUtil.findById(this, R.id.video_view); + this.exoView = null; + initializeVideoViewControls(videoView); + } } - public void setVideoSource(@NonNull MasterSecret masterSecret, @NonNull VideoSlide videoSource) throws IOException { + public void setVideoSource(@NonNull MasterSecret masterSecret, @NonNull VideoSlide videoSource) + throws IOException + { + if (Build.VERSION.SDK_INT >= 14) setExoViewSource(masterSecret, videoSource); + else setVideoViewSource(masterSecret, videoSource); + } + + public void cleanup() { + if (this.attachmentServer != null) { + this.attachmentServer.stop(); + } + + if (this.exoPlayer != null) { + this.exoPlayer.release(); + } + } + + private void setExoViewSource(@NonNull MasterSecret masterSecret, @NonNull VideoSlide videoSource) + throws IOException + { + BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(); + TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(bandwidthMeter); + TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); + LoadControl loadControl = new DefaultLoadControl(); + + exoPlayer = ExoPlayerFactory.newSimpleInstance(getContext(), trackSelector, loadControl); + exoView.setPlayer(exoPlayer); + + DefaultDataSourceFactory defaultDataSourceFactory = new DefaultDataSourceFactory(getContext(), "GenericUserAgent", null); + AttachmentDataSourceFactory attachmentDataSourceFactory = new AttachmentDataSourceFactory(getContext(), masterSecret, defaultDataSourceFactory, null); + ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); + + MediaSource mediaSource = new ExtractorMediaSource(videoSource.getUri(), attachmentDataSourceFactory, extractorsFactory, null, null); + + exoPlayer.prepare(mediaSource); + exoPlayer.setPlayWhenReady(true); + } + + private void setVideoViewSource(@NonNull MasterSecret masterSecret, @NonNull VideoSlide videoSource) + throws IOException + { if (this.attachmentServer != null) { this.attachmentServer.stop(); } @@ -67,12 +134,6 @@ public class VideoPlayer extends FrameLayout { this.videoView.start(); } - public void cleanup() { - if (this.attachmentServer != null) { - this.attachmentServer.stop(); - } - } - private void initializeVideoViewControls(@NonNull VideoView videoView) { MediaController mediaController = new MediaController(getContext()); mediaController.setAnchorView(videoView); diff --git a/src/org/thoughtcrime/securesms/video/exo/AttachmentDataSource.java b/src/org/thoughtcrime/securesms/video/exo/AttachmentDataSource.java new file mode 100644 index 0000000000..d3cf6f2b82 --- /dev/null +++ b/src/org/thoughtcrime/securesms/video/exo/AttachmentDataSource.java @@ -0,0 +1,48 @@ +package org.thoughtcrime.securesms.video.exo; + + +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 org.thoughtcrime.securesms.mms.PartAuthority; + +import java.io.IOException; + +public class AttachmentDataSource implements DataSource { + + private final DefaultDataSource defaultDataSource; + private final PartDataSource partDataSource; + + private DataSource dataSource; + + public AttachmentDataSource(DefaultDataSource defaultDataSource, PartDataSource partDataSource) { + this.defaultDataSource = defaultDataSource; + this.partDataSource = partDataSource; + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + if (PartAuthority.isLocalUri(dataSpec.uri)) dataSource = partDataSource; + else dataSource = defaultDataSource; + + return dataSource.open(dataSpec); + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws IOException { + return dataSource.read(buffer, offset, readLength); + } + + @Override + public Uri getUri() { + return dataSource.getUri(); + } + + @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 new file mode 100644 index 0000000000..657e565364 --- /dev/null +++ b/src/org/thoughtcrime/securesms/video/exo/AttachmentDataSourceFactory.java @@ -0,0 +1,37 @@ +package org.thoughtcrime.securesms.video.exo; + + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; +import com.google.android.exoplayer2.upstream.TransferListener; + +import org.thoughtcrime.securesms.crypto.MasterSecret; + +public class AttachmentDataSourceFactory implements DataSource.Factory { + + private final Context context; + private final MasterSecret masterSecret; + + private final DefaultDataSourceFactory defaultDataSourceFactory; + private final TransferListener listener; + + public AttachmentDataSourceFactory(@NonNull Context context, @NonNull MasterSecret masterSecret, + @NonNull DefaultDataSourceFactory defaultDataSourceFactory, + @Nullable TransferListener listener) + { + this.context = context; + this.masterSecret = masterSecret; + this.defaultDataSourceFactory = defaultDataSourceFactory; + this.listener = listener; + } + + @Override + public AttachmentDataSource createDataSource() { + return new AttachmentDataSource(defaultDataSourceFactory.createDataSource(), + new PartDataSource(context, masterSecret, listener)); + } +} diff --git a/src/org/thoughtcrime/securesms/video/exo/PartDataSource.java b/src/org/thoughtcrime/securesms/video/exo/PartDataSource.java new file mode 100644 index 0000000000..92e6a7b841 --- /dev/null +++ b/src/org/thoughtcrime/securesms/video/exo/PartDataSource.java @@ -0,0 +1,88 @@ +package org.thoughtcrime.securesms.video.exo; + + +import android.content.Context; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.TransferListener; + +import org.thoughtcrime.securesms.attachments.Attachment; +import org.thoughtcrime.securesms.crypto.MasterSecret; +import org.thoughtcrime.securesms.database.AttachmentDatabase; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.mms.PartUriParser; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +public class PartDataSource implements DataSource { + + private final @NonNull Context context; + private final @NonNull MasterSecret masterSecret; + private final @Nullable TransferListener listener; + + private Uri uri; + private InputStream inputSteam; + + public PartDataSource(@NonNull Context context, + @NonNull MasterSecret masterSecret, + @Nullable TransferListener listener) + { + this.context = context.getApplicationContext(); + this.masterSecret = masterSecret; + this.listener = listener; + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + this.uri = dataSpec.uri; + + AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context); + PartUriParser partUri = new PartUriParser(uri); + Attachment attachment = attachmentDatabase.getAttachment(masterSecret, partUri.getPartId()); + + if (attachment == null) throw new IOException("Attachment not found"); + + this.inputSteam = attachmentDatabase.getAttachmentStream(masterSecret, partUri.getPartId()); + + if (inputSteam == null) throw new IOException("InputStream not foudn"); + + long skipped = this.inputSteam.skip(dataSpec.position); + + if (skipped != dataSpec.position) throw new IOException("Skip failed!"); + + if (listener != null) { + listener.onTransferStart(this, dataSpec); + } + + if (attachment.getSize() - dataSpec.position <= 0) throw new EOFException("No more data"); + + return attachment.getSize() - dataSpec.position; + } + + @Override + public int read(byte[] buffer, int offset, int readLength) throws IOException { + int read = inputSteam.read(buffer, offset, readLength); + + if (read > 0 && listener != null) { + listener.onBytesTransferred(this, read); + } + + return read; + } + + @Override + public Uri getUri() { + return uri; + } + + @Override + public void close() throws IOException { + inputSteam.close(); + } +}