Use exoplayer for playing video on API 16+ devices

// FREEBIE
master
Moxie Marlinspike 2017-04-13 14:15:06 -07:00
parent fad697ba2a
commit 4fd41080ac
6 changed files with 264 additions and 13 deletions

View File

@ -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',

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.exoplayer2.ui.SimpleExoPlayerView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"/>
</FrameLayout>

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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<? super DataSource> listener;
public AttachmentDataSourceFactory(@NonNull Context context, @NonNull MasterSecret masterSecret,
@NonNull DefaultDataSourceFactory defaultDataSourceFactory,
@Nullable TransferListener<? super DataSource> 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));
}
}

View File

@ -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<? super PartDataSource> listener;
private Uri uri;
private InputStream inputSteam;
public PartDataSource(@NonNull Context context,
@NonNull MasterSecret masterSecret,
@Nullable TransferListener<? super PartDataSource> 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();
}
}