Shaka Packager SDK
webvtt_utils.cc
1 // Copyright 2017 Google Inc. All rights reserved.
2 //
3 // Use of this source code is governed by a BSD-style
4 // license that can be found in the LICENSE file or at
5 // https://developers.google.com/open-source/licenses/bsd
6 
7 #include "packager/media/formats/webvtt/webvtt_utils.h"
8 
9 #include <ctype.h>
10 #include <inttypes.h>
11 
12 #include <algorithm>
13 #include <unordered_set>
14 
15 #include "packager/base/logging.h"
16 #include "packager/base/strings/string_number_conversions.h"
17 #include "packager/base/strings/string_util.h"
18 #include "packager/base/strings/stringprintf.h"
19 
20 namespace shaka {
21 namespace media {
22 
23 namespace {
24 
25 bool GetTotalMilliseconds(uint64_t hours,
26  uint64_t minutes,
27  uint64_t seconds,
28  uint64_t ms,
29  uint64_t* out) {
30  DCHECK(out);
31  if (minutes > 59 || seconds > 59 || ms > 999) {
32  VLOG(1) << "Hours:" << hours << " Minutes:" << minutes
33  << " Seconds:" << seconds << " MS:" << ms
34  << " shoud have never made it to GetTotalMilliseconds";
35  return false;
36  }
37  *out = 60 * 60 * 1000 * hours + 60 * 1000 * minutes + 1000 * seconds + ms;
38  return true;
39 }
40 
41 enum class StyleTagKind {
42  kUnderline,
43  kBold,
44  kItalic,
45 };
46 
47 std::string GetOpenTag(StyleTagKind tag) {
48  switch (tag) {
49  case StyleTagKind::kUnderline:
50  return "<u>";
51  case StyleTagKind::kBold:
52  return "<b>";
53  case StyleTagKind::kItalic:
54  return "<i>";
55  }
56  return ""; // Not reached, but Windows doesn't like NOTREACHED.
57 }
58 
59 std::string GetCloseTag(StyleTagKind tag) {
60  switch (tag) {
61  case StyleTagKind::kUnderline:
62  return "</u>";
63  case StyleTagKind::kBold:
64  return "</b>";
65  case StyleTagKind::kItalic:
66  return "</i>";
67  }
68  return ""; // Not reached, but Windows doesn't like NOTREACHED.
69 }
70 
71 bool IsWhitespace(char c) {
72  return c == '\t' || c == '\r' || c == '\n' || c == ' ';
73 }
74 
75 // Replace consecutive whitespaces with a single whitespace.
76 std::string CollapseWhitespace(const std::string& data) {
77  std::string output;
78  output.resize(data.size());
79  size_t chars_written = 0;
80  bool in_whitespace = false;
81  for (char c : data) {
82  if (IsWhitespace(c)) {
83  if (!in_whitespace) {
84  in_whitespace = true;
85  output[chars_written++] = ' ';
86  }
87  } else {
88  in_whitespace = false;
89  output[chars_written++] = c;
90  }
91  }
92  output.resize(chars_written);
93  return output;
94 }
95 
96 std::string WriteFragment(const TextFragment& fragment,
97  std::list<StyleTagKind>* tags) {
98  std::string ret;
99  size_t local_tag_count = 0;
100  auto has = [tags](StyleTagKind tag) {
101  return std::find(tags->begin(), tags->end(), tag) != tags->end();
102  };
103  auto push_tag = [tags, &local_tag_count, &has](StyleTagKind tag) {
104  if (has(tag)) {
105  return std::string();
106  }
107  tags->push_back(tag);
108  local_tag_count++;
109  return GetOpenTag(tag);
110  };
111 
112  if ((fragment.style.underline == false && has(StyleTagKind::kUnderline)) ||
113  (fragment.style.bold == false && has(StyleTagKind::kBold)) ||
114  (fragment.style.italic == false && has(StyleTagKind::kItalic))) {
115  LOG(WARNING) << "WebVTT output doesn't support disabling "
116  "underline/bold/italic within a cue";
117  }
118 
119  if (fragment.newline) {
120  // Newlines represent separate WebVTT cues. So close the existing tags to
121  // be nice and re-open them on the new line.
122  for (auto it = tags->rbegin(); it != tags->rend(); it++) {
123  ret += GetCloseTag(*it);
124  }
125  ret += "\n";
126  for (const auto tag : *tags) {
127  ret += GetOpenTag(tag);
128  }
129  } else {
130  if (fragment.style.underline == true) {
131  ret += push_tag(StyleTagKind::kUnderline);
132  }
133  if (fragment.style.bold == true) {
134  ret += push_tag(StyleTagKind::kBold);
135  }
136  if (fragment.style.italic == true) {
137  ret += push_tag(StyleTagKind::kItalic);
138  }
139 
140  if (!fragment.body.empty()) {
141  // Replace newlines and consecutive whitespace with a single space. If
142  // the user wanted an explicit newline, they should use the "newline"
143  // field.
144  ret += CollapseWhitespace(fragment.body);
145  } else {
146  for (const auto& frag : fragment.sub_fragments) {
147  ret += WriteFragment(frag, tags);
148  }
149  }
150 
151  // Pop all the local tags we pushed.
152  while (local_tag_count > 0) {
153  ret += GetCloseTag(tags->back());
154  tags->pop_back();
155  local_tag_count--;
156  }
157  }
158  return ret;
159 }
160 
161 } // namespace
162 
163 bool WebVttTimestampToMs(const base::StringPiece& source, uint64_t* out) {
164  DCHECK(out);
165 
166  if (source.length() < 9) {
167  LOG(WARNING) << "Timestamp '" << source << "' is mal-formed";
168  return false;
169  }
170 
171  const size_t minutes_begin = source.length() - 9;
172  const size_t seconds_begin = source.length() - 6;
173  const size_t milliseconds_begin = source.length() - 3;
174 
175  uint64_t hours = 0;
176  uint64_t minutes = 0;
177  uint64_t seconds = 0;
178  uint64_t ms = 0;
179 
180  const bool has_hours =
181  minutes_begin >= 3 && source[minutes_begin - 1] == ':' &&
182  base::StringToUint64(source.substr(0, minutes_begin - 1), &hours);
183 
184  if ((minutes_begin == 0 || has_hours) && source[seconds_begin - 1] == ':' &&
185  source[milliseconds_begin - 1] == '.' &&
186  base::StringToUint64(source.substr(minutes_begin, 2), &minutes) &&
187  base::StringToUint64(source.substr(seconds_begin, 2), &seconds) &&
188  base::StringToUint64(source.substr(milliseconds_begin, 3), &ms)) {
189  return GetTotalMilliseconds(hours, minutes, seconds, ms, out);
190  }
191 
192  LOG(WARNING) << "Timestamp '" << source << "' is mal-formed";
193  return false;
194 }
195 
196 std::string MsToWebVttTimestamp(uint64_t ms) {
197  uint64_t remaining = ms;
198 
199  uint64_t only_ms = remaining % 1000;
200  remaining /= 1000;
201  uint64_t only_seconds = remaining % 60;
202  remaining /= 60;
203  uint64_t only_minutes = remaining % 60;
204  remaining /= 60;
205  uint64_t only_hours = remaining;
206 
207  return base::StringPrintf("%02" PRIu64 ":%02" PRIu64 ":%02" PRIu64
208  ".%03" PRIu64,
209  only_hours, only_minutes, only_seconds, only_ms);
210 }
211 
212 std::string WebVttSettingsToString(const TextSettings& settings) {
213  std::string ret;
214  if (!settings.region.empty()) {
215  ret += " region:";
216  ret += settings.region;
217  }
218  if (settings.line) {
219  switch (settings.line->type) {
220  case TextUnitType::kPercent:
221  ret += " line:";
222  ret += base::DoubleToString(settings.line->value);
223  ret += "%";
224  break;
225  case TextUnitType::kLines:
226  ret += " line:";
227  ret += base::DoubleToString(settings.line->value);
228  break;
229  case TextUnitType::kPixels:
230  LOG(WARNING) << "WebVTT doesn't support pixel line settings";
231  break;
232  }
233  }
234  if (settings.position) {
235  if (settings.position->type == TextUnitType::kPercent) {
236  ret += " position:";
237  ret += base::DoubleToString(settings.position->value);
238  ret += "%";
239  } else {
240  LOG(WARNING) << "WebVTT only supports percent position settings";
241  }
242  }
243  if (settings.width) {
244  if (settings.width->type == TextUnitType::kPercent) {
245  ret += " size:";
246  ret += base::DoubleToString(settings.width->value);
247  ret += "%";
248  } else {
249  LOG(WARNING) << "WebVTT only supports percent width settings";
250  }
251  }
252  if (settings.height) {
253  LOG(WARNING) << "WebVTT doesn't support cue heights";
254  }
255  if (settings.writing_direction != WritingDirection::kHorizontal) {
256  ret += " direction:";
257  if (settings.writing_direction == WritingDirection::kVerticalGrowingLeft) {
258  ret += "rl";
259  } else {
260  ret += "lr";
261  }
262  }
263  switch (settings.text_alignment) {
264  case TextAlignment::kStart:
265  ret += " align:start";
266  break;
267  case TextAlignment::kEnd:
268  ret += " align:end";
269  break;
270  case TextAlignment::kLeft:
271  ret += " align:left";
272  break;
273  case TextAlignment::kRight:
274  ret += " align:right";
275  break;
276  case TextAlignment::kCenter:
277  ret += " align:center";
278  break;
279  }
280 
281  if (!ret.empty()) {
282  DCHECK_EQ(ret[0], ' ');
283  ret.erase(0, 1);
284  }
285  return ret;
286 }
287 
288 std::string WebVttFragmentToString(const TextFragment& fragment) {
289  std::list<StyleTagKind> tags;
290  return WriteFragment(fragment, &tags);
291 }
292 
293 std::string WebVttGetPreamble(const TextStreamInfo& stream_info) {
294  std::string ret;
295  for (const auto& pair : stream_info.regions()) {
296  if (!ret.empty()) {
297  ret += "\n\n";
298  }
299 
300  if (pair.second.width.type != TextUnitType::kPercent ||
301  pair.second.height.type != TextUnitType::kLines ||
302  pair.second.window_anchor_x.type != TextUnitType::kPercent ||
303  pair.second.window_anchor_y.type != TextUnitType::kPercent ||
304  pair.second.region_anchor_x.type != TextUnitType::kPercent ||
305  pair.second.region_anchor_y.type != TextUnitType::kPercent) {
306  LOG(WARNING) << "Unsupported unit type in WebVTT region";
307  continue;
308  }
309 
310  base::StringAppendF(
311  &ret,
312  "REGION\n"
313  "id:%s\n"
314  "width:%f%%\n"
315  "lines:%d\n"
316  "viewportanchor:%f%%,%f%%\n"
317  "regionanchor:%f%%,%f%%",
318  pair.first.c_str(), pair.second.width.value,
319  static_cast<int>(pair.second.height.value),
320  pair.second.window_anchor_x.value, pair.second.window_anchor_y.value,
321  pair.second.region_anchor_x.value, pair.second.region_anchor_y.value);
322  if (pair.second.scroll) {
323  ret += "\nscroll:up";
324  }
325  }
326 
327  if (!stream_info.css_styles().empty()) {
328  if (!ret.empty()) {
329  ret += "\n\n";
330  }
331  ret += "STYLE\n" + stream_info.css_styles();
332  }
333 
334  return ret;
335 }
336 
337 } // namespace media
338 } // namespace shaka
All the methods that are virtual are virtual for mocking.