Use exoplayer for playing video on API 16+ devices

// FREEBIE
This commit is contained in:
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

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