diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java index e52bb377fd..80274ecc7b 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/PlayerActivity.java @@ -26,6 +26,8 @@ import com.google.android.exoplayer.demo.player.DemoPlayer.RendererBuilder; import com.google.android.exoplayer.demo.player.HlsRendererBuilder; import com.google.android.exoplayer.demo.player.SmoothStreamingRendererBuilder; import com.google.android.exoplayer.demo.player.UnsupportedDrmException; +import com.google.android.exoplayer.metadata.GeobMetadata; +import com.google.android.exoplayer.metadata.PrivMetadata; import com.google.android.exoplayer.metadata.TxxxMetadata; import com.google.android.exoplayer.text.CaptionStyleCompat; import com.google.android.exoplayer.text.SubtitleView; @@ -446,11 +448,22 @@ public class PlayerActivity extends Activity implements SurfaceHolder.Callback, @Override public void onId3Metadata(Map metadata) { - for (int i = 0; i < metadata.size(); i++) { - if (metadata.containsKey(TxxxMetadata.TYPE)) { - TxxxMetadata txxxMetadata = (TxxxMetadata) metadata.get(TxxxMetadata.TYPE); - Log.i(TAG, String.format("ID3 TimedMetadata: description=%s, value=%s", - txxxMetadata.description, txxxMetadata.value)); + for (Map.Entry entry : metadata.entrySet()) { + if (TxxxMetadata.TYPE.equals(entry.getKey())) { + TxxxMetadata txxxMetadata = (TxxxMetadata) entry.getValue(); + Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s, value=%s", + TxxxMetadata.TYPE, txxxMetadata.description, txxxMetadata.value)); + } else if (PrivMetadata.TYPE.equals(entry.getKey())) { + PrivMetadata privMetadata = (PrivMetadata) entry.getValue(); + Log.i(TAG, String.format("ID3 TimedMetadata %s: owner=%s", + PrivMetadata.TYPE, privMetadata.owner)); + } else if (GeobMetadata.TYPE.equals(entry.getKey())) { + GeobMetadata geobMetadata = (GeobMetadata) entry.getValue(); + Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, filename=%s, description=%s", + GeobMetadata.TYPE, geobMetadata.mimeType, geobMetadata.filename, + geobMetadata.description)); + } else { + Log.i(TAG, String.format("ID3 TimedMetadata %s", entry.getKey())); } } } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java index a22e825960..682d474972 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/Samples.java @@ -123,6 +123,8 @@ import java.util.Locale; new Sample("Apple AAC media playlist", "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear0/" + "prog_index.m3u8", DemoUtil.TYPE_HLS), + new Sample("Apple ID3 metadata", "http://devimages.apple.com/samplecode/adDemo/ad.m3u8", + DemoUtil.TYPE_HLS), }; public static final Sample[] MISC = new Sample[] { diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java index 61b12e33b1..405855df19 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java @@ -370,6 +370,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { codecHotswapTimeMs = -1; inputIndex = -1; outputIndex = -1; + waitingForKeys = false; decodeOnlyPresentationTimestamps.clear(); inputBuffers = null; outputBuffers = null; @@ -418,7 +419,6 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { sourceState = SOURCE_STATE_NOT_READY; inputStreamEnded = false; outputStreamEnded = false; - waitingForKeys = false; } @Override @@ -478,6 +478,7 @@ public abstract class MediaCodecTrackRenderer extends TrackRenderer { inputIndex = -1; outputIndex = -1; waitingForFirstSyncFrame = true; + waitingForKeys = false; decodeOnlyPresentationTimestamps.clear(); // Workaround for framework bugs. // See [Internal: b/8347958], [Internal: b/8578467], [Internal: b/8543366]. diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index 7186412722..412c645eb4 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -581,7 +581,7 @@ public class DashChunkSource implements ChunkSource { } if ((result & Extractor.RESULT_READ_INDEX) != 0) { representationHolders.get(format.id).segmentIndex = - new DashWrappingSegmentIndex(extractor.getIndex(), uri, indexAnchor); + new DashWrappingSegmentIndex(extractor.getIndex(), uri.toString(), indexAnchor); } } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashWrappingSegmentIndex.java b/library/src/main/java/com/google/android/exoplayer/dash/DashWrappingSegmentIndex.java index 44648469af..d62d6ee5c2 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashWrappingSegmentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashWrappingSegmentIndex.java @@ -19,8 +19,6 @@ import com.google.android.exoplayer.chunk.parser.SegmentIndex; import com.google.android.exoplayer.dash.mpd.RangedUri; import com.google.android.exoplayer.util.Util; -import android.net.Uri; - /** * An implementation of {@link DashSegmentIndex} that wraps a {@link SegmentIndex} parsed from a * media stream. @@ -28,16 +26,16 @@ import android.net.Uri; public class DashWrappingSegmentIndex implements DashSegmentIndex { private final SegmentIndex segmentIndex; - private final Uri uri; + private final String uri; private final long indexAnchor; /** * @param segmentIndex The {@link SegmentIndex} to wrap. - * @param uri The {@link Uri} where the data is located. + * @param uri The URI where the data is located. * @param indexAnchor The index anchor point. This value is added to the byte offsets specified * in the wrapped {@link SegmentIndex}. */ - public DashWrappingSegmentIndex(SegmentIndex segmentIndex, Uri uri, long indexAnchor) { + public DashWrappingSegmentIndex(SegmentIndex segmentIndex, String uri, long indexAnchor) { this.segmentIndex = segmentIndex; this.uri = uri; this.indexAnchor = indexAnchor; diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java index 813ce0c4e0..86722c0020 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java @@ -24,9 +24,9 @@ import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase; import com.google.android.exoplayer.upstream.NetworkLoadable; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.MimeTypes; +import com.google.android.exoplayer.util.UriUtil; import com.google.android.exoplayer.util.Util; -import android.net.Uri; import android.text.TextUtils; import org.xml.sax.helpers.DefaultHandler; @@ -83,7 +83,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler throw new ParserException( "inputStream does not contain a valid media presentation description"); } - return parseMediaPresentationDescription(xpp, Util.parseBaseUri(connectionUrl)); + return parseMediaPresentationDescription(xpp, connectionUrl); } catch (XmlPullParserException e) { throw new ParserException(e); } catch (ParseException e) { @@ -92,7 +92,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler } protected MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp, - Uri baseUrl) throws XmlPullParserException, IOException, ParseException { + String baseUrl) throws XmlPullParserException, IOException, ParseException { long availabilityStartTime = parseDateTime(xpp, "availabilityStartTime", -1); long durationMs = parseDuration(xpp, "mediaPresentationDuration", -1); long minBufferTimeMs = parseDuration(xpp, "minBufferTime", -1); @@ -137,7 +137,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler return new UtcTimingElement(schemeIdUri, value); } - protected Period parsePeriod(XmlPullParser xpp, Uri baseUrl, long mpdDurationMs) + protected Period parsePeriod(XmlPullParser xpp, String baseUrl, long mpdDurationMs) throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); long startMs = parseDuration(xpp, "start", 0); @@ -170,7 +170,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler // AdaptationSet parsing. - protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, Uri baseUrl, long periodStartMs, + protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String baseUrl, long periodStartMs, long periodDurationMs, SegmentBase segmentBase) throws XmlPullParserException, IOException { String mimeType = xpp.getAttributeValue(null, "mimeType"); @@ -287,9 +287,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler // Representation parsing. - protected Representation parseRepresentation(XmlPullParser xpp, Uri baseUrl, long periodStartMs, - long periodDurationMs, String mimeType, String language, SegmentBase segmentBase) - throws XmlPullParserException, IOException { + protected Representation parseRepresentation(XmlPullParser xpp, String baseUrl, + long periodStartMs, long periodDurationMs, String mimeType, String language, + SegmentBase segmentBase) throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); int bandwidth = parseInt(xpp, "bandwidth"); int audioSamplingRate = parseInt(xpp, "audioSamplingRate"); @@ -335,7 +335,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler // SegmentBase, SegmentList and SegmentTemplate parsing. - protected SingleSegmentBase parseSegmentBase(XmlPullParser xpp, Uri baseUrl, + protected SingleSegmentBase parseSegmentBase(XmlPullParser xpp, String baseUrl, SingleSegmentBase parent) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); @@ -364,12 +364,12 @@ public class MediaPresentationDescriptionParser extends DefaultHandler } protected SingleSegmentBase buildSingleSegmentBase(RangedUri initialization, long timescale, - long presentationTimeOffset, Uri baseUrl, long indexStart, long indexLength) { + long presentationTimeOffset, String baseUrl, long indexStart, long indexLength) { return new SingleSegmentBase(initialization, timescale, presentationTimeOffset, baseUrl, indexStart, indexLength); } - protected SegmentList parseSegmentList(XmlPullParser xpp, Uri baseUrl, SegmentList parent, + protected SegmentList parseSegmentList(XmlPullParser xpp, String baseUrl, SegmentList parent, long periodDurationMs) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); @@ -413,7 +413,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler startNumber, duration, timeline, segments); } - protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, Uri baseUrl, + protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, String baseUrl, SegmentTemplate parent, long periodDurationMs) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); @@ -450,7 +450,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler protected SegmentTemplate buildSegmentTemplate(RangedUri initialization, long timescale, long presentationTimeOffset, long periodDurationMs, int startNumber, long duration, List timeline, UrlTemplate initializationTemplate, - UrlTemplate mediaTemplate, Uri baseUrl) { + UrlTemplate mediaTemplate, String baseUrl) { return new SegmentTemplate(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl); } @@ -487,15 +487,15 @@ public class MediaPresentationDescriptionParser extends DefaultHandler return defaultValue; } - protected RangedUri parseInitialization(XmlPullParser xpp, Uri baseUrl) { + protected RangedUri parseInitialization(XmlPullParser xpp, String baseUrl) { return parseRangedUrl(xpp, baseUrl, "sourceURL", "range"); } - protected RangedUri parseSegmentUrl(XmlPullParser xpp, Uri baseUrl) { + protected RangedUri parseSegmentUrl(XmlPullParser xpp, String baseUrl) { return parseRangedUrl(xpp, baseUrl, "media", "mediaRange"); } - protected RangedUri parseRangedUrl(XmlPullParser xpp, Uri baseUrl, String urlAttribute, + protected RangedUri parseRangedUrl(XmlPullParser xpp, String baseUrl, String urlAttribute, String rangeAttribute) { String urlText = xpp.getAttributeValue(null, urlAttribute); long rangeStart = 0; @@ -509,7 +509,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler return buildRangedUri(baseUrl, urlText, rangeStart, rangeLength); } - protected RangedUri buildRangedUri(Uri baseUrl, String urlText, long rangeStart, + protected RangedUri buildRangedUri(String baseUrl, String urlText, long rangeStart, long rangeLength) { return new RangedUri(baseUrl, urlText, rangeStart, rangeLength); } @@ -548,15 +548,10 @@ public class MediaPresentationDescriptionParser extends DefaultHandler } } - protected static Uri parseBaseUrl(XmlPullParser xpp, Uri parentBaseUrl) + protected static String parseBaseUrl(XmlPullParser xpp, String parentBaseUrl) throws XmlPullParserException, IOException { xpp.next(); - String newBaseUrlText = xpp.getText(); - Uri newBaseUri = Uri.parse(newBaseUrlText); - if (!newBaseUri.isAbsolute()) { - newBaseUri = Uri.withAppendedPath(parentBaseUrl, newBaseUrlText); - } - return newBaseUri; + return UriUtil.resolve(parentBaseUrl, xpp.getText()); } protected static int parseInt(XmlPullParser xpp, String name) { diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java index 2ce5ad3092..22a8bfdee5 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java @@ -16,7 +16,7 @@ package com.google.android.exoplayer.dash.mpd; import com.google.android.exoplayer.util.Assertions; -import com.google.android.exoplayer.util.Util; +import com.google.android.exoplayer.util.UriUtil; import android.net.Uri; @@ -35,31 +35,28 @@ public final class RangedUri { */ public final long length; - // The {@link Uri} is stored internally in two parts, {@link #baseUri} and {@link uriString}. - // This helps optimize memory usage in the same way that DASH manifests allow many URLs to be - // expressed concisely in the form of a single BaseURL and many relative paths. Note that this - // optimization relies on the same {@code Uri} being passed as the {@link #baseUri} to many + // The URI is stored internally in two parts: reference URI and a base URI to use when + // resolving it. This helps optimize memory usage in the same way that DASH manifests allow many + // URLs to be expressed concisely in the form of a single BaseURL and many relative paths. Note + // that this optimization relies on the same object being passed as the base URI to many // instances of this class. - private final Uri baseUri; - private final String stringUri; + private final String baseUri; + private final String referenceUri; private int hashCode; /** * Constructs an ranged uri. - *

- * See {@link Util#getMergedUri(Uri, String)} for a description of how {@code baseUri} and - * {@code stringUri} are merged. * * @param baseUri A uri that can form the base of the uri defined by the instance. - * @param stringUri A relative or absolute uri in string form. + * @param referenceUri A reference uri that should be resolved with respect to {@code baseUri}. * @param start The (zero based) index of the first byte of the range. * @param length The length of the range, or -1 to indicate that the range is unbounded. */ - public RangedUri(Uri baseUri, String stringUri, long start, long length) { - Assertions.checkArgument(baseUri != null || stringUri != null); + public RangedUri(String baseUri, String referenceUri, long start, long length) { + Assertions.checkArgument(baseUri != null || referenceUri != null); this.baseUri = baseUri; - this.stringUri = stringUri; + this.referenceUri = referenceUri; this.start = start; this.length = length; } @@ -70,7 +67,16 @@ public final class RangedUri { * @return The {@link Uri} represented by the instance. */ public Uri getUri() { - return Util.getMergedUri(baseUri, stringUri); + return UriUtil.resolveToUri(baseUri, referenceUri); + } + + /** + * Returns the uri represented by the instance as a string. + * + * @return The uri represented by the instance. + */ + public String getUriString() { + return UriUtil.resolve(baseUri, referenceUri); } /** @@ -85,13 +91,13 @@ public final class RangedUri { * @return The merged {@link RangedUri} if the merge was successful. Null otherwise. */ public RangedUri attemptMerge(RangedUri other) { - if (other == null || !getUri().equals(other.getUri())) { + if (other == null || !getUriString().equals(other.getUriString())) { return null; } else if (length != -1 && start + length == other.start) { - return new RangedUri(baseUri, stringUri, start, + return new RangedUri(baseUri, referenceUri, start, other.length == -1 ? -1 : length + other.length); } else if (other.length != -1 && other.start + other.length == start) { - return new RangedUri(baseUri, stringUri, other.start, + return new RangedUri(baseUri, referenceUri, other.start, length == -1 ? -1 : other.length + length); } else { return null; @@ -104,7 +110,7 @@ public final class RangedUri { int result = 17; result = 31 * result + (int) start; result = 31 * result + (int) length; - result = 31 * result + getUri().hashCode(); + result = 31 * result + getUriString().hashCode(); hashCode = result; } return hashCode; @@ -121,7 +127,7 @@ public final class RangedUri { RangedUri other = (RangedUri) obj; return this.start == other.start && this.length == other.length - && getUri().equals(other.getUri()); + && getUriString().equals(other.getUriString()); } } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Representation.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Representation.java index afae71de23..c5b0fcdad4 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Representation.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Representation.java @@ -147,7 +147,7 @@ public abstract class Representation { public static class SingleSegmentRepresentation extends Representation { /** - * The {@link Uri} of the single segment. + * The uri of the single segment. */ public final Uri uri; @@ -174,7 +174,7 @@ public abstract class Representation { * @param contentLength The content length, or -1 if unknown. */ public static SingleSegmentRepresentation newInstance(long periodStartMs, long periodDurationMs, - String contentId, long revisionId, Format format, Uri uri, long initializationStart, + String contentId, long revisionId, Format format, String uri, long initializationStart, long initializationEnd, long indexStart, long indexEnd, long contentLength) { RangedUri rangedUri = new RangedUri(uri, null, initializationStart, initializationEnd - initializationStart + 1); @@ -197,13 +197,13 @@ public abstract class Representation { public SingleSegmentRepresentation(long periodStartMs, long periodDurationMs, String contentId, long revisionId, Format format, SingleSegmentBase segmentBase, long contentLength) { super(periodStartMs, periodDurationMs, contentId, revisionId, format, segmentBase); - this.uri = segmentBase.uri; + this.uri = Uri.parse(segmentBase.uri); this.indexUri = segmentBase.getIndex(); this.contentLength = contentLength; // If we have an index uri then the index is defined externally, and we shouldn't return one // directly. If we don't, then we can't do better than an index defining a single segment. segmentIndex = indexUri != null ? null : new DashSingleSegmentIndex(periodStartMs * 1000, - periodDurationMs * 1000, new RangedUri(uri, null, 0, -1)); + periodDurationMs * 1000, new RangedUri(segmentBase.uri, null, 0, -1)); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java index c6eec00602..dffe7200fa 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java @@ -19,8 +19,6 @@ import com.google.android.exoplayer.C; import com.google.android.exoplayer.dash.DashSegmentIndex; import com.google.android.exoplayer.util.Util; -import android.net.Uri; - import java.util.List; /** @@ -73,7 +71,7 @@ public abstract class SegmentBase { /** * The uri of the segment. */ - public final Uri uri; + public final String uri; /* package */ final long indexStart; /* package */ final long indexLength; @@ -89,7 +87,7 @@ public abstract class SegmentBase { * @param indexLength The length of the index data in bytes. */ public SingleSegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset, - Uri uri, long indexStart, long indexLength) { + String uri, long indexStart, long indexLength) { super(initialization, timescale, presentationTimeOffset); this.uri = uri; this.indexStart = indexStart; @@ -99,7 +97,7 @@ public abstract class SegmentBase { /** * @param uri The uri of the segment. */ - public SingleSegmentBase(Uri uri) { + public SingleSegmentBase(String uri) { this(null, 1, 0, uri, 0, -1); } @@ -289,7 +287,7 @@ public abstract class SegmentBase { /* package */ final UrlTemplate initializationTemplate; /* package */ final UrlTemplate mediaTemplate; - private final Uri baseUrl; + private final String baseUrl; /** * @param initialization A {@link RangedUri} corresponding to initialization data, if such data @@ -315,7 +313,7 @@ public abstract class SegmentBase { public SegmentTemplate(RangedUri initialization, long timescale, long presentationTimeOffset, long periodDurationMs, int startNumber, long duration, List segmentTimeline, UrlTemplate initializationTemplate, - UrlTemplate mediaTemplate, Uri baseUrl) { + UrlTemplate mediaTemplate, String baseUrl) { super(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber, duration, segmentTimeline); this.initializationTemplate = initializationTemplate; diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/UtcTimingElementResolver.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/UtcTimingElementResolver.java index bfb8d042b2..eb87b23cee 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/UtcTimingElementResolver.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/UtcTimingElementResolver.java @@ -32,6 +32,7 @@ import java.io.InputStreamReader; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Locale; +import java.util.TimeZone; import java.util.concurrent.CancellationException; /** @@ -173,6 +174,7 @@ public class UtcTimingElementResolver implements Loader.Callback { try { // TODO: It may be necessary to handle timestamp offsets from UTC. SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); + format.setTimeZone(TimeZone.getTimeZone("UTC")); return format.parse(firstLine).getTime(); } catch (ParseException e) { throw new ParserException(e); diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java index 1a4025ef89..6995a4beb5 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java @@ -27,6 +27,7 @@ import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException; import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.UriUtil; import com.google.android.exoplayer.util.Util; import android.net.Uri; @@ -106,6 +107,11 @@ public class HlsChunkSource { */ public static final long DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS = 20000; + /** + * The default time for which a media playlist should be blacklisted. + */ + public static final long DEFAULT_PLAYLIST_BLACKLIST_MS = 60000; + private static final String TAG = "HlsChunkSource"; private static final String AAC_FILE_EXTENSION = ".aac"; private static final float BANDWIDTH_FRACTION = 0.8f; @@ -116,7 +122,7 @@ public class HlsChunkSource { private final Variant[] enabledVariants; private final BandwidthMeter bandwidthMeter; private final int adaptiveMode; - private final Uri baseUri; + private final String baseUri; private final int maxWidth; private final int maxHeight; private final int targetBufferSize; @@ -126,7 +132,7 @@ public class HlsChunkSource { /* package */ byte[] scratchSpace; /* package */ final HlsMediaPlaylist[] mediaPlaylists; - /* package */ final boolean[] mediaPlaylistBlacklistFlags; + /* package */ final long[] mediaPlaylistBlacklistTimesMs; /* package */ final long[] lastMediaPlaylistLoadTimesMs; /* package */ boolean live; /* package */ long durationUs; @@ -181,14 +187,14 @@ public class HlsChunkSource { if (playlist.type == HlsPlaylist.TYPE_MEDIA) { enabledVariants = new Variant[] {new Variant(0, playlistUrl, 0, null, -1, -1)}; mediaPlaylists = new HlsMediaPlaylist[1]; - mediaPlaylistBlacklistFlags = new boolean[1]; + mediaPlaylistBlacklistTimesMs = new long[1]; lastMediaPlaylistLoadTimesMs = new long[1]; setMediaPlaylist(0, (HlsMediaPlaylist) playlist); } else { Assertions.checkState(playlist.type == HlsPlaylist.TYPE_MASTER); enabledVariants = filterVariants((HlsMasterPlaylist) playlist, variantIndices); mediaPlaylists = new HlsMediaPlaylist[enabledVariants.length]; - mediaPlaylistBlacklistFlags = new boolean[enabledVariants.length]; + mediaPlaylistBlacklistTimesMs = new long[enabledVariants.length]; lastMediaPlaylistLoadTimesMs = new long[enabledVariants.length]; } @@ -296,11 +302,11 @@ public class HlsChunkSource { } HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex); - Uri chunkUri = Util.getMergedUri(mediaPlaylist.baseUri, segment.url); + Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); // Check if encryption is specified. if (HlsMediaPlaylist.ENCRYPTION_METHOD_AES_128.equals(segment.encryptionMethod)) { - Uri keyUri = Util.getMergedUri(mediaPlaylist.baseUri, segment.encryptionKeyUri); + Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri); if (!keyUri.equals(encryptionKeyUri)) { // Encryption is specified and the key has changed. HlsChunk toReturn = newEncryptionKeyChunk(keyUri, segment.encryptionIV); @@ -361,7 +367,7 @@ public class HlsChunkSource { int responseCode = responseCodeException.responseCode; if (responseCode == 404 || responseCode == 410) { MediaPlaylistChunk playlistChunk = (MediaPlaylistChunk) chunk; - mediaPlaylistBlacklistFlags[playlistChunk.variantIndex] = true; + mediaPlaylistBlacklistTimesMs[playlistChunk.variantIndex] = SystemClock.elapsedRealtime(); if (!allPlaylistsBlacklisted()) { // We've handled the 404/410 by blacklisting the playlist. Log.w(TAG, "Blacklisted playlist (" + responseCode + "): " @@ -371,7 +377,7 @@ public class HlsChunkSource { // This was the last non-blacklisted playlist. Don't blacklist it. Log.w(TAG, "Final playlist not blacklisted (" + responseCode + "): " + playlistChunk.dataSpec.uri); - mediaPlaylistBlacklistFlags[playlistChunk.variantIndex] = false; + mediaPlaylistBlacklistTimesMs[playlistChunk.variantIndex] = 0; return false; } } @@ -380,6 +386,7 @@ public class HlsChunkSource { } private int getNextVariantIndex(TsChunk previousTsChunk, long playbackPositionUs) { + clearStaleBlacklistedPlaylists(); int idealVariantIndex = getVariantIndexForBandwdith( (int) (bandwidthMeter.getBitrateEstimate() * BANDWIDTH_FRACTION)); if (idealVariantIndex == variantIndex) { @@ -392,7 +399,7 @@ public class HlsChunkSource { : adaptiveMode == ADAPTIVE_MODE_SPLICE ? previousTsChunk.startTimeUs : previousTsChunk.endTimeUs; long bufferedUs = bufferedPositionUs - playbackPositionUs; - if (mediaPlaylistBlacklistFlags[variantIndex] + if (mediaPlaylistBlacklistTimesMs[variantIndex] != 0 || (idealVariantIndex > variantIndex && bufferedUs < maxBufferDurationToSwitchDownUs) || (idealVariantIndex < variantIndex && bufferedUs > minBufferDurationToSwitchUpUs)) { // Switch variant. @@ -405,7 +412,7 @@ public class HlsChunkSource { private int getVariantIndexForBandwdith(int bandwidth) { int lowestQualityEnabledVariant = 0; for (int i = 0; i < enabledVariants.length; i++) { - if (!mediaPlaylistBlacklistFlags[i]) { + if (mediaPlaylistBlacklistTimesMs[i] == 0) { if (enabledVariants[i].bandwidth <= bandwidth) { return i; } @@ -431,14 +438,15 @@ public class HlsChunkSource { } private MediaPlaylistChunk newMediaPlaylistChunk(int variantIndex) { - Uri mediaPlaylistUri = Util.getMergedUri(baseUri, enabledVariants[variantIndex].url); - DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null); + Uri mediaPlaylistUri = UriUtil.resolveToUri(baseUri, enabledVariants[variantIndex].url); + DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null, + DataSpec.FLAG_ALLOW_GZIP); return new MediaPlaylistChunk(variantIndex, upstreamDataSource, dataSpec, mediaPlaylistUri.toString()); } private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv) { - DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNBOUNDED, null); + DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNBOUNDED, null, DataSpec.FLAG_ALLOW_GZIP); return new EncryptionKeyChunk(upstreamDataSource, dataSpec, iv); } @@ -533,14 +541,24 @@ public class HlsChunkSource { } private boolean allPlaylistsBlacklisted() { - for (int i = 0; i < mediaPlaylistBlacklistFlags.length; i++) { - if (!mediaPlaylistBlacklistFlags[i]) { + for (int i = 0; i < mediaPlaylistBlacklistTimesMs.length; i++) { + if (mediaPlaylistBlacklistTimesMs[i] == 0) { return false; } } return true; } + private void clearStaleBlacklistedPlaylists() { + long currentTime = SystemClock.elapsedRealtime(); + for (int i = 0; i < mediaPlaylistBlacklistTimesMs.length; i++) { + if (mediaPlaylistBlacklistTimesMs[i] != 0 + && currentTime - mediaPlaylistBlacklistTimesMs[i] > DEFAULT_PLAYLIST_BLACKLIST_MS) { + mediaPlaylistBlacklistTimesMs[i] = 0; + } + } + } + private class MediaPlaylistChunk extends DataChunk { @SuppressWarnings("hiding") diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java index 7ce299df0d..0a2b008d2f 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java @@ -15,8 +15,6 @@ */ package com.google.android.exoplayer.hls; -import android.net.Uri; - import java.util.List; /** @@ -26,7 +24,7 @@ public final class HlsMasterPlaylist extends HlsPlaylist { public final List variants; - public HlsMasterPlaylist(Uri baseUri, List variants) { + public HlsMasterPlaylist(String baseUri, List variants) { super(baseUri, HlsPlaylist.TYPE_MASTER); this.variants = variants; } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylist.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylist.java index 3e9f151c08..16e90083d0 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylist.java @@ -17,8 +17,6 @@ package com.google.android.exoplayer.hls; import com.google.android.exoplayer.C; -import android.net.Uri; - import java.util.List; /** @@ -70,7 +68,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { public final boolean live; public final long durationUs; - public HlsMediaPlaylist(Uri baseUri, int mediaSequence, int targetDurationSecs, int version, + public HlsMediaPlaylist(String baseUri, int mediaSequence, int targetDurationSecs, int version, boolean live, List segments) { super(baseUri, HlsPlaylist.TYPE_MEDIA); this.mediaSequence = mediaSequence; diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylist.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylist.java index 3c86328ba6..b6cd9dac9a 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylist.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylist.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer.hls; -import android.net.Uri; /** * Represents an HLS playlist. @@ -25,10 +24,10 @@ public abstract class HlsPlaylist { public final static int TYPE_MASTER = 0; public final static int TYPE_MEDIA = 1; - public final Uri baseUri; + public final String baseUri; public final int type; - protected HlsPlaylist(Uri baseUri, int type) { + protected HlsPlaylist(String baseUri, int type) { this.baseUri = baseUri; this.type = type; } diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java index 8db2094c9f..3fe3d15fd5 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java @@ -19,9 +19,6 @@ import com.google.android.exoplayer.C; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.hls.HlsMediaPlaylist.Segment; import com.google.android.exoplayer.upstream.NetworkLoadable; -import com.google.android.exoplayer.util.Util; - -import android.net.Uri; import java.io.BufferedReader; import java.io.IOException; @@ -86,7 +83,6 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser extraLines = new LinkedList(); String line; @@ -97,7 +93,7 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser variants = new ArrayList(); int bandwidth = 0; @@ -160,7 +156,7 @@ public final class HlsPlaylistParser implements NetworkLoadable.Parser> { + private static final int ID3_TEXT_ENCODING_ISO_8859_1 = 0; + private static final int ID3_TEXT_ENCODING_UTF_16 = 1; + private static final int ID3_TEXT_ENCODING_UTF_16BE = 2; + private static final int ID3_TEXT_ENCODING_UTF_8 = 3; + @Override public boolean canParse(String mimeType) { return mimeType.equals(MimeTypes.APPLICATION_ID3); @@ -60,13 +65,48 @@ public class Id3Parser implements MetadataParser> { byte[] frame = new byte[frameSize - 1]; id3Data.readBytes(frame, 0, frameSize - 1); - int firstZeroIndex = indexOf(frame, 0, (byte) 0); + int firstZeroIndex = indexOfEOS(frame, 0, encoding); String description = new String(frame, 0, firstZeroIndex, charset); - int valueStartIndex = indexOfNot(frame, firstZeroIndex, (byte) 0); - int valueEndIndex = indexOf(frame, valueStartIndex, (byte) 0); + int valueStartIndex = firstZeroIndex + delimiterLength(encoding); + int valueEndIndex = indexOfEOS(frame, valueStartIndex, encoding); String value = new String(frame, valueStartIndex, valueEndIndex - valueStartIndex, charset); metadata.put(TxxxMetadata.TYPE, new TxxxMetadata(description, value)); + } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') { + // Check frame ID == PRIV + byte[] frame = new byte[frameSize]; + id3Data.readBytes(frame, 0, frameSize); + + int firstZeroIndex = indexOf(frame, 0, (byte) 0); + String owner = new String(frame, 0, firstZeroIndex, "ISO-8859-1"); + byte[] privateData = new byte[frameSize - firstZeroIndex - 1]; + System.arraycopy(frame, firstZeroIndex + 1, privateData, 0, frameSize - firstZeroIndex - 1); + metadata.put(PrivMetadata.TYPE, new PrivMetadata(owner, privateData)); + } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' && frameId3 == 'B') { + // Check frame ID == GEOB + int encoding = id3Data.readUnsignedByte(); + String charset = getCharsetName(encoding); + byte[] frame = new byte[frameSize - 1]; + id3Data.readBytes(frame, 0, frameSize - 1); + + int firstZeroIndex = indexOf(frame, 0, (byte) 0); + String mimeType = new String(frame, 0, firstZeroIndex, "ISO-8859-1"); + int filenameStartIndex = firstZeroIndex + 1; + int filenameEndIndex = indexOfEOS(frame, filenameStartIndex, encoding); + String filename = new String(frame, filenameStartIndex, + filenameEndIndex - filenameStartIndex, charset); + int descriptionStartIndex = filenameEndIndex + delimiterLength(encoding); + int descriptionEndIndex = indexOfEOS(frame, descriptionStartIndex, encoding); + String description = new String(frame, descriptionStartIndex, + descriptionEndIndex - descriptionStartIndex, charset); + + int objectDataSize = frameSize - 1 /* encoding byte */ - descriptionEndIndex + - delimiterLength(encoding); + byte[] objectData = new byte[objectDataSize]; + System.arraycopy(frame, descriptionEndIndex + delimiterLength(encoding), objectData, 0, + objectDataSize); + metadata.put(GeobMetadata.TYPE, new GeobMetadata(mimeType, filename, + description, objectData)); } else { String type = String.format("%c%c%c%c", frameId0, frameId1, frameId2, frameId3); byte[] frame = new byte[frameSize]; @@ -89,15 +129,30 @@ public class Id3Parser implements MetadataParser> { return data.length; } - private static int indexOfNot(byte[] data, int fromIndex, byte key) { - for (int i = fromIndex; i < data.length; i++) { - if (data[i] != key) { - return i; - } + private static int indexOfEOS(byte[] data, int fromIndex, int encodingByte) { + int terminationPos = indexOf(data, fromIndex, (byte) 0); + + // For single byte encoding charsets, we are done + if (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 || encodingByte == ID3_TEXT_ENCODING_UTF_8) { + return terminationPos; } + + // Otherwise, look for a two zero bytes + while (terminationPos < data.length - 1) { + if (data[terminationPos + 1] == (byte) 0) { + return terminationPos; + } + terminationPos = indexOf(data, terminationPos + 1, (byte) 0); + } + return data.length; } + private static int delimiterLength(int encodingByte) { + return (encodingByte == ID3_TEXT_ENCODING_ISO_8859_1 + || encodingByte == ID3_TEXT_ENCODING_UTF_8) ? 1 : 2; + } + /** * Parses an ID3 header. * @@ -142,13 +197,13 @@ public class Id3Parser implements MetadataParser> { */ private static String getCharsetName(int encodingByte) { switch (encodingByte) { - case 0: + case ID3_TEXT_ENCODING_ISO_8859_1: return "ISO-8859-1"; - case 1: + case ID3_TEXT_ENCODING_UTF_16: return "UTF-16"; - case 2: + case ID3_TEXT_ENCODING_UTF_16BE: return "UTF-16BE"; - case 3: + case ID3_TEXT_ENCODING_UTF_8: return "UTF-8"; default: return "ISO-8859-1"; diff --git a/library/src/main/java/com/google/android/exoplayer/metadata/PrivMetadata.java b/library/src/main/java/com/google/android/exoplayer/metadata/PrivMetadata.java new file mode 100644 index 0000000000..8573b25906 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/metadata/PrivMetadata.java @@ -0,0 +1,34 @@ +/* + * 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.metadata; + +/** + * A metadata that contains parsed ID3 PRIV (Private) frame data associated + * with time indices. + */ +public class PrivMetadata { + + public static final String TYPE = "PRIV"; + + public final String owner; + public final byte[] privateData; + + public PrivMetadata(String owner, byte[] privateData) { + this.owner = owner; + this.privateData = privateData; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/mp4/Atom.java b/library/src/main/java/com/google/android/exoplayer/mp4/Atom.java index 292c231087..b3b96dce94 100644 --- a/library/src/main/java/com/google/android/exoplayer/mp4/Atom.java +++ b/library/src/main/java/com/google/android/exoplayer/mp4/Atom.java @@ -24,6 +24,7 @@ import java.util.List; public abstract class Atom { + public static final int TYPE_ftyp = getAtomTypeInteger("ftyp"); public static final int TYPE_avc1 = getAtomTypeInteger("avc1"); public static final int TYPE_avc3 = getAtomTypeInteger("avc3"); public static final int TYPE_esds = getAtomTypeInteger("esds"); diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java index d7e6ee1358..b04e272575 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer.smoothstreaming; import com.google.android.exoplayer.C; import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.UriUtil; import com.google.android.exoplayer.util.Util; import android.net.Uri; @@ -197,14 +198,14 @@ public class SmoothStreamingManifest { public final TrackElement[] tracks; public final int chunkCount; - private final Uri baseUri; + private final String baseUri; private final String chunkTemplate; private final List chunkStartTimes; private final long[] chunkStartTimesUs; private final long lastChunkDurationUs; - public StreamElement(Uri baseUri, String chunkTemplate, int type, String subType, + public StreamElement(String baseUri, String chunkTemplate, int type, String subType, long timescale, String name, int qualityLevels, int maxWidth, int maxHeight, int displayWidth, int displayHeight, String language, TrackElement[] tracks, List chunkStartTimes, long lastChunkDuration) { @@ -274,7 +275,7 @@ public class SmoothStreamingManifest { String chunkUrl = chunkTemplate .replace(URL_PLACEHOLDER_BITRATE, Integer.toString(tracks[track].bitrate)) .replace(URL_PLACEHOLDER_START_TIME, chunkStartTimes.get(chunkIndex).toString()); - return Util.getMergedUri(baseUri, chunkUrl); + return UriUtil.resolveToUri(baseUri, chunkUrl); } } diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java index 5cfbc829e3..27dd7752da 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingManifestParser.java @@ -23,9 +23,7 @@ import com.google.android.exoplayer.upstream.NetworkLoadable; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.CodecSpecificDataUtil; import com.google.android.exoplayer.util.MimeTypes; -import com.google.android.exoplayer.util.Util; -import android.net.Uri; import android.util.Base64; import android.util.Pair; @@ -65,8 +63,8 @@ public class SmoothStreamingManifestParser implements try { XmlPullParser xmlParser = xmlParserFactory.newPullParser(); xmlParser.setInput(inputStream, null); - SmoothStreamMediaParser smoothStreamMediaParser = new SmoothStreamMediaParser(null, - Util.parseBaseUri(connectionUrl)); + SmoothStreamMediaParser smoothStreamMediaParser = + new SmoothStreamMediaParser(null, connectionUrl); return (SmoothStreamingManifest) smoothStreamMediaParser.parse(xmlParser); } catch (XmlPullParserException e) { throw new ParserException(e); @@ -89,13 +87,13 @@ public class SmoothStreamingManifestParser implements */ private static abstract class ElementParser { - private final Uri baseUri; + private final String baseUri; private final String tag; private final ElementParser parent; private final List> normalizedAttributes; - public ElementParser(ElementParser parent, Uri baseUri, String tag) { + public ElementParser(ElementParser parent, String baseUri, String tag) { this.parent = parent; this.baseUri = baseUri; this.tag = tag; @@ -158,7 +156,7 @@ public class SmoothStreamingManifestParser implements } } - private ElementParser newChildParser(ElementParser parent, String name, Uri baseUri) { + private ElementParser newChildParser(ElementParser parent, String name, String baseUri) { if (TrackElementParser.TAG.equals(name)) { return new TrackElementParser(parent, baseUri); } else if (ProtectionElementParser.TAG.equals(name)) { @@ -342,7 +340,7 @@ public class SmoothStreamingManifestParser implements private ProtectionElement protectionElement; private List streamElements; - public SmoothStreamMediaParser(ElementParser parent, Uri baseUri) { + public SmoothStreamMediaParser(ElementParser parent, String baseUri) { super(parent, baseUri, TAG); lookAheadCount = -1; protectionElement = null; @@ -392,7 +390,7 @@ public class SmoothStreamingManifestParser implements private UUID uuid; private byte[] initData; - public ProtectionElementParser(ElementParser parent, Uri baseUri) { + public ProtectionElementParser(ElementParser parent, String baseUri) { super(parent, baseUri, TAG); } @@ -455,7 +453,7 @@ public class SmoothStreamingManifestParser implements private static final String KEY_FRAGMENT_START_TIME = "t"; private static final String KEY_FRAGMENT_REPEAT_COUNT = "r"; - private final Uri baseUri; + private final String baseUri; private final List tracks; private int type; @@ -473,7 +471,7 @@ public class SmoothStreamingManifestParser implements private long lastChunkDuration; - public StreamElementParser(ElementParser parent, Uri baseUri) { + public StreamElementParser(ElementParser parent, String baseUri) { super(parent, baseUri, TAG); this.baseUri = baseUri; tracks = new LinkedList(); @@ -615,7 +613,7 @@ public class SmoothStreamingManifestParser implements private int nalUnitLengthField; private String content; - public TrackElementParser(ElementParser parent, Uri baseUri) { + public TrackElementParser(ElementParser parent, String baseUri) { super(parent, baseUri, TAG); this.csd = new LinkedList(); } diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java b/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java index 5c4dcd65b2..33519d4e6c 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DataSourceStream.java @@ -230,7 +230,7 @@ public final class DataSourceStream implements Loadable, NonBlockingInputStream long remainingLength = resolvedLength != C.LENGTH_UNBOUNDED ? resolvedLength - loadPosition : C.LENGTH_UNBOUNDED; loadDataSpec = new DataSpec(dataSpec.uri, dataSpec.position + loadPosition, - remainingLength, dataSpec.key); + remainingLength, dataSpec.key, dataSpec.flags); dataSource.open(loadDataSpec); } diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DataSpec.java b/library/src/main/java/com/google/android/exoplayer/upstream/DataSpec.java index ff3b7dda0d..a153d955cb 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DataSpec.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DataSpec.java @@ -25,22 +25,32 @@ import android.net.Uri; */ public final class DataSpec { + /** + * Permits an underlying network stack to request that the server use gzip compression. + *

+ * Should not typically be set if the data being requested is already compressed (e.g. most audio + * and video requests). May be set when requesting other data. + *

+ * When a {@link DataSource} is used to request data with this flag set, and if the + * {@link DataSource} does make a network request, then the value returned from + * {@link DataSource#open(DataSpec)} will typically be {@link C#LENGTH_UNBOUNDED}. The data read + * from {@link DataSource#read(byte[], int, int)} will be the decompressed data. + */ + public static final int FLAG_ALLOW_GZIP = 1; + /** * Identifies the source from which data should be read. */ public final Uri uri; - /** - * True if the data at {@link #uri} is the full stream. False otherwise. An example where this - * may be false is if {@link #uri} defines the location of a cached part of the stream. - */ - public final boolean uriIsFullStream; /** * The absolute position of the data in the full stream. */ public final long absoluteStreamPosition; /** - * The position of the data when read from {@link #uri}. Always equal to - * {@link #absoluteStreamPosition} if {@link #uriIsFullStream}. + * The position of the data when read from {@link #uri}. + *

+ * Always equal to {@link #absoluteStreamPosition} unless the {@link #uri} defines the location + * of a subset of the underyling data. */ public final long position; /** @@ -52,6 +62,10 @@ public final class DataSpec { * {@link DataSpec} is not intended to be used in conjunction with a cache. */ public final String key; + /** + * Request flags. Currently {@link #FLAG_ALLOW_GZIP} is the only supported flag. + */ + public final int flags; /** * Construct a {@link DataSpec} for the given uri and with {@link #key} set to null. @@ -59,11 +73,21 @@ public final class DataSpec { * @param uri {@link #uri}. */ public DataSpec(Uri uri) { - this(uri, 0, C.LENGTH_UNBOUNDED, null); + this(uri, 0); } /** - * Construct a {@link DataSpec} for which {@link #uriIsFullStream} is true. + * Construct a {@link DataSpec} for the given uri and with {@link #key} set to null. + * + * @param uri {@link #uri}. + * @param flags {@link #flags}. + */ + public DataSpec(Uri uri, int flags) { + this(uri, 0, C.LENGTH_UNBOUNDED, null, flags); + } + + /** + * Construct a {@link DataSpec} where {@link #position} equals {@link #absoluteStreamPosition}. * * @param uri {@link #uri}. * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}. @@ -71,50 +95,50 @@ public final class DataSpec { * @param key {@link #key}. */ public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key) { - this(uri, absoluteStreamPosition, length, key, absoluteStreamPosition, true); + this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, 0); } /** - * Construct a {@link DataSpec} for which {@link #uriIsFullStream} is false. + * Construct a {@link DataSpec} where {@link #position} equals {@link #absoluteStreamPosition}. * * @param uri {@link #uri}. - * @param absoluteStreamPosition {@link #absoluteStreamPosition}. + * @param absoluteStreamPosition {@link #absoluteStreamPosition}, equal to {@link #position}. * @param length {@link #length}. * @param key {@link #key}. - * @param position {@link #position}. + * @param flags {@link #flags}. */ - public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key, long position) { - this(uri, absoluteStreamPosition, length, key, position, false); + public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key, int flags) { + this(uri, absoluteStreamPosition, absoluteStreamPosition, length, key, flags); } /** - * Construct a {@link DataSpec}. + * Construct a {@link DataSpec} where {@link #position} may differ from + * {@link #absoluteStreamPosition}. * * @param uri {@link #uri}. * @param absoluteStreamPosition {@link #absoluteStreamPosition}. + * @param position {@link #position}. * @param length {@link #length}. * @param key {@link #key}. - * @param position {@link #position}. - * @param uriIsFullStream {@link #uriIsFullStream}. + * @param flags {@link #flags}. */ - public DataSpec(Uri uri, long absoluteStreamPosition, long length, String key, long position, - boolean uriIsFullStream) { + public DataSpec(Uri uri, long absoluteStreamPosition, long position, long length, String key, + int flags) { Assertions.checkArgument(absoluteStreamPosition >= 0); Assertions.checkArgument(position >= 0); Assertions.checkArgument(length > 0 || length == C.LENGTH_UNBOUNDED); - Assertions.checkArgument(absoluteStreamPosition == position || !uriIsFullStream); this.uri = uri; - this.uriIsFullStream = uriIsFullStream; this.absoluteStreamPosition = absoluteStreamPosition; this.position = position; this.length = length; this.key = key; + this.flags = flags; } @Override public String toString() { - return "DataSpec[" + uri + ", " + uriIsFullStream + ", " + absoluteStreamPosition + ", " + - position + ", " + length + ", " + key + "]"; + return "DataSpec[" + uri + ", " + ", " + absoluteStreamPosition + ", " + + position + ", " + length + ", " + key + ", " + flags + "]"; } } diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultHttpDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultHttpDataSource.java index 6fbab8982e..8e0a4b75ec 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/DefaultHttpDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/DefaultHttpDataSource.java @@ -132,19 +132,6 @@ public class DefaultHttpDataSource implements HttpDataSource { } } - /* - * TODO: If the server uses gzip compression when serving the response, this may end up returning - * the size of the compressed response, where-as it should be returning the decompressed size or - * -1. See: developer.android.com/reference/java/net/HttpURLConnection.html - * - * To fix this we should: - * - * 1. Explicitly require no compression for media requests (since media should be compressed - * already) by setting the Accept-Encoding header to "identity" - * 2. In other cases, for example when requesting manifests, we don't want to disable compression. - * For these cases we should ensure that we return -1 here (and avoid performing any sanity - * checks on the content length). - */ @Override public long open(DataSpec dataSpec) throws HttpDataSourceException { this.dataSpec = dataSpec; @@ -177,16 +164,23 @@ public class DefaultHttpDataSource implements HttpDataSource { throw new InvalidContentTypeException(contentType, dataSpec); } - long contentLength = getContentLength(connection); - dataLength = dataSpec.length == C.LENGTH_UNBOUNDED ? contentLength : dataSpec.length; - - if (dataSpec.length != C.LENGTH_UNBOUNDED && contentLength != C.LENGTH_UNBOUNDED - && contentLength != dataSpec.length) { - // The DataSpec specified a length and we resolved a length from the response headers, but - // the two lengths do not match. - closeConnection(); - throw new HttpDataSourceException( - new UnexpectedLengthException(dataSpec.length, contentLength), dataSpec); + if ((dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) == 0) { + long contentLength = getContentLength(connection); + dataLength = dataSpec.length == C.LENGTH_UNBOUNDED ? contentLength : dataSpec.length; + if (dataSpec.length != C.LENGTH_UNBOUNDED && contentLength != C.LENGTH_UNBOUNDED + && contentLength != dataSpec.length) { + // The DataSpec specified a length and we resolved a length from the response headers, but + // the two lengths do not match. + closeConnection(); + throw new HttpDataSourceException( + new UnexpectedLengthException(dataSpec.length, contentLength), dataSpec); + } + } else { + // Gzip is enabled. If the server opts to use gzip then the content length in the response + // will be that of the compressed data, which isn't what we want. Furthermore, there isn't a + // reliable way to determine whether the gzip was used or not. Hence we always treat the + // length as unknown. + dataLength = C.LENGTH_UNBOUNDED; } try { @@ -301,6 +295,9 @@ public class DefaultHttpDataSource implements HttpDataSource { } setRangeHeader(connection, dataSpec); connection.setRequestProperty("User-Agent", userAgent); + if ((dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) == 0) { + connection.setRequestProperty("Accept-Encoding", "identity"); + } connection.connect(); return connection; } diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/NetworkLoadable.java b/library/src/main/java/com/google/android/exoplayer/upstream/NetworkLoadable.java index 5e8058f6dd..d4f4e6abf7 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/NetworkLoadable.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/NetworkLoadable.java @@ -63,7 +63,7 @@ public final class NetworkLoadable implements Loadable { public NetworkLoadable(String url, HttpDataSource httpDataSource, Parser parser) { this.httpDataSource = httpDataSource; this.parser = parser; - dataSpec = new DataSpec(Uri.parse(url)); + dataSpec = new DataSpec(Uri.parse(url), DataSpec.FLAG_ALLOW_GZIP); } /** diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/TeeDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/TeeDataSource.java index cbb571f308..2e140db6c8 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/TeeDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/TeeDataSource.java @@ -42,8 +42,8 @@ public final class TeeDataSource implements DataSource { long dataLength = upstream.open(dataSpec); if (dataSpec.length == C.LENGTH_UNBOUNDED && dataLength != C.LENGTH_UNBOUNDED) { // Reconstruct dataSpec in order to provide the resolved length to the sink. - dataSpec = new DataSpec(dataSpec.uri, dataSpec.absoluteStreamPosition, dataLength, - dataSpec.key, dataSpec.position, dataSpec.uriIsFullStream); + dataSpec = new DataSpec(dataSpec.uri, dataSpec.absoluteStreamPosition, dataSpec.position, + dataLength, dataSpec.key, dataSpec.flags); } dataSink.open(dataSpec); return dataLength; diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java index 5842d742ab..63a3763133 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/cache/CacheDataSource.java @@ -22,7 +22,6 @@ import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.FileDataSource; import com.google.android.exoplayer.upstream.TeeDataSource; import com.google.android.exoplayer.upstream.cache.CacheDataSink.CacheDataSinkException; -import com.google.android.exoplayer.util.Assertions; import android.net.Uri; import android.util.Log; @@ -64,6 +63,7 @@ public final class CacheDataSource implements DataSource { private DataSource currentDataSource; private Uri uri; + private int flags; private String key; private long readPosition; private long bytesRemaining; @@ -125,9 +125,9 @@ public final class CacheDataSource implements DataSource { @Override public long open(DataSpec dataSpec) throws IOException { - Assertions.checkState(dataSpec.uriIsFullStream); try { uri = dataSpec.uri; + flags = dataSpec.flags; key = dataSpec.key; readPosition = dataSpec.position; bytesRemaining = dataSpec.length; @@ -201,19 +201,19 @@ public final class CacheDataSource implements DataSource { // The data is locked in the cache, or we're ignoring the cache. Bypass the cache and read // from upstream. currentDataSource = upstreamDataSource; - dataSpec = new DataSpec(uri, readPosition, bytesRemaining, key); + dataSpec = new DataSpec(uri, readPosition, bytesRemaining, key, flags); } else if (span.isCached) { // Data is cached, read from cache. Uri fileUri = Uri.fromFile(span.file); long filePosition = readPosition - span.position; long length = Math.min(span.length - filePosition, bytesRemaining); - dataSpec = new DataSpec(fileUri, readPosition, length, key, filePosition); + dataSpec = new DataSpec(fileUri, readPosition, filePosition, length, key, flags); currentDataSource = cacheReadDataSource; } else { // Data is not cached, and data is not locked, read from upstream with cache backing. lockedSpan = span; long length = span.isOpenEnded() ? bytesRemaining : Math.min(span.length, bytesRemaining); - dataSpec = new DataSpec(uri, readPosition, length, key); + dataSpec = new DataSpec(uri, readPosition, length, key, flags); currentDataSource = cacheWriteDataSource != null ? cacheWriteDataSource : upstreamDataSource; } diff --git a/library/src/main/java/com/google/android/exoplayer/util/UriUtil.java b/library/src/main/java/com/google/android/exoplayer/util/UriUtil.java new file mode 100644 index 0000000000..61eb8fa0a4 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/util/UriUtil.java @@ -0,0 +1,258 @@ +/* + * 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.util; + +import android.net.Uri; +import android.text.TextUtils; + +/** + * Utility methods for manipulating URIs. + */ +public final class UriUtil { + + /** + * The length of arrays returned by {@link #getUriIndices(String)}. + */ + private static final int INDEX_COUNT = 4; + /** + * An index into an array returned by {@link #getUriIndices(String)}. + *

+ * The value at this position in the array is the index of the ':' after the scheme. Equals -1 if + * the URI is a relative reference (no scheme). The hier-part starts at (schemeColon + 1), + * including when the URI has no scheme. + */ + private static final int SCHEME_COLON = 0; + /** + * An index into an array returned by {@link #getUriIndices(String)}. + *

+ * The value at this position in the array is the index of the path part. Equals (schemeColon + 1) + * if no authority part, (schemeColon + 3) if the authority part consists of just "//", and + * (query) if no path part. The characters starting at this index can be "//" only if the + * authority part is non-empty (in this case the double-slash means the first segment is empty). + */ + private static final int PATH = 1; + /** + * An index into an array returned by {@link #getUriIndices(String)}. + *

+ * The value at this position in the array is the index of the query part, including the '?' + * before the query. Equals fragment if no query part, and (fragment - 1) if the query part is a + * single '?' with no data. + */ + private static final int QUERY = 2; + /** + * An index into an array returned by {@link #getUriIndices(String)}. + *

+ * The value at this position in the array is the index of the fragment part, including the '#' + * before the fragment. Equal to the length of the URI if no fragment part, and (length - 1) if + * the fragment part is a single '#' with no data. + */ + private static final int FRAGMENT = 3; + + private UriUtil() {} + + /** + * Like {@link #resolve(String, String)}, but returns a {@link Uri} instead of a {@link String}. + * + * @param baseUri The base URI. + * @param referenceUri The reference URI to resolve. + */ + public static Uri resolveToUri(String baseUri, String referenceUri) { + return Uri.parse(resolve(baseUri, referenceUri)); + } + + /** + * Performs relative resolution of a {@code referenceUri} with respect to a {@code baseUri}. + *

+ * The resolution is performed as specified by RFC-3986. + * + * @param baseUri The base URI. + * @param referenceUri The reference URI to resolve. + */ + public static String resolve(String baseUri, String referenceUri) { + StringBuilder uri = new StringBuilder(); + + // Map null onto empty string, to make the following logic simpler. + baseUri = baseUri == null ? "" : baseUri; + referenceUri = referenceUri == null ? "" : referenceUri; + + int[] refIndices = getUriIndices(referenceUri); + if (refIndices[SCHEME_COLON] != -1) { + // The reference is absolute. The target Uri is the reference. + uri.append(referenceUri); + removeDotSegments(uri, refIndices[PATH], refIndices[QUERY]); + return uri.toString(); + } + + int[] baseIndices = getUriIndices(baseUri); + if (refIndices[FRAGMENT] == 0) { + // The reference is empty or contains just the fragment part, then the target Uri is the + // concatenation of the base Uri without its fragment, and the reference. + return uri.append(baseUri, 0, baseIndices[FRAGMENT]).append(referenceUri).toString(); + } + + if (refIndices[QUERY] == 0) { + // The reference starts with the query part. The target is the base up to (but excluding) the + // query, plus the reference. + return uri.append(baseUri, 0, baseIndices[QUERY]).append(referenceUri).toString(); + } + + if (refIndices[PATH] != 0) { + // The reference has authority. The target is the base scheme plus the reference. + int baseLimit = baseIndices[SCHEME_COLON] + 1; + uri.append(baseUri, 0, baseLimit).append(referenceUri); + return removeDotSegments(uri, baseLimit + refIndices[PATH], baseLimit + refIndices[QUERY]); + } + + if (refIndices[PATH] != refIndices[QUERY] && referenceUri.charAt(refIndices[PATH]) == '/') { + // The reference path is rooted. The target is the base scheme and authority (if any), plus + // the reference. + uri.append(baseUri, 0, baseIndices[PATH]).append(referenceUri); + return removeDotSegments(uri, baseIndices[PATH], baseIndices[PATH] + refIndices[QUERY]); + } + + // The target Uri is the concatenation of the base Uri up to (but excluding) the last segment, + // and the reference. This can be split into 2 cases: + if (baseIndices[SCHEME_COLON] + 2 < baseIndices[PATH] + && baseIndices[PATH] == baseIndices[QUERY]) { + // Case 1: The base hier-part is just the authority, with an empty path. An additional '/' is + // needed after the authority, before appending the reference. + uri.append(baseUri, 0, baseIndices[PATH]).append('/').append(referenceUri); + return removeDotSegments(uri, baseIndices[PATH], baseIndices[PATH] + refIndices[QUERY] + 1); + } else { + // Case 2: Otherwise, find the last '/' in the base hier-part and append the reference after + // it. If base hier-part has no '/', it could only mean that it is completely empty or + // contains only one segment, in which case the whole hier-part is excluded and the reference + // is appended right after the base scheme colon without an added '/'. + int lastSlashIndex = baseUri.lastIndexOf('/', baseIndices[QUERY] - 1); + int baseLimit = lastSlashIndex == -1 ? baseIndices[PATH] : lastSlashIndex + 1; + uri.append(baseUri, 0, baseLimit).append(referenceUri); + return removeDotSegments(uri, baseIndices[PATH], baseLimit + refIndices[QUERY]); + } + } + + /** + * Removes dot segments from the path of a URI. + * + * @param uri A {@link StringBuilder} containing the URI. + * @param offset The index of the start of the path in {@code uri}. + * @param limit The limit (exclusive) of the path in {@code uri}. + */ + private static String removeDotSegments(StringBuilder uri, int offset, int limit) { + if (offset >= limit) { + // Nothing to do. + return uri.toString(); + } + if (uri.charAt(offset) == '/') { + // If the path starts with a /, always retain it. + offset++; + } + // The first character of the current path segment. + int segmentStart = offset; + int i = offset; + while (i <= limit) { + int nextSegmentStart = -1; + if (i == limit) { + nextSegmentStart = i; + } else if (uri.charAt(i) == '/') { + nextSegmentStart = i + 1; + } else { + i++; + continue; + } + // We've encountered the end of a segment or the end of the path. If the final segment was + // "." or "..", remove the appropriate segments of the path. + if (i == segmentStart + 1 && uri.charAt(segmentStart) == '.') { + // Given "abc/def/./ghi", remove "./" to get "abc/def/ghi". + uri.delete(segmentStart, nextSegmentStart); + limit -= nextSegmentStart - segmentStart; + i = segmentStart; + } else if (i == segmentStart + 2 && uri.charAt(segmentStart) == '.' + && uri.charAt(segmentStart + 1) == '.') { + // Given "abc/def/../ghi", remove "def/../" to get "abc/ghi". + int prevSegmentStart = uri.lastIndexOf("/", segmentStart - 2) + 1; + int removeFrom = prevSegmentStart > offset ? prevSegmentStart : offset; + uri.delete(removeFrom, nextSegmentStart); + limit -= nextSegmentStart - removeFrom; + segmentStart = prevSegmentStart; + i = prevSegmentStart; + } else { + i++; + segmentStart = i; + } + } + return uri.toString(); + } + + /** + * Calculates indices of the constituent components of a URI. + * + * @param uriString The URI as a string. + * @return The corresponding indices. + */ + private static int[] getUriIndices(String uriString) { + int[] indices = new int[INDEX_COUNT]; + if (TextUtils.isEmpty(uriString)) { + indices[SCHEME_COLON] = -1; + return indices; + } + + // Determine outer structure from right to left. + // Uri = scheme ":" hier-part [ "?" query ] [ "#" fragment ] + int length = uriString.length(); + int fragmentIndex = uriString.indexOf('#'); + if (fragmentIndex == -1) { + fragmentIndex = length; + } + int queryIndex = uriString.indexOf('?'); + if (queryIndex == -1 || queryIndex > fragmentIndex) { + // '#' before '?': '?' is within the fragment. + queryIndex = fragmentIndex; + } + // Slashes are allowed only in hier-part so any colon after the first slash is part of the + // hier-part, not the scheme colon separator. + int schemeIndexLimit = uriString.indexOf('/'); + if (schemeIndexLimit == -1 || schemeIndexLimit > queryIndex) { + schemeIndexLimit = queryIndex; + } + int schemeIndex = uriString.indexOf(':'); + if (schemeIndex > schemeIndexLimit) { + // '/' before ':' + schemeIndex = -1; + } + + // Determine hier-part structure: hier-part = "//" authority path / path + // This block can also cope with schemeIndex == -1. + boolean hasAuthority = schemeIndex + 2 < queryIndex + && uriString.charAt(schemeIndex + 1) == '/' + && uriString.charAt(schemeIndex + 2) == '/'; + int pathIndex; + if (hasAuthority) { + pathIndex = uriString.indexOf('/', schemeIndex + 3); // find first '/' after "://" + if (pathIndex == -1 || pathIndex > queryIndex) { + pathIndex = queryIndex; + } + } else { + pathIndex = schemeIndex + 1; + } + + indices[SCHEME_COLON] = schemeIndex; + indices[PATH] = pathIndex; + indices[QUERY] = queryIndex; + indices[FRAGMENT] = fragmentIndex; + return indices; + } + +} diff --git a/library/src/main/java/com/google/android/exoplayer/util/Util.java b/library/src/main/java/com/google/android/exoplayer/util/Util.java index 7e096cfa1b..5081532f7a 100644 --- a/library/src/main/java/com/google/android/exoplayer/util/Util.java +++ b/library/src/main/java/com/google/android/exoplayer/util/Util.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer.util; import com.google.android.exoplayer.upstream.DataSource; -import android.net.Uri; import android.text.TextUtils; import java.io.IOException; @@ -134,54 +133,6 @@ public final class Util { return text == null ? null : text.toLowerCase(Locale.US); } - /** - * Like {@link Uri#parse(String)}, but discards the part of the uri that follows the final - * forward slash. - * - * @param uriString An RFC 2396-compliant, encoded uri. - * @return The parsed base uri. - */ - public static Uri parseBaseUri(String uriString) { - return Uri.parse(uriString.substring(0, uriString.lastIndexOf('/'))); - } - - /** - * Merges a uri and a string to produce a new uri. - *

- * The uri is built according to the following rules: - *

    - *
  • If {@code baseUri} is null or if {@code stringUri} is absolute, then {@code baseUri} is - * ignored and the uri consists solely of {@code stringUri}. - *
  • If {@code stringUri} is null, then the uri consists solely of {@code baseUrl}. - *
  • Otherwise, the uri consists of the concatenation of {@code baseUri} and {@code stringUri}. - *
- * - * @param baseUri A uri that can form the base of the merged uri. - * @param stringUri A relative or absolute uri in string form. - * @return The merged uri. - */ - public static Uri getMergedUri(Uri baseUri, String stringUri) { - if (stringUri == null) { - return baseUri; - } - if (baseUri == null) { - return Uri.parse(stringUri); - } - if (stringUri.startsWith("/")) { - stringUri = stringUri.substring(1); - return new Uri.Builder() - .scheme(baseUri.getScheme()) - .authority(baseUri.getAuthority()) - .appendEncodedPath(stringUri) - .build(); - } - Uri uri = Uri.parse(stringUri); - if (uri.isAbsolute()) { - return uri; - } - return Uri.withAppendedPath(baseUri, stringUri); - } - /** * Returns the index of the largest value in an array that is less than (or optionally equal to) * a specified key. diff --git a/library/src/test/.classpath b/library/src/test/.classpath new file mode 100644 index 0000000000..171a8c3ec8 --- /dev/null +++ b/library/src/test/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/library/src/test/.project b/library/src/test/.project new file mode 100644 index 0000000000..d63886b065 --- /dev/null +++ b/library/src/test/.project @@ -0,0 +1,62 @@ + + + ExoPlayerTests + + + ExoPlayerLib + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + + + libs/dexmaker-1.2.jar + 1 + $%7BPARENT-3-PROJECT_LOC%7D/third_party/dexmaker/dexmaker-1.2.jar + + + libs/dexmaker-mockito-1.2.jar + 1 + $%7BPARENT-3-PROJECT_LOC%7D/third_party/dexmaker/dexmaker-mockito-1.2.jar + + + libs/mockito-all-1.9.5.jar + 1 + $%7BPARENT-3-PROJECT_LOC%7D/third_party/mockito/mockito-all-1.9.5.jar + + + + + 1425657306619 + + 14 + + org.eclipse.ui.ide.multiFilter + 1.0-name-matches-true-false-BUILD + + + + diff --git a/library/src/test/AndroidManifest.xml b/library/src/test/AndroidManifest.xml new file mode 100644 index 0000000000..517161f3b9 --- /dev/null +++ b/library/src/test/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + diff --git a/library/src/test/assets/dash/sample_mpd_1 b/library/src/test/assets/dash/sample_mpd_1 new file mode 100755 index 0000000000..07bcdd4f50 --- /dev/null +++ b/library/src/test/assets/dash/sample_mpd_1 @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + +http://www.test.com/141 + + + + + + + + + + + + + +http://www.test.com/135 + + + + + + + + + + + + + +http://www.test.com/vtt + + + + + + + + + + + diff --git a/library/src/test/assets/webm/vorbis_codec_private b/library/src/test/assets/webm/vorbis_codec_private new file mode 100644 index 0000000000..6a613449a7 Binary files /dev/null and b/library/src/test/assets/webm/vorbis_codec_private differ diff --git a/library/src/test/assets/webvtt/empty b/library/src/test/assets/webvtt/empty new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/library/src/test/assets/webvtt/empty @@ -0,0 +1 @@ + diff --git a/library/src/test/assets/webvtt/typical b/library/src/test/assets/webvtt/typical new file mode 100644 index 0000000000..d1395efe1b --- /dev/null +++ b/library/src/test/assets/webvtt/typical @@ -0,0 +1,8 @@ +WEBVTT +X-TIMESTAMP-MAP=LOCAL:00:00.000,MPEGTS:450000 + +00:00.000 --> 00:01.234 +This is the first subtitle. + +00:02.345 --> 00:03.456 +This is the second subtitle. diff --git a/library/src/test/assets/webvtt/typical_with_identifiers b/library/src/test/assets/webvtt/typical_with_identifiers new file mode 100644 index 0000000000..e2c5df065b --- /dev/null +++ b/library/src/test/assets/webvtt/typical_with_identifiers @@ -0,0 +1,10 @@ +WEBVTT +X-TIMESTAMP-MAP=LOCAL:00:00.000,MPEGTS:450000 + +1 +00:00.000 --> 00:01.234 +This is the first subtitle. + +2 +00:02.345 --> 00:03.456 +This is the second subtitle. diff --git a/library/src/test/assets/webvtt/typical_with_tags b/library/src/test/assets/webvtt/typical_with_tags new file mode 100644 index 0000000000..36e630e240 --- /dev/null +++ b/library/src/test/assets/webvtt/typical_with_tags @@ -0,0 +1,14 @@ +WEBVTT +X-TIMESTAMP-MAP=LOCAL:00:00.000,MPEGTS:450000 + +00:00.000 --> 00:01.234 +This is the first subtitle. + +00:02.345 --> 00:03.456 +This is the second subtitle. + +00:04.000 --> 00:05.000 +This is the third subtitle. + +00:06.000 --> 00:07.000 +This is the <fourth> &subtitle. diff --git a/library/src/test/java/com/google/android/exoplayer/MediaFormatTest.java b/library/src/test/java/com/google/android/exoplayer/MediaFormatTest.java new file mode 100644 index 0000000000..c9d50c43b0 --- /dev/null +++ b/library/src/test/java/com/google/android/exoplayer/MediaFormatTest.java @@ -0,0 +1,60 @@ +/* + * 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 com.google.android.exoplayer.util.Util; + +import android.annotation.TargetApi; + +import junit.framework.TestCase; + +import java.util.ArrayList; +import java.util.List; + +/** + * Unit test for {@link MediaFormat}. + */ +public class MediaFormatTest extends TestCase { + + public void testConversionToFrameworkFormat() { + if (Util.SDK_INT < 16) { + // Test doesn't apply. + return; + } + + byte[] initData1 = new byte[] {1, 2, 3}; + byte[] initData2 = new byte[] {4, 5, 6}; + List initData = new ArrayList(); + initData.add(initData1); + initData.add(initData2); + + testConversionToFrameworkFormatV16( + MediaFormat.createVideoFormat("video/xyz", 102400, 1280, 720, 1.5f, initData)); + testConversionToFrameworkFormatV16( + MediaFormat.createAudioFormat("audio/xyz", 102400, 5, 44100, initData)); + } + + @TargetApi(16) + private void testConversionToFrameworkFormatV16(MediaFormat format) { + // Convert to a framework MediaFormat and back again. + MediaFormat convertedFormat = MediaFormat.createFromFrameworkMediaFormatV16( + format.getFrameworkMediaFormatV16()); + // Assert that we end up with an equivalent object to the one we started with. + assertEquals(format.hashCode(), convertedFormat.hashCode()); + assertEquals(format, convertedFormat); + } + +} diff --git a/library/src/test/java/com/google/android/exoplayer/chunk/parser/webm/DefaultEbmlReaderTest.java b/library/src/test/java/com/google/android/exoplayer/chunk/parser/webm/DefaultEbmlReaderTest.java new file mode 100644 index 0000000000..5d4a30cab9 --- /dev/null +++ b/library/src/test/java/com/google/android/exoplayer/chunk/parser/webm/DefaultEbmlReaderTest.java @@ -0,0 +1,350 @@ +/* + * 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.chunk.parser.webm; + +import com.google.android.exoplayer.ParserException; +import com.google.android.exoplayer.upstream.ByteArrayNonBlockingInputStream; +import com.google.android.exoplayer.upstream.NonBlockingInputStream; + +import junit.framework.TestCase; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Tests {@link DefaultEbmlReader}. + */ +public class DefaultEbmlReaderTest extends TestCase { + + private final EventCapturingEbmlEventHandler eventHandler = + new EventCapturingEbmlEventHandler(); + + public void testNothing() { + NonBlockingInputStream input = createTestInputStream(); + assertNoEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM); + } + + public void testMasterElement() { + NonBlockingInputStream input = + createTestInputStream(0x1A, 0x45, 0xDF, 0xA3, 0x84, 0x42, 0x85, 0x81, 0x01); + EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler(); + expected.onMasterElementStart(EventCapturingEbmlEventHandler.ID_EBML, 0, 5, 4); + expected.onIntegerElement(EventCapturingEbmlEventHandler.ID_DOC_TYPE_READ_VERSION, 1); + expected.onMasterElementEnd(EventCapturingEbmlEventHandler.ID_EBML); + assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events); + } + + public void testMasterElementEmpty() { + NonBlockingInputStream input = createTestInputStream(0x18, 0x53, 0x80, 0x67, 0x80); + EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler(); + expected.onMasterElementStart(EventCapturingEbmlEventHandler.ID_SEGMENT, 0, 5, 0); + expected.onMasterElementEnd(EventCapturingEbmlEventHandler.ID_SEGMENT); + assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events); + } + + public void testUnsignedIntegerElement() { + // 0xFE is chosen because for signed integers it should be interpreted as -2 + NonBlockingInputStream input = createTestInputStream(0x42, 0xF7, 0x81, 0xFE); + EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler(); + expected.onIntegerElement(EventCapturingEbmlEventHandler.ID_EBML_READ_VERSION, 254); + assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events); + } + + public void testUnsignedIntegerElementLarge() { + NonBlockingInputStream input = + createTestInputStream(0x42, 0xF7, 0x88, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF); + EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler(); + expected.onIntegerElement(EventCapturingEbmlEventHandler.ID_EBML_READ_VERSION, Long.MAX_VALUE); + assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events); + } + + public void testUnsignedIntegerElementTooLargeBecomesNegative() { + NonBlockingInputStream input = + createTestInputStream(0x42, 0xF7, 0x88, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF); + EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler(); + expected.onIntegerElement(EventCapturingEbmlEventHandler.ID_EBML_READ_VERSION, -1); + assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events); + } + + public void testStringElement() { + NonBlockingInputStream input = + createTestInputStream(0x42, 0x82, 0x86, 0x41, 0x62, 0x63, 0x31, 0x32, 0x33); + EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler(); + expected.onStringElement(EventCapturingEbmlEventHandler.ID_DOC_TYPE, "Abc123"); + assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events); + } + + public void testStringElementEmpty() { + NonBlockingInputStream input = createTestInputStream(0x42, 0x82, 0x80); + EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler(); + expected.onStringElement(EventCapturingEbmlEventHandler.ID_DOC_TYPE, ""); + assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events); + } + + public void testFloatElementThreeBytes() { + try { + eventHandler.read(createTestInputStream(0x44, 0x89, 0x83, 0x3F, 0x80, 0x00)); + fail(); + } catch (IllegalStateException exception) { + // Expected + } + assertNoEvents(); + } + + public void testFloatElementFourBytes() { + NonBlockingInputStream input = + createTestInputStream(0x44, 0x89, 0x84, 0x3F, 0x80, 0x00, 0x00); + EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler(); + expected.onFloatElement(EventCapturingEbmlEventHandler.ID_DURATION, 1.0); + assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events); + } + + public void testFloatElementEightBytes() { + NonBlockingInputStream input = + createTestInputStream(0x44, 0x89, 0x88, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00); + + EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler(); + expected.onFloatElement(EventCapturingEbmlEventHandler.ID_DURATION, -2.0); + assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events); + } + + public void testBinaryElementReadBytes() { + eventHandler.binaryElementHandler = EventCapturingEbmlEventHandler.HANDLER_READ_BYTES; + NonBlockingInputStream input = + createTestInputStream(0xA3, 0x88, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08); + + EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler(); + expected.binaryElementHandler = EventCapturingEbmlEventHandler.HANDLER_READ_BYTES; + expected.onBinaryElement( + EventCapturingEbmlEventHandler.ID_SIMPLE_BLOCK, 0, 0, 8, + createTestInputStream(0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08)); + assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events); + } + + public void testBinaryElementReadVarint() { + eventHandler.binaryElementHandler = EventCapturingEbmlEventHandler.HANDLER_READ_VARINT; + NonBlockingInputStream input = createTestInputStream(0xA3, 0x82, 0x40, 0x2A); + + EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler(); + expected.binaryElementHandler = EventCapturingEbmlEventHandler.HANDLER_READ_VARINT; + expected.onBinaryElement( + EventCapturingEbmlEventHandler.ID_SIMPLE_BLOCK, 0, 0, 0, + createTestInputStream(0x40, 0x2A)); + assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events); + } + + public void testBinaryElementSkipBytes() { + eventHandler.binaryElementHandler = EventCapturingEbmlEventHandler.HANDLER_SKIP_BYTES; + NonBlockingInputStream input = + createTestInputStream(0xA3, 0x88, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08); + + EventCapturingEbmlEventHandler expected = new EventCapturingEbmlEventHandler(); + expected.binaryElementHandler = EventCapturingEbmlEventHandler.HANDLER_SKIP_BYTES; + expected.onBinaryElement( + EventCapturingEbmlEventHandler.ID_SIMPLE_BLOCK, 0, 0, 8, + createTestInputStream(0, 0, 0, 0, 0, 0, 0, 0)); + assertEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM, expected.events); + } + + public void testBinaryElementDoNothing() { + eventHandler.binaryElementHandler = EventCapturingEbmlEventHandler.HANDLER_DO_NOTHING; + try { + eventHandler.read( + createTestInputStream(0xA3, 0x88, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08)); + fail(); + } catch (IllegalStateException exception) { + // Expected + } + assertNoEvents(); + } + + public void testBinaryElementNotEnoughBytes() { + NonBlockingInputStream input = createTestInputStream(0xA3, 0x88, 0x01, 0x02, 0x03); + assertNoEvents(input, EbmlReader.READ_RESULT_NEED_MORE_DATA); + } + + public void testUnknownElement() { + NonBlockingInputStream input = createTestInputStream(0xEC, 0x81, 0x00); + assertNoEvents(input, EbmlReader.READ_RESULT_END_OF_STREAM); + } + + /** + * Helper to build a {@link ByteArrayNonBlockingInputStream} quickly from zero or more + * integer arguments. + * + *

Each argument must be able to cast to a byte value. + * + * @param data Zero or more integers with values between {@code 0x00} and {@code 0xFF} + * @return A {@link ByteArrayNonBlockingInputStream} containing the given byte values + */ + private NonBlockingInputStream createTestInputStream(int... data) { + byte[] bytes = new byte[data.length]; + for (int i = 0; i < data.length; i++) { + bytes[i] = (byte) data[i]; + } + return new ByteArrayNonBlockingInputStream(bytes); + } + + private void assertReads(NonBlockingInputStream input, int continues, int finalResult) { + for (int i = 0; i < continues; i++) { + assertEquals(EbmlReader.READ_RESULT_CONTINUE, eventHandler.read(input)); + } + assertEquals(finalResult, eventHandler.read(input)); + } + + private void assertNoEvents() { + assertEvents(Collections.emptyList()); + } + + private void assertEvents(List events) { + assertEquals(events.size(), eventHandler.events.size()); + for (int i = 0; i < events.size(); i++) { + assertEquals(events.get(i), eventHandler.events.get(i)); + } + } + + private void assertNoEvents(NonBlockingInputStream input, int finalResult) { + assertReads(input, 0, finalResult); + assertNoEvents(); + } + + private void assertEvents(NonBlockingInputStream input, int finalResult, List events) { + assertReads(input, events.size(), finalResult); + assertEvents(events); + } + + /** + * An {@link EbmlEventHandler} which captures all event callbacks made by + * {@link DefaultEbmlReader} for testing purposes. + */ + private static final class EventCapturingEbmlEventHandler implements EbmlEventHandler { + + // Element IDs + private static final int ID_EBML = 0x1A45DFA3; + private static final int ID_EBML_READ_VERSION = 0x42F7; + private static final int ID_DOC_TYPE = 0x4282; + private static final int ID_DOC_TYPE_READ_VERSION = 0x4285; + + private static final int ID_SEGMENT = 0x18538067; + private static final int ID_DURATION = 0x4489; + private static final int ID_SIMPLE_BLOCK = 0xA3; + + // Various ways to handle things in onBinaryElement() + private static final int HANDLER_DO_NOTHING = 0; + private static final int HANDLER_READ_BYTES = 1; + private static final int HANDLER_READ_VARINT = 2; + private static final int HANDLER_SKIP_BYTES = 3; + + private final EbmlReader reader = new DefaultEbmlReader(); + private final List events = new ArrayList(); + + private int binaryElementHandler; + + private EventCapturingEbmlEventHandler() { + reader.setEventHandler(this); + } + + private int read(NonBlockingInputStream inputStream) { + try { + return reader.read(inputStream); + } catch (ParserException e) { + // should never happen. + fail(); + return -1; + } + } + + @Override + public int getElementType(int id) { + switch (id) { + case ID_EBML: + case ID_SEGMENT: + return EbmlReader.TYPE_MASTER; + case ID_EBML_READ_VERSION: + case ID_DOC_TYPE_READ_VERSION: + return EbmlReader.TYPE_UNSIGNED_INT; + case ID_DOC_TYPE: + return EbmlReader.TYPE_STRING; + case ID_SIMPLE_BLOCK: + return EbmlReader.TYPE_BINARY; + case ID_DURATION: + return EbmlReader.TYPE_FLOAT; + default: + return EbmlReader.TYPE_UNKNOWN; + } + } + + @Override + public void onMasterElementStart( + int id, long elementOffset, int headerSize, long contentsSize) { + events.add(formatEvent(id, "start elementOffset=" + elementOffset + + " headerSize=" + headerSize + " contentsSize=" + contentsSize)); + } + + @Override + public void onMasterElementEnd(int id) { + events.add(formatEvent(id, "end")); + } + + @Override + public void onIntegerElement(int id, long value) { + events.add(formatEvent(id, "integer=" + String.valueOf(value))); + } + + @Override + public void onFloatElement(int id, double value) { + events.add(formatEvent(id, "float=" + String.valueOf(value))); + } + + @Override + public void onStringElement(int id, String value) { + events.add(formatEvent(id, "string=" + value)); + } + + @Override + public boolean onBinaryElement( + int id, long elementOffset, int headerSize, int contentsSize, + NonBlockingInputStream inputStream) { + switch (binaryElementHandler) { + case HANDLER_READ_BYTES: + byte[] bytes = new byte[contentsSize]; + reader.readBytes(inputStream, bytes, contentsSize); + events.add(formatEvent(id, "bytes=" + Arrays.toString(bytes))); + break; + case HANDLER_READ_VARINT: + long value = reader.readVarint(inputStream); + events.add(formatEvent(id, "varint=" + String.valueOf(value))); + break; + case HANDLER_SKIP_BYTES: + reader.skipBytes(inputStream, contentsSize); + events.add(formatEvent(id, "skipped " + contentsSize + " byte(s)")); + break; + case HANDLER_DO_NOTHING: + default: + // pass + } + return true; + } + + private static String formatEvent(int id, String event) { + return "[" + Integer.toHexString(id) + "] " + event; + } + + } + +} diff --git a/library/src/test/java/com/google/android/exoplayer/chunk/parser/webm/WebmExtractorTest.java b/library/src/test/java/com/google/android/exoplayer/chunk/parser/webm/WebmExtractorTest.java new file mode 100644 index 0000000000..757db52ae2 --- /dev/null +++ b/library/src/test/java/com/google/android/exoplayer/chunk/parser/webm/WebmExtractorTest.java @@ -0,0 +1,523 @@ +/* + * 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.chunk.parser.webm; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.ParserException; +import com.google.android.exoplayer.SampleHolder; +import com.google.android.exoplayer.chunk.parser.SegmentIndex; +import com.google.android.exoplayer.upstream.ByteArrayNonBlockingInputStream; +import com.google.android.exoplayer.upstream.NonBlockingInputStream; +import com.google.android.exoplayer.util.MimeTypes; + +import android.test.InstrumentationTestCase; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Arrays; + +public class WebmExtractorTest extends InstrumentationTestCase { + + private static final int INFO_ELEMENT_BYTE_SIZE = 31; + private static final int TRACKS_ELEMENT_BYTE_SIZE = 48; + private static final int CUES_ELEMENT_BYTE_SIZE = 12; + private static final int CUE_POINT_ELEMENT_BYTE_SIZE = 31; + + private static final int DEFAULT_TIMECODE_SCALE = 1000000; + + private static final long TEST_DURATION_US = 9920000L; + private static final int TEST_WIDTH = 1280; + private static final int TEST_HEIGHT = 720; + private static final int TEST_CHANNEL_COUNT = 1; + private static final int TEST_SAMPLE_RATE = 48000; + private static final long TEST_CODEC_DELAY = 6500000; + private static final long TEST_SEEK_PRE_ROLL = 80000000; + private static final int TEST_OPUS_CODEC_PRIVATE_SIZE = 2; + private static final String TEST_VORBIS_CODEC_PRIVATE = "webm/vorbis_codec_private"; + private static final int TEST_VORBIS_INFO_SIZE = 30; + private static final int TEST_VORBIS_BOOKS_SIZE = 4140; + + private static final int ID_VP9 = 0; + private static final int ID_OPUS = 1; + private static final int ID_VORBIS = 2; + + private static final int EXPECTED_INIT_RESULT = WebmExtractor.RESULT_READ_INIT + | WebmExtractor.RESULT_READ_INDEX | WebmExtractor.RESULT_END_OF_STREAM; + private static final int EXPECTED_INIT_AND_SAMPLE_RESULT = WebmExtractor.RESULT_READ_INIT + | WebmExtractor.RESULT_READ_INDEX | WebmExtractor.RESULT_READ_SAMPLE; + + private final WebmExtractor extractor = new WebmExtractor(); + private final SampleHolder sampleHolder = + new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED); + + @Override + public void setUp() { + sampleHolder.data = ByteBuffer.allocate(1024); + } + + public void testPrepare() throws ParserException { + NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream( + createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9)); + assertEquals(EXPECTED_INIT_RESULT, extractor.read(testInputStream, sampleHolder)); + assertFormat(); + assertIndex(new IndexPoint(0, 0, TEST_DURATION_US)); + } + + public void testPrepareOpus() throws ParserException { + NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream( + createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_OPUS)); + assertEquals(EXPECTED_INIT_RESULT, extractor.read(testInputStream, sampleHolder)); + assertAudioFormat(ID_OPUS); + assertIndex(new IndexPoint(0, 0, TEST_DURATION_US)); + } + + public void testPrepareVorbis() throws ParserException { + NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream( + createInitializationSegment(1, 0, true, DEFAULT_TIMECODE_SCALE, ID_VORBIS)); + assertEquals(EXPECTED_INIT_RESULT, extractor.read(testInputStream, sampleHolder)); + assertAudioFormat(ID_VORBIS); + assertIndex(new IndexPoint(0, 0, TEST_DURATION_US)); + } + + public void testPrepareThreeCuePoints() throws ParserException { + NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream( + createInitializationSegment(3, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9)); + assertEquals(EXPECTED_INIT_RESULT, extractor.read(testInputStream, sampleHolder)); + assertFormat(); + assertIndex( + new IndexPoint(0, 0, 10000), + new IndexPoint(10000, 0, 10000), + new IndexPoint(20000, 0, TEST_DURATION_US - 20000)); + } + + public void testPrepareCustomTimecodeScale() throws ParserException { + NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream( + createInitializationSegment(3, 0, true, 1000, ID_VP9)); + assertEquals(EXPECTED_INIT_RESULT, extractor.read(testInputStream, sampleHolder)); + assertFormat(); + assertIndex( + new IndexPoint(0, 0, 10), + new IndexPoint(10, 0, 10), + new IndexPoint(20, 0, (TEST_DURATION_US / 1000) - 20)); + } + + public void testPrepareNoCuePoints() { + NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream( + createInitializationSegment(0, 0, true, DEFAULT_TIMECODE_SCALE, ID_VP9)); + try { + extractor.read(testInputStream, sampleHolder); + fail(); + } catch (ParserException exception) { + assertEquals("Invalid/missing cue points", exception.getMessage()); + } + } + + public void testPrepareInvalidDocType() { + NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream( + createInitializationSegment(1, 0, false, DEFAULT_TIMECODE_SCALE, ID_VP9)); + try { + extractor.read(testInputStream, sampleHolder); + fail(); + } catch (ParserException exception) { + assertEquals("DocType webB not supported", exception.getMessage()); + } + } + + public void testReadSampleKeyframe() throws ParserException { + MediaSegment mediaSegment = createMediaSegment(100, 0, 0, true, false, true); + byte[] testInputData = joinByteArrays( + createInitializationSegment( + 1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, ID_VP9), + mediaSegment.clusterBytes); + NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(testInputData); + assertEquals(EXPECTED_INIT_AND_SAMPLE_RESULT, extractor.read(testInputStream, sampleHolder)); + assertFormat(); + assertSample(mediaSegment, 0, true, false); + assertEquals(WebmExtractor.RESULT_END_OF_STREAM, extractor.read(testInputStream, sampleHolder)); + } + + public void testReadBlock() throws ParserException { + MediaSegment mediaSegment = createMediaSegment(100, 0, 0, true, false, false); + byte[] testInputData = joinByteArrays( + createInitializationSegment( + 1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, ID_OPUS), + mediaSegment.clusterBytes); + NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(testInputData); + assertEquals(EXPECTED_INIT_AND_SAMPLE_RESULT, extractor.read(testInputStream, sampleHolder)); + assertAudioFormat(ID_OPUS); + assertSample(mediaSegment, 0, true, false); + assertEquals(WebmExtractor.RESULT_END_OF_STREAM, extractor.read(testInputStream, sampleHolder)); + } + + public void testReadSampleInvisible() throws ParserException { + MediaSegment mediaSegment = createMediaSegment(100, 12, 13, false, true, true); + byte[] testInputData = joinByteArrays( + createInitializationSegment( + 1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, ID_VP9), + mediaSegment.clusterBytes); + NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(testInputData); + assertEquals(EXPECTED_INIT_AND_SAMPLE_RESULT, extractor.read(testInputStream, sampleHolder)); + assertFormat(); + assertSample(mediaSegment, 25000, false, true); + assertEquals(WebmExtractor.RESULT_END_OF_STREAM, extractor.read(testInputStream, sampleHolder)); + } + + public void testReadSampleCustomTimescale() throws ParserException { + MediaSegment mediaSegment = createMediaSegment(100, 12, 13, false, false, true); + byte[] testInputData = joinByteArrays( + createInitializationSegment( + 1, mediaSegment.clusterBytes.length, true, 1000, ID_VP9), + mediaSegment.clusterBytes); + NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(testInputData); + assertEquals(EXPECTED_INIT_AND_SAMPLE_RESULT, extractor.read(testInputStream, sampleHolder)); + assertFormat(); + assertSample(mediaSegment, 25, false, false); + assertEquals(WebmExtractor.RESULT_END_OF_STREAM, extractor.read(testInputStream, sampleHolder)); + } + + public void testReadSampleNegativeSimpleBlockTimecode() throws ParserException { + MediaSegment mediaSegment = createMediaSegment(100, 13, -12, true, true, true); + byte[] testInputData = joinByteArrays( + createInitializationSegment( + 1, mediaSegment.clusterBytes.length, true, DEFAULT_TIMECODE_SCALE, ID_VP9), + mediaSegment.clusterBytes); + NonBlockingInputStream testInputStream = new ByteArrayNonBlockingInputStream(testInputData); + assertEquals(EXPECTED_INIT_AND_SAMPLE_RESULT, extractor.read(testInputStream, sampleHolder)); + assertFormat(); + assertSample(mediaSegment, 1000, true, true); + assertEquals(WebmExtractor.RESULT_END_OF_STREAM, extractor.read(testInputStream, sampleHolder)); + } + + private void assertFormat() { + MediaFormat format = extractor.getFormat(); + assertEquals(TEST_WIDTH, format.width); + assertEquals(TEST_HEIGHT, format.height); + assertEquals(MimeTypes.VIDEO_VP9, format.mimeType); + } + + private void assertAudioFormat(int codecId) { + MediaFormat format = extractor.getFormat(); + assertEquals(TEST_CHANNEL_COUNT, format.channelCount); + assertEquals(TEST_SAMPLE_RATE, format.sampleRate); + if (codecId == ID_OPUS) { + assertEquals(MimeTypes.AUDIO_OPUS, format.mimeType); + assertEquals(3, format.initializationData.size()); + assertEquals(TEST_OPUS_CODEC_PRIVATE_SIZE, format.initializationData.get(0).length); + assertEquals(TEST_CODEC_DELAY, ByteBuffer.wrap(format.initializationData.get(1)).getLong()); + assertEquals(TEST_SEEK_PRE_ROLL, ByteBuffer.wrap(format.initializationData.get(2)).getLong()); + } else if (codecId == ID_VORBIS) { + assertEquals(MimeTypes.AUDIO_VORBIS, format.mimeType); + assertEquals(2, format.initializationData.size()); + assertEquals(TEST_VORBIS_INFO_SIZE, format.initializationData.get(0).length); + assertEquals(TEST_VORBIS_BOOKS_SIZE, format.initializationData.get(1).length); + } + } + + private void assertIndex(IndexPoint... indexPoints) { + SegmentIndex index = extractor.getIndex(); + assertEquals(CUES_ELEMENT_BYTE_SIZE + CUE_POINT_ELEMENT_BYTE_SIZE * indexPoints.length, + index.sizeBytes); + assertEquals(indexPoints.length, index.length); + for (int i = 0; i < indexPoints.length; i++) { + IndexPoint indexPoint = indexPoints[i]; + assertEquals(indexPoint.timeUs, index.timesUs[i]); + assertEquals(indexPoint.size, index.sizes[i]); + assertEquals(indexPoint.durationUs, index.durationsUs[i]); + } + } + + private void assertSample( + MediaSegment mediaSegment, int timeUs, boolean keyframe, boolean invisible) { + assertTrue(Arrays.equals( + mediaSegment.videoBytes, Arrays.copyOf(sampleHolder.data.array(), sampleHolder.size))); + assertEquals(timeUs, sampleHolder.timeUs); + assertEquals(keyframe, (sampleHolder.flags & C.SAMPLE_FLAG_SYNC) != 0); + assertEquals(invisible, sampleHolder.decodeOnly); + } + + private byte[] createInitializationSegment( + int cuePoints, int mediaSegmentSize, boolean docTypeIsWebm, int timecodeScale, + int codecId) { + int initalizationSegmentSize = INFO_ELEMENT_BYTE_SIZE + TRACKS_ELEMENT_BYTE_SIZE + + CUES_ELEMENT_BYTE_SIZE + CUE_POINT_ELEMENT_BYTE_SIZE * cuePoints; + byte[] tracksElement = null; + switch (codecId) { + case ID_VP9: + tracksElement = createTracksElementWithVideo(true, TEST_WIDTH, TEST_HEIGHT); + break; + case ID_OPUS: + tracksElement = createTracksElementWithOpusAudio(TEST_CHANNEL_COUNT); + break; + case ID_VORBIS: + tracksElement = createTracksElementWithVorbisAudio(TEST_CHANNEL_COUNT); + break; + } + byte[] bytes = joinByteArrays(createEbmlElement(1, docTypeIsWebm, 2), + createSegmentElement(initalizationSegmentSize + mediaSegmentSize), + createInfoElement(timecodeScale), + tracksElement, + createCuesElement(CUE_POINT_ELEMENT_BYTE_SIZE * cuePoints)); + for (int i = 0; i < cuePoints; i++) { + bytes = joinByteArrays(bytes, createCuePointElement(10 * i, initalizationSegmentSize)); + } + return bytes; + } + + private static MediaSegment createMediaSegment(int videoBytesLength, int clusterTimecode, + int blockTimecode, boolean keyframe, boolean invisible, boolean isSimple) { + byte[] videoBytes = createVideoBytes(videoBytesLength); + byte[] blockBytes; + if (isSimple) { + blockBytes = createSimpleBlockElement(videoBytes.length, blockTimecode, + keyframe, invisible, true); + } else { + blockBytes = createBlockElement(videoBytes.length, blockTimecode, invisible, true); + } + byte[] clusterBytes = + createClusterElement(blockBytes.length + videoBytes.length, clusterTimecode); + return new MediaSegment(joinByteArrays(clusterBytes, blockBytes, videoBytes), videoBytes); + } + + private static byte[] joinByteArrays(byte[]... byteArrays) { + int length = 0; + for (byte[] byteArray : byteArrays) { + length += byteArray.length; + } + byte[] joined = new byte[length]; + length = 0; + for (byte[] byteArray : byteArrays) { + System.arraycopy(byteArray, 0, joined, length, byteArray.length); + length += byteArray.length; + } + return joined; + } + + private static byte[] createEbmlElement( + int ebmlReadVersion, boolean docTypeIsWebm, int docTypeReadVersion) { + return createByteArray( + 0x1A, 0x45, 0xDF, 0xA3, // EBML + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, // size=15 + 0x42, 0xF7, // EBMLReadVersion + 0x81, ebmlReadVersion, // size=1 + 0x42, 0x82, // DocType + 0x84, 0x77, 0x65, 0x62, docTypeIsWebm ? 0x6D : 0x42, // size=4 value=webm/B + 0x42, 0x85, // DocTypeReadVersion + 0x81, docTypeReadVersion); // size=1 + } + + private static byte[] createSegmentElement(int size) { + byte[] sizeBytes = getIntegerBytes(size); + return createByteArray( + 0x18, 0x53, 0x80, 0x67, // Segment + 0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3]); + } + + private static byte[] createInfoElement(int timecodeScale) { + byte[] scaleBytes = getIntegerBytes(timecodeScale); + return createByteArray( + 0x15, 0x49, 0xA9, 0x66, // Info + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, // size=19 + 0x2A, 0xD7, 0xB1, // TimecodeScale + 0x84, scaleBytes[0], scaleBytes[1], scaleBytes[2], scaleBytes[3], // size=4 + 0x44, 0x89, // Duration + 0x88, 0x40, 0xC3, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00); // size=8 value=9920.0 + } + + private static byte[] createTracksElementWithVideo( + boolean codecIsVp9, int pixelWidth, int pixelHeight) { + byte[] widthBytes = getIntegerBytes(pixelWidth); + byte[] heightBytes = getIntegerBytes(pixelHeight); + return createByteArray( + 0x16, 0x54, 0xAE, 0x6B, // Tracks + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, // size=36 + 0xAE, // TrackEntry + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1B, // size=27 + 0x86, // CodecID + 0x85, 0x56, 0x5F, 0x56, 0x50, codecIsVp9 ? 0x39 : 0x30, // size=5 value=V_VP9/0 + 0xE0, // Video + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // size=8 + 0xB0, // PixelWidth + 0x82, widthBytes[2], widthBytes[3], // size=2 + 0xBA, // PixelHeight + 0x82, heightBytes[2], heightBytes[3]); // size=2 + } + + private static byte[] createTracksElementWithOpusAudio(int channelCount) { + byte[] channelCountBytes = getIntegerBytes(channelCount); + return createByteArray( + 0x16, 0x54, 0xAE, 0x6B, // Tracks + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x39, // size=57 + 0xAE, // TrackEntry + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, // size=48 + 0x86, // CodecID + 0x86, 0x41, 0x5F, 0x4F, 0x50, 0x55, 0x53, // size=6 value=A_OPUS + 0x56, 0xAA, // CodecDelay + 0x83, 0x63, 0x2E, 0xA0, // size=3 value=6500000 + 0x56, 0xBB, // SeekPreRoll + 0x84, 0x04, 0xC4, 0xB4, 0x00, // size=4 value=80000000 + 0xE1, // Audio + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, // size=13 + 0x9F, // Channels + 0x81, channelCountBytes[3], // size=1 + 0xB5, // SamplingFrequency + 0x88, 0x40, 0xE7, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, // size=8 value=48000 + 0x63, 0xA2, // CodecPrivate + 0x82, 0x00, 0x00); // size=2 + } + + private byte[] createTracksElementWithVorbisAudio(int channelCount) { + byte[] channelCountBytes = getIntegerBytes(channelCount); + byte[] tracksElement = createByteArray( + 0x16, 0x54, 0xAE, 0x6B, // Tracks + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x9C, // size=4252 + 0xAE, // TrackEntry + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x93, // size=4243 (36+4207) + 0x86, // CodecID + 0x88, 0x41, 0x5f, 0x56, 0x4f, 0x52, 0x42, 0x49, 0x53, // size=8 value=A_VORBIS + 0xE1, // Audio + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, // size=13 + 0x9F, // Channels + 0x81, channelCountBytes[3], // size=1 + 0xB5, // SamplingFrequency + 0x88, 0x40, 0xE7, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, // size=8 value=48000 + 0x63, 0xA2, // CodecPrivate + 0x50, 0x6F); // size=4207 + byte[] codecPrivate = new byte[4207]; + try { + getInstrumentation().getContext().getResources().getAssets().open(TEST_VORBIS_CODEC_PRIVATE) + .read(codecPrivate); + } catch (IOException e) { + fail(); // should never happen + } + return joinByteArrays(tracksElement, codecPrivate); + } + + private static byte[] createCuesElement(int size) { + byte[] sizeBytes = getIntegerBytes(size); + return createByteArray( + 0x1C, 0x53, 0xBB, 0x6B, // Cues + 0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3]); // size=31 + } + + private static byte[] createCuePointElement(int cueTime, int cueClusterPosition) { + byte[] positionBytes = getIntegerBytes(cueClusterPosition); + return createByteArray( + 0xBB, // CuePoint + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, // size=22 + 0xB3, // CueTime + 0x81, cueTime, // size=1 + 0xB7, // CueTrackPositions + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, // size=10 + 0xF1, // CueClusterPosition + 0x88, 0x00, 0x00, 0x00, 0x00, positionBytes[0], positionBytes[1], + positionBytes[2], positionBytes[3]); // size=8 + } + + private static byte[] createClusterElement(int size, int timecode) { + byte[] sizeBytes = getIntegerBytes(size); + byte[] timeBytes = getIntegerBytes(timecode); + return createByteArray( + 0x1F, 0x43, 0xB6, 0x75, // Cluster + 0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3], + 0xE7, // Timecode + 0x84, timeBytes[0], timeBytes[1], timeBytes[2], timeBytes[3]); // size=4 + } + + private static byte[] createSimpleBlockElement( + int size, int timecode, boolean keyframe, boolean invisible, boolean noLacing) { + byte[] sizeBytes = getIntegerBytes(size + 4); + byte[] timeBytes = getIntegerBytes(timecode); + byte flags = (byte) + ((keyframe ? 0x80 : 0x00) | (invisible ? 0x08 : 0x00) | (noLacing ? 0x00 : 0x06)); + return createByteArray( + 0xA3, // SimpleBlock + 0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3], + 0x81, // Track number value=1 + timeBytes[2], timeBytes[3], flags); // Timecode and flags + } + + private static byte[] createBlockElement( + int size, int timecode, boolean invisible, boolean noLacing) { + int blockSize = size + 4; + byte[] blockSizeBytes = getIntegerBytes(blockSize); + byte[] timeBytes = getIntegerBytes(timecode); + int blockElementSize = 1 + 8 + blockSize; // id + size + length of data + byte[] sizeBytes = getIntegerBytes(blockElementSize); + byte flags = (byte) ((invisible ? 0x08 : 0x00) | (noLacing ? 0x00 : 0x06)); + return createByteArray( + 0xA0, // BlockGroup + 0x01, 0x00, 0x00, 0x00, sizeBytes[0], sizeBytes[1], sizeBytes[2], sizeBytes[3], + 0xA1, // Block + 0x01, 0x00, 0x00, 0x00, + blockSizeBytes[0], blockSizeBytes[1], blockSizeBytes[2], blockSizeBytes[3], + 0x81, // Track number value=1 + timeBytes[2], timeBytes[3], flags); // Timecode and flags + } + + private static byte[] createVideoBytes(int size) { + byte[] videoBytes = new byte[size]; + for (int i = 0; i < size; i++) { + videoBytes[i] = (byte) i; + } + return videoBytes; + } + + private static byte[] getIntegerBytes(int value) { + return createByteArray( + (value & 0xFF000000) >> 24, + (value & 0x00FF0000) >> 16, + (value & 0x0000FF00) >> 8, + (value & 0x000000FF)); + } + + private static byte[] createByteArray(int... intArray) { + byte[] byteArray = new byte[intArray.length]; + for (int i = 0; i < byteArray.length; i++) { + byteArray[i] = (byte) intArray[i]; + } + return byteArray; + } + + /** Used by {@link #createMediaSegment} to return both cluster and video bytes together. */ + private static final class MediaSegment { + + private final byte[] clusterBytes; + private final byte[] videoBytes; + + private MediaSegment(byte[] clusterBytes, byte[] videoBytes) { + this.clusterBytes = clusterBytes; + this.videoBytes = videoBytes; + } + + } + + /** Used by {@link #assertIndex(IndexPoint...)} to validate index elements. */ + private static final class IndexPoint { + + private final long timeUs; + private final int size; + private final long durationUs; + + private IndexPoint(long timeUs, int size, long durationUs) { + this.timeUs = timeUs; + this.size = size; + this.durationUs = durationUs; + } + + } + +} diff --git a/library/src/test/java/com/google/android/exoplayer/dash/DashChunkSourceTest.java b/library/src/test/java/com/google/android/exoplayer/dash/DashChunkSourceTest.java new file mode 100644 index 0000000000..d4e116e833 --- /dev/null +++ b/library/src/test/java/com/google/android/exoplayer/dash/DashChunkSourceTest.java @@ -0,0 +1,49 @@ +/* + * 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.dash; + +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.chunk.Format; +import com.google.android.exoplayer.dash.mpd.Representation; +import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase; + +import junit.framework.TestCase; + +/** + * Tests {@link DashChunkSource}. + */ +public class DashChunkSourceTest extends TestCase { + + public void testMaxVideoDimensions() { + SingleSegmentBase segmentBase1 = new SingleSegmentBase("https://example.com/1.mp4"); + Format format1 = new Format("1", "video/mp4", 100, 200, -1, -1, 1000); + Representation representation1 = + Representation.newInstance(0, 0, null, 0, format1, segmentBase1); + + SingleSegmentBase segmentBase2 = new SingleSegmentBase("https://example.com/2.mp4"); + Format format2 = new Format("2", "video/mp4", 400, 50, -1, -1, 1000); + Representation representation2 = + Representation.newInstance(0, 0, null, 0, format2, segmentBase2); + + DashChunkSource chunkSource = new DashChunkSource(null, null, representation1, representation2); + MediaFormat out = MediaFormat.createVideoFormat("video/h264", 1, 1, 1, 1, null); + chunkSource.getMaxVideoDimensions(out); + + assertEquals(400, out.getMaxVideoWidth()); + assertEquals(200, out.getMaxVideoHeight()); + } + +} diff --git a/library/src/test/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParserTest.java b/library/src/test/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParserTest.java new file mode 100644 index 0000000000..7aa65564a0 --- /dev/null +++ b/library/src/test/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParserTest.java @@ -0,0 +1,38 @@ +/* + * 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.dash.mpd; + +import android.test.InstrumentationTestCase; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Unit tests for {@link MediaPresentationDescriptionParser}. + */ +public class MediaPresentationDescriptionParserTest extends InstrumentationTestCase { + + private static final String SAMPLE_MPD_1 = "dash/sample_mpd_1"; + + public void testParseMediaPresentationDescription() throws IOException { + MediaPresentationDescriptionParser parser = new MediaPresentationDescriptionParser(); + InputStream inputStream = + getInstrumentation().getContext().getResources().getAssets().open(SAMPLE_MPD_1); + // Simple test to ensure that the sample manifest parses without throwing any exceptions. + parser.parse("https://example.com/test.mpd", inputStream); + } + +} diff --git a/library/src/test/java/com/google/android/exoplayer/dash/mpd/RangedUriTest.java b/library/src/test/java/com/google/android/exoplayer/dash/mpd/RangedUriTest.java new file mode 100644 index 0000000000..52d5c1dd07 --- /dev/null +++ b/library/src/test/java/com/google/android/exoplayer/dash/mpd/RangedUriTest.java @@ -0,0 +1,78 @@ +/* + * 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.dash.mpd; + +import junit.framework.TestCase; + +/** + * Unit test for {@link RangedUri}. + */ +public class RangedUriTest extends TestCase { + + private static final String FULL_URI = "http://www.test.com/path/file.ext"; + + public void testMerge() { + RangedUri rangeA = new RangedUri(null, FULL_URI, 0, 10); + RangedUri rangeB = new RangedUri(null, FULL_URI, 10, 10); + RangedUri expected = new RangedUri(null, FULL_URI, 0, 20); + assertMerge(rangeA, rangeB, expected); + } + + public void testMergeUnbounded() { + RangedUri rangeA = new RangedUri(null, FULL_URI, 0, 10); + RangedUri rangeB = new RangedUri(null, FULL_URI, 10, -1); + RangedUri expected = new RangedUri(null, FULL_URI, 0, -1); + assertMerge(rangeA, rangeB, expected); + } + + public void testNonMerge() { + // A and B do not overlap, so should not merge + RangedUri rangeA = new RangedUri(null, FULL_URI, 0, 10); + RangedUri rangeB = new RangedUri(null, FULL_URI, 11, 10); + assertNonMerge(rangeA, rangeB); + + // A and B do not overlap, so should not merge + rangeA = new RangedUri(null, FULL_URI, 0, 10); + rangeB = new RangedUri(null, FULL_URI, 11, -1); + assertNonMerge(rangeA, rangeB); + + // A and B are bounded but overlap, so should not merge + rangeA = new RangedUri(null, FULL_URI, 0, 11); + rangeB = new RangedUri(null, FULL_URI, 10, 10); + assertNonMerge(rangeA, rangeB); + + // A and B overlap due to unboundedness, so should not merge + rangeA = new RangedUri(null, FULL_URI, 0, -1); + rangeB = new RangedUri(null, FULL_URI, 10, -1); + assertNonMerge(rangeA, rangeB); + + } + + private void assertMerge(RangedUri rangeA, RangedUri rangeB, RangedUri expected) { + RangedUri merged = rangeA.attemptMerge(rangeB); + assertEquals(expected, merged); + merged = rangeB.attemptMerge(rangeA); + assertEquals(expected, merged); + } + + private void assertNonMerge(RangedUri rangeA, RangedUri rangeB) { + RangedUri merged = rangeA.attemptMerge(rangeB); + assertNull(merged); + merged = rangeB.attemptMerge(rangeA); + assertNull(merged); + } + +} diff --git a/library/src/test/java/com/google/android/exoplayer/dash/mpd/RepresentationTest.java b/library/src/test/java/com/google/android/exoplayer/dash/mpd/RepresentationTest.java new file mode 100644 index 0000000000..19d2226014 --- /dev/null +++ b/library/src/test/java/com/google/android/exoplayer/dash/mpd/RepresentationTest.java @@ -0,0 +1,42 @@ +/* + * 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.dash.mpd; + +import com.google.android.exoplayer.chunk.Format; +import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase; +import com.google.android.exoplayer.util.MimeTypes; + +import junit.framework.TestCase; + +/** + * Unit test for {@link Representation}. + */ +public class RepresentationTest extends TestCase { + + public void testGetCacheKey() { + String uri = "http://www.google.com"; + SegmentBase base = new SingleSegmentBase(new RangedUri(uri, null, 0, 1), 1, 0, uri, 1, 1); + Format format = new Format("0", MimeTypes.VIDEO_MP4, 1920, 1080, 0, 0, 2500000); + Representation representation = Representation.newInstance(-1, -1, "test_stream_1", 3, + format, base); + assertEquals("test_stream_1.0.3", representation.getCacheKey()); + + format = new Format("150", MimeTypes.VIDEO_MP4, 1920, 1080, 0, 0, 2500000); + representation = Representation.newInstance(-1, -1, "test_stream_1", -1, format, base); + assertEquals("test_stream_1.150.-1", representation.getCacheKey()); + } + +} diff --git a/library/src/test/java/com/google/android/exoplayer/dash/mpd/UrlTemplateTest.java b/library/src/test/java/com/google/android/exoplayer/dash/mpd/UrlTemplateTest.java new file mode 100644 index 0000000000..55084cc4c9 --- /dev/null +++ b/library/src/test/java/com/google/android/exoplayer/dash/mpd/UrlTemplateTest.java @@ -0,0 +1,66 @@ +/* + * 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.dash.mpd; + +import junit.framework.TestCase; + +/** + * Unit test for {@link UrlTemplate}. + */ +public class UrlTemplateTest extends TestCase { + + public void testRealExamples() { + String template = "QualityLevels($Bandwidth$)/Fragments(video=$Time$,format=mpd-time-csf)"; + UrlTemplate urlTemplate = UrlTemplate.compile(template); + String url = urlTemplate.buildUri("abc1", 10, 650000, 5000); + assertEquals("QualityLevels(650000)/Fragments(video=5000,format=mpd-time-csf)", url); + + template = "$RepresentationID$/$Number$"; + urlTemplate = UrlTemplate.compile(template); + url = urlTemplate.buildUri("abc1", 10, 650000, 5000); + assertEquals("abc1/10", url); + + template = "chunk_ctvideo_cfm4s_rid$RepresentationID$_cn$Number$_w2073857842_mpd.m4s"; + urlTemplate = UrlTemplate.compile(template); + url = urlTemplate.buildUri("abc1", 10, 650000, 5000); + assertEquals("chunk_ctvideo_cfm4s_ridabc1_cn10_w2073857842_mpd.m4s", url); + } + + public void testFull() { + String template = "$Bandwidth$_a_$RepresentationID$_b_$Time$_c_$Number$"; + UrlTemplate urlTemplate = UrlTemplate.compile(template); + String url = urlTemplate.buildUri("abc1", 10, 650000, 5000); + assertEquals("650000_a_abc1_b_5000_c_10", url); + } + + public void testFullWithDollarEscaping() { + String template = "$$$Bandwidth$$$_a$$_$RepresentationID$_b_$Time$_c_$Number$$$"; + UrlTemplate urlTemplate = UrlTemplate.compile(template); + String url = urlTemplate.buildUri("abc1", 10, 650000, 5000); + assertEquals("$650000$_a$_abc1_b_5000_c_10$", url); + } + + public void testInvalidSubstitution() { + String template = "$IllegalId$"; + try { + UrlTemplate.compile(template); + assertTrue(false); + } catch (IllegalArgumentException e) { + // Expected. + } + } + +} diff --git a/library/src/test/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParserTest.java b/library/src/test/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParserTest.java new file mode 100644 index 0000000000..bd5126160a --- /dev/null +++ b/library/src/test/java/com/google/android/exoplayer/hls/HlsMasterPlaylistParserTest.java @@ -0,0 +1,108 @@ +/* + * 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.hls; + +import com.google.android.exoplayer.C; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.List; + +/** + * Test for {@link HlsMasterPlaylistParserTest} + */ +public class HlsMasterPlaylistParserTest extends TestCase { + + public void testParseMasterPlaylist() { + String playlistUrl = "https://example.com/test.m3u8"; + String playlistString = "#EXTM3U\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + + "http://example.com/low.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n" + + "http://example.com/spaces_in_codecs.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=384x160\n" + + "http://example.com/mid.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=7680000\n" + + "http://example.com/hi.m3u8\n" + + "\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n" + + "http://example.com/audio-only.m3u8"; + ByteArrayInputStream inputStream = new ByteArrayInputStream( + playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + try { + HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUrl, inputStream); + assertNotNull(playlist); + assertEquals(HlsPlaylist.TYPE_MASTER, playlist.type); + + HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; + + List variants = masterPlaylist.variants; + assertNotNull(variants); + assertEquals(5, variants.size()); + + assertEquals(0, variants.get(0).index); + assertEquals(1280000, variants.get(0).bandwidth); + assertNotNull(variants.get(0).codecs); + assertEquals(2, variants.get(0).codecs.length); + assertEquals("mp4a.40.2", variants.get(0).codecs[0]); + assertEquals("avc1.66.30", variants.get(0).codecs[1]); + assertEquals(304, variants.get(0).width); + assertEquals(128, variants.get(0).height); + assertEquals("http://example.com/low.m3u8", variants.get(0).url); + + assertEquals(1, variants.get(1).index); + assertEquals(1280000, variants.get(1).bandwidth); + assertNotNull(variants.get(1).codecs); + assertEquals(2, variants.get(1).codecs.length); + assertEquals("mp4a.40.2", variants.get(1).codecs[0]); + assertEquals("avc1.66.30", variants.get(1).codecs[1]); + assertEquals("http://example.com/spaces_in_codecs.m3u8", variants.get(1).url); + + assertEquals(2, variants.get(2).index); + assertEquals(2560000, variants.get(2).bandwidth); + assertEquals(null, variants.get(2).codecs); + assertEquals(384, variants.get(2).width); + assertEquals(160, variants.get(2).height); + assertEquals("http://example.com/mid.m3u8", variants.get(2).url); + + assertEquals(3, variants.get(3).index); + assertEquals(7680000, variants.get(3).bandwidth); + assertEquals(null, variants.get(3).codecs); + assertEquals(-1, variants.get(3).width); + assertEquals(-1, variants.get(3).height); + assertEquals("http://example.com/hi.m3u8", variants.get(3).url); + + assertEquals(4, variants.get(4).index); + assertEquals(65000, variants.get(4).bandwidth); + assertNotNull(variants.get(4).codecs); + assertEquals(1, variants.get(4).codecs.length); + assertEquals("mp4a.40.5", variants.get(4).codecs[0]); + assertEquals(-1, variants.get(4).width); + assertEquals(-1, variants.get(4).height); + assertEquals("http://example.com/audio-only.m3u8", variants.get(4).url); + } catch (IOException exception) { + fail(exception.getMessage()); + } + } + +} diff --git a/library/src/test/java/com/google/android/exoplayer/hls/HlsMediaPlaylistParserTest.java b/library/src/test/java/com/google/android/exoplayer/hls/HlsMediaPlaylistParserTest.java new file mode 100644 index 0000000000..7d63db1efe --- /dev/null +++ b/library/src/test/java/com/google/android/exoplayer/hls/HlsMediaPlaylistParserTest.java @@ -0,0 +1,135 @@ +/* + * 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.hls; + +import com.google.android.exoplayer.C; + +import junit.framework.TestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Locale; + +/** + * Test for {@link HlsMediaPlaylistParserTest} + */ +public class HlsMediaPlaylistParserTest extends TestCase { + + public void testParseMediaPlaylist() { + String playlistUrl = "https://example.com/test.m3u8"; + String playlistString = "#EXTM3U\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-TARGETDURATION:8\n" + + "#EXT-X-MEDIA-SEQUENCE:2679\n" + + "#EXT-X-ALLOW-CACHE:YES\n" + + "\n" + + "#EXTINF:7.975,\n" + + "#EXT-X-BYTERANGE:51370@0\n" + + "https://priv.example.com/fileSequence2679.ts\n" + + "\n" + + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2680\",IV=0x1566B\n" + + "#EXTINF:7.975,\n" + + "#EXT-X-BYTERANGE:51501@51370\n" + + "https://priv.example.com/fileSequence2680.ts\n" + + "\n" + + "#EXT-X-KEY:METHOD=NONE\n" + + "#EXTINF:7.941,\n" + + "#EXT-X-BYTERANGE:51501\n" // @102871 + + "https://priv.example.com/fileSequence2681.ts\n" + + "\n" + + "#EXT-X-DISCONTINUITY\n" + + "#EXT-X-KEY:METHOD=AES-128,URI=\"https://priv.example.com/key.php?r=2682\"\n" + + "#EXTINF:7.975,\n" + + "#EXT-X-BYTERANGE:51740\n" // @154372 + + "https://priv.example.com/fileSequence2682.ts\n" + + "\n" + + "#EXTINF:7.975,\n" + + "https://priv.example.com/fileSequence2683.ts\n" + + "#EXT-X-ENDLIST"; + InputStream inputStream = new ByteArrayInputStream( + playlistString.getBytes(Charset.forName(C.UTF8_NAME))); + try { + HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUrl, inputStream); + assertNotNull(playlist); + assertEquals(HlsPlaylist.TYPE_MEDIA, playlist.type); + + HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist; + + assertEquals(2679, mediaPlaylist.mediaSequence); + assertEquals(8, mediaPlaylist.targetDurationSecs); + assertEquals(3, mediaPlaylist.version); + assertEquals(false, mediaPlaylist.live); + List segments = mediaPlaylist.segments; + assertNotNull(segments); + assertEquals(5, segments.size()); + + assertEquals(false, segments.get(0).discontinuity); + assertEquals(7.975, segments.get(0).durationSecs); + assertEquals(null, segments.get(0).encryptionMethod); + assertEquals(null, segments.get(0).encryptionKeyUri); + assertEquals(null, segments.get(0).encryptionIV); + assertEquals(51370, segments.get(0).byterangeLength); + assertEquals(0, segments.get(0).byterangeOffset); + assertEquals("https://priv.example.com/fileSequence2679.ts", segments.get(0).url); + + assertEquals(false, segments.get(1).discontinuity); + assertEquals(7.975, segments.get(1).durationSecs); + assertEquals("AES-128", segments.get(1).encryptionMethod); + assertEquals("https://priv.example.com/key.php?r=2680", segments.get(1).encryptionKeyUri); + assertEquals("0x1566B", segments.get(1).encryptionIV); + assertEquals(51501, segments.get(1).byterangeLength); + assertEquals(51370, segments.get(1).byterangeOffset); + assertEquals("https://priv.example.com/fileSequence2680.ts", segments.get(1).url); + + assertEquals(false, segments.get(2).discontinuity); + assertEquals(7.941, segments.get(2).durationSecs); + assertEquals(HlsMediaPlaylist.ENCRYPTION_METHOD_NONE, segments.get(2).encryptionMethod); + assertEquals(null, segments.get(2).encryptionKeyUri); + assertEquals(null, segments.get(2).encryptionIV); + assertEquals(51501, segments.get(2).byterangeLength); + assertEquals(102871, segments.get(2).byterangeOffset); + assertEquals("https://priv.example.com/fileSequence2681.ts", segments.get(2).url); + + assertEquals(true, segments.get(3).discontinuity); + assertEquals(7.975, segments.get(3).durationSecs); + assertEquals("AES-128", segments.get(3).encryptionMethod); + assertEquals("https://priv.example.com/key.php?r=2682", segments.get(3).encryptionKeyUri); + // 0xA7A == 2682. + assertNotNull(segments.get(3).encryptionIV); + assertEquals("A7A", segments.get(3).encryptionIV.toUpperCase(Locale.getDefault())); + assertEquals(51740, segments.get(3).byterangeLength); + assertEquals(154372, segments.get(3).byterangeOffset); + assertEquals("https://priv.example.com/fileSequence2682.ts", segments.get(3).url); + + assertEquals(false, segments.get(4).discontinuity); + assertEquals(7.975, segments.get(4).durationSecs); + assertEquals("AES-128", segments.get(4).encryptionMethod); + assertEquals("https://priv.example.com/key.php?r=2682", segments.get(4).encryptionKeyUri); + // 0xA7A == 2682. + assertNotNull(segments.get(4).encryptionIV); + assertEquals("A7A", segments.get(4).encryptionIV.toUpperCase(Locale.getDefault())); + assertEquals(C.LENGTH_UNBOUNDED, segments.get(4).byterangeLength); + assertEquals(0, segments.get(4).byterangeOffset); + assertEquals("https://priv.example.com/fileSequence2683.ts", segments.get(4).url); + } catch (IOException exception) { + fail(exception.getMessage()); + } + } + +} diff --git a/library/src/test/java/com/google/android/exoplayer/metadata/Id3ParserTest.java b/library/src/test/java/com/google/android/exoplayer/metadata/Id3ParserTest.java new file mode 100644 index 0000000000..1dead0a139 --- /dev/null +++ b/library/src/test/java/com/google/android/exoplayer/metadata/Id3ParserTest.java @@ -0,0 +1,46 @@ +/* + * 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.metadata; + +import junit.framework.TestCase; + +import java.util.Map; + +/** + * Test for {@link Id3Parser} + */ +public class Id3ParserTest extends TestCase { + + public void testParseTxxxFrames() { + byte[] rawId3 = new byte[] { 73, 68, 51, 4, 0, 0, 0, 0, 0, 41, 84, 88, 88, 88, 0, 0, 0, 31, + 0, 0, 3, 0, 109, 100, 105, 97, 108, 111, 103, 95, 86, 73, 78, 68, 73, 67, 79, 49, 53, 50, + 55, 54, 54, 52, 95, 115, 116, 97, 114, 116, 0 }; + + Id3Parser parser = new Id3Parser(); + try { + Map metadata = parser.parse(rawId3, rawId3.length); + assertNotNull(metadata); + assertEquals(1, metadata.size()); + TxxxMetadata txxx = (TxxxMetadata) metadata.get(TxxxMetadata.TYPE); + assertNotNull(txxx); + assertEquals("", txxx.description); + assertEquals("mdialog_VINDICO1527664_start", txxx.value); + } catch (Exception exception) { + fail(exception.getMessage()); + } + } + +} diff --git a/library/src/test/java/com/google/android/exoplayer/mp4/Mp4UtilTest.java b/library/src/test/java/com/google/android/exoplayer/mp4/Mp4UtilTest.java new file mode 100644 index 0000000000..fe304b14b6 --- /dev/null +++ b/library/src/test/java/com/google/android/exoplayer/mp4/Mp4UtilTest.java @@ -0,0 +1,133 @@ +/* + * 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.mp4; + +import junit.framework.TestCase; + +import java.util.Arrays; + +/** + * Tests for {@link Mp4Util}. + */ +public class Mp4UtilTest extends TestCase { + + private static final int TEST_PARTIAL_NAL_POSITION = 4; + private static final int TEST_NAL_POSITION = 10; + + public void testFindNalUnit() { + byte[] data = buildTestData(); + + // Should find NAL unit. + int result = Mp4Util.findNalUnit(data, 0, data.length); + assertEquals(TEST_NAL_POSITION, result); + // Should find NAL unit whose prefix ends one byte before the limit. + result = Mp4Util.findNalUnit(data, 0, TEST_NAL_POSITION + 4); + assertEquals(TEST_NAL_POSITION, result); + // Shouldn't find NAL unit whose prefix ends at the limit (since the limit is exclusive). + result = Mp4Util.findNalUnit(data, 0, TEST_NAL_POSITION + 3); + assertEquals(TEST_NAL_POSITION + 3, result); + // Should find NAL unit whose prefix starts at the offset. + result = Mp4Util.findNalUnit(data, TEST_NAL_POSITION, data.length); + assertEquals(TEST_NAL_POSITION, result); + // Shouldn't find NAL unit whose prefix starts one byte past the offset. + result = Mp4Util.findNalUnit(data, TEST_NAL_POSITION + 1, data.length); + assertEquals(data.length, result); + } + + public void testFindNalUnitWithPrefix() { + byte[] data = buildTestData(); + + // First byte of NAL unit in data1, rest in data2. + boolean[] prefixFlags = new boolean[3]; + byte[] data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1); + byte[] data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, data.length); + int result = Mp4Util.findNalUnit(data1, 0, data1.length, prefixFlags); + assertEquals(data1.length, result); + result = Mp4Util.findNalUnit(data2, 0, data2.length, prefixFlags); + assertEquals(-1, result); + assertPrefixFlagsCleared(prefixFlags); + + // First three bytes of NAL unit in data1, rest in data2. + prefixFlags = new boolean[3]; + data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 3); + data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 3, data.length); + result = Mp4Util.findNalUnit(data1, 0, data1.length, prefixFlags); + assertEquals(data1.length, result); + result = Mp4Util.findNalUnit(data2, 0, data2.length, prefixFlags); + assertEquals(-3, result); + assertPrefixFlagsCleared(prefixFlags); + + // First byte of NAL unit in data1, second byte in data2, rest in data3. + prefixFlags = new boolean[3]; + data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1); + data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, TEST_NAL_POSITION + 2); + byte[] data3 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, data.length); + result = Mp4Util.findNalUnit(data1, 0, data1.length, prefixFlags); + assertEquals(data1.length, result); + result = Mp4Util.findNalUnit(data2, 0, data2.length, prefixFlags); + assertEquals(data2.length, result); + result = Mp4Util.findNalUnit(data3, 0, data3.length, prefixFlags); + assertEquals(-2, result); + assertPrefixFlagsCleared(prefixFlags); + + // NAL unit split with one byte in four arrays. + prefixFlags = new boolean[3]; + data1 = Arrays.copyOfRange(data, 0, TEST_NAL_POSITION + 1); + data2 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 1, TEST_NAL_POSITION + 2); + data3 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, TEST_NAL_POSITION + 3); + byte[] data4 = Arrays.copyOfRange(data, TEST_NAL_POSITION + 2, data.length); + result = Mp4Util.findNalUnit(data1, 0, data1.length, prefixFlags); + assertEquals(data1.length, result); + result = Mp4Util.findNalUnit(data2, 0, data2.length, prefixFlags); + assertEquals(data2.length, result); + result = Mp4Util.findNalUnit(data3, 0, data3.length, prefixFlags); + assertEquals(data3.length, result); + result = Mp4Util.findNalUnit(data4, 0, data4.length, prefixFlags); + assertEquals(-3, result); + assertPrefixFlagsCleared(prefixFlags); + + // NAL unit entirely in data2. data1 ends with partial prefix. + prefixFlags = new boolean[3]; + data1 = Arrays.copyOfRange(data, 0, TEST_PARTIAL_NAL_POSITION + 2); + data2 = Arrays.copyOfRange(data, TEST_PARTIAL_NAL_POSITION + 2, data.length); + result = Mp4Util.findNalUnit(data1, 0, data1.length, prefixFlags); + assertEquals(data1.length, result); + result = Mp4Util.findNalUnit(data2, 0, data2.length, prefixFlags); + assertEquals(4, result); + assertPrefixFlagsCleared(prefixFlags); + } + + private static byte[] buildTestData() { + byte[] data = new byte[20]; + for (int i = 0; i < data.length; i++) { + data[i] = (byte) 0xFF; + } + // Insert an incomplete NAL unit start code. + data[TEST_PARTIAL_NAL_POSITION] = 0; + data[TEST_PARTIAL_NAL_POSITION + 1] = 0; + // Insert a complete NAL unit start code. + data[TEST_NAL_POSITION] = 0; + data[TEST_NAL_POSITION + 1] = 0; + data[TEST_NAL_POSITION + 2] = 1; + data[TEST_NAL_POSITION + 3] = 5; + return data; + } + + private static void assertPrefixFlagsCleared(boolean[] flags) { + assertEquals(false, flags[0] || flags[1] || flags[2]); + } + +} diff --git a/library/src/test/java/com/google/android/exoplayer/testutil/Util.java b/library/src/test/java/com/google/android/exoplayer/testutil/Util.java new file mode 100644 index 0000000000..5f86b51829 --- /dev/null +++ b/library/src/test/java/com/google/android/exoplayer/testutil/Util.java @@ -0,0 +1,38 @@ +/* + * 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.testutil; + +import java.util.Random; + +/** + * Utility methods for tests. + */ +public class Util { + + private Util() {} + + public static byte[] buildTestData(int length) { + return buildTestData(length, length); + } + + public static byte[] buildTestData(int length, int seed) { + Random random = new Random(seed); + byte[] source = new byte[length]; + random.nextBytes(source); + return source; + } + +} diff --git a/library/src/test/java/com/google/android/exoplayer/text/webvtt/WebvttParserTest.java b/library/src/test/java/com/google/android/exoplayer/text/webvtt/WebvttParserTest.java new file mode 100644 index 0000000000..426c5152df --- /dev/null +++ b/library/src/test/java/com/google/android/exoplayer/text/webvtt/WebvttParserTest.java @@ -0,0 +1,134 @@ +/* + * 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.text.webvtt; + +import com.google.android.exoplayer.C; + +import android.test.InstrumentationTestCase; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Unit test for {@link WebvttParser}. + */ +public class WebvttParserTest extends InstrumentationTestCase { + + private static final String TYPICAL_WEBVTT_FILE = "webvtt/typical"; + private static final String TYPICAL_WITH_IDS_WEBVTT_FILE = "webvtt/typical_with_identifiers"; + private static final String TYPICAL_WITH_TAGS_WEBVTT_FILE = "webvtt/typical_with_tags"; + private static final String EMPTY_WEBVTT_FILE = "webvtt/empty"; + + public void testParseNullWebvttFile() throws IOException { + WebvttParser parser = new WebvttParser(); + InputStream inputStream = + getInstrumentation().getContext().getResources().getAssets().open(EMPTY_WEBVTT_FILE); + + try { + parser.parse(inputStream, C.UTF8_NAME, 0); + fail("Expected IOException"); + } catch (IOException expected) { + // Do nothing. + } + } + + public void testParseTypicalWebvttFile() throws IOException { + WebvttParser parser = new WebvttParser(); + InputStream inputStream = + getInstrumentation().getContext().getResources().getAssets().open(TYPICAL_WEBVTT_FILE); + WebvttSubtitle subtitle = parser.parse(inputStream, C.UTF8_NAME, 0); + + // test start time and event count + long startTimeUs = 5000000; + assertEquals(startTimeUs, subtitle.getStartTime()); + assertEquals(4, subtitle.getEventTimeCount()); + + // test first cue + assertEquals(startTimeUs, subtitle.getEventTime(0)); + assertEquals("This is the first subtitle.", + subtitle.getText(subtitle.getEventTime(0))); + assertEquals(startTimeUs + 1234000, subtitle.getEventTime(1)); + + // test second cue + assertEquals(startTimeUs + 2345000, subtitle.getEventTime(2)); + assertEquals("This is the second subtitle.", + subtitle.getText(subtitle.getEventTime(2))); + assertEquals(startTimeUs + 3456000, subtitle.getEventTime(3)); + } + + public void testParseTypicalWithIdsWebvttFile() throws IOException { + WebvttParser parser = new WebvttParser(); + InputStream inputStream = + getInstrumentation().getContext().getResources().getAssets() + .open(TYPICAL_WITH_IDS_WEBVTT_FILE); + WebvttSubtitle subtitle = parser.parse(inputStream, C.UTF8_NAME, 0); + + // test start time and event count + long startTimeUs = 5000000; + assertEquals(startTimeUs, subtitle.getStartTime()); + assertEquals(4, subtitle.getEventTimeCount()); + + // test first cue + assertEquals(startTimeUs, subtitle.getEventTime(0)); + assertEquals("This is the first subtitle.", + subtitle.getText(subtitle.getEventTime(0))); + assertEquals(startTimeUs + 1234000, subtitle.getEventTime(1)); + + // test second cue + assertEquals(startTimeUs + 2345000, subtitle.getEventTime(2)); + assertEquals("This is the second subtitle.", + subtitle.getText(subtitle.getEventTime(2))); + assertEquals(startTimeUs + 3456000, subtitle.getEventTime(3)); + } + + public void testParseTypicalWithTagsWebvttFile() throws IOException { + WebvttParser parser = new WebvttParser(); + InputStream inputStream = + getInstrumentation().getContext().getResources().getAssets() + .open(TYPICAL_WITH_TAGS_WEBVTT_FILE); + WebvttSubtitle subtitle = parser.parse(inputStream, C.UTF8_NAME, 0); + + // test start time and event count + long startTimeUs = 5000000; + assertEquals(startTimeUs, subtitle.getStartTime()); + assertEquals(8, subtitle.getEventTimeCount()); + + // test first cue + assertEquals(startTimeUs, subtitle.getEventTime(0)); + assertEquals("This is the first subtitle.", + subtitle.getText(subtitle.getEventTime(0))); + assertEquals(startTimeUs + 1234000, subtitle.getEventTime(1)); + + // test second cue + assertEquals(startTimeUs + 2345000, subtitle.getEventTime(2)); + assertEquals("This is the second subtitle.", + subtitle.getText(subtitle.getEventTime(2))); + assertEquals(startTimeUs + 3456000, subtitle.getEventTime(3)); + + // test third cue + assertEquals(startTimeUs + 4000000, subtitle.getEventTime(4)); + assertEquals("This is the third subtitle.", + subtitle.getText(subtitle.getEventTime(4))); + assertEquals(startTimeUs + 5000000, subtitle.getEventTime(5)); + + // test fourth cue + assertEquals(startTimeUs + 6000000, subtitle.getEventTime(6)); + assertEquals("This is the &subtitle.", + subtitle.getText(subtitle.getEventTime(6))); + assertEquals(startTimeUs + 7000000, subtitle.getEventTime(7)); + } + +} diff --git a/library/src/test/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitleTest.java b/library/src/test/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitleTest.java new file mode 100644 index 0000000000..e95482f0fb --- /dev/null +++ b/library/src/test/java/com/google/android/exoplayer/text/webvtt/WebvttSubtitleTest.java @@ -0,0 +1,204 @@ +/* + * 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.text.webvtt; + +import junit.framework.TestCase; + +/** + * Unit test for {@link WebvttSubtitle}. + */ +public class WebvttSubtitleTest extends TestCase { + + private static final String FIRST_SUBTITLE_STRING = "This is the first subtitle."; + private static final String SECOND_SUBTITLE_STRING = "This is the second subtitle."; + private static final String FIRST_AND_SECOND_SUBTITLE_STRING = + FIRST_SUBTITLE_STRING + SECOND_SUBTITLE_STRING; + + private WebvttSubtitle emptySubtitle = new WebvttSubtitle(new String[] {}, 0, new long[] {}); + + private WebvttSubtitle simpleSubtitle = new WebvttSubtitle( + new String[] {FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING}, 0, + new long[] {1000000, 2000000, 3000000, 4000000}); + + private WebvttSubtitle overlappingSubtitle = new WebvttSubtitle( + new String[] {FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING}, 0, + new long[] {1000000, 3000000, 2000000, 4000000}); + + private WebvttSubtitle nestedSubtitle = new WebvttSubtitle( + new String[] {FIRST_SUBTITLE_STRING, SECOND_SUBTITLE_STRING}, 0, + new long[] {1000000, 4000000, 2000000, 3000000}); + + public void testEventCount() { + assertEquals(0, emptySubtitle.getEventTimeCount()); + assertEquals(4, simpleSubtitle.getEventTimeCount()); + assertEquals(4, overlappingSubtitle.getEventTimeCount()); + assertEquals(4, nestedSubtitle.getEventTimeCount()); + } + + public void testStartTime() { + assertEquals(0, emptySubtitle.getStartTime()); + assertEquals(0, simpleSubtitle.getStartTime()); + assertEquals(0, overlappingSubtitle.getStartTime()); + assertEquals(0, nestedSubtitle.getStartTime()); + } + + public void testLastEventTime() { + assertEquals(-1, emptySubtitle.getLastEventTime()); + assertEquals(4000000, simpleSubtitle.getLastEventTime()); + assertEquals(4000000, overlappingSubtitle.getLastEventTime()); + assertEquals(4000000, nestedSubtitle.getLastEventTime()); + } + + public void testSimpleSubtitleEventTimes() { + testSubtitleEventTimesHelper(simpleSubtitle); + } + + public void testSimpleSubtitleEventIndices() { + testSubtitleEventIndicesHelper(simpleSubtitle); + } + + public void testSimpleSubtitleText() { + // Test before first subtitle + assertNull(simpleSubtitle.getText(0)); + assertNull(simpleSubtitle.getText(500000)); + assertNull(simpleSubtitle.getText(999999)); + + // Test first subtitle + assertEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getText(1000000)); + assertEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getText(1500000)); + assertEquals(FIRST_SUBTITLE_STRING, simpleSubtitle.getText(1999999)); + + // Test after first subtitle, before second subtitle + assertNull(simpleSubtitle.getText(2000000)); + assertNull(simpleSubtitle.getText(2500000)); + assertNull(simpleSubtitle.getText(2999999)); + + // Test second subtitle + assertEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getText(3000000)); + assertEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getText(3500000)); + assertEquals(SECOND_SUBTITLE_STRING, simpleSubtitle.getText(3999999)); + + // Test after second subtitle + assertNull(simpleSubtitle.getText(4000000)); + assertNull(simpleSubtitle.getText(4500000)); + assertNull(simpleSubtitle.getText(Long.MAX_VALUE)); + } + + public void testOverlappingSubtitleEventTimes() { + testSubtitleEventTimesHelper(overlappingSubtitle); + } + + public void testOverlappingSubtitleEventIndices() { + testSubtitleEventIndicesHelper(overlappingSubtitle); + } + + public void testOverlappingSubtitleText() { + // Test before first subtitle + assertNull(overlappingSubtitle.getText(0)); + assertNull(overlappingSubtitle.getText(500000)); + assertNull(overlappingSubtitle.getText(999999)); + + // Test first subtitle + assertEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getText(1000000)); + assertEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getText(1500000)); + assertEquals(FIRST_SUBTITLE_STRING, overlappingSubtitle.getText(1999999)); + + // Test after first and second subtitle + assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(2000000)); + assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(2500000)); + assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(2999999)); + + // Test second subtitle + assertEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(3000000)); + assertEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(3500000)); + assertEquals(SECOND_SUBTITLE_STRING, overlappingSubtitle.getText(3999999)); + + // Test after second subtitle + assertNull(overlappingSubtitle.getText(4000000)); + assertNull(overlappingSubtitle.getText(4500000)); + assertNull(overlappingSubtitle.getText(Long.MAX_VALUE)); + } + + public void testNestedSubtitleEventTimes() { + testSubtitleEventTimesHelper(nestedSubtitle); + } + + public void testNestedSubtitleEventIndices() { + testSubtitleEventIndicesHelper(nestedSubtitle); + } + + public void testNestedSubtitleText() { + // Test before first subtitle + assertNull(nestedSubtitle.getText(0)); + assertNull(nestedSubtitle.getText(500000)); + assertNull(nestedSubtitle.getText(999999)); + + // Test first subtitle + assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(1000000)); + assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(1500000)); + assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(1999999)); + + // Test after first and second subtitle + assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getText(2000000)); + assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getText(2500000)); + assertEquals(FIRST_AND_SECOND_SUBTITLE_STRING, nestedSubtitle.getText(2999999)); + + // Test first subtitle + assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(3000000)); + assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(3500000)); + assertEquals(FIRST_SUBTITLE_STRING, nestedSubtitle.getText(3999999)); + + // Test after second subtitle + assertNull(nestedSubtitle.getText(4000000)); + assertNull(nestedSubtitle.getText(4500000)); + assertNull(nestedSubtitle.getText(Long.MAX_VALUE)); + } + + private void testSubtitleEventTimesHelper(WebvttSubtitle subtitle) { + assertEquals(1000000, subtitle.getEventTime(0)); + assertEquals(2000000, subtitle.getEventTime(1)); + assertEquals(3000000, subtitle.getEventTime(2)); + assertEquals(4000000, subtitle.getEventTime(3)); + } + + private void testSubtitleEventIndicesHelper(WebvttSubtitle subtitle) { + // Test first event + assertEquals(0, subtitle.getNextEventTimeIndex(0)); + assertEquals(0, subtitle.getNextEventTimeIndex(500000)); + assertEquals(0, subtitle.getNextEventTimeIndex(999999)); + + // Test second event + assertEquals(1, subtitle.getNextEventTimeIndex(1000000)); + assertEquals(1, subtitle.getNextEventTimeIndex(1500000)); + assertEquals(1, subtitle.getNextEventTimeIndex(1999999)); + + // Test third event + assertEquals(2, subtitle.getNextEventTimeIndex(2000000)); + assertEquals(2, subtitle.getNextEventTimeIndex(2500000)); + assertEquals(2, subtitle.getNextEventTimeIndex(2999999)); + + // Test fourth event + assertEquals(3, subtitle.getNextEventTimeIndex(3000000)); + assertEquals(3, subtitle.getNextEventTimeIndex(3500000)); + assertEquals(3, subtitle.getNextEventTimeIndex(3999999)); + + // Test null event (i.e. look for events after the last event) + assertEquals(-1, subtitle.getNextEventTimeIndex(4000000)); + assertEquals(-1, subtitle.getNextEventTimeIndex(4500000)); + assertEquals(-1, subtitle.getNextEventTimeIndex(Long.MAX_VALUE)); + } + +} diff --git a/library/src/test/java/com/google/android/exoplayer/upstream/ByteArrayDataSourceTest.java b/library/src/test/java/com/google/android/exoplayer/upstream/ByteArrayDataSourceTest.java new file mode 100644 index 0000000000..550cb149a8 --- /dev/null +++ b/library/src/test/java/com/google/android/exoplayer/upstream/ByteArrayDataSourceTest.java @@ -0,0 +1,151 @@ +/* + * 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 junit.framework.TestCase; + +import java.io.IOException; + +/** + * Unit tests for {@link ByteArrayDataSource}. + */ +public class ByteArrayDataSourceTest extends TestCase { + + private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + private static final byte[] TEST_DATA_ODD = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + + public void testFullReadSingleBytes() { + readTestData(TEST_DATA, 0, C.LENGTH_UNBOUNDED, 1, 0, 1, false); + } + + public void testFullReadAllBytes() { + readTestData(TEST_DATA, 0, C.LENGTH_UNBOUNDED, 100, 0, 100, false); + } + + public void testLimitReadSingleBytes() { + // Limit set to the length of the data. + readTestData(TEST_DATA, 0, TEST_DATA.length, 1, 0, 1, false); + // And less. + readTestData(TEST_DATA, 0, 6, 1, 0, 1, false); + } + + public void testFullReadTwoBytes() { + // Try with the total data length an exact multiple of the size of each individual read. + readTestData(TEST_DATA, 0, C.LENGTH_UNBOUNDED, 2, 0, 2, false); + // And not. + readTestData(TEST_DATA_ODD, 0, C.LENGTH_UNBOUNDED, 2, 0, 2, false); + } + + public void testLimitReadTwoBytes() { + // Try with the limit an exact multiple of the size of each individual read. + readTestData(TEST_DATA, 0, 6, 2, 0, 2, false); + // And not. + readTestData(TEST_DATA, 0, 7, 2, 0, 2, false); + } + + public void testReadFromValidOffsets() { + // Read from an offset without bound. + readTestData(TEST_DATA, 1, C.LENGTH_UNBOUNDED, 1, 0, 1, false); + // And with bound. + readTestData(TEST_DATA, 1, 6, 1, 0, 1, false); + // Read from the last possible offset without bound. + readTestData(TEST_DATA, TEST_DATA.length - 1, C.LENGTH_UNBOUNDED, 1, 0, 1, false); + // And with bound. + readTestData(TEST_DATA, TEST_DATA.length - 1, 1, 1, 0, 1, false); + } + + public void testReadFromInvalidOffsets() { + // Read from first invalid offset and check failure without bound. + readTestData(TEST_DATA, TEST_DATA.length, C.LENGTH_UNBOUNDED, 1, 0, 1, true); + // And with bound. + readTestData(TEST_DATA, TEST_DATA.length, 1, 1, 0, 1, true); + } + + public void testReadWithInvalidLength() { + // Read more data than is available. + readTestData(TEST_DATA, 0, TEST_DATA.length + 1, 1, 0, 1, true); + // And with bound. + readTestData(TEST_DATA, 1, TEST_DATA.length, 1, 0, 1, true); + } + + /** + * Tests reading from a {@link ByteArrayDataSource} with various parameters. + * + * @param testData The data that the {@link ByteArrayDataSource} will wrap. + * @param dataOffset The offset from which to read data. + * @param dataLength The total length of data to read. + * @param outputBufferLength The length of the target buffer for each read. + * @param writeOffset The offset into {@code outputBufferLength} for each read. + * @param maxReadLength The maximum length of each read. + * @param expectFailOnOpen Whether it is expected that opening the source will fail. + */ + private void readTestData(byte[] testData, int dataOffset, int dataLength, int outputBufferLength, + int writeOffset, int maxReadLength, boolean expectFailOnOpen) { + int expectedFinalBytesRead = + dataLength == C.LENGTH_UNBOUNDED ? (testData.length - dataOffset) : dataLength; + ByteArrayDataSource dataSource = new ByteArrayDataSource(testData); + boolean opened = false; + try { + // Open the source. + long length = dataSource.open(new DataSpec(null, dataOffset, dataLength, null)); + opened = true; + assertFalse(expectFailOnOpen); + + // Verify the resolved length is as we expect. + assertEquals(expectedFinalBytesRead, length); + + byte[] outputBuffer = new byte[outputBufferLength]; + int accumulatedBytesRead = 0; + while (true) { + // Calculate a valid length for the next read, constraining by the specified output buffer + // length, write offset and maximum write length input parameters. + int requestedReadLength = Math.min(maxReadLength, outputBufferLength - writeOffset); + assertTrue(requestedReadLength > 0); + + int bytesRead = dataSource.read(outputBuffer, writeOffset, requestedReadLength); + if (bytesRead != -1) { + assertTrue(bytesRead > 0); + assertTrue(bytesRead <= requestedReadLength); + // Check the data read was correct. + for (int i = 0; i < bytesRead; i++) { + assertEquals(testData[dataOffset + accumulatedBytesRead + i], + outputBuffer[writeOffset + i]); + } + // Check that we haven't read more data than we were expecting. + accumulatedBytesRead += bytesRead; + assertTrue(accumulatedBytesRead <= expectedFinalBytesRead); + // If we haven't read all of the bytes the request should have been satisfied in full. + assertTrue(accumulatedBytesRead == expectedFinalBytesRead + || bytesRead == requestedReadLength); + } else { + // We're done. Check we read the expected number of bytes. + assertEquals(expectedFinalBytesRead, accumulatedBytesRead); + return; + } + } + } catch (IOException e) { + if (expectFailOnOpen && !opened) { + // Expected. + return; + } + // Unexpected failure. + fail(); + } + } + +} diff --git a/library/src/test/java/com/google/android/exoplayer/upstream/DataSourceStreamTest.java b/library/src/test/java/com/google/android/exoplayer/upstream/DataSourceStreamTest.java new file mode 100644 index 0000000000..7c40378116 --- /dev/null +++ b/library/src/test/java/com/google/android/exoplayer/upstream/DataSourceStreamTest.java @@ -0,0 +1,60 @@ +/* + * 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.testutil.Util; + +import junit.framework.TestCase; + +import java.io.IOException; +import java.util.Arrays; + +/** + * Unit tests for {@link DataSourceStream}. + */ +public class DataSourceStreamTest extends TestCase { + + private static final int DATA_LENGTH = 1024; + private static final int BUFFER_LENGTH = 128; + + public void testGetLoadedData() throws IOException, InterruptedException { + byte[] testData = Util.buildTestData(DATA_LENGTH); + DataSource dataSource = new ByteArrayDataSource(testData); + DataSpec dataSpec = new DataSpec(null, 0, DATA_LENGTH, null); + DataSourceStream dataSourceStream = new DataSourceStream(dataSource, dataSpec, + new BufferPool(BUFFER_LENGTH)); + + dataSourceStream.load(); + // Assert that the read and load positions are correct. + assertEquals(0, dataSourceStream.getReadPosition()); + assertEquals(testData.length, dataSourceStream.getLoadPosition()); + + int halfTestDataLength = testData.length / 2; + byte[] readData = new byte[testData.length]; + int bytesRead = dataSourceStream.read(readData, 0, halfTestDataLength); + // Assert that the read position is updated correctly. + assertEquals(halfTestDataLength, bytesRead); + assertEquals(halfTestDataLength, dataSourceStream.getReadPosition()); + + bytesRead += dataSourceStream.read(readData, bytesRead, testData.length - bytesRead); + // Assert that the read position was updated correctly. + assertEquals(testData.length, bytesRead); + assertEquals(testData.length, dataSourceStream.getReadPosition()); + // Assert that the data read using the two read calls either side of getLoadedData is correct. + assertTrue(Arrays.equals(testData, readData)); + } + +} diff --git a/library/src/test/java/com/google/android/exoplayer/util/ParsableByteArrayTest.java b/library/src/test/java/com/google/android/exoplayer/util/ParsableByteArrayTest.java new file mode 100644 index 0000000000..950710cf1d --- /dev/null +++ b/library/src/test/java/com/google/android/exoplayer/util/ParsableByteArrayTest.java @@ -0,0 +1,124 @@ +/* + * 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.util; + +import junit.framework.TestCase; + +import java.util.Arrays; + +/** + * Tests for {@link ParsableByteArray}. + */ +public class ParsableByteArrayTest extends TestCase { + + private static final byte[] ARRAY_ELEMENTS = + new byte[] {0x0F, (byte) 0xFF, (byte) 0x42, (byte) 0x0F, 0x00, 0x00, 0x00, 0x00}; + + private ParsableByteArray parsableByteArray; + + @Override + public void setUp() { + parsableByteArray = new ParsableByteArray(ARRAY_ELEMENTS.length); + System.arraycopy(ARRAY_ELEMENTS, 0, parsableByteArray.data, 0, ARRAY_ELEMENTS.length); + } + + public void testReadInt() { + // When reading a signed integer + int value = parsableByteArray.readInt(); + + // Then the read value is equal to the array elements interpreted as an int. + assertEquals((0xFF & ARRAY_ELEMENTS[0]) << 24 | (0xFF & ARRAY_ELEMENTS[1]) << 16 + | (0xFF & ARRAY_ELEMENTS[2]) << 8 | (0xFF & ARRAY_ELEMENTS[3]), value); + } + + public void testSkipBack() { + // When reading an unsigned integer + long value = parsableByteArray.readUnsignedInt(); + + // Then skipping back and reading gives the same value. + parsableByteArray.skip(-4); + assertEquals(value, parsableByteArray.readUnsignedInt()); + } + + public void testReadingMovesPosition() { + // Given an array at the start + assertEquals(0, parsableByteArray.getPosition()); + + // When reading an integer, the position advances + parsableByteArray.readUnsignedInt(); + assertEquals(4, parsableByteArray.getPosition()); + } + + public void testOutOfBoundsThrows() { + // Given an array at the end + parsableByteArray.readUnsignedLongToLong(); + assertEquals(ARRAY_ELEMENTS.length, parsableByteArray.getPosition()); + + // Then reading more data throws. + try { + parsableByteArray.readUnsignedInt(); + fail(); + } catch (Exception e) { + // Expected. + } + } + + public void testModificationsAffectParsableArray() { + // When modifying the wrapped byte array + byte[] data = parsableByteArray.data; + long readValue = parsableByteArray.readUnsignedInt(); + data[0] = (byte) (ARRAY_ELEMENTS[0] + 1); + parsableByteArray.setPosition(0); + + // Then the parsed value changes. + assertFalse(parsableByteArray.readUnsignedInt() == readValue); + } + + public void testReadingUnsignedLongWithMsbSetThrows() { + // Given an array with the most-significant bit set on the top byte + byte[] data = parsableByteArray.data; + data[0] = (byte) 0x80; + + // Then reading an unsigned long throws. + try { + parsableByteArray.readUnsignedLongToLong(); + fail(); + } catch (Exception e) { + // Expected. + } + } + + public void testReadUnsignedFixedPoint1616() { + // When reading the integer part of a 16.16 fixed point value + int value = parsableByteArray.readUnsignedFixedPoint1616(); + + // Then the read value is equal to the array elements interpreted as a short. + assertEquals((0xFF & ARRAY_ELEMENTS[0]) << 8 | (ARRAY_ELEMENTS[1] & 0xFF), value); + assertEquals(4, parsableByteArray.getPosition()); + } + + public void testReadingBytesReturnsCopy() { + // When reading all the bytes back + int length = parsableByteArray.limit(); + assertEquals(ARRAY_ELEMENTS.length, length); + byte[] copy = new byte[length]; + parsableByteArray.readBytes(copy, 0, length); + + // Then the array elements are the same. + assertTrue(Arrays.equals(parsableByteArray.data, copy)); + } + +} diff --git a/library/src/test/java/com/google/android/exoplayer/util/UriUtilTest.java b/library/src/test/java/com/google/android/exoplayer/util/UriUtilTest.java new file mode 100644 index 0000000000..f482d99c47 --- /dev/null +++ b/library/src/test/java/com/google/android/exoplayer/util/UriUtilTest.java @@ -0,0 +1,98 @@ +/* + * 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.util; + +import junit.framework.TestCase; + +/** + * Unit tests for {@link UriUtil}. + */ +public class UriUtilTest extends TestCase { + + /** + * Tests normal usage of {@link UriUtil#resolve(String, String)}. + *

+ * The test cases are taken from RFC-3986 5.4.1. + */ + public void testResolveNormal() { + String base = "http://a/b/c/d;p?q"; + + assertEquals("g:h", UriUtil.resolve(base, "g:h")); + assertEquals("http://a/b/c/g", UriUtil.resolve(base, "g")); + assertEquals("http://a/b/c/g/", UriUtil.resolve(base, "g/")); + assertEquals("http://a/g", UriUtil.resolve(base, "/g")); + assertEquals("http://g", UriUtil.resolve(base, "//g")); + assertEquals("http://a/b/c/d;p?y", UriUtil.resolve(base, "?y")); + assertEquals("http://a/b/c/g?y", UriUtil.resolve(base, "g?y")); + assertEquals("http://a/b/c/d;p?q#s", UriUtil.resolve(base, "#s")); + assertEquals("http://a/b/c/g#s", UriUtil.resolve(base, "g#s")); + assertEquals("http://a/b/c/g?y#s", UriUtil.resolve(base, "g?y#s")); + assertEquals("http://a/b/c/;x", UriUtil.resolve(base, ";x")); + assertEquals("http://a/b/c/g;x", UriUtil.resolve(base, "g;x")); + assertEquals("http://a/b/c/g;x?y#s", UriUtil.resolve(base, "g;x?y#s")); + assertEquals("http://a/b/c/d;p?q", UriUtil.resolve(base, "")); + assertEquals("http://a/b/c/", UriUtil.resolve(base, ".")); + assertEquals("http://a/b/c/", UriUtil.resolve(base, "./")); + assertEquals("http://a/b/", UriUtil.resolve(base, "..")); + assertEquals("http://a/b/", UriUtil.resolve(base, "../")); + assertEquals("http://a/b/g", UriUtil.resolve(base, "../g")); + assertEquals("http://a/", UriUtil.resolve(base, "../..")); + assertEquals("http://a/", UriUtil.resolve(base, "../../")); + assertEquals("http://a/g", UriUtil.resolve(base, "../../g")); + } + + /** + * Tests abnormal usage of {@link UriUtil#resolve(String, String)}. + *

+ * The test cases are taken from RFC-3986 5.4.2. + */ + public void testResolveAbnormal() { + String base = "http://a/b/c/d;p?q"; + + assertEquals("http://a/g", UriUtil.resolve(base, "../../../g")); + assertEquals("http://a/g", UriUtil.resolve(base, "../../../../g")); + + assertEquals("http://a/g", UriUtil.resolve(base, "/./g")); + assertEquals("http://a/g", UriUtil.resolve(base, "/../g")); + assertEquals("http://a/b/c/g.", UriUtil.resolve(base, "g.")); + assertEquals("http://a/b/c/.g", UriUtil.resolve(base, ".g")); + assertEquals("http://a/b/c/g..", UriUtil.resolve(base, "g..")); + assertEquals("http://a/b/c/..g", UriUtil.resolve(base, "..g")); + + assertEquals("http://a/b/g", UriUtil.resolve(base, "./../g")); + assertEquals("http://a/b/c/g/", UriUtil.resolve(base, "./g/.")); + assertEquals("http://a/b/c/g/h", UriUtil.resolve(base, "g/./h")); + assertEquals("http://a/b/c/h", UriUtil.resolve(base, "g/../h")); + assertEquals("http://a/b/c/g;x=1/y", UriUtil.resolve(base, "g;x=1/./y")); + assertEquals("http://a/b/c/y", UriUtil.resolve(base, "g;x=1/../y")); + + assertEquals("http://a/b/c/g?y/./x", UriUtil.resolve(base, "g?y/./x")); + assertEquals("http://a/b/c/g?y/../x", UriUtil.resolve(base, "g?y/../x")); + assertEquals("http://a/b/c/g#s/./x", UriUtil.resolve(base, "g#s/./x")); + assertEquals("http://a/b/c/g#s/../x", UriUtil.resolve(base, "g#s/../x")); + + assertEquals("http:g", UriUtil.resolve(base, "http:g")); + } + + /** + * Tests additional abnormal usage of {@link UriUtil#resolve(String, String)}. + */ + public void testResolveAbnormalAdditional() { + assertEquals("c:e", UriUtil.resolve("http://a/b", "c:d/../e")); + assertEquals("a:c", UriUtil.resolve("a:b", "../c")); + } + +} diff --git a/library/src/test/java/com/google/android/exoplayer/util/UtilTest.java b/library/src/test/java/com/google/android/exoplayer/util/UtilTest.java new file mode 100644 index 0000000000..351e93c339 --- /dev/null +++ b/library/src/test/java/com/google/android/exoplayer/util/UtilTest.java @@ -0,0 +1,147 @@ +/* + * 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.util; + +import junit.framework.TestCase; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; + +/** + * Unit tests for {@link Util}. + */ +public class UtilTest extends TestCase { + + public void testArrayBinarySearchFloor() { + long[] values = new long[0]; + assertEquals(-1, Util.binarySearchFloor(values, 0, false, false)); + assertEquals(0, Util.binarySearchFloor(values, 0, false, true)); + + values = new long[] {1, 3, 5}; + assertEquals(-1, Util.binarySearchFloor(values, 0, false, false)); + assertEquals(-1, Util.binarySearchFloor(values, 0, true, false)); + assertEquals(0, Util.binarySearchFloor(values, 0, false, true)); + assertEquals(0, Util.binarySearchFloor(values, 0, true, true)); + + assertEquals(-1, Util.binarySearchFloor(values, 1, false, false)); + assertEquals(0, Util.binarySearchFloor(values, 1, true, false)); + assertEquals(0, Util.binarySearchFloor(values, 1, false, true)); + assertEquals(0, Util.binarySearchFloor(values, 1, true, true)); + + assertEquals(1, Util.binarySearchFloor(values, 4, false, false)); + assertEquals(1, Util.binarySearchFloor(values, 4, true, false)); + + assertEquals(1, Util.binarySearchFloor(values, 5, false, false)); + assertEquals(2, Util.binarySearchFloor(values, 5, true, false)); + + assertEquals(2, Util.binarySearchFloor(values, 6, false, false)); + assertEquals(2, Util.binarySearchFloor(values, 6, true, false)); + } + + public void testListBinarySearchFloor() { + List values = new ArrayList(); + assertEquals(-1, Util.binarySearchFloor(values, 0, false, false)); + assertEquals(0, Util.binarySearchFloor(values, 0, false, true)); + + values.add(1); + values.add(3); + values.add(5); + assertEquals(-1, Util.binarySearchFloor(values, 0, false, false)); + assertEquals(-1, Util.binarySearchFloor(values, 0, true, false)); + assertEquals(0, Util.binarySearchFloor(values, 0, false, true)); + assertEquals(0, Util.binarySearchFloor(values, 0, true, true)); + + assertEquals(-1, Util.binarySearchFloor(values, 1, false, false)); + assertEquals(0, Util.binarySearchFloor(values, 1, true, false)); + assertEquals(0, Util.binarySearchFloor(values, 1, false, true)); + assertEquals(0, Util.binarySearchFloor(values, 1, true, true)); + + assertEquals(1, Util.binarySearchFloor(values, 4, false, false)); + assertEquals(1, Util.binarySearchFloor(values, 4, true, false)); + + assertEquals(1, Util.binarySearchFloor(values, 5, false, false)); + assertEquals(2, Util.binarySearchFloor(values, 5, true, false)); + + assertEquals(2, Util.binarySearchFloor(values, 6, false, false)); + assertEquals(2, Util.binarySearchFloor(values, 6, true, false)); + } + + public void testArrayBinarySearchCeil() { + long[] values = new long[0]; + assertEquals(0, Util.binarySearchCeil(values, 0, false, false)); + assertEquals(-1, Util.binarySearchCeil(values, 0, false, true)); + + values = new long[] {1, 3, 5}; + assertEquals(0, Util.binarySearchCeil(values, 0, false, false)); + assertEquals(0, Util.binarySearchCeil(values, 0, true, false)); + + assertEquals(1, Util.binarySearchCeil(values, 1, false, false)); + assertEquals(0, Util.binarySearchCeil(values, 1, true, false)); + + assertEquals(1, Util.binarySearchCeil(values, 2, false, false)); + assertEquals(1, Util.binarySearchCeil(values, 2, true, false)); + + assertEquals(3, Util.binarySearchCeil(values, 5, false, false)); + assertEquals(2, Util.binarySearchCeil(values, 5, true, false)); + assertEquals(2, Util.binarySearchCeil(values, 5, false, true)); + assertEquals(2, Util.binarySearchCeil(values, 5, true, true)); + + assertEquals(3, Util.binarySearchCeil(values, 6, false, false)); + assertEquals(3, Util.binarySearchCeil(values, 6, true, false)); + assertEquals(2, Util.binarySearchCeil(values, 6, false, true)); + assertEquals(2, Util.binarySearchCeil(values, 6, true, true)); + } + + public void testListBinarySearchCeil() { + List values = new ArrayList(); + assertEquals(0, Util.binarySearchCeil(values, 0, false, false)); + assertEquals(-1, Util.binarySearchCeil(values, 0, false, true)); + + values.add(1); + values.add(3); + values.add(5); + assertEquals(0, Util.binarySearchCeil(values, 0, false, false)); + assertEquals(0, Util.binarySearchCeil(values, 0, true, false)); + + assertEquals(1, Util.binarySearchCeil(values, 1, false, false)); + assertEquals(0, Util.binarySearchCeil(values, 1, true, false)); + + assertEquals(1, Util.binarySearchCeil(values, 2, false, false)); + assertEquals(1, Util.binarySearchCeil(values, 2, true, false)); + + assertEquals(3, Util.binarySearchCeil(values, 5, false, false)); + assertEquals(2, Util.binarySearchCeil(values, 5, true, false)); + assertEquals(2, Util.binarySearchCeil(values, 5, false, true)); + assertEquals(2, Util.binarySearchCeil(values, 5, true, true)); + + assertEquals(3, Util.binarySearchCeil(values, 6, false, false)); + assertEquals(3, Util.binarySearchCeil(values, 6, true, false)); + assertEquals(2, Util.binarySearchCeil(values, 6, false, true)); + assertEquals(2, Util.binarySearchCeil(values, 6, true, true)); + } + + public void testParseXsDuration() { + assertEquals(150279L, Util.parseXsDuration("PT150.279S")); + assertEquals(1500L, Util.parseXsDuration("PT1.500S")); + } + + public void testParseXsDateTime() throws ParseException { + assertEquals(1403219262000L, Util.parseXsDateTime("2014-06-19T23:07:42")); + assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00Z")); + } + +} diff --git a/library/src/test/libs/.README.txt b/library/src/test/libs/.README.txt new file mode 100644 index 0000000000..3f37353a9d --- /dev/null +++ b/library/src/test/libs/.README.txt @@ -0,0 +1 @@ +This file is needed to make sure the libs directory is present. diff --git a/library/src/test/project.properties b/library/src/test/project.properties new file mode 100644 index 0000000000..6e18427a42 --- /dev/null +++ b/library/src/test/project.properties @@ -0,0 +1,14 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-21 diff --git a/library/src/test/res/.README.txt b/library/src/test/res/.README.txt new file mode 100644 index 0000000000..c27147ce56 --- /dev/null +++ b/library/src/test/res/.README.txt @@ -0,0 +1,2 @@ +This file is needed to make sure the res directory is present. +The file is ignored by the Android toolchain because its name starts with a dot. diff --git a/third_party/dexmaker/LICENSE b/third_party/dexmaker/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/third_party/dexmaker/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/third_party/dexmaker/dexmaker-1.2.jar b/third_party/dexmaker/dexmaker-1.2.jar new file mode 100644 index 0000000000..08d1b85f18 Binary files /dev/null and b/third_party/dexmaker/dexmaker-1.2.jar differ diff --git a/third_party/dexmaker/dexmaker-mockito-1.2.jar b/third_party/dexmaker/dexmaker-mockito-1.2.jar new file mode 100644 index 0000000000..a3e19759c2 Binary files /dev/null and b/third_party/dexmaker/dexmaker-mockito-1.2.jar differ diff --git a/third_party/mockito/LICENSE b/third_party/mockito/LICENSE new file mode 100644 index 0000000000..2a5730a7ce --- /dev/null +++ b/third_party/mockito/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2008-2010 Mockito contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/third_party/mockito/mockito-all-1.9.5.jar b/third_party/mockito/mockito-all-1.9.5.jar new file mode 100644 index 0000000000..00416eb387 Binary files /dev/null and b/third_party/mockito/mockito-all-1.9.5.jar differ