public static final String KEY_FLAC_COMPRESSION_LEVEL = "flac-compression-level";
/**
- * A key for boolean AUTOSELECT field. Tracks with AUTOSELECT=true are
- * considered when automatically selecting a track without specific user
- * choice (as defined by HLS).
- * @hide
+ * A key for boolean AUTOSELECT behavior for the track. Tracks with AUTOSELECT=true
+ * are considered when automatically selecting a track without specific user
+ * choice, based on the current locale.
+ * This is currently only used for subtitle tracks, when the user selected
+ * 'Default' for the captioning locale.
+ * The associated value is an integer, where non-0 means TRUE. This is an optional
+ * field; if not specified, AUTOSELECT defaults to TRUE.
*/
- public static final String KEY_AUTOSELECT = "autoselect";
+ public static final String KEY_IS_AUTOSELECT = "is-autoselect";
/**
- * A key for boolean DEFAULT field. The track with DEFAULT=true is selected
- * in the absence of a specific user choice (as defined by HLS).
- * @hide
+ * A key for boolean DEFAULT behavior for the track. The track with DEFAULT=true is
+ * selected in the absence of a specific user choice.
+ * This is currently only used for subtitle tracks, when the user selected
+ * 'Default' for the captioning locale.
+ * The associated value is an integer, where non-0 means TRUE. This is an optional
+ * field; if not specified, DEFAULT is considered to be FALSE.
*/
- public static final String KEY_DEFAULT = "default";
+ public static final String KEY_IS_DEFAULT = "is-default";
+
/**
- * A key for boolean FORCED field for subtitle tracks. True if it is a
- * forced subtitle track.
- * @hide
+ * A key for the FORCED field for subtitle tracks. True if it is a
+ * forced subtitle track. Forced subtitle tracks are essential for the
+ * content and are shown even when the user turns off Captions. They
+ * are used for example to translate foreign/alien dialogs or signs.
+ * The associated value is an integer, where non-0 means TRUE. This is an
+ * optional field; if not specified, FORCED defaults to FALSE.
*/
- public static final String KEY_FORCED = "forced";
+ public static final String KEY_IS_FORCED_SUBTITLE = "is-forced-subtitle";
/* package private */ MediaFormat(Map<String, Object> map) {
mMap = map;
} else if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
mFormat = MediaFormat.createSubtitleFormat(
MEDIA_MIMETYPE_TEXT_VTT, language);
- mFormat.setInteger(MediaFormat.KEY_AUTOSELECT, in.readInt());
- mFormat.setInteger(MediaFormat.KEY_DEFAULT, in.readInt());
- mFormat.setInteger(MediaFormat.KEY_FORCED, in.readInt());
+ mFormat.setInteger(MediaFormat.KEY_IS_AUTOSELECT, in.readInt());
+ mFormat.setInteger(MediaFormat.KEY_IS_DEFAULT, in.readInt());
+ mFormat.setInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE, in.readInt());
} else {
mFormat = new MediaFormat();
mFormat.setString(MediaFormat.KEY_LANGUAGE, language);
dest.writeString(getLanguage());
if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
- dest.writeInt(mFormat.getInteger(MediaFormat.KEY_AUTOSELECT));
- dest.writeInt(mFormat.getInteger(MediaFormat.KEY_DEFAULT));
- dest.writeInt(mFormat.getInteger(MediaFormat.KEY_FORCED));
+ dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_AUTOSELECT));
+ dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_DEFAULT));
+ dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE));
}
}
@Override
public void onSubtitleTrackSelected(SubtitleTrack track) {
if (mSelectedSubtitleTrackIndex >= 0) {
- deselectTrack(mSelectedSubtitleTrackIndex);
+ try {
+ selectOrDeselectInbandTrack(mSelectedSubtitleTrackIndex, false);
+ } catch (IllegalStateException e) {
+ }
+ mSelectedSubtitleTrackIndex = -1;
}
- mSelectedSubtitleTrackIndex = -1;
setOnSubtitleDataListener(null);
for (int i = 0; i < mInbandSubtitleTracks.length; i++) {
if (mInbandSubtitleTracks[i] == track) {
Log.v(TAG, "Selecting subtitle track " + i);
- selectTrack(i);
mSelectedSubtitleTrackIndex = i;
+ try {
+ selectOrDeselectInbandTrack(mSelectedSubtitleTrackIndex, true);
+ } catch (IllegalStateException e) {
+ }
setOnSubtitleDataListener(mSubtitleDataListener);
break;
}
private void selectOrDeselectTrack(int index, boolean select)
throws IllegalStateException {
- // ignore out-of-band tracks
- TrackInfo[] trackInfo = getInbandTrackInfo();
- if (index >= trackInfo.length &&
- index < trackInfo.length + mOutOfBandSubtitleTracks.size()) {
+ // handle subtitle track through subtitle controller
+ SubtitleTrack track = null;
+ if (index < mInbandSubtitleTracks.length) {
+ track = mInbandSubtitleTracks[index];
+ } else if (index < mInbandSubtitleTracks.length + mOutOfBandSubtitleTracks.size()) {
+ track = mOutOfBandSubtitleTracks.get(index - mInbandSubtitleTracks.length);
+ }
+
+ if (mSubtitleController != null && track != null) {
+ if (select) {
+ mSubtitleController.selectTrack(track);
+ } else if (mSubtitleController.getSelectedTrack() == track) {
+ mSubtitleController.selectTrack(null);
+ } else {
+ Log.w(TAG, "trying to deselect track that was not selected");
+ }
return;
}
+ selectOrDeselectInbandTrack(index, select);
+ }
+
+ private void selectOrDeselectInbandTrack(int index, boolean select)
+ throws IllegalStateException {
Parcel request = Parcel.obtain();
Parcel reply = Parcel.obtain();
try {
private boolean mShowing;
private CaptioningManager mCaptioningManager;
+ private CaptioningManager.CaptioningChangeListener mCaptioningChangeListener =
+ new CaptioningManager.CaptioningChangeListener() {
+ /** @hide */
+ @Override
+ public void onEnabledChanged(boolean enabled) {
+ selectDefaultTrack();
+ }
+
+ /** @hide */
+ @Override
+ public void onLocaleChanged(Locale locale) {
+ selectDefaultTrack();
+ }
+ };
+
/**
* Creates a subtitle controller for a media playback object that implements
* the MediaTimeProvider interface.
(CaptioningManager)context.getSystemService(Context.CAPTIONING_SERVICE);
}
+ @Override
+ protected void finalize() throws Throwable {
+ mCaptioningManager.removeCaptioningChangeListener(
+ mCaptioningChangeListener);
+ super.finalize();
+ }
+
/**
* @return the available subtitle tracks for this media. These include
* the tracks found by {@link MediaPlayer} as well as any tracks added
* manually via {@link #addTrack}.
*/
public SubtitleTrack[] getTracks() {
- SubtitleTrack[] tracks = new SubtitleTrack[mTracks.size()];
- mTracks.toArray(tracks);
- return tracks;
+ synchronized(mTracks) {
+ SubtitleTrack[] tracks = new SubtitleTrack[mTracks.size()];
+ mTracks.toArray(tracks);
+ return tracks;
+ }
}
/**
* in-band data from the {@link MediaPlayer}. However, this does
* not change the subtitle visibility.
*
+ * Must be called from the UI thread.
+ *
* @param track The subtitle track to select. This must be one of the
* tracks in {@link #getTracks}.
* @return true if the track was successfully selected.
}
mSelectedTrack = track;
- mAnchor.setSubtitleWidget(getRenderingWidget());
+ if (mAnchor != null) {
+ mAnchor.setSubtitleWidget(getRenderingWidget());
+ }
if (mSelectedTrack != null) {
mSelectedTrack.setTimeProvider(mTimeProvider);
/**
* @return the default subtitle track based on system preferences, or null,
* if no such track exists in this manager.
+ *
+ * Supports HLS-flags: AUTOSELECT, FORCED & DEFAULT.
+ *
+ * 1. If captioning is disabled, only consider FORCED tracks. Otherwise,
+ * consider all tracks, but prefer non-FORCED ones.
+ * 2. If user selected "Default" caption language:
+ * a. If there is a considered track with DEFAULT=yes, returns that track
+ * (favor the first one in the current language if there are more than
+ * one default tracks, or the first in general if none of them are in
+ * the current language).
+ * b. Otherwise, if there is a track with AUTOSELECT=yes in the current
+ * language, return that one.
+ * c. If there are no default tracks, and no autoselectable tracks in the
+ * current language, return null.
+ * 3. If there is a track with the caption language, select that one. Prefer
+ * the one with AUTOSELECT=no.
+ *
+ * The default values for these flags are DEFAULT=no, AUTOSELECT=yes
+ * and FORCED=no.
+ *
+ * Must be called from the UI thread.
*/
public SubtitleTrack getDefaultTrack() {
- Locale locale = mCaptioningManager.getLocale();
-
- for (SubtitleTrack track: mTracks) {
- MediaFormat format = track.getFormat();
- String language = format.getString(MediaFormat.KEY_LANGUAGE);
- // TODO: select track with best renderer. For now, we select first
- // track with local's language or first track if locale has none
- if (locale == null ||
- locale.getLanguage().equals("") ||
- locale.getISO3Language().equals(language) ||
- locale.getLanguage().equals(language)) {
- return track;
+ SubtitleTrack bestTrack = null;
+ int bestScore = -1;
+
+ Locale selectedLocale = mCaptioningManager.getLocale();
+ Locale locale = selectedLocale;
+ if (locale == null) {
+ locale = Locale.getDefault();
+ }
+ boolean selectForced = !mCaptioningManager.isEnabled();
+
+ synchronized(mTracks) {
+ for (SubtitleTrack track: mTracks) {
+ MediaFormat format = track.getFormat();
+ String language = format.getString(MediaFormat.KEY_LANGUAGE);
+ boolean forced =
+ format.getInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE, 0) != 0;
+ boolean autoselect =
+ format.getInteger(MediaFormat.KEY_IS_AUTOSELECT, 1) != 0;
+ boolean is_default =
+ format.getInteger(MediaFormat.KEY_IS_DEFAULT, 0) != 0;
+
+ boolean languageMatches =
+ (locale == null ||
+ locale.getLanguage().equals("") ||
+ locale.getISO3Language().equals(language) ||
+ locale.getLanguage().equals(language));
+ // is_default is meaningless unless caption language is 'default'
+ int score = (forced ? 0 : 8) +
+ (((selectedLocale == null) && is_default) ? 4 : 0) +
+ (autoselect ? 0 : 2) + (languageMatches ? 1 : 0);
+
+ if (selectForced && !forced) {
+ continue;
+ }
+
+ // we treat null locale/language as matching any language
+ if ((selectedLocale == null && is_default) ||
+ (languageMatches &&
+ (autoselect || forced || selectedLocale != null))) {
+ if (score > bestScore) {
+ bestScore = score;
+ bestTrack = track;
+ }
+ }
}
}
- return null;
+ return bestTrack;
}
private boolean mTrackIsExplicit = false;
private boolean mVisibilityIsExplicit = false;
- /** @hide */
+ /** @hide - called from UI thread */
public void selectDefaultTrack() {
if (mTrackIsExplicit) {
+ // If track selection is explicit, but visibility
+ // is not, it falls back to the captioning setting
+ if (!mVisibilityIsExplicit) {
+ if (mCaptioningManager.isEnabled() ||
+ (mSelectedTrack != null &&
+ mSelectedTrack.getFormat().getInteger(
+ MediaFormat.KEY_IS_FORCED_SUBTITLE, 0) != 0)) {
+ show();
+ } else {
+ hide();
+ }
+ mVisibilityIsExplicit = false;
+ }
return;
}
+ // We can have a default (forced) track even if captioning
+ // is not enabled. This is handled by getDefaultTrack().
+ // Show this track unless subtitles were explicitly hidden.
SubtitleTrack track = getDefaultTrack();
if (track != null) {
selectTrack(track);
mTrackIsExplicit = false;
if (!mVisibilityIsExplicit) {
- if (mCaptioningManager.isEnabled()) {
- show();
- } else {
- hide();
- }
+ show();
mVisibilityIsExplicit = false;
}
}
}
- /** @hide */
+ /** @hide - called from UI thread */
public void reset() {
hide();
selectTrack(null);
mTracks.clear();
mTrackIsExplicit = false;
mVisibilityIsExplicit = false;
+ mCaptioningManager.removeCaptioningChangeListener(
+ mCaptioningChangeListener);
}
/**
* @return the created {@link SubtitleTrack} object
*/
public SubtitleTrack addTrack(MediaFormat format) {
- for (Renderer renderer: mRenderers) {
- if (renderer.supports(format)) {
- SubtitleTrack track = renderer.createTrack(format);
- if (track != null) {
- mTracks.add(track);
- return track;
+ synchronized(mRenderers) {
+ for (Renderer renderer: mRenderers) {
+ if (renderer.supports(format)) {
+ SubtitleTrack track = renderer.createTrack(format);
+ if (track != null) {
+ synchronized(mTracks) {
+ if (mTracks.size() == 0) {
+ mCaptioningManager.addCaptioningChangeListener(
+ mCaptioningChangeListener);
+ }
+ mTracks.add(track);
+ }
+ return track;
+ }
}
}
}
/**
* Show the selected (or default) subtitle track.
+ *
+ * Must be called from the UI thread.
*/
public void show() {
mShowing = true;
/**
* Hide the selected (or default) subtitle track.
+ *
+ * Must be called from the UI thread.
*/
public void hide() {
mVisibilityIsExplicit = true;
* support for a subtitle format.
*/
public void registerRenderer(Renderer renderer) {
- // TODO how to get available renderers in the system
- if (!mRenderers.contains(renderer)) {
- // TODO should added renderers override existing ones (to allow replacing?)
- mRenderers.add(renderer);
+ synchronized(mRenderers) {
+ // TODO how to get available renderers in the system
+ if (!mRenderers.contains(renderer)) {
+ // TODO should added renderers override existing ones (to allow replacing?)
+ mRenderers.add(renderer);
+ }
}
}
private Anchor mAnchor;
- /** @hide */
+ /** @hide - called from UI thread */
public void setAnchor(Anchor anchor) {
if (mAnchor == anchor) {
return;