/* * Copyright (C) 2016 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 androidx.media3.extractor.text.webvtt; import android.text.TextUtils; import androidx.annotation.Nullable; import androidx.media3.common.Format; import androidx.media3.common.Format.CueReplacementBehavior; import androidx.media3.common.ParserException; import androidx.media3.common.util.Consumer; import androidx.media3.common.util.ParsableByteArray; import androidx.media3.common.util.UnstableApi; import androidx.media3.extractor.text.CuesWithTiming; import androidx.media3.extractor.text.LegacySubtitleUtil; import androidx.media3.extractor.text.SubtitleParser; import java.util.ArrayList; import java.util.List; /** * A {@link SubtitleParser} for WebVTT. * *

See the WebVTT specification. */ @UnstableApi public final class WebvttParser implements SubtitleParser { /** * The {@link CueReplacementBehavior} for consecutive {@link CuesWithTiming} emitted by this * implementation. */ public static final @CueReplacementBehavior int CUE_REPLACEMENT_BEHAVIOR = Format.CUE_REPLACEMENT_BEHAVIOR_MERGE; private static final int EVENT_NONE = -1; private static final int EVENT_END_OF_FILE = 0; private static final int EVENT_COMMENT = 1; private static final int EVENT_STYLE_BLOCK = 2; private static final int EVENT_CUE = 3; private static final String COMMENT_START = "NOTE"; private static final String STYLE_START = "STYLE"; private final ParsableByteArray parsableWebvttData; private final WebvttCssParser cssParser; public WebvttParser() { parsableWebvttData = new ParsableByteArray(); cssParser = new WebvttCssParser(); } @Override public @CueReplacementBehavior int getCueReplacementBehavior() { return CUE_REPLACEMENT_BEHAVIOR; } @Override public void parse( byte[] data, int offset, int length, OutputOptions outputOptions, Consumer output) { parsableWebvttData.reset(data, /* limit= */ offset + length); parsableWebvttData.setPosition(offset); List definedStyles = new ArrayList<>(); // Validate the first line of the header, and skip the remainder. try { WebvttParserUtil.validateWebvttHeaderLine(parsableWebvttData); } catch (ParserException e) { throw new IllegalArgumentException(e); } while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {} int event; List cueInfos = new ArrayList<>(); while ((event = getNextEvent(parsableWebvttData)) != EVENT_END_OF_FILE) { if (event == EVENT_COMMENT) { skipComment(parsableWebvttData); } else if (event == EVENT_STYLE_BLOCK) { if (!cueInfos.isEmpty()) { throw new IllegalArgumentException("A style block was found after the first cue."); } parsableWebvttData.readLine(); // Consume the "STYLE" header. definedStyles.addAll(cssParser.parseBlock(parsableWebvttData)); } else if (event == EVENT_CUE) { @Nullable WebvttCueInfo cueInfo = WebvttCueParser.parseCue(parsableWebvttData, definedStyles); if (cueInfo != null) { cueInfos.add(cueInfo); } } } WebvttSubtitle subtitle = new WebvttSubtitle(cueInfos); LegacySubtitleUtil.toCuesWithTiming(subtitle, outputOptions, output); } /** * Positions the input right before the next event, and returns the kind of event found. Does not * consume any data from such event, if any. * * @return The kind of event found. */ private static int getNextEvent(ParsableByteArray parsableWebvttData) { int foundEvent = EVENT_NONE; int currentInputPosition = 0; while (foundEvent == EVENT_NONE) { currentInputPosition = parsableWebvttData.getPosition(); String line = parsableWebvttData.readLine(); if (line == null) { foundEvent = EVENT_END_OF_FILE; } else if (STYLE_START.equals(line)) { foundEvent = EVENT_STYLE_BLOCK; } else if (line.startsWith(COMMENT_START)) { foundEvent = EVENT_COMMENT; } else { foundEvent = EVENT_CUE; } } parsableWebvttData.setPosition(currentInputPosition); return foundEvent; } private static void skipComment(ParsableByteArray parsableWebvttData) { while (!TextUtils.isEmpty(parsableWebvttData.readLine())) {} } }