diff --git a/packager/media/formats/webvtt/text_readers.cc b/packager/media/formats/webvtt/text_readers.cc new file mode 100644 index 0000000000..9d77f4c5f3 --- /dev/null +++ b/packager/media/formats/webvtt/text_readers.cc @@ -0,0 +1,103 @@ +// Copyright 2017 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "packager/media/formats/webvtt/text_readers.h" + +#include "packager/base/logging.h" + +namespace shaka { +namespace media { + +PeekingCharReader::PeekingCharReader(std::unique_ptr source) + : source_(std::move(source)) {} + +bool PeekingCharReader::Next(char* out) { + DCHECK(out); + if (Peek(out)) { + has_cached_next_ = false; + return true; + } + return false; +} + +bool PeekingCharReader::Peek(char* out) { + DCHECK(out); + if (!has_cached_next_ && source_->Next(&cached_next_)) { + has_cached_next_ = true; + } + if (has_cached_next_) { + *out = cached_next_; + return true; + } + return false; +} + +LineReader::LineReader(std::unique_ptr source) + : source_(std::move(source)) {} + +// Split lines based on https://w3c.github.io/webvtt/#webvtt-line-terminator +bool LineReader::Next(std::string* out) { + DCHECK(out); + out->clear(); + bool read_something = false; + char now; + while (source_.Next(&now)) { + read_something = true; + // handle \n + if (now == '\n') { + break; + } + // handle \r and \r\n + if (now == '\r') { + char next; + if (source_.Peek(&next) && next == '\n') { + source_.Next(&next); // Read in the '\n' that was just seen via |Peek| + } + break; + } + out->push_back(now); + } + return read_something; +} + +BlockReader::BlockReader(std::unique_ptr source) + : source_(std::move(source)) {} + +bool BlockReader::Next(std::vector* out) { + DCHECK(out); + + out->clear(); + + bool in_block = false; + + // Read through lines until a non-empty line is found. With a non-empty + // line is found, start adding the lines to the output and once an empty + // line if found again, stop adding lines and exit. + std::string line; + while (source_.Next(&line)) { + if (in_block && line.empty()) { + break; + } + if (in_block || !line.empty()) { + out->push_back(line); + in_block = true; + } + } + + return in_block; +} + +StringCharReader::StringCharReader(const std::string& str) : source_(str) {} + +bool StringCharReader::Next(char* out) { + if (pos_ < source_.length()) { + *out = source_[pos_++]; + return true; + } + return false; +} +} // namespace media +} // namespace shaka diff --git a/packager/media/formats/webvtt/text_readers.h b/packager/media/formats/webvtt/text_readers.h new file mode 100644 index 0000000000..12c219e89b --- /dev/null +++ b/packager/media/formats/webvtt/text_readers.h @@ -0,0 +1,81 @@ +// Copyright 2017 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef PACKAGER_MEDIA_FORMATS_WEBVTT_TEXT_READERS_H_ +#define PACKAGER_MEDIA_FORMATS_WEBVTT_TEXT_READERS_H_ + +#include +#include +#include + +namespace shaka { +namespace media { + +class CharReader { + public: + virtual bool Next(char* out) = 0; +}; + +class PeekingCharReader : public CharReader { + public: + explicit PeekingCharReader(std::unique_ptr source); + + bool Next(char* out) override; + bool Peek(char* out); + + private: + PeekingCharReader(const PeekingCharReader&) = delete; + PeekingCharReader operator=(const PeekingCharReader&) = delete; + + std::unique_ptr source_; + char cached_next_ = 0; + bool has_cached_next_ = false; +}; + +class LineReader { + public: + explicit LineReader(std::unique_ptr source); + + bool Next(std::string* out); + + private: + LineReader(const LineReader&) = delete; + LineReader operator=(const LineReader&) = delete; + + PeekingCharReader source_; +}; + +class BlockReader { + public: + explicit BlockReader(std::unique_ptr source); + + bool Next(std::vector* out); + + private: + BlockReader(const BlockReader&) = delete; + BlockReader operator=(const BlockReader&) = delete; + + LineReader source_; +}; + +class StringCharReader : public CharReader { + public: + explicit StringCharReader(const std::string& str); + + bool Next(char* out) override; + + private: + StringCharReader(const StringCharReader&) = delete; + StringCharReader& operator=(const StringCharReader&) = delete; + + const std::string source_; + size_t pos_ = 0; +}; + +} // namespace media +} // namespace shaka + +#endif // MEDIA_FORMATS_WEBVTT_TEXT_READERS_H_ diff --git a/packager/media/formats/webvtt/text_readers_unittest.cc b/packager/media/formats/webvtt/text_readers_unittest.cc new file mode 100644 index 0000000000..a399433e60 --- /dev/null +++ b/packager/media/formats/webvtt/text_readers_unittest.cc @@ -0,0 +1,169 @@ +// Copyright 2017 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include + +#include "packager/media/formats/webvtt/text_readers.h" + +namespace shaka { +namespace media { + +TEST(TextReadersTest, ReadWholeStream) { + const std::string input = "abcd"; + StringCharReader source(input); + + char c; + ASSERT_TRUE(source.Next(&c)); + ASSERT_EQ(c, 'a'); + ASSERT_TRUE(source.Next(&c)); + ASSERT_EQ(c, 'b'); + ASSERT_TRUE(source.Next(&c)); + ASSERT_EQ(c, 'c'); + ASSERT_TRUE(source.Next(&c)); + ASSERT_EQ(c, 'd'); + ASSERT_FALSE(source.Next(&c)); +} + +TEST(TextReadersTest, Peeking) { + const std::string input = "abc"; + std::unique_ptr source(new StringCharReader(input)); + PeekingCharReader reader(std::move(source)); + + char c; + ASSERT_TRUE(reader.Peek(&c)); + ASSERT_EQ(c, 'a'); + ASSERT_TRUE(reader.Next(&c)); + ASSERT_EQ(c, 'a'); + ASSERT_TRUE(reader.Peek(&c)); + ASSERT_EQ(c, 'b'); + ASSERT_TRUE(reader.Next(&c)); + ASSERT_EQ(c, 'b'); + ASSERT_TRUE(reader.Peek(&c)); + ASSERT_EQ(c, 'c'); + ASSERT_TRUE(reader.Next(&c)); + ASSERT_EQ(c, 'c'); + ASSERT_FALSE(reader.Peek(&c)); + ASSERT_FALSE(reader.Next(&c)); +} + +TEST(TextReadersTest, ReadLinesWithNewLine) { + const std::string input = "a\nb\nc"; + std::unique_ptr source(new StringCharReader(input)); + LineReader reader(std::move(source)); + + std::string s; + ASSERT_TRUE(reader.Next(&s)); + ASSERT_EQ(s, "a"); + ASSERT_TRUE(reader.Next(&s)); + ASSERT_EQ(s, "b"); + ASSERT_TRUE(reader.Next(&s)); + ASSERT_EQ(s, "c"); + ASSERT_FALSE(reader.Next(&s)); +} + +TEST(TextReadersTest, ReadLinesWithReturnsAndNewLine) { + const std::string input = "a\r\nb\r\nc"; + std::unique_ptr source(new StringCharReader(input)); + LineReader reader(std::move(source)); + + std::string s; + ASSERT_TRUE(reader.Next(&s)); + ASSERT_EQ(s, "a"); + ASSERT_TRUE(reader.Next(&s)); + ASSERT_EQ(s, "b"); + ASSERT_TRUE(reader.Next(&s)); + ASSERT_EQ(s, "c"); + ASSERT_FALSE(reader.Next(&s)); +} + +TEST(TextReadersTest, ReadLinesWithNewLineAndReturns) { + const std::string input = "a\n\rb\n\rc"; + std::unique_ptr source(new StringCharReader(input)); + LineReader reader(std::move(source)); + + std::string s; + ASSERT_TRUE(reader.Next(&s)); + ASSERT_EQ(s, "a"); + ASSERT_TRUE(reader.Next(&s)); + ASSERT_EQ(s, ""); + ASSERT_TRUE(reader.Next(&s)); + ASSERT_EQ(s, "b"); + ASSERT_TRUE(reader.Next(&s)); + ASSERT_EQ(s, ""); + ASSERT_TRUE(reader.Next(&s)); + ASSERT_EQ(s, "c"); + ASSERT_FALSE(reader.Next(&s)); +} + +TEST(TextReadersTest, ReadLinesWithReturnAtEnd) { + const std::string input = "a\r\nb\r\nc\r"; + std::unique_ptr source(new StringCharReader(input)); + LineReader reader(std::move(source)); + + std::string s; + ASSERT_TRUE(reader.Next(&s)); + ASSERT_EQ(s, "a"); + ASSERT_TRUE(reader.Next(&s)); + ASSERT_EQ(s, "b"); + ASSERT_TRUE(reader.Next(&s)); + ASSERT_EQ(s, "c"); + ASSERT_FALSE(reader.Next(&s)); +} + +TEST(TextReadersTest, ReadBlocksReadMultilineBlock) { + const std::string input = + "block 1 - line 1\n" + "block 1 - line 2"; + + std::unique_ptr source(new StringCharReader(input)); + BlockReader reader(std::move(source)); + + std::vector block; + + ASSERT_TRUE(reader.Next(&block)); + ASSERT_EQ(2u, block.size()); + ASSERT_EQ("block 1 - line 1", block[0]); + ASSERT_EQ("block 1 - line 2", block[1]); + + ASSERT_FALSE(reader.Next(&block)); +} + +TEST(TextReadersTest, ReadBlocksSkipBlankLinesBeforeBlocks) { + const std::string input = + "\n" + "\n" + "block 1\n" + "\n" + "\n" + "block 2\n"; + + std::unique_ptr source(new StringCharReader(input)); + BlockReader reader(std::move(source)); + + std::vector block; + + ASSERT_TRUE(reader.Next(&block)); + ASSERT_EQ(1u, block.size()); + ASSERT_EQ("block 1", block[0]); + + ASSERT_TRUE(reader.Next(&block)); + ASSERT_EQ(1u, block.size()); + ASSERT_EQ("block 2", block[0]); + + ASSERT_FALSE(reader.Next(&block)); +} + +TEST(TextReadersTest, ReadBlocksWithOnlyBlankLines) { + const std::string input = "\n\n\n\n"; + + std::unique_ptr source(new StringCharReader(input)); + BlockReader reader(std::move(source)); + + std::vector block; + ASSERT_FALSE(reader.Next(&block)); +} +} // namespace media +} // namespace shaka diff --git a/packager/media/formats/webvtt/webvtt.gyp b/packager/media/formats/webvtt/webvtt.gyp index 46864452d3..337d0a7c26 100644 --- a/packager/media/formats/webvtt/webvtt.gyp +++ b/packager/media/formats/webvtt/webvtt.gyp @@ -15,8 +15,12 @@ 'sources': [ 'cue.cc', 'cue.h', + 'text_readers.cc', + 'text_readers.h', 'webvtt_media_parser.cc', 'webvtt_media_parser.h', + 'webvtt_parser.cc', + 'webvtt_parser.h', 'webvtt_sample_converter.cc', 'webvtt_sample_converter.h', 'webvtt_timestamp.cc', @@ -33,13 +37,16 @@ 'target_name': 'webvtt_unittest', 'type': '<(gtest_target_type)', 'sources': [ + 'text_readers_unittest.cc', 'webvtt_media_parser_unittest.cc', + 'webvtt_parser_unittest.cc', 'webvtt_sample_converter_unittest.cc', 'webvtt_timestamp_unittest.cc', ], 'dependencies': [ '../../../testing/gmock.gyp:gmock', '../../../testing/gtest.gyp:gtest', + '../../base/media_base.gyp:media_handler_test_base', '../../test/media_test.gyp:media_test_support', 'webvtt', ] diff --git a/packager/media/formats/webvtt/webvtt_parser.cc b/packager/media/formats/webvtt/webvtt_parser.cc new file mode 100644 index 0000000000..9b285d7b9d --- /dev/null +++ b/packager/media/formats/webvtt/webvtt_parser.cc @@ -0,0 +1,204 @@ +// Copyright 2017 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "packager/media/formats/webvtt/webvtt_parser.h" + +#include +#include + +#include "packager/base/logging.h" +#include "packager/base/strings/string_split.h" +#include "packager/base/strings/string_util.h" +#include "packager/media/base/text_stream_info.h" +#include "packager/media/formats/webvtt/webvtt_timestamp.h" + +namespace shaka { +namespace media { +namespace { + +// Comments are just blocks that are preceded by a blank line, start with the +// word "NOTE" (followed by a space or newline), and end at the first blank +// line. +// SOURCE: https://www.w3.org/TR/webvtt1 +bool IsLikelyNote(const std::string& line) { + return line == "NOTE" || + base::StartsWith(line, "NOTE ", base::CompareCase::SENSITIVE) || + base::StartsWith(line, "NOTE\t", base::CompareCase::SENSITIVE); +} + +// As cue time is the only part of a WEBVTT file that is allowed to have +// "-->" appear, then if the given line contains it, we can safely assume +// that the line is likely to be a cue time. +bool IsLikelyCueTiming(const std::string& line) { + return line.find("-->") != std::string::npos; +} + +// A WebVTT cue identifier is any sequence of one or more characters not +// containing the substring "-->" (U+002D HYPHEN-MINUS, U+002D HYPHEN-MINUS, +// U+003E GREATER-THAN SIGN), nor containing any U+000A LINE FEED (LF) +// characters or U+000D CARRIAGE RETURN (CR) characters. +// SOURCE: https://www.w3.org/TR/webvtt1/#webvtt-cue-identifier +bool MaybeCueId(const std::string& line) { + return line.find("-->") == std::string::npos; +} +} // namespace + +WebVttParser::WebVttParser(std::unique_ptr source) + : reader_(std::move(source)) {} + +Status WebVttParser::InitializeInternal() { + return Status::OK; +} + +bool WebVttParser::ValidateOutputStreamIndex(size_t stream_index) const { + // Only support one output + return stream_index == 0; +} + +Status WebVttParser::Run() { + return Parse() + ? FlushDownstream(0) + : Status(error::INTERNAL_ERROR, + "Failed to parse WebVTT source. See log for details."); +} + +void WebVttParser::Cancel() { + keep_reading_ = false; +} + +bool WebVttParser::Parse() { + std::vector block; + if (!reader_.Next(&block)) { + LOG(ERROR) << "Failed to read WEBVTT HEADER - No blocks in source."; + return false; + } + + // Check the header. It is possible for a 0xFEFF BOM to come before the + // header text. + if (block.size() != 1) { + LOG(ERROR) << "Failed to read WEBVTT header - " + << "block size should be 1 but was " << block.size() << "."; + return false; + } + if (block[0] != "WEBVTT" && block[0] != "\xFE\xFFWEBVTT") { + LOG(ERROR) << "Failed to read WEBVTT header - should be WEBVTT but was " + << block[0]; + return false; + } + + const Status send_stream_info_result = DispatchTextStreamInfo(); + + if (send_stream_info_result != Status::OK) { + LOG(ERROR) << "Failed to send stream info down stream:" + << send_stream_info_result.error_message(); + return false; + } + + while (reader_.Next(&block) && keep_reading_) { + // NOTE + if (IsLikelyNote(block[0])) { + // We can safely ignore the whole block. + continue; + } + + // CUE with ID + if (block.size() > 2 && MaybeCueId(block[0]) && + IsLikelyCueTiming(block[1]) && ParseCueWithId(block)) { + continue; + } + + // CUE with no ID + if (block.size() > 1 && IsLikelyCueTiming(block[0]) && + ParseCueWithNoId(block)) { + continue; + } + + LOG(ERROR) << "Failed to determine block classification:"; + LOG(ERROR) << " --- BLOCK START ---"; + for (const std::string& line : block) { + LOG(ERROR) << " " << line; + } + LOG(ERROR) << " --- BLOCK END ---"; + return false; + } + + return keep_reading_; +} + +bool WebVttParser::ParseCueWithNoId(const std::vector& block) { + return ParseCue("", block.data(), block.size()); +} + +bool WebVttParser::ParseCueWithId(const std::vector& block) { + return ParseCue(block[0], block.data() + 1, block.size() - 1); +} + +bool WebVttParser::ParseCue(const std::string& id, + const std::string* block, + size_t block_size) { + std::shared_ptr sample(new TextSample); + sample->set_id(id); + + const std::vector time_and_style = base::SplitString( + block[0], " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); + + uint64_t start_time; + uint64_t end_time; + if (time_and_style.size() >= 3 && time_and_style[1] == "-->" && + WebVttTimestampToMs(time_and_style[0], &start_time) && + WebVttTimestampToMs(time_and_style[2], &end_time)) { + sample->SetTime(start_time, end_time); + } else { + LOG(ERROR) << "Could not parse start time, -->, and end time from " + << block[0]; + return false; + } + + // The rest of time_and_style are the style tokens. + for (size_t i = 3; i < time_and_style.size(); i++) { + sample->AppendStyle(time_and_style[i]); + } + + // The rest of the block is the payload. + for (size_t i = 1; i < block_size; i++) { + sample->AppendPayload(block[i]); + } + + const Status send_result = DispatchTextSample(0, sample); + + if (send_result != Status::OK) { + LOG(ERROR) << "Failed to send text sample down stream:" + << send_result.error_message(); + } + + return send_result == Status::OK; +} + +Status WebVttParser::DispatchTextStreamInfo() { + // The resolution of timings are in milliseconds. + const int kTimescale = 1000; + + // The duration passed here is not very important. Also the whole file + // must be read before determining the real duration which doesn't + // work nicely with the current demuxer. + const int kDuration = 0; + + // There is no one metadata to determine what the language is. Parts + // of the text may be annotated as some specific language. + const char kLanguage[] = ""; + + const char kWebVttCodecString[] = "wvtt"; + + StreamInfo* info = new TextStreamInfo(0, kTimescale, kDuration, kCodecWebVtt, + kWebVttCodecString, "", + 0, // width + 0, // height + kLanguage); + + return DispatchStreamInfo(0, std::shared_ptr(info)); +} +} // namespace media +} // namespace shaka diff --git a/packager/media/formats/webvtt/webvtt_parser.h b/packager/media/formats/webvtt/webvtt_parser.h new file mode 100644 index 0000000000..5a532009dc --- /dev/null +++ b/packager/media/formats/webvtt/webvtt_parser.h @@ -0,0 +1,51 @@ +// Copyright 2017 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef PACKAGER_MEDIA_FORMATS_WEBVTT_WEBVTT_PARSER_H_ +#define PACKAGER_MEDIA_FORMATS_WEBVTT_WEBVTT_PARSER_H_ + +#include + +#include + +#include "packager/media/formats/webvtt/text_readers.h" +#include "packager/media/origin/origin_handler.h" + +namespace shaka { +namespace media { + +// Used to parse a WebVTT source into Cues that will be sent downstream. +class WebVttParser : public OriginHandler { + public: + explicit WebVttParser(std::unique_ptr source); + + Status Run() override; + void Cancel() override; + + private: + WebVttParser(const WebVttParser&) = delete; + WebVttParser& operator=(const WebVttParser&) = delete; + + Status InitializeInternal() override; + bool ValidateOutputStreamIndex(size_t stream_index) const override; + + bool Parse(); + bool ParseCueWithNoId(const std::vector& block); + bool ParseCueWithId(const std::vector& block); + bool ParseCue(const std::string& id, + const std::string* block, + size_t block_size); + + Status DispatchTextStreamInfo(); + + BlockReader reader_; + bool keep_reading_ = true; +}; + +} // namespace media +} // namespace shaka + +#endif // MEDIA_FORMATS_WEBVTT_WEBVTT_PARSER_H_ diff --git a/packager/media/formats/webvtt/webvtt_parser_unittest.cc b/packager/media/formats/webvtt/webvtt_parser_unittest.cc new file mode 100644 index 0000000000..10c289fad9 --- /dev/null +++ b/packager/media/formats/webvtt/webvtt_parser_unittest.cc @@ -0,0 +1,275 @@ +// Copyright 2017 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include +#include + +#include "packager/media/base/media_handler_test_base.h" +#include "packager/media/formats/webvtt/text_readers.h" +#include "packager/media/formats/webvtt/webvtt_parser.h" +#include "packager/status_test_util.h" + +namespace shaka { +namespace media { +namespace { +const size_t kInputCount = 0; +const size_t kOutputCount = 1; +const size_t kOutputIndex = 0; + +const size_t kStreamIndex = 0; +const size_t kTimeScale = 1000; +const bool kEncrypted = true; + +const char* kNoId = ""; +const char* kNoSettings = ""; +} // namespace + +class WebVttParserTest : public MediaHandlerTestBase { + protected: + void SetUpAndInitializeGraph(const std::string& text_input) { + parser_ = std::make_shared( + std::unique_ptr(new StringCharReader(text_input))); + ASSERT_OK(MediaHandlerTestBase::SetUpAndInitializeGraph( + parser_, kInputCount, kOutputCount)); + } + + std::shared_ptr parser_; +}; + +TEST_F(WebVttParserTest, FailToParseEmptyFile) { + SetUpAndInitializeGraph(""); + + EXPECT_CALL(*Output(kOutputIndex), OnProcess(testing::_)).Times(0); + EXPECT_CALL(*Output(kOutputIndex), OnFlush(testing::_)).Times(0); + + ASSERT_NE(Status::OK, parser_->Run()); +} + +TEST_F(WebVttParserTest, ParseOnlyHeader) { + SetUpAndInitializeGraph( + "WEBVTT\n" + "\n"); + + { + testing::InSequence s; + EXPECT_CALL(*Output(kOutputIndex), + OnProcess(IsStreamInfo(kStreamIndex, kTimeScale, !kEncrypted))); + EXPECT_CALL(*Output(kOutputIndex), OnFlush(kStreamIndex)); + } + + ASSERT_OK(parser_->Run()); +} + +TEST_F(WebVttParserTest, ParseHeaderWithBOM) { + SetUpAndInitializeGraph( + "\xFE\xFFWEBVTT\n" + "\n"); + + { + testing::InSequence s; + EXPECT_CALL(*Output(kOutputIndex), + OnProcess(IsStreamInfo(kStreamIndex, kTimeScale, !kEncrypted))); + EXPECT_CALL(*Output(kOutputIndex), OnFlush(kStreamIndex)); + } + + ASSERT_OK(parser_->Run()); +} + +TEST_F(WebVttParserTest, FailToParseHeaderWrongWord) { + SetUpAndInitializeGraph( + "NOT WEBVTT\n" + "\n"); + + EXPECT_CALL(*Output(kOutputIndex), OnProcess(testing::_)).Times(0); + EXPECT_CALL(*Output(kOutputIndex), OnFlush(testing::_)).Times(0); + + ASSERT_NE(Status::OK, parser_->Run()); +} + +TEST_F(WebVttParserTest, FailToParseHeaderNotOneLine) { + SetUpAndInitializeGraph( + "WEBVTT\n" + "WEBVTT\n" + "\n"); + + EXPECT_CALL(*Output(kOutputIndex), OnProcess(testing::_)).Times(0); + EXPECT_CALL(*Output(kOutputIndex), OnFlush(testing::_)).Times(0); + + ASSERT_NE(Status::OK, parser_->Run()); +} + +// TODO: Add style blocks support to WebVttParser. +// This test is disabled until WebVTT parses STYLE blocks. +TEST_F(WebVttParserTest, DISABLED_ParseStyleBlocks) { + SetUpAndInitializeGraph( + "WEBVTT\n" + "\n" + "STYLE\n" + "::cue {\n" + " background-image: linear-gradient(to bottom, dimgray, lightgray);\n" + " color: papayawhip;\n" + "}"); + + { + testing::InSequence s; + EXPECT_CALL(*Output(kOutputIndex), + OnProcess(IsStreamInfo(kStreamIndex, kTimeScale, !kEncrypted))); + EXPECT_CALL(*Output(kOutputIndex), OnFlush(kStreamIndex)); + } + + ASSERT_OK(parser_->Run()); +} + +TEST_F(WebVttParserTest, ParseOneCue) { + SetUpAndInitializeGraph( + "WEBVTT\n" + "\n" + "00:01:00.000 --> 01:00:00.000\n" + "subtitle\n"); + + { + testing::InSequence s; + EXPECT_CALL(*Output(kOutputIndex), + OnProcess(IsStreamInfo(kStreamIndex, kTimeScale, !kEncrypted))); + EXPECT_CALL(*Output(kOutputIndex), + OnProcess(IsTextSample(kNoId, 60000u, 3600000u, kNoSettings, + "subtitle"))); + EXPECT_CALL(*Output(kOutputIndex), OnFlush(kStreamIndex)); + } + + ASSERT_OK(parser_->Run()); +} + +TEST_F(WebVttParserTest, FailToParseCueWithArrowInId) { + SetUpAndInitializeGraph( + "WEBVTT\n" + "\n" + "-->\n" + "00:01:00.000 --> 01:00:00.000\n" + "subtitle\n"); + + ASSERT_NE(Status::OK, parser_->Run()); +} + +TEST_F(WebVttParserTest, ParseOneCueWithId) { + SetUpAndInitializeGraph( + "WEBVTT\n" + "\n" + "id\n" + "00:01:00.000 --> 01:00:00.000\n" + "subtitle\n"); + + { + testing::InSequence s; + EXPECT_CALL(*Output(kOutputIndex), + OnProcess(IsStreamInfo(kStreamIndex, kTimeScale, !kEncrypted))); + EXPECT_CALL(*Output(kOutputIndex), + OnProcess(IsTextSample("id", 60000u, 3600000u, kNoSettings, + "subtitle"))); + EXPECT_CALL(*Output(kOutputIndex), OnFlush(kStreamIndex)); + } + + ASSERT_OK(parser_->Run()); +} + +TEST_F(WebVttParserTest, ParseOneCueWithSettings) { + SetUpAndInitializeGraph( + "WEBVTT\n" + "\n" + "00:01:00.000 --> 01:00:00.000 size:50%\n" + "subtitle\n"); + + { + testing::InSequence s; + EXPECT_CALL(*Output(kOutputIndex), + OnProcess(IsStreamInfo(kStreamIndex, kTimeScale, !kEncrypted))); + EXPECT_CALL(*Output(kOutputIndex), + OnProcess(IsTextSample(kNoId, 60000u, 3600000u, "size:50%", + "subtitle"))); + EXPECT_CALL(*Output(kOutputIndex), OnFlush(kStreamIndex)); + } + + ASSERT_OK(parser_->Run()); +} + +// Verify that a typical case with mulitple cues work. +TEST_F(WebVttParserTest, ParseMultipleCues) { + SetUpAndInitializeGraph( + "WEBVTT\n" + "\n" + "00:00:01.000 --> 00:00:05.200\n" + "subtitle A\n" + "\n" + "00:00:02.321 --> 00:00:07.000\n" + "subtitle B\n" + "\n" + "00:00:05.800 --> 00:00:08.000\n" + "subtitle C\n"); + + { + testing::InSequence s; + EXPECT_CALL(*Output(kOutputIndex), + OnProcess(IsStreamInfo(kStreamIndex, kTimeScale, !kEncrypted))); + EXPECT_CALL(*Output(kOutputIndex), + OnProcess(IsTextSample(kNoId, 1000u, 5200u, kNoSettings, + "subtitle A"))); + EXPECT_CALL(*Output(kOutputIndex), + OnProcess(IsTextSample(kNoId, 2321u, 7000u, kNoSettings, + "subtitle B"))); + EXPECT_CALL(*Output(kOutputIndex), + OnProcess(IsTextSample(kNoId, 5800u, 8000u, kNoSettings, + "subtitle C"))); + EXPECT_CALL(*Output(kOutputIndex), OnFlush(kStreamIndex)); + } + + ASSERT_OK(parser_->Run()); +} + +// Verify that a typical case with mulitple cues work even when comments are +// present. +TEST_F(WebVttParserTest, ParseWithComments) { + SetUpAndInitializeGraph( + "WEBVTT\n" + "\n" + "NOTE This is a one line comment\n" + "\n" + "00:00:01.000 --> 00:00:05.200\n" + "subtitle A\n" + "\n" + "NOTE\n" + "This is a multi-line comment\n" + "\n" + "00:00:02.321 --> 00:00:07.000\n" + "subtitle B\n" + "\n" + "NOTE This is a single line comment that\n" + "spans two lines\n" + "\n" + "NOTE\tThis is a comment that using a tab\n" + "\n" + "00:00:05.800 --> 00:00:08.000\n" + "subtitle C\n"); + + { + testing::InSequence s; + EXPECT_CALL(*Output(kOutputIndex), + OnProcess(IsStreamInfo(kStreamIndex, kTimeScale, !kEncrypted))); + EXPECT_CALL(*Output(kOutputIndex), + OnProcess(IsTextSample(kNoId, 1000u, 5200u, kNoSettings, + "subtitle A"))); + EXPECT_CALL(*Output(kOutputIndex), + OnProcess(IsTextSample(kNoId, 2321u, 7000u, kNoSettings, + "subtitle B"))); + EXPECT_CALL(*Output(kOutputIndex), + OnProcess(IsTextSample(kNoId, 5800u, 8000u, kNoSettings, + "subtitle C"))); + EXPECT_CALL(*Output(kOutputIndex), OnFlush(kStreamIndex)); + } + + ASSERT_OK(parser_->Run()); +} +} // namespace media +} // namespace shaka