HLS: More control over buffering + tweak caption impl.

- Move all three buffering constants to a single class (the
  chunk source).
- Increase the target buffer to 40s for increased robustness
  against temporary network blips.
- Make values configurable via the chunk source constructor.
- Treat captions as a text track for HLS. This allows them to
  be enabled/disabled through the demo app UI.

Issue: #165
This commit is contained in:
Oliver Woodman 2014-11-26 17:21:41 +00:00
parent 8e2801ce9b
commit 40f3172237
5 changed files with 87 additions and 76 deletions

View File

@ -25,7 +25,6 @@ import com.google.android.exoplayer.demo.full.player.DemoPlayer;
import com.google.android.exoplayer.demo.full.player.DemoPlayer.RendererBuilder;
import com.google.android.exoplayer.demo.full.player.HlsRendererBuilder;
import com.google.android.exoplayer.demo.full.player.SmoothStreamingRendererBuilder;
import com.google.android.exoplayer.metadata.ClosedCaption;
import com.google.android.exoplayer.metadata.TxxxMetadata;
import com.google.android.exoplayer.text.CaptionStyleCompat;
import com.google.android.exoplayer.text.SubtitleView;
@ -57,15 +56,13 @@ import android.widget.PopupMenu;
import android.widget.PopupMenu.OnMenuItemClickListener;
import android.widget.TextView;
import java.util.List;
import java.util.Map;
/**
* An activity that plays media using {@link DemoPlayer}.
*/
public class FullPlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener,
DemoPlayer.Listener, DemoPlayer.TextListener, DemoPlayer.Id3MetadataListener,
DemoPlayer.ClosedCaptionListener {
DemoPlayer.Listener, DemoPlayer.TextListener, DemoPlayer.Id3MetadataListener {
private static final String TAG = "FullPlayerActivity";
@ -198,7 +195,6 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
player.addListener(this);
player.setTextListener(this);
player.setMetadataListener(this);
player.setClosedCaptionListener(this);
player.seekTo(playerPosition);
playerNeedsPrepare = true;
mediaController.setMediaPlayer(player.getPlayerControl());
@ -428,31 +424,6 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba
}
}
// DemoPlayer.ClosedCaptioListener implementation
@Override
public void onClosedCaption(List<ClosedCaption> closedCaptions) {
StringBuilder stringBuilder = new StringBuilder();
for (ClosedCaption caption : closedCaptions) {
// Ignore control characters and just insert a new line in between words.
if (caption.type == ClosedCaption.TYPE_CTRL) {
if (stringBuilder.length() > 0
&& stringBuilder.charAt(stringBuilder.length() - 1) != '\n') {
stringBuilder.append('\n');
}
} else if (caption.type == ClosedCaption.TYPE_TEXT) {
stringBuilder.append(caption.text);
}
}
if (stringBuilder.length() > 0 && stringBuilder.charAt(stringBuilder.length() - 1) == '\n') {
stringBuilder.deleteCharAt(stringBuilder.length() - 1);
}
if (stringBuilder.length() > 0) {
subtitleView.setVisibility(View.VISIBLE);
subtitleView.setText(stringBuilder.toString());
}
}
// SurfaceHolder.Callback implementation
@Override

View File

@ -145,13 +145,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
void onId3Metadata(Map<String, Object> metadata);
}
/**
* A listener for receiving closed captions parsed from the media stream.
*/
public interface ClosedCaptionListener {
void onClosedCaption(List<ClosedCaption> closedCaptions);
}
// Constants pulled into this class for convenience.
public static final int STATE_IDLE = ExoPlayer.STATE_IDLE;
public static final int STATE_PREPARING = ExoPlayer.STATE_PREPARING;
@ -162,13 +155,12 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
public static final int DISABLED_TRACK = -1;
public static final int PRIMARY_TRACK = 0;
public static final int RENDERER_COUNT = 6;
public static final int RENDERER_COUNT = 5;
public static final int TYPE_VIDEO = 0;
public static final int TYPE_AUDIO = 1;
public static final int TYPE_TEXT = 2;
public static final int TYPE_TIMED_METADATA = 3;
public static final int TYPE_CLOSED_CAPTIONS = 4;
public static final int TYPE_DEBUG = 5;
public static final int TYPE_DEBUG = 4;
private static final int RENDERER_BUILDING_STATE_IDLE = 1;
private static final int RENDERER_BUILDING_STATE_BUILDING = 2;
@ -179,6 +171,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
private final PlayerControl playerControl;
private final Handler mainHandler;
private final CopyOnWriteArrayList<Listener> listeners;
private final StringBuilder closedCaptionStringBuilder;
private int rendererBuildingState;
private int lastReportedPlaybackState;
@ -194,7 +187,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
private TextListener textListener;
private Id3MetadataListener id3MetadataListener;
private ClosedCaptionListener closedCaptionListener;
private InternalErrorListener internalErrorListener;
private InfoListener infoListener;
@ -210,6 +202,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
selectedTracks = new int[RENDERER_COUNT];
// Disable text initially.
selectedTracks[TYPE_TEXT] = DISABLED_TRACK;
closedCaptionStringBuilder = new StringBuilder();
}
public PlayerControl getPlayerControl() {
@ -240,10 +233,6 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
id3MetadataListener = listener;
}
public void setClosedCaptionListener(ClosedCaptionListener listener) {
closedCaptionListener = listener;
}
public void setSurface(Surface surface) {
this.surface = surface;
pushSurfaceAndVideoTrack(false);
@ -275,6 +264,9 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
pushSurfaceAndVideoTrack(false);
} else {
pushTrackSelection(type, true);
if (type == TYPE_TEXT && index == DISABLED_TRACK && textListener != null) {
textListener.onText(null);
}
}
}
@ -483,36 +475,28 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
@Override
public void onText(String text) {
if (textListener != null) {
textListener.onText(text);
}
processText(text);
}
/* package */ MetadataTrackRenderer.MetadataRenderer<Map<String, Object>>
getId3MetadataRenderer() {
return new MetadataTrackRenderer.MetadataRenderer<Map<String, Object>>() {
@Override
public void onMetadata(Map<String, Object> metadata) {
if (id3MetadataListener != null) {
id3MetadataListener.onId3Metadata(metadata);
}
}
};
}
/* package */ MetadataTrackRenderer.MetadataRenderer<List<ClosedCaption>>
getClosedCaptionMetadataRenderer() {
return new MetadataTrackRenderer.MetadataRenderer<List<ClosedCaption>>() {
@Override
public void onMetadata(List<ClosedCaption> metadata) {
if (closedCaptionListener != null) {
closedCaptionListener.onClosedCaption(metadata);
}
processClosedCaption(metadata);
}
};
}
@ -607,6 +591,36 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
}
}
/* package */ void processText(String text) {
if (textListener == null || selectedTracks[TYPE_TEXT] == DISABLED_TRACK) {
return;
}
textListener.onText(text);
}
/* package */ void processClosedCaption(List<ClosedCaption> metadata) {
if (textListener == null || selectedTracks[TYPE_TEXT] == DISABLED_TRACK) {
return;
}
closedCaptionStringBuilder.setLength(0);
for (ClosedCaption caption : metadata) {
// Ignore control characters and just insert a new line in between words.
if (caption.type == ClosedCaption.TYPE_CTRL) {
if (closedCaptionStringBuilder.length() > 0
&& closedCaptionStringBuilder.charAt(closedCaptionStringBuilder.length() - 1) != '\n') {
closedCaptionStringBuilder.append('\n');
}
} else if (caption.type == ClosedCaption.TYPE_TEXT) {
closedCaptionStringBuilder.append(caption.text);
}
}
if (closedCaptionStringBuilder.length() > 0
&& closedCaptionStringBuilder.charAt(closedCaptionStringBuilder.length() - 1) == '\n') {
closedCaptionStringBuilder.deleteCharAt(closedCaptionStringBuilder.length() - 1);
}
textListener.onText(closedCaptionStringBuilder.toString());
}
private class InternalRendererBuilderCallback implements RendererBuilderCallback {
private boolean canceled;

View File

@ -100,7 +100,7 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<Hls
renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer;
renderers[DemoPlayer.TYPE_AUDIO] = audioRenderer;
renderers[DemoPlayer.TYPE_TIMED_METADATA] = id3Renderer;
renderers[DemoPlayer.TYPE_CLOSED_CAPTIONS] = closedCaptionRenderer;
renderers[DemoPlayer.TYPE_TEXT] = closedCaptionRenderer;
callback.onRenderers(null, null, renderers);
}

View File

@ -80,9 +80,24 @@ public class HlsChunkSource {
*/
public static final int ADAPTIVE_MODE_ABRUPT = 3;
/**
* The default target buffer duration in milliseconds.
*/
public static final long DEFAULT_TARGET_BUFFER_DURATION_MS = 40000;
/**
* The default minimum duration of media that needs to be buffered for a switch to a higher
* quality variant to be considered.
*/
public static final long DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS = 5000;
/**
* The default maximum duration of media that needs to be buffered for a switch to a lower
* quality variant to be considered.
*/
public static final long DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS = 20000;
private static final float BANDWIDTH_FRACTION = 0.8f;
private static final long MIN_BUFFER_TO_SWITCH_UP_US = 5000000;
private static final long MAX_BUFFER_TO_SWITCH_DOWN_US = 15000000;
private final SamplePool samplePool = new TsExtractor.SamplePool();
private final DataSource upstreamDataSource;
@ -94,6 +109,9 @@ public class HlsChunkSource {
private final Uri baseUri;
private final int maxWidth;
private final int maxHeight;
private final long targetBufferDurationUs;
private final long minBufferDurationToSwitchUpUs;
private final long maxBufferDurationToSwitchDownUs;
/* package */ final HlsMediaPlaylist[] mediaPlaylists;
/* package */ final long[] lastMediaPlaylistLoadTimesMs;
@ -106,6 +124,13 @@ public class HlsChunkSource {
private String encryptedDataSourceIv;
private byte[] encryptedDataSourceSecretKey;
public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist,
BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode) {
this(dataSource, playlistUrl, playlist, bandwidthMeter, variantIndices, adaptiveMode,
DEFAULT_TARGET_BUFFER_DURATION_MS, DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS,
DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS);
}
/**
* @param dataSource A {@link DataSource} suitable for loading the media data.
* @param playlistUrl The playlist URL.
@ -116,12 +141,24 @@ public class HlsChunkSource {
* @param adaptiveMode The mode for switching from one variant to another. One of
* {@link #ADAPTIVE_MODE_NONE}, {@link #ADAPTIVE_MODE_ABRUPT} and
* {@link #ADAPTIVE_MODE_SPLICE}.
* @param targetBufferDurationMs The targeted duration of media to buffer ahead of the current
* playback position. Note that the greater this value, the greater the amount of memory
* that will be consumed.
* @param minBufferDurationToSwitchUpMs The minimum duration of media that needs to be buffered
* for a switch to a higher quality variant to be considered.
* @param maxBufferDurationToSwitchDownMs The maximum duration of media that needs to be buffered
* for a switch to a lower quality variant to be considered.
*/
public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist,
BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode) {
BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode,
long targetBufferDurationMs, long minBufferDurationToSwitchUpMs,
long maxBufferDurationToSwitchDownMs) {
this.upstreamDataSource = dataSource;
this.bandwidthMeter = bandwidthMeter;
this.adaptiveMode = adaptiveMode;
targetBufferDurationUs = targetBufferDurationMs * 1000;
minBufferDurationToSwitchUpUs = minBufferDurationToSwitchUpMs * 1000;
maxBufferDurationToSwitchDownUs = maxBufferDurationToSwitchDownMs * 1000;
baseUri = playlist.baseUri;
bitArray = new BitArray();
playlistParser = new HlsPlaylistParser();
@ -182,8 +219,9 @@ public class HlsChunkSource {
*/
public HlsChunk getChunkOperation(TsChunk previousTsChunk, long seekPositionUs,
long playbackPositionUs) {
if (previousTsChunk != null && previousTsChunk.isLastChunk) {
// We're already finished.
if (previousTsChunk != null && (previousTsChunk.isLastChunk
|| previousTsChunk.endTimeUs - playbackPositionUs >= targetBufferDurationUs)) {
// We're either finished, or we have the target amount of data buffered.
return null;
}
@ -303,8 +341,8 @@ public class HlsChunkSource {
: adaptiveMode == ADAPTIVE_MODE_SPLICE ? previousTsChunk.startTimeUs
: previousTsChunk.endTimeUs;
long bufferedUs = bufferedPositionUs - playbackPositionUs;
if ((idealVariantIndex > variantIndex && bufferedUs < MAX_BUFFER_TO_SWITCH_DOWN_US)
|| (idealVariantIndex < variantIndex && bufferedUs > MIN_BUFFER_TO_SWITCH_UP_US)) {
if ((idealVariantIndex > variantIndex && bufferedUs < maxBufferDurationToSwitchDownUs)
|| (idealVariantIndex < variantIndex && bufferedUs > minBufferDurationToSwitchUpUs)) {
// Switch variant.
return idealVariantIndex;
}

View File

@ -40,7 +40,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
*/
public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 1;
private static final long BUFFER_DURATION_US = 20000000;
private static final int NO_RESET_PENDING = -1;
private final HlsChunkSource chunkSource;
@ -350,7 +349,7 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
}
private void maybeStartLoading() {
if (currentLoadableExceptionFatal || loadingFinished) {
if (currentLoadableExceptionFatal || loadingFinished || loader.isLoading()) {
return;
}
@ -364,17 +363,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback {
return;
}
boolean bufferFull = false;
if (!extractors.isEmpty()) {
long largestSampleTimestamp = extractors.getLast().getLargestSampleTimestamp();
bufferFull = largestSampleTimestamp != Long.MIN_VALUE
&& (largestSampleTimestamp - downstreamPositionUs) >= BUFFER_DURATION_US;
}
if (loader.isLoading() || bufferFull) {
return;
}
HlsChunk nextLoadable = chunkSource.getChunkOperation(previousTsLoadable,
pendingResetPositionUs, downstreamPositionUs);
if (nextLoadable == null) {