WebVTT Parser
Took the WebVTT Media Parser and created the WebVTT Parser that will take in a file and output a stream of cues that will later be passed to another Media Handler that takes in cues and chunks them. Bug: 36138902 Change-Id: Ic77813fe19678e85d500269e69f46917510ab7ec
This commit is contained in:
parent
a8f46fcd2e
commit
4dcfe413f5
|
@ -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<CharReader> 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<CharReader> 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<CharReader> source)
|
||||||
|
: source_(std::move(source)) {}
|
||||||
|
|
||||||
|
bool BlockReader::Next(std::vector<std::string>* 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
|
|
@ -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 <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace shaka {
|
||||||
|
namespace media {
|
||||||
|
|
||||||
|
class CharReader {
|
||||||
|
public:
|
||||||
|
virtual bool Next(char* out) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PeekingCharReader : public CharReader {
|
||||||
|
public:
|
||||||
|
explicit PeekingCharReader(std::unique_ptr<CharReader> source);
|
||||||
|
|
||||||
|
bool Next(char* out) override;
|
||||||
|
bool Peek(char* out);
|
||||||
|
|
||||||
|
private:
|
||||||
|
PeekingCharReader(const PeekingCharReader&) = delete;
|
||||||
|
PeekingCharReader operator=(const PeekingCharReader&) = delete;
|
||||||
|
|
||||||
|
std::unique_ptr<CharReader> source_;
|
||||||
|
char cached_next_ = 0;
|
||||||
|
bool has_cached_next_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class LineReader {
|
||||||
|
public:
|
||||||
|
explicit LineReader(std::unique_ptr<CharReader> 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<CharReader> source);
|
||||||
|
|
||||||
|
bool Next(std::vector<std::string>* 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_
|
|
@ -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 <gtest/gtest.h>
|
||||||
|
|
||||||
|
#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<CharReader> 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<CharReader> 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<CharReader> 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<CharReader> 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<CharReader> 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<CharReader> source(new StringCharReader(input));
|
||||||
|
BlockReader reader(std::move(source));
|
||||||
|
|
||||||
|
std::vector<std::string> 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<CharReader> source(new StringCharReader(input));
|
||||||
|
BlockReader reader(std::move(source));
|
||||||
|
|
||||||
|
std::vector<std::string> 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<CharReader> source(new StringCharReader(input));
|
||||||
|
BlockReader reader(std::move(source));
|
||||||
|
|
||||||
|
std::vector<std::string> block;
|
||||||
|
ASSERT_FALSE(reader.Next(&block));
|
||||||
|
}
|
||||||
|
} // namespace media
|
||||||
|
} // namespace shaka
|
|
@ -15,8 +15,12 @@
|
||||||
'sources': [
|
'sources': [
|
||||||
'cue.cc',
|
'cue.cc',
|
||||||
'cue.h',
|
'cue.h',
|
||||||
|
'text_readers.cc',
|
||||||
|
'text_readers.h',
|
||||||
'webvtt_media_parser.cc',
|
'webvtt_media_parser.cc',
|
||||||
'webvtt_media_parser.h',
|
'webvtt_media_parser.h',
|
||||||
|
'webvtt_parser.cc',
|
||||||
|
'webvtt_parser.h',
|
||||||
'webvtt_sample_converter.cc',
|
'webvtt_sample_converter.cc',
|
||||||
'webvtt_sample_converter.h',
|
'webvtt_sample_converter.h',
|
||||||
'webvtt_timestamp.cc',
|
'webvtt_timestamp.cc',
|
||||||
|
@ -33,13 +37,16 @@
|
||||||
'target_name': 'webvtt_unittest',
|
'target_name': 'webvtt_unittest',
|
||||||
'type': '<(gtest_target_type)',
|
'type': '<(gtest_target_type)',
|
||||||
'sources': [
|
'sources': [
|
||||||
|
'text_readers_unittest.cc',
|
||||||
'webvtt_media_parser_unittest.cc',
|
'webvtt_media_parser_unittest.cc',
|
||||||
|
'webvtt_parser_unittest.cc',
|
||||||
'webvtt_sample_converter_unittest.cc',
|
'webvtt_sample_converter_unittest.cc',
|
||||||
'webvtt_timestamp_unittest.cc',
|
'webvtt_timestamp_unittest.cc',
|
||||||
],
|
],
|
||||||
'dependencies': [
|
'dependencies': [
|
||||||
'../../../testing/gmock.gyp:gmock',
|
'../../../testing/gmock.gyp:gmock',
|
||||||
'../../../testing/gtest.gyp:gtest',
|
'../../../testing/gtest.gyp:gtest',
|
||||||
|
'../../base/media_base.gyp:media_handler_test_base',
|
||||||
'../../test/media_test.gyp:media_test_support',
|
'../../test/media_test.gyp:media_test_support',
|
||||||
'webvtt',
|
'webvtt',
|
||||||
]
|
]
|
||||||
|
|
|
@ -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 <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#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<CharReader> 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<std::string> 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<std::string>& block) {
|
||||||
|
return ParseCue("", block.data(), block.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WebVttParser::ParseCueWithId(const std::vector<std::string>& 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<TextSample> sample(new TextSample);
|
||||||
|
sample->set_id(id);
|
||||||
|
|
||||||
|
const std::vector<std::string> 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<StreamInfo>(info));
|
||||||
|
}
|
||||||
|
} // namespace media
|
||||||
|
} // namespace shaka
|
|
@ -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 <stdint.h>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#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<CharReader> 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<std::string>& block);
|
||||||
|
bool ParseCueWithId(const std::vector<std::string>& 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_
|
|
@ -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 <gmock/gmock.h>
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#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<WebVttParser>(
|
||||||
|
std::unique_ptr<StringCharReader>(new StringCharReader(text_input)));
|
||||||
|
ASSERT_OK(MediaHandlerTestBase::SetUpAndInitializeGraph(
|
||||||
|
parser_, kInputCount, kOutputCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<OriginHandler> 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
|
Loading…
Reference in New Issue