add subtitle to the media item

PiperOrigin-RevId: 303753642
This commit is contained in:
bachinger 2020-03-30 17:10:36 +01:00 committed by Oliver Woodman
parent 0782c5f247
commit 315ba6f324
5 changed files with 208 additions and 38 deletions

View File

@ -33,7 +33,6 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player;
@ -47,8 +46,6 @@ import com.google.android.exoplayer2.offline.DownloadRequest;
import com.google.android.exoplayer2.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.ads.AdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
@ -412,43 +409,24 @@ public class PlayerActivity extends AppCompatActivity
: new UriSample[] {(UriSample) intentAsSample};
boolean seenAdsTagUri = false;
List<MediaSource> mediaSources = new ArrayList<>();
for (UriSample sample : samples) {
seenAdsTagUri |= sample.adTagUri != null;
if (!Util.checkCleartextTrafficPermitted(sample.uri)) {
showToast(R.string.error_cleartext_not_permitted);
return Collections.emptyList();
}
if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, sample.uri)) {
if (Util.maybeRequestReadExternalStoragePermission(/* activity= */ this, sample.uri)
|| (sample.subtitleInfo != null
&& Util.maybeRequestReadExternalStoragePermission(
/* activity= */ this, sample.subtitleInfo.uri))) {
// The player will be reinitialized if the permission is granted.
return Collections.emptyList();
}
}
List<MediaSource> mediaSources = new ArrayList<>();
for (UriSample sample : samples) {
MediaSource mediaSource = createLeafMediaSource(sample);
if (mediaSource == null) {
continue;
if (mediaSource != null) {
mediaSources.add(mediaSource);
}
Sample.SubtitleInfo subtitleInfo = sample.subtitleInfo;
if (subtitleInfo != null) {
if (Util.maybeRequestReadExternalStoragePermission(
/* activity= */ this, subtitleInfo.uri)) {
// The player will be reinitialized if the permission is granted.
return Collections.emptyList();
}
Format subtitleFormat =
new Format.Builder()
.setSampleMimeType(subtitleInfo.mimeType)
.setSelectionFlags(C.SELECTION_FLAG_DEFAULT)
.setLanguage(subtitleInfo.language)
.build();
MediaSource subtitleMediaSource =
new SingleSampleMediaSource.Factory(dataSourceFactory)
.createMediaSource(subtitleInfo.uri, subtitleFormat, C.TIME_UNSET);
mediaSource = new MergingMediaSource(mediaSource, subtitleMediaSource);
}
mediaSources.add(mediaSource);
}
if (seenAdsTagUri && mediaSources.size() == 1) {
Uri adTagUri = samples[0].adTagUri;
@ -497,6 +475,15 @@ public class PlayerActivity extends AppCompatActivity
drmSessionForClearTypes = parameters.drmInfo.drmSessionForClearTypes;
drmDataSourceFactory = ((DemoApplication) getApplication()).buildHttpDataSourceFactory();
}
if (parameters.subtitleInfo != null) {
builder.setSubtitles(
Collections.singletonList(
new MediaItem.Subtitle(
parameters.subtitleInfo.uri,
parameters.subtitleInfo.mimeType,
parameters.subtitleInfo.language,
C.SELECTION_FLAG_DEFAULT)));
}
DownloadRequest downloadRequest =
((DemoApplication) getApplication())
@ -531,7 +518,7 @@ public class PlayerActivity extends AppCompatActivity
debugViewHelper = null;
player.release();
player = null;
mediaSources = null;
mediaSources = Collections.emptyList();
trackSelector = null;
}
if (adsLoader != null) {

View File

@ -61,12 +61,14 @@ public final class MediaItem {
@Nullable private UUID drmUuid;
private boolean drmMultiSession;
private List<StreamKey> streamKeys;
private List<Subtitle> subtitles;
@Nullable private Object tag;
@Nullable private MediaMetadata mediaMetadata;
/** Creates a builder. */
public Builder() {
streamKeys = Collections.emptyList();
subtitles = Collections.emptyList();
drmLicenseRequestHeaders = Collections.emptyMap();
}
@ -138,6 +140,8 @@ public final class MediaItem {
/**
* Sets the optional request headers attached to the drm license request.
*
* <p>{@code null} or an empty {@link Map} can be used for a reset.
*
* <p>If no valid drm configuration is specified, the drm license request headers are ignored.
*/
public Builder setDrmLicenseRequestHeaders(
@ -176,6 +180,8 @@ public final class MediaItem {
* Sets the optional stream keys by which the manifest is filtered (only used for adaptive
* streams).
*
* <p>{@code null} or an empty {@link List} can be used for a reset.
*
* <p>If a {@link PlaybackProperties#sourceUri} is set, the stream keys are used to create a
* {@link PlaybackProperties} object. Otherwise it will be ignored.
*/
@ -187,6 +193,22 @@ public final class MediaItem {
return this;
}
/**
* Sets the optional subtitles.
*
* <p>{@code null} or an empty {@link List} can be used for a reset.
*
* <p>If a {@link PlaybackProperties#sourceUri} is set, the subtitles are used to create a
* {@link PlaybackProperties} object. Otherwise it will be ignored.
*/
public Builder setSubtitles(@Nullable List<Subtitle> subtitles) {
this.subtitles =
subtitles != null && !subtitles.isEmpty()
? Collections.unmodifiableList(new ArrayList<>(subtitles))
: Collections.emptyList();
return this;
}
/**
* Sets the optional tag for custom attributes. The tag for the media source which will be
* published in the {@link com.google.android.exoplayer2.Timeline} of the source as {@link
@ -222,6 +244,7 @@ public final class MediaItem {
drmUuid, drmLicenseUri, drmLicenseRequestHeaders, drmMultiSession)
: null,
streamKeys,
subtitles,
tag);
mediaId = mediaId != null ? mediaId : sourceUri.toString();
}
@ -266,7 +289,7 @@ public final class MediaItem {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
if (!(obj instanceof DrmConfiguration)) {
return false;
}
@ -307,6 +330,9 @@ public final class MediaItem {
/** Optional stream keys by which the manifest is filtered. */
public final List<StreamKey> streamKeys;
/** Optional subtitles to be sideloaded. */
public final List<Subtitle> subtitles;
/**
* Optional tag for custom attributes. The tag for the media source which will be published in
* the {@link com.google.android.exoplayer2.Timeline} of the source as {@link
@ -319,11 +345,13 @@ public final class MediaItem {
@Nullable String mimeType,
@Nullable DrmConfiguration drmConfiguration,
List<StreamKey> streamKeys,
List<Subtitle> subtitles,
@Nullable Object tag) {
this.sourceUri = sourceUri;
this.mimeType = mimeType;
this.drmConfiguration = drmConfiguration;
this.streamKeys = streamKeys;
this.subtitles = subtitles;
this.tag = tag;
}
@ -332,7 +360,7 @@ public final class MediaItem {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
if (!(obj instanceof PlaybackProperties)) {
return false;
}
PlaybackProperties other = (PlaybackProperties) obj;
@ -340,7 +368,8 @@ public final class MediaItem {
return sourceUri.equals(other.sourceUri)
&& Util.areEqual(mimeType, other.mimeType)
&& Util.areEqual(drmConfiguration, other.drmConfiguration)
&& Util.areEqual(streamKeys, other.streamKeys)
&& streamKeys.equals(other.streamKeys)
&& subtitles.equals(other.subtitles)
&& Util.areEqual(tag, other.tag);
}
@ -350,11 +379,78 @@ public final class MediaItem {
result = 31 * result + (mimeType == null ? 0 : mimeType.hashCode());
result = 31 * result + (drmConfiguration == null ? 0 : drmConfiguration.hashCode());
result = 31 * result + streamKeys.hashCode();
result = 31 * result + subtitles.hashCode();
result = 31 * result + (tag == null ? 0 : tag.hashCode());
return result;
}
}
/** Properties for a text track. */
public static final class Subtitle {
/** The {@link Uri} to the subtitle file. */
public final Uri uri;
/** The MIME type. */
public final String mimeType;
/** The language. */
@Nullable public final String language;
/** The selection flags. */
@C.SelectionFlags public final int selectionFlags;
/**
* Creates an instance.
*
* @param uri The {@link Uri uri} to the subtitle file.
* @param mimeType The mime type.
* @param language The optional language.
*/
public Subtitle(Uri uri, String mimeType, @Nullable String language) {
this(uri, mimeType, language, /* selectionFlags= */ 0);
}
/**
* Creates an instance with the given selection flags.
*
* @param uri The {@link Uri uri} to the subtitle file.
* @param mimeType The mime type.
* @param language The optional language.
* @param selectionFlags The selection flags.
*/
public Subtitle(
Uri uri, String mimeType, @Nullable String language, @C.SelectionFlags int selectionFlags) {
this.uri = uri;
this.mimeType = mimeType;
this.language = language;
this.selectionFlags = selectionFlags;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Subtitle)) {
return false;
}
Subtitle other = (Subtitle) obj;
return uri.equals(other.uri)
&& mimeType.equals(other.mimeType)
&& Util.areEqual(language, other.language)
&& selectionFlags == other.selectionFlags;
}
@Override
public int hashCode() {
int result = uri.hashCode();
result = 31 * result + mimeType.hashCode();
result = 31 * result + (language == null ? 0 : language.hashCode());
result = 31 * result + selectionFlags;
return result;
}
}
/** Identifies the media item. */
public final String mediaId;
@ -374,15 +470,15 @@ public final class MediaItem {
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (o == null || getClass() != o.getClass()) {
if (!(obj instanceof MediaItem)) {
return false;
}
MediaItem other = (MediaItem) o;
MediaItem other = (MediaItem) obj;
return Util.areEqual(mediaId, other.mediaId)
&& Util.areEqual(playbackProperties, other.playbackProperties)

View File

@ -21,6 +21,7 @@ import android.util.SparseArray;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManager;
@ -122,6 +123,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
return new DefaultMediaSourceFactory(context, dataSourceFactory);
}
private final DataSource.Factory dataSourceFactory;
private final SparseArray<MediaSourceFactory> mediaSourceFactories;
@C.ContentType private final int[] supportedTypes;
private final String userAgent;
@ -133,6 +135,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
@Nullable private List<StreamKey> streamKeys;
private DefaultMediaSourceFactory(Context context, DataSource.Factory dataSourceFactory) {
this.dataSourceFactory = dataSourceFactory;
drmSessionManager = DrmSessionManager.getDummyDrmSessionManager();
userAgent = Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY);
drmHttpDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent);
@ -244,7 +247,30 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
!mediaItem.playbackProperties.streamKeys.isEmpty()
? mediaItem.playbackProperties.streamKeys
: streamKeys);
return mediaSourceFactory.createMediaSource(mediaItem);
MediaSource leafMediaSource = mediaSourceFactory.createMediaSource(mediaItem);
if (mediaItem.playbackProperties.subtitles.isEmpty()) {
return leafMediaSource;
}
List<MediaItem.Subtitle> subtitles = mediaItem.playbackProperties.subtitles;
MediaSource[] mediaSources = new MediaSource[subtitles.size() + 1];
mediaSources[0] = leafMediaSource;
SingleSampleMediaSource.Factory singleSampleSourceFactory =
new SingleSampleMediaSource.Factory(dataSourceFactory);
for (int i = 0; i < subtitles.size(); i++) {
MediaItem.Subtitle subtitle = subtitles.get(i);
Format subtitleFormat =
new Format.Builder()
.setSampleMimeType(subtitle.mimeType)
.setLanguage(subtitle.language)
.setSelectionFlags(subtitle.selectionFlags)
.build();
mediaSources[i + 1] =
singleSampleSourceFactory.createMediaSource(subtitle.uri, subtitleFormat, C.TIME_UNSET);
}
return new MergingMediaSource(mediaSources);
}
// internal methods

View File

@ -23,6 +23,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.util.MimeTypes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -130,6 +131,24 @@ public class MediaItemTest {
assertThat(mediaItem.playbackProperties.streamKeys).isEqualTo(streamKeys);
}
@Test
public void builderSetSubtitles_setsSubtitles() {
List<MediaItem.Subtitle> subtitles =
Arrays.asList(
new MediaItem.Subtitle(
Uri.parse(URI_STRING + "/en"), MimeTypes.APPLICATION_TTML, /* language= */ "en"),
new MediaItem.Subtitle(
Uri.parse(URI_STRING + "/de"),
MimeTypes.APPLICATION_TTML,
/* language= */ null,
C.SELECTION_FLAG_DEFAULT));
MediaItem mediaItem =
new MediaItem.Builder().setSourceUri(URI_STRING).setSubtitles(subtitles).build();
assertThat(mediaItem.playbackProperties.subtitles).isEqualTo(subtitles);
}
@Test
public void builderSetTag_isNullByDefault() {
MediaItem mediaItem = new MediaItem.Builder().setSourceUri(URI_STRING).build();

View File

@ -17,10 +17,15 @@ package com.google.android.exoplayer2.source;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.util.MimeTypes;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -29,6 +34,7 @@ import org.junit.runner.RunWith;
public final class DefaultMediaSourceFactoryTest {
private static final String URI_MEDIA = "http://exoplayer.dev/video";
private static final String URI_TEXT = "http://exoplayer.dev/text";
@Test
public void createMediaSource_withoutMimeType_progressiveSource() {
@ -80,6 +86,42 @@ public final class DefaultMediaSourceFactoryTest {
assertThat(mediaSource).isNotNull();
}
@Test
public void createMediaSource_withSubtitle_isMergingMediaSource() {
DefaultMediaSourceFactory defaultMediaSourceFactory =
DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext());
List<MediaItem.Subtitle> subtitles =
Arrays.asList(
new MediaItem.Subtitle(Uri.parse(URI_TEXT), MimeTypes.APPLICATION_TTML, "en"),
new MediaItem.Subtitle(
Uri.parse(URI_TEXT), MimeTypes.APPLICATION_TTML, "de", C.SELECTION_FLAG_DEFAULT));
MediaItem mediaItem =
new MediaItem.Builder().setSourceUri(URI_MEDIA).setSubtitles(subtitles).build();
MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem);
assertThat(mediaSource).isInstanceOf(MergingMediaSource.class);
}
@Test
public void createMediaSource_withSubtitle_hasTag() {
DefaultMediaSourceFactory defaultMediaSourceFactory =
DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext());
Object tag = new Object();
MediaItem mediaItem =
new MediaItem.Builder()
.setTag(tag)
.setSourceUri(URI_MEDIA)
.setSubtitles(
Collections.singletonList(
new MediaItem.Subtitle(Uri.parse(URI_TEXT), MimeTypes.APPLICATION_TTML, "en")))
.build();
MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem);
assertThat(mediaSource.getTag()).isEqualTo(tag);
}
@Test
public void getSupportedTypes_coreModule_onlyOther() {
int[] supportedTypes =