feat: teletext formatting (#1384)
This PR adds parsing of teletext styling, and rendering of the styling in output TTML and WebVTT subtitle tracks. Beyond unit tests, I've used the sample https://drive.google.com/file/d/19ZYsoeUfH85gEilQkaAdLbPhC4CxhDEh/view?usp=sharing which has rather advanced subtitling with two separate rows at the same time, where one is left aligned and another is right aligned. This necessitates two parallel cues to be rendered. It also has some colored text. Solve #1335. ## parse teletext styling and formatting Extend the teletext parser to parse the teletext styling and formatting. This includes translating rows into regions, calculating alignment from start and stop position of the text, and extracting text and background colors. The colors are limited to full lines. Both lines and regions are propagated in the TextSample structures. This is because the number of lines may differ from different sources. For teletext, there are 24 rows, but they are essentially always used with double height, so the number of output lines is 12 from 0 to 11. There are also corresponding regions are denoted "ttx_R", where R is an integer row number. A renderer can use either the line number or the region ID to render the text. ## ttml generation for teletext to EBU-TT-D Add support to render teletext input in EBU-TT-D (IMSC-1) format. This includes appropriate regions ttx_0 to ttx_11 signalled in the TextSamples, alignment and text and background colors. The general TTML output has been changed to always include metadata, layout, and styling nodes, even if they are empty. EBU-TT-D is detected by the presence of "ttx_?" regions in the samples. If detected, extra TTML elements will be added and the EBU-TT-D linePadding used as well. Appropriate styles for background and text colors are generated depending on the color and backgroundColor attributes in the text fragments. ## adapt WebVTT output to teletext TextSample. Teletext input generates both a region with prefix ttx_ and a floating point line number (e.g. 9.5) in the range 0 to 11.5 (due to input 0-23 as double lines). The output is adopted to drop such regions and convert the line number to an integer since the standard only used floats for percent values but not for plain line numbers.
This commit is contained in:
parent
84009d82ef
commit
4b5e80d02c
|
@ -54,6 +54,7 @@ Sanil Raut <sr1990003@gmail.com>
|
|||
Sergio Ammirata <sergio@ammirata.net>
|
||||
Thomas Inskip <tinskip@google.com>
|
||||
Tim Lansen <tim.lansen@gmail.com>
|
||||
Torbjörn Einarsson <torbjorn.einarsson@eyevinn.se>
|
||||
Vincent Nguyen <nvincen@amazon.com>
|
||||
Weiguo Shao <weiguo.shao@dolby.com>
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -4,7 +4,7 @@
|
|||
<Period id="0" start="PT0S">
|
||||
<AdaptationSet id="0" contentType="text" segmentAlignment="true">
|
||||
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="subtitle"/>
|
||||
<Representation id="0" bandwidth="4120" codecs="stpp" mimeType="application/mp4">
|
||||
<Representation id="0" bandwidth="4552" codecs="stpp" mimeType="application/mp4">
|
||||
<SegmentTemplate timescale="1000" initialization="bear-english-text-init.mp4" media="bear-english-text-$Number$.m4s" startNumber="1">
|
||||
<SegmentTimeline>
|
||||
<S t="0" d="1000" r="4"/>
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tt xmlns="http://www.w3.org/ns/ttml" xmlns:tts="http://www.w3.org/ns/ttml#styling" xml:lang="">
|
||||
<head/>
|
||||
<head>
|
||||
<metadata/>
|
||||
<styling/>
|
||||
<layout/>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<p xml:space="preserve" begin="00:00:00.000" end="00:00:00.800" tts:textAlign="center">Yup, that's a bear, eh.</p>
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tt xmlns="http://www.w3.org/ns/ttml" xmlns:tts="http://www.w3.org/ns/ttml#styling" xml:lang="">
|
||||
<head/>
|
||||
<head>
|
||||
<metadata/>
|
||||
<styling/>
|
||||
<layout/>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<p xml:space="preserve" begin="00:00:01.000" end="00:00:04.700" tts:textAlign="center">He 's... um... doing bear-like stuff.</p>
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tt xmlns="http://www.w3.org/ns/ttml" xmlns:tts="http://www.w3.org/ns/ttml#styling" xml:lang="">
|
||||
<head/>
|
||||
<head>
|
||||
<metadata/>
|
||||
<styling/>
|
||||
<layout/>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<p xml:space="preserve" begin="00:00:01.000" end="00:00:04.700" tts:textAlign="center">He 's... um... doing bear-like stuff.</p>
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tt xmlns="http://www.w3.org/ns/ttml" xmlns:tts="http://www.w3.org/ns/ttml#styling" xml:lang="">
|
||||
<head/>
|
||||
<head>
|
||||
<metadata/>
|
||||
<styling/>
|
||||
<layout/>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<p xml:space="preserve" begin="00:00:01.000" end="00:00:04.700" tts:textAlign="center">He 's... um... doing bear-like stuff.</p>
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tt xmlns="http://www.w3.org/ns/ttml" xmlns:tts="http://www.w3.org/ns/ttml#styling" xml:lang="">
|
||||
<head/>
|
||||
<head>
|
||||
<metadata/>
|
||||
<styling/>
|
||||
<layout/>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<p xml:space="preserve" begin="00:00:01.000" end="00:00:04.700" tts:textAlign="center">He 's... um... doing bear-like stuff.</p>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<Period id="0" start="PT0S">
|
||||
<AdaptationSet id="0" contentType="text" segmentAlignment="true">
|
||||
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="subtitle"/>
|
||||
<Representation id="0" bandwidth="2616" mimeType="application/ttml+xml">
|
||||
<Representation id="0" bandwidth="3048" mimeType="application/ttml+xml">
|
||||
<SegmentTemplate timescale="1000" media="bear-english-text-$Number$.ttml" startNumber="1">
|
||||
<SegmentTimeline>
|
||||
<S t="0" d="1000" r="4"/>
|
||||
|
|
|
@ -80,6 +80,11 @@ struct TextFragmentStyle {
|
|||
std::optional<bool> underline;
|
||||
std::optional<bool> bold;
|
||||
std::optional<bool> italic;
|
||||
// The colors could be any string that can be interpreted as
|
||||
// a color in TTML (or WebVTT). As a start, the 8 teletext colors are used,
|
||||
// i.e. black, red, green, yellow, blue, magenta, cyan, and white
|
||||
std::string color;
|
||||
std::string backgroundColor;
|
||||
};
|
||||
|
||||
/// Represents a recursive structure of styled blocks of text. Only one of
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
#include <packager/media/formats/mp2t/es_parser_teletext.h>
|
||||
|
||||
#include <packager/media/base/bit_reader.h>
|
||||
#include <packager/media/base/text_stream_info.h>
|
||||
#include <packager/media/base/timestamp.h>
|
||||
#include <packager/media/formats/mp2t/es_parser_teletext_tables.h>
|
||||
#include <packager/media/formats/mp2t/mp2t_common.h>
|
||||
#include <iostream>
|
||||
|
||||
namespace shaka {
|
||||
namespace media {
|
||||
|
@ -18,6 +18,8 @@ namespace mp2t {
|
|||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kRegionTeletextPrefix = "ttx_";
|
||||
|
||||
const uint8_t EBU_TELETEXT_WITH_SUBTITLING = 0x03;
|
||||
const int kPayloadSize = 40;
|
||||
const int kNumTriplets = 13;
|
||||
|
@ -94,14 +96,6 @@ bool ParseSubtitlingDescriptor(
|
|||
return true;
|
||||
}
|
||||
|
||||
std::string RemoveTrailingSpaces(const std::string& input) {
|
||||
const auto index = input.find_last_not_of(' ');
|
||||
if (index == std::string::npos) {
|
||||
return "";
|
||||
}
|
||||
return input.substr(0, index + 1);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EsParserTeletext::EsParserTeletext(const uint32_t pid,
|
||||
|
@ -169,7 +163,7 @@ bool EsParserTeletext::ParseInternal(const uint8_t* data,
|
|||
const int64_t pts) {
|
||||
BitReader reader(data, size);
|
||||
RCHECK(reader.SkipBits(8));
|
||||
std::vector<std::string> lines;
|
||||
std::vector<TextRow> rows;
|
||||
|
||||
while (reader.bits_available()) {
|
||||
uint8_t data_unit_id;
|
||||
|
@ -178,15 +172,17 @@ bool EsParserTeletext::ParseInternal(const uint8_t* data,
|
|||
uint8_t data_unit_length;
|
||||
RCHECK(reader.ReadBits(8, &data_unit_length));
|
||||
|
||||
if (data_unit_id != EBU_TELETEXT_WITH_SUBTITLING) {
|
||||
RCHECK(reader.SkipBytes(data_unit_length));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (data_unit_length != 44) {
|
||||
// Teletext data unit length is always 44 bytes
|
||||
LOG(ERROR) << "Bad Teletext data length";
|
||||
break;
|
||||
}
|
||||
|
||||
if (data_unit_id != EBU_TELETEXT_WITH_SUBTITLING) {
|
||||
RCHECK(reader.SkipBytes(44));
|
||||
continue;
|
||||
}
|
||||
|
||||
RCHECK(reader.SkipBits(16));
|
||||
|
||||
|
@ -207,27 +203,26 @@ bool EsParserTeletext::ParseInternal(const uint8_t* data,
|
|||
const uint8_t* data_block = reader.current_byte_ptr();
|
||||
RCHECK(reader.SkipBytes(40));
|
||||
|
||||
std::string display_text;
|
||||
if (ParseDataBlock(pts, data_block, packet_nr, magazine, display_text)) {
|
||||
lines.emplace_back(std::move(display_text));
|
||||
TextRow row;
|
||||
if (ParseDataBlock(pts, data_block, packet_nr, magazine, row)) {
|
||||
rows.emplace_back(std::move(row));
|
||||
}
|
||||
}
|
||||
|
||||
if (lines.empty()) {
|
||||
if (rows.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint16_t index = magazine_ * 100 + page_number_;
|
||||
auto page_state_itr = page_state_.find(index);
|
||||
if (page_state_itr == page_state_.end()) {
|
||||
page_state_.emplace(index, TextBlock{std::move(lines), {}, last_pts_});
|
||||
page_state_.emplace(index, TextBlock{std::move(rows), {}, last_pts_});
|
||||
|
||||
} else {
|
||||
for (auto& line : lines) {
|
||||
auto& page_state_lines = page_state_itr->second.lines;
|
||||
page_state_lines.emplace_back(std::move(line));
|
||||
for (auto& row : rows) {
|
||||
auto& page_state_lines = page_state_itr->second.rows;
|
||||
page_state_lines.emplace_back(std::move(row));
|
||||
}
|
||||
lines.clear();
|
||||
rows.clear();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -237,13 +232,17 @@ bool EsParserTeletext::ParseDataBlock(const int64_t pts,
|
|||
const uint8_t* data_block,
|
||||
const uint8_t packet_nr,
|
||||
const uint8_t magazine,
|
||||
std::string& display_text) {
|
||||
TextRow& row) {
|
||||
if (packet_nr == 0) {
|
||||
last_pts_ = pts;
|
||||
BitReader reader(data_block, 32);
|
||||
|
||||
const uint8_t page_number_units = ReadHamming(reader);
|
||||
const uint8_t page_number_tens = ReadHamming(reader);
|
||||
if (page_number_units == 0xf || page_number_tens == 0xf) {
|
||||
RCHECK(reader.SkipBits(40));
|
||||
return false;
|
||||
}
|
||||
const uint8_t page_number = 10 * page_number_tens + page_number_units;
|
||||
|
||||
const uint16_t index = magazine * 100 + page_number;
|
||||
|
@ -251,9 +250,6 @@ bool EsParserTeletext::ParseDataBlock(const int64_t pts,
|
|||
|
||||
page_number_ = page_number;
|
||||
magazine_ = magazine;
|
||||
if (page_number == 0xFF) {
|
||||
return false;
|
||||
}
|
||||
|
||||
RCHECK(reader.SkipBits(40));
|
||||
const uint8_t subcode_c11_c14 = ReadHamming(reader);
|
||||
|
@ -273,7 +269,7 @@ bool EsParserTeletext::ParseDataBlock(const int64_t pts,
|
|||
return false;
|
||||
}
|
||||
|
||||
display_text = BuildText(data_block, packet_nr);
|
||||
row = BuildRow(data_block, packet_nr);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -317,45 +313,81 @@ void EsParserTeletext::SendPending(const uint16_t index, const int64_t pts) {
|
|||
auto page_state_itr = page_state_.find(index);
|
||||
|
||||
if (page_state_itr == page_state_.end() ||
|
||||
page_state_itr->second.lines.empty()) {
|
||||
page_state_itr->second.rows.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& pending_lines = page_state_itr->second.lines;
|
||||
const auto& pending_rows = page_state_itr->second.rows;
|
||||
const auto pending_pts = page_state_itr->second.pts;
|
||||
|
||||
TextFragmentStyle text_fragment_style;
|
||||
TextSettings text_settings;
|
||||
std::shared_ptr<TextSample> text_sample;
|
||||
|
||||
if (pending_lines.size() == 1) {
|
||||
TextFragment text_fragment(text_fragment_style, pending_lines[0].c_str());
|
||||
text_sample = std::make_shared<TextSample>("", pending_pts, pts,
|
||||
text_settings, text_fragment);
|
||||
|
||||
} else {
|
||||
std::vector<TextFragment> sub_fragments;
|
||||
for (const auto& line : pending_lines) {
|
||||
sub_fragments.emplace_back(text_fragment_style, line.c_str());
|
||||
sub_fragments.emplace_back(text_fragment_style, true);
|
||||
|
||||
if (pending_rows.size() == 1) {
|
||||
// This is a single line of formatted text.
|
||||
// Propagate row number/2 and alignment
|
||||
const float line_nr = float(pending_rows[0].row_number) / 2.0;
|
||||
text_settings.line = TextNumber(line_nr, TextUnitType::kLines);
|
||||
text_settings.region = kRegionTeletextPrefix + std::to_string(int(line_nr));
|
||||
text_settings.text_alignment = pending_rows[0].alignment;
|
||||
text_sample = std::make_shared<TextSample>(
|
||||
"", pending_pts, pts, text_settings, pending_rows[0].fragment);
|
||||
text_sample->set_sub_stream_index(index);
|
||||
emit_sample_cb_(text_sample);
|
||||
page_state_.erase(index);
|
||||
return;
|
||||
} else {
|
||||
int32_t latest_row_nr = -1;
|
||||
bool last_double_height = false;
|
||||
bool new_sample = true;
|
||||
for (const auto& row : pending_rows) {
|
||||
int row_nr = row.row_number;
|
||||
bool double_height = row.double_height;
|
||||
int row_step = last_double_height ? 2 : 1;
|
||||
if (latest_row_nr != -1) { // Not the first row
|
||||
if (row_nr != latest_row_nr + row_step) {
|
||||
// Send what has been collected since not adjacent
|
||||
text_sample =
|
||||
std::make_shared<TextSample>("", pending_pts, pts, text_settings,
|
||||
TextFragment({}, sub_fragments));
|
||||
text_sample->set_sub_stream_index(index);
|
||||
emit_sample_cb_(text_sample);
|
||||
new_sample = true;
|
||||
} else {
|
||||
// Add a newline and the next row to the current sample
|
||||
sub_fragments.push_back(TextFragment({}, true));
|
||||
sub_fragments.push_back(row.fragment);
|
||||
new_sample = false;
|
||||
}
|
||||
}
|
||||
if (new_sample) {
|
||||
const float line_nr = float(row.row_number) / 2.0;
|
||||
text_settings.line = TextNumber(line_nr, TextUnitType::kLines);
|
||||
text_settings.region =
|
||||
kRegionTeletextPrefix + std::to_string(int(line_nr));
|
||||
text_settings.text_alignment = row.alignment;
|
||||
sub_fragments.clear();
|
||||
sub_fragments.push_back(row.fragment);
|
||||
}
|
||||
last_double_height = double_height;
|
||||
latest_row_nr = row_nr;
|
||||
}
|
||||
sub_fragments.pop_back();
|
||||
TextFragment text_fragment(text_fragment_style, sub_fragments);
|
||||
text_sample = std::make_shared<TextSample>("", pending_pts, pts,
|
||||
text_settings, text_fragment);
|
||||
}
|
||||
|
||||
text_sample = std::make_shared<TextSample>(
|
||||
"", pending_pts, pts, text_settings, TextFragment({}, sub_fragments));
|
||||
text_sample->set_sub_stream_index(index);
|
||||
emit_sample_cb_(text_sample);
|
||||
|
||||
page_state_.erase(index);
|
||||
}
|
||||
|
||||
std::string EsParserTeletext::BuildText(const uint8_t* data_block,
|
||||
// BuildRow builds a row with alignment information.
|
||||
EsParserTeletext::TextRow EsParserTeletext::BuildRow(const uint8_t* data_block,
|
||||
const uint8_t row) const {
|
||||
std::string next_string;
|
||||
next_string.reserve(kPayloadSize * 2);
|
||||
bool leading_spaces = true;
|
||||
|
||||
const uint16_t index = magazine_ * 100 + page_number_;
|
||||
const auto page_state_itr = page_state_.find(index);
|
||||
|
@ -370,12 +402,19 @@ std::string EsParserTeletext::BuildText(const uint8_t* data_block,
|
|||
}
|
||||
}
|
||||
|
||||
int32_t start_pos = 0;
|
||||
int32_t end_pos = 0;
|
||||
bool double_height = false;
|
||||
TextFragmentStyle text_style = TextFragmentStyle();
|
||||
text_style.color = "white";
|
||||
text_style.backgroundColor = "black";
|
||||
// A typical 40 character line looks like:
|
||||
// doubleHeight, [color] spaces, Start, Start, text, End End, spaces
|
||||
for (size_t i = 0; i < kPayloadSize; ++i) {
|
||||
if (column_replacement_map) {
|
||||
const auto column_itr = column_replacement_map->find(i);
|
||||
if (column_itr != column_replacement_map->cend()) {
|
||||
next_string.append(column_itr->second);
|
||||
leading_spaces = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
@ -383,17 +422,68 @@ std::string EsParserTeletext::BuildText(const uint8_t* data_block,
|
|||
char next_char =
|
||||
static_cast<char>(TELETEXT_BITREVERSE_8[data_block[i]] & 0x7f);
|
||||
|
||||
if (next_char < 32) {
|
||||
next_char = 0x20;
|
||||
if (next_char < 0x20) {
|
||||
// Here are control characters, which are not printable.
|
||||
// These include colors, double-height, flashing, etc.
|
||||
// We only handle one-foreground color and double-height.
|
||||
switch (next_char) {
|
||||
case 0x0: // Alpha Black (not included in Level 1.5)
|
||||
// color = ColorBlack
|
||||
break;
|
||||
case 0x1:
|
||||
text_style.color = "red";
|
||||
break;
|
||||
case 0x2:
|
||||
text_style.color = "green";
|
||||
break;
|
||||
case 0x3:
|
||||
text_style.color = "yellow";
|
||||
break;
|
||||
case 0x4:
|
||||
text_style.color = "blue";
|
||||
break;
|
||||
case 0x5:
|
||||
text_style.color = "magenta";
|
||||
break;
|
||||
case 0x6:
|
||||
text_style.color = "cyan";
|
||||
break;
|
||||
case 0x7:
|
||||
text_style.color = "white";
|
||||
break;
|
||||
case 0x08: // Flash (not handled)
|
||||
break;
|
||||
case 0x09: // Steady (not handled)
|
||||
break;
|
||||
case 0xa: // End Box
|
||||
end_pos = i - 1;
|
||||
break;
|
||||
case 0xb: // Start Box, typically twice due to double height
|
||||
start_pos = i + 1;
|
||||
continue; // Do not propagate as a space
|
||||
break;
|
||||
case 0xc: // Normal size
|
||||
break;
|
||||
case 0xd: // Double height, typically always used
|
||||
double_height = true;
|
||||
break;
|
||||
case 0x1c: // Black background (not handled)
|
||||
break;
|
||||
case 0x1d: // Set background color from text color.
|
||||
text_style.backgroundColor = text_style.color;
|
||||
text_style.color = "black"; // Avoid having same as background
|
||||
break;
|
||||
default:
|
||||
// Rest of codes below 0x20 are not part of Level 1.5 or related to
|
||||
// mosaic graphics (non-text)
|
||||
break;
|
||||
}
|
||||
|
||||
if (leading_spaces) {
|
||||
if (next_char == 0x20) {
|
||||
next_char =
|
||||
0x20; // These characters result in a space if between start and end
|
||||
}
|
||||
if (start_pos == 0 || end_pos != 0) { // Not between start and end
|
||||
continue;
|
||||
}
|
||||
leading_spaces = false;
|
||||
}
|
||||
|
||||
switch (next_char) {
|
||||
case '&':
|
||||
next_string.append("&");
|
||||
|
@ -407,8 +497,25 @@ std::string EsParserTeletext::BuildText(const uint8_t* data_block,
|
|||
} break;
|
||||
}
|
||||
}
|
||||
if (end_pos == 0) {
|
||||
end_pos = kPayloadSize - 1;
|
||||
}
|
||||
|
||||
return RemoveTrailingSpaces(next_string);
|
||||
// Using start_pos and end_pos we approximated alignment of text
|
||||
// depending on the number of spaces to the left and right of the text.
|
||||
auto left_right_diff = start_pos - (kPayloadSize - 1 - end_pos);
|
||||
TextAlignment alignment;
|
||||
if (left_right_diff > 4) {
|
||||
alignment = TextAlignment::kRight;
|
||||
} else if (left_right_diff < -4) {
|
||||
alignment = TextAlignment::kLeft;
|
||||
} else {
|
||||
alignment = TextAlignment::kCenter;
|
||||
}
|
||||
const auto text_row = TextRow(
|
||||
{alignment, row, double_height, {TextFragment(text_style, next_string)}});
|
||||
|
||||
return text_row;
|
||||
}
|
||||
|
||||
void EsParserTeletext::ParsePacket26(const uint8_t* data_block) {
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <packager/media/base/text_stream_info.h>
|
||||
#include <packager/media/formats/mp2t/es_parser.h>
|
||||
|
||||
namespace shaka {
|
||||
|
@ -37,8 +38,15 @@ class EsParserTeletext : public EsParser {
|
|||
using RowColReplacementMap =
|
||||
std::unordered_map<uint8_t, std::unordered_map<uint8_t, std::string>>;
|
||||
|
||||
struct TextRow {
|
||||
TextAlignment alignment;
|
||||
int row_number;
|
||||
bool double_height;
|
||||
TextFragment fragment;
|
||||
};
|
||||
|
||||
struct TextBlock {
|
||||
std::vector<std::string> lines;
|
||||
std::vector<TextRow> rows;
|
||||
RowColReplacementMap packet_26_replacements;
|
||||
int64_t pts;
|
||||
};
|
||||
|
@ -48,10 +56,10 @@ class EsParserTeletext : public EsParser {
|
|||
const uint8_t* data_block,
|
||||
const uint8_t packet_nr,
|
||||
const uint8_t magazine,
|
||||
std::string& display_text);
|
||||
TextRow& display_text);
|
||||
void UpdateCharset();
|
||||
void SendPending(const uint16_t index, const int64_t pts);
|
||||
std::string BuildText(const uint8_t* data_block, const uint8_t row) const;
|
||||
TextRow BuildRow(const uint8_t* data_block, const uint8_t row) const;
|
||||
void ParsePacket26(const uint8_t* data_block);
|
||||
void UpdateNationalSubset(const uint8_t national_subset[13][3]);
|
||||
|
||||
|
|
|
@ -166,6 +166,65 @@ const uint8_t PES_8937764[] = {
|
|||
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x23, 0xc7, 0x75, 0x8c, 0x1c,
|
||||
0x04, 0x04, 0x04, 0x86, 0x4f, 0xce, 0x75, 0x75, 0x75, 0x8c, 0x8c};
|
||||
|
||||
// Start (packet0, page88) with packet26 and row 18 left (packet18)
|
||||
const uint8_t PES_867681[] = {
|
||||
0x10, 0x03, 0x2c, 0xf6, 0xe4, 0xa8, 0xa8, 0x0b, 0x0b, 0xa8, 0x0b, 0xa8,
|
||||
0x0b, 0xf4, 0xa8, 0xb3, 0x83, 0x32, 0xa2, 0x73, 0x2a, 0xa2, 0x73, 0x23,
|
||||
0x83, 0x73, 0x2a, 0xcb, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
|
||||
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x03,
|
||||
0x2c, 0xd4, 0xe4, 0xa8, 0x6d, 0xa8, 0x9e, 0xc9, 0x00, 0x4e, 0x93, 0xa7,
|
||||
0x90, 0x53, 0xa7, 0x2e, 0xfe, 0xff, 0x2e, 0xfe, 0xff, 0x2e, 0xfe, 0xff,
|
||||
0x2e, 0xfe, 0xff, 0x2e, 0xfe, 0xff, 0x2e, 0xfe, 0xff, 0x2e, 0xfe, 0xff,
|
||||
0x2e, 0xfe, 0xff, 0x2e, 0xfe, 0xff, 0x2e, 0xfe, 0xff, 0x03, 0x2c, 0xd6,
|
||||
0xe4, 0xa8, 0xe3, 0xb0, 0x04, 0x04, 0x04, 0x04, 0xd0, 0xd0, 0xb5, 0x32,
|
||||
0xae, 0xd6, 0xa7, 0x04, 0x85, 0x51, 0x51, 0x04, 0x04, 0x04, 0x04, 0x04,
|
||||
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
|
||||
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04};
|
||||
|
||||
// row 22 right (packet22)
|
||||
const uint8_t PES_871281[] = {
|
||||
0x10, 0x03, 0x2c, 0xf4, 0xe4, 0xa8, 0xd9, 0xb0, 0x04, 0x04, 0x04, 0x04,
|
||||
0x04, 0x04, 0xd0, 0xd0, 0xb5, 0x52, 0xa7, 0x04, 0x6e, 0x86, 0x97, 0xce,
|
||||
0x04, 0x86, 0xae, 0x1f, 0x04, 0xc7, 0xf7, 0xae, 0x4f, 0xce, 0x04, 0x26,
|
||||
0xe5, 0xa7, 0x2f, 0xa7, 0x75, 0x51, 0x51, 0x04, 0x04, 0x04, 0x04};
|
||||
|
||||
// End (packet 0, page 88)
|
||||
const uint8_t PES_1011695[] = {
|
||||
0x10, 0x03, 0x2c, 0xf6, 0xe4, 0xa8, 0xa8, 0x0b, 0x0b, 0xa8, 0x0b, 0xa8,
|
||||
0x0b, 0xf4, 0xa8, 0xb3, 0x83, 0x32, 0xa2, 0x73, 0x2a, 0xa2, 0x73, 0x23,
|
||||
0x83, 0x73, 0x2a, 0xcb, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
|
||||
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04};
|
||||
|
||||
// Start (packet0, page 88) with packet26 and row 20 (centered yellow)
|
||||
// (packet20)
|
||||
const uint8_t PES_1033297[] = {
|
||||
0x10, 0x03, 0x2c, 0xf6, 0xe4, 0xa8, 0xa8, 0x0b, 0x0b, 0xa8, 0x0b, 0xa8,
|
||||
0x0b, 0xf4, 0xa8, 0xb3, 0x83, 0x32, 0xa2, 0x73, 0x2a, 0xa2, 0x73, 0x23,
|
||||
0x83, 0x73, 0x2a, 0xcb, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
|
||||
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x03,
|
||||
0x2c, 0xd4, 0xe4, 0xa8, 0x6d, 0xa8, 0x06, 0xc9, 0x01, 0x62, 0x93, 0xa6,
|
||||
0x9e, 0xc9, 0x00, 0xf4, 0xa2, 0x86, 0x06, 0xa3, 0xa7, 0x2e, 0xfe, 0xff,
|
||||
0x2e, 0xfe, 0xff, 0x2e, 0xfe, 0xff, 0x2e, 0xfe, 0xff, 0x2e, 0xfe, 0xff,
|
||||
0x2e, 0xfe, 0xff, 0x2e, 0xfe, 0xff, 0x2e, 0xfe, 0xff, 0x03, 0x2c, 0xd6,
|
||||
0xe4, 0xa8, 0x31, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
|
||||
0x04, 0x04, 0x04, 0xb0, 0xc1, 0xd0, 0xd0, 0x52, 0xe5, 0x86, 0x97, 0x04,
|
||||
0x37, 0xf7, 0xae, 0x0e, 0xa7, 0x51, 0x51, 0x04, 0x04, 0x04, 0x04, 0x04,
|
||||
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04};
|
||||
|
||||
// row 22 centered blue on yellow background (packet22)
|
||||
const uint8_t PES_1036900[] = {
|
||||
0x10, 0x03, 0x2c, 0xf4, 0xe4, 0xa8, 0xd9, 0x04, 0x04, 0xb0, 0xc1, 0xb9,
|
||||
0x20, 0xd0, 0xd0, 0x37, 0xe5, 0x97, 0x76, 0x97, 0x2f, 0x97, 0x86, 0x2f,
|
||||
0x97, 0xf7, 0x76, 0x04, 0x86, 0x04, 0x37, 0xe5, 0x86, 0x37, 0xe6, 0xa7,
|
||||
0x46, 0x4f, 0xa7, 0x75, 0x51, 0x51, 0x04, 0x04, 0x04, 0x04, 0x04};
|
||||
|
||||
// End (packet 0, page 88)
|
||||
const uint8_t PES_1173713[] = {
|
||||
0x10, 0x03, 0x2c, 0xf6, 0xe4, 0xa8, 0xa8, 0x0b, 0x0b, 0xa8, 0x0b, 0xa8,
|
||||
0x0b, 0xf4, 0xa8, 0xb3, 0x83, 0x32, 0xa2, 0x73, 0x2a, 0xa2, 0x73, 0x23,
|
||||
0x83, 0x73, 0x2a, 0xcb, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
|
||||
0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04};
|
||||
|
||||
const uint32_t kPesPid = 123;
|
||||
|
||||
} // namespace
|
||||
|
@ -179,10 +238,12 @@ class EsParserTeletextTest : public ::testing::Test {
|
|||
void OnEmitTextSample(uint32_t pes_pid,
|
||||
std::shared_ptr<TextSample> text_sample) {
|
||||
text_sample_ = text_sample;
|
||||
text_samples_.push_back(text_sample);
|
||||
}
|
||||
|
||||
protected:
|
||||
std::shared_ptr<StreamInfo> stream_info_;
|
||||
std::vector<std::shared_ptr<TextSample>> text_samples_;
|
||||
std::shared_ptr<TextSample> text_sample_;
|
||||
};
|
||||
|
||||
|
@ -230,6 +291,13 @@ TEST_F(EsParserTeletextTest, pes_283413_line_emitted_on_next_pes) {
|
|||
EXPECT_EQ(283413, text_sample_->start_time());
|
||||
EXPECT_EQ(407876, text_sample_->EndTime());
|
||||
EXPECT_EQ("Bon dia!", text_sample_->body().body);
|
||||
EXPECT_EQ("black", text_sample_->body().style.backgroundColor);
|
||||
EXPECT_EQ("white", text_sample_->body().style.color);
|
||||
TextSettings settings = text_sample_->settings();
|
||||
EXPECT_EQ(TextAlignment::kCenter, settings.text_alignment);
|
||||
EXPECT_TRUE(settings.line.has_value());
|
||||
EXPECT_EQ(11, settings.line.value().value);
|
||||
EXPECT_EQ(TextUnitType::kLines, settings.line.value().type);
|
||||
}
|
||||
|
||||
TEST_F(EsParserTeletextTest, multiple_lines_with_same_pts) {
|
||||
|
@ -260,6 +328,98 @@ TEST_F(EsParserTeletextTest, multiple_lines_with_same_pts) {
|
|||
EXPECT_EQ("-Sí?", text_sample_->body().sub_fragments[0].body);
|
||||
EXPECT_TRUE(text_sample_->body().sub_fragments[1].newline);
|
||||
EXPECT_EQ("-Sí.", text_sample_->body().sub_fragments[2].body);
|
||||
TextSettings settings = text_sample_->settings();
|
||||
EXPECT_EQ(10, settings.line.value().value);
|
||||
EXPECT_EQ("ttx_10", settings.region);
|
||||
EXPECT_EQ(1, text_samples_.size());
|
||||
}
|
||||
|
||||
// separate_lines_with_slightly_different_pts has the original lines
|
||||
// 18 and 22, with different alignment, which means that they should
|
||||
// result in two parallel text samples.
|
||||
TEST_F(EsParserTeletextTest, separate_lines_with_slightly_different_pts) {
|
||||
auto on_new_stream = std::bind(&EsParserTeletextTest::OnNewStreamInfo, this,
|
||||
kPesPid, std::placeholders::_1);
|
||||
auto on_emit_text = std::bind(&EsParserTeletextTest::OnEmitTextSample, this,
|
||||
kPesPid, std::placeholders::_1);
|
||||
|
||||
std::unique_ptr<EsParserTeletext> es_parser_teletext(new EsParserTeletext(
|
||||
kPesPid, on_new_stream, on_emit_text, DESCRIPTOR, 12));
|
||||
|
||||
auto parse_result =
|
||||
es_parser_teletext->Parse(PES_867681, sizeof(PES_867681), 867681, 0);
|
||||
EXPECT_TRUE(parse_result);
|
||||
|
||||
parse_result =
|
||||
es_parser_teletext->Parse(PES_871281, sizeof(PES_871281), 871281, 0);
|
||||
EXPECT_TRUE(parse_result);
|
||||
|
||||
parse_result =
|
||||
es_parser_teletext->Parse(PES_1011695, sizeof(PES_1011695), 1011695, 0);
|
||||
EXPECT_TRUE(parse_result);
|
||||
|
||||
EXPECT_NE(nullptr, text_sample_.get());
|
||||
EXPECT_EQ(2, text_samples_.size());
|
||||
// The subtitles should get the same start and end time
|
||||
EXPECT_EQ(867681, text_samples_[0]->start_time());
|
||||
EXPECT_EQ(867681, text_samples_[1]->start_time());
|
||||
EXPECT_EQ(1011695, text_samples_[0]->EndTime());
|
||||
EXPECT_EQ(1011695, text_samples_[0]->EndTime());
|
||||
EXPECT_EQ(1, text_samples_[0]->body().sub_fragments.size());
|
||||
EXPECT_EQ(1, text_samples_[1]->body().sub_fragments.size());
|
||||
EXPECT_EQ("-Luke !", text_samples_[0]->body().sub_fragments[0].body);
|
||||
EXPECT_EQ("ttx_9", text_samples_[0]->settings().region);
|
||||
EXPECT_EQ(TextAlignment::kLeft, text_samples_[0]->settings().text_alignment);
|
||||
EXPECT_EQ("-Je vais aux cours d'été.",
|
||||
text_samples_[1]->body().sub_fragments[0].body);
|
||||
EXPECT_EQ(11, text_samples_[1]->settings().line.value().value);
|
||||
EXPECT_EQ("ttx_11", text_samples_[1]->settings().region);
|
||||
EXPECT_EQ(TextAlignment::kCenter,
|
||||
text_samples_[1]->settings().text_alignment);
|
||||
}
|
||||
|
||||
// consecutive_lines_with_slightly_different_pts has the original lines
|
||||
// 20 and 22 with same alignment, which means that they should
|
||||
// result in one text sample with two lines.
|
||||
TEST_F(EsParserTeletextTest, consecutive_lines_with_slightly_different_pts) {
|
||||
auto on_new_stream = std::bind(&EsParserTeletextTest::OnNewStreamInfo, this,
|
||||
kPesPid, std::placeholders::_1);
|
||||
auto on_emit_text = std::bind(&EsParserTeletextTest::OnEmitTextSample, this,
|
||||
kPesPid, std::placeholders::_1);
|
||||
|
||||
std::unique_ptr<EsParserTeletext> es_parser_teletext(new EsParserTeletext(
|
||||
kPesPid, on_new_stream, on_emit_text, DESCRIPTOR, 12));
|
||||
|
||||
auto parse_result =
|
||||
es_parser_teletext->Parse(PES_1033297, sizeof(PES_1033297), 1033297, 0);
|
||||
EXPECT_TRUE(parse_result);
|
||||
|
||||
parse_result =
|
||||
es_parser_teletext->Parse(PES_1036900, sizeof(PES_1036900), 1036900, 0);
|
||||
EXPECT_TRUE(parse_result);
|
||||
|
||||
parse_result =
|
||||
es_parser_teletext->Parse(PES_1173713, sizeof(PES_1173713), 1173713, 0);
|
||||
EXPECT_TRUE(parse_result);
|
||||
|
||||
EXPECT_NE(nullptr, text_sample_.get());
|
||||
EXPECT_EQ(1, text_samples_.size());
|
||||
// The subtitles should get the same start and end time
|
||||
EXPECT_EQ(1033297, text_sample_->start_time());
|
||||
EXPECT_EQ(1173713, text_sample_->EndTime());
|
||||
EXPECT_EQ(3, text_sample_->body().sub_fragments.size());
|
||||
TextSettings settings = text_sample_->settings();
|
||||
EXPECT_EQ(10, settings.line.value().value);
|
||||
EXPECT_EQ("ttx_10", settings.region);
|
||||
EXPECT_EQ(TextAlignment::kCenter, settings.text_alignment);
|
||||
EXPECT_EQ("J'ai loupé", text_sample_->body().sub_fragments[0].body);
|
||||
EXPECT_EQ("yellow", text_sample_->body().sub_fragments[0].style.color);
|
||||
EXPECT_TRUE(text_sample_->body().sub_fragments[1].newline);
|
||||
EXPECT_EQ("l'initiation à l'algèbre.",
|
||||
text_sample_->body().sub_fragments[2].body);
|
||||
EXPECT_EQ("yellow",
|
||||
text_sample_->body().sub_fragments[2].style.backgroundColor);
|
||||
EXPECT_EQ("blue", text_sample_->body().sub_fragments[2].style.color);
|
||||
}
|
||||
|
||||
} // namespace mp2t
|
||||
|
|
|
@ -18,6 +18,7 @@ namespace ttml {
|
|||
namespace {
|
||||
|
||||
constexpr const char* kRegionIdPrefix = "_shaka_region_";
|
||||
constexpr const char* kRegionTeletextPrefix = "ttx_";
|
||||
|
||||
std::string ToTtmlTime(int64_t time, int32_t timescale) {
|
||||
int64_t remaining = time * 1000 / timescale;
|
||||
|
@ -54,6 +55,18 @@ void TtmlGenerator::Initialize(const std::map<std::string, TextRegion>& regions,
|
|||
regions_ = regions;
|
||||
language_ = language;
|
||||
time_scale_ = time_scale;
|
||||
// Add ebu_tt_d_regions
|
||||
float step = 74.1f / 11;
|
||||
for (int i = 0; i < 12; i++) {
|
||||
TextRegion region;
|
||||
float verPos = 10.0 + int(float(step) * float(i));
|
||||
region.width = TextNumber(80, TextUnitType::kPercent);
|
||||
region.height = TextNumber(15, TextUnitType::kPercent);
|
||||
region.window_anchor_x = TextNumber(10, TextUnitType::kPercent);
|
||||
region.window_anchor_y = TextNumber(verPos, TextUnitType::kPercent);
|
||||
const std::string id = kRegionTeletextPrefix + std::to_string(i);
|
||||
regions_.emplace(id, region);
|
||||
}
|
||||
}
|
||||
|
||||
void TtmlGenerator::AddSample(const TextSample& sample) {
|
||||
|
@ -66,72 +79,92 @@ void TtmlGenerator::Reset() {
|
|||
|
||||
bool TtmlGenerator::Dump(std::string* result) const {
|
||||
xml::XmlNode root("tt");
|
||||
bool ebuTTDFormat = isEbuTTTD();
|
||||
RCHECK(root.SetStringAttribute("xmlns", kTtNamespace));
|
||||
RCHECK(root.SetStringAttribute("xmlns:tts",
|
||||
"http://www.w3.org/ns/ttml#styling"));
|
||||
|
||||
bool did_log = false;
|
||||
xml::XmlNode head("head");
|
||||
RCHECK(root.SetStringAttribute("xmlns:tts",
|
||||
"http://www.w3.org/ns/ttml#styling"));
|
||||
RCHECK(root.SetStringAttribute("xml:lang", language_));
|
||||
for (const auto& pair : regions_) {
|
||||
if (!did_log && (pair.second.region_anchor_x.value != 0 &&
|
||||
pair.second.region_anchor_y.value != 0)) {
|
||||
LOG(WARNING) << "TTML doesn't support non-0 region anchor";
|
||||
did_log = true;
|
||||
|
||||
if (ebuTTDFormat) {
|
||||
RCHECK(root.SetStringAttribute("xmlns:ttp",
|
||||
"http://www.w3.org/ns/ttml#parameter"));
|
||||
RCHECK(root.SetStringAttribute("xmlns:ttm",
|
||||
"http://www.w3.org/ns/ttml#metadata"));
|
||||
RCHECK(root.SetStringAttribute("xmlns:ebuttm", "urn:ebu:tt:metadata"));
|
||||
RCHECK(root.SetStringAttribute("xmlns:ebutts", "urn:ebu:tt:style"));
|
||||
RCHECK(root.SetStringAttribute("xml:space", "default"));
|
||||
RCHECK(root.SetStringAttribute("ttp:timeBase", "media"));
|
||||
RCHECK(root.SetStringAttribute("ttp:cellResolution", "32 15"));
|
||||
}
|
||||
|
||||
xml::XmlNode region("region");
|
||||
const auto origin =
|
||||
ToTtmlSize(pair.second.window_anchor_x, pair.second.window_anchor_y);
|
||||
const auto extent = ToTtmlSize(pair.second.width, pair.second.height);
|
||||
RCHECK(region.SetStringAttribute("xml:id", pair.first));
|
||||
RCHECK(region.SetStringAttribute("tts:origin", origin));
|
||||
RCHECK(region.SetStringAttribute("tts:extent", extent));
|
||||
RCHECK(head.AddChild(std::move(region)));
|
||||
}
|
||||
RCHECK(root.AddChild(std::move(head)));
|
||||
|
||||
size_t image_count = 0;
|
||||
xml::XmlNode head("head");
|
||||
xml::XmlNode styling("styling");
|
||||
xml::XmlNode metadata("metadata");
|
||||
xml::XmlNode layout("layout");
|
||||
RCHECK(addRegions(layout));
|
||||
|
||||
xml::XmlNode body("body");
|
||||
if (ebuTTDFormat) {
|
||||
RCHECK(body.SetStringAttribute("style", "default"));
|
||||
}
|
||||
size_t image_count = 0;
|
||||
std::unordered_set<std::string> fragmentStyles;
|
||||
xml::XmlNode div("div");
|
||||
for (const auto& sample : samples_) {
|
||||
RCHECK(AddSampleToXml(sample, &div, &metadata, &image_count));
|
||||
RCHECK(
|
||||
AddSampleToXml(sample, &div, &metadata, fragmentStyles, &image_count));
|
||||
}
|
||||
RCHECK(body.AddChild(std::move(div)));
|
||||
if (image_count > 0) {
|
||||
RCHECK(root.SetStringAttribute(
|
||||
"xmlns:smpte", "http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt"));
|
||||
RCHECK(root.AddChild(std::move(metadata)));
|
||||
}
|
||||
RCHECK(body.AddChild(std::move(div)));
|
||||
RCHECK(head.AddChild(std::move(metadata)));
|
||||
RCHECK(addStyling(styling, fragmentStyles));
|
||||
RCHECK(head.AddChild(std::move(styling)));
|
||||
RCHECK(head.AddChild(std::move(layout)));
|
||||
RCHECK(root.AddChild(std::move(head)));
|
||||
|
||||
RCHECK(root.AddChild(std::move(body)));
|
||||
|
||||
*result = root.ToString(/* comment= */ "");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TtmlGenerator::AddSampleToXml(const TextSample& sample,
|
||||
bool TtmlGenerator::AddSampleToXml(
|
||||
const TextSample& sample,
|
||||
xml::XmlNode* body,
|
||||
xml::XmlNode* metadata,
|
||||
std::unordered_set<std::string>& fragmentStyles,
|
||||
size_t* image_count) const {
|
||||
xml::XmlNode p("p");
|
||||
if (!isEbuTTTD()) {
|
||||
RCHECK(p.SetStringAttribute("xml:space", "preserve"));
|
||||
}
|
||||
RCHECK(p.SetStringAttribute("begin",
|
||||
ToTtmlTime(sample.start_time(), time_scale_)));
|
||||
RCHECK(
|
||||
p.SetStringAttribute("end", ToTtmlTime(sample.EndTime(), time_scale_)));
|
||||
RCHECK(ConvertFragmentToXml(sample.body(), &p, metadata, image_count));
|
||||
RCHECK(ConvertFragmentToXml(sample.body(), &p, metadata, fragmentStyles,
|
||||
image_count));
|
||||
if (!sample.id().empty())
|
||||
RCHECK(p.SetStringAttribute("xml:id", sample.id()));
|
||||
|
||||
const auto& settings = sample.settings();
|
||||
if (settings.line || settings.position || settings.width || settings.height) {
|
||||
// TTML positioning needs to be from a region.
|
||||
bool regionFound = false;
|
||||
if (!settings.region.empty()) {
|
||||
LOG(WARNING)
|
||||
<< "Using both text regions and positioning isn't supported in TTML";
|
||||
auto reg = regions_.find(settings.region);
|
||||
if (reg != regions_.end()) {
|
||||
regionFound = true;
|
||||
RCHECK(p.SetStringAttribute("region", settings.region));
|
||||
}
|
||||
}
|
||||
|
||||
if (!regionFound && (settings.line || settings.position || settings.width ||
|
||||
settings.height)) {
|
||||
// TTML positioning needs to be from a region.
|
||||
const auto origin = ToTtmlSize(
|
||||
settings.position.value_or(TextNumber(0, TextUnitType::kPixels)),
|
||||
settings.line.value_or(TextNumber(0, TextUnitType::kPixels)));
|
||||
|
@ -146,8 +179,6 @@ bool TtmlGenerator::AddSampleToXml(const TextSample& sample,
|
|||
RCHECK(region.SetStringAttribute("tts:extent", extent));
|
||||
RCHECK(p.SetStringAttribute("region", id));
|
||||
RCHECK(body->AddChild(std::move(region)));
|
||||
} else if (!settings.region.empty()) {
|
||||
RCHECK(p.SetStringAttribute("region", settings.region));
|
||||
}
|
||||
|
||||
if (settings.writing_direction != WritingDirection::kHorizontal) {
|
||||
|
@ -179,19 +210,22 @@ bool TtmlGenerator::AddSampleToXml(const TextSample& sample,
|
|||
return true;
|
||||
}
|
||||
|
||||
bool TtmlGenerator::ConvertFragmentToXml(const TextFragment& body,
|
||||
bool TtmlGenerator::ConvertFragmentToXml(
|
||||
const TextFragment& body,
|
||||
xml::XmlNode* parent,
|
||||
xml::XmlNode* metadata,
|
||||
std::unordered_set<std::string>& fragmentStyles,
|
||||
size_t* image_count) const {
|
||||
if (body.newline) {
|
||||
xml::XmlNode br("br");
|
||||
return parent->AddChild(std::move(br));
|
||||
}
|
||||
|
||||
// If we have new styles, add a new <span>.
|
||||
xml::XmlNode span("span");
|
||||
xml::XmlNode* node = parent;
|
||||
if (body.style.bold || body.style.italic || body.style.underline) {
|
||||
bool useSpan =
|
||||
(body.style.bold || body.style.italic || body.style.underline ||
|
||||
!body.style.color.empty() || !body.style.backgroundColor.empty());
|
||||
if (useSpan) {
|
||||
node = &span;
|
||||
if (body.style.bold) {
|
||||
RCHECK(span.SetStringAttribute("tts:fontWeight",
|
||||
|
@ -206,6 +240,20 @@ bool TtmlGenerator::ConvertFragmentToXml(const TextFragment& body,
|
|||
"tts:textDecoration",
|
||||
*body.style.underline ? "underline" : "noUnderline"));
|
||||
}
|
||||
std::string color = "white";
|
||||
std::string backgroundColor = "black";
|
||||
|
||||
if (!body.style.color.empty()) {
|
||||
color = body.style.color;
|
||||
}
|
||||
|
||||
if (!body.style.backgroundColor.empty()) {
|
||||
backgroundColor = body.style.backgroundColor;
|
||||
}
|
||||
|
||||
const std::string fragStyle = color + "_" + backgroundColor;
|
||||
fragmentStyles.insert(fragStyle);
|
||||
RCHECK(span.SetStringAttribute("style", fragStyle));
|
||||
}
|
||||
|
||||
if (!body.body.empty()) {
|
||||
|
@ -226,16 +274,91 @@ bool TtmlGenerator::ConvertFragmentToXml(const TextFragment& body,
|
|||
RCHECK(node->SetStringAttribute("smpte:backgroundImage", "#" + id));
|
||||
} else {
|
||||
for (const auto& frag : body.sub_fragments) {
|
||||
if (!ConvertFragmentToXml(frag, node, metadata, image_count))
|
||||
if (!ConvertFragmentToXml(frag, node, metadata, fragmentStyles,
|
||||
image_count))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (body.style.bold || body.style.italic || body.style.underline)
|
||||
if (useSpan)
|
||||
RCHECK(parent->AddChild(std::move(span)));
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::string> TtmlGenerator::usedRegions() const {
|
||||
std::vector<std::string> uRegions;
|
||||
for (const auto& sample : samples_) {
|
||||
if (!sample.settings().region.empty()) {
|
||||
uRegions.push_back(sample.settings().region);
|
||||
}
|
||||
}
|
||||
return uRegions;
|
||||
}
|
||||
|
||||
bool TtmlGenerator::addRegions(xml::XmlNode& layout) const {
|
||||
auto regNames = usedRegions();
|
||||
for (const auto& r : regions_) {
|
||||
bool used = false;
|
||||
for (const auto& name : regNames) {
|
||||
if (r.first == name) {
|
||||
used = true;
|
||||
}
|
||||
}
|
||||
if (used) {
|
||||
xml::XmlNode region("region");
|
||||
const auto origin =
|
||||
ToTtmlSize(r.second.window_anchor_x, r.second.window_anchor_y);
|
||||
const auto extent = ToTtmlSize(r.second.width, r.second.height);
|
||||
RCHECK(region.SetStringAttribute("xml:id", r.first));
|
||||
RCHECK(region.SetStringAttribute("tts:origin", origin));
|
||||
RCHECK(region.SetStringAttribute("tts:extent", extent));
|
||||
RCHECK(region.SetStringAttribute("tts:overflow", "visible"));
|
||||
RCHECK(layout.AddChild(std::move(region)));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TtmlGenerator::addStyling(
|
||||
xml::XmlNode& styling,
|
||||
const std::unordered_set<std::string>& fragmentStyles) const {
|
||||
if (fragmentStyles.empty()) {
|
||||
return true;
|
||||
}
|
||||
// Add default style
|
||||
xml::XmlNode defaultStyle("style");
|
||||
RCHECK(defaultStyle.SetStringAttribute("xml:id", "default"));
|
||||
RCHECK(defaultStyle.SetStringAttribute("tts:fontStyle", "normal"));
|
||||
RCHECK(defaultStyle.SetStringAttribute("tts:fontFamily", "sansSerif"));
|
||||
RCHECK(defaultStyle.SetStringAttribute("tts:fontSize", "100%"));
|
||||
RCHECK(defaultStyle.SetStringAttribute("tts:lineHeight", "normal"));
|
||||
RCHECK(defaultStyle.SetStringAttribute("tts:textAlign", "center"));
|
||||
RCHECK(defaultStyle.SetStringAttribute("ebutts:linePadding", "0.5c"));
|
||||
RCHECK(styling.AddChild(std::move(defaultStyle)));
|
||||
|
||||
for (const auto& name : fragmentStyles) {
|
||||
auto pos = name.find('_');
|
||||
auto color = name.substr(0, pos);
|
||||
auto backgroundColor = name.substr(pos + 1, name.size());
|
||||
xml::XmlNode fragStyle("style");
|
||||
RCHECK(fragStyle.SetStringAttribute("xml:id", name));
|
||||
RCHECK(
|
||||
fragStyle.SetStringAttribute("tts:backgroundColor", backgroundColor));
|
||||
RCHECK(fragStyle.SetStringAttribute("tts:color", color));
|
||||
RCHECK(styling.AddChild(std::move(fragStyle)));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TtmlGenerator::isEbuTTTD() const {
|
||||
for (const auto& sample : samples_) {
|
||||
if (sample.settings().region.rfind(kRegionTeletextPrefix, 0) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace ttml
|
||||
} // namespace media
|
||||
} // namespace shaka
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include <list>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <packager/media/base/text_sample.h>
|
||||
#include <packager/media/base/text_stream_info.h>
|
||||
|
@ -38,12 +39,20 @@ class TtmlGenerator {
|
|||
bool AddSampleToXml(const TextSample& sample,
|
||||
xml::XmlNode* body,
|
||||
xml::XmlNode* metadata,
|
||||
std::unordered_set<std::string>& fragmentStyles,
|
||||
size_t* image_count) const;
|
||||
bool ConvertFragmentToXml(const TextFragment& fragment,
|
||||
xml::XmlNode* parent,
|
||||
xml::XmlNode* metadata,
|
||||
std::unordered_set<std::string>& fragmentStyles,
|
||||
size_t* image_count) const;
|
||||
|
||||
bool addStyling(xml::XmlNode& styling,
|
||||
const std::unordered_set<std::string>& fragmentStyles) const;
|
||||
bool addRegions(xml::XmlNode& layout) const;
|
||||
std::vector<std::string> usedRegions() const;
|
||||
bool isEbuTTTD() const;
|
||||
|
||||
std::list<TextSample> samples_;
|
||||
std::map<std::string, TextRegion> regions_;
|
||||
std::string language_;
|
||||
|
|
|
@ -64,7 +64,11 @@ TEST_F(TtmlMuxerTest, WithOneSegmentAndWithOneSample) {
|
|||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
"<tt xmlns=\"http://www.w3.org/ns/ttml\" "
|
||||
"xmlns:tts=\"http://www.w3.org/ns/ttml#styling\" xml:lang=\"\">\n"
|
||||
" <head/>\n"
|
||||
" <head>\n"
|
||||
" <metadata/>\n"
|
||||
" <styling/>\n"
|
||||
" <layout/>\n"
|
||||
" </head>\n"
|
||||
" <body>\n"
|
||||
" <div>\n"
|
||||
" <p xml:space=\"preserve\" begin=\"00:00:05.000\" "
|
||||
|
@ -83,7 +87,11 @@ TEST_F(TtmlMuxerTest, MultipleFragmentsWithNewlines) {
|
|||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
"<tt xmlns=\"http://www.w3.org/ns/ttml\" "
|
||||
"xmlns:tts=\"http://www.w3.org/ns/ttml#styling\" xml:lang=\"\">\n"
|
||||
" <head/>\n"
|
||||
" <head>\n"
|
||||
" <metadata/>\n"
|
||||
" <styling/>\n"
|
||||
" <layout/>\n"
|
||||
" </head>\n"
|
||||
" <body>\n"
|
||||
" <div>\n"
|
||||
" <p xml:space=\"preserve\" begin=\"00:00:05.000\" "
|
||||
|
@ -106,7 +114,11 @@ TEST_F(TtmlMuxerTest, HandlesStyles) {
|
|||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
"<tt xmlns=\"http://www.w3.org/ns/ttml\" "
|
||||
"xmlns:tts=\"http://www.w3.org/ns/ttml#styling\" xml:lang=\"\">\n"
|
||||
" <head/>\n"
|
||||
" <head>\n"
|
||||
" <metadata/>\n"
|
||||
" <styling/>\n"
|
||||
" <layout/>\n"
|
||||
" </head>\n"
|
||||
" <body>\n"
|
||||
" <div>\n"
|
||||
" <p xml:space=\"preserve\" begin=\"00:00:05.000\" "
|
||||
|
@ -136,8 +148,12 @@ TEST_F(TtmlMuxerTest, HandlesRegions) {
|
|||
"<tt xmlns=\"http://www.w3.org/ns/ttml\" "
|
||||
"xmlns:tts=\"http://www.w3.org/ns/ttml#styling\" xml:lang=\"\">\n"
|
||||
" <head>\n"
|
||||
" <metadata/>\n"
|
||||
" <styling/>\n"
|
||||
" <layout>\n"
|
||||
" <region xml:id=\"foo\" tts:origin=\"20px 40px\" "
|
||||
"tts:extent=\"22% 33%\"/>\n"
|
||||
"tts:extent=\"22% 33%\" tts:overflow=\"visible\"/>\n"
|
||||
" </layout>\n"
|
||||
" </head>\n"
|
||||
" <body>\n"
|
||||
" <div>\n"
|
||||
|
@ -166,7 +182,11 @@ TEST_F(TtmlMuxerTest, HandlesLanguage) {
|
|||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
"<tt xmlns=\"http://www.w3.org/ns/ttml\" "
|
||||
"xmlns:tts=\"http://www.w3.org/ns/ttml#styling\" xml:lang=\"foo\">\n"
|
||||
" <head/>\n"
|
||||
" <head>\n"
|
||||
" <metadata/>\n"
|
||||
" <styling/>\n"
|
||||
" <layout/>\n"
|
||||
" </head>\n"
|
||||
" <body>\n"
|
||||
" <div>\n"
|
||||
" <p xml:space=\"preserve\" begin=\"00:00:05.000\" "
|
||||
|
@ -187,7 +207,11 @@ TEST_F(TtmlMuxerTest, HandlesPosition) {
|
|||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
"<tt xmlns=\"http://www.w3.org/ns/ttml\" "
|
||||
"xmlns:tts=\"http://www.w3.org/ns/ttml#styling\" xml:lang=\"\">\n"
|
||||
" <head/>\n"
|
||||
" <head>\n"
|
||||
" <metadata/>\n"
|
||||
" <styling/>\n"
|
||||
" <layout/>\n"
|
||||
" </head>\n"
|
||||
" <body>\n"
|
||||
" <div>\n"
|
||||
" <region xml:id=\"_shaka_region_0\" tts:origin=\"30% 4em\" "
|
||||
|
@ -213,7 +237,11 @@ TEST_F(TtmlMuxerTest, HandlesOtherSettings) {
|
|||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
"<tt xmlns=\"http://www.w3.org/ns/ttml\" "
|
||||
"xmlns:tts=\"http://www.w3.org/ns/ttml#styling\" xml:lang=\"\">\n"
|
||||
" <head/>\n"
|
||||
" <head>\n"
|
||||
" <metadata/>\n"
|
||||
" <styling/>\n"
|
||||
" <layout/>\n"
|
||||
" </head>\n"
|
||||
" <body>\n"
|
||||
" <div>\n"
|
||||
" <p xml:space=\"preserve\" begin=\"00:00:05.000\" "
|
||||
|
@ -237,7 +265,11 @@ TEST_F(TtmlMuxerTest, HandlesCueId) {
|
|||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
"<tt xmlns=\"http://www.w3.org/ns/ttml\" "
|
||||
"xmlns:tts=\"http://www.w3.org/ns/ttml#styling\" xml:lang=\"\">\n"
|
||||
" <head/>\n"
|
||||
" <head>\n"
|
||||
" <metadata/>\n"
|
||||
" <styling/>\n"
|
||||
" <layout/>\n"
|
||||
" </head>\n"
|
||||
" <body>\n"
|
||||
" <div>\n"
|
||||
" <p xml:space=\"preserve\" begin=\"00:00:05.000\" "
|
||||
|
@ -260,8 +292,12 @@ TEST_F(TtmlMuxerTest, EscapesSpecialChars) {
|
|||
"xmlns:tts=\"http://www.w3.org/ns/ttml#styling\" "
|
||||
"xml:lang=\"foo&"a\">\n"
|
||||
" <head>\n"
|
||||
" <metadata/>\n"
|
||||
" <styling/>\n"
|
||||
" <layout>\n"
|
||||
" <region xml:id=\"<a&"\" tts:origin=\"0% 0%\" "
|
||||
"tts:extent=\"100% 100%\"/>\n"
|
||||
"tts:extent=\"100% 100%\" tts:overflow=\"visible\"/>\n"
|
||||
" </layout>\n"
|
||||
" </head>\n"
|
||||
" <body>\n"
|
||||
" <div>\n"
|
||||
|
@ -287,7 +323,11 @@ TEST_F(TtmlMuxerTest, HandlesReset) {
|
|||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
"<tt xmlns=\"http://www.w3.org/ns/ttml\" "
|
||||
"xmlns:tts=\"http://www.w3.org/ns/ttml#styling\" xml:lang=\"foobar\">\n"
|
||||
" <head/>\n"
|
||||
" <head>\n"
|
||||
" <metadata/>\n"
|
||||
" <styling/>\n"
|
||||
" <layout/>\n"
|
||||
" </head>\n"
|
||||
" <body>\n"
|
||||
" <div>\n"
|
||||
" <p xml:space=\"preserve\" begin=\"00:00:05.000\" "
|
||||
|
@ -299,7 +339,11 @@ TEST_F(TtmlMuxerTest, HandlesReset) {
|
|||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
"<tt xmlns=\"http://www.w3.org/ns/ttml\" "
|
||||
"xmlns:tts=\"http://www.w3.org/ns/ttml#styling\" xml:lang=\"foobar\">\n"
|
||||
" <head/>\n"
|
||||
" <head>\n"
|
||||
" <metadata/>\n"
|
||||
" <styling/>\n"
|
||||
" <layout/>\n"
|
||||
" </head>\n"
|
||||
" <body>\n"
|
||||
" <div>\n"
|
||||
" <p xml:space=\"preserve\" begin=\"00:00:08.000\" "
|
||||
|
@ -332,11 +376,15 @@ TEST_F(TtmlMuxerTest, HandlesImage) {
|
|||
"<tt xmlns=\"http://www.w3.org/ns/ttml\" "
|
||||
"xmlns:tts=\"http://www.w3.org/ns/ttml#styling\" xml:lang=\"\" "
|
||||
"xmlns:smpte=\"http://www.smpte-ra.org/schemas/2052-1/2010/smpte-tt\">\n"
|
||||
" <head/>\n"
|
||||
" <head>\n"
|
||||
" <metadata>\n"
|
||||
" <smpte:image imageType=\"PNG\" encoding=\"Base64\" xml:id=\"img_1\">"
|
||||
" <smpte:image imageType=\"PNG\" encoding=\"Base64\" "
|
||||
"xml:id=\"img_1\">"
|
||||
"AQID</smpte:image>\n"
|
||||
" </metadata>\n"
|
||||
" <styling/>\n"
|
||||
" <layout/>\n"
|
||||
" </head>\n"
|
||||
" <body>\n"
|
||||
" <div>\n"
|
||||
" <p xml:space=\"preserve\" begin=\"00:00:05.000\" "
|
||||
|
@ -357,7 +405,11 @@ TEST_F(TtmlMuxerTest, FormatsTimeWithFixedNumberOfDigits) {
|
|||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
"<tt xmlns=\"http://www.w3.org/ns/ttml\" "
|
||||
"xmlns:tts=\"http://www.w3.org/ns/ttml#styling\" xml:lang=\"\">\n"
|
||||
" <head/>\n"
|
||||
" <head>\n"
|
||||
" <metadata/>\n"
|
||||
" <styling/>\n"
|
||||
" <layout/>\n"
|
||||
" </head>\n"
|
||||
" <body>\n"
|
||||
" <div>\n"
|
||||
" <p xml:space=\"preserve\" begin=\"00:00:00.000\" "
|
||||
|
@ -373,6 +425,51 @@ TEST_F(TtmlMuxerTest, FormatsTimeWithFixedNumberOfDigits) {
|
|||
ParseSingleCue(kExpectedOutput, properties);
|
||||
}
|
||||
|
||||
TEST_F(TtmlMuxerTest, HandlesTeleTextToEbuTTD) {
|
||||
const char* kExpectedOutput =
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
"<tt xmlns=\"http://www.w3.org/ns/ttml\" "
|
||||
"xmlns:tts=\"http://www.w3.org/ns/ttml#styling\" xml:lang=\"\" "
|
||||
"xmlns:ttp=\"http://www.w3.org/ns/ttml#parameter\" "
|
||||
"xmlns:ttm=\"http://www.w3.org/ns/ttml#metadata\" "
|
||||
"xmlns:ebuttm=\"urn:ebu:tt:metadata\" xmlns:ebutts=\"urn:ebu:tt:style\" "
|
||||
"xml:space=\"default\" "
|
||||
"ttp:timeBase=\"media\" ttp:cellResolution=\"32 15\">\n"
|
||||
" <head>\n"
|
||||
" <metadata/>\n"
|
||||
" <styling>\n"
|
||||
" <style xml:id=\"default\" tts:fontStyle=\"normal\" "
|
||||
"tts:fontFamily=\"sansSerif\" tts:fontSize=\"100%\" "
|
||||
"tts:lineHeight=\"normal\" "
|
||||
"tts:textAlign=\"center\" ebutts:linePadding=\"0.5c\"/>\n"
|
||||
" <style xml:id=\"red_cyan\" tts:backgroundColor=\"cyan\" "
|
||||
"tts:color=\"red\"/>\n"
|
||||
" </styling>\n"
|
||||
" <layout>\n"
|
||||
" <region xml:id=\"ttx_9\" tts:origin=\"10% 70%\" "
|
||||
"tts:extent=\"80% 15%\" tts:overflow=\"visible\"/>\n"
|
||||
" </layout>\n"
|
||||
" </head>\n"
|
||||
" <body style=\"default\">\n"
|
||||
" <div>\n"
|
||||
" <p begin=\"00:00:05.000\" end=\"00:00:06.000\" region=\"ttx_9\">\n"
|
||||
" <span style=\"red_cyan\">teletext to EBU-TT-D</span>\n"
|
||||
" </p>\n"
|
||||
" </div>\n"
|
||||
" </body>\n"
|
||||
"</tt>\n";
|
||||
|
||||
TestProperties properties;
|
||||
properties.settings.line.emplace(9, TextUnitType::kLines);
|
||||
properties.settings.region = "ttx_9";
|
||||
properties.settings.height.emplace(1, TextUnitType::kLines);
|
||||
properties.body.body = "teletext to EBU-TT-D";
|
||||
properties.body.style.color = "red";
|
||||
properties.body.style.backgroundColor = "cyan";
|
||||
|
||||
ParseSingleCue(kExpectedOutput, properties);
|
||||
}
|
||||
|
||||
} // namespace ttml
|
||||
} // namespace media
|
||||
} // namespace shaka
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cinttypes>
|
||||
#include <cmath>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <absl/log/check.h>
|
||||
|
@ -23,6 +24,8 @@ namespace media {
|
|||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kRegionTeletextPrefix = "ttx_";
|
||||
|
||||
bool GetTotalMilliseconds(uint64_t hours,
|
||||
uint64_t minutes,
|
||||
uint64_t seconds,
|
||||
|
@ -228,7 +231,9 @@ std::string FloatToString(double number) {
|
|||
|
||||
std::string WebVttSettingsToString(const TextSettings& settings) {
|
||||
std::string ret;
|
||||
if (!settings.region.empty()) {
|
||||
if (!settings.region.empty() &&
|
||||
settings.region.find(kRegionTeletextPrefix) != 0) {
|
||||
// Don't add teletext ttx_ regions, since accompanied by global line numbers
|
||||
ret += " region:";
|
||||
ret += settings.region;
|
||||
}
|
||||
|
@ -241,7 +246,8 @@ std::string WebVttSettingsToString(const TextSettings& settings) {
|
|||
break;
|
||||
case TextUnitType::kLines:
|
||||
ret += " line:";
|
||||
ret += FloatToString(settings.line->value);
|
||||
// The line number should be an integer
|
||||
ret += FloatToString(std::round(settings.line->value));
|
||||
break;
|
||||
case TextUnitType::kPixels:
|
||||
LOG(WARNING) << "WebVTT doesn't support pixel line settings";
|
||||
|
|
|
@ -160,6 +160,16 @@ TEST(WebVttUtilsTest, SettingsToString) {
|
|||
"region:foo line:27% position:42% size:54% direction:rl align:end");
|
||||
}
|
||||
|
||||
TEST(WebVttUtilsTest, TeletextSettingsToStringRemovesRegionOutputsIntegerLine) {
|
||||
TextSettings settings;
|
||||
settings.region = "ttx_9";
|
||||
settings.line = TextNumber(9.5, TextUnitType::kLines);
|
||||
settings.text_alignment = TextAlignment::kCenter;
|
||||
|
||||
const auto actual = WebVttSettingsToString(settings);
|
||||
EXPECT_EQ(actual, "line:10 align:center");
|
||||
}
|
||||
|
||||
TEST(WebVttUtilsTest, SettingsToString_IgnoresDefaults) {
|
||||
TextSettings settings;
|
||||
settings.region = "foo";
|
||||
|
|
Loading…
Reference in New Issue