Add support for AudioRollRecoveryEntry
This is part of the effort to support Opus in iso-bmff #83. Changes in this CL: - Add support for multiple SampleGroupDescription and SampleToGroup boxes in TrackFragment box; - Add support for SampleGroupDescription and SampleToGroup boxes in SampleTable box; - Add support for AudioRollRecoveryEntry in Sample Group and 'roll' sample grouping type; - Also fix a bug that default length was not set correctly in SampleGroupDescription when constant iv is used. Change-Id: I7e31386ea6cd17a8ee5e1dca4c1a90937d51368f
This commit is contained in:
parent
cf4a2447c1
commit
940cc43de2
|
@ -82,6 +82,7 @@ enum FourCC : uint32_t {
|
||||||
FOURCC_pdin = 0x7064696e,
|
FOURCC_pdin = 0x7064696e,
|
||||||
FOURCC_prft = 0x70726674,
|
FOURCC_prft = 0x70726674,
|
||||||
FOURCC_pssh = 0x70737368,
|
FOURCC_pssh = 0x70737368,
|
||||||
|
FOURCC_roll = 0x726f6c6c,
|
||||||
FOURCC_saio = 0x7361696f,
|
FOURCC_saio = 0x7361696f,
|
||||||
FOURCC_saiz = 0x7361697a,
|
FOURCC_saiz = 0x7361697a,
|
||||||
FOURCC_sbgp = 0x73626770,
|
FOURCC_sbgp = 0x73626770,
|
||||||
|
|
|
@ -921,6 +921,187 @@ uint32_t SyncSample::ComputeSizeInternal() {
|
||||||
sizeof(uint32_t) * sample_number.size();
|
sizeof(uint32_t) * sample_number.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CencSampleEncryptionInfoEntry::CencSampleEncryptionInfoEntry()
|
||||||
|
: is_protected(0),
|
||||||
|
per_sample_iv_size(0),
|
||||||
|
crypt_byte_block(0),
|
||||||
|
skip_byte_block(0) {}
|
||||||
|
CencSampleEncryptionInfoEntry::~CencSampleEncryptionInfoEntry() {};
|
||||||
|
|
||||||
|
bool CencSampleEncryptionInfoEntry::ReadWrite(BoxBuffer* buffer) {
|
||||||
|
if (!buffer->Reading()) {
|
||||||
|
if (key_id.size() != kCencKeyIdSize) {
|
||||||
|
LOG(WARNING) << "CENC defines key id length of " << kCencKeyIdSize
|
||||||
|
<< " bytes; got " << key_id.size()
|
||||||
|
<< ". Resized accordingly.";
|
||||||
|
key_id.resize(kCencKeyIdSize);
|
||||||
|
}
|
||||||
|
RCHECK(crypt_byte_block < 16 && skip_byte_block < 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
RCHECK(buffer->IgnoreBytes(1)); // reserved.
|
||||||
|
|
||||||
|
uint8_t pattern = crypt_byte_block << 4 | skip_byte_block;
|
||||||
|
RCHECK(buffer->ReadWriteUInt8(&pattern));
|
||||||
|
crypt_byte_block = pattern >> 4;
|
||||||
|
skip_byte_block = pattern & 0x0F;
|
||||||
|
|
||||||
|
RCHECK(buffer->ReadWriteUInt8(&is_protected) &&
|
||||||
|
buffer->ReadWriteUInt8(&per_sample_iv_size) &&
|
||||||
|
buffer->ReadWriteVector(&key_id, kCencKeyIdSize));
|
||||||
|
|
||||||
|
if (is_protected == 1) {
|
||||||
|
if (per_sample_iv_size == 0) { // For constant iv.
|
||||||
|
uint8_t constant_iv_size = constant_iv.size();
|
||||||
|
RCHECK(buffer->ReadWriteUInt8(&constant_iv_size));
|
||||||
|
RCHECK(constant_iv_size == 8 || constant_iv_size == 16);
|
||||||
|
RCHECK(buffer->ReadWriteVector(&constant_iv, constant_iv_size));
|
||||||
|
} else {
|
||||||
|
RCHECK(per_sample_iv_size == 8 || per_sample_iv_size == 16);
|
||||||
|
DCHECK(constant_iv.empty());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Expect |is_protected| to be 0, i.e. not protected. Other values of
|
||||||
|
// |is_protected| is not supported.
|
||||||
|
RCHECK(is_protected == 0);
|
||||||
|
RCHECK(per_sample_iv_size == 0);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t CencSampleEncryptionInfoEntry::ComputeSize() const {
|
||||||
|
return sizeof(uint32_t) + kCencKeyIdSize +
|
||||||
|
(constant_iv.empty() ? 0 : (sizeof(uint8_t) + constant_iv.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioRollRecoveryEntry::AudioRollRecoveryEntry(): roll_distance(0) {}
|
||||||
|
AudioRollRecoveryEntry::~AudioRollRecoveryEntry() {}
|
||||||
|
|
||||||
|
bool AudioRollRecoveryEntry::ReadWrite(BoxBuffer* buffer) {
|
||||||
|
RCHECK(buffer->ReadWriteInt16(&roll_distance));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t AudioRollRecoveryEntry::ComputeSize() const {
|
||||||
|
return sizeof(roll_distance);
|
||||||
|
}
|
||||||
|
|
||||||
|
SampleGroupDescription::SampleGroupDescription() : grouping_type(0) {}
|
||||||
|
SampleGroupDescription::~SampleGroupDescription() {}
|
||||||
|
FourCC SampleGroupDescription::BoxType() const { return FOURCC_sgpd; }
|
||||||
|
|
||||||
|
bool SampleGroupDescription::ReadWriteInternal(BoxBuffer* buffer) {
|
||||||
|
RCHECK(ReadWriteHeaderInternal(buffer) &&
|
||||||
|
buffer->ReadWriteUInt32(&grouping_type));
|
||||||
|
|
||||||
|
switch (grouping_type) {
|
||||||
|
case FOURCC_seig:
|
||||||
|
return ReadWriteEntries(buffer, &cenc_sample_encryption_info_entries);
|
||||||
|
case FOURCC_roll:
|
||||||
|
return ReadWriteEntries(buffer, &audio_roll_recovery_entries);
|
||||||
|
default:
|
||||||
|
DCHECK(buffer->Reading());
|
||||||
|
DLOG(WARNING) << "Sample group '" << grouping_type
|
||||||
|
<< "' is not supported.";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
bool SampleGroupDescription::ReadWriteEntries(BoxBuffer* buffer,
|
||||||
|
std::vector<T>* entries) {
|
||||||
|
uint32_t default_length = 0;
|
||||||
|
if (!buffer->Reading()) {
|
||||||
|
DCHECK(!entries->empty());
|
||||||
|
default_length = (*entries)[0].ComputeSize();
|
||||||
|
DCHECK_NE(default_length, 0u);
|
||||||
|
}
|
||||||
|
if (version == 1)
|
||||||
|
RCHECK(buffer->ReadWriteUInt32(&default_length));
|
||||||
|
if (version >= 2) {
|
||||||
|
NOTIMPLEMENTED() << "Unsupported SampleGroupDescriptionBox 'sgpd' version "
|
||||||
|
<< static_cast<int>(version);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t count = entries->size();
|
||||||
|
RCHECK(buffer->ReadWriteUInt32(&count));
|
||||||
|
RCHECK(count != 0);
|
||||||
|
entries->resize(count);
|
||||||
|
|
||||||
|
for (T& entry : *entries) {
|
||||||
|
if (version == 1) {
|
||||||
|
uint32_t description_length = default_length;
|
||||||
|
if (buffer->Reading() && default_length == 0)
|
||||||
|
RCHECK(buffer->ReadWriteUInt32(&description_length));
|
||||||
|
RCHECK(entry.ReadWrite(buffer));
|
||||||
|
RCHECK(entry.ComputeSize() == description_length);
|
||||||
|
} else {
|
||||||
|
RCHECK(entry.ReadWrite(buffer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t SampleGroupDescription::ComputeSizeInternal() {
|
||||||
|
// Version 0 is obsoleted, so always generate version 1 box.
|
||||||
|
version = 1;
|
||||||
|
size_t entries_size = 0;
|
||||||
|
switch (grouping_type) {
|
||||||
|
case FOURCC_seig:
|
||||||
|
for (const auto& entry : cenc_sample_encryption_info_entries)
|
||||||
|
entries_size += entry.ComputeSize();
|
||||||
|
break;
|
||||||
|
case FOURCC_roll:
|
||||||
|
for (const auto& entry : audio_roll_recovery_entries)
|
||||||
|
entries_size += entry.ComputeSize();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// This box is optional. Skip it if it is not used.
|
||||||
|
if (entries_size == 0)
|
||||||
|
return 0;
|
||||||
|
return HeaderSize() + sizeof(grouping_type) +
|
||||||
|
(version == 1 ? sizeof(uint32_t) : 0) + sizeof(uint32_t) +
|
||||||
|
entries_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
SampleToGroup::SampleToGroup() : grouping_type(0), grouping_type_parameter(0) {}
|
||||||
|
SampleToGroup::~SampleToGroup() {}
|
||||||
|
FourCC SampleToGroup::BoxType() const { return FOURCC_sbgp; }
|
||||||
|
|
||||||
|
bool SampleToGroup::ReadWriteInternal(BoxBuffer* buffer) {
|
||||||
|
RCHECK(ReadWriteHeaderInternal(buffer) &&
|
||||||
|
buffer->ReadWriteUInt32(&grouping_type));
|
||||||
|
if (version == 1)
|
||||||
|
RCHECK(buffer->ReadWriteUInt32(&grouping_type_parameter));
|
||||||
|
|
||||||
|
if (grouping_type != FOURCC_seig && grouping_type != FOURCC_roll) {
|
||||||
|
DCHECK(buffer->Reading());
|
||||||
|
DLOG(WARNING) << "Sample group "
|
||||||
|
<< FourCCToString(static_cast<FourCC>(grouping_type))
|
||||||
|
<< " is not supported.";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t count = entries.size();
|
||||||
|
RCHECK(buffer->ReadWriteUInt32(&count));
|
||||||
|
entries.resize(count);
|
||||||
|
for (uint32_t i = 0; i < count; ++i) {
|
||||||
|
RCHECK(buffer->ReadWriteUInt32(&entries[i].sample_count) &&
|
||||||
|
buffer->ReadWriteUInt32(&entries[i].group_description_index));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t SampleToGroup::ComputeSizeInternal() {
|
||||||
|
// This box is optional. Skip it if it is not used.
|
||||||
|
if (entries.empty())
|
||||||
|
return 0;
|
||||||
|
return HeaderSize() + sizeof(grouping_type) +
|
||||||
|
(version == 1 ? sizeof(grouping_type_parameter) : 0) +
|
||||||
|
sizeof(uint32_t) + entries.size() * sizeof(entries[0]);
|
||||||
|
}
|
||||||
|
|
||||||
SampleTable::SampleTable() {}
|
SampleTable::SampleTable() {}
|
||||||
SampleTable::~SampleTable() {}
|
SampleTable::~SampleTable() {}
|
||||||
FourCC SampleTable::BoxType() const { return FOURCC_stbl; }
|
FourCC SampleTable::BoxType() const { return FOURCC_stbl; }
|
||||||
|
@ -961,15 +1142,30 @@ bool SampleTable::ReadWriteInternal(BoxBuffer* buffer) {
|
||||||
buffer->ReadWriteChild(&chunk_large_offset));
|
buffer->ReadWriteChild(&chunk_large_offset));
|
||||||
}
|
}
|
||||||
RCHECK(buffer->TryReadWriteChild(&sync_sample));
|
RCHECK(buffer->TryReadWriteChild(&sync_sample));
|
||||||
|
if (buffer->Reading()) {
|
||||||
|
RCHECK(buffer->reader()->TryReadChildren(&sample_group_descriptions) &&
|
||||||
|
buffer->reader()->TryReadChildren(&sample_to_groups));
|
||||||
|
} else {
|
||||||
|
for (auto& sample_group_description : sample_group_descriptions)
|
||||||
|
RCHECK(buffer->ReadWriteChild(&sample_group_description));
|
||||||
|
for (auto& sample_to_group : sample_to_groups)
|
||||||
|
RCHECK(buffer->ReadWriteChild(&sample_to_group));
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t SampleTable::ComputeSizeInternal() {
|
uint32_t SampleTable::ComputeSizeInternal() {
|
||||||
return HeaderSize() + description.ComputeSize() +
|
uint32_t box_size =
|
||||||
decoding_time_to_sample.ComputeSize() +
|
HeaderSize() + description.ComputeSize() +
|
||||||
composition_time_to_sample.ComputeSize() +
|
decoding_time_to_sample.ComputeSize() +
|
||||||
sample_to_chunk.ComputeSize() + sample_size.ComputeSize() +
|
composition_time_to_sample.ComputeSize() + sample_to_chunk.ComputeSize() +
|
||||||
chunk_large_offset.ComputeSize() + sync_sample.ComputeSize();
|
sample_size.ComputeSize() + chunk_large_offset.ComputeSize() +
|
||||||
|
sync_sample.ComputeSize();
|
||||||
|
for (auto& sample_group_description : sample_group_descriptions)
|
||||||
|
box_size += sample_group_description.ComputeSize();
|
||||||
|
for (auto& sample_to_group : sample_to_groups)
|
||||||
|
box_size += sample_to_group.ComputeSize();
|
||||||
|
return box_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
EditList::EditList() {}
|
EditList::EditList() {}
|
||||||
|
@ -2199,152 +2395,6 @@ uint32_t TrackFragmentRun::ComputeSizeInternal() {
|
||||||
return box_size;
|
return box_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
SampleToGroup::SampleToGroup() : grouping_type(0), grouping_type_parameter(0) {}
|
|
||||||
SampleToGroup::~SampleToGroup() {}
|
|
||||||
FourCC SampleToGroup::BoxType() const { return FOURCC_sbgp; }
|
|
||||||
|
|
||||||
bool SampleToGroup::ReadWriteInternal(BoxBuffer* buffer) {
|
|
||||||
RCHECK(ReadWriteHeaderInternal(buffer) &&
|
|
||||||
buffer->ReadWriteUInt32(&grouping_type));
|
|
||||||
if (version == 1)
|
|
||||||
RCHECK(buffer->ReadWriteUInt32(&grouping_type_parameter));
|
|
||||||
|
|
||||||
if (grouping_type != FOURCC_seig) {
|
|
||||||
DCHECK(buffer->Reading());
|
|
||||||
DLOG(WARNING) << "Sample group "
|
|
||||||
<< FourCCToString(static_cast<FourCC>(grouping_type))
|
|
||||||
<< " is not supported.";
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t count = entries.size();
|
|
||||||
RCHECK(buffer->ReadWriteUInt32(&count));
|
|
||||||
entries.resize(count);
|
|
||||||
for (uint32_t i = 0; i < count; ++i) {
|
|
||||||
RCHECK(buffer->ReadWriteUInt32(&entries[i].sample_count) &&
|
|
||||||
buffer->ReadWriteUInt32(&entries[i].group_description_index));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t SampleToGroup::ComputeSizeInternal() {
|
|
||||||
// This box is optional. Skip it if it is not used.
|
|
||||||
if (entries.empty())
|
|
||||||
return 0;
|
|
||||||
return HeaderSize() + sizeof(grouping_type) +
|
|
||||||
(version == 1 ? sizeof(grouping_type_parameter) : 0) +
|
|
||||||
sizeof(uint32_t) + entries.size() * sizeof(entries[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
CencSampleEncryptionInfoEntry::CencSampleEncryptionInfoEntry()
|
|
||||||
: is_protected(0),
|
|
||||||
per_sample_iv_size(0),
|
|
||||||
crypt_byte_block(0),
|
|
||||||
skip_byte_block(0) {}
|
|
||||||
CencSampleEncryptionInfoEntry::~CencSampleEncryptionInfoEntry() {};
|
|
||||||
|
|
||||||
SampleGroupDescription::SampleGroupDescription() : grouping_type(0) {}
|
|
||||||
SampleGroupDescription::~SampleGroupDescription() {}
|
|
||||||
FourCC SampleGroupDescription::BoxType() const { return FOURCC_sgpd; }
|
|
||||||
|
|
||||||
bool SampleGroupDescription::ReadWriteInternal(BoxBuffer* buffer) {
|
|
||||||
RCHECK(ReadWriteHeaderInternal(buffer) &&
|
|
||||||
buffer->ReadWriteUInt32(&grouping_type));
|
|
||||||
|
|
||||||
if (grouping_type != FOURCC_seig) {
|
|
||||||
DCHECK(buffer->Reading());
|
|
||||||
DLOG(WARNING) << "Sample group '" << grouping_type << "' is not supported.";
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const size_t kEntrySize = sizeof(uint32_t) + kCencKeyIdSize;
|
|
||||||
uint32_t default_length = 0;
|
|
||||||
if (version == 1) {
|
|
||||||
if (buffer->Reading()) {
|
|
||||||
RCHECK(buffer->ReadWriteUInt32(&default_length));
|
|
||||||
RCHECK(default_length == 0 || default_length >= kEntrySize);
|
|
||||||
} else {
|
|
||||||
default_length = kEntrySize;
|
|
||||||
RCHECK(buffer->ReadWriteUInt32(&default_length));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t count = entries.size();
|
|
||||||
RCHECK(buffer->ReadWriteUInt32(&count));
|
|
||||||
entries.resize(count);
|
|
||||||
for (uint32_t i = 0; i < count; ++i) {
|
|
||||||
if (version == 1) {
|
|
||||||
if (buffer->Reading() && default_length == 0) {
|
|
||||||
uint32_t description_length = 0;
|
|
||||||
RCHECK(buffer->ReadWriteUInt32(&description_length));
|
|
||||||
RCHECK(description_length >= kEntrySize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!buffer->Reading()) {
|
|
||||||
if (entries[i].key_id.size() != kCencKeyIdSize) {
|
|
||||||
LOG(WARNING) << "CENC defines key id length of " << kCencKeyIdSize
|
|
||||||
<< " bytes; got " << entries[i].key_id.size()
|
|
||||||
<< ". Resized accordingly.";
|
|
||||||
entries[i].key_id.resize(kCencKeyIdSize);
|
|
||||||
}
|
|
||||||
RCHECK(entries[i].crypt_byte_block < 16 &&
|
|
||||||
entries[i].skip_byte_block < 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
RCHECK(buffer->IgnoreBytes(1)); // reserved.
|
|
||||||
|
|
||||||
uint8_t pattern =
|
|
||||||
entries[i].crypt_byte_block << 4 | entries[i].skip_byte_block;
|
|
||||||
RCHECK(buffer->ReadWriteUInt8(&pattern));
|
|
||||||
entries[i].crypt_byte_block = pattern >> 4;
|
|
||||||
entries[i].skip_byte_block = pattern & 0x0F;
|
|
||||||
|
|
||||||
RCHECK(buffer->ReadWriteUInt8(&entries[i].is_protected) &&
|
|
||||||
buffer->ReadWriteUInt8(&entries[i].per_sample_iv_size) &&
|
|
||||||
buffer->ReadWriteVector(&entries[i].key_id, kCencKeyIdSize));
|
|
||||||
|
|
||||||
if (entries[i].is_protected == 1) {
|
|
||||||
if (entries[i].per_sample_iv_size == 0) { // For constant iv.
|
|
||||||
uint8_t constant_iv_size = entries[i].constant_iv.size();
|
|
||||||
RCHECK(buffer->ReadWriteUInt8(&constant_iv_size));
|
|
||||||
RCHECK(constant_iv_size == 8 || constant_iv_size == 16);
|
|
||||||
RCHECK(
|
|
||||||
buffer->ReadWriteVector(&entries[i].constant_iv, constant_iv_size));
|
|
||||||
} else {
|
|
||||||
RCHECK(entries[i].per_sample_iv_size == 8 ||
|
|
||||||
entries[i].per_sample_iv_size == 16);
|
|
||||||
RCHECK(entries[i].constant_iv.empty());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Expect |is_protected| to be 0, i.e. not protected. Other values of
|
|
||||||
// |is_protected| is not supported.
|
|
||||||
RCHECK(entries[i].is_protected == 0);
|
|
||||||
RCHECK(entries[i].per_sample_iv_size == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t SampleGroupDescription::ComputeSizeInternal() {
|
|
||||||
// Version 0 is obsoleted, so always generate version 1 box.
|
|
||||||
version = 1;
|
|
||||||
// This box is optional. Skip it if it is not used.
|
|
||||||
if (entries.empty())
|
|
||||||
return 0;
|
|
||||||
size_t entries_size = 0;
|
|
||||||
for (const auto& entry : entries) {
|
|
||||||
entries_size += sizeof(uint32_t) + kCencKeyIdSize +
|
|
||||||
(entry.constant_iv.empty()
|
|
||||||
? 0
|
|
||||||
: (sizeof(uint8_t) + entry.constant_iv.size()));
|
|
||||||
}
|
|
||||||
return HeaderSize() + sizeof(grouping_type) +
|
|
||||||
(version == 1 ? sizeof(uint32_t) : 0) + sizeof(uint32_t) +
|
|
||||||
entries_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
TrackFragment::TrackFragment() : decode_time_absent(false) {}
|
TrackFragment::TrackFragment() : decode_time_absent(false) {}
|
||||||
TrackFragment::~TrackFragment() {}
|
TrackFragment::~TrackFragment() {}
|
||||||
FourCC TrackFragment::BoxType() const { return FOURCC_traf; }
|
FourCC TrackFragment::BoxType() const { return FOURCC_traf; }
|
||||||
|
@ -2358,27 +2408,18 @@ bool TrackFragment::ReadWriteInternal(BoxBuffer* buffer) {
|
||||||
decode_time_absent = !buffer->reader()->ChildExist(&decode_time);
|
decode_time_absent = !buffer->reader()->ChildExist(&decode_time);
|
||||||
if (!decode_time_absent)
|
if (!decode_time_absent)
|
||||||
RCHECK(buffer->ReadWriteChild(&decode_time));
|
RCHECK(buffer->ReadWriteChild(&decode_time));
|
||||||
RCHECK(buffer->reader()->TryReadChildren(&runs));
|
RCHECK(buffer->reader()->TryReadChildren(&runs) &&
|
||||||
|
buffer->reader()->TryReadChildren(&sample_group_descriptions) &&
|
||||||
// There could be multiple SampleGroupDescription and SampleToGroup boxes
|
buffer->reader()->TryReadChildren(&sample_to_groups));
|
||||||
// with different grouping types. For common encryption, the relevant
|
|
||||||
// grouping type is 'seig'. Continue reading until 'seig' is found, or
|
|
||||||
// until running out of child boxes.
|
|
||||||
while (sample_to_group.grouping_type != FOURCC_seig &&
|
|
||||||
buffer->reader()->ChildExist(&sample_to_group)) {
|
|
||||||
RCHECK(buffer->reader()->ReadChild(&sample_to_group));
|
|
||||||
}
|
|
||||||
while (sample_group_description.grouping_type != FOURCC_seig &&
|
|
||||||
buffer->reader()->ChildExist(&sample_group_description)) {
|
|
||||||
RCHECK(buffer->reader()->ReadChild(&sample_group_description));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (!decode_time_absent)
|
if (!decode_time_absent)
|
||||||
RCHECK(buffer->ReadWriteChild(&decode_time));
|
RCHECK(buffer->ReadWriteChild(&decode_time));
|
||||||
for (uint32_t i = 0; i < runs.size(); ++i)
|
for (uint32_t i = 0; i < runs.size(); ++i)
|
||||||
RCHECK(buffer->ReadWriteChild(&runs[i]));
|
RCHECK(buffer->ReadWriteChild(&runs[i]));
|
||||||
RCHECK(buffer->TryReadWriteChild(&sample_to_group) &&
|
for (uint32_t i = 0; i < sample_to_groups.size(); ++i)
|
||||||
buffer->TryReadWriteChild(&sample_group_description));
|
RCHECK(buffer->ReadWriteChild(&sample_to_groups[i]));
|
||||||
|
for (uint32_t i = 0; i < sample_group_descriptions.size(); ++i)
|
||||||
|
RCHECK(buffer->ReadWriteChild(&sample_group_descriptions[i]));
|
||||||
}
|
}
|
||||||
return buffer->TryReadWriteChild(&auxiliary_size) &&
|
return buffer->TryReadWriteChild(&auxiliary_size) &&
|
||||||
buffer->TryReadWriteChild(&auxiliary_offset) &&
|
buffer->TryReadWriteChild(&auxiliary_offset) &&
|
||||||
|
@ -2388,11 +2429,14 @@ bool TrackFragment::ReadWriteInternal(BoxBuffer* buffer) {
|
||||||
uint32_t TrackFragment::ComputeSizeInternal() {
|
uint32_t TrackFragment::ComputeSizeInternal() {
|
||||||
uint32_t box_size =
|
uint32_t box_size =
|
||||||
HeaderSize() + header.ComputeSize() + decode_time.ComputeSize() +
|
HeaderSize() + header.ComputeSize() + decode_time.ComputeSize() +
|
||||||
sample_to_group.ComputeSize() + sample_group_description.ComputeSize() +
|
|
||||||
auxiliary_size.ComputeSize() + auxiliary_offset.ComputeSize() +
|
auxiliary_size.ComputeSize() + auxiliary_offset.ComputeSize() +
|
||||||
sample_encryption.ComputeSize();
|
sample_encryption.ComputeSize();
|
||||||
for (uint32_t i = 0; i < runs.size(); ++i)
|
for (uint32_t i = 0; i < runs.size(); ++i)
|
||||||
box_size += runs[i].ComputeSize();
|
box_size += runs[i].ComputeSize();
|
||||||
|
for (uint32_t i = 0; i < sample_group_descriptions.size(); ++i)
|
||||||
|
box_size += sample_group_descriptions[i].ComputeSize();
|
||||||
|
for (uint32_t i = 0; i < sample_to_groups.size(); ++i)
|
||||||
|
box_size += sample_to_groups[i].ComputeSize();
|
||||||
return box_size;
|
return box_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -468,6 +468,67 @@ struct SyncSample : FullBox {
|
||||||
std::vector<uint32_t> sample_number;
|
std::vector<uint32_t> sample_number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct CencSampleEncryptionInfoEntry {
|
||||||
|
CencSampleEncryptionInfoEntry();
|
||||||
|
~CencSampleEncryptionInfoEntry();
|
||||||
|
|
||||||
|
bool ReadWrite(BoxBuffer* buffer);
|
||||||
|
uint32_t ComputeSize() const;
|
||||||
|
|
||||||
|
uint8_t is_protected;
|
||||||
|
uint8_t per_sample_iv_size;
|
||||||
|
std::vector<uint8_t> key_id;
|
||||||
|
|
||||||
|
// For pattern-based encryption.
|
||||||
|
uint8_t crypt_byte_block;
|
||||||
|
uint8_t skip_byte_block;
|
||||||
|
|
||||||
|
// Present only if |is_protected == 1 && per_sample_iv_size == 0|.
|
||||||
|
std::vector<uint8_t> constant_iv;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AudioRollRecoveryEntry {
|
||||||
|
AudioRollRecoveryEntry();
|
||||||
|
~AudioRollRecoveryEntry();
|
||||||
|
|
||||||
|
bool ReadWrite(BoxBuffer* buffer);
|
||||||
|
uint32_t ComputeSize() const;
|
||||||
|
|
||||||
|
int16_t roll_distance;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SampleGroupDescription : FullBox {
|
||||||
|
DECLARE_BOX_METHODS(SampleGroupDescription);
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
bool ReadWriteEntries(BoxBuffer* buffer, std::vector<T>* entries);
|
||||||
|
|
||||||
|
uint32_t grouping_type;
|
||||||
|
// Only present if grouping_type == 'seig'.
|
||||||
|
std::vector<CencSampleEncryptionInfoEntry>
|
||||||
|
cenc_sample_encryption_info_entries;
|
||||||
|
// Only present if grouping_type == 'roll'.
|
||||||
|
std::vector<AudioRollRecoveryEntry> audio_roll_recovery_entries;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SampleToGroupEntry {
|
||||||
|
enum GroupDescriptionIndexBase {
|
||||||
|
kTrackGroupDescriptionIndexBase = 0,
|
||||||
|
kTrackFragmentGroupDescriptionIndexBase = 0x10000,
|
||||||
|
};
|
||||||
|
|
||||||
|
uint32_t sample_count;
|
||||||
|
uint32_t group_description_index;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SampleToGroup : FullBox {
|
||||||
|
DECLARE_BOX_METHODS(SampleToGroup);
|
||||||
|
|
||||||
|
uint32_t grouping_type;
|
||||||
|
uint32_t grouping_type_parameter; // Version 1 only.
|
||||||
|
std::vector<SampleToGroupEntry> entries;
|
||||||
|
};
|
||||||
|
|
||||||
struct SampleTable : Box {
|
struct SampleTable : Box {
|
||||||
DECLARE_BOX_METHODS(SampleTable);
|
DECLARE_BOX_METHODS(SampleTable);
|
||||||
|
|
||||||
|
@ -481,6 +542,8 @@ struct SampleTable : Box {
|
||||||
// ChunkLargeOffset.
|
// ChunkLargeOffset.
|
||||||
ChunkLargeOffset chunk_large_offset;
|
ChunkLargeOffset chunk_large_offset;
|
||||||
SyncSample sync_sample;
|
SyncSample sync_sample;
|
||||||
|
std::vector<SampleGroupDescription> sample_group_descriptions;
|
||||||
|
std::vector<SampleToGroup> sample_to_groups;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct MediaHeader : FullBox {
|
struct MediaHeader : FullBox {
|
||||||
|
@ -654,47 +717,6 @@ struct TrackFragmentRun : FullBox {
|
||||||
std::vector<int64_t> sample_composition_time_offsets;
|
std::vector<int64_t> sample_composition_time_offsets;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SampleToGroupEntry {
|
|
||||||
enum GroupDescriptionIndexBase {
|
|
||||||
kTrackGroupDescriptionIndexBase = 0,
|
|
||||||
kTrackFragmentGroupDescriptionIndexBase = 0x10000,
|
|
||||||
};
|
|
||||||
|
|
||||||
uint32_t sample_count;
|
|
||||||
uint32_t group_description_index;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct SampleToGroup : FullBox {
|
|
||||||
DECLARE_BOX_METHODS(SampleToGroup);
|
|
||||||
|
|
||||||
uint32_t grouping_type;
|
|
||||||
uint32_t grouping_type_parameter; // Version 1 only.
|
|
||||||
std::vector<SampleToGroupEntry> entries;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct CencSampleEncryptionInfoEntry {
|
|
||||||
CencSampleEncryptionInfoEntry();
|
|
||||||
~CencSampleEncryptionInfoEntry();
|
|
||||||
|
|
||||||
uint8_t is_protected;
|
|
||||||
uint8_t per_sample_iv_size;
|
|
||||||
std::vector<uint8_t> key_id;
|
|
||||||
|
|
||||||
// For pattern-based encryption.
|
|
||||||
uint8_t crypt_byte_block;
|
|
||||||
uint8_t skip_byte_block;
|
|
||||||
|
|
||||||
// Present only if |is_protected == 1 && per_sample_iv_size == 0|.
|
|
||||||
std::vector<uint8_t> constant_iv;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct SampleGroupDescription : FullBox {
|
|
||||||
DECLARE_BOX_METHODS(SampleGroupDescription);
|
|
||||||
|
|
||||||
uint32_t grouping_type;
|
|
||||||
std::vector<CencSampleEncryptionInfoEntry> entries;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct TrackFragment : Box {
|
struct TrackFragment : Box {
|
||||||
DECLARE_BOX_METHODS(TrackFragment);
|
DECLARE_BOX_METHODS(TrackFragment);
|
||||||
|
|
||||||
|
@ -702,8 +724,8 @@ struct TrackFragment : Box {
|
||||||
std::vector<TrackFragmentRun> runs;
|
std::vector<TrackFragmentRun> runs;
|
||||||
bool decode_time_absent;
|
bool decode_time_absent;
|
||||||
TrackFragmentDecodeTime decode_time;
|
TrackFragmentDecodeTime decode_time;
|
||||||
SampleToGroup sample_to_group;
|
std::vector<SampleGroupDescription> sample_group_descriptions;
|
||||||
SampleGroupDescription sample_group_description;
|
std::vector<SampleToGroup> sample_to_groups;
|
||||||
SampleAuxiliaryInformationSize auxiliary_size;
|
SampleAuxiliaryInformationSize auxiliary_size;
|
||||||
SampleAuxiliaryInformationOffset auxiliary_offset;
|
SampleAuxiliaryInformationOffset auxiliary_offset;
|
||||||
SampleEncryption sample_encryption;
|
SampleEncryption sample_encryption;
|
||||||
|
|
|
@ -156,6 +156,42 @@ inline bool operator==(const SyncSample& lhs, const SyncSample& rhs) {
|
||||||
return lhs.sample_number == rhs.sample_number;
|
return lhs.sample_number == rhs.sample_number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline bool operator==(const CencSampleEncryptionInfoEntry& lhs,
|
||||||
|
const CencSampleEncryptionInfoEntry& rhs) {
|
||||||
|
return lhs.is_protected == rhs.is_protected &&
|
||||||
|
lhs.per_sample_iv_size == rhs.per_sample_iv_size &&
|
||||||
|
lhs.key_id == rhs.key_id &&
|
||||||
|
lhs.crypt_byte_block == rhs.crypt_byte_block &&
|
||||||
|
lhs.skip_byte_block == rhs.skip_byte_block &&
|
||||||
|
lhs.constant_iv == rhs.constant_iv;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool operator==(const AudioRollRecoveryEntry& lhs,
|
||||||
|
const AudioRollRecoveryEntry& rhs) {
|
||||||
|
return lhs.roll_distance == rhs.roll_distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool operator==(const SampleGroupDescription& lhs,
|
||||||
|
const SampleGroupDescription& rhs) {
|
||||||
|
return lhs.grouping_type == rhs.grouping_type &&
|
||||||
|
lhs.cenc_sample_encryption_info_entries ==
|
||||||
|
rhs.cenc_sample_encryption_info_entries &&
|
||||||
|
lhs.audio_roll_recovery_entries == rhs.audio_roll_recovery_entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool operator==(const SampleToGroupEntry& lhs,
|
||||||
|
const SampleToGroupEntry& rhs) {
|
||||||
|
return lhs.sample_count == rhs.sample_count &&
|
||||||
|
lhs.group_description_index == rhs.group_description_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool operator==(const SampleToGroup& lhs,
|
||||||
|
const SampleToGroup& rhs) {
|
||||||
|
return lhs.grouping_type == rhs.grouping_type &&
|
||||||
|
lhs.grouping_type_parameter == rhs.grouping_type_parameter &&
|
||||||
|
lhs.entries == rhs.entries;
|
||||||
|
}
|
||||||
|
|
||||||
inline bool operator==(const SampleTable& lhs, const SampleTable& rhs) {
|
inline bool operator==(const SampleTable& lhs, const SampleTable& rhs) {
|
||||||
return lhs.description == rhs.description &&
|
return lhs.description == rhs.description &&
|
||||||
lhs.decoding_time_to_sample == rhs.decoding_time_to_sample &&
|
lhs.decoding_time_to_sample == rhs.decoding_time_to_sample &&
|
||||||
|
@ -163,7 +199,9 @@ inline bool operator==(const SampleTable& lhs, const SampleTable& rhs) {
|
||||||
lhs.sample_to_chunk == rhs.sample_to_chunk &&
|
lhs.sample_to_chunk == rhs.sample_to_chunk &&
|
||||||
lhs.sample_size == rhs.sample_size &&
|
lhs.sample_size == rhs.sample_size &&
|
||||||
lhs.chunk_large_offset == rhs.chunk_large_offset &&
|
lhs.chunk_large_offset == rhs.chunk_large_offset &&
|
||||||
lhs.sync_sample == rhs.sync_sample;
|
lhs.sync_sample == rhs.sync_sample &&
|
||||||
|
lhs.sample_group_descriptions == rhs.sample_group_descriptions &&
|
||||||
|
lhs.sample_to_groups == rhs.sample_to_groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool operator==(const EditListEntry& lhs, const EditListEntry& rhs) {
|
inline bool operator==(const EditListEntry& lhs, const EditListEntry& rhs) {
|
||||||
|
@ -385,35 +423,6 @@ inline bool operator==(const TrackFragmentRun& lhs,
|
||||||
rhs.sample_composition_time_offsets;
|
rhs.sample_composition_time_offsets;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool operator==(const SampleToGroupEntry& lhs,
|
|
||||||
const SampleToGroupEntry& rhs) {
|
|
||||||
return lhs.sample_count == rhs.sample_count &&
|
|
||||||
lhs.group_description_index == rhs.group_description_index;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool operator==(const SampleToGroup& lhs,
|
|
||||||
const SampleToGroup& rhs) {
|
|
||||||
return lhs.grouping_type == rhs.grouping_type &&
|
|
||||||
lhs.grouping_type_parameter == rhs.grouping_type_parameter &&
|
|
||||||
lhs.entries == rhs.entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool operator==(const CencSampleEncryptionInfoEntry& lhs,
|
|
||||||
const CencSampleEncryptionInfoEntry& rhs) {
|
|
||||||
return lhs.is_protected == rhs.is_protected &&
|
|
||||||
lhs.per_sample_iv_size == rhs.per_sample_iv_size &&
|
|
||||||
lhs.key_id == rhs.key_id &&
|
|
||||||
lhs.crypt_byte_block == rhs.crypt_byte_block &&
|
|
||||||
lhs.skip_byte_block == rhs.skip_byte_block &&
|
|
||||||
lhs.constant_iv == rhs.constant_iv;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool operator==(const SampleGroupDescription& lhs,
|
|
||||||
const SampleGroupDescription& rhs) {
|
|
||||||
return lhs.grouping_type == rhs.grouping_type &&
|
|
||||||
lhs.entries == rhs.entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool operator==(const TrackFragment& lhs, const TrackFragment& rhs) {
|
inline bool operator==(const TrackFragment& lhs, const TrackFragment& rhs) {
|
||||||
return lhs.header == rhs.header && lhs.runs == rhs.runs &&
|
return lhs.header == rhs.header && lhs.runs == rhs.runs &&
|
||||||
lhs.decode_time == rhs.decode_time &&
|
lhs.decode_time == rhs.decode_time &&
|
||||||
|
|
|
@ -567,6 +567,45 @@ class BoxDefinitionsTestGeneral : public testing::Test {
|
||||||
|
|
||||||
void Modify(SyncSample* stss) { stss->sample_number.pop_back(); }
|
void Modify(SyncSample* stss) { stss->sample_number.pop_back(); }
|
||||||
|
|
||||||
|
void Fill(SampleGroupDescription* sgpd) {
|
||||||
|
sgpd->grouping_type = FOURCC_seig;
|
||||||
|
sgpd->cenc_sample_encryption_info_entries.resize(2);
|
||||||
|
sgpd->cenc_sample_encryption_info_entries[0].is_protected = 1;
|
||||||
|
sgpd->cenc_sample_encryption_info_entries[0].per_sample_iv_size = 8;
|
||||||
|
sgpd->cenc_sample_encryption_info_entries[0].key_id.assign(
|
||||||
|
kData16Bytes, kData16Bytes + arraysize(kData16Bytes));
|
||||||
|
sgpd->cenc_sample_encryption_info_entries[0].crypt_byte_block = 3;
|
||||||
|
sgpd->cenc_sample_encryption_info_entries[0].skip_byte_block = 7;
|
||||||
|
sgpd->cenc_sample_encryption_info_entries[1].is_protected = 0;
|
||||||
|
sgpd->cenc_sample_encryption_info_entries[1].per_sample_iv_size = 0;
|
||||||
|
sgpd->cenc_sample_encryption_info_entries[1].key_id.resize(16);
|
||||||
|
sgpd->version = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Modify(SampleGroupDescription* sgpd) {
|
||||||
|
sgpd->cenc_sample_encryption_info_entries.resize(1);
|
||||||
|
sgpd->cenc_sample_encryption_info_entries[0].is_protected = 1;
|
||||||
|
sgpd->cenc_sample_encryption_info_entries[0].per_sample_iv_size = 0;
|
||||||
|
sgpd->cenc_sample_encryption_info_entries[0].constant_iv.assign(
|
||||||
|
kData16Bytes, kData16Bytes + arraysize(kData16Bytes));
|
||||||
|
sgpd->cenc_sample_encryption_info_entries[0].key_id.resize(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Fill(SampleToGroup* sbgp) {
|
||||||
|
sbgp->grouping_type = FOURCC_seig;
|
||||||
|
sbgp->entries.resize(2);
|
||||||
|
sbgp->entries[0].sample_count = 3;
|
||||||
|
sbgp->entries[0].group_description_index = 0x10002;
|
||||||
|
sbgp->entries[1].sample_count = 1212;
|
||||||
|
sbgp->entries[1].group_description_index = 0x10001;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Modify(SampleToGroup* sbgp) {
|
||||||
|
sbgp->entries.resize(1);
|
||||||
|
sbgp->entries[0].sample_count = 5;
|
||||||
|
sbgp->entries[0].group_description_index = 0x10001;
|
||||||
|
}
|
||||||
|
|
||||||
void Fill(SampleTable* stbl) {
|
void Fill(SampleTable* stbl) {
|
||||||
Fill(&stbl->description);
|
Fill(&stbl->description);
|
||||||
Fill(&stbl->decoding_time_to_sample);
|
Fill(&stbl->decoding_time_to_sample);
|
||||||
|
@ -575,11 +614,17 @@ class BoxDefinitionsTestGeneral : public testing::Test {
|
||||||
Fill(&stbl->sample_size);
|
Fill(&stbl->sample_size);
|
||||||
Fill(&stbl->chunk_large_offset);
|
Fill(&stbl->chunk_large_offset);
|
||||||
Fill(&stbl->sync_sample);
|
Fill(&stbl->sync_sample);
|
||||||
|
stbl->sample_group_descriptions.resize(1);
|
||||||
|
Fill(&stbl->sample_group_descriptions[0]);
|
||||||
|
stbl->sample_to_groups.resize(1);
|
||||||
|
Fill(&stbl->sample_to_groups[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Modify(SampleTable* stbl) {
|
void Modify(SampleTable* stbl) {
|
||||||
Modify(&stbl->chunk_large_offset);
|
Modify(&stbl->chunk_large_offset);
|
||||||
Modify(&stbl->sync_sample);
|
Modify(&stbl->sync_sample);
|
||||||
|
stbl->sample_group_descriptions.clear();
|
||||||
|
stbl->sample_to_groups.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Fill(MediaHeader* mdhd) {
|
void Fill(MediaHeader* mdhd) {
|
||||||
|
@ -768,47 +813,6 @@ class BoxDefinitionsTestGeneral : public testing::Test {
|
||||||
trun->version = 0;
|
trun->version = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Fill(SampleToGroup* sbgp) {
|
|
||||||
sbgp->grouping_type = FOURCC_seig;
|
|
||||||
sbgp->entries.resize(2);
|
|
||||||
sbgp->entries[0].sample_count = 3;
|
|
||||||
sbgp->entries[0].group_description_index = 0x10002;
|
|
||||||
sbgp->entries[1].sample_count = 1212;
|
|
||||||
sbgp->entries[1].group_description_index = 0x10001;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Modify(SampleToGroup* sbgp) {
|
|
||||||
sbgp->entries.resize(1);
|
|
||||||
sbgp->entries[0].sample_count = 5;
|
|
||||||
sbgp->entries[0].group_description_index = 0x10001;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Fill(SampleGroupDescription* sgpd) {
|
|
||||||
sgpd->grouping_type = FOURCC_seig;
|
|
||||||
sgpd->entries.resize(3);
|
|
||||||
sgpd->entries[0].is_protected = 1;
|
|
||||||
sgpd->entries[0].per_sample_iv_size = 8;
|
|
||||||
sgpd->entries[0].key_id.assign(kData16Bytes,
|
|
||||||
kData16Bytes + arraysize(kData16Bytes));
|
|
||||||
sgpd->entries[0].crypt_byte_block = 3;
|
|
||||||
sgpd->entries[0].skip_byte_block = 7;
|
|
||||||
sgpd->entries[1].is_protected = 0;
|
|
||||||
sgpd->entries[1].per_sample_iv_size = 0;
|
|
||||||
sgpd->entries[1].key_id.resize(16);
|
|
||||||
sgpd->entries[2].is_protected = 1;
|
|
||||||
sgpd->entries[2].per_sample_iv_size = 0;
|
|
||||||
sgpd->entries[2].constant_iv.assign(kData16Bytes,
|
|
||||||
kData16Bytes + arraysize(kData16Bytes));
|
|
||||||
sgpd->entries[2].key_id.resize(16);
|
|
||||||
sgpd->version = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Modify(SampleGroupDescription* sgpd) {
|
|
||||||
sgpd->entries.resize(1);
|
|
||||||
sgpd->entries[0].key_id[4] = 88;
|
|
||||||
sgpd->version = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Fill(TrackFragment* traf) {
|
void Fill(TrackFragment* traf) {
|
||||||
Fill(&traf->header);
|
Fill(&traf->header);
|
||||||
traf->runs.resize(1);
|
traf->runs.resize(1);
|
||||||
|
@ -821,8 +825,19 @@ class BoxDefinitionsTestGeneral : public testing::Test {
|
||||||
void Modify(TrackFragment* traf) {
|
void Modify(TrackFragment* traf) {
|
||||||
Modify(&traf->header);
|
Modify(&traf->header);
|
||||||
Modify(&traf->decode_time);
|
Modify(&traf->decode_time);
|
||||||
Fill(&traf->sample_to_group);
|
|
||||||
Fill(&traf->sample_group_description);
|
traf->sample_group_descriptions.resize(2);
|
||||||
|
Fill(&traf->sample_group_descriptions[0]);
|
||||||
|
traf->sample_group_descriptions[1].grouping_type = FOURCC_roll;
|
||||||
|
traf->sample_group_descriptions[1].audio_roll_recovery_entries.resize(1);
|
||||||
|
traf->sample_group_descriptions[1]
|
||||||
|
.audio_roll_recovery_entries[0]
|
||||||
|
.roll_distance = -10;
|
||||||
|
|
||||||
|
traf->sample_to_groups.resize(2);
|
||||||
|
Fill(&traf->sample_to_groups[0]);
|
||||||
|
Modify(&traf->sample_to_groups[1]);
|
||||||
|
traf->sample_to_groups[1].grouping_type = FOURCC_roll;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Fill(MovieFragment* moof) {
|
void Fill(MovieFragment* moof) {
|
||||||
|
@ -951,10 +966,10 @@ class BoxDefinitionsTestGeneral : public testing::Test {
|
||||||
bool IsOptional(const WebVTTSourceLabelBox* box) { return true; }
|
bool IsOptional(const WebVTTSourceLabelBox* box) { return true; }
|
||||||
bool IsOptional(const CompositionTimeToSample* box) { return true; }
|
bool IsOptional(const CompositionTimeToSample* box) { return true; }
|
||||||
bool IsOptional(const SyncSample* box) { return true; }
|
bool IsOptional(const SyncSample* box) { return true; }
|
||||||
|
bool IsOptional(const SampleGroupDescription* box) { return true; }
|
||||||
|
bool IsOptional(const SampleToGroup* box) { return true; }
|
||||||
bool IsOptional(const MovieExtendsHeader* box) { return true; }
|
bool IsOptional(const MovieExtendsHeader* box) { return true; }
|
||||||
bool IsOptional(const MovieExtends* box) { return true; }
|
bool IsOptional(const MovieExtends* box) { return true; }
|
||||||
bool IsOptional(const SampleToGroup* box) { return true; }
|
|
||||||
bool IsOptional(const SampleGroupDescription* box) { return true; }
|
|
||||||
bool IsOptional(const CueSourceIDBox* box) { return true; }
|
bool IsOptional(const CueSourceIDBox* box) { return true; }
|
||||||
bool IsOptional(const CueIDBox* box) { return true; }
|
bool IsOptional(const CueIDBox* box) { return true; }
|
||||||
bool IsOptional(const CueTimeBox* box) { return true; }
|
bool IsOptional(const CueTimeBox* box) { return true; }
|
||||||
|
@ -1006,6 +1021,8 @@ typedef testing::Types<FileType,
|
||||||
ChunkLargeOffset,
|
ChunkLargeOffset,
|
||||||
ChunkOffset,
|
ChunkOffset,
|
||||||
SyncSample,
|
SyncSample,
|
||||||
|
SampleGroupDescription,
|
||||||
|
SampleToGroup,
|
||||||
SampleTable>
|
SampleTable>
|
||||||
Boxes;
|
Boxes;
|
||||||
typedef testing::Types<MediaHeader,
|
typedef testing::Types<MediaHeader,
|
||||||
|
@ -1029,8 +1046,6 @@ typedef testing::Types<MediaHeader,
|
||||||
TrackFragment,
|
TrackFragment,
|
||||||
MovieFragment,
|
MovieFragment,
|
||||||
SegmentIndex,
|
SegmentIndex,
|
||||||
SampleToGroup,
|
|
||||||
SampleGroupDescription,
|
|
||||||
CueSourceIDBox,
|
CueSourceIDBox,
|
||||||
CueTimeBox,
|
CueTimeBox,
|
||||||
CueIDBox,
|
CueIDBox,
|
||||||
|
|
|
@ -67,7 +67,7 @@ EncryptingFragmenter::EncryptingFragmenter(
|
||||||
FourCC protection_scheme,
|
FourCC protection_scheme,
|
||||||
uint8_t crypt_byte_block,
|
uint8_t crypt_byte_block,
|
||||||
uint8_t skip_byte_block)
|
uint8_t skip_byte_block)
|
||||||
: Fragmenter(traf),
|
: Fragmenter(info, traf),
|
||||||
info_(info),
|
info_(info),
|
||||||
encryption_key_(encryption_key.Pass()),
|
encryption_key_(encryption_key.Pass()),
|
||||||
nalu_length_size_(GetNaluLengthSize(*info)),
|
nalu_length_size_(GetNaluLengthSize(*info)),
|
||||||
|
|
|
@ -26,6 +26,7 @@ namespace mp4 {
|
||||||
/// EncryptingFragmenter generates MP4 fragments with sample encrypted.
|
/// EncryptingFragmenter generates MP4 fragments with sample encrypted.
|
||||||
class EncryptingFragmenter : public Fragmenter {
|
class EncryptingFragmenter : public Fragmenter {
|
||||||
public:
|
public:
|
||||||
|
/// @param info contains stream information.
|
||||||
/// @param traf points to a TrackFragment box.
|
/// @param traf points to a TrackFragment box.
|
||||||
/// @param encryption_key contains the encryption parameters.
|
/// @param encryption_key contains the encryption parameters.
|
||||||
/// @param clear_time specifies clear lead duration in units of the current
|
/// @param clear_time specifies clear lead duration in units of the current
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
|
||||||
#include "packager/media/base/buffer_writer.h"
|
#include "packager/media/base/buffer_writer.h"
|
||||||
|
#include "packager/media/base/audio_stream_info.h"
|
||||||
#include "packager/media/base/media_sample.h"
|
#include "packager/media/base/media_sample.h"
|
||||||
#include "packager/media/formats/mp4/box_definitions.h"
|
#include "packager/media/formats/mp4/box_definitions.h"
|
||||||
|
|
||||||
|
@ -18,10 +19,18 @@ namespace mp4 {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
const int64_t kInvalidTime = std::numeric_limits<int64_t>::max();
|
const int64_t kInvalidTime = std::numeric_limits<int64_t>::max();
|
||||||
|
|
||||||
|
uint64_t GetSeekPreroll(const StreamInfo& stream_info) {
|
||||||
|
if (stream_info.stream_type() != kStreamAudio) return 0;
|
||||||
|
const AudioStreamInfo& audio_stream_info =
|
||||||
|
static_cast<const AudioStreamInfo&>(stream_info);
|
||||||
|
return audio_stream_info.seek_preroll_ns();
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Fragmenter::Fragmenter(TrackFragment* traf)
|
Fragmenter::Fragmenter(scoped_refptr<StreamInfo> info, TrackFragment* traf)
|
||||||
: traf_(traf),
|
: traf_(traf),
|
||||||
|
seek_preroll_(GetSeekPreroll(*info)),
|
||||||
fragment_initialized_(false),
|
fragment_initialized_(false),
|
||||||
fragment_finalized_(false),
|
fragment_finalized_(false),
|
||||||
fragment_duration_(0),
|
fragment_duration_(0),
|
||||||
|
@ -80,6 +89,8 @@ Status Fragmenter::InitializeFragment(int64_t first_sample_dts) {
|
||||||
traf_->runs.clear();
|
traf_->runs.clear();
|
||||||
traf_->runs.resize(1);
|
traf_->runs.resize(1);
|
||||||
traf_->runs[0].flags = TrackFragmentRun::kDataOffsetPresentMask;
|
traf_->runs[0].flags = TrackFragmentRun::kDataOffsetPresentMask;
|
||||||
|
traf_->sample_group_descriptions.clear();
|
||||||
|
traf_->sample_to_groups.clear();
|
||||||
traf_->header.sample_description_index = 1; // 1-based.
|
traf_->header.sample_description_index = 1; // 1-based.
|
||||||
traf_->header.flags = TrackFragmentHeader::kDefaultBaseIsMoofMask |
|
traf_->header.flags = TrackFragmentHeader::kDefaultBaseIsMoofMask |
|
||||||
TrackFragmentHeader::kSampleDescriptionIndexPresentMask;
|
TrackFragmentHeader::kSampleDescriptionIndexPresentMask;
|
||||||
|
@ -113,6 +124,35 @@ void Fragmenter::FinalizeFragment() {
|
||||||
traf_->runs[0].flags |= TrackFragmentRun::kSampleFlagsPresentMask;
|
traf_->runs[0].flags |= TrackFragmentRun::kSampleFlagsPresentMask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add SampleToGroup boxes. A SampleToGroup box with grouping type of 'roll'
|
||||||
|
// needs to be added if there is seek preroll, referencing sample group
|
||||||
|
// description in track level; Also need to add SampleToGroup boxes
|
||||||
|
// correponding to every SampleGroupDescription boxes, referencing sample
|
||||||
|
// group description in fragment level.
|
||||||
|
DCHECK_EQ(traf_->sample_to_groups.size(), 0u);
|
||||||
|
if (seek_preroll_ > 0) {
|
||||||
|
traf_->sample_to_groups.resize(traf_->sample_to_groups.size() + 1);
|
||||||
|
SampleToGroup& sample_to_group = traf_->sample_to_groups.back();
|
||||||
|
sample_to_group.grouping_type = FOURCC_roll;
|
||||||
|
|
||||||
|
sample_to_group.entries.resize(1);
|
||||||
|
SampleToGroupEntry& sample_to_group_entry = sample_to_group.entries.back();
|
||||||
|
sample_to_group_entry.sample_count = traf_->runs[0].sample_count;
|
||||||
|
sample_to_group_entry.group_description_index =
|
||||||
|
SampleToGroupEntry::kTrackGroupDescriptionIndexBase + 1;
|
||||||
|
}
|
||||||
|
for (const auto& sample_group_description : traf_->sample_group_descriptions) {
|
||||||
|
traf_->sample_to_groups.resize(traf_->sample_to_groups.size() + 1);
|
||||||
|
SampleToGroup& sample_to_group = traf_->sample_to_groups.back();
|
||||||
|
sample_to_group.grouping_type = sample_group_description.grouping_type;
|
||||||
|
|
||||||
|
sample_to_group.entries.resize(1);
|
||||||
|
SampleToGroupEntry& sample_to_group_entry = sample_to_group.entries.back();
|
||||||
|
sample_to_group_entry.sample_count = traf_->runs[0].sample_count;
|
||||||
|
sample_to_group_entry.group_description_index =
|
||||||
|
SampleToGroupEntry::kTrackFragmentGroupDescriptionIndexBase + 1;
|
||||||
|
}
|
||||||
|
|
||||||
fragment_finalized_ = true;
|
fragment_finalized_ = true;
|
||||||
fragment_initialized_ = false;
|
fragment_initialized_ = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ namespace media {
|
||||||
|
|
||||||
class BufferWriter;
|
class BufferWriter;
|
||||||
class MediaSample;
|
class MediaSample;
|
||||||
|
class StreamInfo;
|
||||||
|
|
||||||
namespace mp4 {
|
namespace mp4 {
|
||||||
|
|
||||||
|
@ -29,8 +30,9 @@ struct TrackFragment;
|
||||||
/// box and corresponding 'mdat' box.
|
/// box and corresponding 'mdat' box.
|
||||||
class Fragmenter {
|
class Fragmenter {
|
||||||
public:
|
public:
|
||||||
|
/// @param info contains stream information.
|
||||||
/// @param traf points to a TrackFragment box.
|
/// @param traf points to a TrackFragment box.
|
||||||
Fragmenter(TrackFragment* traf);
|
Fragmenter(scoped_refptr<StreamInfo> info, TrackFragment* traf);
|
||||||
|
|
||||||
virtual ~Fragmenter();
|
virtual ~Fragmenter();
|
||||||
|
|
||||||
|
@ -74,6 +76,7 @@ class Fragmenter {
|
||||||
bool StartsWithSAP();
|
bool StartsWithSAP();
|
||||||
|
|
||||||
TrackFragment* traf_;
|
TrackFragment* traf_;
|
||||||
|
uint64_t seek_preroll_;
|
||||||
bool fragment_initialized_;
|
bool fragment_initialized_;
|
||||||
bool fragment_finalized_;
|
bool fragment_finalized_;
|
||||||
uint64_t fragment_duration_;
|
uint64_t fragment_duration_;
|
||||||
|
|
|
@ -107,9 +107,15 @@ Status KeyRotationFragmenter::PrepareFragmentForEncryption(
|
||||||
// i.e. there is at most one key for the fragment. So there should be only
|
// i.e. there is at most one key for the fragment. So there should be only
|
||||||
// one entry in SampleGroupDescription box and one entry in SampleToGroup box.
|
// one entry in SampleGroupDescription box and one entry in SampleToGroup box.
|
||||||
// Fill in SampleGroupDescription box information.
|
// Fill in SampleGroupDescription box information.
|
||||||
traf()->sample_group_description.grouping_type = FOURCC_seig;
|
traf()->sample_group_descriptions.resize(
|
||||||
traf()->sample_group_description.entries.resize(1);
|
traf()->sample_group_descriptions.size() + 1);
|
||||||
auto& sample_group_entry = traf()->sample_group_description.entries[0];
|
SampleGroupDescription& sample_group_description =
|
||||||
|
traf()->sample_group_descriptions.back();
|
||||||
|
sample_group_description.grouping_type = FOURCC_seig;
|
||||||
|
|
||||||
|
sample_group_description.cenc_sample_encryption_info_entries.resize(1);
|
||||||
|
CencSampleEncryptionInfoEntry& sample_group_entry =
|
||||||
|
sample_group_description.cenc_sample_encryption_info_entries.back();
|
||||||
sample_group_entry.is_protected = 1;
|
sample_group_entry.is_protected = 1;
|
||||||
if (protection_scheme() == FOURCC_cbcs) {
|
if (protection_scheme() == FOURCC_cbcs) {
|
||||||
// For 'cbcs' scheme, Constant IVs SHALL be used.
|
// For 'cbcs' scheme, Constant IVs SHALL be used.
|
||||||
|
@ -122,23 +128,9 @@ Status KeyRotationFragmenter::PrepareFragmentForEncryption(
|
||||||
sample_group_entry.skip_byte_block = skip_byte_block();
|
sample_group_entry.skip_byte_block = skip_byte_block();
|
||||||
sample_group_entry.key_id = encryption_key()->key_id;
|
sample_group_entry.key_id = encryption_key()->key_id;
|
||||||
|
|
||||||
// Fill in SampleToGroup box information.
|
|
||||||
traf()->sample_to_group.grouping_type = FOURCC_seig;
|
|
||||||
traf()->sample_to_group.entries.resize(1);
|
|
||||||
// sample_count is adjusted in |FinalizeFragment| later.
|
|
||||||
traf()->sample_to_group.entries[0].group_description_index =
|
|
||||||
SampleToGroupEntry::kTrackFragmentGroupDescriptionIndexBase + 1;
|
|
||||||
|
|
||||||
return Status::OK;
|
return Status::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
void KeyRotationFragmenter::FinalizeFragmentForEncryption() {
|
|
||||||
EncryptingFragmenter::FinalizeFragmentForEncryption();
|
|
||||||
DCHECK_EQ(1u, traf()->sample_to_group.entries.size());
|
|
||||||
traf()->sample_to_group.entries[0].sample_count =
|
|
||||||
traf()->auxiliary_size.sample_count;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace mp4
|
} // namespace mp4
|
||||||
} // namespace media
|
} // namespace media
|
||||||
} // namespace edash_packager
|
} // namespace edash_packager
|
||||||
|
|
|
@ -23,6 +23,7 @@ struct MovieFragment;
|
||||||
class KeyRotationFragmenter : public EncryptingFragmenter {
|
class KeyRotationFragmenter : public EncryptingFragmenter {
|
||||||
public:
|
public:
|
||||||
/// @param moof points to a MovieFragment box.
|
/// @param moof points to a MovieFragment box.
|
||||||
|
/// @param info contains stream information.
|
||||||
/// @param traf points to a TrackFragment box.
|
/// @param traf points to a TrackFragment box.
|
||||||
/// @param encryption_key_source points to the source which generates
|
/// @param encryption_key_source points to the source which generates
|
||||||
/// encryption keys.
|
/// encryption keys.
|
||||||
|
@ -57,7 +58,6 @@ class KeyRotationFragmenter : public EncryptingFragmenter {
|
||||||
/// @name Fragmenter implementation overrides.
|
/// @name Fragmenter implementation overrides.
|
||||||
/// @{
|
/// @{
|
||||||
Status PrepareFragmentForEncryption(bool enable_encryption) override;
|
Status PrepareFragmentForEncryption(bool enable_encryption) override;
|
||||||
void FinalizeFragmentForEncryption() override;
|
|
||||||
/// @}
|
/// @}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
@ -440,6 +440,34 @@ bool MP4MediaParser::ParseMoov(BoxReader* reader) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract possible seek preroll.
|
||||||
|
uint64_t seek_preroll_ns = 0;
|
||||||
|
for (const auto& sample_group_description :
|
||||||
|
track->media.information.sample_table.sample_group_descriptions) {
|
||||||
|
if (sample_group_description.grouping_type != FOURCC_roll)
|
||||||
|
continue;
|
||||||
|
const auto& audio_roll_recovery_entries =
|
||||||
|
sample_group_description.audio_roll_recovery_entries;
|
||||||
|
if (audio_roll_recovery_entries.size() != 1) {
|
||||||
|
LOG(WARNING) << "Unexpected number of entries in "
|
||||||
|
"SampleGroupDescription table with grouping type "
|
||||||
|
"'roll'.";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const int16_t roll_distance_in_samples =
|
||||||
|
audio_roll_recovery_entries[0].roll_distance;
|
||||||
|
if (roll_distance_in_samples < 0) {
|
||||||
|
RCHECK(sampling_frequency != 0);
|
||||||
|
seek_preroll_ns = kNanosecondsPerSecond *
|
||||||
|
(-roll_distance_in_samples) / sampling_frequency;
|
||||||
|
} else {
|
||||||
|
LOG(WARNING)
|
||||||
|
<< "Roll distance is supposed to be negative, but seeing "
|
||||||
|
<< roll_distance_in_samples;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
const bool is_encrypted =
|
const bool is_encrypted =
|
||||||
entry.sinf.info.track_encryption.default_is_protected == 1;
|
entry.sinf.info.track_encryption.default_is_protected == 1;
|
||||||
DVLOG(1) << "is_audio_track_encrypted_: " << is_encrypted;
|
DVLOG(1) << "is_audio_track_encrypted_: " << is_encrypted;
|
||||||
|
@ -453,7 +481,7 @@ bool MP4MediaParser::ParseMoov(BoxReader* reader) {
|
||||||
entry.samplesize,
|
entry.samplesize,
|
||||||
num_channels,
|
num_channels,
|
||||||
sampling_frequency,
|
sampling_frequency,
|
||||||
0 /* seek preroll */,
|
seek_preroll_ns,
|
||||||
codec_delay_ns,
|
codec_delay_ns,
|
||||||
max_bitrate,
|
max_bitrate,
|
||||||
avg_bitrate,
|
avg_bitrate,
|
||||||
|
|
|
@ -287,10 +287,41 @@ void MP4Muxer::GenerateAudioTrak(const AudioStreamInfo* audio_info,
|
||||||
audio.channelcount = audio_info->num_channels();
|
audio.channelcount = audio_info->num_channels();
|
||||||
audio.samplesize = audio_info->sample_bits();
|
audio.samplesize = audio_info->sample_bits();
|
||||||
audio.samplerate = audio_info->sampling_frequency();
|
audio.samplerate = audio_info->sampling_frequency();
|
||||||
SampleDescription& sample_description =
|
SampleTable& sample_table = trak->media.information.sample_table;
|
||||||
trak->media.information.sample_table.description;
|
SampleDescription& sample_description = sample_table.description;
|
||||||
sample_description.type = kAudio;
|
sample_description.type = kAudio;
|
||||||
sample_description.audio_entries.push_back(audio);
|
sample_description.audio_entries.push_back(audio);
|
||||||
|
|
||||||
|
// Opus requires at least one sample group description box and at least one
|
||||||
|
// sample to group box with grouping type 'roll' within sample table box.
|
||||||
|
if (audio_info->codec() == kCodecOpus) {
|
||||||
|
sample_table.sample_group_descriptions.resize(1);
|
||||||
|
SampleGroupDescription& sample_group_description =
|
||||||
|
sample_table.sample_group_descriptions.back();
|
||||||
|
sample_group_description.grouping_type = FOURCC_roll;
|
||||||
|
sample_group_description.audio_roll_recovery_entries.resize(1);
|
||||||
|
// The roll distance is expressed in sample units and always takes negative
|
||||||
|
// values.
|
||||||
|
const uint64_t kNanosecondsPerSecond = 1000000000ull;
|
||||||
|
sample_group_description.audio_roll_recovery_entries[0].roll_distance =
|
||||||
|
-(audio_info->seek_preroll_ns() * audio.samplerate +
|
||||||
|
kNanosecondsPerSecond / 2) /
|
||||||
|
kNanosecondsPerSecond;
|
||||||
|
|
||||||
|
sample_table.sample_to_groups.resize(1);
|
||||||
|
SampleToGroup& sample_to_group = sample_table.sample_to_groups.back();
|
||||||
|
sample_to_group.grouping_type = FOURCC_roll;
|
||||||
|
|
||||||
|
sample_to_group.entries.resize(1);
|
||||||
|
SampleToGroupEntry& sample_to_group_entry = sample_to_group.entries.back();
|
||||||
|
// All samples are in track fragments.
|
||||||
|
sample_to_group_entry.sample_count = 0;
|
||||||
|
sample_to_group_entry.group_description_index =
|
||||||
|
SampleToGroupEntry::kTrackGroupDescriptionIndexBase + 1;
|
||||||
|
} else if (audio_info->seek_preroll_ns() != 0) {
|
||||||
|
LOG(WARNING) << "Unexpected seek preroll for codec " << audio_info->codec();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MP4Muxer::GetInitRangeStartAndEnd(uint32_t* start, uint32_t* end) {
|
bool MP4Muxer::GetInitRangeStartAndEnd(uint32_t* start, uint32_t* end) {
|
||||||
|
|
|
@ -170,7 +170,7 @@ Status Segmenter::Initialize(const std::vector<MediaStream*>& streams,
|
||||||
sidx_->reference_id = i + 1;
|
sidx_->reference_id = i + 1;
|
||||||
}
|
}
|
||||||
if (!encryption_key_source) {
|
if (!encryption_key_source) {
|
||||||
fragmenters_[i] = new Fragmenter(&moof_->tracks[i]);
|
fragmenters_[i] = new Fragmenter(streams[i]->info(), &moof_->tracks[i]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue