diff options
Diffstat (limited to '')
-rw-r--r-- | dom/media/TimeUnits.cpp | 430 |
1 files changed, 430 insertions, 0 deletions
diff --git a/dom/media/TimeUnits.cpp b/dom/media/TimeUnits.cpp new file mode 100644 index 0000000000..346b3c48eb --- /dev/null +++ b/dom/media/TimeUnits.cpp @@ -0,0 +1,430 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <cstdint> +#include <cmath> +#include <inttypes.h> +#include <limits> +#include <type_traits> + +#include "TimeUnits.h" +#include "Intervals.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/Maybe.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "nsDebug.h" +#include "nsPrintfCString.h" +#include "nsStringFwd.h" + +namespace mozilla::media { +class TimeIntervals; +} // namespace mozilla::media + +namespace mozilla { + +namespace media { + +TimeUnit TimeUnit::FromSeconds(double aValue, int64_t aBase) { + MOZ_ASSERT(!std::isnan(aValue)); + MOZ_ASSERT(aBase > 0); + + if (std::isinf(aValue)) { + return aValue > 0 ? FromInfinity() : FromNegativeInfinity(); + } + // Warn that a particular value won't be able to be roundtrip at the same + // base -- we can keep this for some time until we're confident this is + // stable. + double inBase = aValue * static_cast<double>(aBase); + if (std::abs(inBase) > + static_cast<double>(std::numeric_limits<int64_t>::max())) { + NS_WARNING( + nsPrintfCString("Warning: base %" PRId64 + " is too high to represent %lfs, returning Infinity.", + aBase, aValue) + .get()); + if (inBase > 0) { + return TimeUnit::FromInfinity(); + } + return TimeUnit::FromNegativeInfinity(); + } + + // inBase can large enough that it doesn't map to an exact integer, warn in + // this case. This happens if aBase is large, and so the loss of precision is + // likely small. + if (inBase > std::pow(2, std::numeric_limits<double>::digits) - 1) { + NS_WARNING(nsPrintfCString("Warning: base %" PRId64 + " is too high to represent %lfs accurately.", + aBase, aValue) + .get()); + } + return TimeUnit(static_cast<int64_t>(inBase), aBase); +} + +TimeUnit TimeUnit::FromInfinity() { return TimeUnit(INT64_MAX); } + +TimeUnit TimeUnit::FromNegativeInfinity() { return TimeUnit(INT64_MIN); } + +TimeUnit TimeUnit::FromTimeDuration(const TimeDuration& aDuration) { + // This could be made to choose the base + return TimeUnit(AssertedCast<int64_t>(aDuration.ToMicroseconds()), + USECS_PER_S); +} + +TimeUnit TimeUnit::Invalid() { + TimeUnit ret; + ret.mTicks = CheckedInt64(INT64_MAX); + // Force an overflow to render the CheckedInt invalid. + ret.mTicks += 1; + return ret; +} + +int64_t TimeUnit::ToMilliseconds() const { return ToCommonUnit(MSECS_PER_S); } + +int64_t TimeUnit::ToMicroseconds() const { return ToCommonUnit(USECS_PER_S); } + +int64_t TimeUnit::ToNanoseconds() const { return ToCommonUnit(NSECS_PER_S); } + +int64_t TimeUnit::ToTicksAtRate(int64_t aRate) const { + // Common case + if (aRate == mBase) { + return mTicks.value(); + } + // Approximation + return mTicks.value() * aRate / mBase; +} + +double TimeUnit::ToSeconds() const { + if (IsPosInf()) { + return PositiveInfinity<double>(); + } + if (IsNegInf()) { + return NegativeInfinity<double>(); + } + return static_cast<double>(mTicks.value()) / static_cast<double>(mBase); +} + +nsCString TimeUnit::ToString() const { + nsCString dump; + if (mTicks.isValid()) { + dump += nsPrintfCString("{%" PRId64 ",%" PRId64 "}", mTicks.value(), mBase); + } else { + dump += nsLiteralCString("{invalid}"_ns); + } + return dump; +} + +TimeDuration TimeUnit::ToTimeDuration() const { + return TimeDuration::FromSeconds(ToSeconds()); +} + +bool TimeUnit::IsInfinite() const { return IsPosInf() || IsNegInf(); } + +bool TimeUnit::IsPositive() const { return mTicks.value() > 0; } + +bool TimeUnit::IsPositiveOrZero() const { return mTicks.value() >= 0; } + +bool TimeUnit::IsZero() const { return mTicks.value() == 0; } + +bool TimeUnit::IsNegative() const { return mTicks.value() < 0; } + +// Returns true if the fractions are equal when converted to the smallest +// base. +bool TimeUnit::EqualsAtLowestResolution(const TimeUnit& aOther) const { + MOZ_ASSERT(IsValid() && aOther.IsValid()); + if (aOther.mBase == mBase) { + return mTicks == aOther.mTicks; + } + if (mBase > aOther.mBase) { + TimeUnit thisInBase = ToBase(aOther.mBase); + return thisInBase.mTicks == aOther.mTicks; + } + TimeUnit otherInBase = aOther.ToBase(mBase); + return otherInBase.mTicks == mTicks; +} + +// Strict equality -- the fractions must be exactly equal +bool TimeUnit::operator==(const TimeUnit& aOther) const { + MOZ_ASSERT(IsValid() && aOther.IsValid()); + if (aOther.mBase == mBase) { + return mTicks == aOther.mTicks; + } + // debatable mathematically + if ((IsPosInf() && aOther.IsPosInf()) || (IsNegInf() && aOther.IsNegInf())) { + return true; + } + if ((IsPosInf() && !aOther.IsPosInf()) || + (IsNegInf() && !aOther.IsNegInf())) { + return false; + } + CheckedInt<int64_t> lhs = mTicks * aOther.mBase; + CheckedInt<int64_t> rhs = aOther.mTicks * mBase; + if (lhs.isValid() && rhs.isValid()) { + return lhs == rhs; + } + // Reduce the fractions and try again + const TimeUnit a = Reduced(); + const TimeUnit b = aOther.Reduced(); + lhs = a.mTicks * b.mBase; + rhs = b.mTicks * a.mBase; + + if (lhs.isValid() && rhs.isValid()) { + return lhs.value() == rhs.value(); + } + // last ditch, convert the reduced fractions to doubles + double lhsFloating = + static_cast<double>(a.mTicks.value()) * static_cast<double>(a.mBase); + double rhsFloating = + static_cast<double>(b.mTicks.value()) * static_cast<double>(b.mBase); + + return lhsFloating == rhsFloating; +} +bool TimeUnit::operator!=(const TimeUnit& aOther) const { + MOZ_ASSERT(IsValid() && aOther.IsValid()); + return !(aOther == *this); +} +bool TimeUnit::operator>=(const TimeUnit& aOther) const { + MOZ_ASSERT(IsValid() && aOther.IsValid()); + if (aOther.mBase == mBase) { + return mTicks.value() >= aOther.mTicks.value(); + } + if ((!IsPosInf() && aOther.IsPosInf()) || + (IsNegInf() && !aOther.IsNegInf())) { + return false; + } + if ((IsPosInf() && !aOther.IsPosInf()) || + (!IsNegInf() && aOther.IsNegInf())) { + return true; + } + CheckedInt<int64_t> lhs = mTicks * aOther.mBase; + CheckedInt<int64_t> rhs = aOther.mTicks * mBase; + if (lhs.isValid() && rhs.isValid()) { + return lhs.value() >= rhs.value(); + } + // Reduce the fractions and try again + const TimeUnit a = Reduced(); + const TimeUnit b = aOther.Reduced(); + lhs = a.mTicks * b.mBase; + rhs = b.mTicks * a.mBase; + + if (lhs.isValid() && rhs.isValid()) { + return lhs.value() >= rhs.value(); + } + // last ditch, convert the reduced fractions to doubles + return ToSeconds() >= aOther.ToSeconds(); +} +bool TimeUnit::operator>(const TimeUnit& aOther) const { + return !(*this <= aOther); +} +bool TimeUnit::operator<=(const TimeUnit& aOther) const { + MOZ_ASSERT(IsValid() && aOther.IsValid()); + if (aOther.mBase == mBase) { + return mTicks.value() <= aOther.mTicks.value(); + } + if ((!IsPosInf() && aOther.IsPosInf()) || + (IsNegInf() && !aOther.IsNegInf())) { + return true; + } + if ((IsPosInf() && !aOther.IsPosInf()) || + (!IsNegInf() && aOther.IsNegInf())) { + return false; + } + CheckedInt<int64_t> lhs = mTicks * aOther.mBase; + CheckedInt<int64_t> rhs = aOther.mTicks * mBase; + if (lhs.isValid() && rhs.isValid()) { + return lhs.value() <= rhs.value(); + } + // Reduce the fractions and try again + const TimeUnit a = Reduced(); + const TimeUnit b = aOther.Reduced(); + lhs = a.mTicks * b.mBase; + rhs = b.mTicks * a.mBase; + if (lhs.isValid() && rhs.isValid()) { + return lhs.value() <= rhs.value(); + } + // last ditch, convert the reduced fractions to doubles + return ToSeconds() <= aOther.ToSeconds(); +} +bool TimeUnit::operator<(const TimeUnit& aOther) const { + return !(*this >= aOther); +} + +TimeUnit TimeUnit::operator%(const TimeUnit& aOther) const { + MOZ_ASSERT(IsValid() && aOther.IsValid()); + if (aOther.mBase == mBase) { + return TimeUnit(mTicks % aOther.mTicks, mBase); + } + // This path can be made better if need be. + double a = ToSeconds(); + double b = aOther.ToSeconds(); + return TimeUnit::FromSeconds(fmod(a, b), mBase); +} + +TimeUnit TimeUnit::operator+(const TimeUnit& aOther) const { + if (IsInfinite() || aOther.IsInfinite()) { + // When adding at least one infinite value, the result is either + // +/-Inf, or NaN. So do the calculation in floating point for + // simplicity. + double result = ToSeconds() + aOther.ToSeconds(); + return std::isnan(result) ? TimeUnit::Invalid() : FromSeconds(result); + } + if (aOther.mBase == mBase) { + return TimeUnit(mTicks + aOther.mTicks, mBase); + } + if (aOther.IsZero()) { + return *this; + } + if (IsZero()) { + return aOther; + } + + double error; + TimeUnit inBase = aOther.ToBase(mBase, error); + if (error == 0.0) { + return *this + inBase; + } + + // Last ditch: not exact + double a = ToSeconds(); + double b = aOther.ToSeconds(); + return TimeUnit::FromSeconds(a + b, mBase); +} + +TimeUnit TimeUnit::operator-(const TimeUnit& aOther) const { + if (IsInfinite() || aOther.IsInfinite()) { + // When subtracting at least one infinite value, the result is either + // +/-Inf, or NaN. So do the calculation in floating point for + // simplicity. + double result = ToSeconds() - aOther.ToSeconds(); + return std::isnan(result) ? TimeUnit::Invalid() : FromSeconds(result); + } + if (aOther.mBase == mBase) { + return TimeUnit(mTicks - aOther.mTicks, mBase); + } + if (aOther.IsZero()) { + return *this; + } + + if (IsZero()) { + return TimeUnit(-aOther.mTicks, aOther.mBase); + } + + double error = 0.0; + TimeUnit inBase = aOther.ToBase(mBase, error); + if (error == 0) { + return *this - inBase; + } + + // Last ditch: not exact + double a = ToSeconds(); + double b = aOther.ToSeconds(); + return TimeUnit::FromSeconds(a - b, mBase); +} +TimeUnit& TimeUnit::operator+=(const TimeUnit& aOther) { + if (aOther.mBase == mBase) { + mTicks += aOther.mTicks; + return *this; + } + *this = *this + aOther; + return *this; +} +TimeUnit& TimeUnit::operator-=(const TimeUnit& aOther) { + if (aOther.mBase == mBase) { + mTicks -= aOther.mTicks; + return *this; + } + *this = *this - aOther; + return *this; +} + +TimeUnit TimeUnit::MultDouble(double aVal) const { + double multiplied = AssertedCast<double>(mTicks.value()) * aVal; + // Check is the result of the multiplication can be represented exactly as + // an integer, in a double. + if (multiplied > std::pow(2, std::numeric_limits<double>::digits) - 1) { + printf_stderr("TimeUnit tick count after multiplication %" PRId64 + " * %lf is too" + " high for the result to be exact", + mTicks.value(), aVal); + MOZ_CRASH(); + } + // static_cast is ok, the magnitude of the number has been checked just above. + return TimeUnit(static_cast<int64_t>(multiplied), mBase); +} + +bool TimeUnit::IsValid() const { return mTicks.isValid(); } + +bool TimeUnit::IsPosInf() const { + return mTicks.isValid() && mTicks.value() == INT64_MAX; +} +bool TimeUnit::IsNegInf() const { + return mTicks.isValid() && mTicks.value() == INT64_MIN; +} + +int64_t TimeUnit::ToCommonUnit(int64_t aRatio) const { + CheckedInt<int64_t> rv = mTicks; + // Avoid the risk overflowing in common cases, e.g. converting a TimeUnit + // with a base of 1e9 back to nanoseconds. + if (mBase == aRatio) { + return rv.value(); + } + // Avoid overflowing in other common cases, e.g. converting a TimeUnit with + // a base of 1e9 to microseconds: the denominator is divisible by the target + // unit so we can reorder the computation and keep the number within int64_t + // range. + if (aRatio < mBase && (mBase % aRatio) == 0) { + int64_t exactDivisor = mBase / aRatio; + rv /= exactDivisor; + return rv.value(); + } + rv *= aRatio; + rv /= mBase; + if (rv.isValid()) { + return rv.value(); + } + // Last ditch, perform the computation in floating point. + double ratioFloating = AssertedCast<double>(aRatio); + double baseFloating = AssertedCast<double>(mBase); + double ticksFloating = static_cast<double>(mTicks.value()); + double approx = ticksFloating * (ratioFloating / baseFloating); + // Clamp to a valid range. If this is clamped it's outside any usable time + // value even in nanoseconds (thousands of years). + if (approx > static_cast<double>(std::numeric_limits<int64_t>::max())) { + return std::numeric_limits<int64_t>::max(); + } + if (approx < static_cast<double>(std::numeric_limits<int64_t>::lowest())) { + return std::numeric_limits<int64_t>::lowest(); + } + return static_cast<int64_t>(approx); +} + +// Reduce a TimeUnit to the smallest possible ticks and base. This is useful +// to comparison with big time values that can otherwise overflow. +TimeUnit TimeUnit::Reduced() const { + int64_t gcd = GCD(mTicks.value(), mBase); + return TimeUnit(mTicks.value() / gcd, mBase / gcd); +} + +double RoundToMicrosecondResolution(double aSeconds) { + return std::round(aSeconds * USECS_PER_S) / USECS_PER_S; +} + +TimeRanges TimeRanges::ToMicrosecondResolution() const { + TimeRanges output; + + for (const auto& interval : mIntervals) { + TimeRange reducedPrecision{RoundToMicrosecondResolution(interval.mStart), + RoundToMicrosecondResolution(interval.mEnd), + RoundToMicrosecondResolution(interval.mFuzz)}; + output += reducedPrecision; + } + return output; +} + +}; // namespace media + +} // namespace mozilla |