Add DVB-sub parser

Note that this only supports a single page within the DVB-sub stream.
Multiple pages will be merged together.  A follow-up will allow
selecting a specific page.

This only supports outputting using TTML or MP4+TTML; you cannot have
DVB-sub output nor can you output it in WebVTT.  Since DVB-sub
uses images, it is hard to impossible to do this with WebVTT.

This also only supports interlaced images, not progressive images
nor text.

Closes #832

Change-Id: Id6dbb6393c7b9a05722e61c6bd255bef5e69a7d8
This commit is contained in:
Jacob Trimble 2020-10-05 15:39:59 -07:00
parent 95089593fc
commit 78be14c092
10 changed files with 1048 additions and 5 deletions

View File

@ -15,6 +15,8 @@
'sources': [ 'sources': [
'dvb_image.cc', 'dvb_image.cc',
'dvb_image.h', 'dvb_image.h',
'dvb_sub_parser.cc',
'dvb_sub_parser.h',
'subtitle_composer.cc', 'subtitle_composer.cc',
'subtitle_composer.h', 'subtitle_composer.h',
], ],
@ -28,6 +30,7 @@
'type': '<(gtest_target_type)', 'type': '<(gtest_target_type)',
'sources': [ 'sources': [
'dvb_image_unittest.cc', 'dvb_image_unittest.cc',
'dvb_sub_parser_unittest.cc',
'subtitle_composer_unittest.cc', 'subtitle_composer_unittest.cc',
], ],
'dependencies': [ 'dependencies': [

View File

@ -0,0 +1,475 @@
// Copyright 2020 Google LLC. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#include "packager/media/formats/dvb/dvb_sub_parser.h"
#include "packager/base/logging.h"
#include "packager/media/formats/mp2t/mp2t_common.h"
namespace shaka {
namespace media {
namespace {
RgbaColor ConvertYuv(uint8_t Y, uint8_t Cr, uint8_t Cb, uint8_t T) {
// See https://en.wikipedia.org/wiki/YCbCr
RgbaColor color;
const double y_transform = 255.0 / 219 * (Y - 16);
const double cb_transform = 255.0 / 244 * 1.772 * (Cb - 128);
const double cr_transform = 255.0 / 244 * 1.402 * (Cr - 128);
const double f1 = 0.114 / 0.587;
const double f2 = 0.299 / 0.587;
color.r = static_cast<uint8_t>(y_transform + cr_transform);
color.g =
static_cast<uint8_t>(y_transform - cb_transform * f1 - cr_transform * f2);
color.b = static_cast<uint8_t>(y_transform + cb_transform);
color.a = 255 - T;
return color;
}
} // namespace
DvbSubParser::DvbSubParser() : last_pts_(0), timeout_(0) {}
DvbSubParser::~DvbSubParser() {}
bool DvbSubParser::Parse(DvbSubSegmentType segment_type,
int64_t pts,
const uint8_t* payload,
size_t size,
std::vector<std::shared_ptr<TextSample>>* samples) {
switch (segment_type) {
case DvbSubSegmentType::kPageComposition:
return ParsePageComposition(pts, payload, size, samples);
case DvbSubSegmentType::kRegionComposition:
return ParseRegionComposition(payload, size);
case DvbSubSegmentType::kClutDefinition:
return ParseClutDefinition(payload, size);
case DvbSubSegmentType::kObjectData:
return ParseObjectData(pts, payload, size);
case DvbSubSegmentType::kDisplayDefinition:
return ParseDisplayDefinition(payload, size);
case DvbSubSegmentType::kEndOfDisplay:
// This signals all the current objects are available. But we need to
// know the end time, so we do nothing for now.
return true;
default:
LOG(WARNING) << "Unknown DVB-sub segment_type=0x" << std::hex
<< static_cast<uint32_t>(segment_type);
return true;
}
}
bool DvbSubParser::Flush(std::vector<std::shared_ptr<TextSample>>* samples) {
RCHECK(composer_.GetSamples(last_pts_, last_pts_ + timeout_ * kMpeg2Timescale,
samples));
composer_.ClearObjects();
return true;
}
const DvbImageColorSpace* DvbSubParser::GetColorSpace(uint8_t clut_id) {
return composer_.GetColorSpace(clut_id);
}
const DvbImageBuilder* DvbSubParser::GetImageForObject(uint16_t object_id) {
return composer_.GetObjectImage(object_id);
}
bool DvbSubParser::ParsePageComposition(
int64_t pts,
const uint8_t* data,
size_t size,
std::vector<std::shared_ptr<TextSample>>* samples) {
// See ETSI EN 300 743 Section 7.2.2.
BitReader reader(data, size);
uint8_t page_state;
RCHECK(reader.ReadBits(8, &timeout_));
RCHECK(reader.SkipBits(4)); // page_version_number
RCHECK(reader.ReadBits(2, &page_state));
RCHECK(reader.SkipBits(2)); // reserved
if (page_state == 0x1 || page_state == 0x2) {
// If this is a "acquisition point" or a "mode change", then this is a new
// page and we should clear the old data.
RCHECK(composer_.GetSamples(last_pts_, pts, samples));
composer_.ClearObjects();
last_pts_ = pts;
}
while (reader.bits_available() > 0u) {
uint8_t region_id;
uint16_t x, y;
RCHECK(reader.ReadBits(8, &region_id));
RCHECK(reader.SkipBits(8)); // reserved
RCHECK(reader.ReadBits(16, &x));
RCHECK(reader.ReadBits(16, &y));
RCHECK(composer_.SetRegionPosition(region_id, x, y));
}
return true;
}
bool DvbSubParser::ParseRegionComposition(const uint8_t* data, size_t size) {
// See ETSI EN 300 743 Section 7.2.3.
BitReader reader(data, size);
uint8_t region_id, clut_id;
uint16_t region_width, region_height;
bool region_fill_flag;
int background_pixel_code;
RCHECK(reader.ReadBits(8, &region_id));
RCHECK(reader.SkipBits(4)); // region_version_number
RCHECK(reader.ReadBits(1, &region_fill_flag));
RCHECK(reader.SkipBits(3)); // reserved
RCHECK(reader.ReadBits(16, &region_width));
RCHECK(reader.ReadBits(16, &region_height));
RCHECK(reader.SkipBits(3)); // region_level_of_compatibility
RCHECK(reader.SkipBits(3)); // region_depth
RCHECK(reader.SkipBits(2)); // reserved
RCHECK(reader.ReadBits(8, &clut_id));
RCHECK(reader.ReadBits(8, &background_pixel_code));
RCHECK(reader.SkipBits(4)); // region_4-bit_pixel_code
RCHECK(reader.SkipBits(2)); // region_2-bit_pixel_code
RCHECK(reader.SkipBits(2)); // reserved
RCHECK(
composer_.SetRegionInfo(region_id, clut_id, region_width, region_height));
if (!region_fill_flag)
background_pixel_code = -1;
while (reader.bits_available() > 0) {
uint16_t object_id, x, y;
uint8_t object_type;
RCHECK(reader.ReadBits(16, &object_id));
RCHECK(reader.ReadBits(2, &object_type));
RCHECK(reader.SkipBits(2)); // object_provider_flag
RCHECK(reader.ReadBits(12, &x));
RCHECK(reader.SkipBits(4)); // reserved
RCHECK(reader.ReadBits(12, &y));
if (object_type == 0x01 || object_type == 0x02) {
RCHECK(reader.SkipBits(8)); // foreground_pixel_code
RCHECK(reader.SkipBits(8)); // background_pixel_code
}
RCHECK(composer_.SetObjectInfo(object_id, region_id, x, y,
background_pixel_code));
}
return true;
}
bool DvbSubParser::ParseClutDefinition(const uint8_t* data, size_t size) {
// See ETSI EN 300 743 Section 7.2.4.
BitReader reader(data, size);
uint8_t clut_id;
RCHECK(reader.ReadBits(8, &clut_id));
auto* color_space = composer_.GetColorSpace(clut_id);
RCHECK(reader.SkipBits(4)); // CLUT_version_number
RCHECK(reader.SkipBits(4)); // reserved
while (reader.bits_available() > 0) {
uint8_t clut_entry_id;
uint8_t has_2_bit;
uint8_t has_4_bit;
uint8_t has_8_bit;
uint8_t full_range_flag;
RCHECK(reader.ReadBits(8, &clut_entry_id));
RCHECK(reader.ReadBits(1, &has_2_bit));
RCHECK(reader.ReadBits(1, &has_4_bit));
RCHECK(reader.ReadBits(1, &has_8_bit));
RCHECK(reader.SkipBits(4)); // reserved
RCHECK(reader.ReadBits(1, &full_range_flag));
if (has_2_bit + has_4_bit + has_8_bit != 1) {
LOG(ERROR) << "Must specify exactly one bit depth in CLUT definition";
return false;
}
const BitDepth bit_depth =
has_2_bit ? BitDepth::k2Bit
: (has_4_bit ? BitDepth::k4Bit : BitDepth::k8Bit);
uint8_t Y, Cr, Cb, T;
if (full_range_flag) {
RCHECK(reader.ReadBits(8, &Y));
RCHECK(reader.ReadBits(8, &Cr));
RCHECK(reader.ReadBits(8, &Cb));
RCHECK(reader.ReadBits(8, &T));
} else {
// These store the most-significant bits, so shift them up.
RCHECK(reader.ReadBits(6, &Y));
Y <<= 2;
RCHECK(reader.ReadBits(4, &Cr));
Cr <<= 4;
RCHECK(reader.ReadBits(4, &Cb));
Cb <<= 4;
RCHECK(reader.ReadBits(2, &T));
T <<= 6;
}
color_space->SetColor(bit_depth, clut_entry_id, ConvertYuv(Y, Cr, Cb, T));
}
return true;
}
bool DvbSubParser::ParseObjectData(int64_t pts,
const uint8_t* data,
size_t size) {
// See ETSI EN 300 743 Section 7.2.5 Table 17.
BitReader reader(data, size);
uint16_t object_id;
uint8_t object_coding_method;
RCHECK(reader.ReadBits(16, &object_id));
RCHECK(reader.SkipBits(4)); // object_version_number
RCHECK(reader.ReadBits(2, &object_coding_method));
RCHECK(reader.SkipBits(1)); // non_modifying_colour_flag
RCHECK(reader.SkipBits(1)); // reserved
auto* image = composer_.GetObjectImage(object_id);
auto* color_space = composer_.GetColorSpaceForObject(object_id);
if (!image || !color_space)
return false;
if (object_coding_method == 0) {
uint16_t top_field_length;
uint16_t bottom_field_length;
RCHECK(reader.ReadBits(16, &top_field_length));
RCHECK(reader.ReadBits(16, &bottom_field_length));
RCHECK(ParsePixelDataSubObject(top_field_length, true, &reader, color_space,
image));
RCHECK(ParsePixelDataSubObject(bottom_field_length, false, &reader,
color_space, image));
// Ignore 8_stuff_bits since we don't need to read to the end.
if (bottom_field_length == 0) {
// If there are no bottom rows, then the top rows are used instead. See
// beginning of section 7.2.5.1.
image->MirrorToBottomRows();
}
} else {
LOG(ERROR) << "Unsupported DVB-sub object coding method: "
<< static_cast<int>(object_coding_method);
return false;
}
return true;
}
bool DvbSubParser::ParseDisplayDefinition(const uint8_t* data, size_t size) {
// See ETSI EN 300 743 Section 7.2.1.
BitReader reader(data, size);
uint16_t width, height;
RCHECK(reader.SkipBits(4)); // dds_version_number
RCHECK(reader.SkipBits(1)); // display_window_flag
RCHECK(reader.SkipBits(3)); // reserved
RCHECK(reader.ReadBits(16, &width));
RCHECK(reader.ReadBits(16, &height));
// Size is stored as -1.
composer_.SetDisplaySize(width + 1, height + 1);
return true;
}
bool DvbSubParser::ParsePixelDataSubObject(size_t sub_object_length,
bool is_top_fields,
BitReader* reader,
DvbImageColorSpace* color_space,
DvbImageBuilder* image) {
const size_t start = reader->bit_position() / 8;
while (reader->bit_position() / 8 < start + sub_object_length) {
// See ETSI EN 300 743 Section 7.2.5.1 Table 20
uint8_t data_type;
RCHECK(reader->ReadBits(8, &data_type));
uint8_t temp[16];
switch (data_type) {
case 0x10:
RCHECK(Parse2BitPixelData(is_top_fields, reader, image));
reader->SkipToNextByte();
break;
case 0x11:
RCHECK(Parse4BitPixelData(is_top_fields, reader, image));
reader->SkipToNextByte();
break;
case 0x12:
RCHECK(Parse8BitPixelData(is_top_fields, reader, image));
break;
case 0x20:
for (int i = 0; i < 4; i++) {
RCHECK(reader->ReadBits(4, &temp[i]));
}
color_space->Set2To4BitDepthMap(temp);
break;
case 0x21:
for (int i = 0; i < 4; i++) {
RCHECK(reader->ReadBits(8, &temp[i]));
}
color_space->Set2To8BitDepthMap(temp);
break;
case 0x22:
for (int i = 0; i < 16; i++) {
RCHECK(reader->ReadBits(8, &temp[i]));
}
color_space->Set4To8BitDepthMap(temp);
break;
case 0xf0:
image->NewRow(is_top_fields);
break;
default:
LOG(ERROR) << "Unsupported DVB-sub pixel data format: 0x" << std::hex
<< static_cast<int>(data_type);
return false;
}
}
return true;
}
bool DvbSubParser::Parse2BitPixelData(bool is_top_fields,
BitReader* reader,
DvbImageBuilder* image) {
// 2-bit/pixel code string, Section 7.2.5.2.1, Table 22.
while (true) {
uint8_t peek;
RCHECK(reader->ReadBits(2, &peek));
if (peek != 0) {
RCHECK(image->AddPixel(BitDepth::k2Bit, peek, is_top_fields));
} else {
uint8_t switch_1;
RCHECK(reader->ReadBits(1, &switch_1));
if (switch_1 == 1) {
uint8_t count_minus_3;
RCHECK(reader->ReadBits(3, &count_minus_3));
RCHECK(reader->ReadBits(2, &peek));
for (uint8_t i = 0; i < count_minus_3 + 3; i++)
RCHECK(image->AddPixel(BitDepth::k2Bit, peek, is_top_fields));
} else {
uint8_t switch_2;
RCHECK(reader->ReadBits(1, &switch_2));
if (switch_2 == 1) {
RCHECK(image->AddPixel(BitDepth::k2Bit, 0, is_top_fields));
} else {
uint8_t switch_3;
RCHECK(reader->ReadBits(2, &switch_3));
if (switch_3 == 0) {
break;
} else if (switch_3 == 1) {
RCHECK(image->AddPixel(BitDepth::k2Bit, 0, is_top_fields));
RCHECK(image->AddPixel(BitDepth::k2Bit, 0, is_top_fields));
} else if (switch_3 == 2) {
uint8_t count_minus_12;
RCHECK(reader->ReadBits(4, &count_minus_12));
RCHECK(reader->ReadBits(2, &peek));
for (uint8_t i = 0; i < count_minus_12 + 12; i++)
RCHECK(image->AddPixel(BitDepth::k2Bit, peek, is_top_fields));
} else if (switch_3 == 3) {
uint8_t count_minus_29;
RCHECK(reader->ReadBits(8, &count_minus_29));
RCHECK(reader->ReadBits(2, &peek));
for (uint8_t i = 0; i < count_minus_29 + 29; i++)
RCHECK(image->AddPixel(BitDepth::k2Bit, peek, is_top_fields));
}
}
}
}
}
return true;
}
bool DvbSubParser::Parse4BitPixelData(bool is_top_fields,
BitReader* reader,
DvbImageBuilder* image) {
// 4-bit/pixel code string, Section 7.2.5.2.2, Table 24.
DCHECK(reader->bits_available() % 8 == 0);
while (true) {
uint8_t peek;
RCHECK(reader->ReadBits(4, &peek));
if (peek != 0) {
RCHECK(image->AddPixel(BitDepth::k4Bit, peek, is_top_fields));
} else {
uint8_t switch_1;
RCHECK(reader->ReadBits(1, &switch_1));
if (switch_1 == 0) {
RCHECK(reader->ReadBits(3, &peek));
if (peek != 0) {
for (int i = 0; i < peek + 2; i++)
RCHECK(image->AddPixel(BitDepth::k4Bit, 0, is_top_fields));
} else {
break;
}
} else {
uint8_t switch_2;
RCHECK(reader->ReadBits(1, &switch_2));
if (switch_2 == 0) {
RCHECK(reader->ReadBits(2, &peek)); // run_length_4-7
uint8_t code;
RCHECK(reader->ReadBits(4, &code));
for (int i = 0; i < peek + 4; i++)
RCHECK(image->AddPixel(BitDepth::k4Bit, code, is_top_fields));
} else {
uint8_t switch_3;
RCHECK(reader->ReadBits(2, &switch_3));
if (switch_3 == 0) {
RCHECK(image->AddPixel(BitDepth::k4Bit, 0, is_top_fields));
} else if (switch_3 == 1) {
RCHECK(image->AddPixel(BitDepth::k4Bit, 0, is_top_fields));
RCHECK(image->AddPixel(BitDepth::k4Bit, 0, is_top_fields));
} else if (switch_3 == 2) {
RCHECK(reader->ReadBits(4, &peek)); // run_length_9-24
uint8_t code;
RCHECK(reader->ReadBits(4, &code));
for (int i = 0; i < peek + 9; i++)
RCHECK(image->AddPixel(BitDepth::k4Bit, code, is_top_fields));
} else {
// switch_3 == 3
RCHECK(reader->ReadBits(8, &peek)); // run_length_25-280
uint8_t code;
RCHECK(reader->ReadBits(4, &code));
for (int i = 0; i < peek + 25; i++)
RCHECK(image->AddPixel(BitDepth::k4Bit, code, is_top_fields));
}
}
}
}
}
return true;
}
bool DvbSubParser::Parse8BitPixelData(bool is_top_fields,
BitReader* reader,
DvbImageBuilder* image) {
// 8-bit/pixel code string, Section 7.2.5.2.3, Table 26.
while (true) {
uint8_t peek;
RCHECK(reader->ReadBits(8, &peek));
if (peek != 0) {
RCHECK(image->AddPixel(BitDepth::k8Bit, peek, is_top_fields));
} else {
uint8_t switch_1;
RCHECK(reader->ReadBits(1, &switch_1));
if (switch_1 == 0) {
RCHECK(reader->ReadBits(7, &peek));
if (peek != 0) {
for (uint8_t i = 0; i < peek; i++)
RCHECK(image->AddPixel(BitDepth::k8Bit, 0, is_top_fields));
} else {
break;
}
} else {
uint8_t count;
RCHECK(reader->ReadBits(7, &count));
RCHECK(reader->ReadBits(8, &peek));
for (uint8_t i = 0; i < count; i++)
RCHECK(image->AddPixel(BitDepth::k8Bit, peek, is_top_fields));
}
}
}
return true;
}
} // namespace media
} // namespace shaka

View File

@ -0,0 +1,86 @@
// Copyright 2020 Google LLC. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#ifndef PACKAGER_MEDIA_DVB_DVB_SUB_PARSER_H_
#define PACKAGER_MEDIA_DVB_DVB_SUB_PARSER_H_
#include <memory>
#include <vector>
#include "packager/media/base/bit_reader.h"
#include "packager/media/base/text_sample.h"
#include "packager/media/formats/dvb/dvb_image.h"
#include "packager/media/formats/dvb/subtitle_composer.h"
namespace shaka {
namespace media {
// See ETSI EN 300 743 Section 7.2.0.1 and Table 7.
enum class DvbSubSegmentType : uint16_t {
kPageComposition = 0x10,
kRegionComposition = 0x11,
kClutDefinition = 0x12,
kObjectData = 0x13,
kDisplayDefinition = 0x14,
kDisparitySignalling = 0x15,
kAlternativeClut = 0x16,
kEndOfDisplay = 0x80,
};
class DvbSubParser {
public:
DvbSubParser();
~DvbSubParser();
DvbSubParser(const DvbSubParser&) = delete;
DvbSubParser& operator=(const DvbSubParser&) = delete;
bool Parse(DvbSubSegmentType segment_type,
int64_t pts,
const uint8_t* payload,
size_t size,
std::vector<std::shared_ptr<TextSample>>* samples);
bool Flush(std::vector<std::shared_ptr<TextSample>>* samples);
private:
friend class DvbSubParserTest;
const DvbImageColorSpace* GetColorSpace(uint8_t clut_id);
const DvbImageBuilder* GetImageForObject(uint16_t object_id);
bool ParsePageComposition(int64_t pts,
const uint8_t* data,
size_t size,
std::vector<std::shared_ptr<TextSample>>* samples);
bool ParseRegionComposition(const uint8_t* data, size_t size);
bool ParseClutDefinition(const uint8_t* data, size_t size);
bool ParseObjectData(int64_t pts, const uint8_t* data, size_t size);
bool ParseDisplayDefinition(const uint8_t* data, size_t size);
bool ParsePixelDataSubObject(size_t sub_object_length,
bool is_top_fields,
BitReader* reader,
DvbImageColorSpace* color_space,
DvbImageBuilder* image);
bool Parse2BitPixelData(bool is_top_fields,
BitReader* reader,
DvbImageBuilder* image);
bool Parse4BitPixelData(bool is_top_fields,
BitReader* reader,
DvbImageBuilder* image);
bool Parse8BitPixelData(bool is_top_fields,
BitReader* reader,
DvbImageBuilder* image);
SubtitleComposer composer_;
int64_t last_pts_;
uint8_t timeout_;
};
} // namespace media
} // namespace shaka
#endif // PACKAGER_MEDIA_DVB_DVB_SUB_PARSER_H_

View File

@ -0,0 +1,305 @@
// Copyright 2020 Google LLC. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#include "packager/media/formats/dvb/dvb_sub_parser.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <string>
#include <utility>
#include <vector>
namespace shaka {
namespace media {
namespace {
constexpr const uint8_t kRegionId = 7;
constexpr const uint8_t kClutId = 12;
constexpr const uint8_t kObjectId1 = 1;
constexpr const uint8_t kObjectId2 = 2;
constexpr const int64_t kNoPts = 0;
/// @param object_id The Object ID.
/// @param pairs A vector of data_type plus data body. Each pair should be a
/// single row. It should be image order (i.e. not interlaced). The
/// body is an array of strings containing binary codes (e.g. "01").
std::vector<uint8_t> GenerateObjectData(
uint8_t object_id,
const std::vector<std::pair<uint8_t, std::vector<std::string>>>& pairs) {
std::vector<uint8_t> ret;
ret.push_back(0);
ret.push_back(object_id);
ret.push_back(0);
ret.insert(ret.end(), 4, 0); // insert dummy bytes for size
auto push_data = [&](size_t start_index) {
const auto start_size = ret.size();
for (size_t i = start_index; i < pairs.size(); i += 2) {
ret.push_back(pairs[i].first);
uint8_t temp = 0;
uint8_t count = 0;
for (const auto& str : pairs[i].second) {
for (const auto& ch : str) {
if (ch == ' ')
continue;
CHECK(ch == '0' || ch == '1');
temp = (temp << 1) | (ch - '0');
if (++count == 8) {
ret.push_back(temp);
temp = count = 0;
}
}
}
if (count != 0)
ret.push_back(temp << (8 - count));
ret.push_back(0xf0); // end-of-line
}
return ret.size() - start_size;
};
const size_t top_size = push_data(0);
const size_t bottom_size = push_data(1);
CHECK(top_size <= 0xffff);
CHECK(bottom_size <= 0xffff);
ret[3] = (top_size >> 8) & 0xff;
ret[4] = top_size & 0xff;
ret[5] = (bottom_size >> 8) & 0xff;
ret[6] = bottom_size & 0xff;
return ret;
}
} // namespace
class DvbSubParserTest : public testing::Test {
protected:
const DvbImageColorSpace* GetColorSpace(DvbSubParser* parser,
uint8_t clut_id) {
return parser->GetColorSpace(clut_id);
}
const DvbImageBuilder* GetImage(DvbSubParser* parser, uint8_t object_id) {
return parser->GetImageForObject(object_id);
}
};
TEST_F(DvbSubParserTest, TestHelper) {
const uint8_t kResult[] = {
// clang-format off
0x00, 0x12, 0x00,
0x00, 0x09, // top-rows size
0x00, 0x04, // bottom-rows size
// Top-rows
0x30, 0x1b, 0xe9, 0x6b,
0xf0,
0x88, 0xc8, 0xe0,
0xf0,
// Bottom-rows
0x11, 0xe6, 0xcd,
0xf0,
// clang-format on
};
std::vector<uint8_t> expected(kResult, kResult + sizeof(kResult));
const auto actual =
GenerateObjectData(0x12, {{0x30, {"00011011", "11101001", "01101011"}},
{0x11, {"11100110", "11001101"}},
{0x88, {"11", "00", "10", "00", "11", "10"}}});
EXPECT_EQ(actual, expected);
}
TEST_F(DvbSubParserTest, BasicFlow) {
// Note up to segment_length is handled by caller.
constexpr const uint8_t kDisplayDefinitionSegment[] = {
0x00, // dds_version_number(4) | display_window_flag(1) | reserved(3)
0x00, 99, // display_width
0x00, 99, // display_height
};
constexpr const uint8_t kPageCompositionSegment[] = {
0x02, // page_time_out
0x04, // page_version_number(4) | page_state(2) | reserved(2)
// First region
kRegionId, // region_id
0x00, // reserved
0x00, 0x11, // region_horizontal_address
0x00, 0x12, // region_vertical_address
};
constexpr const uint8_t kRegionCompositionSegment[] = {
kRegionId, // region_id
0x08, // region_version_number(4) | region_fill_flag(1) |
// reserved(3)
0x00, 50, // region_width
0x00, 50, // region_height
0x6c, // region_level_of_compatibility(3) | region_depth(3) |
// reserved(2)
kClutId, // CLUT_id
0x02, // region_8-bit_pixel_code,
0x28, // region_4-bit_pixel_code(4) | region_2-bit_pixel_code(2) |
// reserved(2)
// First object
0x00, kObjectId1, // object_id
0x00, 0x07, // object_type(2) | object_provider_flag(2) |
// object_horizontal_position(12)
0x00, 0x08, // reserved(4) | object_vertical_position(12)
// Second object
0x00, kObjectId2, // object_id
0x00, 0x09, // object_type(2) | object_provider_flag(2) |
// object_horizontal_position(12)
0x00, 0x0c, // reserved(4) | object_vertical_position(12)
};
constexpr const uint8_t kClutDefinitionSegment[] = {
// clang-format off
kClutId, // CLUT_id
0x00, // CLUT_version_number(4) | reserved(4)
// First color
0x00, // CLUT_entry_id
0x81, // flags (2-bit,full-range)
70, 141, 117, 0,
0x00, // CLUT_entry_id
0x41, // flags (4-bit,full-range)
70, 141, 117, 0,
0x00, // CLUT_entry_id
0x21, // flags (8-bit,full-range)
70, 141, 117, 0,
// Second color
0x01, // CLUT_entry_id
0x81, // flags (2-bit,full-range)
33, 134, 122, 0,
0x01, // CLUT_entry_id
0x41, // flags (4-bit,full-range)
33, 134, 122, 0,
0x01, // CLUT_entry_id
0x21, // flags (8-bit,full-range)
33, 134, 122, 0,
// Third color
0x02, // CLUT_entry_id
0x81, // flags (2-bit,full-range)
100, 128, 127, 0,
0x02, // CLUT_entry_id
0x41, // flags (4-bit,full-range)
100, 128, 127, 0,
0x02, // CLUT_entry_id
0x21, // flags (8-bit,full-range)
100, 128, 127, 0,
// clang-format on
};
// 0 0 0 0 1 1
// 0 1 1 1 0 0
// 1 0 1 1 1 1
// 0 0 0 1
const auto kObjectData1 = GenerateObjectData(
kObjectId1,
{
{0x10, {"00100100", "01", "01", "000000"}},
{0x10, {"0001", "00100001", "000001", "000000"}},
{0x11, {"0001", "0000 1100", "0000 1000 0001", "0000 0000"}},
{0x11, {"0000 0001", "0001", "0000 0000"}},
});
// 1 1 0 0
// 0 0 1 0
// 1 0 0 0
const uint8_t kObjectData2[] = {
0x00, kObjectId2, 0x00,
0x00, 0x0f, // top-rows length
0x00, 0x09, // bottom-rows length
0x12, 0x01, 0x01, 0x00, 0x02, 0x00, 0x00, 0xf0, // row 0
0x12, 0x01, 0x00, 0x03, 0x00, 0x00, 0xf0, // row 2
0x12, 0x00, 0x02, 0x01, 0x00, 0x01, 0x00, 0x00, 0xf0, // row 1
};
constexpr const uint8_t kEndOfDisplaySegment[] = {0x00};
auto check_image_data = [&](DvbSubParser* parser, uint8_t object_id,
const std::vector<uint8_t>& data) {
const RgbaColor* pixels;
uint16_t width, height;
auto* color_space = GetColorSpace(parser, kClutId);
auto* image = GetImage(parser, object_id);
ASSERT_TRUE(image);
ASSERT_TRUE(color_space);
ASSERT_TRUE(image->GetPixels(&pixels, &width, &height));
ASSERT_EQ(static_cast<size_t>(width * height), data.size());
for (size_t y = 0; y < height; y++) {
for (size_t x = 0; x < width; x++) {
auto color =
color_space->GetColor(BitDepth::k8Bit, data[x + y * width]);
EXPECT_EQ(pixels[x + y * image->max_width()], color)
<< "Object=" << static_cast<int>(object_id) << ", X=" << x
<< ", Y=" << y;
}
}
};
DvbSubParser parser;
std::vector<std::shared_ptr<TextSample>> samples;
ASSERT_TRUE(parser.Parse(DvbSubSegmentType::kDisplayDefinition, kNoPts,
kDisplayDefinitionSegment,
sizeof(kDisplayDefinitionSegment), &samples));
ASSERT_TRUE(parser.Parse(DvbSubSegmentType::kPageComposition, kNoPts,
kPageCompositionSegment,
sizeof(kPageCompositionSegment), &samples));
ASSERT_TRUE(parser.Parse(DvbSubSegmentType::kRegionComposition, kNoPts,
kRegionCompositionSegment,
sizeof(kRegionCompositionSegment), &samples));
ASSERT_TRUE(parser.Parse(DvbSubSegmentType::kClutDefinition, kNoPts,
kClutDefinitionSegment,
sizeof(kClutDefinitionSegment), &samples));
ASSERT_TRUE(parser.Parse(DvbSubSegmentType::kObjectData, kNoPts,
kObjectData1.data(), kObjectData1.size(), &samples));
ASSERT_TRUE(parser.Parse(DvbSubSegmentType::kObjectData, kNoPts, kObjectData2,
sizeof(kObjectData2), &samples));
ASSERT_TRUE(parser.Parse(DvbSubSegmentType::kEndOfDisplay, kNoPts,
kEndOfDisplaySegment, sizeof(kEndOfDisplaySegment),
&samples));
check_image_data(&parser, kObjectId1, {0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0,
1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 2, 2});
check_image_data(&parser, kObjectId2, {1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0});
ASSERT_TRUE(parser.Flush(&samples));
ASSERT_EQ(samples.size(), 2u);
for (auto& sample : samples) {
ASSERT_TRUE(sample->settings().line);
ASSERT_EQ(sample->settings().line->type, TextUnitType::kPercent);
ASSERT_TRUE(sample->settings().position);
ASSERT_EQ(sample->settings().position->type, TextUnitType::kPercent);
ASSERT_TRUE(sample->settings().width);
ASSERT_EQ(sample->settings().width->type, TextUnitType::kPercent);
ASSERT_TRUE(sample->settings().height);
ASSERT_EQ(sample->settings().height->type, TextUnitType::kPercent);
ASSERT_FALSE(sample->body().image.empty());
}
// Allow in either order.
if (samples[0]->settings().position->value == 0x1a)
std::swap(samples[0], samples[1]);
EXPECT_EQ(samples[0]->settings().position->value, 0x18);
EXPECT_EQ(samples[0]->settings().line->value, 0x1a);
EXPECT_EQ(samples[0]->settings().width->value, 6);
EXPECT_EQ(samples[0]->settings().height->value, 4);
EXPECT_EQ(samples[1]->settings().position->value, 0x1a);
EXPECT_EQ(samples[1]->settings().line->value, 0x1e);
EXPECT_EQ(samples[1]->settings().width->value, 4);
EXPECT_EQ(samples[1]->settings().height->value, 3);
}
} // namespace media
} // namespace shaka

View File

@ -0,0 +1,94 @@
// Copyright 2020 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#include "packager/media/formats/mp2t/es_parser_dvb.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/mp2t_common.h"
namespace shaka {
namespace media {
namespace mp2t {
EsParserDvb::EsParserDvb(uint32_t pid,
const NewStreamInfoCB& new_stream_info_cb,
const EmitTextSampleCB& emit_sample_cb)
: EsParser(pid),
new_stream_info_cb_(new_stream_info_cb),
emit_sample_cb_(emit_sample_cb) {}
EsParserDvb::~EsParserDvb() {}
bool EsParserDvb::Parse(const uint8_t* buf,
int size,
int64_t pts,
int64_t dts) {
if (!sent_info_) {
sent_info_ = true;
std::shared_ptr<StreamInfo> info = std::make_shared<TextStreamInfo>(
pid(), kMpeg2Timescale, kInfiniteDuration, kCodecText,
/* codec_string= */ "", /* codec_config= */ "", /* width= */ 0,
/* height= */ 0, /* language= */ "");
new_stream_info_cb_.Run(info);
}
// TODO: Handle buffering and multiple reads? All content so far has been
// a whole segment, so it may not be needed.
return ParseInternal(buf, size, pts);
}
bool EsParserDvb::Flush() {
for (auto& pair : parsers_) {
std::vector<std::shared_ptr<TextSample>> samples;
RCHECK(pair.second.Flush(&samples));
for (auto sample : samples)
emit_sample_cb_.Run(sample);
}
return true;
}
void EsParserDvb::Reset() {
parsers_.clear();
}
bool EsParserDvb::ParseInternal(const uint8_t* data, size_t size, int64_t pts) {
// See EN 300 743 Table 3.
BitReader reader(data, size);
int data_identifier;
int subtitle_stream_id;
RCHECK(reader.ReadBits(8, &data_identifier));
RCHECK(reader.ReadBits(8, &subtitle_stream_id));
RCHECK(data_identifier == 0x20);
RCHECK(subtitle_stream_id == 0);
int temp;
while (reader.ReadBits(8, &temp) && temp == 0xf) {
DvbSubSegmentType segment_type;
uint16_t page_id;
size_t segment_length;
RCHECK(reader.ReadBits(8, &segment_type));
RCHECK(reader.ReadBits(16, &page_id));
RCHECK(reader.ReadBits(16, &segment_length));
RCHECK(reader.bits_available() > segment_length * 8);
const uint8_t* payload = data + (size - reader.bits_available() / 8);
std::vector<std::shared_ptr<TextSample>> samples;
RCHECK(parsers_[page_id].Parse(segment_type, pts, payload, segment_length,
&samples));
for (auto sample : samples)
emit_sample_cb_.Run(sample);
RCHECK(reader.SkipBytes(segment_length));
}
return temp == 0xff;
}
} // namespace mp2t
} // namespace media
} // namespace shaka

View File

@ -0,0 +1,54 @@
// Copyright 2020 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#ifndef PACKAGER_MEDIA_FORMATS_MP2T_ES_PARSER_DVB_H_
#define PACKAGER_MEDIA_FORMATS_MP2T_ES_PARSER_DVB_H_
#include <unordered_map>
#include "packager/base/callback.h"
#include "packager/media/base/byte_queue.h"
#include "packager/media/formats/dvb/dvb_sub_parser.h"
#include "packager/media/formats/mp2t/es_parser.h"
namespace shaka {
namespace media {
namespace mp2t {
class EsParserDvb : public EsParser {
public:
EsParserDvb(uint32_t pid,
const NewStreamInfoCB& new_stream_info_cb,
const EmitTextSampleCB& emit_sample_cb);
~EsParserDvb() override;
// EsParser implementation.
bool Parse(const uint8_t* buf, int size, int64_t pts, int64_t dts) override;
bool Flush() override;
void Reset() override;
private:
EsParserDvb(const EsParserDvb&) = delete;
EsParserDvb& operator=(const EsParserDvb&) = delete;
bool ParseInternal(const uint8_t* data, size_t size, int64_t pts);
// Callbacks:
// - to signal a new audio configuration,
// - to send ES buffers.
NewStreamInfoCB new_stream_info_cb_;
EmitTextSampleCB emit_sample_cb_;
// A map of page_id to parser.
std::unordered_map<uint16_t, DvbSubParser> parsers_;
bool sent_info_ = false;
};
} // namespace mp2t
} // namespace media
} // namespace shaka
#endif // PACKAGER_MEDIA_FORMATS_MP2T_ES_PARSER_DVB_H_

View File

@ -22,6 +22,8 @@
'continuity_counter.h', 'continuity_counter.h',
'es_parser_audio.cc', 'es_parser_audio.cc',
'es_parser_audio.h', 'es_parser_audio.h',
'es_parser_dvb.cc',
'es_parser_dvb.h',
'es_parser_h264.cc', 'es_parser_h264.cc',
'es_parser_h264.h', 'es_parser_h264.h',
'es_parser_h265.cc', 'es_parser_h265.cc',
@ -63,6 +65,7 @@
'../../base/media_base.gyp:media_base', '../../base/media_base.gyp:media_base',
'../../crypto/crypto.gyp:crypto', '../../crypto/crypto.gyp:crypto',
'../../codecs/codecs.gyp:codecs', '../../codecs/codecs.gyp:codecs',
'../dvb/dvb.gyp:dvb',
], ],
}, },
{ {

View File

@ -12,6 +12,7 @@
#include "packager/media/base/text_sample.h" #include "packager/media/base/text_sample.h"
#include "packager/media/formats/mp2t/es_parser.h" #include "packager/media/formats/mp2t/es_parser.h"
#include "packager/media/formats/mp2t/es_parser_audio.h" #include "packager/media/formats/mp2t/es_parser_audio.h"
#include "packager/media/formats/mp2t/es_parser_dvb.h"
#include "packager/media/formats/mp2t/es_parser_h264.h" #include "packager/media/formats/mp2t/es_parser_h264.h"
#include "packager/media/formats/mp2t/es_parser_h265.h" #include "packager/media/formats/mp2t/es_parser_h265.h"
#include "packager/media/formats/mp2t/mp2t_common.h" #include "packager/media/formats/mp2t/mp2t_common.h"
@ -33,6 +34,7 @@ class PidState {
kPidPmt, kPidPmt,
kPidAudioPes, kPidAudioPes,
kPidVideoPes, kPidVideoPes,
kPidTextPes,
}; };
PidState(int pid, PidState(int pid,
@ -281,12 +283,14 @@ void Mp2tMediaParser::RegisterPes(int pmt_pid,
<< static_cast<int>(stream_type) << std::dec; << static_cast<int>(stream_type) << std::dec;
// Create a stream parser corresponding to the stream type. // Create a stream parser corresponding to the stream type.
bool is_audio = false; PidState::PidType pid_type = PidState::kPidVideoPes;
std::unique_ptr<EsParser> es_parser; std::unique_ptr<EsParser> es_parser;
auto on_new_stream = base::Bind(&Mp2tMediaParser::OnNewStreamInfo, auto on_new_stream = base::Bind(&Mp2tMediaParser::OnNewStreamInfo,
base::Unretained(this), pes_pid); base::Unretained(this), pes_pid);
auto on_emit_media = base::Bind(&Mp2tMediaParser::OnEmitMediaSample, auto on_emit_media = base::Bind(&Mp2tMediaParser::OnEmitMediaSample,
base::Unretained(this), pes_pid); base::Unretained(this), pes_pid);
auto on_emit_text = base::Bind(&Mp2tMediaParser::OnEmitTextSample,
base::Unretained(this), pes_pid);
switch (stream_type) { switch (stream_type) {
case TsStreamType::kAvc: case TsStreamType::kAvc:
es_parser.reset(new EsParserH264(pes_pid, on_new_stream, on_emit_media)); es_parser.reset(new EsParserH264(pes_pid, on_new_stream, on_emit_media));
@ -300,10 +304,15 @@ void Mp2tMediaParser::RegisterPes(int pmt_pid,
es_parser.reset( es_parser.reset(
new EsParserAudio(pes_pid, static_cast<TsStreamType>(stream_type), new EsParserAudio(pes_pid, static_cast<TsStreamType>(stream_type),
on_new_stream, on_emit_media, sbr_in_mimetype_)); on_new_stream, on_emit_media, sbr_in_mimetype_));
is_audio = true; pid_type = PidState::kPidAudioPes;
break;
case TsStreamType::kDvbSubtitles:
es_parser.reset(new EsParserDvb(pes_pid, on_new_stream, on_emit_text));
pid_type = PidState::kPidTextPes;
break; break;
default: { default: {
auto type = static_cast<int>(stream_type); auto type = static_cast<int>(stream_type);
DCHECK(type <= 0xff);
LOG_IF(ERROR, !stream_type_logged_once_[type]) LOG_IF(ERROR, !stream_type_logged_once_[type])
<< "Ignore unsupported MPEG2TS stream type 0x" << std::hex << type << "Ignore unsupported MPEG2TS stream type 0x" << std::hex << type
<< std::dec; << std::dec;
@ -316,8 +325,6 @@ void Mp2tMediaParser::RegisterPes(int pmt_pid,
DVLOG(1) << "Create a new PES state"; DVLOG(1) << "Create a new PES state";
std::unique_ptr<TsSection> pes_section_parser( std::unique_ptr<TsSection> pes_section_parser(
new TsSectionPes(std::move(es_parser))); new TsSectionPes(std::move(es_parser)));
PidState::PidType pid_type =
is_audio ? PidState::kPidAudioPes : PidState::kPidVideoPes;
std::unique_ptr<PidState> pes_pid_state( std::unique_ptr<PidState> pes_pid_state(
new PidState(pes_pid, pid_type, std::move(pes_section_parser))); new PidState(pes_pid, pid_type, std::move(pes_section_parser)));
pes_pid_state->Enable(); pes_pid_state->Enable();
@ -363,7 +370,8 @@ bool Mp2tMediaParser::FinishInitializationIfNeeded() {
uint32_t num_es(0); uint32_t num_es(0);
for (const auto& pair : pids_) { for (const auto& pair : pids_) {
if ((pair.second->pid_type() == PidState::kPidAudioPes || if ((pair.second->pid_type() == PidState::kPidAudioPes ||
pair.second->pid_type() == PidState::kPidVideoPes) && pair.second->pid_type() == PidState::kPidVideoPes ||
pair.second->pid_type() == PidState::kPidTextPes) &&
pair.second->IsEnabled()) { pair.second->IsEnabled()) {
++num_es; ++num_es;
if (pair.second->config()) if (pair.second->config())

View File

@ -95,6 +95,17 @@ bool TsSectionPmt::ParsePsiSection(BitReader* bit_reader) {
// Read the ES info descriptors. // Read the ES info descriptors.
// Defined in section 2.6 of ISO-13818. // Defined in section 2.6 of ISO-13818.
if (es_info_length > 0) {
uint8_t descriptor_tag;
RCHECK(bit_reader->ReadBits(8, &descriptor_tag));
es_info_length--;
// See ETSI EN 300 468 Section 6.1
if (stream_type == TsStreamType::kPesPrivateData &&
descriptor_tag == 0x59) { // subtitling_descriptor
pid_map[pid_es] = TsStreamType::kDvbSubtitles;
}
}
RCHECK(bit_reader->SkipBits(8 * es_info_length)); RCHECK(bit_reader->SkipBits(8 * es_info_length));
} }

View File

@ -41,6 +41,10 @@ enum class TsStreamType {
kEncryptedEac3 = 0xC2, kEncryptedEac3 = 0xC2,
kEncryptedAdtsAac = 0xCF, kEncryptedAdtsAac = 0xCF,
kEncryptedAvc = 0xDB, kEncryptedAvc = 0xDB,
// Below are internal values used to select other stream types based on other
// info in headers.
kDvbSubtitles = 0x100,
}; };
} // namespace mp2t } // namespace mp2t