allow precalculation of MP4 tag size

This commit is contained in:
Martchus 2015-12-10 13:50:46 +01:00
parent d9c197dcf2
commit 0c82c4c1fd
5 changed files with 314 additions and 159 deletions

View File

@ -157,7 +157,8 @@ void MatroskaTagField::make(ostream &stream)
/*!
* \class Media::MatroskaTagFieldMaker
* \brief The MatroskaTagFieldMaker class helps making tag fields. It allows to calculate the
* \brief The MatroskaTagFieldMaker class helps making tag fields.
* It allows to calculate the required size.
* \sa See MatroskaTagField::prepareMaking() for more information.
*/

View File

@ -228,6 +228,21 @@ void Mp4Tag::parse(Mp4Atom &metaAtom)
}
}
/*!
* \brief Prepares making.
* \returns Returns a Mp4TagMaker object which can be used to actually make the tag.
* \remarks The tag must NOT be mutated after making is prepared when it is intended to actually
* make the tag using the make method of the returned object.
* \throws Throws Media::Failure or a derived exception when a making error occurs.
*
* This method might be useful when it is necessary to know the size of the tag before making it.
* \sa make()
*/
Mp4TagMaker Mp4Tag::prepareMaking()
{
return Mp4TagMaker(*this);
}
/*!
* \brief Writes tag information to the specified \a stream.
*
@ -237,14 +252,49 @@ void Mp4Tag::parse(Mp4Atom &metaAtom)
*/
void Mp4Tag::make(ostream &stream)
{
invalidateStatus();
static const string context("making MP4 tag");
// write meta atom
ostream::pos_type metaOff = stream.tellp();
static const byte metaData[8] = {
0x00, 0x00, 0x00, 0x00, 0x6D, 0x65, 0x74, 0x61
};
stream.write(reinterpret_cast<const char *>(metaData), sizeof(metaData));
prepareMaking().make(stream);
}
/*!
* \brief Prepares making the specified \a tag.
* \sa See Mp4Tag::prepareMaking() for more information.
*/
Mp4TagMaker::Mp4TagMaker(Mp4Tag &tag) :
m_tag(tag),
// meta head, hdlr atom
m_metaSize(8 + 37),
// ilst head
m_ilstSize(8),
// ensure there only one genre atom is written (prefer genre as string)
m_omitPreDefinedGenre(m_tag.fields().count(Mp4TagAtomIds::PreDefinedGenre) && m_tag.fields().count(Mp4TagAtomIds::Genre))
{
m_tag.invalidateStatus();
m_makers.reserve(m_tag.fields().size());
for(auto &field : m_tag.fields()) {
if(!field.second.value().isEmpty() &&
(!m_omitPreDefinedGenre || field.first == Mp4TagAtomIds::PreDefinedGenre)) {
m_makers.emplace_back(field.second.prepareMaking());
m_ilstSize += m_makers.back().requiredSize();
}
}
if(m_ilstSize != 8) {
m_metaSize += m_ilstSize;
}
}
/*!
* \brief Saves the tag (specified when constructing the object) to the
* specified \a stream.
* \throws Throws std::ios_base::failure when an IO error occurs.
* \throws Throws Assumes the data is already validated and thus does NOT
* throw Media::Failure or a derived exception.
*/
void Mp4TagMaker::make(ostream &stream)
{
// write meta head
BinaryWriter writer(&stream);
writer.writeUInt32BE(m_metaSize);
writer.writeUInt32BE(Mp4AtomIds::Meta);
// write hdlr atom
static const byte hdlrData[37] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x68, 0x64, 0x6C, 0x72, 0x00, 0x00,
@ -252,37 +302,18 @@ void Mp4Tag::make(ostream &stream)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
stream.write(reinterpret_cast<const char *>(hdlrData), sizeof(hdlrData));
// write ilst atom
ostream::pos_type ilstOff = stream.tellp();
static const byte ilstData[8] = {
0x00, 0x00, 0x00, 0x00, 0x69, 0x6C, 0x73, 0x74
};
stream.write(reinterpret_cast<const char *>(ilstData), sizeof(ilstData));
// ensure there is only one genre atom (prefer genre as string)
if(fields().count(Mp4TagAtomIds::PreDefinedGenre)
&& fields().count(Mp4TagAtomIds::Genre)) {
fields().erase(Mp4TagAtomIds::PreDefinedGenre);
}
// write actual tag data
int tagFieldsWritten = 0;
for(auto i = fields().begin(), end = fields().end(); i != end; ++i) {
Mp4TagField &field = i->second;
if(!field.value().isEmpty()) {
field.invalidateNotifications();
try {
field.make(stream);
++tagFieldsWritten;
} catch(Failure &) {
// nothing to do since notifications will be added anyways
}
addNotifications(context, field);
if(m_ilstSize != 8) {
// write ilst head
writer.writeUInt32BE(m_ilstSize);
writer.writeUInt32BE(Mp4AtomIds::ItunesList);
// write fields
for(auto &maker : m_makers) {
maker.make(stream);
}
} else {
// no fields to be written -> no ilst to be written
m_tag.addNotification(NotificationType::Warning, "Tag is empty.", "making MP4 tag");
}
if(!tagFieldsWritten) {
addNotification(NotificationType::Warning, "No tag atoms have be written.", context);
}
Mp4Atom::seekBackAndWriteAtomSize(stream, ilstOff);
Mp4Atom::seekBackAndWriteAtomSize(stream, metaOff);
}
}

View File

@ -9,6 +9,42 @@ namespace Media
{
class Mp4Atom;
class Mp4Tag;
class LIB_EXPORT Mp4TagMaker
{
friend class Mp4Tag;
public:
void make(std::ostream &stream);
const Mp4Tag &tag() const;
uint64 requiredSize() const;
private:
Mp4TagMaker(Mp4Tag &tag);
Mp4Tag &m_tag;
std::vector<Mp4TagFieldMaker> m_makers;
uint64 m_metaSize;
uint64 m_ilstSize;
bool m_omitPreDefinedGenre;
};
/*!
* \brief Returns the associated tag.
*/
inline const Mp4Tag &Mp4TagMaker::tag() const
{
return m_tag;
}
/*!
* \brief Returns the number of bytes which will be written when making the tag.
*/
inline uint64 Mp4TagMaker::requiredSize() const
{
return m_metaSize;
}
class LIB_EXPORT Mp4Tag : public FieldMapBasedTag<Mp4TagField>
{
@ -33,6 +69,7 @@ public:
bool hasField(KnownField value) const;
void parse(Mp4Atom &metaAtom);
Mp4TagMaker prepareMaking();
void make(std::ostream &stream);
};

View File

@ -254,6 +254,21 @@ void Mp4TagField::reparse(Mp4Atom &ilstChild)
}
}
/*!
* \brief Prepares making.
* \returns Returns a Mp4TagFieldMaker object which can be used to actually make the field.
* \remarks The field must NOT be mutated after making is prepared when it is intended to actually
* make the field using the make method of the returned object.
* \throws Throws Media::Failure or a derived exception when a making
* error occurs.
*
* This method might be useful when it is necessary to know the size of the field before making it.
*/
Mp4TagFieldMaker Mp4TagField::prepareMaking()
{
return Mp4TagFieldMaker(*this);
}
/*!
* \brief Saves the field to the specified \a stream.
*
@ -263,127 +278,7 @@ void Mp4TagField::reparse(Mp4Atom &ilstChild)
*/
void Mp4TagField::make(ostream &stream)
{
invalidateStatus();
if(!id()) {
addNotification(NotificationType::Warning, "Invalid tag atom id.", "making MP4 tag field");
throw InvalidDataException();
}
const string context("making MP4 tag field " + ConversionUtilities::interpretIntegerAsString<identifierType>(id()));
// there might be only mean and name info, but no data
if(value().isEmpty() && (!m_mean.empty() || !m_name.empty())) {
addNotification(NotificationType::Critical, "No tag value assigned.", context);
throw InvalidDataException();
}
uint32 rawDataType = 0;
try {
// try to use appropriate raw data type
rawDataType = appropriateRawDataType();
} catch(Failure &) {
// unable to obtain appropriate raw data type
// assume utf-8 text
rawDataType = RawDataType::Utf8;
addNotification(NotificationType::Warning, "It was not possible to find an appropriate raw data type id. UTF-8 will be assumed.", context);
}
stringstream convertedData(stringstream::in | stringstream::out | stringstream::binary);
BinaryWriter writer(&convertedData);
try {
if(!value().isEmpty()) { // there might be only mean and name info, but no data
convertedData.exceptions(std::stringstream::failbit | std::stringstream::badbit);
switch(rawDataType) {
case RawDataType::Utf8:
case RawDataType::Utf16:
writer.writeString(value().toString());
break;
case RawDataType::BeSignedInt: {
int number = value().toInteger();
if(number <= numeric_limits<int16>::max()
&& number >= numeric_limits<int16>::min()) {
writer.writeInt16BE(static_cast<int16>(number));
} else {
writer.writeInt32BE(number);
}
break;
} case RawDataType::BeUnsignedInt: {
int number = value().toInteger();
if(number <= numeric_limits<uint16>::max()
&& number >= numeric_limits<uint16>::min()) {
writer.writeUInt16BE(static_cast<uint16>(number));
} else if(number > 0) {
writer.writeUInt32BE(number);
} else {
throw ConversionException("Negative integer can not be assigned to the field with the id \"" + interpretIntegerAsString<uint32>(id()) + "\".");
}
break;
} case RawDataType::Bmp: case RawDataType::Jpeg: case RawDataType::Png:
break; // leave converted data empty to write original data later
default:
switch(id()) {
// track number and disk number are exceptions
// raw data type 0 is used, information is stored as pair of unsigned integers
case Mp4TagAtomIds::TrackPosition: case Mp4TagAtomIds::DiskPosition: {
PositionInSet pos = value().toPositionIntSet();
writer.writeInt32BE(pos.position());
if(pos.total() <= numeric_limits<int16>::max()) {
writer.writeInt16BE(static_cast<int16>(pos.total()));
} else {
throw ConversionException("Integer can not be assigned to the field with the id \"" + interpretIntegerAsString<uint32>(id()) + "\" because it is to big.");
}
writer.writeUInt16BE(0);
break;
}
case Mp4TagAtomIds::PreDefinedGenre:
writer.writeUInt16BE(value().toStandardGenreIndex());
break;
default:
; // leave converted data empty to write original data later
}
}
}
} catch (ConversionException &ex) {
// it was not possible to perform required conversions
if(char_traits<char>::length(ex.what())) {
addNotification(NotificationType::Critical, ex.what(), context);
} else {
addNotification(NotificationType::Critical, "The assigned tag value can not be converted to be written appropriately.", context);
}
throw InvalidDataException();
}
// data could be converted successfully
// write data to output stream
writer.setStream(&stream);
uint32 dataSize = value().isEmpty() // calculate data size
? 0 : (convertedData.tellp() ? static_cast<size_t>(convertedData.tellp()) : value().dataSize());
uint32 entireSize = 8 // calculate entire size
+ (name().empty() ? 0 : (12 + name().length()))
+ (mean().empty() ? 0 : (12 + mean().length()))
+ (dataSize ? (16 + dataSize) : 0);
writer.writeUInt32BE(entireSize); // size of entire tag atom
writer.writeUInt32BE(id()); // id of tag atom
if(!mean().empty()) { // write "mean"
writer.writeUInt32BE(12 + mean().length());
writer.writeUInt32BE(Mp4AtomIds::Mean);
writer.writeUInt32BE(0);
writer.writeString(mean());
}
if(!name().empty()) { // write "name"
writer.writeUInt32BE(12 + name().length());
writer.writeUInt32BE(Mp4AtomIds::Name);
writer.writeUInt32BE(0);
writer.writeString(name());
}
if(!value().isEmpty()) { // write data
writer.writeUInt32BE(16 + dataSize); // size of data atom
writer.writeUInt32BE(Mp4AtomIds::Data); // id of data atom
writer.writeByte(0); // version
writer.writeUInt24BE(rawDataType);
writer.writeUInt16BE(m_countryIndicator);
writer.writeUInt16BE(m_langIndicator);
if(convertedData.tellp()) {
stream << convertedData.rdbuf(); // write converted data
} else { // no conversion was needed, write data directly from tag value
stream.write(value().dataPointer(), value().dataSize());
}
}
prepareMaking().make(stream);
}
/*!
@ -487,4 +382,156 @@ void Mp4TagField::cleared()
m_langIndicator = 0;
}
/*!
* \class Media::Mp4TagFieldMaker
* \brief The Mp4TagFieldMaker class helps making tag fields.
* It allows to calculate the required size.
* \sa See Mp4TagFieldMaker::prepareMaking() for more information.
*/
/*!
* \brief Prepares making the specified \a field.
* \sa See Mp4TagFieldMaker::prepareMaking() for more information.
*/
Mp4TagFieldMaker::Mp4TagFieldMaker(Mp4TagField &field) :
m_field(field),
m_convertedData(stringstream::in | stringstream::out | stringstream::binary),
m_writer(&m_convertedData),
m_rawDataType(0)
{
m_field.invalidateStatus();
if(!m_field.id()) {
m_field.addNotification(NotificationType::Warning, "Invalid tag atom id.", "making MP4 tag field");
throw InvalidDataException();
}
const string context("making MP4 tag field " + ConversionUtilities::interpretIntegerAsString<Mp4TagField::identifierType>(m_field.id()));
if(m_field.value().isEmpty() && (!m_field.mean().empty() || !m_field.name().empty())) {
m_field.addNotification(NotificationType::Critical, "No tag value assigned.", context);
throw InvalidDataException();
}
try {
// try to use appropriate raw data type
m_rawDataType = m_field.appropriateRawDataType();
} catch(Failure &) {
// unable to obtain appropriate raw data type
// assume utf-8 text
m_rawDataType = RawDataType::Utf8;
m_field.addNotification(NotificationType::Warning, "It was not possible to find an appropriate raw data type id. UTF-8 will be assumed.", context);
}
try {
if(!m_field.value().isEmpty()) { // there might be only mean and name info, but no data
m_convertedData.exceptions(std::stringstream::failbit | std::stringstream::badbit);
switch(m_rawDataType) {
case RawDataType::Utf8:
case RawDataType::Utf16:
m_writer.writeString(m_field.value().toString());
break;
case RawDataType::BeSignedInt: {
int number = m_field.value().toInteger();
if(number <= numeric_limits<int16>::max()
&& number >= numeric_limits<int16>::min()) {
m_writer.writeInt16BE(static_cast<int16>(number));
} else {
m_writer.writeInt32BE(number);
}
break;
} case RawDataType::BeUnsignedInt: {
int number = m_field.value().toInteger();
if(number <= numeric_limits<uint16>::max()
&& number >= numeric_limits<uint16>::min()) {
m_writer.writeUInt16BE(static_cast<uint16>(number));
} else if(number > 0) {
m_writer.writeUInt32BE(number);
} else {
throw ConversionException("Negative integer can not be assigned to the field with the id \"" + interpretIntegerAsString<uint32>(m_field.id()) + "\".");
}
break;
} case RawDataType::Bmp: case RawDataType::Jpeg: case RawDataType::Png:
break; // leave converted data empty to write original data later
default:
switch(m_field.id()) {
// track number and disk number are exceptions
// raw data type 0 is used, information is stored as pair of unsigned integers
case Mp4TagAtomIds::TrackPosition: case Mp4TagAtomIds::DiskPosition: {
PositionInSet pos = m_field.value().toPositionIntSet();
m_writer.writeInt32BE(pos.position());
if(pos.total() <= numeric_limits<int16>::max()) {
m_writer.writeInt16BE(static_cast<int16>(pos.total()));
} else {
throw ConversionException("Integer can not be assigned to the field with the id \"" + interpretIntegerAsString<uint32>(m_field.id()) + "\" because it is to big.");
}
m_writer.writeUInt16BE(0);
break;
}
case Mp4TagAtomIds::PreDefinedGenre:
m_writer.writeUInt16BE(m_field.value().toStandardGenreIndex());
break;
default:
; // leave converted data empty to write original data later
}
}
}
} catch (ConversionException &ex) {
// it was not possible to perform required conversions
if(char_traits<char>::length(ex.what())) {
m_field.addNotification(NotificationType::Critical, ex.what(), context);
} else {
m_field.addNotification(NotificationType::Critical, "The assigned tag value can not be converted to be written appropriately.", context);
}
throw InvalidDataException();
}
// calculate data size
m_dataSize = m_field.value().isEmpty()
? 0 : (m_convertedData.tellp() ? static_cast<size_t>(m_convertedData.tellp()) : m_field.value().dataSize());
m_totalSize = 8 // calculate entire size
+ (m_field.name().empty() ? 0 : (12 + m_field.name().length()))
+ (m_field.mean().empty() ? 0 : (12 + m_field.mean().length()))
+ (m_dataSize ? (16 + m_dataSize) : 0);
}
/*!
* \brief Saves the field (specified when constructing the object) to the
* specified \a stream. *
* \throws Throws std::ios_base::failure when an IO error occurs.
* \throws Throws Assumes the data is already validated and thus does NOT
* throw Media::Failure or a derived exception.
*/
void Mp4TagFieldMaker::make(ostream &stream)
{
m_writer.setStream(&stream);
// size of entire tag atom
m_writer.writeUInt32BE(m_totalSize);
// id of tag atom
m_writer.writeUInt32BE(m_field.id());
if(!m_field.mean().empty()) {
// write "mean"
m_writer.writeUInt32BE(12 + m_field.mean().size());
m_writer.writeUInt32BE(Mp4AtomIds::Mean);
m_writer.writeUInt32BE(0);
m_writer.writeString(m_field.mean());
}
if(!m_field.name().empty()) {
// write "name"
m_writer.writeUInt32BE(12 + m_field.name().length());
m_writer.writeUInt32BE(Mp4AtomIds::Name);
m_writer.writeUInt32BE(0);
m_writer.writeString(m_field.name());
}
if(!m_field.value().isEmpty()) { // write data
m_writer.writeUInt32BE(16 + m_dataSize); // size of data atom
m_writer.writeUInt32BE(Mp4AtomIds::Data); // id of data atom
m_writer.writeByte(0); // version
m_writer.writeUInt24BE(m_rawDataType);
m_writer.writeUInt16BE(m_field.countryIndicator());
m_writer.writeUInt16BE(m_field.languageIndicator());
if(m_convertedData.tellp()) {
// write converted data
stream << m_convertedData.rdbuf();
} else {
// no conversion was needed, write data directly from tag value
stream.write(m_field.value().dataPointer(), m_field.value().dataSize());
}
}
}
}

View File

@ -4,6 +4,8 @@
#include "../generictagfield.h"
#include "../statusprovider.h"
#include <c++utilities/io/binarywriter.h>
#include <vector>
namespace Media
@ -71,6 +73,42 @@ public:
class Mp4Atom;
class LIB_EXPORT Mp4TagFieldMaker
{
friend class Mp4TagField;
public:
void make(std::ostream &stream);
const Mp4TagField &field() const;
uint64 requiredSize() const;
private:
Mp4TagFieldMaker(Mp4TagField &field);
Mp4TagField &m_field;
std::stringstream m_convertedData;
IoUtilities::BinaryWriter m_writer;
uint32 m_rawDataType;
uint64 m_dataSize;
uint64 m_totalSize;
};
/*!
* \brief Returns the associated field.
*/
inline const Mp4TagField &Mp4TagFieldMaker::field() const
{
return m_field;
}
/*!
* \brief Returns number of bytes which will be written when making the field.
*/
inline uint64 Mp4TagFieldMaker::requiredSize() const
{
return m_totalSize;
}
class LIB_EXPORT Mp4TagField : public TagField<Mp4TagField>, public StatusProvider
{
friend class TagField<Mp4TagField>;
@ -81,6 +119,7 @@ public:
Mp4TagField(const std::string &mean, const std::string &name, const TagValue &value);
void reparse(Mp4Atom &ilstChild);
Mp4TagFieldMaker prepareMaking();
void make(std::ostream &stream);
bool isAdditionalTypeInfoUsed() const;