mirror of
https://github.com/androidx/media.git
synced 2025-05-09 16:40:55 +08:00
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:
parent
8e2801ce9b
commit
40f3172237
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user