/* * 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.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.LongArray; import com.google.android.exoplayer.util.MimeTypes; import android.text.Html; import android.text.Spanned; import android.text.TextUtils; import android.util.Log; 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 SubRip parser. */ public final class SubripParser implements SubtitleParser { private static final String TAG = "SubripParser"; private static final Pattern SUBRIP_TIMING_LINE = Pattern.compile("(\\S*)\\s*-->\\s*(\\S*)"); private static final Pattern SUBRIP_TIMESTAMP = Pattern.compile("(?:(\\d+):)?(\\d+):(\\d+),(\\d+)"); private final StringBuilder textBuilder; private final boolean strictParsing; /** * Equivalent to {@code SubripParser(false)}. */ public SubripParser() { this(false); } /** * @param strictParsing If true, {@link #parse(InputStream)} will throw a {@link ParserException} * if the stream contains invalid data. If false, the parser will make a best effort to ignore * minor errors in the stream. Note however that a {@link ParserException} will still be * thrown when this is not possible. */ public SubripParser(boolean strictParsing) { this.strictParsing = strictParsing; textBuilder = new StringBuilder(); } @Override public SubripSubtitle parse(InputStream inputStream) throws IOException { ArrayList<Cue> cues = new ArrayList<>(); LongArray cueTimesUs = new LongArray(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, C.UTF8_NAME)); boolean haveEndTimecode; String currentLine; while ((currentLine = reader.readLine()) != null) { if (currentLine.length() == 0) { // Skip blank lines. continue; } // Parse the index line as a sanity check. try { Integer.parseInt(currentLine); } catch (NumberFormatException e) { if (!strictParsing) { Log.w(TAG, "Skipping invalid index: " + currentLine); continue; } else { throw new ParserException("Expected numeric counter: " + currentLine); } } // Read and parse the timing line. haveEndTimecode = false; currentLine = reader.readLine(); Matcher matcher = SUBRIP_TIMING_LINE.matcher(currentLine); if (matcher.find()) { cueTimesUs.add(parseTimecode(matcher.group(1))); String endTimecode = matcher.group(2); if (!TextUtils.isEmpty(endTimecode)) { haveEndTimecode = true; cueTimesUs.add(parseTimecode(matcher.group(2))); } } else if (!strictParsing) { Log.w(TAG, "Skipping invalid timing: " + currentLine); continue; } else { throw new ParserException("Expected timing line: " + currentLine); } // Read and parse the text. textBuilder.setLength(0); while (!TextUtils.isEmpty(currentLine = reader.readLine())) { if (textBuilder.length() > 0) { textBuilder.append("<br>"); } textBuilder.append(currentLine.trim()); } Spanned text = Html.fromHtml(textBuilder.toString()); cues.add(new Cue(text)); if (haveEndTimecode) { cues.add(null); } } Cue[] cuesArray = new Cue[cues.size()]; cues.toArray(cuesArray); long[] cueTimesUsArray = cueTimesUs.toArray(); return new SubripSubtitle(cuesArray, cueTimesUsArray); } @Override public boolean canParse(String mimeType) { return MimeTypes.APPLICATION_SUBRIP.equals(mimeType); } private static long parseTimecode(String s) throws NumberFormatException { Matcher matcher = SUBRIP_TIMESTAMP.matcher(s); if (!matcher.matches()) { throw new NumberFormatException("has invalid format"); } long timestampMs = Long.parseLong(matcher.group(1)) * 60 * 60 * 1000; timestampMs += Long.parseLong(matcher.group(2)) * 60 * 1000; timestampMs += Long.parseLong(matcher.group(3)) * 1000; timestampMs += Long.parseLong(matcher.group(4)); return timestampMs * 1000; } }