Index: trunk/src/VBox/Main/src-client/EbmlMkvIDs.h
===================================================================
--- trunk/src/VBox/Main/src-client/EbmlMkvIDs.h	(revision 68447)
+++ trunk/src/VBox/Main/src-client/EbmlMkvIDs.h	(revision 68451)
@@ -70,5 +70,4 @@
     MkvElem_PixelWidth              = 0xB0,
     MkvElem_PixelHeight             = 0xBA,
-    MkvElem_FrameRate               = 0x2383E3,
 
     MkvElem_Audio                   = 0xE1,
Index: trunk/src/VBox/Main/src-client/EbmlWriter.cpp
===================================================================
--- trunk/src/VBox/Main/src-client/EbmlWriter.cpp	(revision 68447)
+++ trunk/src/VBox/Main/src-client/EbmlWriter.cpp	(revision 68451)
@@ -14,4 +14,10 @@
  * VirtualBox OSE distribution. VirtualBox OSE is distributed in the
  * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
+ */
+
+/**
+ * For more information, see:
+ * - https://w3c.github.io/media-source/webm-byte-stream-format.html
+ * - https://www.webmproject.org/docs/container/#muxer-guidelines
  */
 
@@ -309,18 +315,13 @@
 #define VBOX_WEBM_TIMECODESCALE_FACTOR_MS   1000000
 
+/** Maximum time (in ms) a cluster can store. */
+#define VBOX_WEBM_CLUSTER_MAX_LEN_MS        INT16_MAX
+
+/** Maximum time a block can store.
+ *  With signed 16-bit timecodes and a default timecode scale of 1ms per unit this makes 65536ms. */
+#define VBOX_WEBM_BLOCK_MAX_LEN_MS          UINT16_MAX
+
 class WebMWriter_Impl
 {
-    /**
-     * Structure for keeping a cue entry.
-     */
-    struct WebMCueEntry
-    {
-        WebMCueEntry(uint32_t t, uint64_t l)
-            : time(t), loc(l) {}
-
-        uint32_t    time;
-        uint64_t    loc;
-    };
-
     /**
      * Track type enumeration.
@@ -371,5 +372,7 @@
                  *  60           2880
                  */
-                uint16_t csFrame;
+                uint16_t framesPerBlock;
+                /** How many milliseconds (ms) one written (simple) block represents. */
+                uint16_t msPerBlock;
             } Audio;
         };
@@ -389,4 +392,21 @@
 
     /**
+     * Structure for keeping a cue point.
+     */
+    struct WebMCuePoint
+    {
+        WebMCuePoint(WebMTrack *a_pTrack, uint32_t a_tcClusterStart, uint64_t a_offClusterStart)
+            : pTrack(a_pTrack)
+            , tcClusterStart(a_tcClusterStart), offClusterStart(a_offClusterStart) {}
+
+        /** Associated track. */
+        WebMTrack *pTrack;
+        /** Start time code of the related cluster. */
+        uint32_t   tcClusterStart;
+        /** Start offset of the related cluster. */
+        uint64_t   offClusterStart;
+    };
+
+    /**
      * Structure for keeping a WebM cluster entry.
      */
@@ -397,8 +417,7 @@
             , offCluster(0)
             , fOpen(false)
-            , tcStart(0)
-            , tcLast(0)
-            , cBlocks(0)
-            , cbData(0) { }
+            , tcStartMs(0)
+            , tcEndMs(0)
+            , cBlocks(0) { }
 
         /** This cluster's ID. */
@@ -409,12 +428,10 @@
         /** Whether this cluster element is opened currently. */
         bool          fOpen;
-        /** Timecode when starting this cluster. */
-        uint64_t      tcStart;
-        /** Timecode when this cluster was last touched. */
-        uint64_t      tcLast;
+        /** Timecode (in ms) when starting this cluster. */
+        uint64_t      tcStartMs;
+        /** Timecode (in ms) when this cluster ends. */
+        uint64_t      tcEndMs;
         /** Number of (simple) blocks in this cluster. */
         uint64_t      cBlocks;
-        /** Size (in bytes) of data already written. */
-        uint64_t      cbData;
     };
 
@@ -459,5 +476,5 @@
         uint64_t                        offCues;
         /** List of cue points. Needed for seeking table. */
-        std::list<WebMCueEntry>         lstCues;
+        std::list<WebMCuePoint>         lstCues;
 
         /** Map of tracks.
@@ -574,18 +591,22 @@
             return rc;
 
+        const uint8_t uTrack = (uint8_t)CurSeg.mapTracks.size();
+
         m_Ebml.subStart(MkvElem_TrackEntry);
-        m_Ebml.serializeUnsignedInteger(MkvElem_TrackNumber, (uint8_t)CurSeg.mapTracks.size());
-        /** @todo Implement track's "Language" property? Currently this defaults to English ("eng"). */
-
-        uint8_t uTrack = (uint8_t)CurSeg.mapTracks.size();
+
+        m_Ebml.serializeUnsignedInteger(MkvElem_TrackNumber, (uint8_t)uTrack);
+        m_Ebml.serializeString         (MkvElem_Language,    "und" /* "Undefined"; see ISO-639-2. */);
+        m_Ebml.serializeUnsignedInteger(MkvElem_FlagLacing,  (uint8_t)0);
 
         WebMTrack *pTrack = new WebMTrack(WebMTrackType_Audio, uTrack, RTFileTell(m_Ebml.getFile()));
 
-        pTrack->Audio.uHz     = uHz;
-        pTrack->Audio.csFrame = pTrack->Audio.uHz / (1000 /* s in ms */ / 20 /* ms */); /** @todo 20 ms of audio data. Make this configurable? */
+        pTrack->Audio.uHz            = uHz;
+        pTrack->Audio.msPerBlock     = 20; /** Opus uses 20ms by default. Make this configurable? */
+        pTrack->Audio.framesPerBlock = uHz / (1000 /* s in ms */ / pTrack->Audio.msPerBlock);
 
         OpusPrivData opusPrivData(uHz, cChannels);
 
-        LogFunc(("Opus @ %RU16Hz (Frame size is %RU16 samples / channel))\n", pTrack->Audio.uHz, pTrack->Audio.csFrame));
+        LogFunc(("Opus @ %RU16Hz (%RU16ms + %RU16 frames per block)\n",
+                 pTrack->Audio.uHz, pTrack->Audio.msPerBlock, pTrack->Audio.framesPerBlock));
 
         m_Ebml.serializeUnsignedInteger(MkvElem_TrackUID,     pTrack->uUUID, 4)
@@ -617,8 +638,13 @@
     {
 #ifdef VBOX_WITH_LIBVPX
+        RT_NOREF(dbFPS);
+
+        const uint8_t uTrack = (uint8_t)CurSeg.mapTracks.size();
+
         m_Ebml.subStart(MkvElem_TrackEntry);
-        m_Ebml.serializeUnsignedInteger(MkvElem_TrackNumber, (uint8_t)CurSeg.mapTracks.size());
-
-        uint8_t uTrack = (uint8_t)CurSeg.mapTracks.size();
+
+        m_Ebml.serializeUnsignedInteger(MkvElem_TrackNumber, (uint8_t)uTrack);
+        m_Ebml.serializeString         (MkvElem_Language,    "und" /* "Undefined"; see ISO-639-2. */);
+        m_Ebml.serializeUnsignedInteger(MkvElem_FlagLacing,  (uint8_t)0);
 
         WebMTrack *pTrack = new WebMTrack(WebMTrackType_Video, uTrack, RTFileTell(m_Ebml.getFile()));
@@ -629,9 +655,9 @@
               .serializeString(MkvElem_CodecID,              "V_VP8")
               .subStart(MkvElem_Video)
-              .serializeUnsignedInteger(MkvElem_PixelWidth,  uWidth)
-              .serializeUnsignedInteger(MkvElem_PixelHeight, uHeight)
-              .serializeFloat(MkvElem_FrameRate,             dbFPS)
-              .subEnd(MkvElem_Video)
-              .subEnd(MkvElem_TrackEntry);
+                  .serializeUnsignedInteger(MkvElem_PixelWidth,  uWidth)
+                  .serializeUnsignedInteger(MkvElem_PixelHeight, uHeight)
+              .subEnd(MkvElem_Video);
+
+        m_Ebml.subEnd(MkvElem_TrackEntry);
 
         CurSeg.mapTracks[uTrack] = pTrack;
@@ -713,27 +739,29 @@
         RT_NOREF(a_pTrack);
 
-        /* Calculate the PTS of this frame in milliseconds. */
-        uint64_t tcPTS = a_pPkt->data.frame.pts * 1000
+        WebMCluster &Cluster = CurSeg.CurCluster;
+
+        /* Calculate the PTS of this frame (in ms). */
+        uint64_t tcPTSMs = a_pPkt->data.frame.pts * 1000
                          * (uint64_t) a_pCfg->g_timebase.num / a_pCfg->g_timebase.den;
 
-        if (tcPTS <= CurSeg.CurCluster.tcLast)
-            tcPTS = CurSeg.CurCluster.tcLast + 1;
-
-        CurSeg.CurCluster.tcLast = tcPTS;
-
-        if (CurSeg.CurCluster.tcStart == UINT64_MAX)
-            CurSeg.CurCluster.tcStart = CurSeg.CurCluster.tcLast;
-
-        /* Calculate the relative time of this block. */
-        uint16_t tcBlock       = 0;
-        bool     fClusterStart = false;
-
-        /* Did we reach the maximum our timecode can hold? Use a new cluster then. */
-        if (tcPTS - CurSeg.CurCluster.tcStart > m_uTimecodeMax)
+        if (   tcPTSMs
+            && tcPTSMs <= CurSeg.CurCluster.tcEndMs)
+        {
+            tcPTSMs = CurSeg.CurCluster.tcEndMs + 1;
+        }
+
+        /* Whether to start a new cluster or not. */
+        bool fClusterStart = false;
+
+        /* No blocks written yet? Start a new cluster. */
+        if (a_pTrack->cTotalBlocks == 0)
             fClusterStart = true;
-        else
-        {
-            /* Calculate the block's timecode, which is relative to the current cluster's starting timecode. */
-            tcBlock = static_cast<uint16_t>(tcPTS - CurSeg.CurCluster.tcStart);
+
+        /* Did we reach the maximum a cluster can hold? Use a new cluster then. */
+        if (tcPTSMs > VBOX_WEBM_CLUSTER_MAX_LEN_MS)
+        {
+            tcPTSMs = 0;
+
+            fClusterStart = true;
         }
 
@@ -743,8 +771,4 @@
             || fKeyframe)
         {
-            WebMCluster &Cluster = CurSeg.CurCluster;
-
-            a_pTrack->cTotalClusters++;
-
             if (Cluster.fOpen) /* Close current cluster first. */
             {
@@ -753,27 +777,36 @@
             }
 
-            tcBlock = 0;
-
-            /* Open a new cluster. */
             Cluster.fOpen      = true;
-            Cluster.tcStart    = tcPTS;
+            Cluster.uID        = a_pTrack->cTotalClusters;
+            Cluster.tcStartMs  = tcPTSMs;
             Cluster.offCluster = RTFileTell(m_Ebml.getFile());
             Cluster.cBlocks    = 0;
-            Cluster.cbData     = 0;
-
-            LogFunc(("[C%RU64] Start @ tc=%RU64 off=%RU64\n", a_pTrack->cTotalClusters, Cluster.tcStart, Cluster.offCluster));
+
+            LogFunc(("[T%RU8C%RU64] Start @ %RU64ms / %RU64 bytes\n",
+                     a_pTrack->uTrack, Cluster.uID, Cluster.tcStartMs, Cluster.offCluster));
 
             m_Ebml.subStart(MkvElem_Cluster)
-                  .serializeUnsignedInteger(MkvElem_Timecode, Cluster.tcStart);
+                  .serializeUnsignedInteger(MkvElem_Timecode, Cluster.tcStartMs);
 
             /* Save a cue point if this is a keyframe. */
             if (fKeyframe)
             {
-                WebMCueEntry cue(Cluster.tcStart, Cluster.offCluster);
+                WebMCuePoint cue(a_pTrack, Cluster.tcStartMs, Cluster.offCluster);
                 CurSeg.lstCues.push_back(cue);
             }
-        }
-
-        LogFunc(("tcPTS=%RU64, s=%RU64, e=%RU64\n", tcPTS, CurSeg.CurCluster.tcStart, CurSeg.CurCluster.tcLast));
+
+            a_pTrack->cTotalClusters++;
+        }
+
+        Cluster.tcEndMs = tcPTSMs;
+        Cluster.cBlocks++;
+
+        /* Calculate the block's timecode, which is relative to the cluster's starting timecode. */
+        uint16_t tcBlockMs = static_cast<uint16_t>(tcPTSMs - Cluster.tcStartMs);
+
+        Log2Func(("tcPTSMs=%RU64, tcBlockMs=%RU64\n", tcPTSMs, tcBlockMs));
+
+        Log2Func(("[C%RU64] cBlocks=%RU64, tcStartMs=%RU64, tcEndMs=%RU64 (%zums)\n",
+                  Cluster.uID, Cluster.cBlocks, Cluster.tcStartMs, Cluster.tcEndMs, Cluster.tcEndMs - Cluster.tcStartMs));
 
         uint8_t fFlags = 0;
@@ -783,5 +816,5 @@
             fFlags |= VBOX_WEBM_BLOCK_FLAG_INVISIBLE;
 
-        return writeSimpleBlockInternal(a_pTrack, tcBlock, a_pPkt->data.frame.buf, a_pPkt->data.frame.sz, fFlags);
+        return writeSimpleBlockInternal(a_pTrack, tcBlockMs, a_pPkt->data.frame.buf, a_pPkt->data.frame.sz, fFlags);
     }
 #endif /* VBOX_WITH_LIBVPX */
@@ -792,6 +825,6 @@
     {
         AssertPtrReturn(a_pTrack, VERR_INVALID_POINTER);
-        AssertPtrReturn(pvData, VERR_INVALID_POINTER);
-        AssertReturn(cbData, VERR_INVALID_PARAMETER);
+        AssertPtrReturn(pvData,   VERR_INVALID_POINTER);
+        AssertReturn(cbData,      VERR_INVALID_PARAMETER);
 
         RT_NOREF(uTimeStampMs);
@@ -799,38 +832,24 @@
         WebMCluster &Cluster = CurSeg.CurCluster;
 
-        /* Calculate the PTS. */
-        /* Make sure to round the result. This is very important! */
-        uint64_t tcPTSRaw = lround((CurSeg.uTimecodeScaleFactor * 1000 * Cluster.cbData) / a_pTrack->Audio.uHz);
-
-        /* Calculate the absolute PTS. */
-        uint64_t tcPTS = lround(tcPTSRaw / CurSeg.uTimecodeScaleFactor);
-
-        if (Cluster.tcStart == UINT64_MAX)
-            Cluster.tcStart = tcPTS;
-
-        Cluster.tcLast = tcPTS;
-
-        uint16_t tcBlock;
-        bool     fClusterStart = false;
+        /* Calculate the PTS of the current block:
+         *
+         * The "raw PTS" is the exact time of an object represented in nanoseconds):
+         *     Raw Timecode = (Block timecode + Cluster timecode) * TimecodeScaleFactor
+         */
+        uint64_t tcPTSMs  = Cluster.tcStartMs + (Cluster.cBlocks * 20 /*ms */);
+
+        /* Whether to start a new cluster or not. */
+        bool fClusterStart = false;
 
         if (a_pTrack->cTotalBlocks == 0)
             fClusterStart = true;
 
-        /* Did we reach the maximum our timecode can hold? Use a new cluster then. */
-        if (tcPTS - Cluster.tcStart > m_uTimecodeMax)
-        {
-            tcBlock = 0;
-
+        /* Did we reach the maximum a cluster can hold? Use a new cluster then. */
+        if (tcPTSMs > VBOX_WEBM_CLUSTER_MAX_LEN_MS)
             fClusterStart = true;
-        }
-        else
-        {
-            /* Calculate the block's timecode, which is relative to the Cluster timecode. */
-            tcBlock = static_cast<uint16_t>(tcPTS - Cluster.tcStart);
-        }
 
         if (fClusterStart)
         {
-            if (Cluster.fOpen) /* Close current cluster first. */
+            if (Cluster.fOpen) /* Close current clusters first. */
             {
                 m_Ebml.subEnd(MkvElem_Cluster);
@@ -838,29 +857,36 @@
             }
 
-            tcBlock = 0;
-
             Cluster.fOpen      = true;
             Cluster.uID        = a_pTrack->cTotalClusters;
-            Cluster.tcStart    = tcPTS;
-            Cluster.tcLast     = Cluster.tcStart;
+            Cluster.tcStartMs  = Cluster.tcEndMs;
             Cluster.offCluster = RTFileTell(m_Ebml.getFile());
             Cluster.cBlocks    = 0;
-            Cluster.cbData     = 0;
-
-            LogFunc(("[C%RU64] Start @ tc=%RU64 off=%RU64\n", Cluster.uID, Cluster.tcStart, Cluster.offCluster));
+
+            LogFunc(("[T%RU8C%RU64] Start @ %RU64ms / %RU64 bytes\n",
+                     a_pTrack->uTrack, Cluster.uID, Cluster.tcStartMs, Cluster.offCluster));
+
+            /* As all audio frame as key frames, insert a new cue point when a new cluster starts. */
+            WebMCuePoint cue(a_pTrack, Cluster.tcStartMs, Cluster.offCluster);
+            CurSeg.lstCues.push_back(cue);
 
             m_Ebml.subStart(MkvElem_Cluster)
-                  .serializeUnsignedInteger(MkvElem_Timecode, Cluster.tcStart);
+                  .serializeUnsignedInteger(MkvElem_Timecode, Cluster.tcStartMs);
 
             a_pTrack->cTotalClusters++;
         }
 
-        Cluster.cBlocks += 1;
-        Cluster.cbData  += cbData;
-
-        Log2Func(("[C%RU64] tcBlock=%RU64, tcClusterStart=%RU64, tcClusterLast=%RU64, cbData=%zu\n",
-                  Cluster.uID, tcBlock, CurSeg.CurCluster.tcStart, CurSeg.CurCluster.tcLast, CurSeg.CurCluster.cbData));
-
-        return writeSimpleBlockInternal(a_pTrack, tcBlock, pvData, cbData, VBOX_WEBM_BLOCK_FLAG_KEY_FRAME);
+        Cluster.tcEndMs = tcPTSMs;
+        Cluster.cBlocks++;
+
+        /* Calculate the block's timecode, which is relative to the cluster's starting timecode. */
+        uint16_t tcBlockMs = static_cast<uint16_t>(tcPTSMs - Cluster.tcStartMs);
+
+        Log2Func(("tcPTSMs=%RU64, tcBlockMs=%RU64\n", tcPTSMs, tcBlockMs));
+
+        Log2Func(("[C%RU64] cBlocks=%RU64, tcStartMs=%RU64, tcEndMs=%RU64 (%zums)\n",
+                  Cluster.uID, Cluster.cBlocks, Cluster.tcStartMs, Cluster.tcEndMs, Cluster.tcEndMs - Cluster.tcStartMs));
+
+        return writeSimpleBlockInternal(a_pTrack, tcBlockMs,
+                                        pvData, cbData, VBOX_WEBM_BLOCK_FLAG_KEY_FRAME);
     }
 #endif /* VBOX_WITH_LIBOPUS */
@@ -943,31 +969,33 @@
          * Write Cues element.
          */
-        if (CurSeg.lstCues.size())
-        {
-            LogFunc(("Cues @ %RU64\n", RTFileTell(m_Ebml.getFile())));
-
-            CurSeg.offCues = RTFileTell(m_Ebml.getFile());
-
-            m_Ebml.subStart(MkvElem_Cues);
-
-            std::list<WebMCueEntry>::iterator itCuePoint = CurSeg.lstCues.begin();
-            while (itCuePoint != CurSeg.lstCues.end())
-            {
-                LogFunc(("CuePoint @ %RU64\n", RTFileTell(m_Ebml.getFile())));
-
-                m_Ebml.subStart(MkvElem_CuePoint)
-                      .serializeUnsignedInteger(MkvElem_CueTime, itCuePoint->time)
+        LogFunc(("Cues @ %RU64\n", RTFileTell(m_Ebml.getFile())));
+
+        CurSeg.offCues = RTFileTell(m_Ebml.getFile());
+
+        m_Ebml.subStart(MkvElem_Cues);
+
+        std::list<WebMCuePoint>::iterator itCuePoint = CurSeg.lstCues.begin();
+        while (itCuePoint != CurSeg.lstCues.end())
+        {
+            /* Sanity. */
+            AssertPtr(itCuePoint->pTrack);
+
+            const uint64_t uClusterPos = itCuePoint->offClusterStart - CurSeg.offStart;
+
+            LogFunc(("CuePoint @ %RU64: Track #%RU8 (Time %RU64, Pos %RU64)\n",
+                     RTFileTell(m_Ebml.getFile()), itCuePoint->pTrack->uTrack, itCuePoint->tcClusterStart, uClusterPos));
+
+            m_Ebml.subStart(MkvElem_CuePoint)
+                      .serializeUnsignedInteger(MkvElem_CueTime, itCuePoint->tcClusterStart)
                       .subStart(MkvElem_CueTrackPositions)
-                      .serializeUnsignedInteger(MkvElem_CueTrack, 1)
-                      .serializeUnsignedInteger(MkvElem_CueClusterPosition, itCuePoint->loc - CurSeg.offStart, 8)
-                      .subEnd(MkvElem_CueTrackPositions)
-                      .subEnd(MkvElem_CuePoint);
-
-                itCuePoint++;
-            }
-
-            m_Ebml.subEnd(MkvElem_Cues);
-        }
-
+                          .serializeUnsignedInteger(MkvElem_CueTrack,           itCuePoint->pTrack->uTrack)
+                          .serializeUnsignedInteger(MkvElem_CueClusterPosition, uClusterPos, 8)
+                  .subEnd(MkvElem_CueTrackPositions)
+                  .subEnd(MkvElem_CuePoint);
+
+            itCuePoint++;
+        }
+
+        m_Ebml.subEnd(MkvElem_Cues);
         m_Ebml.subEnd(MkvElem_Segment);
 
@@ -1023,13 +1051,10 @@
               .subEnd(MkvElem_Seek);
 
-        if (CurSeg.lstCues.size())
-        {
-            Assert(CurSeg.offCues - CurSeg.offStart > 0); /* Sanity. */
-
-            m_Ebml.subStart(MkvElem_Seek)
-                  .serializeUnsignedInteger(MkvElem_SeekID, MkvElem_Cues)
-                  .serializeUnsignedInteger(MkvElem_SeekPosition, CurSeg.offCues - CurSeg.offStart, 8)
-                  .subEnd(MkvElem_Seek);
-        }
+        Assert(CurSeg.offCues - CurSeg.offStart > 0); /* Sanity. */
+
+        m_Ebml.subStart(MkvElem_Seek)
+              .serializeUnsignedInteger(MkvElem_SeekID, MkvElem_Cues)
+              .serializeUnsignedInteger(MkvElem_SeekPosition, CurSeg.offCues - CurSeg.offStart, 8)
+              .subEnd(MkvElem_Seek);
 
         m_Ebml.subStart(MkvElem_Seek)
@@ -1059,5 +1084,5 @@
         m_Ebml.subStart(MkvElem_Info)
               .serializeUnsignedInteger(MkvElem_TimecodeScale, CurSeg.uTimecodeScaleFactor)
-              .serializeFloat(MkvElem_Segment_Duration, CurSeg.tcEnd /*+ iFrameTime*/ - CurSeg.tcStart)
+              .serializeFloat(MkvElem_Segment_Duration, CurSeg.tcEnd - CurSeg.tcStart)
               .serializeString(MkvElem_MuxingApp, szMux)
               .serializeString(MkvElem_WritingApp, szApp)
Index: trunk/src/VBox/Main/src-client/EbmlWriter.h
===================================================================
--- trunk/src/VBox/Main/src-client/EbmlWriter.h	(revision 68447)
+++ trunk/src/VBox/Main/src-client/EbmlWriter.h	(revision 68451)
@@ -49,7 +49,7 @@
     {
         /** No audio codec specified. */
-        AudioCodec_Unknown = 0,
+        AudioCodec_None = 0,
         /** Opus. */
-        AudioCodec_Opus    = 1
+        AudioCodec_Opus = 1
     };
 
