/* -*- 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 #include #include #include #include #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(aBase); if (std::abs(inBase) > static_cast(std::numeric_limits::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::digits) - 1) { NS_WARNING(nsPrintfCString("Warning: base %" PRId64 " is too high to represent %lfs accurately.", aBase, aValue) .get()); } return TimeUnit(static_cast(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(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(); } if (IsNegInf()) { return NegativeInfinity(); } return static_cast(mTicks.value()) / static_cast(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 lhs = mTicks * aOther.mBase; CheckedInt 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(a.mTicks.value()) * static_cast(a.mBase); double rhsFloating = static_cast(b.mTicks.value()) * static_cast(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 lhs = mTicks * aOther.mBase; CheckedInt 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 lhs = mTicks * aOther.mBase; CheckedInt 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(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::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(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 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(aRatio); double baseFloating = AssertedCast(mBase); double ticksFloating = static_cast(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(std::numeric_limits::max())) { return std::numeric_limits::max(); } if (approx < static_cast(std::numeric_limits::lowest())) { return std::numeric_limits::lowest(); } return static_cast(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