7 #include <packager/media/formats/webvtt/webvtt_utils.h>
13 #include <unordered_set>
15 #include <absl/log/check.h>
16 #include <absl/log/log.h>
17 #include <absl/strings/numbers.h>
18 #include <absl/strings/str_format.h>
20 #include <packager/macros/logging.h>
27 constexpr
const char* kRegionTeletextPrefix =
"ttx_";
29 bool GetTotalMilliseconds(uint64_t hours,
35 if (minutes > 59 || seconds > 59 || ms > 999) {
36 VLOG(1) <<
"Hours:" << hours <<
" Minutes:" << minutes
37 <<
" Seconds:" << seconds <<
" MS:" << ms
38 <<
" shoud have never made it to GetTotalMilliseconds";
41 *out = 60 * 60 * 1000 * hours + 60 * 1000 * minutes + 1000 * seconds + ms;
45 enum class StyleTagKind {
51 std::string GetOpenTag(StyleTagKind tag) {
53 case StyleTagKind::kUnderline:
55 case StyleTagKind::kBold:
57 case StyleTagKind::kItalic:
63 std::string GetCloseTag(StyleTagKind tag) {
65 case StyleTagKind::kUnderline:
67 case StyleTagKind::kBold:
69 case StyleTagKind::kItalic:
75 bool IsWhitespace(
char c) {
76 return c ==
'\t' || c ==
'\r' || c ==
'\n' || c ==
' ';
80 std::string CollapseWhitespace(
const std::string& data) {
82 output.resize(data.size());
83 size_t chars_written = 0;
84 bool in_whitespace =
false;
86 if (IsWhitespace(c)) {
89 output[chars_written++] =
' ';
92 in_whitespace =
false;
93 output[chars_written++] = c;
96 output.resize(chars_written);
100 std::string WriteFragment(
const TextFragment& fragment,
101 std::list<StyleTagKind>* tags) {
103 size_t local_tag_count = 0;
104 auto has = [tags](StyleTagKind tag) {
105 return std::find(tags->begin(), tags->end(), tag) != tags->end();
107 auto push_tag = [tags, &local_tag_count, &has](StyleTagKind tag) {
109 return std::string();
111 tags->push_back(tag);
113 return GetOpenTag(tag);
116 if ((fragment.style.underline ==
false && has(StyleTagKind::kUnderline)) ||
117 (fragment.style.bold ==
false && has(StyleTagKind::kBold)) ||
118 (fragment.style.italic ==
false && has(StyleTagKind::kItalic))) {
119 LOG(WARNING) <<
"WebVTT output doesn't support disabling "
120 "underline/bold/italic within a cue";
123 if (fragment.newline) {
126 for (
auto it = tags->rbegin(); it != tags->rend(); it++) {
127 ret += GetCloseTag(*it);
130 for (
const auto tag : *tags) {
131 ret += GetOpenTag(tag);
134 if (fragment.style.underline ==
true) {
135 ret += push_tag(StyleTagKind::kUnderline);
137 if (fragment.style.bold ==
true) {
138 ret += push_tag(StyleTagKind::kBold);
140 if (fragment.style.italic ==
true) {
141 ret += push_tag(StyleTagKind::kItalic);
144 if (!fragment.body.empty()) {
148 ret += CollapseWhitespace(fragment.body);
150 for (
const auto& frag : fragment.sub_fragments) {
151 ret += WriteFragment(frag, tags);
156 while (local_tag_count > 0) {
157 ret += GetCloseTag(tags->back());
167 bool WebVttTimestampToMs(
const std::string_view& source, int64_t* out) {
170 if (source.length() < 9) {
171 LOG(WARNING) <<
"Timestamp '" << source <<
"' is mal-formed";
175 const size_t minutes_begin = source.length() - 9;
176 const size_t seconds_begin = source.length() - 6;
177 const size_t milliseconds_begin = source.length() - 3;
180 uint64_t minutes = 0;
181 uint64_t seconds = 0;
184 const bool has_hours =
185 minutes_begin >= 3 && source[minutes_begin - 1] ==
':' &&
186 absl::SimpleAtoi(source.substr(0, minutes_begin - 1), &hours);
188 if ((minutes_begin == 0 || has_hours) && source[seconds_begin - 1] ==
':' &&
189 source[milliseconds_begin - 1] ==
'.' &&
190 absl::SimpleAtoi(source.substr(minutes_begin, 2), &minutes) &&
191 absl::SimpleAtoi(source.substr(seconds_begin, 2), &seconds) &&
192 absl::SimpleAtoi(source.substr(milliseconds_begin, 3), &ms)) {
193 return GetTotalMilliseconds(hours, minutes, seconds, ms, out);
196 LOG(WARNING) <<
"Timestamp '" << source <<
"' is mal-formed";
200 std::string MsToWebVttTimestamp(uint64_t ms) {
201 uint64_t remaining = ms;
203 uint64_t only_ms = remaining % 1000;
205 uint64_t only_seconds = remaining % 60;
207 uint64_t only_minutes = remaining % 60;
209 uint64_t only_hours = remaining;
211 return absl::StrFormat(
"%02" PRIu64
":%02" PRIu64
":%02" PRIu64
".%03" PRIu64,
212 only_hours, only_minutes, only_seconds, only_ms);
215 std::string FloatToString(
double number) {
217 std::string formatted = absl::StrFormat(
"%.6g", number);
218 size_t decimalPos = formatted.find(
'.');
219 if (decimalPos != std::string::npos) {
220 size_t lastNonZeroPos = formatted.find_last_not_of(
'0');
221 if (lastNonZeroPos >= decimalPos) {
222 formatted.erase(lastNonZeroPos + 1);
224 if (formatted.back() ==
'.') {
225 formatted.pop_back();
232 std::string WebVttSettingsToString(
const TextSettings& settings) {
234 if (!settings.region.empty() &&
235 settings.region.find(kRegionTeletextPrefix) != 0) {
238 ret += settings.region;
241 switch (settings.line->type) {
242 case TextUnitType::kPercent:
244 ret += FloatToString(settings.line->value);
247 case TextUnitType::kLines:
250 ret += FloatToString(std::round(settings.line->value));
252 case TextUnitType::kPixels:
253 LOG(WARNING) <<
"WebVTT doesn't support pixel line settings";
257 if (settings.position) {
258 if (settings.position->type == TextUnitType::kPercent) {
260 ret += FloatToString(settings.position->value);
263 LOG(WARNING) <<
"WebVTT only supports percent position settings";
266 if (settings.width) {
267 if (settings.width->type == TextUnitType::kPercent) {
269 ret += FloatToString(settings.width->value);
272 LOG(WARNING) <<
"WebVTT only supports percent width settings";
275 if (settings.height) {
276 LOG(WARNING) <<
"WebVTT doesn't support cue heights";
278 if (settings.writing_direction != WritingDirection::kHorizontal) {
279 ret +=
" direction:";
280 if (settings.writing_direction == WritingDirection::kVerticalGrowingLeft) {
286 switch (settings.text_alignment) {
287 case TextAlignment::kStart:
288 ret +=
" align:start";
290 case TextAlignment::kEnd:
293 case TextAlignment::kLeft:
294 ret +=
" align:left";
296 case TextAlignment::kRight:
297 ret +=
" align:right";
299 case TextAlignment::kCenter:
300 ret +=
" align:center";
305 DCHECK_EQ(ret[0],
' ');
311 std::string WebVttFragmentToString(
const TextFragment& fragment) {
312 std::list<StyleTagKind> tags;
313 return WriteFragment(fragment, &tags);
316 std::string WebVttGetPreamble(
const TextStreamInfo& stream_info) {
318 for (
const auto& pair : stream_info.regions()) {
323 if (pair.second.width.type != TextUnitType::kPercent ||
324 pair.second.height.type != TextUnitType::kLines ||
325 pair.second.window_anchor_x.type != TextUnitType::kPercent ||
326 pair.second.window_anchor_y.type != TextUnitType::kPercent ||
327 pair.second.region_anchor_x.type != TextUnitType::kPercent ||
328 pair.second.region_anchor_y.type != TextUnitType::kPercent) {
329 LOG(WARNING) <<
"Unsupported unit type in WebVTT region";
333 absl::StrAppendFormat(
339 "viewportanchor:%f%%,%f%%\n"
340 "regionanchor:%f%%,%f%%",
341 pair.first.c_str(), pair.second.width.value,
342 static_cast<int>(pair.second.height.value),
343 pair.second.window_anchor_x.value, pair.second.window_anchor_y.value,
344 pair.second.region_anchor_x.value, pair.second.region_anchor_y.value);
345 if (pair.second.scroll) {
346 ret +=
"\nscroll:up";
350 if (!stream_info.css_styles().empty()) {
354 ret +=
"STYLE\n" + stream_info.css_styles();
All the methods that are virtual are virtual for mocking.