Fix writing MP4 chunk-by-chunk

This also involves finally implementing
Mp4Track::makeTrack(). Mp4Track::makeSampleTable()
which would enable modifying stbl atom is still not
fully implemented yet, though.
This commit is contained in:
Martchus 2017-05-28 21:01:16 +02:00
parent 527fcb694e
commit 4faef55906
4 changed files with 118 additions and 53 deletions

View File

@ -170,19 +170,18 @@ inline std::istream &AbstractTrack::inputStream()
} }
/*! /*!
* \brief Assigns an other input stream. * \brief Assigns another input stream.
* *
* The track will read from the \a stream to perform * The track will read from the \a stream to perform
* particular operations such as reading header information. * particular operations such as reading header information.
*/ */
inline void AbstractTrack::setInputStream(std::istream &stream) inline void AbstractTrack::setInputStream(std::istream &stream)
{ {
m_istream = &stream; m_reader.setStream(m_istream = &stream);
m_reader.setStream(m_istream);
} }
/*! /*!
* \brief Returns the associated input stream. * \brief Returns the associated output stream.
*/ */
inline std::ostream &AbstractTrack::outputStream() inline std::ostream &AbstractTrack::outputStream()
{ {
@ -190,15 +189,14 @@ inline std::ostream &AbstractTrack::outputStream()
} }
/*! /*!
* \brief Assigns an other output stream. * \brief Assigns another output stream.
* *
* The track will write to the \a stream to perform * The track will write to the \a stream to perform
* particular operations such as updating or making header information. * particular operations such as updating or making header information.
*/ */
inline void AbstractTrack::setOutputStream(std::ostream &stream) inline void AbstractTrack::setOutputStream(std::ostream &stream)
{ {
m_ostream = &stream; m_writer.setStream(m_ostream = &stream);
m_writer.setStream(m_ostream);
} }
/*! /*!
@ -335,6 +333,7 @@ inline void AbstractTrack::setId(uint64 id)
m_id = id; m_id = id;
} }
/*!
* \brief Returns the track name if known; otherwise returns an empty string. * \brief Returns the track name if known; otherwise returns an empty string.
*/ */
inline const std::string AbstractTrack::name() const inline const std::string AbstractTrack::name() const

View File

@ -282,8 +282,8 @@ startParsingSignature:
* *
* This method parses the tracks of the current file if not been parsed yet. * This method parses the tracks of the current file if not been parsed yet.
* After calling this method the methods trackCount(), tracks(), and * After calling this method the methods trackCount(), tracks(), and
* hasTracksOfType() will return the parsed * hasTracksOfType() will return the parsed information.
* information. *
* \throws Throws std::ios_base::failure when an IO error occurs. * \throws Throws std::ios_base::failure when an IO error occurs.
* \throws Throws Media::Failure or a derived exception when a parsing * \throws Throws Media::Failure or a derived exception when a parsing
* error occurs. * error occurs.
@ -777,13 +777,21 @@ const char *MediaFileInfo::mimeType() const
vector<AbstractTrack *> MediaFileInfo::tracks() const vector<AbstractTrack *> MediaFileInfo::tracks() const
{ {
vector<AbstractTrack *> res; vector<AbstractTrack *> res;
size_t trackCount = 0;
size_t containerTrackCount = 0;
if(m_singleTrack) {
trackCount = 1;
}
if(m_container) {
trackCount += (containerTrackCount = m_container->trackCount());
}
res.reserve(trackCount);
if(m_singleTrack) { if(m_singleTrack) {
res.push_back(m_singleTrack.get()); res.push_back(m_singleTrack.get());
} }
if(m_container) { for(size_t i = 0; i != containerTrackCount; ++i) {
for(size_t i = 0, count = m_container->trackCount(); i < count; ++i) { res.push_back(m_container->track(i));
res.push_back(m_container->track(i));
}
} }
return res; return res;
} }

View File

@ -579,6 +579,16 @@ calculatePadding:
progressiveDownloadInfoAtom->discardBuffer(); progressiveDownloadInfoAtom->discardBuffer();
} }
// set input/output streams of each track
for(auto &track : tracks()) {
// ensure the track reads from the original file
if(&track->inputStream() == &outputStream) {
track->setInputStream(backupStream);
}
// ensure the track writes to the output file
track->setOutputStream(outputStream);
}
// write movie atom / padding and media data // write movie atom / padding and media data
for(byte pass = 0; pass != 2; ++pass) { for(byte pass = 0; pass != 2; ++pass) {
if(newTagPos == (pass ? ElementPosition::AfterData : ElementPosition::BeforeData)) { if(newTagPos == (pass ? ElementPosition::AfterData : ElementPosition::BeforeData)) {
@ -701,11 +711,6 @@ calculatePadding:
throw OperationAbortedException(); throw OperationAbortedException();
} }
// ensure the track reads from the original file
if(&track->inputStream() == &outputStream) {
track->setInputStream(backupStream);
}
// emplace information // emplace information
trackInfos.emplace_back(&track->inputStream(), track->readChunkOffsets(), track->readChunkSizes()); trackInfos.emplace_back(&track->inputStream(), track->readChunkOffsets(), track->readChunkSizes());
@ -718,7 +723,7 @@ calculatePadding:
// increase total chunk count and size // increase total chunk count and size
totalChunkCount += track->chunkCount(); totalChunkCount += track->chunkCount();
totalMediaDataSize = accumulate(chunkSizesTable.cbegin(), chunkSizesTable.cend(), totalMediaDataSize); totalMediaDataSize += accumulate(chunkSizesTable.cbegin(), chunkSizesTable.cend(), totalMediaDataSize);
} }
// write media data chunk-by-chunk // write media data chunk-by-chunk
@ -758,7 +763,7 @@ calculatePadding:
} }
// incrase chunk index within track, update progress percentage // incrase chunk index within track, update progress percentage
if(++chunkIndexWithinTrack % 10) { if(!(++chunkIndexWithinTrack % 10)) {
updatePercentage(static_cast<double>(totalChunksCopied) / totalChunkCount); updatePercentage(static_cast<double>(totalChunksCopied) / totalChunkCount);
} }
@ -832,24 +837,24 @@ calculatePadding:
// check whether track count of new file equals track count of old file // check whether track count of new file equals track count of old file
if(trackCount != tracks().size()) { if(trackCount != tracks().size()) {
addNotification(NotificationType::Critical, addNotification(NotificationType::Critical,
"Unable to update chunk offsets (\"stco\"-atom): Number of tracks in the output file (" argsToString("Unable to update chunk offsets (\"stco\"-atom): Number of tracks in the output file (",
% numberToString(tracks().size()) tracks().size(),
% ") differs from the number of tracks in the original file (" ") differs from the number of tracks in the original file (",
% numberToString(trackCount) trackCount,
+ ").", context); ")."), context);
throw Failure(); throw Failure();
} }
// update chunk offset table // update chunk offset table
if(writeChunkByChunk) { if(writeChunkByChunk) {
updateStatus("Updating chunk offset table for each track ..."); updateStatus("Updating chunk offset table for each track ...");
for(size_t trackIndex = 0; trackIndex < trackCount; ++trackIndex) { for(size_t trackIndex = 0; trackIndex != trackCount; ++trackIndex) {
const auto &track = tracks()[trackIndex]; const auto &track = tracks()[trackIndex];
const auto &chunkOffsetTable = get<1>(trackInfos[trackIndex]); const auto &chunkOffsetTable = get<1>(trackInfos[trackIndex]);
if(track->chunkCount() == chunkOffsetTable.size()) { if(track->chunkCount() == chunkOffsetTable.size()) {
track->updateChunkOffsets(chunkOffsetTable); track->updateChunkOffsets(chunkOffsetTable);
} else { } else {
addNotification(NotificationType::Critical, "Unable to update chunk offsets of track " % numberToString(trackIndex + 1) + ": Number of chunks in the output file differs from the number of chunks in the orignal file.", context); addNotification(NotificationType::Critical, argsToString("Unable to update chunk offsets of track ", (trackIndex + 1), ": Number of chunks in the output file differs from the number of chunks in the orignal file."), context);
throw Failure(); throw Failure();
} }
} }

View File

@ -918,6 +918,55 @@ void Mp4Track::addInfo(const AvcConfiguration &avcConfig, AbstractTrack &track)
} }
/*!
* \brief Returns the number of bytes written when calling makeTrack().
* \todo Get rid of const_cast. This is currently required because GenericFileElement::childById() is
* not const.
*/
uint64 Mp4Track::requiredSize() const
{
// add size of
// ... trak header and tkhd total size
uint64 size = 8 + 100;
// ... tref atom (if one exists)
if(Mp4Atom *trefAtom = m_trakAtom->childById(Mp4AtomIds::TrackReference)) {
size += trefAtom->totalSize();
}
// ... edts atom (if one exists)
if(Mp4Atom *edtsAtom = m_trakAtom->childById(Mp4AtomIds::Edit)) {
size += edtsAtom->totalSize();
}
// ... mdia header + mdhd total size + hdlr total size + minf header
size += 8 + 44 + (33 + m_name.size()) + 8;
// ... minf childs
bool dinfAtomWritten = false;
if(m_minfAtom) {
if(Mp4Atom *vmhdAtom = m_minfAtom->childById(Mp4AtomIds::VideoMediaHeader)) {
size += vmhdAtom->totalSize();
}
if(Mp4Atom *smhdAtom = m_minfAtom->childById(Mp4AtomIds::SoundMediaHeader)) {
size += smhdAtom->totalSize();
}
if(Mp4Atom *hmhdAtom = m_minfAtom->childById(Mp4AtomIds::HintMediaHeader)) {
size += hmhdAtom->totalSize();
}
if(Mp4Atom *nmhdAtom = m_minfAtom->childById(Mp4AtomIds::NullMediaHeaderBox)) {
size += nmhdAtom->totalSize();
}
if(Mp4Atom *dinfAtom = m_minfAtom->childById(Mp4AtomIds::DataInformation)) {
size += dinfAtom->totalSize();
dinfAtomWritten = true;
}
if(Mp4Atom *stblAtom = m_minfAtom->childById(Mp4AtomIds::SampleTable)) {
size += stblAtom->totalSize();
}
}
if(!dinfAtomWritten) {
size += 36;
}
return size;
}
/*! /*!
* \brief Makes the track entry ("trak"-atom) for the track. The data is written to the assigned output stream * \brief Makes the track entry ("trak"-atom) for the track. The data is written to the assigned output stream
* at the current position. * at the current position.
@ -925,35 +974,24 @@ void Mp4Track::addInfo(const AvcConfiguration &avcConfig, AbstractTrack &track)
*/ */
void Mp4Track::makeTrack() void Mp4Track::makeTrack()
{ {
/*
// write header // write header
ostream::pos_type trakStartOffset = outputStream().tellp(); ostream::pos_type trakStartOffset = outputStream().tellp();
writer.writeUInt32(0); // write size later m_writer.writeUInt32BE(0); // write size later
writer.writeUInt32(Mp4AtomIds::Track); m_writer.writeUInt32BE(Mp4AtomIds::Track);
// write tkhd atom // write tkhd atom
makeTrackHeader(); makeTrackHeader();
// write tref atom (if one exists) // write tref atom (if one exists)
if(Mp4Atom *trefAtom = trakAtom().childById(Mp4AtomIds::TrackReference)) { if(Mp4Atom *trefAtom = trakAtom().childById(Mp4AtomIds::TrackReference)) {
trefAtom->copyEntireAtomToStream(outputStream()); trefAtom->copyEntirely(outputStream());
} }
// write edts atom (if one exists) // write edts atom (if one exists)
if(Mp4Atom *edtsAtom = trakAtom().childById(Mp4AtomIds::Edit)) { if(Mp4Atom *edtsAtom = trakAtom().childById(Mp4AtomIds::Edit)) {
edtsAtom->copyEntireAtomToStream(outputStream()); edtsAtom->copyEntirely(outputStream());
} }
// write mdia atom // write mdia atom
makeMedia(); makeMedia();
// write size (of trak atom) // write size (of trak atom)
Mp4Atom::seekBackAndWriteAtomSize(outputStream(), trakStartOffset, false); Mp4Atom::seekBackAndWriteAtomSize(outputStream(), trakStartOffset);
*/
trakAtom().copyEntirely(outputStream());
}
/*!
* \brief Returns the number of bytes written when calling makeTrack().
*/
uint64 Mp4Track::requiredSize() const
{
return m_trakAtom->totalSize();
} }
/*! /*!
@ -1013,7 +1051,8 @@ void Mp4Track::makeMedia()
writer().writeUInt32BE(0); // write size later writer().writeUInt32BE(0); // write size later
writer().writeUInt32BE(Mp4AtomIds::Media); writer().writeUInt32BE(Mp4AtomIds::Media);
// write mdhd atom // write mdhd atom
writer().writeUInt32BE(36); // size writer().writeUInt32BE(44); // size
writer().writeUInt32BE(Mp4AtomIds::MediaHeader);
writer().writeByte(1); // version writer().writeByte(1); // version
writer().writeUInt24BE(0); // flags writer().writeUInt24BE(0); // flags
writer().writeUInt64BE(static_cast<uint64>((m_creationTime - startDate).totalSeconds())); writer().writeUInt64BE(static_cast<uint64>((m_creationTime - startDate).totalSeconds()));
@ -1022,9 +1061,10 @@ void Mp4Track::makeMedia()
writer().writeUInt64BE(static_cast<uint64>(m_duration.totalSeconds() * m_timeScale)); writer().writeUInt64BE(static_cast<uint64>(m_duration.totalSeconds() * m_timeScale));
// convert and write language // convert and write language
uint16 language = 0; uint16 language = 0;
for(size_t charIndex = 0; charIndex < m_language.length() && charIndex < 3; ++charIndex) { for(size_t charIndex = 0; charIndex < m_language.size() && charIndex != 3; ++charIndex) {
if(m_language[charIndex] >= 'a' && m_language[charIndex] <= 'z') { const char langChar = m_language[charIndex];
language |= static_cast<uint16>(m_language[charIndex]) << (0xA - charIndex * 0x5); if(langChar >= 'a' && langChar <= 'z') {
language |= static_cast<uint16>(langChar - 0x60) << (0xA - charIndex * 0x5);
} else { // invalid character } else { // invalid character
addNotification(NotificationType::Warning, "Assigned language \"" % m_language + "\" is of an invalid format and will be ignored.", "making mdhd atom"); addNotification(NotificationType::Warning, "Assigned language \"" % m_language + "\" is of an invalid format and will be ignored.", "making mdhd atom");
language = 0x55C4; // und language = 0x55C4; // und
@ -1034,7 +1074,7 @@ void Mp4Track::makeMedia()
writer().writeUInt16BE(language); writer().writeUInt16BE(language);
writer().writeUInt16BE(0); // pre defined writer().writeUInt16BE(0); // pre defined
// write hdlr atom // write hdlr atom
writer().writeUInt32BE(33 + m_name.length()); // size writer().writeUInt32BE(33 + m_name.size()); // size
writer().writeUInt32BE(Mp4AtomIds::HandlerReference); writer().writeUInt32BE(Mp4AtomIds::HandlerReference);
writer().writeUInt64BE(0); // version, flags, pre defined writer().writeUInt64BE(0); // version, flags, pre defined
switch(m_mediaType) { switch(m_mediaType) {
@ -1112,7 +1152,18 @@ void Mp4Track::makeMediaInfo()
writer().writeUInt24BE(0x000001); // flags (media data is in the same file as the movie box) writer().writeUInt24BE(0x000001); // flags (media data is in the same file as the movie box)
} }
// write stbl atom // write stbl atom
makeSampleTable(); // -> just copy existing stbl atom because makeSampleTable() is not fully implemented (yet)
bool stblAtomWritten = false;
if(m_minfAtom) {
if(Mp4Atom *stblAtom = m_minfAtom->childById(Mp4AtomIds::SampleTable)) {
stblAtom->copyEntirely(outputStream());
stblAtomWritten = true;
}
}
if(!stblAtomWritten) {
addNotification(NotificationType::Critical, "Unable to make stbl atom from scratch.", "making stbl atom");
throw NotImplementedException();
}
// write size (of minf atom) // write size (of minf atom)
Mp4Atom::seekBackAndWriteAtomSize(outputStream(), minfStartOffset); Mp4Atom::seekBackAndWriteAtomSize(outputStream(), minfStartOffset);
} }
@ -1151,6 +1202,7 @@ void Mp4Track::makeSampleTable()
cttsAtom->copyEntirely(outputStream()); cttsAtom->copyEntirely(outputStream());
} }
// write stsc atom (sample-to-chunk table) // write stsc atom (sample-to-chunk table)
throw NotImplementedException();
// write stsz atom (sample sizes) // write stsz atom (sample sizes)
@ -1287,10 +1339,11 @@ void Mp4Track::internalParseHeader()
} }
uint16 tmp = reader.readUInt16BE(); uint16 tmp = reader.readUInt16BE();
if(tmp) { if(tmp) {
char buff[3]; const char buff[] = {
buff[0] = ((tmp & 0x7C00) >> 0xA) + 0x60; static_cast<char>(((tmp & 0x7C00) >> 0xA) + 0x60),
buff[1] = ((tmp & 0x03E0) >> 0x5) + 0x60; static_cast<char>(((tmp & 0x03E0) >> 0x5) + 0x60),
buff[2] = ((tmp & 0x001F) >> 0x0) + 0x60; static_cast<char>(((tmp & 0x001F) >> 0x0) + 0x60),
};
m_language = string(buff, 3); m_language = string(buff, 3);
} else { } else {
m_language.clear(); m_language.clear();