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