Commit 709fc773 by Oliver Woodman

First steps toward implementing bounded live seek.

parent 763d68f2
# Release notes # # Release notes #
### r1.3.2 ### ### Current dev branch (from r1.3.2) ###
* Add option to TsExtractor to allow non-IDR keyframes.
* Added MulticastDataSource for connecting to multicast streams.
* (WorkInProgress) - First steps to supporting seeking in DASH DVR window.
### r1.3.2 (from r1.3.1) ###
* DataSource improvements: `DefaultUriDataSource` now handles http://, https://, file://, asset:// * DataSource improvements: `DefaultUriDataSource` now handles http://, https://, file://, asset://
and content:// URIs automatically. It also handles file:///android_asset/* URIs, and file paths and content:// URIs automatically. It also handles file:///android_asset/* URIs, and file paths
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer.demo; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer.demo;
import com.google.android.exoplayer.ExoPlayer; import com.google.android.exoplayer.ExoPlayer;
import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
import com.google.android.exoplayer.TimeRange;
import com.google.android.exoplayer.audio.AudioTrack; import com.google.android.exoplayer.audio.AudioTrack;
import com.google.android.exoplayer.chunk.Format; import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.demo.player.DemoPlayer; import com.google.android.exoplayer.demo.player.DemoPlayer;
...@@ -46,6 +47,7 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener ...@@ -46,6 +47,7 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
private long sessionStartTimeMs; private long sessionStartTimeMs;
private long[] loadStartTimeMs; private long[] loadStartTimeMs;
private long[] seekRangeValuesUs;
public EventLogger() { public EventLogger() {
loadStartTimeMs = new long[DemoPlayer.RENDERER_COUNT]; loadStartTimeMs = new long[DemoPlayer.RENDERER_COUNT];
...@@ -163,7 +165,14 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener ...@@ -163,7 +165,14 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
@Override @Override
public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
long initializationDurationMs) { long initializationDurationMs) {
Log.d(TAG, "decoderInitialized [" + getSessionTimeString() + "]"); Log.d(TAG, "decoderInitialized [" + getSessionTimeString() + ", " + decoderName + "]");
}
@Override
public void onSeekRangeChanged(TimeRange seekRange) {
seekRangeValuesUs = seekRange.getCurrentBoundsUs(seekRangeValuesUs);
Log.d(TAG, "seekRange [ " + seekRange.type + ", " + seekRangeValuesUs[0] + ", "
+ seekRangeValuesUs[1] + "]");
} }
private void printInternalError(String type, Exception e) { private void printInternalError(String type, Exception e) {
......
...@@ -235,14 +235,15 @@ public class DashRendererBuilder implements RendererBuilder, ...@@ -235,14 +235,15 @@ public class DashRendererBuilder implements RendererBuilder,
DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent); DataSource videoDataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher, ChunkSource videoChunkSource = new DashChunkSource(manifestFetcher,
videoAdaptationSetIndex, videoRepresentationIndices, videoDataSource, videoAdaptationSetIndex, videoRepresentationIndices, videoDataSource,
new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset); new AdaptiveEvaluator(bandwidthMeter), LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset,
mainHandler, player);
ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl, ChunkSampleSource videoSampleSource = new ChunkSampleSource(videoChunkSource, loadControl,
VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, VIDEO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player,
DemoPlayer.TYPE_VIDEO); DemoPlayer.TYPE_VIDEO);
videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, drmSessionManager, true, videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, drmSessionManager, true,
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, mainHandler, player, 50); MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, mainHandler, player, 50);
debugRenderer = debugTextView != null debugRenderer = debugTextView != null
? new DebugTrackRenderer(debugTextView, player, videoRenderer) : null; ? new DebugTrackRenderer(debugTextView, player, videoRenderer, bandwidthMeter) : null;
} }
// Build the audio chunk sources. // Build the audio chunk sources.
...@@ -259,7 +260,7 @@ public class DashRendererBuilder implements RendererBuilder, ...@@ -259,7 +260,7 @@ public class DashRendererBuilder implements RendererBuilder,
format.audioSamplingRate + "Hz)"); format.audioSamplingRate + "Hz)");
audioChunkSourceList.add(new DashChunkSource(manifestFetcher, audioAdaptationSetIndex, audioChunkSourceList.add(new DashChunkSource(manifestFetcher, audioAdaptationSetIndex,
new int[] {i}, audioDataSource, audioEvaluator, LIVE_EDGE_LATENCY_MS, new int[] {i}, audioDataSource, audioEvaluator, LIVE_EDGE_LATENCY_MS,
elapsedRealtimeOffset)); elapsedRealtimeOffset, mainHandler, player));
codecs.add(format.codecs); codecs.add(format.codecs);
} }
...@@ -316,7 +317,8 @@ public class DashRendererBuilder implements RendererBuilder, ...@@ -316,7 +317,8 @@ public class DashRendererBuilder implements RendererBuilder,
Representation representation = representations.get(j); Representation representation = representations.get(j);
textTrackNameList.add(representation.format.id); textTrackNameList.add(representation.format.id);
textChunkSourceList.add(new DashChunkSource(manifestFetcher, i, new int[] {j}, textChunkSourceList.add(new DashChunkSource(manifestFetcher, i, new int[] {j},
textDataSource, textEvaluator, LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset)); textDataSource, textEvaluator, LIVE_EDGE_LATENCY_MS, elapsedRealtimeOffset,
mainHandler, player));
} }
} }
} }
......
...@@ -19,6 +19,7 @@ import com.google.android.exoplayer.ExoPlaybackException; ...@@ -19,6 +19,7 @@ import com.google.android.exoplayer.ExoPlaybackException;
import com.google.android.exoplayer.MediaCodecTrackRenderer; import com.google.android.exoplayer.MediaCodecTrackRenderer;
import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.chunk.Format; import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.upstream.BandwidthMeter;
import android.widget.TextView; import android.widget.TextView;
...@@ -31,15 +32,22 @@ import android.widget.TextView; ...@@ -31,15 +32,22 @@ import android.widget.TextView;
private final TextView textView; private final TextView textView;
private final DemoPlayer player; private final DemoPlayer player;
private final MediaCodecTrackRenderer renderer; private final MediaCodecTrackRenderer renderer;
private final BandwidthMeter bandwidthMeter;
private volatile boolean pendingFailure; private volatile boolean pendingFailure;
private volatile long currentPositionUs; private volatile long currentPositionUs;
public DebugTrackRenderer(TextView textView, DemoPlayer player, public DebugTrackRenderer(TextView textView, DemoPlayer player,
MediaCodecTrackRenderer renderer) { MediaCodecTrackRenderer renderer) {
this(textView, player, renderer, null);
}
public DebugTrackRenderer(TextView textView, DemoPlayer player, MediaCodecTrackRenderer renderer,
BandwidthMeter bandwidthMeter) {
this.textView = textView; this.textView = textView;
this.player = player; this.player = player;
this.renderer = renderer; this.renderer = renderer;
this.bandwidthMeter = bandwidthMeter;
} }
public void injectFailure() { public void injectFailure() {
...@@ -77,7 +85,12 @@ import android.widget.TextView; ...@@ -77,7 +85,12 @@ import android.widget.TextView;
} }
private String getRenderString() { private String getRenderString() {
return getQualityString() + " " + renderer.codecCounters.getDebugString(); return getTimeString() + " " + getQualityString() + " " + getBandwidthString() + " "
+ renderer.codecCounters.getDebugString();
}
private String getTimeString() {
return "ms(" + (currentPositionUs / 1000) + ")";
} }
private String getQualityString() { private String getQualityString() {
...@@ -86,6 +99,15 @@ import android.widget.TextView; ...@@ -86,6 +99,15 @@ import android.widget.TextView;
: "id:" + format.id + " br:" + format.bitrate + " h:" + format.height; : "id:" + format.id + " br:" + format.bitrate + " h:" + format.height;
} }
private String getBandwidthString() {
if (bandwidthMeter == null
|| bandwidthMeter.getBitrateEstimate() == BandwidthMeter.NO_ESTIMATE) {
return "bw:?";
} else {
return "bw:" + (bandwidthMeter.getBitrateEstimate() / 1000);
}
}
@Override @Override
protected long getCurrentPositionUs() { protected long getCurrentPositionUs() {
return currentPositionUs; return currentPositionUs;
......
...@@ -21,11 +21,13 @@ import com.google.android.exoplayer.ExoPlayer; ...@@ -21,11 +21,13 @@ import com.google.android.exoplayer.ExoPlayer;
import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException;
import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; import com.google.android.exoplayer.MediaCodecVideoTrackRenderer;
import com.google.android.exoplayer.TimeRange;
import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.audio.AudioTrack; import com.google.android.exoplayer.audio.AudioTrack;
import com.google.android.exoplayer.chunk.ChunkSampleSource; import com.google.android.exoplayer.chunk.ChunkSampleSource;
import com.google.android.exoplayer.chunk.Format; import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.chunk.MultiTrackChunkSource; import com.google.android.exoplayer.chunk.MultiTrackChunkSource;
import com.google.android.exoplayer.dash.DashChunkSource;
import com.google.android.exoplayer.drm.StreamingDrmSessionManager; import com.google.android.exoplayer.drm.StreamingDrmSessionManager;
import com.google.android.exoplayer.hls.HlsSampleSource; import com.google.android.exoplayer.hls.HlsSampleSource;
import com.google.android.exoplayer.metadata.MetadataTrackRenderer; import com.google.android.exoplayer.metadata.MetadataTrackRenderer;
...@@ -50,7 +52,7 @@ import java.util.concurrent.CopyOnWriteArrayList; ...@@ -50,7 +52,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener, public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener,
HlsSampleSource.EventListener, DefaultBandwidthMeter.EventListener, HlsSampleSource.EventListener, DefaultBandwidthMeter.EventListener,
MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener, MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener,
StreamingDrmSessionManager.EventListener, TextRenderer { StreamingDrmSessionManager.EventListener, DashChunkSource.EventListener, TextRenderer {
/** /**
* Builds renderers for the player. * Builds renderers for the player.
...@@ -132,6 +134,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -132,6 +134,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
int mediaStartTimeMs, int mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs); int mediaStartTimeMs, int mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs);
void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
long initializationDurationMs); long initializationDurationMs);
void onSeekRangeChanged(TimeRange seekRange);
} }
/** /**
...@@ -510,6 +513,13 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -510,6 +513,13 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
processText(text); processText(text);
} }
@Override
public void onSeekRangeChanged(TimeRange seekRange) {
if (infoListener != null) {
infoListener.onSeekRangeChanged(seekRange);
}
}
/* package */ MetadataTrackRenderer.MetadataRenderer<Map<String, Object>> /* package */ MetadataTrackRenderer.MetadataRenderer<Map<String, Object>>
getId3MetadataRenderer() { getId3MetadataRenderer() {
return new MetadataTrackRenderer.MetadataRenderer<Map<String, Object>>() { return new MetadataTrackRenderer.MetadataRenderer<Map<String, Object>>() {
......
...@@ -23,6 +23,7 @@ import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilderCallba ...@@ -23,6 +23,7 @@ import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilderCallba
import com.google.android.exoplayer.extractor.Extractor; import com.google.android.exoplayer.extractor.Extractor;
import com.google.android.exoplayer.extractor.ExtractorSampleSource; import com.google.android.exoplayer.extractor.ExtractorSampleSource;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer.upstream.DefaultUriDataSource; import com.google.android.exoplayer.upstream.DefaultUriDataSource;
import android.content.Context; import android.content.Context;
...@@ -55,7 +56,9 @@ public class ExtractorRendererBuilder implements RendererBuilder { ...@@ -55,7 +56,9 @@ public class ExtractorRendererBuilder implements RendererBuilder {
@Override @Override
public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) { public void buildRenderers(DemoPlayer player, RendererBuilderCallback callback) {
// Build the video and audio renderers. // Build the video and audio renderers.
DataSource dataSource = new DefaultUriDataSource(context, userAgent); DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(player.getMainHandler(),
null);
DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, extractor, 2, ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri, dataSource, extractor, 2,
BUFFER_SIZE); BUFFER_SIZE);
MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource, MediaCodecVideoTrackRenderer videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
...@@ -66,7 +69,7 @@ public class ExtractorRendererBuilder implements RendererBuilder { ...@@ -66,7 +69,7 @@ public class ExtractorRendererBuilder implements RendererBuilder {
// Build the debug renderer. // Build the debug renderer.
TrackRenderer debugRenderer = debugTextView != null TrackRenderer debugRenderer = debugTextView != null
? new DebugTrackRenderer(debugTextView, player, videoRenderer) : null; ? new DebugTrackRenderer(debugTextView, player, videoRenderer, bandwidthMeter) : null;
// Invoke the callback. // Invoke the callback.
TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT];
......
...@@ -121,7 +121,7 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<Hls ...@@ -121,7 +121,7 @@ public class HlsRendererBuilder implements RendererBuilder, ManifestCallback<Hls
// Build the debug renderer. // Build the debug renderer.
TrackRenderer debugRenderer = debugTextView != null TrackRenderer debugRenderer = debugTextView != null
? new DebugTrackRenderer(debugTextView, player, videoRenderer) : null; ? new DebugTrackRenderer(debugTextView, player, videoRenderer, bandwidthMeter) : null;
TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT]; TrackRenderer[] renderers = new TrackRenderer[DemoPlayer.RENDERER_COUNT];
renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer; renderers[DemoPlayer.TYPE_VIDEO] = videoRenderer;
......
...@@ -174,7 +174,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, ...@@ -174,7 +174,7 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder,
videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, drmSessionManager, true, videoRenderer = new MediaCodecVideoTrackRenderer(videoSampleSource, drmSessionManager, true,
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, mainHandler, player, 50); MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, 5000, null, mainHandler, player, 50);
debugRenderer = debugTextView != null debugRenderer = debugTextView != null
? new DebugTrackRenderer(debugTextView, player, videoRenderer) : null; ? new DebugTrackRenderer(debugTextView, player, videoRenderer, bandwidthMeter) : null;
} }
// Build the audio renderer. // Build the audio renderer.
......
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer;
/**
* A container to store a start and end time in microseconds.
*/
public final class TimeRange {
/**
* Represents a range of time whose bounds change in bulk increments rather than smoothly over
* time.
*/
public static final int TYPE_SNAPSHOT = 0;
/**
* The type of this time range.
*/
public final int type;
private final long startTimeUs;
private final long endTimeUs;
/**
* Create a new {@link TimeRange} of the appropriate type.
*
* @param type The type of the TimeRange.
* @param startTimeUs The beginning of the TimeRange.
* @param endTimeUs The end of the TimeRange.
*/
public TimeRange(int type, long startTimeUs, long endTimeUs) {
this.type = type;
this.startTimeUs = startTimeUs;
this.endTimeUs = endTimeUs;
}
/**
* Returns the start and end times (in milliseconds) of the TimeRange in the provided array,
* or creates a new one.
*
* @param out An array to store the start and end times; can be null.
* @return An array containing the start time (index 0) and end time (index 1) in milliseconds.
*/
public long[] getCurrentBoundsMs(long[] out) {
out = getCurrentBoundsUs(out);
out[0] /= 1000;
out[1] /= 1000;
return out;
}
/**
* Returns the start and end times (in microseconds) of the TimeRange in the provided array,
* or creates a new one.
*
* @param out An array to store the start and end times; can be null.
* @return An array containing the start time (index 0) and end time (index 1) in microseconds.
*/
public long[] getCurrentBoundsUs(long[] out) {
if (out == null || out.length < 2) {
out = new long[2];
}
out[0] = startTimeUs;
out[1] = endTimeUs;
return out;
}
@Override
public int hashCode() {
int hashCode = 0;
hashCode |= type << 30;
hashCode |= (((startTimeUs + endTimeUs) / 1000) & 0x3FFFFFFF);
return hashCode;
}
@Override
public boolean equals(Object other) {
if (other == this) {
return true;
}
if (other instanceof TimeRange) {
TimeRange otherTimeRange = (TimeRange) other;
return (otherTimeRange.type == type) && (otherTimeRange.startTimeUs == startTimeUs)
&& (otherTimeRange.endTimeUs == endTimeUs);
} else {
return false;
}
}
}
...@@ -37,11 +37,15 @@ import java.util.List; ...@@ -37,11 +37,15 @@ import java.util.List;
private static final String TAG = "H264Reader"; private static final String TAG = "H264Reader";
private static final int NAL_UNIT_TYPE_IDR = 5; private static final int FRAME_TYPE_I = 2;
private static final int NAL_UNIT_TYPE_SEI = 6; private static final int FRAME_TYPE_ALL_I = 7;
private static final int NAL_UNIT_TYPE_SPS = 7;
private static final int NAL_UNIT_TYPE_PPS = 8; private static final int NAL_UNIT_TYPE_IFR = 1; // Coded slice of a non-IDR picture
private static final int NAL_UNIT_TYPE_AUD = 9; private static final int NAL_UNIT_TYPE_IDR = 5; // Coded slice of an IDR picture
private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information
private static final int NAL_UNIT_TYPE_SPS = 7; // Sequence parameter set
private static final int NAL_UNIT_TYPE_PPS = 8; // Picture parameter set
private static final int NAL_UNIT_TYPE_AUD = 9; // Access unit delimiter
private static final int EXTENDED_SAR = 0xFF; private static final int EXTENDED_SAR = 0xFF;
private static final float[] ASPECT_RATIO_IDC_VALUES = new float[] { private static final float[] ASPECT_RATIO_IDC_VALUES = new float[] {
1f /* Unspecified. Assume square */, 1f /* Unspecified. Assume square */,
...@@ -69,6 +73,7 @@ import java.util.List; ...@@ -69,6 +73,7 @@ import java.util.List;
// State that should be reset on seek. // State that should be reset on seek.
private final SeiReader seiReader; private final SeiReader seiReader;
private final boolean[] prefixFlags; private final boolean[] prefixFlags;
private final IfrParserBuffer ifrParserBuffer;
private final NalUnitTargetBuffer sps; private final NalUnitTargetBuffer sps;
private final NalUnitTargetBuffer pps; private final NalUnitTargetBuffer pps;
private final NalUnitTargetBuffer sei; private final NalUnitTargetBuffer sei;
...@@ -84,10 +89,11 @@ import java.util.List; ...@@ -84,10 +89,11 @@ import java.util.List;
private final ParsableByteArray seiWrapper; private final ParsableByteArray seiWrapper;
private int[] scratchEscapePositions; private int[] scratchEscapePositions;
public H264Reader(TrackOutput output, SeiReader seiReader) { public H264Reader(TrackOutput output, SeiReader seiReader, boolean idrKeyframesOnly) {
super(output); super(output);
this.seiReader = seiReader; this.seiReader = seiReader;
prefixFlags = new boolean[3]; prefixFlags = new boolean[3];
ifrParserBuffer = (idrKeyframesOnly) ? null : new IfrParserBuffer();
sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128); sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128);
pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128); pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128);
sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128); sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128);
...@@ -102,6 +108,9 @@ import java.util.List; ...@@ -102,6 +108,9 @@ import java.util.List;
sps.reset(); sps.reset();
pps.reset(); pps.reset();
sei.reset(); sei.reset();
if (ifrParserBuffer != null) {
ifrParserBuffer.reset();
}
writingSample = false; writingSample = false;
totalBytesWritten = 0; totalBytesWritten = 0;
} }
...@@ -132,22 +141,30 @@ import java.util.List; ...@@ -132,22 +141,30 @@ import java.util.List;
int nalUnitType = H264Util.getNalUnitType(dataArray, nextNalUnitOffset); int nalUnitType = H264Util.getNalUnitType(dataArray, nextNalUnitOffset);
int bytesWrittenPastNalUnit = limit - nextNalUnitOffset; int bytesWrittenPastNalUnit = limit - nextNalUnitOffset;
if (nalUnitType == NAL_UNIT_TYPE_AUD) { switch (nalUnitType) {
if (writingSample) { case NAL_UNIT_TYPE_IDR:
if (isKeyframe && !hasOutputFormat && sps.isCompleted() && pps.isCompleted()) { isKeyframe = true;
parseMediaFormat(sps, pps); break;
case NAL_UNIT_TYPE_AUD:
if (writingSample) {
if (ifrParserBuffer != null && ifrParserBuffer.isCompleted()) {
int sliceType = ifrParserBuffer.getSliceType();
isKeyframe |= (sliceType == FRAME_TYPE_I || sliceType == FRAME_TYPE_ALL_I);
ifrParserBuffer.reset();
}
if (isKeyframe && !hasOutputFormat && sps.isCompleted() && pps.isCompleted()) {
parseMediaFormat(sps, pps);
}
int flags = isKeyframe ? C.SAMPLE_FLAG_SYNC : 0;
int size = (int) (totalBytesWritten - samplePosition) - bytesWrittenPastNalUnit;
output.sampleMetadata(sampleTimeUs, flags, size, bytesWrittenPastNalUnit, null);
writingSample = false;
} }
int flags = isKeyframe ? C.SAMPLE_FLAG_SYNC : 0; writingSample = true;
int size = (int) (totalBytesWritten - samplePosition) - bytesWrittenPastNalUnit; samplePosition = totalBytesWritten - bytesWrittenPastNalUnit;
output.sampleMetadata(sampleTimeUs, flags, size, bytesWrittenPastNalUnit, null); sampleTimeUs = pesTimeUs;
writingSample = false; isKeyframe = false;
} break;
writingSample = true;
isKeyframe = false;
sampleTimeUs = pesTimeUs;
samplePosition = totalBytesWritten - bytesWrittenPastNalUnit;
} else if (nalUnitType == NAL_UNIT_TYPE_IDR) {
isKeyframe = true;
} }
// If the length to the start of the unit is negative then we wrote too many bytes to the // If the length to the start of the unit is negative then we wrote too many bytes to the
...@@ -171,6 +188,9 @@ import java.util.List; ...@@ -171,6 +188,9 @@ import java.util.List;
} }
private void feedNalUnitTargetBuffersStart(int nalUnitType) { private void feedNalUnitTargetBuffersStart(int nalUnitType) {
if (ifrParserBuffer != null) {
ifrParserBuffer.startNalUnit(nalUnitType);
}
if (!hasOutputFormat) { if (!hasOutputFormat) {
sps.startNalUnit(nalUnitType); sps.startNalUnit(nalUnitType);
pps.startNalUnit(nalUnitType); pps.startNalUnit(nalUnitType);
...@@ -179,6 +199,9 @@ import java.util.List; ...@@ -179,6 +199,9 @@ import java.util.List;
} }
private void feedNalUnitTargetBuffersData(byte[] dataArray, int offset, int limit) { private void feedNalUnitTargetBuffersData(byte[] dataArray, int offset, int limit) {
if (ifrParserBuffer != null) {
ifrParserBuffer.appendToNalUnit(dataArray, offset, limit);
}
if (!hasOutputFormat) { if (!hasOutputFormat) {
sps.appendToNalUnit(dataArray, offset, limit); sps.appendToNalUnit(dataArray, offset, limit);
pps.appendToNalUnit(dataArray, offset, limit); pps.appendToNalUnit(dataArray, offset, limit);
...@@ -461,4 +484,99 @@ import java.util.List; ...@@ -461,4 +484,99 @@ import java.util.List;
} }
/**
* A buffer specifically for IFR units that can be used to parse the IFR's slice type.
*/
private static final class IfrParserBuffer {
private static final int DEFAULT_BUFFER_SIZE = 128;
private static final int NOT_SET = -1;
private final ParsableBitArray scratchSliceType;
private byte[] ifrData;
private int ifrLength;
private boolean isFilling;
private int sliceType;
public IfrParserBuffer() {
ifrData = new byte[DEFAULT_BUFFER_SIZE];
scratchSliceType = new ParsableBitArray(ifrData);
reset();
}
/**
* Resets the buffer, clearing any data that it holds.
*/
public void reset() {
isFilling = false;
ifrLength = 0;
sliceType = NOT_SET;
}
/**
* True if enough data was added to the buffer that the slice type was determined.
*/
public boolean isCompleted() {
return sliceType != NOT_SET;
}
/**
* Invoked to indicate that a NAL unit has started, and if it is an IFR then the buffer will
* start.
*/
public void startNalUnit(int nalUnitType) {
if (nalUnitType == NAL_UNIT_TYPE_IFR) {
reset();
isFilling = true;
}
}
/**
* Invoked to pass stream data. The data passed should not include 4 byte NAL unit prefixes.
*
* @param data Holds the data being passed.
* @param offset The offset of the data in {@code data}.
* @param limit The limit (exclusive) of the data in {@code data}.
*/
public void appendToNalUnit(byte[] data, int offset, int limit) {
if (!isFilling) {
return;
}
int readLength = limit - offset;
if (ifrData.length < ifrLength + readLength) {
ifrData = Arrays.copyOf(ifrData, (ifrLength + readLength) * 2);
}
System.arraycopy(data, offset, ifrData, ifrLength, readLength);
ifrLength += readLength;
scratchSliceType.reset(ifrData, ifrLength);
// first_mb_in_slice
int len = scratchSliceType.peekExpGolombCodedNumLength();
if ((len == -1) || (len > scratchSliceType.bitsLeft())) {
// Not enough yet
return;
}
scratchSliceType.skipBits(len);
// slice_type
len = scratchSliceType.peekExpGolombCodedNumLength();
if ((len == -1) || (len > scratchSliceType.bitsLeft())) {
// Not enough yet
return;
}
sliceType = scratchSliceType.readUnsignedExpGolombCodedInt();
isFilling = false;
}
/**
* @return the slice type of the IFR.
*/
public int getSliceType() {
return sliceType;
}
}
} }
...@@ -53,6 +53,7 @@ public final class TsExtractor implements Extractor, SeekMap { ...@@ -53,6 +53,7 @@ public final class TsExtractor implements Extractor, SeekMap {
private final ParsableByteArray tsPacketBuffer; private final ParsableByteArray tsPacketBuffer;
private final ParsableBitArray tsScratch; private final ParsableBitArray tsScratch;
private final boolean idrKeyframesOnly;
private final long firstSampleTimestampUs; private final long firstSampleTimestampUs;
/* package */ final SparseBooleanArray streamTypes; /* package */ final SparseBooleanArray streamTypes;
/* package */ final SparseBooleanArray allowedPassthroughStreamTypes; /* package */ final SparseBooleanArray allowedPassthroughStreamTypes;
...@@ -65,11 +66,21 @@ public final class TsExtractor implements Extractor, SeekMap { ...@@ -65,11 +66,21 @@ public final class TsExtractor implements Extractor, SeekMap {
/* package */ Id3Reader id3Reader; /* package */ Id3Reader id3Reader;
public TsExtractor() { public TsExtractor() {
this(0, null); this(0);
}
public TsExtractor(long firstSampleTimestampUs) {
this(firstSampleTimestampUs, null);
} }
public TsExtractor(long firstSampleTimestampUs, AudioCapabilities audioCapabilities) { public TsExtractor(long firstSampleTimestampUs, AudioCapabilities audioCapabilities) {
this(firstSampleTimestampUs, audioCapabilities, true);
}
public TsExtractor(long firstSampleTimestampUs, AudioCapabilities audioCapabilities,
boolean idrKeyframesOnly) {
this.firstSampleTimestampUs = firstSampleTimestampUs; this.firstSampleTimestampUs = firstSampleTimestampUs;
this.idrKeyframesOnly = idrKeyframesOnly;
tsScratch = new ParsableBitArray(new byte[3]); tsScratch = new ParsableBitArray(new byte[3]);
tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE); tsPacketBuffer = new ParsableByteArray(TS_PACKET_SIZE);
streamTypes = new SparseBooleanArray(); streamTypes = new SparseBooleanArray();
...@@ -103,6 +114,8 @@ public final class TsExtractor implements Extractor, SeekMap { ...@@ -103,6 +114,8 @@ public final class TsExtractor implements Extractor, SeekMap {
return RESULT_END_OF_INPUT; return RESULT_END_OF_INPUT;
} }
// Note: see ISO/IEC 13818-1, section 2.4.3.2 for detailed information on the format of
// the header.
tsPacketBuffer.setPosition(0); tsPacketBuffer.setPosition(0);
tsPacketBuffer.setLimit(TS_PACKET_SIZE); tsPacketBuffer.setLimit(TS_PACKET_SIZE);
int syncByte = tsPacketBuffer.readUnsignedByte(); int syncByte = tsPacketBuffer.readUnsignedByte();
...@@ -292,6 +305,8 @@ public final class TsExtractor implements Extractor, SeekMap { ...@@ -292,6 +305,8 @@ public final class TsExtractor implements Extractor, SeekMap {
data.skipBytes(pointerField); data.skipBytes(pointerField);
} }
// Note: see ISO/IEC 13818-1, section 2.4.4.8 for detailed information on the format of
// the header.
data.readBytes(pmtScratch, 3); data.readBytes(pmtScratch, 3);
pmtScratch.skipBits(12); // table_id (8), section_syntax_indicator (1), '0' (1), reserved (2) pmtScratch.skipBits(12); // table_id (8), section_syntax_indicator (1), '0' (1), reserved (2)
int sectionLength = pmtScratch.readBits(12); int sectionLength = pmtScratch.readBits(12);
...@@ -347,7 +362,8 @@ public final class TsExtractor implements Extractor, SeekMap { ...@@ -347,7 +362,8 @@ public final class TsExtractor implements Extractor, SeekMap {
break; break;
case TS_STREAM_TYPE_H264: case TS_STREAM_TYPE_H264:
SeiReader seiReader = new SeiReader(output.track(TS_STREAM_TYPE_EIA608)); SeiReader seiReader = new SeiReader(output.track(TS_STREAM_TYPE_EIA608));
pesPayloadReader = new H264Reader(output.track(TS_STREAM_TYPE_H264), seiReader); pesPayloadReader = new H264Reader(output.track(TS_STREAM_TYPE_H264), seiReader,
idrKeyframesOnly);
break; break;
case TS_STREAM_TYPE_ID3: case TS_STREAM_TYPE_ID3:
pesPayloadReader = id3Reader; pesPayloadReader = id3Reader;
...@@ -502,6 +518,8 @@ public final class TsExtractor implements Extractor, SeekMap { ...@@ -502,6 +518,8 @@ public final class TsExtractor implements Extractor, SeekMap {
} }
private boolean parseHeader() { private boolean parseHeader() {
// Note: see ISO/IEC 13818-1, section 2.4.3.6 for detailed information on the format of
// the header.
pesScratch.setPosition(0); pesScratch.setPosition(0);
int startCodePrefix = pesScratch.readBits(24); int startCodePrefix = pesScratch.readBits(24);
if (startCodePrefix != 0x000001) { if (startCodePrefix != 0x000001) {
...@@ -534,7 +552,7 @@ public final class TsExtractor implements Extractor, SeekMap { ...@@ -534,7 +552,7 @@ public final class TsExtractor implements Extractor, SeekMap {
pesScratch.setPosition(0); pesScratch.setPosition(0);
timeUs = 0; timeUs = 0;
if (ptsFlag) { if (ptsFlag) {
pesScratch.skipBits(4); // '0010' pesScratch.skipBits(4); // '0010' or '0011'
long pts = (long) pesScratch.readBits(3) << 30; long pts = (long) pesScratch.readBits(3) << 30;
pesScratch.skipBits(1); // marker_bit pesScratch.skipBits(1); // marker_bit
pts |= pesScratch.readBits(15) << 15; pts |= pesScratch.readBits(15) << 15;
......
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer.upstream;
import com.google.android.exoplayer.C;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
/**
* A multicast {@link DataSource}.
*/
public class MulticastDataSource implements UriDataSource {
/**
* Thrown when an error is encountered when trying to read from a {@link MulticastDataSource}.
*/
public static final class MulticastDataSourceException extends IOException {
public MulticastDataSourceException(String message) {
super(message);
}
public MulticastDataSourceException(IOException cause) {
super(cause);
}
}
public static final int DEFAULT_MAX_PACKET_SIZE = 2000;
public static final int TRANSFER_LISTENER_PACKET_INTERVAL = 1000;
private final TransferListener transferListener;
private final DatagramPacket packet;
private DataSpec dataSpec;
private MulticastSocket socket;
private boolean opened;
private int packetsReceived;
private byte[] packetBuffer;
private int packetRemaining;
public MulticastDataSource(TransferListener transferListener) {
this(transferListener, DEFAULT_MAX_PACKET_SIZE);
}
public MulticastDataSource(TransferListener transferListener, int maxPacketSize) {
this.transferListener = transferListener;
packetBuffer = new byte[maxPacketSize];
packet = new DatagramPacket(packetBuffer, 0, maxPacketSize);
}
@Override
public long open(DataSpec dataSpec) throws MulticastDataSourceException {
this.dataSpec = dataSpec;
String uri = dataSpec.uri.toString();
String host = uri.substring(0, uri.indexOf(':'));
int port = Integer.parseInt(uri.substring(uri.indexOf(':') + 1));
try {
socket = new MulticastSocket(port);
socket.joinGroup(InetAddress.getByName(host));
} catch (IOException e) {
throw new MulticastDataSourceException(e);
}
opened = true;
transferListener.onTransferStart();
return C.LENGTH_UNBOUNDED;
}
@Override
public void close() {
if (opened) {
socket.close();
socket = null;
transferListener.onTransferEnd();
packetRemaining = 0;
packetsReceived = 0;
opened = false;
}
}
@Override
public int read(byte[] buffer, int offset, int readLength) throws MulticastDataSourceException {
// if we've read all the data, get another packet
if (packetRemaining == 0) {
if (packetsReceived == TRANSFER_LISTENER_PACKET_INTERVAL) {
transferListener.onTransferEnd();
transferListener.onTransferStart();
packetsReceived = 0;
}
try {
socket.receive(packet);
} catch (IOException e) {
throw new MulticastDataSourceException(e);
}
packetRemaining = packet.getLength();
transferListener.onBytesTransferred(packetRemaining);
packetsReceived++;
}
// don't try to read too much
if (packetRemaining < readLength) {
readLength = packetRemaining;
}
int packetOffset = packet.getLength() - packetRemaining;
System.arraycopy(packetBuffer, packetOffset, buffer, offset, readLength);
packetRemaining -= readLength;
return readLength;
}
@Override
public String getUri() {
return dataSpec == null ? null : dataSpec.uri.toString();
}
}
...@@ -26,6 +26,7 @@ public final class ParsableBitArray { ...@@ -26,6 +26,7 @@ public final class ParsableBitArray {
// byte (from 0 to 7). // byte (from 0 to 7).
private int byteOffset; private int byteOffset;
private int bitOffset; private int bitOffset;
private int byteLimit;
/** Creates a new instance that initially has no backing data. */ /** Creates a new instance that initially has no backing data. */
public ParsableBitArray() {} public ParsableBitArray() {}
...@@ -36,7 +37,18 @@ public final class ParsableBitArray { ...@@ -36,7 +37,18 @@ public final class ParsableBitArray {
* @param data The data to wrap. * @param data The data to wrap.
*/ */
public ParsableBitArray(byte[] data) { public ParsableBitArray(byte[] data) {
this(data, data.length);
}
/**
* Creates a new instance that wraps an existing array.
*
* @param data The data to wrap.
* @param limit The limit in bytes.
*/
public ParsableBitArray(byte[] data, int limit) {
this.data = data; this.data = data;
byteLimit = limit;
} }
/** /**
...@@ -45,9 +57,27 @@ public final class ParsableBitArray { ...@@ -45,9 +57,27 @@ public final class ParsableBitArray {
* @param data The array to wrap. * @param data The array to wrap.
*/ */
public void reset(byte[] data) { public void reset(byte[] data) {
reset(data, data.length);
}
/**
* Updates the instance to wrap {@code data}, and resets the position to zero.
*
* @param data The array to wrap.
* @param limit The limit in bytes.
*/
public void reset(byte[] data, int limit) {
this.data = data; this.data = data;
byteOffset = 0; byteOffset = 0;
bitOffset = 0; bitOffset = 0;
byteLimit = limit;
}
/**
* Returns the number of bits yet to be read.
*/
public int bitsLeft() {
return (byteLimit - byteOffset) * 8 - bitOffset;
} }
/** /**
...@@ -67,6 +97,7 @@ public final class ParsableBitArray { ...@@ -67,6 +97,7 @@ public final class ParsableBitArray {
public void setPosition(int position) { public void setPosition(int position) {
byteOffset = position / 8; byteOffset = position / 8;
bitOffset = position - (byteOffset * 8); bitOffset = position - (byteOffset * 8);
assertValidOffset();
} }
/** /**
...@@ -81,6 +112,7 @@ public final class ParsableBitArray { ...@@ -81,6 +112,7 @@ public final class ParsableBitArray {
byteOffset++; byteOffset++;
bitOffset -= 8; bitOffset -= 8;
} }
assertValidOffset();
} }
/** /**
...@@ -103,12 +135,20 @@ public final class ParsableBitArray { ...@@ -103,12 +135,20 @@ public final class ParsableBitArray {
return 0; return 0;
} }
int retval = 0; int returnValue = 0;
// While n >= 8, read whole bytes. // While n >= 8, read whole bytes.
while (n >= 8) { while (n >= 8) {
int byteValue;
if (bitOffset != 0) {
byteValue = ((data[byteOffset] & 0xFF) << bitOffset)
| ((data[byteOffset + 1] & 0xFF) >>> (8 - bitOffset));
} else {
byteValue = data[byteOffset];
}
n -= 8; n -= 8;
retval |= (readUnsignedByte() << n); returnValue |= (byteValue & 0xFF) << n;
byteOffset++;
} }
if (n > 0) { if (n > 0) {
...@@ -117,12 +157,12 @@ public final class ParsableBitArray { ...@@ -117,12 +157,12 @@ public final class ParsableBitArray {
if (nextBit > 8) { if (nextBit > 8) {
// Combine bits from current byte and next byte. // Combine bits from current byte and next byte.
retval |= (((getUnsignedByte(byteOffset) << (nextBit - 8) returnValue |= ((((data[byteOffset] & 0xFF) << (nextBit - 8)
| (getUnsignedByte(byteOffset + 1) >> (16 - nextBit))) & writeMask)); | ((data[byteOffset + 1] & 0xFF) >> (16 - nextBit))) & writeMask));
byteOffset++; byteOffset++;
} else { } else {
// Bits to be read only within current byte. // Bits to be read only within current byte.
retval |= ((getUnsignedByte(byteOffset) >> (8 - nextBit)) & writeMask); returnValue |= (((data[byteOffset] & 0xFF) >> (8 - nextBit)) & writeMask);
if (nextBit == 8) { if (nextBit == 8) {
byteOffset++; byteOffset++;
} }
...@@ -131,7 +171,27 @@ public final class ParsableBitArray { ...@@ -131,7 +171,27 @@ public final class ParsableBitArray {
bitOffset = nextBit % 8; bitOffset = nextBit % 8;
} }
return retval; assertValidOffset();
return returnValue;
}
/**
* Peeks the length of an Exp-Golomb-coded integer (signed or unsigned) starting from the current
* offset, returning the length or -1 if the limit is reached.
*
* @return The length of the Exp-Golob-coded integer, or -1.
*/
public int peekExpGolombCodedNumLength() {
int initialByteOffset = byteOffset;
int initialBitOffset = bitOffset;
int leadingZeros = 0;
while (byteOffset < byteLimit && !readBit()) {
leadingZeros++;
}
boolean hitLimit = byteOffset == byteLimit;
byteOffset = initialByteOffset;
bitOffset = initialBitOffset;
return hitLimit ? -1 : leadingZeros * 2 + 1;
} }
/** /**
...@@ -153,22 +213,6 @@ public final class ParsableBitArray { ...@@ -153,22 +213,6 @@ public final class ParsableBitArray {
return ((codeNum % 2) == 0 ? -1 : 1) * ((codeNum + 1) / 2); return ((codeNum % 2) == 0 ? -1 : 1) * ((codeNum + 1) / 2);
} }
private int readUnsignedByte() {
int value;
if (bitOffset != 0) {
value = ((data[byteOffset] & 0xFF) << bitOffset)
| ((data[byteOffset + 1] & 0xFF) >>> (8 - bitOffset));
} else {
value = data[byteOffset];
}
byteOffset++;
return value & 0xFF;
}
private int getUnsignedByte(int offset) {
return data[offset] & 0xFF;
}
private int readExpGolombCodeNum() { private int readExpGolombCodeNum() {
int leadingZeros = 0; int leadingZeros = 0;
while (!readBit()) { while (!readBit()) {
...@@ -177,4 +221,11 @@ public final class ParsableBitArray { ...@@ -177,4 +221,11 @@ public final class ParsableBitArray {
return (1 << leadingZeros) - 1 + (leadingZeros > 0 ? readBits(leadingZeros) : 0); return (1 << leadingZeros) - 1 + (leadingZeros > 0 ? readBits(leadingZeros) : 0);
} }
private void assertValidOffset() {
// It is fine for position to be at the end of the array, but no further.
Assertions.checkState(byteOffset >= 0
&& (bitOffset >= 0 && bitOffset < 8)
&& (byteOffset < byteLimit || (byteOffset == byteLimit && bitOffset == 0)));
}
} }
...@@ -542,6 +542,22 @@ public final class Util { ...@@ -542,6 +542,22 @@ public final class Util {
} }
/** /**
* Returns a hex string representation of the data provided.
*
* @param data The byte array containing the data to be turned into a hex string.
* @param beginIndex The begin index, inclusive.
* @param endIndex The end index, exclusive.
* @return A string containing the hex representation of the data provided.
*/
public static String getHexStringFromBytes(byte[] data, int beginIndex, int endIndex) {
StringBuffer dataStringBuffer = new StringBuffer(endIndex - beginIndex);
for (int i = beginIndex; i < endIndex; i++) {
dataStringBuffer.append(String.format("%02X", data[i]));
}
return dataStringBuffer.toString();
}
/**
* Returns a user agent string based on the given application name and the library version. * Returns a user agent string based on the given application name and the library version.
* *
* @param context A valid context of the calling application. * @param context A valid context of the calling application.
......
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer;
import junit.framework.TestCase;
/**
* Unit test for {@link TimeRange}.
*/
public class TimeRangeTest extends TestCase {
public void testEquals() {
TimeRange timeRange1 = new TimeRange(TimeRange.TYPE_SNAPSHOT, 0, 30000000);
assertTrue(timeRange1.equals(timeRange1));
TimeRange timeRange2 = new TimeRange(TimeRange.TYPE_SNAPSHOT, 0, 30000000);
assertTrue(timeRange1.equals(timeRange2));
TimeRange timeRange3 = new TimeRange(TimeRange.TYPE_SNAPSHOT, 0, 60000000);
assertFalse(timeRange1.equals(timeRange3));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment