7#include <packager/media/formats/webvtt/webvtt_parser.h>
16#include <absl/log/check.h>
17#include <absl/log/log.h>
18#include <absl/strings/ascii.h>
19#include <absl/strings/match.h>
20#include <absl/strings/numbers.h>
22#include <packager/kv_pairs/kv_pairs.h>
23#include <packager/media/base/media_parser.h>
24#include <packager/media/base/stream_info.h>
25#include <packager/media/base/text_sample.h>
26#include <packager/media/base/text_stream_info.h>
27#include <packager/media/formats/webvtt/webvtt_utils.h>
28#include <packager/utils/string_trim_split.h>
34const uint64_t kStreamIndex = 0;
36std::string BlockToString(
const std::string* block,
size_t size) {
37 std::string out =
" --- BLOCK START ---\n";
39 for (
size_t i = 0; i < size; i++) {
45 out.append(
" --- BLOCK END ---");
54bool IsLikelyNote(
const std::string& line) {
55 return line ==
"NOTE" || absl::StartsWith(line,
"NOTE ") ||
56 absl::StartsWith(line,
"NOTE\t");
62bool IsLikelyCueTiming(
const std::string& line) {
63 return line.find(
"-->") != std::string::npos;
71bool MaybeCueId(
const std::string& line) {
72 return line.find(
"-->") == std::string::npos;
79bool IsLikelyStyle(
const std::string& line) {
80 return absl::StripTrailingAsciiWhitespace(line) ==
"STYLE";
87bool IsLikelyRegion(
const std::string& line) {
88 return absl::StripTrailingAsciiWhitespace(line) ==
"REGION";
91bool ParsePercent(
const std::string& str,
float* value) {
94 if (str[str.size() - 1] !=
'%') {
99 if (!absl::SimpleAtod(str.substr(0, str.size() - 1), &temp) || temp > 100) {
106bool ParseDoublePercent(
const std::string& str,
float* a,
float* b) {
107 std::vector<std::string> percents = SplitAndTrimSkipEmpty(str,
',');
109 if (percents.size() != 2) {
112 float temp_a, temp_b;
113 if (!ParsePercent(percents[0], &temp_a) ||
114 !ParsePercent(percents[1], &temp_b)) {
122void ParseSettings(
const std::string&
id,
123 const std::string& value,
124 TextSettings* settings) {
126 if (
id ==
"region") {
127 settings->region = value;
128 }
else if (
id ==
"vertical") {
130 settings->writing_direction = WritingDirection::kVerticalGrowingLeft;
131 }
else if (value ==
"lr") {
132 settings->writing_direction = WritingDirection::kVerticalGrowingRight;
134 LOG(WARNING) <<
"Invalid WebVTT vertical setting: " << value;
136 }
else if (
id ==
"line") {
137 const auto pos = value.find(
',');
138 const std::string line = value.substr(0, pos);
139 const std::string align =
140 pos != std::string::npos ? value.substr(pos + 1) :
"";
141 if (pos != std::string::npos) {
142 LOG(WARNING) <<
"WebVTT line alignment isn't supported";
145 if (!line.empty() && line[line.size() - 1] ==
'%') {
147 if (!ParsePercent(line, &temp)) {
148 LOG(WARNING) <<
"Invalid WebVTT line: " << value;
151 settings->line.emplace(temp, TextUnitType::kPercent);
154 if (!absl::SimpleAtod(line, &temp)) {
155 LOG(WARNING) <<
"Invalid WebVTT line: " << value;
158 settings->line.emplace(temp, TextUnitType::kLines);
160 }
else if (
id ==
"position") {
161 const auto pos = value.find(
',');
162 const std::string position = value.substr(0, pos);
163 const std::string align =
164 pos != std::string::npos ? value.substr(pos + 1) :
"";
165 if (pos != std::string::npos) {
166 LOG(WARNING) <<
"WebVTT position alignment isn't supported";
170 if (ParsePercent(position, &temp)) {
171 settings->position.emplace(temp, TextUnitType::kPercent);
173 LOG(WARNING) <<
"Invalid WebVTT position: " << value;
175 }
else if (
id ==
"size") {
177 if (ParsePercent(value, &temp)) {
178 settings->width.emplace(temp, TextUnitType::kPercent);
180 LOG(WARNING) <<
"Invalid WebVTT size: " << value;
182 }
else if (
id ==
"align") {
183 if (value ==
"start") {
184 settings->text_alignment = TextAlignment::kStart;
185 }
else if (value ==
"center" || value ==
"middle") {
186 settings->text_alignment = TextAlignment::kCenter;
187 }
else if (value ==
"end") {
188 settings->text_alignment = TextAlignment::kEnd;
189 }
else if (value ==
"left") {
190 settings->text_alignment = TextAlignment::kLeft;
191 }
else if (value ==
"right") {
192 settings->text_alignment = TextAlignment::kRight;
194 LOG(WARNING) <<
"Invalid WebVTT align: " << value;
197 LOG(WARNING) <<
"Unknown WebVTT setting: " << id;
203WebVttParser::WebVttParser() {}
209 DCHECK(init_cb_ ==
nullptr);
210 DCHECK(init_cb !=
nullptr);
211 DCHECK(new_text_sample_cb !=
nullptr);
212 DCHECK(!decryption_key_source) <<
"Encrypted WebVTT not supported";
215 new_text_sample_cb_ = new_text_sample_cb;
223bool WebVttParser::Parse(
const uint8_t* buf,
int size) {
228bool WebVttParser::Parse() {
230 std::vector<std::string> block;
231 if (!reader_.
Next(&block)) {
237 if (block.size() != 1) {
238 LOG(WARNING) <<
"Failed to read WEBVTT header - "
239 <<
"block size should be 1 but was " << block.size() <<
".";
241 if (block[0] !=
"WEBVTT" && block[0] !=
"\xEF\xBB\xBFWEBVTT") {
242 LOG(WARNING) <<
"Failed to read WEBVTT header - should be WEBVTT but was "
248 std::vector<std::string> block;
249 while (reader_.
Next(&block)) {
250 if (!ParseBlock(block))
256bool WebVttParser::ParseBlock(
const std::vector<std::string>& block) {
258 if (IsLikelyNote(block[0])) {
264 if (IsLikelyStyle(block[0])) {
267 <<
"Found style block after seeing cue. Ignoring style block";
269 for (
size_t i = 1; i < block.size(); i++) {
270 if (!css_styles_.empty())
272 css_styles_ += block[i];
279 if (IsLikelyRegion(block[0])) {
282 <<
"Found region block after seeing cue. Ignoring region block";
285 return ParseRegion(block);
290 if (block.size() >= 2 && MaybeCueId(block[0]) &&
291 IsLikelyCueTiming(block[1]) && ParseCueWithId(block)) {
297 if (IsLikelyCueTiming(block[0]) && ParseCueWithNoId(block)) {
302 LOG(ERROR) <<
"Failed to determine block classification:\n"
303 << BlockToString(block.data(), block.size());
307bool WebVttParser::ParseRegion(
const std::vector<std::string>& block) {
309 std::string region_id;
312 region.width.value = 100;
313 region.width.type = TextUnitType::kPercent;
314 region.height.value = 3;
315 region.height.type = TextUnitType::kLines;
316 region.window_anchor_x.value = 0;
317 region.window_anchor_x.type = TextUnitType::kPercent;
318 region.window_anchor_y.value = 100;
319 region.window_anchor_y.type = TextUnitType::kPercent;
320 region.region_anchor_x.value = 0;
321 region.region_anchor_x.type = TextUnitType::kPercent;
322 region.region_anchor_y.value = 100;
323 region.region_anchor_y.type = TextUnitType::kPercent;
326 for (
const auto& line : block) {
333 std::vector<KVPair> kv_pairs = SplitStringIntoKeyValuePairs(line,
':',
' ');
335 for (
const auto& pair : kv_pairs) {
336 const std::string& value = pair.second;
337 if (pair.first ==
"id") {
338 if (value.find(
"-->") != std::string::npos) {
339 LOG(ERROR) <<
"Invalid WebVTT REGION ID: " << value;
342 if (regions_.find(value) != regions_.end()) {
343 LOG(ERROR) <<
"Duplicate WebVTT REGION: " << value;
347 }
else if (pair.first ==
"width") {
348 if (!ParsePercent(value, ®ion.width.value)) {
349 LOG(ERROR) <<
"Invalid WebVTT REGION width: " << value;
352 }
else if (pair.first ==
"lines") {
354 if (!absl::SimpleAtoi(value, &temp)) {
355 LOG(ERROR) <<
"Invalid WebVTT REGION lines: " << value;
358 region.height.value = temp;
359 }
else if (pair.first ==
"regionanchor") {
360 if (!ParseDoublePercent(value, ®ion.region_anchor_x.value,
361 ®ion.region_anchor_y.value)) {
362 LOG(ERROR) <<
"Invalid WebVTT REGION regionanchor: " << value;
365 }
else if (pair.first ==
"viewportanchor") {
366 if (!ParseDoublePercent(value, ®ion.window_anchor_x.value,
367 ®ion.window_anchor_y.value)) {
368 LOG(ERROR) <<
"Invalid WebVTT REGION windowanchor: " << value;
371 }
else if (pair.first ==
"scroll") {
373 LOG(ERROR) <<
"Invalid WebVTT REGION scroll: " << value;
376 region.scroll =
true;
378 LOG(ERROR) <<
"Unknown WebVTT REGION setting: " << pair.first;
383 if (region_id.empty()) {
384 LOG(ERROR) <<
"WebVTT REGION id is required";
387 regions_.insert(std::make_pair(region_id, std::move(region)));
391bool WebVttParser::ParseCueWithNoId(
const std::vector<std::string>& block) {
392 return ParseCue(
"", block.data(), block.size());
395bool WebVttParser::ParseCueWithId(
const std::vector<std::string>& block) {
396 return ParseCue(block[0], block.data() + 1, block.size() - 1);
399bool WebVttParser::ParseCue(
const std::string&
id,
400 const std::string* block,
402 std::vector<std::string> time_and_style =
403 SplitAndTrimSkipEmpty(block[0],
' ');
405 int64_t start_time = 0;
406 int64_t end_time = 0;
408 const bool parsed_time =
409 time_and_style.size() >= 3 && time_and_style[1] ==
"-->" &&
410 WebVttTimestampToMs(time_and_style[0], &start_time) &&
411 WebVttTimestampToMs(time_and_style[2], &end_time);
414 LOG(ERROR) <<
"Could not parse start time, -->, and end time from "
419 if (!stream_info_dispatched_)
420 DispatchTextStreamInfo();
433 if (end_time <= start_time) {
434 LOG(WARNING) <<
"WebVTT input is not spec compliant. Start time ("
435 << start_time <<
") should be less than end time (" << end_time
436 <<
"). Skipping webvtt cue:"
437 << BlockToString(block, block_size);
441 TextSettings settings;
442 for (
size_t i = 3; i < time_and_style.size(); i++) {
443 const auto pos = time_and_style[i].find(
':');
444 if (pos == std::string::npos) {
448 const std::string key = time_and_style[i].substr(0, pos);
449 const std::string value = time_and_style[i].substr(pos + 1);
450 ParseSettings(key, value, &settings);
456 TextFragmentStyle no_styles;
457 for (
size_t i = 1; i < block_size; i++) {
458 if (i > 1 && i != block_size) {
459 body.sub_fragments.emplace_back(no_styles,
true);
461 body.sub_fragments.emplace_back(no_styles, block[i]);
465 std::make_shared<TextSample>(
id, start_time, end_time, settings, body);
466 return new_text_sample_cb_(kStreamIndex, sample);
469void WebVttParser::DispatchTextStreamInfo() {
470 stream_info_dispatched_ =
true;
472 const int kTrackId = 0;
474 const int kTimescale = 1000;
478 const int kDuration = 0;
479 const char kWebVttCodecString[] =
"wvtt";
480 const int64_t kNoWidth = 0;
481 const int64_t kNoHeight = 0;
483 const char kNoLanguage[] =
"";
485 const auto stream = std::make_shared<TextStreamInfo>(
486 kTrackId, kTimescale, kDuration, kCodecWebVtt, kWebVttCodecString,
"",
487 kNoWidth, kNoHeight, kNoLanguage);
488 stream->set_css_styles(css_styles_);
489 for (
const auto& pair : regions_)
490 stream->AddRegion(pair.first, pair.second);
492 std::vector<std::shared_ptr<StreamInfo>> streams{stream};
All the methods that are virtual are virtual for mocking.