diff --git a/doc/software-manual.tex b/doc/software-manual.tex
index deb0fef18383d24812c20448e0d2680ad415a0e1..1b2fcd8f2895a370e8a08508480009e046a21d42 100644
--- a/doc/software-manual.tex
+++ b/doc/software-manual.tex
@@ -844,9 +844,10 @@ two for 4:2:0).
 \Option{FrameRate (-fr)} &
 %\ShortOption{-fr} &
 \Default{0} &
-Specifies the frame rate of the input video.
+Specifies the frame rate of the input video. A frame rate may be specified by two numbers
+such as 30000:1001 to define a non-integer value (e.g., 29.97).
 
-Note: This option only affects the reported bit rates.
+Note: This option affects the reported bit rates.
 \\
 
 \Option{FrameSkip (-fs)} &
diff --git a/source/App/DecoderApp/DecApp.cpp b/source/App/DecoderApp/DecApp.cpp
index 753c36bf608cedd935fa9e95203d399b261ccd23..c00f4c8a79dccff841c58d44a324c59cea7ffaa2 100644
--- a/source/App/DecoderApp/DecApp.cpp
+++ b/source/App/DecoderApp/DecApp.cpp
@@ -36,6 +36,7 @@
 */
 
 #include <list>
+#include <numeric>
 #include <vector>
 #include <stdio.h>
 #include <fcntl.h>
@@ -51,13 +52,6 @@
 //! \ingroup DecoderApp
 //! \{
 
-static int calcGcd(int a, int b)
-{
-  // assume that a >= b
-  return b == 0 ? a : calcGcd(b, a % b);
-}
-
-
 // ====================================================================================================================
 // Constructor / destructor / initialization / destroy
 // ====================================================================================================================
@@ -461,9 +455,8 @@ uint32_t DecApp::decode()
             if (isY4mFileExt(reconFileName))
             {
               const auto sps        = pcListPic->front()->cs->sps;
-            int        frameRate  = 50;
-              int        frameScale = 1;
-              if(sps->getGeneralHrdParametersPresentFlag())
+              Fraction   frameRate  = { 50, 1 };
+              if (sps->getGeneralHrdParametersPresentFlag())
               {
                 const auto hrd                 = sps->getGeneralHrdParameters();
                 const auto olsHrdParam         = sps->getOlsHrdParameters()[sps->getMaxTLayers() - 1];
@@ -474,13 +467,14 @@ uint32_t DecApp::decode()
                 }
                 else
                 {
-                  msg(WARNING, "\nWarning: No fixed picture rate info is found in the bitstream, best guess is used.\n");
+                  msg(WARNING,
+                      "\nWarning: No fixed picture rate info is found in the bitstream, best guess is used.\n");
                 }
-                frameRate  = hrd->getTimeScale() * elementDurationInTc;
-                frameScale = hrd->getNumUnitsInTick();
-                int gcd    = calcGcd(std::max(frameRate, frameScale), std::min(frameRate, frameScale));
-                frameRate /= gcd;
-                frameScale /= gcd;
+                frameRate.num = hrd->getTimeScale() * elementDurationInTc;
+                frameRate.den = hrd->getNumUnitsInTick();
+                const int gcd = std::gcd(frameRate.num, frameRate.den);
+                frameRate.num /= gcd;
+                frameRate.den /= gcd;
               }
               else
               {
@@ -492,9 +486,8 @@ uint32_t DecApp::decode()
               const auto sy = SPS::getWinUnitY(sps->getChromaFormatIdc());
               const int picWidth = pps->getPicWidthInLumaSamples() - (confWindow.getWindowLeftOffset() + confWindow.getWindowRightOffset()) * sx;
               const int picHeight = pps->getPicHeightInLumaSamples() - (confWindow.getWindowTopOffset() + confWindow.getWindowBottomOffset()) * sy;
-              m_cVideoIOYuvReconFile[nalu.m_nuhLayerId].setOutputY4mInfo(picWidth, picHeight, frameRate, frameScale,
-                                                                         layerOutputBitDepth[ChannelType::LUMA],
-                                                                         sps->getChromaFormatIdc());
+              m_cVideoIOYuvReconFile[nalu.m_nuhLayerId].setOutputY4mInfo(
+                picWidth, picHeight, frameRate, layerOutputBitDepth[ChannelType::LUMA], sps->getChromaFormatIdc());
             }
             m_cVideoIOYuvReconFile[nalu.m_nuhLayerId].open(reconFileName, true, layerOutputBitDepth,
                                                            layerOutputBitDepth, bitDepths);   // write mode
diff --git a/source/App/EncoderApp/EncApp.cpp b/source/App/EncoderApp/EncApp.cpp
index 17120c27c1526770a2d3c0f8446ecfaab72bbd83..1019f1e77af7fb6e5346be6118adda756c0cca34 100644
--- a/source/App/EncoderApp/EncApp.cpp
+++ b/source/App/EncoderApp/EncApp.cpp
@@ -1298,15 +1298,18 @@ void EncApp::xInitLibCfg( int layerIdx )
   m_cEncLib.setDepQuantEnabledFlag                               ( m_depQuantEnabledFlag);
   m_cEncLib.setSignDataHidingEnabledFlag                         ( m_signDataHidingEnabledFlag);
   m_cEncLib.setUseRateCtrl                                       ( m_RCEnableRateControl );
-  m_cEncLib.setTargetBitrate                                     ( m_RCTargetBitrate );
-  m_cEncLib.setKeepHierBit                                       ( m_RCKeepHierarchicalBit );
-  m_cEncLib.setLCULevelRC                                        ( m_RCLCULevelRC );
-  m_cEncLib.setUseLCUSeparateModel                               ( m_RCUseLCUSeparateModel );
-  m_cEncLib.setInitialQP                                         ( m_RCInitialQP );
-  m_cEncLib.setForceIntraQP                                      ( m_RCForceIntraQP );
-  m_cEncLib.setCpbSaturationEnabled                              ( m_RCCpbSaturationEnabled );
-  m_cEncLib.setCpbSize                                           ( m_RCCpbSize );
-  m_cEncLib.setInitialCpbFullness                                ( m_RCInitialCpbFullness );
+  if (m_RCEnableRateControl)
+  {
+    m_cEncLib.setTargetBitrate(m_RCTargetBitrate);
+    m_cEncLib.setKeepHierBit(m_RCKeepHierarchicalBit);
+    m_cEncLib.setLCULevelRC(m_RCLCULevelRC);
+    m_cEncLib.setUseLCUSeparateModel(m_RCUseLCUSeparateModel);
+    m_cEncLib.setInitialQP(m_RCInitialQP);
+    m_cEncLib.setForceIntraQP(m_RCForceIntraQP);
+    m_cEncLib.setCpbSaturationEnabled(m_RCCpbSaturationEnabled);
+    m_cEncLib.setCpbSize(m_RCCpbSize);
+    m_cEncLib.setInitialCpbFullness(m_RCInitialCpbFullness);
+  }
   m_cEncLib.setCostMode                                          ( m_costMode );
   m_cEncLib.setTSRCdisableLL                                     ( m_TSRCdisableLL );
   m_cEncLib.setUseRecalculateQPAccordingToLambda                 ( m_recalculateQPAccordingToLambda );
@@ -1488,7 +1491,7 @@ void EncApp::xCreateLib( std::list<PelUnitBuf*>& recBufList, const int layerId )
       const auto sx = SPS::getWinUnitX(m_chromaFormatIdc);
       const auto sy = SPS::getWinUnitY(m_chromaFormatIdc);
       m_cVideoIOYuvReconFile.setOutputY4mInfo(m_sourceWidth - (m_confWinLeft + m_confWinRight) * sx,
-                                              m_sourceHeight - (m_confWinTop + m_confWinBottom) * sy, m_frameRate, 1,
+                                              m_sourceHeight - (m_confWinTop + m_confWinBottom) * sy, m_frameRate,
                                               m_internalBitDepth[ChannelType::LUMA], m_chromaFormatIdc);
     }
     m_cVideoIOYuvReconFile.open( reconFileName, true, m_outputBitDepth, m_outputBitDepth, m_internalBitDepth );  // write mode
@@ -1586,6 +1589,12 @@ void EncApp::createLib( const int layerIdx )
     }
   }
 
+  if (isY4mFileExt(m_inputFileName))
+  {
+    // Force signalling of HRD parameters to carry frame rate information
+    m_hrdParametersPresentFlag = true;
+  }
+
   // initialize internal class & member variables and VPS
   xInitLibCfg( layerIdx );
   const int layerId = m_cEncLib.getVPS() == nullptr ? 0 : m_cEncLib.getVPS()->getLayerId( layerIdx );
@@ -1980,7 +1989,7 @@ void EncApp::rateStatsAccum(const AccessUnit& au, const std::vector<uint32_t>& a
 
 void EncApp::printRateSummary()
 {
-  double time = (double) m_frameRcvd / m_frameRate * m_temporalSubsampleRatio;
+  double time = (double) m_frameRcvd / m_frameRate.getFloatVal() * m_temporalSubsampleRatio;
   msg( DETAILS,"Bytes written to file: %u (%.3f kbps)\n", m_totalBytes, 0.008 * m_totalBytes / time );
   if (m_summaryVerboseness > 0)
   {
diff --git a/source/App/EncoderApp/EncAppCfg.cpp b/source/App/EncoderApp/EncAppCfg.cpp
index 8b41b7a4a6a6b129b3b1c9e277fd9d2fe14d78b8..fa33f47be055df6f01523ea06c041fd97655efed 100644
--- a/source/App/EncoderApp/EncAppCfg.cpp
+++ b/source/App/EncoderApp/EncAppCfg.cpp
@@ -768,6 +768,7 @@ bool EncAppCfg::parseCfg( int argc, char* argv[] )
 #if ENABLE_SIMD_OPT
   std::string ignore;
 #endif
+  std::string frameRate;
 
   bool sdr = false;
 
@@ -826,7 +827,7 @@ bool EncAppCfg::parseCfg( int argc, char* argv[] )
   ("ConfWinBottom",                                   m_confWinBottom,                                      0, "Bottom offset for window conformance mode 3")
   ("AccessUnitDelimiter",                             m_AccessUnitDelimiter,                            false, "Enable Access Unit Delimiter NALUs")
   ("EnablePictureHeaderInSliceHeader",                m_enablePictureHeaderInSliceHeader,                true, "Enable Picture Header in Slice Header")
-  ("FrameRate,-fr",                                   m_frameRate,                                         0, "Frame rate")
+  ("FrameRate,-fr",                                   frameRate,                            std::to_string(0), "Frame rate")
   ("FrameSkip,-fs",                                   m_frameSkip,                                         0u, "Number of frames to skip at start of input YUV")
   ("TemporalSubsampleRatio,-ts",                      m_temporalSubsampleRatio,                            1u, "Temporal sub-sample ratio when reading input YUV")
   ("FramesToBeEncoded,f",                             m_framesToBeEncoded,                                  0, "Number of frames to be encoded (default=all)")
@@ -1961,6 +1962,11 @@ bool EncAppCfg::parseCfg( int argc, char* argv[] )
     m_rprRASLtoolSwitch      = false;
   }
 
+  const size_t columnPos = frameRate.find_first_of(':');
+
+  m_frameRate.num = std::stoi(frameRate.substr(0, columnPos));
+  m_frameRate.den = columnPos == frameRate.length() ? 1 : std::stoi(frameRate.substr(columnPos + 1));
+
   if( m_fractionOfFrames != 1.0 )
   {
     m_framesToBeEncoded = int( m_framesToBeEncoded * m_fractionOfFrames );
@@ -1968,7 +1974,7 @@ bool EncAppCfg::parseCfg( int argc, char* argv[] )
 
   if (m_resChangeInClvsEnabled && !m_switchPocPeriod)
   {
-    m_switchPocPeriod = m_frameRate / 2 / m_gopSize * m_gopSize;
+    m_switchPocPeriod = m_frameRate.getIntValRound() / 2 / m_gopSize * m_gopSize;
   }
 
   //Check the given value of intra period and decoding refresh type. If intra period is -1, set decoding refresh type to be equal to 0. And vice versa
@@ -2032,12 +2038,12 @@ bool EncAppCfg::parseCfg( int argc, char* argv[] )
 
     if (m_gdrPeriod < 0)
     {
-      m_gdrPeriod = m_frameRate * 2;
+      m_gdrPeriod = m_frameRate.getIntValRound() * 2;
     }
 
     if (m_gdrInterval < 0)
     {
-      m_gdrInterval = m_frameRate;
+      m_gdrInterval = m_frameRate.getIntValRound();
     }
 
     if (m_gdrPocStart < 0)
@@ -2047,8 +2053,8 @@ bool EncAppCfg::parseCfg( int argc, char* argv[] )
 
     if (m_intraPeriod == -1)
     {
-      m_frameRate = (m_frameRate == 0) ? 30 : m_frameRate;
-      if (m_gdrPocStart % m_frameRate != 0)
+      m_frameRate = (m_frameRate.num == 0) ? Fraction{ 30, 1 } : m_frameRate;
+      if (m_gdrPocStart % m_frameRate.getIntValRound() != 0)
       {
         m_intraPeriod = -1;
       }
@@ -2466,7 +2472,10 @@ bool EncAppCfg::parseCfg( int argc, char* argv[] )
 
   if (isY4mFileExt(m_inputFileName))
   {
-    int          width = 0, height = 0, frameRate = 0, inputBitDepth = 0;
+    int          width          = 0;
+    int          height         = 0;
+    Fraction     frameRate;
+    int          inputBitDepth  = 0;
     ChromaFormat chromaFormat = ChromaFormat::_420;
     VideoIOYuv   inputFile;
     inputFile.parseY4mFileHeader(m_inputFileName, width, height, frameRate, inputBitDepth, chromaFormat);
@@ -2848,7 +2857,7 @@ bool EncAppCfg::parseCfg( int argc, char* argv[] )
     }
     if (m_rprSwitchingTime != 0.0)
     {
-      int segmentSize = 8 * int(((double)m_frameRate * m_rprSwitchingTime + 4) / 8);
+      const int segmentSize     = 8 * int(m_frameRate.getFloatVal() * m_rprSwitchingTime / 8 + 0.5);
       m_rprSwitchingSegmentSize = segmentSize;
     }
   }
@@ -3031,7 +3040,7 @@ bool EncAppCfg::parseCfg( int argc, char* argv[] )
     if (m_fgcSEIAnalysisEnabled && m_fgcSEITemporalFilterStrengths.empty())
     {
       // By default: in random-acces = filter RAPs, in all-intra = filter every frame, otherwise = filter every 2s
-      int filteredFrame                              = m_intraPeriod < 1 ? 2 * m_frameRate : m_intraPeriod;
+      int filteredFrame = m_intraPeriod < 1 ? 2 * m_frameRate.getIntValRound() : m_intraPeriod;
       m_fgcSEITemporalFilterStrengths[filteredFrame] = 1.5;
     }
     uint32_t numModelCtr;
@@ -3317,9 +3326,9 @@ bool EncAppCfg::parseCfg( int argc, char* argv[] )
     }
   }
   m_reshapeCW.binCW.resize(3);
-  m_reshapeCW.rspFps     = m_frameRate;
+  m_reshapeCW.rspFps     = m_frameRate.getIntValRound();
   m_reshapeCW.rspPicSize = m_sourceWidth*m_sourceHeight;
-  m_reshapeCW.rspFpsToIp = std::max(16, 16 * (int) (round((double) m_frameRate / 16.0)));
+  m_reshapeCW.rspFpsToIp = std::max(16, 16 * (int) (round(m_frameRate.getFloatVal() / 16.0)));
   m_reshapeCW.rspBaseQP = m_iQP;
   m_reshapeCW.updateCtrl = m_updateCtrl;
   m_reshapeCW.adpOption = m_adpOption;
@@ -3399,7 +3408,7 @@ bool EncAppCfg::parseCfg( int argc, char* argv[] )
                                 (blending_ratio * m_siiSEISubLayerNumUnitsInSI[siiMaxSubLayersMinus1]))
     {
       m_ShutterFilterEnable = true;
-      double  fpsHFR        = (double) m_frameRate;
+      double  fpsHFR        = m_frameRate.getFloatVal();
       int32_t i;
       bool    checkEqualValuesOfSFR = true;
       bool    checkSubLayerSI       = false;
@@ -3767,7 +3776,7 @@ bool EncAppCfg::xCheckParameter()
   std::string sTempIPCSC="InputColourSpaceConvert must be empty, "+getListOfColourSpaceConverts(true);
   xConfirmPara( m_inputColourSpaceConvert >= NUMBER_INPUT_COLOUR_SPACE_CONVERSIONS,         sTempIPCSC.c_str() );
   xConfirmPara(m_inputChromaFormatIDC >= ChromaFormat::NUM, "InputChromaFormatIDC must be either 400, 420, 422 or 444");
-  xConfirmPara(m_frameRate <= 0, "Frame rate must be more than 1");
+  xConfirmPara(m_frameRate.getFloatVal() <= 0, "Frame rate cannot be 0 or less");
   xConfirmPara( m_framesToBeEncoded <= 0,                                                   "Total Number Of Frames encoded must be more than 0" );
   xConfirmPara( m_framesToBeEncoded < m_switchPOC,                                          "debug POC out of range" );
 
@@ -5090,9 +5099,9 @@ void EncAppCfg::xPrintParameter()
   }
 #endif
   msg(DETAILS, "Real     Format                        : %dx%d %gHz\n", m_sourceWidth - m_confWinLeft - m_confWinRight,
-      m_sourceHeight - m_confWinTop - m_confWinBottom, (double) m_frameRate / m_temporalSubsampleRatio);
+      m_sourceHeight - m_confWinTop - m_confWinBottom, m_frameRate.getFloatVal() / m_temporalSubsampleRatio);
   msg(DETAILS, "Internal Format                        : %dx%d %gHz\n", m_sourceWidth, m_sourceHeight,
-      (double) m_frameRate / m_temporalSubsampleRatio);
+      m_frameRate.getFloatVal() / m_temporalSubsampleRatio);
   msg( DETAILS, "Sequence PSNR output                   : %s\n", ( m_printMSEBasedSequencePSNR ? "Linear average, MSE-based" : "Linear average only" ) );
   msg( DETAILS, "Hexadecimal PSNR output                : %s\n", ( m_printHexPsnr ? "Enabled" : "Disabled" ) );
   msg( DETAILS, "Sequence MSE output                    : %s\n", ( m_printSequenceMSE ? "Enabled" : "Disabled" ) );
diff --git a/source/App/EncoderApp/EncAppCfg.h b/source/App/EncoderApp/EncAppCfg.h
index 64029f899451a34311c0add5ad77f075495ba490..4985ac7d41ae67d7b3190d5edf89b6e8557b379b 100644
--- a/source/App/EncoderApp/EncAppCfg.h
+++ b/source/App/EncoderApp/EncAppCfg.h
@@ -93,7 +93,7 @@ protected:
   double    m_dIntraQpFactor;                                 ///< Intra Q Factor. If negative, use a default equation: 0.57*(1.0 - Clip3( 0.0, 0.5, 0.05*(double)(isField ? (GopSize-1)/2 : GopSize-1) ))
 
   // source specification
-  int           m_frameRate;                                      ///< source frame-rates (Hz)
+  Fraction      m_frameRate;
   uint32_t      m_frameSkip;                                      ///< number of skipped frames from the beginning
   uint32_t      m_temporalSubsampleRatio;                         ///< temporal subsample ratio, 2 means code every two frames
   int       m_sourceWidth;                                   ///< source width in pixel
diff --git a/source/Lib/CommonLib/TypeDef.h b/source/Lib/CommonLib/TypeDef.h
index e547758b1c91699f05c2095ad88bc30c4d639223..3e7cf0d1cdb0b08819a9ae08ecd24add3c37a8b8 100644
--- a/source/Lib/CommonLib/TypeDef.h
+++ b/source/Lib/CommonLib/TypeDef.h
@@ -777,6 +777,18 @@ enum POST_FILTER_MODE
   URI = 1
 };
 
+struct Fraction
+{
+  int num = 0;
+  int den = 1;
+
+  int    getIntValRound() const { return (num + den / 2) / den; }
+  double getFloatVal() const { return static_cast<double>(num) / den; }
+};
+
+inline bool operator==(const Fraction& a, const Fraction& b) { return a.num == b.num && a.den == b.den; }
+inline bool operator!=(const Fraction& a, const Fraction& b) { return !(a == b); }
+
 #define NUM_SAO_BO_CLASSES_LOG2  5
 #define NUM_SAO_BO_CLASSES       (1<<NUM_SAO_BO_CLASSES_LOG2)
 
diff --git a/source/Lib/EncoderLib/Analyze.h b/source/Lib/EncoderLib/Analyze.h
index bf30f5664df5dcadc10f4cf97e9eda4578b97480..e0c0b7c04bec4261e13d1a2053dc49e2d8d0b64b 100644
--- a/source/Lib/EncoderLib/Analyze.h
+++ b/source/Lib/EncoderLib/Analyze.h
@@ -63,7 +63,7 @@ private:
 
   double m_dPSNRSum[MAX_NUM_COMPONENT];
   double m_dAddBits;
-  double m_frameRate;
+  Fraction m_frameRate;
   double m_mseYuvFrame[MAX_NUM_COMPONENT];   // sum of MSEs
   double m_upscaledPSNR[MAX_NUM_COMPONENT];
   double m_msssim[MAX_NUM_COMPONENT];
@@ -124,7 +124,7 @@ public:
   }
 #endif
 
-  void setFrameRate(double frameRate) { m_frameRate = frameRate; }
+  void setFrameRate(const Fraction& frameRate) { m_frameRate = frameRate; }
   void clear()
   {
     m_dAddBits = 0;
@@ -204,7 +204,7 @@ public:
       return y;
     };
 
-    double fps   = m_frameRate;
+    double fps   = m_frameRate.getFloatVal();
     double scale = fps / 1000 / (double) m_picCount;
 
     double mseBasedSNR[MAX_NUM_COMPONENT];
@@ -327,7 +327,7 @@ public:
   {
     FILE* pFile = fopen (sFilename.c_str(), "at");
 
-    double dFps     =   m_frameRate;
+    double dFps     = m_frameRate.getFloatVal();
     double dScale   = dFps / 1000 / (double) m_picCount;
     switch (chFmt)
     {
diff --git a/source/Lib/EncoderLib/EncCfg.h b/source/Lib/EncoderLib/EncCfg.h
index ece6e4a32adaf845e86c603a995cbfe9d480297b..017751786616a1243d9fd37b3d2b776e7821243a 100644
--- a/source/Lib/EncoderLib/EncCfg.h
+++ b/source/Lib/EncoderLib/EncCfg.h
@@ -155,7 +155,7 @@ class EncCfg
 {
 protected:
   //==== File I/O ========
-  int       m_frameRate;
+  Fraction  m_frameRate;
   int       m_frameSkip;
   uint32_t  m_temporalSubsampleRatio;
   int       m_sourceWidth;
@@ -1182,7 +1182,7 @@ public:
   bool      getNoReverseLastSigCoeffConstraintFlag() const { return m_noReverseLastSigCoeffConstraintFlag; }
   void      setNoReverseLastSigCoeffConstraintFlag(bool val) { m_noReverseLastSigCoeffConstraintFlag = val; }
 
-  void      setFrameRate(int i) { m_frameRate = i; }
+  void      setFrameRate(const Fraction& fr) { m_frameRate = fr; }
   void      setFrameSkip(uint32_t i) { m_frameSkip = i; }
   void      setTemporalSubsampleRatio       ( uint32_t  i )      { m_temporalSubsampleRatio = i; }
   void      setSourceWidth                  ( int   i )      { m_sourceWidth = i; }
@@ -1669,7 +1669,7 @@ public:
 #endif
 
   //====== Sequence ========
-  int           getFrameRate() const { return m_frameRate; }
+  const Fraction& getFrameRate() const { return m_frameRate; }
   uint32_t      getFrameSkip() const { return m_frameSkip; }
   uint32_t      getTemporalSubsampleRatio       () const     { return  m_temporalSubsampleRatio; }
   int       getSourceWidth                  () const     { return  m_sourceWidth; }
diff --git a/source/Lib/EncoderLib/EncGOP.cpp b/source/Lib/EncoderLib/EncGOP.cpp
index 1ca8f4dffbc6267578742022058f167bdd1ffdb2..fc7b8db85942b223effa5a1c2f980a728497ce09 100644
--- a/source/Lib/EncoderLib/EncGOP.cpp
+++ b/source/Lib/EncoderLib/EncGOP.cpp
@@ -1487,9 +1487,9 @@ validateMinCrRequirements(const ProfileTierLevelFeatures &plt, std::size_t numBy
   {
     const uint32_t formatCapabilityFactorx1000 = plt.getProfileFeatures()->formatCapabilityFactorx1000;
     const uint64_t maxLumaSr = plt.getTierLevelFeatures()->maxLumaSr;
-    const uint32_t frameRate = pCfg->getFrameRate();
+    const double   frameRate                   = pCfg->getFrameRate().getFloatVal();
     const double   minCr = plt.getMinCr();
-    const double   denominator = (minCr * frameRate * 1000);
+    const double   denominator                 = minCr * frameRate * 1000;
     if (denominator!=0)
     {
       const double   threshold =(formatCapabilityFactorx1000 * maxLumaSr) / (denominator);
@@ -1512,7 +1512,7 @@ validateMinCrRequirements(const ProfileTierLevelFeatures &plt, std::size_t numBy
     {
       const uint32_t formatCapabilityFactorx1000 = plt.getProfileFeatures()->formatCapabilityFactorx1000;
       const uint64_t maxLumaSr = plt.getTierLevelFeatures()->maxLumaSr;
-      const double   denomx1000x256 = (256 * plt.getMinCr() * pCfg->getFrameRate() * 1000 * 256);
+      const double   denomx1000x256 = 256 * plt.getMinCr() * pCfg->getFrameRate().getFloatVal() * 1000 * 256;
 
       for (int i = 0; i < seiSubpic.m_numRefLevels; i++)
       {
@@ -2549,7 +2549,8 @@ void EncGOP::compressGOP(int pocLast, int numPicRcvd, PicList &rcListPic, std::l
       setNewestBgPOC(pocCurr);
       setLastLTRefPoc(pocCurr);
     }
-    else if (m_pcCfg->getUseCompositeRef() && getLastLTRefPoc() >= 0 && getEncodedLTRef()==false && !getPicBg()->getSpliceFull() && (pocCurr - getLastLTRefPoc()) > (m_pcCfg->getFrameRate() * 2))
+    else if (m_pcCfg->getUseCompositeRef() && getLastLTRefPoc() >= 0 && getEncodedLTRef() == false
+             && !getPicBg()->getSpliceFull() && pocCurr - getLastLTRefPoc() > m_pcCfg->getFrameRate().getFloatVal() * 2)
     {
       setUseLTRef(false);
       setPrepareLTRef(false);
@@ -3824,7 +3825,9 @@ void EncGOP::compressGOP(int pocLast, int numPicRcvd, PicList &rcListPic, std::l
 
     if (m_pcCfg->getFilmGrainAnalysisEnabled())
     {
-      int  filteredFrame    = m_pcCfg->getIntraPeriod() < 1 ? 2 * m_pcCfg->getFrameRate() : m_pcCfg->getIntraPeriod();
+      int  filteredFrame    = m_pcCfg->getIntraPeriod() < 1
+                                ? static_cast<int>(2 * m_pcCfg->getFrameRate().getFloatVal() + 0.5)
+                                : m_pcCfg->getIntraPeriod();
       bool readyToAnalyze   = pcPic->getPOC() % filteredFrame
                                 ? false
                                 : true;   // either it is mctf denoising or external source for film grain analysis. note:
@@ -4330,8 +4333,9 @@ void EncGOP::compressGOP(int pocLast, int numPicRcvd, PicList &rcListPic, std::l
         seiGreenMetadataInfo->m_greenMetadataGranularityType = m_pcCfg->getSEIGreenMetadataGranularityType();
         seiGreenMetadataInfo->m_greenMetadataExtendedRepresentation = m_pcCfg->getSEIGreenMetadataExtendedRepresentation();
         int64_t codedFrames = m_featureCounter.iSlices + m_featureCounter.bSlices + m_featureCounter.pSlices;
-        int numberFrames = seiGreenMetadataInfo->m_numSeconds * m_pcCfg->getFrameRate();
-    
+        int     numberFrames =
+          static_cast<int>(seiGreenMetadataInfo->m_numSeconds * m_pcCfg->getFrameRate().getFloatVal() + 0.5);
+
         if (seiGreenMetadataInfo->m_greenMetadataType == 0)
         {
           switch (m_pcCfg->getSEIGreenMetadataPeriodType()) // Period type
@@ -4356,7 +4360,7 @@ void EncGOP::compressGOP(int pocLast, int numPicRcvd, PicList &rcListPic, std::l
             seiGreenMetadataInfo->m_numSeconds = m_pcCfg->getSEIGreenMetadataPeriodNumSeconds();
             if( ((codedFrames% numberFrames) == 0) || (codedFrames == m_pcCfg->getFramesToBeEncoded()))
             {
-              seiGreenMetadataInfo->m_numSeconds = int(floor(double(codedFrames)/double(m_pcCfg->getFrameRate())));
+              seiGreenMetadataInfo->m_numSeconds = int(floor(codedFrames / m_pcCfg->getFrameRate().getFloatVal()));
               xCalculateGreenComplexityMetrics(m_featureCounter, m_featureCounterReference, seiGreenMetadataInfo);
               m_seiEncoder.initSEIGreenMetadataInfo(seiGreenMetadataInfo,  m_featureCounter, m_SEIGreenQualityMetrics,m_SEIGreenComplexityMetrics);
               leadingSeiMessages.push_back(seiGreenMetadataInfo);
@@ -4524,7 +4528,9 @@ void EncGOP::printOutSummary(uint32_t numAllPicCoded, bool isField, const bool p
     CHECK(!(numAllPicCoded == m_gcAnalyzeAll.getNumPic()), "Unspecified error");
   }
 
-  const double picRate = m_pcCfg->getFrameRate() * (isField ? 2.0 : 1.0) / m_pcCfg->getTemporalSubsampleRatio();
+  Fraction picRate = m_pcCfg->getFrameRate();
+  picRate.num *= isField ? 2 : 1;
+  picRate.den *= m_pcCfg->getTemporalSubsampleRatio();
 
   m_gcAnalyzeAll.setFrameRate(picRate);
   m_gcAnalyzeI.setFrameRate(picRate);
@@ -4607,7 +4613,9 @@ void EncGOP::printOutSummary(uint32_t numAllPicCoded, bool isField, const bool p
   if(isField)
   {
     //-- interlaced summary
-    m_gcAnalyzeAllField.setFrameRate(m_pcCfg->getFrameRate() / (double) m_pcCfg->getTemporalSubsampleRatio());
+    Fraction frameRate = m_pcCfg->getFrameRate();
+    frameRate.den *= m_pcCfg->getTemporalSubsampleRatio();
+    m_gcAnalyzeAllField.setFrameRate(frameRate);
     m_gcAnalyzeAllField.setBits(m_gcAnalyzeAll.getBits());
     // prior to the above statement, the interlace analyser does not contain the correct total number of bits.
     id="a";
@@ -5645,7 +5653,8 @@ void EncGOP::xCalculateGreenComplexityMetrics( FeatureCounterStruct featureCount
   seiGreenMetadataInfo->m_greenComplexityMetrics.portionSaoInstances = int(floor( 255.0 * numSaoFilteredBlocks / totalNum4BlocksInPeriod));
   seiGreenMetadataInfo->m_greenComplexityMetrics.portionAlfInstances = int(floor( 255.0 * numAlfFilteredBlocks / totalNum4BlocksInPeriod));
   seiGreenMetadataInfo->m_numPictures = int(featureCounterDifference.iSlices + featureCounterDifference.bSlices +featureCounterDifference.pSlices);
-  seiGreenMetadataInfo->m_numSeconds = int(floor(double(seiGreenMetadataInfo->m_numPictures) / double(m_pcCfg->getFrameRate())));
+  seiGreenMetadataInfo->m_numSeconds =
+    int(floor(seiGreenMetadataInfo->m_numPictures / m_pcCfg->getFrameRateScale().getFloatVal()));
 }
 #endif
 
diff --git a/source/Lib/EncoderLib/EncHRD.cpp b/source/Lib/EncoderLib/EncHRD.cpp
index d49cd4fc373b4c6fd6e47657e35ec7b350f51e7b..6076b91209464dd3961072a49f5de3f7fe3950a9 100644
--- a/source/Lib/EncoderLib/EncHRD.cpp
+++ b/source/Lib/EncoderLib/EncHRD.cpp
@@ -31,6 +31,8 @@
 * THE POSSIBILITY OF SUCH DAMAGE.
 */
 
+#include <numeric>
+
 #include "EncHRD.h"
 
 
@@ -55,54 +57,40 @@ int EncHRD::xCalcScale(int x)
 
 void EncHRD::initHRDParameters(EncCfg* encCfg)
 {
-  bool useSubCpbParams = encCfg->getNoPicPartitionFlag() == false;
-  int  bitRate = encCfg->getTargetBitrate();
-  int cpbSize = encCfg->getCpbSize();
-  CHECK(!(cpbSize != 0), "Unspecified error");  // CPB size may not be equal to zero. ToDo: have a better default and check for level constraints
   if (!encCfg->getHrdParametersPresentFlag() && !encCfg->getCpbSaturationEnabled())
   {
     return;
   }
 
-  switch (encCfg->getFrameRate())
-  {
-  case 24:
-    m_generalHrdParams.setNumUnitsInTick(1125000);    m_generalHrdParams.setTimeScale(27000000);
-    break;
-  case 25:
-    m_generalHrdParams.setNumUnitsInTick(1080000);    m_generalHrdParams.setTimeScale(27000000);
-    break;
-  case 30:
-    m_generalHrdParams.setNumUnitsInTick(900900);     m_generalHrdParams.setTimeScale(27000000);
-    break;
-  case 50:
-    m_generalHrdParams.setNumUnitsInTick(540000);     m_generalHrdParams.setTimeScale(27000000);
-    break;
-  case 60:
-    m_generalHrdParams.setNumUnitsInTick(450450);     m_generalHrdParams.setTimeScale(27000000);
-    break;
-  default:
-    m_generalHrdParams.setNumUnitsInTick(1001);       m_generalHrdParams.setTimeScale(60000);
-    break;
-  }
+  int numUnitsInTick = encCfg->getFrameRate().den * encCfg->getTemporalSubsampleRatio();
+  int timeScale      = encCfg->getFrameRate().num;
 
-  if (encCfg->getTemporalSubsampleRatio() > 1)
+  constexpr int DEFAULT_TICKS = 27000000;   // Default 27 Mhz clock
+  if (numUnitsInTick < timeScale && std::gcd(DEFAULT_TICKS, timeScale) == timeScale)
   {
-    uint32_t temporalSubsampleRatio = encCfg->getTemporalSubsampleRatio();
-    if (double(m_generalHrdParams.getNumUnitsInTick()) * temporalSubsampleRatio > std::numeric_limits<uint32_t>::max())
-    {
-      m_generalHrdParams.setTimeScale(m_generalHrdParams.getTimeScale() / temporalSubsampleRatio);
-    }
-    else
-    {
-      m_generalHrdParams.setNumUnitsInTick(m_generalHrdParams.getNumUnitsInTick() * temporalSubsampleRatio);
-    }
+    // Use default clock if >1 fps and no loss of precision
+    numUnitsInTick *= DEFAULT_TICKS / timeScale;
+    timeScale = DEFAULT_TICKS;
   }
-  bool rateCnt = (bitRate > 0);
+  m_generalHrdParams.setNumUnitsInTick(numUnitsInTick);
+  m_generalHrdParams.setTimeScale(timeScale);
 
+  const int bitRate = encCfg->getTargetBitrate();
+
+  const bool rateCnt = (bitRate > 0);
   m_generalHrdParams.setGeneralNalHrdParametersPresentFlag(rateCnt);
   m_generalHrdParams.setGeneralVclHrdParametersPresentFlag(rateCnt);
 
+  if (!rateCnt)
+  {
+    return;
+  }
+
+  int cpbSize = encCfg->getCpbSize();
+  CHECK(!(cpbSize != 0), "Unspecified error");   // CPB size may not be equal to zero. ToDo: have a better default and
+                                                 // check for level constraints
+  bool useSubCpbParams = encCfg->getNoPicPartitionFlag() == false;
+
   m_generalHrdParams.setGeneralSamePicTimingInAllOlsFlag(encCfg->getSamePicTimingInAllOLS());
   useSubCpbParams &= (m_generalHrdParams.getGeneralNalHrdParametersPresentFlag() || m_generalHrdParams.getGeneralVclHrdParametersPresentFlag());
   m_generalHrdParams.setGeneralDecodingUnitHrdParamsPresentFlag(useSubCpbParams);
@@ -176,4 +164,3 @@ void EncHRD::initHRDParameters(EncCfg* encCfg)
     }
   }
 }
-
diff --git a/source/Lib/EncoderLib/EncLib.cpp b/source/Lib/EncoderLib/EncLib.cpp
index 6afc28b8e2e1a1eed82619e15d30e5b28677ae2b..346b60f1f84eccc0124afe5f2bfd02d72edea174 100644
--- a/source/Lib/EncoderLib/EncLib.cpp
+++ b/source/Lib/EncoderLib/EncLib.cpp
@@ -118,9 +118,10 @@ void EncLib::create( const int layerId )
   }
   if ( m_RCEnableRateControl )
   {
-    m_cRateCtrl.init(m_framesToBeEncoded, m_RCTargetBitrate,
-                     (int) ((double) m_frameRate / m_temporalSubsampleRatio + 0.5), m_gopSize, m_intraPeriod,
-                     m_sourceWidth, m_sourceHeight, m_maxCUWidth, m_maxCUHeight, getBitDepth(ChannelType::LUMA),
+    Fraction frameRate = m_frameRate;
+    frameRate.den *= m_temporalSubsampleRatio;
+    m_cRateCtrl.init(m_framesToBeEncoded, m_RCTargetBitrate, frameRate, m_gopSize, m_intraPeriod, m_sourceWidth,
+                     m_sourceHeight, m_maxCUWidth, m_maxCUHeight, getBitDepth(ChannelType::LUMA),
                      m_RCKeepHierarchicalBit, m_RCUseLCUSeparateModel, m_GOPList);
   }
 
@@ -1635,6 +1636,11 @@ void EncLib::xInitSPS( SPS& sps )
   sps.setChromaQpMappingTableFromParams(m_chromaQpMappingTableParams, sps.getQpBDOffset(ChannelType::CHROMA));
   sps.deriveChromaQPMappingTables();
 
+  if (m_hrdParametersPresentFlag)
+  {
+    sps.setGeneralHrdParametersPresentFlag(true);
+    xInitHrdParameters(sps);
+  }
   if( getPictureTimingSEIEnabled() || getDecodingUnitInfoSEIEnabled() || getCpbSaturationEnabled() )
   {
     xInitHrdParameters(sps);
diff --git a/source/Lib/EncoderLib/RateCtrl.cpp b/source/Lib/EncoderLib/RateCtrl.cpp
index a1a69679b71d6d801fb6795dd8add68787406cca..010c7840af4497bcdc3d5231ff6bd70b3f25c59b 100644
--- a/source/Lib/EncoderLib/RateCtrl.cpp
+++ b/source/Lib/EncoderLib/RateCtrl.cpp
@@ -46,7 +46,6 @@ EncRCSeq::EncRCSeq()
 {
   m_totalFrames         = 0;
   m_targetRate          = 0;
-  m_frameRate           = 0;
   m_targetBits          = 0;
   m_GOPSize             = 0;
   m_intraPeriod         = 0;
@@ -75,7 +74,9 @@ EncRCSeq::~EncRCSeq()
   destroy();
 }
 
-void EncRCSeq::create(int totalFrames, int targetBitrate, int frameRate, int GOPSize, int intraPeriod, int picWidth, int picHeight, int LCUWidth, int LCUHeight, int numberOfLevel, bool useLCUSeparateModel, int adaptiveBit)
+void EncRCSeq::create(int totalFrames, int targetBitrate, const Fraction& frameRate, int GOPSize, int intraPeriod,
+                      int picWidth, int picHeight, int LCUWidth, int LCUHeight, int numberOfLevel,
+                      bool useLCUSeparateModel, int adaptiveBit)
 {
   destroy();
   m_totalFrames         = totalFrames;
@@ -91,8 +92,9 @@ void EncRCSeq::create(int totalFrames, int targetBitrate, int frameRate, int GOP
   m_useLCUSeparateModel = useLCUSeparateModel;
 
   m_numberOfPixel   = m_picWidth * m_picHeight;
-  m_targetBits      = (int64_t)m_totalFrames * (int64_t)m_targetRate / (int64_t)m_frameRate;
-  m_seqTargetBpp = (double)m_targetRate / (double)m_frameRate / (double)m_numberOfPixel;
+  m_targetBits =
+    (int64_t) m_totalFrames * (int64_t) m_targetRate * (int64_t) m_frameRate.den / (int64_t) m_frameRate.num;
+  m_seqTargetBpp = (double) m_targetRate / m_frameRate.getFloatVal() / (double) m_numberOfPixel;
   if ( m_seqTargetBpp < 0.03 )
   {
     m_alphaUpdate = 0.01;
@@ -1451,7 +1453,9 @@ void RateCtrl::destroy()
   }
 }
 
-void RateCtrl::init(int totalFrames, int targetBitrate, int frameRate, int GOPSize, int intraPeriod, int picWidth, int picHeight, int LCUWidth, int LCUHeight, int bitDepth, int keepHierBits, bool useLCUSeparateModel, GOPEntry  GOPList[MAX_GOP])
+void RateCtrl::init(int totalFrames, int targetBitrate, const Fraction& frameRate, int GOPSize, int intraPeriod,
+                    int picWidth, int picHeight, int LCUWidth, int LCUHeight, int bitDepth, int keepHierBits,
+                    bool useLCUSeparateModel, GOPEntry GOPList[MAX_GOP])
 {
   destroy();
 
@@ -1488,7 +1492,7 @@ void RateCtrl::init(int totalFrames, int targetBitrate, int frameRate, int GOPSi
 
   if ( keepHierBits > 0 )
   {
-    double bpp = (double)( targetBitrate / (double)( frameRate*picWidth*picHeight ) );
+    double bpp = (double) (targetBitrate / (frameRate.getFloatVal() * picWidth * picHeight));
     if ( GOPSize == 4 && isLowdelay )
     {
       if ( bpp > 0.2 )
@@ -1896,7 +1900,7 @@ void RateCtrl::init(int totalFrames, int targetBitrate, int frameRate, int GOPSi
   m_CpbSaturationEnabled = false;
   m_cpbSize              = targetBitrate;
   m_cpbState             = (uint32_t)(m_cpbSize*0.5f);
-  m_bufferingRate        = (int)(targetBitrate / frameRate);
+  m_bufferingRate        = (int) (targetBitrate * frameRate.den / frameRate.num);
 
   delete[] bitsRatio;
   delete[] GOPID2Level;
@@ -1934,12 +1938,14 @@ int  RateCtrl::updateCpbState(int actualBits)
   return cpbState;
 }
 
-void RateCtrl::initHrdParam(const GeneralHrdParams* generalHrd, const OlsHrdParams* olsHrd, int iFrameRate, double fInitialCpbFullness)
+void RateCtrl::initHrdParam(const GeneralHrdParams* generalHrd, const OlsHrdParams* olsHrd, const Fraction& frameRate,
+                            double fInitialCpbFullness)
 {
   m_CpbSaturationEnabled = true;
   m_cpbSize = (olsHrd->getCpbSizeValueMinus1(0, 0) + 1) << (4 + generalHrd->getCpbSizeScale());
   m_cpbState = (uint32_t)(m_cpbSize*fInitialCpbFullness);
-  m_bufferingRate = (uint32_t)(((olsHrd->getBitRateValueMinus1(0, 0) + 1) << (6 + generalHrd->getBitRateScale())) / iFrameRate);
+  m_bufferingRate = (uint32_t) (((olsHrd->getBitRateValueMinus1(0, 0) + 1) << (6 + generalHrd->getBitRateScale()))
+                                * frameRate.den / frameRate.num);
   msg(NOTICE, "\nHRD - [Initial CPB state %6d] [CPB Size %6d] [Buffering Rate %6d]\n", m_cpbState, m_cpbSize, m_bufferingRate);
 }
 
diff --git a/source/Lib/EncoderLib/RateCtrl.h b/source/Lib/EncoderLib/RateCtrl.h
index 0ed7b6c625c528d311a8bb3674482552178c2b27..a47bae2de34a80c7c3e0901cdc60e5be22de9bf6 100644
--- a/source/Lib/EncoderLib/RateCtrl.h
+++ b/source/Lib/EncoderLib/RateCtrl.h
@@ -99,7 +99,8 @@ public:
   ~EncRCSeq();
 
 public:
-  void create(int totalFrames, int targetBitrate, int frameRate, int GOPSize, int intraPeriod, int picWidth, int picHeight, int LCUWidth, int LCUHeight, int numberOfLevel, bool useLCUSeparateModel, int adaptiveBit);
+  void create(int totalFrames, int targetBitrate, const Fraction& frameRate, int GOPSize, int intraPeriod, int picWidth,
+              int picHeight, int LCUWidth, int LCUHeight, int numberOfLevel, bool useLCUSeparateModel, int adaptiveBit);
   void destroy();
   void initBitsRatio( int bitsRatio[] );
   void initGOPID2Level( int GOPID2Level[] );
@@ -111,7 +112,7 @@ public:
 public:
   int  getTotalFrames()                 { return m_totalFrames; }
   int  getTargetRate()                  { return m_targetRate; }
-  int  getFrameRate()                   { return m_frameRate; }
+  const Fraction& getFrameRate() const { return m_frameRate; }
   int  getGOPSize()                     { return m_GOPSize; }
   int  getIntraPeriod()                 { return m_intraPeriod; }
   int  getPicWidth()                    { return m_picWidth; }
@@ -154,7 +155,7 @@ public:
 private:
   int m_totalFrames;
   int m_targetRate;
-  int m_frameRate;
+  Fraction m_frameRate;
   int m_GOPSize;
   int m_intraPeriod;
   int m_picWidth;
@@ -325,7 +326,9 @@ public:
   ~RateCtrl();
 
 public:
-  void init(int totalFrames, int targetBitrate, int frameRate, int GOPSize, int intraPeriod, int picWidth, int picHeight, int LCUWidth, int LCUHeight, int bitDepth, int keepHierBits, bool useLCUSeparateModel, GOPEntry GOPList[MAX_GOP]);
+  void init(int totalFrames, int targetBitrate, const Fraction& frameRate, int GOPSize, int intraPeriod, int picWidth,
+            int picHeight, int LCUWidth, int LCUHeight, int bitDepth, int keepHierBits, bool useLCUSeparateModel,
+            GOPEntry GOPList[MAX_GOP]);
   void destroy();
   void initRCPic( int frameLevel );
   void initRCGOP( int numberOfPictures );
@@ -355,7 +358,8 @@ public:
   uint32_t       getCpbSize()               { return m_cpbSize;        }
   uint32_t       getBufferingRate()         { return m_bufferingRate;  }
   int        updateCpbState(int actualBits);
-  void       initHrdParam(const GeneralHrdParams* generalHrd, const OlsHrdParams* olsHrd, int iFrameRate, double fInitialCpbFullness);
+  void initHrdParam(const GeneralHrdParams* generalHrd, const OlsHrdParams* olsHrd, const Fraction& frameRate,
+                    double fInitialCpbFullness);
 
 private:
   EncRCSeq* m_encRCSeq;
diff --git a/source/Lib/Utilities/VideoIOYuv.cpp b/source/Lib/Utilities/VideoIOYuv.cpp
index 6722a627ce8a88202c37572f2225c098d5baa277..98123631d2b52a47e5c0f9a694102d4b8895d5a6 100644
--- a/source/Lib/Utilities/VideoIOYuv.cpp
+++ b/source/Lib/Utilities/VideoIOYuv.cpp
@@ -171,7 +171,10 @@ void VideoIOYuv::open(const std::string &fileName, bool bWriteMode, const BitDep
     {
       if (m_inY4mFileHeaderLength == 0)
       {
-        int          dummyWidth = 0, dummyHeight = 0, dummyFrameRate = 0, dummyBitDepth = 0;
+        int          dummyWidth        = 0;
+        int          dummyHeight       = 0;
+        Fraction     dummyFrameRate;
+        int          dummyBitDepth     = 0;
         ChromaFormat dummyChromaFormat = ChromaFormat::_420;
         parseY4mFileHeader(fileName, dummyWidth, dummyHeight, dummyFrameRate, dummyBitDepth, dummyChromaFormat);
       }
@@ -192,8 +195,8 @@ void VideoIOYuv::open(const std::string &fileName, bool bWriteMode, const BitDep
   return;
 }
 
-void VideoIOYuv::parseY4mFileHeader(const std::string &fileName, int &width, int &height, int &frameRate, int &bitDepth,
-                               ChromaFormat &chromaFormat)
+void VideoIOYuv::parseY4mFileHeader(const std::string& fileName, int& width, int& height, Fraction& frameRate,
+                                    int& bitDepth, ChromaFormat& chromaFormat)
 {
   m_cHandle.open(fileName.c_str(), std::ios::binary | std::ios::in);
   CHECK(m_cHandle.fail(), "File open failed.")
@@ -260,7 +263,8 @@ void VideoIOYuv::parseY4mFileHeader(const std::string &fileName, int &width, int
       {
         if (denominator != 0)
         {
-          frameRate = (int) (1.0 * numerator / denominator + 0.5);
+          frameRate.num = numerator;
+          frameRate.den = denominator;
         }
       }
       break;
@@ -276,24 +280,24 @@ void VideoIOYuv::parseY4mFileHeader(const std::string &fileName, int &width, int
   m_cHandle.close();
 }
 
-void VideoIOYuv::setOutputY4mInfo(int width, int height, int frameRate, int frameScale, int bitDepth, ChromaFormat chromaFormat)
+void VideoIOYuv::setOutputY4mInfo(int width, int height, const Fraction& frameRate, int bitDepth,
+                                  ChromaFormat chromaFormat)
 {
   m_outPicWidth     = width;
   m_outPicHeight    = height;
   m_outBitDepth     = bitDepth;
   m_outFrameRate    = frameRate;
-  m_outFrameScale   = frameScale;
   m_outChromaFormat = chromaFormat;
 }
 
 void VideoIOYuv::writeY4mFileHeader()
 {
-  CHECK(m_outPicWidth == 0 || m_outPicHeight == 0 || m_outBitDepth == 0 || m_outFrameRate == 0,
+  CHECK(m_outPicWidth == 0 || m_outPicHeight == 0 || m_outBitDepth == 0 || m_outFrameRate.num == 0,
         "Output Y4M file into has not been set");
   std::string header = y4mSignature;
   header += "W" + std::to_string(m_outPicWidth) + " ";
   header += "H" + std::to_string(m_outPicHeight) + " ";
-  header += "F" + std::to_string(m_outFrameRate) + ":" + std::to_string(m_outFrameScale) + " ";
+  header += "F" + std::to_string(m_outFrameRate.num) + ":" + std::to_string(m_outFrameRate.den) + " ";
   header += "Ip A0:0 ";
   switch (m_outChromaFormat)
   {
diff --git a/source/Lib/Utilities/VideoIOYuv.h b/source/Lib/Utilities/VideoIOYuv.h
index 3cdf7f852cbc1d1416a28e5205bace9ce6d44ab8..fc006aeb3d078696604cbed0c6b0b29b3259d644 100644
--- a/source/Lib/Utilities/VideoIOYuv.h
+++ b/source/Lib/Utilities/VideoIOYuv.h
@@ -65,8 +65,7 @@ private:
   int          m_outPicWidth           = 0;
   int          m_outPicHeight          = 0;
   int          m_outBitDepth           = 0;
-  int          m_outFrameRate          = 0;
-  int          m_outFrameScale         = 1;
+  Fraction     m_outFrameRate;
   ChromaFormat m_outChromaFormat       = ChromaFormat::_420;
   bool         m_outY4m                = false;
 
@@ -74,9 +73,9 @@ public:
   VideoIOYuv()           {}
   virtual ~VideoIOYuv()  {}
 
-  void parseY4mFileHeader(const std::string &fileName, int &width, int &height, int &frameRate, int &bitDepth,
-                          ChromaFormat &chromaFormat);
-  void setOutputY4mInfo(int width, int height, int frameRate, int frameScale, int bitDepth, ChromaFormat chromaFormat);
+  void parseY4mFileHeader(const std::string& fileName, int& width, int& height, Fraction& frameRate, int& bitDepth,
+                          ChromaFormat& chromaFormat);
+  void setOutputY4mInfo(int width, int height, const Fraction& frameRate, int bitDepth, ChromaFormat chromaFormat);
   void writeY4mFileHeader();
   void open(const std::string &fileName, bool bWriteMode, const BitDepths &fileBitDepth,
             const BitDepths &MSBExtendedBitDepth,