diff --git a/library/src/main/java/com/google/android/exoplayer/text/subrip/SubripCue.java b/library/src/main/java/com/google/android/exoplayer/text/subrip/SubripCue.java
new file mode 100644
index 0000000000..c60aeeb679
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/text/subrip/SubripCue.java
@@ -0,0 +1,48 @@
+/*
+ * 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.subrip;
+
+import com.google.android.exoplayer.text.Cue;
+
+/**
+ * A representation of a SubRip cue.
+ */
+/* package */ final class SubripCue extends Cue {
+
+ public final long startTime;
+ public final long endTime;
+
+ public SubripCue(CharSequence text) {
+ this(Cue.UNSET_VALUE, Cue.UNSET_VALUE, Cue.UNSET_VALUE,text);
+ }
+
+ public SubripCue(long startTime, long endTime, int position, CharSequence text) {
+ super(text, Cue.UNSET_VALUE, position, null, Cue.UNSET_VALUE);
+ this.startTime = startTime;
+ this.endTime = endTime;
+ }
+
+ /**
+ * Returns whether or not this cue should be placed in the default position and rolled-up with
+ * the other "normal" cues.
+ *
+ * @return True if this cue should be placed in the default position; false otherwise.
+ */
+ public boolean isNormalCue() {
+ return (line == UNSET_VALUE && position == UNSET_VALUE);
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/text/subrip/SubripParser.java b/library/src/main/java/com/google/android/exoplayer/text/subrip/SubripParser.java
new file mode 100644
index 0000000000..66a4dbff86
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/text/subrip/SubripParser.java
@@ -0,0 +1,149 @@
+/*
+ * 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.subrip;
+
+import android.text.Html;
+
+import com.google.android.exoplayer.C;
+import com.google.android.exoplayer.ParserException;
+import com.google.android.exoplayer.text.Cue;
+import com.google.android.exoplayer.text.SubtitleParser;
+import com.google.android.exoplayer.util.MimeTypes;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A simple SRT parser.
+ *
+ *
+ * @see Wikipedia on SRT
+ */
+public final class SubripParser implements SubtitleParser {
+
+ private static final String TAG = "SubRipParser";
+
+ private static final String SUBRIP_POSITION_STRING = "^(\\d)$";
+ private static final Pattern SUBRIP_POSITION = Pattern.compile(SUBRIP_POSITION_STRING);
+
+ private static final String SUBRIP_CUE_IDENTIFIER_STRING = "^(.*)\\s-->\\s(.*)$";
+ private static final Pattern SUBRIP_CUE_IDENTIFIER =
+ Pattern.compile(SUBRIP_CUE_IDENTIFIER_STRING);
+
+ private static final String SUBRIP_TIMESTAMP_STRING = "(\\d+:)?[0-5]\\d:[0-5]\\d:[0-5]\\d,\\d{3}";
+ // private static final Pattern SUBRIP_TIMESTAMP = Pattern.compile(SUBRIP_TIMESTAMP_STRING);
+
+ private final StringBuilder textBuilder;
+
+ private final boolean strictParsing;
+
+ public SubripParser() {
+ this(true);
+ }
+
+ public SubripParser(boolean strictParsing) {
+ this.strictParsing = strictParsing;
+
+ textBuilder = new StringBuilder();
+ }
+
+ @Override
+ public SubripSubtitle parse(InputStream inputStream, String inputEncoding, long startTimeUs)
+ throws IOException {
+ ArrayList subtitles = new ArrayList<>();
+
+ // file should not be empty
+ if (inputStream.available() == 0) {
+ throw new ParserException("File is empty?");
+ }
+
+ BufferedReader subripData = new BufferedReader(new InputStreamReader(inputStream, C.UTF8_NAME));
+ String line;
+
+
+ // process the cues and text
+ while ((line = subripData.readLine()) != null) {
+ long startTime = Cue.UNSET_VALUE;
+ long endTime = Cue.UNSET_VALUE;
+ CharSequence text = null;
+ int position = Cue.UNSET_VALUE;
+
+ Matcher matcher = SUBRIP_POSITION.matcher(line);
+ if (matcher.matches()) {
+ position = Integer.parseInt(matcher.group());
+ }
+
+ line = subripData.readLine();
+
+ // parse cue time
+ matcher = SUBRIP_CUE_IDENTIFIER.matcher(line);
+ if (!matcher.find()) {
+ throw new ParserException("Expected cue start time: " + line);
+ } else {
+ startTime = parseTimestampUs(matcher.group(1)) + startTimeUs;
+ endTime = parseTimestampUs(matcher.group(2)) + startTimeUs;
+ }
+
+ // parse text
+ textBuilder.setLength(0);
+ while (((line = subripData.readLine()) != null) && (!line.isEmpty())) {
+ if (textBuilder.length() > 0) {
+ textBuilder.append("
");
+ }
+ textBuilder.append(line.trim());
+ }
+ text = Html.fromHtml(textBuilder.toString());
+
+ SubripCue cue = new SubripCue(startTime, endTime, position, text);
+ subtitles.add(cue);
+ }
+
+ subripData.close();
+ inputStream.close();
+ SubripSubtitle subtitle = new SubripSubtitle(subtitles, startTimeUs);
+ return subtitle;
+ }
+
+ @Override
+ public boolean canParse(String mimeType) {
+ return MimeTypes.APPLICATION_SUBRIP.equals(mimeType);
+ }
+
+ private void handleNoncompliantLine(String line) throws ParserException {
+ if (strictParsing) {
+ throw new ParserException("Unexpected line: " + line);
+ }
+ }
+
+ private static long parseTimestampUs(String s) throws NumberFormatException {
+ if (!s.matches(SUBRIP_TIMESTAMP_STRING)) {
+ throw new NumberFormatException("has invalid format");
+ }
+
+ String[] parts = s.split(",", 2);
+ long value = 0;
+ for (String group : parts[0].split(":")) {
+ value = value * 60 + Long.parseLong(group);
+ }
+ return (value * 1000 + Long.parseLong(parts[1])) * 1000;
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/text/subrip/SubripSubtitle.java b/library/src/main/java/com/google/android/exoplayer/text/subrip/SubripSubtitle.java
new file mode 100644
index 0000000000..27cf7988ea
--- /dev/null
+++ b/library/src/main/java/com/google/android/exoplayer/text/subrip/SubripSubtitle.java
@@ -0,0 +1,138 @@
+/*
+ * 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.subrip;
+
+import android.text.SpannableStringBuilder;
+
+import com.google.android.exoplayer.text.Cue;
+import com.google.android.exoplayer.text.Subtitle;
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.Util;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A representation of a SubRip subtitle.
+ */
+/* package */ final class SubripSubtitle implements Subtitle {
+
+ private final List cues;
+ private final int numCues;
+ private final long startTimeUs;
+ private final long[] cueTimesUs;
+ private final long[] sortedCueTimesUs;
+
+ /**
+ * @param cues A list of the cues in this subtitle.
+ * @param startTimeUs The start time of the subtitle.
+ */
+ public SubripSubtitle(List cues, long startTimeUs) {
+ this.cues = cues;
+ numCues = cues.size();
+ this.startTimeUs = startTimeUs;
+
+ this.cueTimesUs = new long[2 * numCues];
+ for (int cueIndex = 0; cueIndex < numCues; cueIndex++) {
+ SubripCue cue = cues.get(cueIndex);
+ int arrayIndex = cueIndex * 2;
+ cueTimesUs[arrayIndex] = cue.startTime;
+ cueTimesUs[arrayIndex + 1] = cue.endTime;
+ }
+
+ this.sortedCueTimesUs = Arrays.copyOf(cueTimesUs, cueTimesUs.length);
+ Arrays.sort(sortedCueTimesUs);
+ }
+
+ @Override
+ public long getStartTime() {
+ return startTimeUs;
+ }
+
+ @Override
+ public int getNextEventTimeIndex(long timeUs) {
+ Assertions.checkArgument(timeUs >= 0);
+ int index = Util.binarySearchCeil(sortedCueTimesUs, timeUs, false, false);
+ return index < sortedCueTimesUs.length ? index : -1;
+ }
+
+ @Override
+ public int getEventTimeCount() {
+ return sortedCueTimesUs.length;
+ }
+
+ @Override
+ public long getEventTime(int index) {
+ Assertions.checkArgument(index >= 0);
+ Assertions.checkArgument(index < sortedCueTimesUs.length);
+ return sortedCueTimesUs[index];
+ }
+
+ @Override
+ public long getLastEventTime() {
+ if (getEventTimeCount() == 0) {
+ return -1;
+ }
+ return sortedCueTimesUs[sortedCueTimesUs.length - 1];
+ }
+
+ @Override
+ public List getCues(long timeUs) {
+ ArrayList list = null;
+ SubripCue firstNormalCue = null;
+ SpannableStringBuilder normalCueTextBuilder = null;
+
+ for (int i = 0; i < numCues; i++) {
+ if ((cueTimesUs[i * 2] <= timeUs) && (timeUs < cueTimesUs[i * 2 + 1])) {
+ if (list == null) {
+ list = new ArrayList<>();
+ }
+ SubripCue cue = cues.get(i);
+ if (cue.isNormalCue()) {
+ // we want to merge all of the normal cues into a single cue to ensure they are drawn
+ // correctly (i.e. don't overlap) and to emulate roll-up, but only if there are multiple
+ // normal cues, otherwise we can just append the single normal cue
+ if (firstNormalCue == null) {
+ firstNormalCue = cue;
+ } else if (normalCueTextBuilder == null) {
+ normalCueTextBuilder = new SpannableStringBuilder();
+ normalCueTextBuilder.append(firstNormalCue.text).append("\n").append(cue.text);
+ } else {
+ normalCueTextBuilder.append("\n").append(cue.text);
+ }
+ } else {
+ list.add(cue);
+ }
+ }
+ }
+ if (normalCueTextBuilder != null) {
+ // there were multiple normal cues, so create a new cue with all of the text
+ list.add(new SubripCue(normalCueTextBuilder));
+ } else if (firstNormalCue != null) {
+ // there was only a single normal cue, so just add it to the list
+ list.add(firstNormalCue);
+ }
+
+ if (list != null) {
+ return list;
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
+}
diff --git a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java
index 3a6ea01c6f..393421e57b 100644
--- a/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java
+++ b/library/src/main/java/com/google/android/exoplayer/util/MimeTypes.java
@@ -55,6 +55,7 @@ public class MimeTypes {
public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3";
public static final String APPLICATION_EIA608 = BASE_TYPE_APPLICATION + "/eia-608";
+ public static final String APPLICATION_SUBRIP = BASE_TYPE_APPLICATION + "/x-subrip";
public static final String APPLICATION_TTML = BASE_TYPE_APPLICATION + "/ttml+xml";
public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL";
diff --git a/library/src/test/assets/subrip/empty b/library/src/test/assets/subrip/empty
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/library/src/test/assets/subrip/typical b/library/src/test/assets/subrip/typical
new file mode 100644
index 0000000000..800b0678c5
--- /dev/null
+++ b/library/src/test/assets/subrip/typical
@@ -0,0 +1,8 @@
+1
+00:00:00,000 --> 00:00:01,234
+This is the first subtitle.
+
+2
+00:00:02,345 --> 00:00:03,456
+This is the second subtitle.
+Second subtitle with second line.
\ No newline at end of file
diff --git a/library/src/test/java/com/google/android/exoplayer/text/subrip/SubripParserTest.java b/library/src/test/java/com/google/android/exoplayer/text/subrip/SubripParserTest.java
new file mode 100644
index 0000000000..0735d023f6
--- /dev/null
+++ b/library/src/test/java/com/google/android/exoplayer/text/subrip/SubripParserTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.subrip;
+
+import android.test.InstrumentationTestCase;
+
+import com.google.android.exoplayer.C;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Unit test for {@link SubripParser}.
+ */
+public class SubripParserTest extends InstrumentationTestCase {
+
+ private static final String TYPICAL_SUBRIP_FILE = "subrip/typical";
+ private static final String EMPTY_SUBRIP_FILE = "subrip/empty";
+
+ public void testParseNullSubripFile() throws IOException {
+ SubripParser parser = new SubripParser();
+ InputStream inputStream =
+ getInstrumentation().getContext().getResources().getAssets().open(EMPTY_SUBRIP_FILE);
+
+ try {
+ parser.parse(inputStream, C.UTF8_NAME, 0);
+ fail("Expected IOException");
+ } catch (IOException expected) {
+ // Do nothing.
+ }
+ }
+
+ public void testParseTypicalSubripFile() throws IOException {
+ SubripParser parser = new SubripParser();
+ InputStream inputStream =
+ getInstrumentation().getContext().getResources().getAssets().open(TYPICAL_SUBRIP_FILE);
+ SubripSubtitle subtitle = parser.parse(inputStream, C.UTF8_NAME, 0);
+
+ // test start time and event count
+ long startTimeUs = 0;
+ assertEquals(startTimeUs, subtitle.getStartTime());
+ assertEquals(4, subtitle.getEventTimeCount());
+
+ // test first cue
+ assertEquals(startTimeUs, subtitle.getEventTime(0));
+ assertEquals("This is the first subtitle.",
+ subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString());
+ assertEquals(startTimeUs + 1234000, subtitle.getEventTime(1));
+
+ // test second cue
+ assertEquals(startTimeUs + 2345000, subtitle.getEventTime(2));
+ assertEquals("This is the second subtitle.\nSecond subtitle with second line.",
+ subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString());
+ assertEquals(startTimeUs + 3456000, subtitle.getEventTime(3));
+ }
+
+}