summaryrefslogtreecommitdiffstats
path: root/dom/media/TimeUnits.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/TimeUnits.cpp')
-rw-r--r--dom/media/TimeUnits.cpp430
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