Improve handling time values of MP4 files

* Cache result of `verifyPresentTrackHeader()` instead of running the code
  unnecassarily multiple times
* Do not tamper with existing/raw values by default to avoid
  inconsistencies through rounding errors and possibly fix
  https://github.com/Martchus/tageditor/issues/80
* Avoid conversions to double (depending on the time scale rounding errors
  might still occur)
This commit is contained in:
Martchus 2022-02-15 23:22:48 +01:00
parent d19a09db5b
commit 503fb725a2
6 changed files with 109 additions and 54 deletions

View File

@ -8,9 +8,9 @@ set(META_APP_NAME "Tag Parser")
set(META_APP_AUTHOR "Martchus")
set(META_APP_URL "https://github.com/${META_APP_AUTHOR}/${META_PROJECT_NAME}")
set(META_APP_DESCRIPTION "C++ library for reading and writing MP4 (iTunes), ID3, Vorbis, Opus, FLAC and Matroska tags")
set(META_VERSION_MAJOR 10)
set(META_VERSION_MINOR 4)
set(META_VERSION_PATCH 1)
set(META_VERSION_MAJOR 11)
set(META_VERSION_MINOR 0)
set(META_VERSION_PATCH 0)
set(META_REQUIRED_CPP_UNIT_VERSION 1.14.0)
set(META_ADD_DEFAULT_CPP_UNIT_TEST_APPLICATION ON)
@ -184,7 +184,7 @@ set(RES_FILES "${LANGUAGE_HEADER_ISO_639_2}")
set(CONFIGURATION_PACKAGE_SUFFIX
""
CACHE STRING "sets the suffix for find_package() calls to packages configured via c++utilities")
find_package(c++utilities${CONFIGURATION_PACKAGE_SUFFIX} 5.10.0 REQUIRED)
find_package(c++utilities${CONFIGURATION_PACKAGE_SUFFIX} 5.13.0 REQUIRED)
use_cpp_utilities(VISIBILITY PUBLIC)
# link against a possibly required extra library for std::filesystem

View File

@ -91,7 +91,7 @@ MediaFileInfo::MediaFileInfo(std::string &&path)
, m_tagPosition(ElementPosition::BeforeData)
, m_indexPosition(ElementPosition::BeforeData)
, m_fileHandlingFlags(MediaFileHandlingFlags::ForceRewrite | MediaFileHandlingFlags::ForceTagPosition | MediaFileHandlingFlags::ForceIndexPosition
| MediaFileHandlingFlags::NormalizeKnownTagFieldIds)
| MediaFileHandlingFlags::NormalizeKnownTagFieldIds | MediaFileHandlingFlags::PreserveRawTimingValues)
{
}

View File

@ -63,6 +63,7 @@ enum class MediaFileHandlingFlags : std::uint64_t {
ForceTagPosition = (1 << 2), /**< enforces the tag position when applying changes, see remarks of MediaFileInfo::setTagPosition() */
ForceIndexPosition = (1 << 3), /**< enforces the index position when applying changes, see remarks of MediaFileInfo::setIndexPosition() */
NormalizeKnownTagFieldIds = (1 << 4), /**< normalizes known tag field IDs when parsing to match the tag specification's recommendations */
PreserveRawTimingValues = (1 << 8), /**< preverves raw timing values (so far only used when making MP4 tracks) */
};
} // namespace TagParser

View File

@ -1,3 +1,5 @@
#define CHRONO_UTILITIES_TIMESPAN_INTEGER_SCALE_OVERLOADS
#include "./mp4container.h"
#include "./mp4ids.h"
@ -130,16 +132,22 @@ void Mp4Container::internalParseTracks(Diagnostics &diag, AbortableProgressFeedb
stream().seekg(3, ios_base::cur); // skip flags
switch (version) {
case 0:
m_creationTime = DateTime::fromDate(1904, 1, 1) + TimeSpan::fromSeconds(reader().readUInt32BE());
m_modificationTime = DateTime::fromDate(1904, 1, 1) + TimeSpan::fromSeconds(reader().readUInt32BE());
m_creationTime
= DateTime::fromDate(1904, 1, 1) + TimeSpan::fromSeconds(static_cast<TimeSpan::TickType>(reader().readUInt32BE()));
m_modificationTime
= DateTime::fromDate(1904, 1, 1) + TimeSpan::fromSeconds(static_cast<TimeSpan::TickType>(reader().readUInt32BE()));
m_timeScale = reader().readUInt32BE();
m_duration = TimeSpan::fromSeconds(static_cast<double>(reader().readUInt32BE()) / static_cast<double>(m_timeScale));
m_duration = TimeSpan::fromSeconds(static_cast<TimeSpan::TickType>(reader().readUInt32BE()))
/ static_cast<TimeSpan::TickType>(m_timeScale);
break;
case 1:
m_creationTime = DateTime::fromDate(1904, 1, 1) + TimeSpan::fromSeconds(static_cast<double>(reader().readUInt64BE()));
m_modificationTime = DateTime::fromDate(1904, 1, 1) + TimeSpan::fromSeconds(static_cast<double>(reader().readUInt64BE()));
m_creationTime
= DateTime::fromDate(1904, 1, 1) + TimeSpan::fromSeconds(static_cast<TimeSpan::TickType>(reader().readUInt64BE()));
m_modificationTime
= DateTime::fromDate(1904, 1, 1) + TimeSpan::fromSeconds(static_cast<TimeSpan::TickType>(reader().readUInt64BE()));
m_timeScale = reader().readUInt32BE();
m_duration = TimeSpan::fromSeconds(static_cast<double>(reader().readUInt64BE()) / static_cast<double>(m_timeScale));
m_duration = TimeSpan::fromSeconds(static_cast<TimeSpan::TickType>(reader().readUInt64BE()))
/ static_cast<TimeSpan::TickType>(m_timeScale);
break;
default:;
}

View File

@ -1,3 +1,5 @@
#define CHRONO_UTILITIES_TIMESPAN_INTEGER_SCALE_OVERLOADS
#include "./mp4track.h"
#include "./mp4atom.h"
#include "./mp4container.h"
@ -12,6 +14,7 @@
#include "../mpegaudio/mpegaudioframestream.h"
#include "../exceptions.h"
#include "../mediafileinfo.h"
#include "../mediaformat.h"
#include <c++utilities/conversion/stringbuilder.h>
@ -31,10 +34,11 @@ namespace TagParser {
* \brief The Mp4Timings struct holds timing values found in multiple MP4 atoms.
*/
struct Mp4Timings {
std::uint64_t creationTime = 0;
std::uint64_t modificationTime = 0;
std::uint64_t duration = 0;
constexpr std::uint8_t requiredVersion() const;
std::uint64_t tkhdCreationTime, mdhdCreationTime = 0;
std::uint64_t tkhdModificationTime, mdhdModificationTime = 0;
std::uint64_t tkhdDuration, mdhdDuration = 0;
constexpr std::uint8_t requiredTkhdVersion() const;
constexpr std::uint8_t requiredMdhdVersion() const;
};
/*!
@ -69,12 +73,20 @@ private:
bool discardBuffer = false;
};
constexpr std::uint8_t Mp4Timings::requiredVersion() const
constexpr std::uint8_t Mp4Timings::requiredTkhdVersion() const
{
return (creationTime > std::numeric_limits<std::uint32_t>::max() || modificationTime > std::numeric_limits<std::uint32_t>::max()
|| duration > std::numeric_limits<std::uint32_t>::max())
? 1
: 0;
return (tkhdCreationTime > std::numeric_limits<std::uint32_t>::max() || tkhdModificationTime > std::numeric_limits<std::uint32_t>::max()
|| tkhdDuration > std::numeric_limits<std::uint32_t>::max())
? 1
: 0;
}
constexpr std::uint8_t Mp4Timings::requiredMdhdVersion() const
{
return (mdhdCreationTime > std::numeric_limits<std::uint32_t>::max() || mdhdModificationTime > std::numeric_limits<std::uint32_t>::max()
|| mdhdDuration > std::numeric_limits<std::uint32_t>::max())
? 1
: 0;
}
/// \brief Dates within MP4 tracks are expressed as the number of seconds since this date.
@ -156,6 +168,12 @@ Mp4Track::Mp4Track(Mp4Atom &trakAtom)
, m_chunkOffsetSize(4)
, m_chunkCount(0)
, m_sampleToChunkEntryCount(0)
, m_rawTkhdCreationTime(0)
, m_rawMdhdCreationTime(0)
, m_rawTkhdModificationTime(0)
, m_rawMdhdModificationTime(0)
, m_rawTkhdDuration(0)
, m_rawMdhdDuration(0)
{
}
@ -405,11 +423,14 @@ void Mp4Track::addChunkSizeEntries(
* \brief Verifies the present track header (tkhd atom) and returns relevant information for making a new track header
* based on it.
*/
TrackHeaderInfo Mp4Track::verifyPresentTrackHeader() const
const TrackHeaderInfo &Mp4Track::verifyPresentTrackHeader() const
{
TrackHeaderInfo info;
if (m_trackHeaderInfo) {
return *m_trackHeaderInfo;
}
// return the default TrackHeaderInfo in case there is no track header prsent
auto &info = *(m_trackHeaderInfo = std::make_unique<TrackHeaderInfo>());
if (!m_tkhdAtom) {
return info;
}
@ -447,7 +468,7 @@ TrackHeaderInfo Mp4Track::verifyPresentTrackHeader() const
// determine required size
info.requiredSize = m_tkhdAtom->dataSize() + 8;
info.timings = computeTimings();
info.timingsVersion = info.timings.requiredVersion();
info.timingsVersion = info.timings.requiredTkhdVersion();
if (info.version == 0) {
info.writeVersion = info.timingsVersion;
// add 12 byte to size if update from version 0 to version 1 is required (which needs 12 byte more)
@ -470,9 +491,18 @@ TrackHeaderInfo Mp4Track::verifyPresentTrackHeader() const
Mp4Timings Mp4Track::computeTimings() const
{
auto timings = Mp4Timings();
timings.creationTime = static_cast<std::uint64_t>((m_creationTime - startDate).totalSeconds());
timings.modificationTime = static_cast<std::uint64_t>((m_modificationTime - startDate).totalSeconds());
timings.duration = static_cast<std::uint64_t>(m_duration.totalTicks() * m_timeScale / TimeSpan::ticksPerSecond);
if (m_trakAtom && (m_trakAtom->container().fileInfo().fileHandlingFlags() & MediaFileHandlingFlags::PreserveRawTimingValues)) {
timings.tkhdCreationTime = m_rawTkhdCreationTime;
timings.tkhdModificationTime = m_rawTkhdModificationTime;
timings.tkhdDuration = m_rawTkhdDuration;
timings.mdhdCreationTime = m_rawMdhdCreationTime;
timings.mdhdModificationTime = m_rawMdhdModificationTime;
timings.mdhdDuration = m_rawMdhdDuration;
} else {
timings.tkhdCreationTime = timings.mdhdCreationTime = static_cast<std::uint64_t>((m_creationTime - startDate).totalSeconds());
timings.tkhdModificationTime = timings.mdhdModificationTime = static_cast<std::uint64_t>((m_modificationTime - startDate).totalSeconds());
timings.tkhdDuration = timings.mdhdDuration = static_cast<std::uint64_t>(m_duration.totalTicks() * m_timeScale / TimeSpan::ticksPerSecond);
}
return timings;
}
@ -1115,11 +1145,11 @@ std::uint64_t Mp4Track::requiredSize(Diagnostics &diag) const
{
CPP_UTILITIES_UNUSED(diag)
const auto info = verifyPresentTrackHeader();
const auto &info = verifyPresentTrackHeader();
// add size of
// ... trak header
std::uint64_t size = 8;
// ... tkhd atom (TODO: buffer TrackHeaderInfo in next major release)
// ... tkhd atom
size += info.requiredSize;
// ... children beside tkhd and mdia
for (Mp4Atom *trakChild = m_trakAtom->firstChild(); trakChild; trakChild = trakChild->nextSibling()) {
@ -1195,7 +1225,7 @@ void Mp4Track::makeTrack(Diagnostics &diag)
void Mp4Track::makeTrackHeader(Diagnostics &diag)
{
// verify the existing track header to make the new one based on it (if possible)
const TrackHeaderInfo info(verifyPresentTrackHeader());
const auto &info = verifyPresentTrackHeader();
// add notifications in case the present track header could not be parsed
if (info.versionUnknown) {
@ -1234,20 +1264,20 @@ void Mp4Track::makeTrackHeader(Diagnostics &diag)
// make creation and modification time
if (info.writeVersion != 0) {
writer().writeUInt64BE(info.timings.creationTime);
writer().writeUInt64BE(info.timings.modificationTime);
writer().writeUInt64BE(info.timings.tkhdCreationTime);
writer().writeUInt64BE(info.timings.tkhdModificationTime);
} else {
writer().writeUInt32BE(static_cast<std::uint32_t>(info.timings.creationTime));
writer().writeUInt32BE(static_cast<std::uint32_t>(info.timings.modificationTime));
writer().writeUInt32BE(static_cast<std::uint32_t>(info.timings.tkhdCreationTime));
writer().writeUInt32BE(static_cast<std::uint32_t>(info.timings.tkhdModificationTime));
}
// make track ID and duration
writer().writeUInt32BE(static_cast<std::uint32_t>(m_id));
writer().writeUInt32BE(0); // reserved
if (info.writeVersion != 0) {
writer().writeUInt64BE(info.timings.duration);
writer().writeUInt64BE(info.timings.tkhdDuration);
} else {
writer().writeUInt32BE(static_cast<std::uint32_t>(info.timings.duration));
writer().writeUInt32BE(static_cast<std::uint32_t>(info.timings.tkhdDuration));
}
writer().writeUInt32BE(0); // reserved
writer().writeUInt32BE(0); // reserved
@ -1286,24 +1316,25 @@ void Mp4Track::makeMedia(Diagnostics &diag)
writer().writeUInt32BE(0); // write size later
writer().writeUInt32BE(Mp4AtomIds::Media);
// write mdhd atom
const auto timings = computeTimings();
const auto timingsVersion = timings.requiredVersion();
const auto &info = verifyPresentTrackHeader();
const auto &timings = info.timings;
const auto timingsVersion = timings.requiredMdhdVersion();
writer().writeUInt32BE(timingsVersion != 0 ? 44 : 32); // size
writer().writeUInt32BE(Mp4AtomIds::MediaHeader);
writer().writeByte(timingsVersion); // version
writer().writeUInt24BE(0); // flags
if (timingsVersion != 0) {
writer().writeUInt64BE(timings.creationTime);
writer().writeUInt64BE(timings.modificationTime);
writer().writeUInt64BE(timings.mdhdCreationTime);
writer().writeUInt64BE(timings.mdhdModificationTime);
} else {
writer().writeUInt32BE(static_cast<std::uint32_t>(timings.creationTime));
writer().writeUInt32BE(static_cast<std::uint32_t>(timings.modificationTime));
writer().writeUInt32BE(static_cast<std::uint32_t>(timings.mdhdCreationTime));
writer().writeUInt32BE(static_cast<std::uint32_t>(timings.mdhdModificationTime));
}
writer().writeUInt32BE(m_timeScale);
if (timingsVersion != 0) {
writer().writeUInt64BE(timings.duration);
writer().writeUInt64BE(timings.mdhdDuration);
} else {
writer().writeUInt32BE(static_cast<std::uint32_t>(timings.duration));
writer().writeUInt32BE(static_cast<std::uint32_t>(timings.mdhdDuration));
}
// convert and write language
const std::string &language = m_locale.abbreviatedName(LocaleFormat::ISO_639_2_T, LocaleFormat::Unknown);
@ -1556,19 +1587,22 @@ void Mp4Track::internalParseHeader(Diagnostics &diag, AbortableProgressFeedback
modFlagEnum(m_flags, TrackFlags::UsedWhenPreviewing, flags & 0x000004);
switch (atomVersion) {
case 0:
m_creationTime = startDate + TimeSpan::fromSeconds(reader.readUInt32BE());
m_modificationTime = startDate + TimeSpan::fromSeconds(reader.readUInt32BE());
m_rawTkhdCreationTime = reader.readUInt32BE();
m_rawTkhdModificationTime = reader.readUInt32BE();
m_id = reader.readUInt32BE();
m_rawTkhdDuration = reader.readUInt32BE();
break;
case 1:
m_creationTime = startDate + TimeSpan::fromSeconds(static_cast<double>(reader.readUInt64BE()));
m_modificationTime = startDate + TimeSpan::fromSeconds(static_cast<double>(reader.readUInt64BE()));
m_rawTkhdCreationTime = reader.readUInt64BE();
m_rawTkhdModificationTime = reader.readUInt64BE();
m_id = reader.readUInt32BE();
m_rawTkhdDuration = reader.readUInt64BE();
break;
default:
diag.emplace_back(DiagLevel::Critical,
"Version of \"tkhd\"-atom not supported. It will be ignored. Track ID, creation time and modification time might not be be determined.",
context);
m_rawTkhdCreationTime = m_rawTkhdModificationTime = m_rawTkhdDuration = 0;
m_creationTime = DateTime();
m_modificationTime = DateTime();
m_id = 0;
@ -1580,25 +1614,30 @@ void Mp4Track::internalParseHeader(Diagnostics &diag, AbortableProgressFeedback
m_istream->seekg(3, ios_base::cur); // skip flags
switch (atomVersion) {
case 0:
m_creationTime = startDate + TimeSpan::fromSeconds(reader.readUInt32BE());
m_modificationTime = startDate + TimeSpan::fromSeconds(reader.readUInt32BE());
m_rawMdhdCreationTime = reader.readUInt32BE();
m_rawMdhdModificationTime = reader.readUInt32BE();
m_timeScale = reader.readUInt32BE();
m_duration = TimeSpan::fromSeconds(static_cast<double>(reader.readUInt32BE()) / static_cast<double>(m_timeScale));
m_rawMdhdDuration = reader.readUInt32BE();
break;
case 1:
m_creationTime = startDate + TimeSpan::fromSeconds(static_cast<double>(reader.readUInt64BE()));
m_modificationTime = startDate + TimeSpan::fromSeconds(static_cast<double>(reader.readUInt64BE()));
m_rawMdhdCreationTime = reader.readUInt64BE();
m_rawMdhdModificationTime = reader.readUInt64BE();
m_timeScale = reader.readUInt32BE();
m_duration = TimeSpan::fromSeconds(static_cast<double>(reader.readUInt64BE()) / static_cast<double>(m_timeScale));
m_rawMdhdDuration = reader.readUInt64BE();
break;
default:
diag.emplace_back(DiagLevel::Warning,
"Version of \"mdhd\"-atom not supported. It will be ignored. Creation time, modification time, time scale and duration might not be "
"determined.",
context);
m_rawMdhdCreationTime = m_rawMdhdModificationTime = m_rawMdhdDuration = 0;
m_timeScale = 0;
m_duration = TimeSpan();
}
m_creationTime = startDate + TimeSpan::fromSeconds(static_cast<TimeSpan::TickType>(m_rawMdhdCreationTime));
m_modificationTime = startDate + TimeSpan::fromSeconds(static_cast<TimeSpan::TickType>(m_rawMdhdModificationTime));
m_duration = TimeSpan::fromSeconds(static_cast<TimeSpan::TickType>(m_rawMdhdDuration)) / static_cast<TimeSpan::TickType>(m_timeScale);
std::uint16_t tmp = reader.readUInt16BE();
if (tmp) {
const char buff[] = {

View File

@ -170,7 +170,7 @@ private:
std::uint64_t accumulateSampleSizes(std::size_t &sampleIndex, std::size_t count, Diagnostics &diag);
void addChunkSizeEntries(
std::vector<std::uint64_t> &chunkSizeTable, std::size_t count, std::size_t &sampleIndex, std::uint32_t sampleCount, Diagnostics &diag);
TrackHeaderInfo verifyPresentTrackHeader() const;
const TrackHeaderInfo &verifyPresentTrackHeader() const;
Mp4Timings computeTimings() const;
Mp4Atom *m_trakAtom;
@ -190,9 +190,16 @@ private:
unsigned int m_chunkOffsetSize;
std::uint32_t m_chunkCount;
std::uint32_t m_sampleToChunkEntryCount;
std::uint64_t m_rawTkhdCreationTime;
std::uint64_t m_rawMdhdCreationTime;
std::uint64_t m_rawTkhdModificationTime;
std::uint64_t m_rawMdhdModificationTime;
std::uint64_t m_rawTkhdDuration;
std::uint64_t m_rawMdhdDuration;
std::unique_ptr<Mpeg4ElementaryStreamInfo> m_esInfo;
std::unique_ptr<AvcConfiguration> m_avcConfig;
std::unique_ptr<Av1Configuration> m_av1Config;
mutable std::unique_ptr<TrackHeaderInfo> m_trackHeaderInfo;
};
/*!