summaryrefslogtreecommitdiffstats
path: root/src/VBox/Main/src-client/WebMWriter.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/VBox/Main/src-client/WebMWriter.cpp')
-rw-r--r--src/VBox/Main/src-client/WebMWriter.cpp918
1 files changed, 918 insertions, 0 deletions
diff --git a/src/VBox/Main/src-client/WebMWriter.cpp b/src/VBox/Main/src-client/WebMWriter.cpp
new file mode 100644
index 00000000..7bdb9062
--- /dev/null
+++ b/src/VBox/Main/src-client/WebMWriter.cpp
@@ -0,0 +1,918 @@
+/* $Id: WebMWriter.cpp $ */
+/** @file
+ * WebMWriter.cpp - WebM container handling.
+ */
+
+/*
+ * Copyright (C) 2013-2019 Oracle Corporation
+ *
+ * This file is part of VirtualBox Open Source Edition (OSE), as
+ * available from http://www.virtualbox.org. This file is free software;
+ * you can redistribute it and/or modify it under the terms of the GNU
+ * General Public License (GPL) as published by the Free Software
+ * Foundation, in version 2 as it comes in the "COPYING" file of the
+ * 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
+ */
+
+#define LOG_GROUP LOG_GROUP_MAIN_DISPLAY
+#include "LoggingNew.h"
+
+#include <iprt/cdefs.h>
+#include <iprt/critsect.h>
+#include <iprt/errcore.h>
+#include <iprt/file.h>
+#include <iprt/buildconfig.h>
+
+#include <VBox/log.h>
+#include <VBox/version.h>
+
+#include "WebMWriter.h"
+#include "EBML_MKV.h"
+
+
+WebMWriter::WebMWriter(void)
+{
+ /* Size (in bytes) of time code to write. We use 2 bytes (16 bit) by default. */
+ m_cbTimecode = 2;
+ m_uTimecodeMax = UINT16_MAX;
+
+ m_fInTracksSection = false;
+}
+
+WebMWriter::~WebMWriter(void)
+{
+ Close();
+}
+
+/**
+ * Opens (creates) an output file using an already open file handle.
+ *
+ * @returns VBox status code.
+ * @param a_pszFilename Name of the file the file handle points at.
+ * @param a_phFile Pointer to open file handle to use.
+ * @param a_enmAudioCodec Audio codec to use.
+ * @param a_enmVideoCodec Video codec to use.
+ */
+int WebMWriter::OpenEx(const char *a_pszFilename, PRTFILE a_phFile,
+ WebMWriter::AudioCodec a_enmAudioCodec, WebMWriter::VideoCodec a_enmVideoCodec)
+{
+ try
+ {
+ m_enmAudioCodec = a_enmAudioCodec;
+ m_enmVideoCodec = a_enmVideoCodec;
+
+ LogFunc(("Creating '%s'\n", a_pszFilename));
+
+ int rc = createEx(a_pszFilename, a_phFile);
+ if (RT_SUCCESS(rc))
+ {
+ rc = init();
+ if (RT_SUCCESS(rc))
+ rc = writeHeader();
+ }
+ }
+ catch(int rc)
+ {
+ return rc;
+ }
+ return VINF_SUCCESS;
+}
+
+/**
+ * Opens an output file.
+ *
+ * @returns VBox status code.
+ * @param a_pszFilename Name of the file to create.
+ * @param a_fOpen File open mode of type RTFILE_O_.
+ * @param a_enmAudioCodec Audio codec to use.
+ * @param a_enmVideoCodec Video codec to use.
+ */
+int WebMWriter::Open(const char *a_pszFilename, uint64_t a_fOpen,
+ WebMWriter::AudioCodec a_enmAudioCodec, WebMWriter::VideoCodec a_enmVideoCodec)
+{
+ try
+ {
+ m_enmAudioCodec = a_enmAudioCodec;
+ m_enmVideoCodec = a_enmVideoCodec;
+
+ LogFunc(("Creating '%s'\n", a_pszFilename));
+
+ int rc = create(a_pszFilename, a_fOpen);
+ if (RT_SUCCESS(rc))
+ {
+ rc = init();
+ if (RT_SUCCESS(rc))
+ rc = writeHeader();
+ }
+ }
+ catch(int rc)
+ {
+ return rc;
+ }
+ return VINF_SUCCESS;
+}
+
+/**
+ * Closes the WebM file and drains all queues.
+ *
+ * @returns IPRT status code.
+ */
+int WebMWriter::Close(void)
+{
+ LogFlowFuncEnter();
+
+ if (!isOpen())
+ return VINF_SUCCESS;
+
+ /* Make sure to drain all queues. */
+ processQueue(&CurSeg.queueBlocks, true /* fForce */);
+
+ writeFooter();
+
+ WebMTracks::iterator itTrack = CurSeg.mapTracks.begin();
+ while (itTrack != CurSeg.mapTracks.end())
+ {
+ WebMTrack *pTrack = itTrack->second;
+ if (pTrack) /* Paranoia. */
+ delete pTrack;
+
+ CurSeg.mapTracks.erase(itTrack);
+
+ itTrack = CurSeg.mapTracks.begin();
+ }
+
+ Assert(CurSeg.queueBlocks.Map.size() == 0);
+ Assert(CurSeg.mapTracks.size() == 0);
+
+ com::Utf8Str strFileName = getFileName().c_str();
+
+ close();
+
+ int rc = VINF_SUCCESS;
+
+ /* If no clusters (= data) was written, delete the file again. */
+ if (!CurSeg.cClusters)
+ rc = RTFileDelete(strFileName.c_str());
+
+ LogFlowFuncLeaveRC(rc);
+ return rc;
+}
+
+/**
+ * Adds an audio track.
+ *
+ * @returns IPRT status code.
+ * @param uHz Input sampling rate.
+ * Must be supported by the selected audio codec.
+ * @param cChannels Number of input audio channels.
+ * @param cBits Number of input bits per channel.
+ * @param puTrack Track number on successful creation. Optional.
+ */
+int WebMWriter::AddAudioTrack(uint16_t uHz, uint8_t cChannels, uint8_t cBits, uint8_t *puTrack)
+{
+#ifdef VBOX_WITH_LIBOPUS
+ AssertReturn(uHz, VERR_INVALID_PARAMETER);
+ AssertReturn(cBits, VERR_INVALID_PARAMETER);
+ AssertReturn(cChannels, VERR_INVALID_PARAMETER);
+
+ /*
+ * Adjust the handed-in Hz rate to values which are supported by the Opus codec.
+ *
+ * Only the following values are supported by an Opus standard build
+ * -- every other rate only is supported by a custom build.
+ *
+ * See opus_encoder_create() for more information.
+ */
+ if (uHz > 24000) uHz = 48000;
+ else if (uHz > 16000) uHz = 24000;
+ else if (uHz > 12000) uHz = 16000;
+ else if (uHz > 8000 ) uHz = 12000;
+ else uHz = 8000;
+
+ /* Some players (e.g. Firefox with Nestegg) rely on track numbers starting at 1.
+ * Using a track number 0 will show those files as being corrupted. */
+ const uint8_t uTrack = (uint8_t)CurSeg.mapTracks.size() + 1;
+
+ subStart(MkvElem_TrackEntry);
+
+ serializeUnsignedInteger(MkvElem_TrackNumber, (uint8_t)uTrack);
+ serializeString (MkvElem_Language, "und" /* "Undefined"; see ISO-639-2. */);
+ serializeUnsignedInteger(MkvElem_FlagLacing, (uint8_t)0);
+
+ WebMTrack *pTrack = new WebMTrack(WebMTrackType_Audio, uTrack, RTFileTell(getFile()));
+
+ 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);
+
+ WEBMOPUSPRIVDATA opusPrivData(uHz, cChannels);
+
+ LogFunc(("Opus @ %RU16Hz (%RU16ms + %RU16 frames per block)\n",
+ pTrack->Audio.uHz, pTrack->Audio.msPerBlock, pTrack->Audio.framesPerBlock));
+
+ serializeUnsignedInteger(MkvElem_TrackUID, pTrack->uUUID, 4)
+ .serializeUnsignedInteger(MkvElem_TrackType, 2 /* Audio */)
+ .serializeString(MkvElem_CodecID, "A_OPUS")
+ .serializeData(MkvElem_CodecPrivate, &opusPrivData, sizeof(opusPrivData))
+ .serializeUnsignedInteger(MkvElem_CodecDelay, 0)
+ .serializeUnsignedInteger(MkvElem_SeekPreRoll, 80 * 1000000) /* 80ms in ns. */
+ .subStart(MkvElem_Audio)
+ .serializeFloat(MkvElem_SamplingFrequency, (float)uHz)
+ .serializeUnsignedInteger(MkvElem_Channels, cChannels)
+ .serializeUnsignedInteger(MkvElem_BitDepth, cBits)
+ .subEnd(MkvElem_Audio)
+ .subEnd(MkvElem_TrackEntry);
+
+ CurSeg.mapTracks[uTrack] = pTrack;
+
+ if (puTrack)
+ *puTrack = uTrack;
+
+ return VINF_SUCCESS;
+#else
+ RT_NOREF(uHz, cChannels, cBits, puTrack);
+ return VERR_NOT_SUPPORTED;
+#endif
+}
+
+/**
+ * Adds a video track.
+ *
+ * @returns IPRT status code.
+ * @param uWidth Width (in pixels) of the video track.
+ * @param uHeight Height (in pixels) of the video track.
+ * @param uFPS FPS (Frames Per Second) of the video track.
+ * @param puTrack Track number of the added video track on success. Optional.
+ */
+int WebMWriter::AddVideoTrack(uint16_t uWidth, uint16_t uHeight, uint32_t uFPS, uint8_t *puTrack)
+{
+#ifdef VBOX_WITH_LIBVPX
+ /* Some players (e.g. Firefox with Nestegg) rely on track numbers starting at 1.
+ * Using a track number 0 will show those files as being corrupted. */
+ const uint8_t uTrack = (uint8_t)CurSeg.mapTracks.size() + 1;
+
+ subStart(MkvElem_TrackEntry);
+
+ serializeUnsignedInteger(MkvElem_TrackNumber, (uint8_t)uTrack);
+ serializeString (MkvElem_Language, "und" /* "Undefined"; see ISO-639-2. */);
+ serializeUnsignedInteger(MkvElem_FlagLacing, (uint8_t)0);
+
+ WebMTrack *pTrack = new WebMTrack(WebMTrackType_Video, uTrack, RTFileTell(getFile()));
+
+ /** @todo Resolve codec type. */
+ serializeUnsignedInteger(MkvElem_TrackUID, pTrack->uUUID /* UID */, 4)
+ .serializeUnsignedInteger(MkvElem_TrackType, 1 /* Video */)
+ .serializeString(MkvElem_CodecID, "V_VP8")
+ .subStart(MkvElem_Video)
+ .serializeUnsignedInteger(MkvElem_PixelWidth, uWidth)
+ .serializeUnsignedInteger(MkvElem_PixelHeight, uHeight)
+ /* Some players rely on the FPS rate for timing calculations.
+ * So make sure to *always* include that. */
+ .serializeFloat (MkvElem_FrameRate, (float)uFPS)
+ .subEnd(MkvElem_Video);
+
+ subEnd(MkvElem_TrackEntry);
+
+ CurSeg.mapTracks[uTrack] = pTrack;
+
+ if (puTrack)
+ *puTrack = uTrack;
+
+ return VINF_SUCCESS;
+#else
+ RT_NOREF(uWidth, uHeight, dbFPS, puTrack);
+ return VERR_NOT_SUPPORTED;
+#endif
+}
+
+/**
+ * Gets file name.
+ *
+ * @returns File name as UTF-8 string.
+ */
+const com::Utf8Str& WebMWriter::GetFileName(void)
+{
+ return getFileName();
+}
+
+/**
+ * Gets current output file size.
+ *
+ * @returns File size in bytes.
+ */
+uint64_t WebMWriter::GetFileSize(void)
+{
+ return getFileSize();
+}
+
+/**
+ * Gets current free storage space available for the file.
+ *
+ * @returns Available storage free space.
+ */
+uint64_t WebMWriter::GetAvailableSpace(void)
+{
+ return getAvailableSpace();
+}
+
+/**
+ * Takes care of the initialization of the instance.
+ *
+ * @returns IPRT status code.
+ */
+int WebMWriter::init(void)
+{
+ return CurSeg.init();
+}
+
+/**
+ * Takes care of the destruction of the instance.
+ */
+void WebMWriter::destroy(void)
+{
+ CurSeg.uninit();
+}
+
+/**
+ * Writes the WebM file header.
+ *
+ * @returns IPRT status code.
+ */
+int WebMWriter::writeHeader(void)
+{
+ LogFunc(("Header @ %RU64\n", RTFileTell(getFile())));
+
+ subStart(MkvElem_EBML)
+ .serializeUnsignedInteger(MkvElem_EBMLVersion, 1)
+ .serializeUnsignedInteger(MkvElem_EBMLReadVersion, 1)
+ .serializeUnsignedInteger(MkvElem_EBMLMaxIDLength, 4)
+ .serializeUnsignedInteger(MkvElem_EBMLMaxSizeLength, 8)
+ .serializeString(MkvElem_DocType, "webm")
+ .serializeUnsignedInteger(MkvElem_DocTypeVersion, 2)
+ .serializeUnsignedInteger(MkvElem_DocTypeReadVersion, 2)
+ .subEnd(MkvElem_EBML);
+
+ subStart(MkvElem_Segment);
+
+ /* Save offset of current segment. */
+ CurSeg.offStart = RTFileTell(getFile());
+
+ writeSeekHeader();
+
+ /* Save offset of upcoming tracks segment. */
+ CurSeg.offTracks = RTFileTell(getFile());
+
+ /* The tracks segment starts right after this header. */
+ subStart(MkvElem_Tracks);
+ m_fInTracksSection = true;
+
+ return VINF_SUCCESS;
+}
+
+/**
+ * Writes a simple block into the EBML structure.
+ *
+ * @returns IPRT status code.
+ * @param a_pTrack Track the simple block is assigned to.
+ * @param a_pBlock Simple block to write.
+ */
+int WebMWriter::writeSimpleBlockEBML(WebMTrack *a_pTrack, WebMSimpleBlock *a_pBlock)
+{
+#ifdef LOG_ENABLED
+ WebMCluster &Cluster = CurSeg.CurCluster;
+
+ Log3Func(("[T%RU8C%RU64] Off=%RU64, AbsPTSMs=%RU64, RelToClusterMs=%RU16, %zu bytes\n",
+ a_pTrack->uTrack, Cluster.uID, RTFileTell(getFile()),
+ a_pBlock->Data.tcAbsPTSMs, a_pBlock->Data.tcRelToClusterMs, a_pBlock->Data.cb));
+#endif
+ /*
+ * Write a "Simple Block".
+ */
+ writeClassId(MkvElem_SimpleBlock);
+ /* Block size. */
+ writeUnsignedInteger(0x10000000u | ( 1 /* Track number size. */
+ + m_cbTimecode /* Timecode size .*/
+ + 1 /* Flags size. */
+ + a_pBlock->Data.cb /* Actual frame data size. */), 4);
+ /* Track number. */
+ writeSize(a_pTrack->uTrack);
+ /* Timecode (relative to cluster opening timecode). */
+ writeUnsignedInteger(a_pBlock->Data.tcRelToClusterMs, m_cbTimecode);
+ /* Flags. */
+ writeUnsignedInteger(a_pBlock->Data.fFlags, 1);
+ /* Frame data. */
+ write(a_pBlock->Data.pv, a_pBlock->Data.cb);
+
+ return VINF_SUCCESS;
+}
+
+/**
+ * Writes a simple block and enqueues it into the segment's render queue.
+ *
+ * @returns IPRT status code.
+ * @param a_pTrack Track the simple block is assigned to.
+ * @param a_pBlock Simple block to write and enqueue.
+ */
+int WebMWriter::writeSimpleBlockQueued(WebMTrack *a_pTrack, WebMSimpleBlock *a_pBlock)
+{
+ RT_NOREF(a_pTrack);
+
+ int rc = VINF_SUCCESS;
+
+ try
+ {
+ const WebMTimecodeAbs tcAbsPTS = a_pBlock->Data.tcAbsPTSMs;
+
+ /* See if we already have an entry for the specified timecode in our queue. */
+ WebMBlockMap::iterator itQueue = CurSeg.queueBlocks.Map.find(tcAbsPTS);
+ if (itQueue != CurSeg.queueBlocks.Map.end()) /* Use existing queue. */
+ {
+ WebMTimecodeBlocks &Blocks = itQueue->second;
+ Blocks.Enqueue(a_pBlock);
+ }
+ else /* Create a new timecode entry. */
+ {
+ WebMTimecodeBlocks Blocks;
+ Blocks.Enqueue(a_pBlock);
+
+ CurSeg.queueBlocks.Map[tcAbsPTS] = Blocks;
+ }
+
+ rc = processQueue(&CurSeg.queueBlocks, false /* fForce */);
+ }
+ catch(...)
+ {
+ delete a_pBlock;
+ a_pBlock = NULL;
+
+ rc = VERR_NO_MEMORY;
+ }
+
+ return rc;
+}
+
+#ifdef VBOX_WITH_LIBVPX
+/**
+ * Writes VPX (VP8 video) simple data block.
+ *
+ * @returns IPRT status code.
+ * @param a_pTrack Track ID to write data to.
+ * @param a_pCfg VPX encoder configuration to use.
+ * @param a_pPkt VPX packet video data packet to write.
+ */
+int WebMWriter::writeSimpleBlockVP8(WebMTrack *a_pTrack, const vpx_codec_enc_cfg_t *a_pCfg, const vpx_codec_cx_pkt_t *a_pPkt)
+{
+ RT_NOREF(a_pTrack);
+
+ /* Calculate the absolute PTS of this frame (in ms). */
+ WebMTimecodeAbs tcAbsPTSMs = a_pPkt->data.frame.pts * 1000
+ * (uint64_t) a_pCfg->g_timebase.num / a_pCfg->g_timebase.den;
+ if ( tcAbsPTSMs
+ && tcAbsPTSMs <= a_pTrack->tcAbsLastWrittenMs)
+ {
+ AssertFailed(); /* Should never happen. */
+ tcAbsPTSMs = a_pTrack->tcAbsLastWrittenMs + 1;
+ }
+
+ const bool fKeyframe = RT_BOOL(a_pPkt->data.frame.flags & VPX_FRAME_IS_KEY);
+
+ uint8_t fFlags = VBOX_WEBM_BLOCK_FLAG_NONE;
+ if (fKeyframe)
+ fFlags |= VBOX_WEBM_BLOCK_FLAG_KEY_FRAME;
+ if (a_pPkt->data.frame.flags & VPX_FRAME_IS_INVISIBLE)
+ fFlags |= VBOX_WEBM_BLOCK_FLAG_INVISIBLE;
+
+ return writeSimpleBlockQueued(a_pTrack,
+ new WebMSimpleBlock(a_pTrack,
+ tcAbsPTSMs, a_pPkt->data.frame.buf, a_pPkt->data.frame.sz, fFlags));
+}
+#endif /* VBOX_WITH_LIBVPX */
+
+#ifdef VBOX_WITH_LIBOPUS
+/**
+ * Writes an Opus (audio) simple data block.
+ *
+ * @returns IPRT status code.
+ * @param a_pTrack Track ID to write data to.
+ * @param pvData Pointer to simple data block to write.
+ * @param cbData Size (in bytes) of simple data block to write.
+ * @param tcAbsPTSMs Absolute PTS of simple data block.
+ *
+ * @remarks Audio blocks that have same absolute timecode as video blocks SHOULD be written before the video blocks.
+ */
+int WebMWriter::writeSimpleBlockOpus(WebMTrack *a_pTrack, const void *pvData, size_t cbData, WebMTimecodeAbs tcAbsPTSMs)
+{
+ AssertPtrReturn(a_pTrack, VERR_INVALID_POINTER);
+ AssertPtrReturn(pvData, VERR_INVALID_POINTER);
+ AssertReturn(cbData, VERR_INVALID_PARAMETER);
+
+ /* Every Opus frame is a key frame. */
+ const uint8_t fFlags = VBOX_WEBM_BLOCK_FLAG_KEY_FRAME;
+
+ return writeSimpleBlockQueued(a_pTrack,
+ new WebMSimpleBlock(a_pTrack, tcAbsPTSMs, pvData, cbData, fFlags));
+}
+#endif /* VBOX_WITH_LIBOPUS */
+
+/**
+ * Writes a data block to the specified track.
+ *
+ * @returns IPRT status code.
+ * @param uTrack Track ID to write data to.
+ * @param pvData Pointer to data block to write.
+ * @param cbData Size (in bytes) of data block to write.
+ */
+int WebMWriter::WriteBlock(uint8_t uTrack, const void *pvData, size_t cbData)
+{
+ RT_NOREF(cbData); /* Only needed for assertions for now. */
+
+ int rc = RTCritSectEnter(&CurSeg.CritSect);
+ AssertRC(rc);
+
+ WebMTracks::iterator itTrack = CurSeg.mapTracks.find(uTrack);
+ if (itTrack == CurSeg.mapTracks.end())
+ {
+ RTCritSectLeave(&CurSeg.CritSect);
+ return VERR_NOT_FOUND;
+ }
+
+ WebMTrack *pTrack = itTrack->second;
+ AssertPtr(pTrack);
+
+ if (m_fInTracksSection)
+ {
+ subEnd(MkvElem_Tracks);
+ m_fInTracksSection = false;
+ }
+
+ switch (pTrack->enmType)
+ {
+
+ case WebMTrackType_Audio:
+ {
+#ifdef VBOX_WITH_LIBOPUS
+ if (m_enmAudioCodec == WebMWriter::AudioCodec_Opus)
+ {
+ Assert(cbData == sizeof(WebMWriter::BlockData_Opus));
+ WebMWriter::BlockData_Opus *pData = (WebMWriter::BlockData_Opus *)pvData;
+ rc = writeSimpleBlockOpus(pTrack, pData->pvData, pData->cbData, pData->uPTSMs);
+ }
+ else
+#endif /* VBOX_WITH_LIBOPUS */
+ rc = VERR_NOT_SUPPORTED;
+ break;
+ }
+
+ case WebMTrackType_Video:
+ {
+#ifdef VBOX_WITH_LIBVPX
+ if (m_enmVideoCodec == WebMWriter::VideoCodec_VP8)
+ {
+ Assert(cbData == sizeof(WebMWriter::BlockData_VP8));
+ WebMWriter::BlockData_VP8 *pData = (WebMWriter::BlockData_VP8 *)pvData;
+ rc = writeSimpleBlockVP8(pTrack, pData->pCfg, pData->pPkt);
+ }
+ else
+#endif /* VBOX_WITH_LIBVPX */
+ rc = VERR_NOT_SUPPORTED;
+ break;
+ }
+
+ default:
+ rc = VERR_NOT_SUPPORTED;
+ break;
+ }
+
+ int rc2 = RTCritSectLeave(&CurSeg.CritSect);
+ AssertRC(rc2);
+
+ return rc;
+}
+
+/**
+ * Processes a render queue.
+ *
+ * @returns IPRT status code.
+ * @param pQueue Queue to process.
+ * @param fForce Whether forcing to process the render queue or not.
+ * Needed to drain the queues when terminating.
+ */
+int WebMWriter::processQueue(WebMQueue *pQueue, bool fForce)
+{
+ if (pQueue->tsLastProcessedMs == 0)
+ pQueue->tsLastProcessedMs = RTTimeMilliTS();
+
+ if (!fForce)
+ {
+ /* Only process when we reached a certain threshold. */
+ if (RTTimeMilliTS() - pQueue->tsLastProcessedMs < 5000 /* ms */ /** @todo Make this configurable */)
+ return VINF_SUCCESS;
+ }
+
+ WebMCluster &Cluster = CurSeg.CurCluster;
+
+ /* Iterate through the block map. */
+ WebMBlockMap::iterator it = pQueue->Map.begin();
+ while (it != CurSeg.queueBlocks.Map.end())
+ {
+ WebMTimecodeAbs mapAbsPTSMs = it->first;
+ WebMTimecodeBlocks mapBlocks = it->second;
+
+ /* Whether to start a new cluster or not. */
+ bool fClusterStart = false;
+
+ /* If the current segment does not have any clusters (yet),
+ * take the first absolute PTS as the starting point for that segment. */
+ if (CurSeg.cClusters == 0)
+ {
+ CurSeg.tcAbsStartMs = mapAbsPTSMs;
+ fClusterStart = true;
+ }
+
+ /* Determine if we need to start a new cluster. */
+ /* No blocks written yet? Start a new cluster. */
+ if ( Cluster.cBlocks == 0
+ /* Did we reach the maximum a cluster can hold? Use a new cluster then. */
+ || mapAbsPTSMs - Cluster.tcAbsStartMs > VBOX_WEBM_CLUSTER_MAX_LEN_MS
+ /* If the block map indicates that a cluster is needed for this timecode, create one. */
+ || mapBlocks.fClusterNeeded)
+ {
+ fClusterStart = true;
+ }
+
+ if ( fClusterStart
+ && !mapBlocks.fClusterStarted)
+ {
+ /* Last written timecode of the current cluster. */
+ uint64_t tcAbsClusterLastWrittenMs;
+
+ if (Cluster.fOpen) /* Close current cluster first. */
+ {
+ Log2Func(("[C%RU64] End @ %RU64ms (duration = %RU64ms)\n",
+ Cluster.uID, Cluster.tcAbsLastWrittenMs, Cluster.tcAbsLastWrittenMs - Cluster.tcAbsStartMs));
+
+ /* Make sure that the current cluster contained some data. */
+ Assert(Cluster.offStart);
+ Assert(Cluster.cBlocks);
+
+ /* Save the last written timecode of the current cluster before closing it. */
+ tcAbsClusterLastWrittenMs = Cluster.tcAbsLastWrittenMs;
+
+ subEnd(MkvElem_Cluster);
+ Cluster.fOpen = false;
+ }
+ else /* First cluster ever? Use the segment's starting timecode. */
+ tcAbsClusterLastWrittenMs = CurSeg.tcAbsStartMs;
+
+ Cluster.fOpen = true;
+ Cluster.uID = CurSeg.cClusters;
+ /* Use the block map's currently processed TC as the cluster's starting TC. */
+ Cluster.tcAbsStartMs = mapAbsPTSMs;
+ Cluster.tcAbsLastWrittenMs = Cluster.tcAbsStartMs;
+ Cluster.offStart = RTFileTell(getFile());
+ Cluster.cBlocks = 0;
+
+ AssertMsg(Cluster.tcAbsStartMs <= mapAbsPTSMs,
+ ("Cluster #%RU64 start TC (%RU64) must not bigger than the block map's currently processed TC (%RU64)\n",
+ Cluster.uID, Cluster.tcAbsStartMs, mapAbsPTSMs));
+
+ Log2Func(("[C%RU64] Start @ %RU64ms (map TC is %RU64) / %RU64 bytes\n",
+ Cluster.uID, Cluster.tcAbsStartMs, mapAbsPTSMs, Cluster.offStart));
+
+ /* Insert cue points for all tracks if a new cluster has been started. */
+ WebMCuePoint *pCuePoint = new WebMCuePoint(Cluster.tcAbsStartMs);
+
+ WebMTracks::iterator itTrack = CurSeg.mapTracks.begin();
+ while (itTrack != CurSeg.mapTracks.end())
+ {
+ pCuePoint->Pos[itTrack->first] = new WebMCueTrackPosEntry(Cluster.offStart);
+ ++itTrack;
+ }
+
+ CurSeg.lstCuePoints.push_back(pCuePoint);
+
+ subStart(MkvElem_Cluster)
+ .serializeUnsignedInteger(MkvElem_Timecode, Cluster.tcAbsStartMs - CurSeg.tcAbsStartMs);
+
+ CurSeg.cClusters++;
+
+ mapBlocks.fClusterStarted = true;
+ }
+
+ Log2Func(("[C%RU64] SegTcAbsStartMs=%RU64, ClusterTcAbsStartMs=%RU64, ClusterTcAbsLastWrittenMs=%RU64, mapAbsPTSMs=%RU64\n",
+ Cluster.uID, CurSeg.tcAbsStartMs, Cluster.tcAbsStartMs, Cluster.tcAbsLastWrittenMs, mapAbsPTSMs));
+
+ /* Iterate through all blocks related to the current timecode. */
+ while (!mapBlocks.Queue.empty())
+ {
+ WebMSimpleBlock *pBlock = mapBlocks.Queue.front();
+ AssertPtr(pBlock);
+
+ WebMTrack *pTrack = pBlock->pTrack;
+ AssertPtr(pTrack);
+
+ /* Calculate the block's relative time code to the current cluster's starting time code. */
+ Assert(pBlock->Data.tcAbsPTSMs >= Cluster.tcAbsStartMs);
+ pBlock->Data.tcRelToClusterMs = pBlock->Data.tcAbsPTSMs - Cluster.tcAbsStartMs;
+
+ int rc2 = writeSimpleBlockEBML(pTrack, pBlock);
+ AssertRC(rc2);
+
+ Cluster.cBlocks++;
+ Cluster.tcAbsLastWrittenMs = pBlock->Data.tcAbsPTSMs;
+
+ pTrack->cTotalBlocks++;
+ pTrack->tcAbsLastWrittenMs = Cluster.tcAbsLastWrittenMs;
+
+ if (CurSeg.tcAbsLastWrittenMs < pTrack->tcAbsLastWrittenMs)
+ CurSeg.tcAbsLastWrittenMs = pTrack->tcAbsLastWrittenMs;
+
+ /* Save a cue point if this is a keyframe (if no new cluster has been started,
+ * as this implies that a cue point already is present. */
+ if ( !fClusterStart
+ && (pBlock->Data.fFlags & VBOX_WEBM_BLOCK_FLAG_KEY_FRAME))
+ {
+ /* Insert cue points for all tracks if a new cluster has been started. */
+ WebMCuePoint *pCuePoint = new WebMCuePoint(Cluster.tcAbsLastWrittenMs);
+
+ WebMTracks::iterator itTrack = CurSeg.mapTracks.begin();
+ while (itTrack != CurSeg.mapTracks.end())
+ {
+ pCuePoint->Pos[itTrack->first] = new WebMCueTrackPosEntry(Cluster.offStart);
+ ++itTrack;
+ }
+
+ CurSeg.lstCuePoints.push_back(pCuePoint);
+ }
+
+ delete pBlock;
+ pBlock = NULL;
+
+ mapBlocks.Queue.pop();
+ }
+
+ Assert(mapBlocks.Queue.empty());
+
+ CurSeg.queueBlocks.Map.erase(it);
+
+ it = CurSeg.queueBlocks.Map.begin();
+ }
+
+ Assert(CurSeg.queueBlocks.Map.empty());
+
+ pQueue->tsLastProcessedMs = RTTimeMilliTS();
+
+ return VINF_SUCCESS;
+}
+
+/**
+ * Writes the WebM footer.
+ *
+ * @returns IPRT status code.
+ */
+int WebMWriter::writeFooter(void)
+{
+ AssertReturn(isOpen(), VERR_WRONG_ORDER);
+
+ if (m_fInTracksSection)
+ {
+ subEnd(MkvElem_Tracks);
+ m_fInTracksSection = false;
+ }
+
+ if (CurSeg.CurCluster.fOpen)
+ {
+ subEnd(MkvElem_Cluster);
+ CurSeg.CurCluster.fOpen = false;
+ }
+
+ /*
+ * Write Cues element.
+ */
+ CurSeg.offCues = RTFileTell(getFile());
+ LogFunc(("Cues @ %RU64\n", CurSeg.offCues));
+
+ subStart(MkvElem_Cues);
+
+ WebMCuePointList::iterator itCuePoint = CurSeg.lstCuePoints.begin();
+ while (itCuePoint != CurSeg.lstCuePoints.end())
+ {
+ WebMCuePoint *pCuePoint = (*itCuePoint);
+ AssertPtr(pCuePoint);
+
+ LogFunc(("CuePoint @ %RU64: %zu tracks, tcAbs=%RU64)\n",
+ RTFileTell(getFile()), pCuePoint->Pos.size(), pCuePoint->tcAbs));
+
+ subStart(MkvElem_CuePoint)
+ .serializeUnsignedInteger(MkvElem_CueTime, pCuePoint->tcAbs);
+
+ WebMCueTrackPosMap::iterator itTrackPos = pCuePoint->Pos.begin();
+ while (itTrackPos != pCuePoint->Pos.end())
+ {
+ WebMCueTrackPosEntry *pTrackPos = itTrackPos->second;
+ AssertPtr(pTrackPos);
+
+ LogFunc(("TrackPos (track #%RU32) @ %RU64, offCluster=%RU64)\n",
+ itTrackPos->first, RTFileTell(getFile()), pTrackPos->offCluster));
+
+ subStart(MkvElem_CueTrackPositions)
+ .serializeUnsignedInteger(MkvElem_CueTrack, itTrackPos->first)
+ .serializeUnsignedInteger(MkvElem_CueClusterPosition, pTrackPos->offCluster - CurSeg.offStart, 8)
+ .subEnd(MkvElem_CueTrackPositions);
+
+ ++itTrackPos;
+ }
+
+ subEnd(MkvElem_CuePoint);
+
+ ++itCuePoint;
+ }
+
+ subEnd(MkvElem_Cues);
+ subEnd(MkvElem_Segment);
+
+ /*
+ * Re-Update seek header with final information.
+ */
+
+ writeSeekHeader();
+
+ return RTFileSeek(getFile(), 0, RTFILE_SEEK_END, NULL);
+}
+
+/**
+ * Writes the segment's seek header.
+ */
+void WebMWriter::writeSeekHeader(void)
+{
+ if (CurSeg.offSeekInfo)
+ RTFileSeek(getFile(), CurSeg.offSeekInfo, RTFILE_SEEK_BEGIN, NULL);
+ else
+ CurSeg.offSeekInfo = RTFileTell(getFile());
+
+ LogFunc(("Seek Header @ %RU64\n", CurSeg.offSeekInfo));
+
+ subStart(MkvElem_SeekHead);
+
+ subStart(MkvElem_Seek)
+ .serializeUnsignedInteger(MkvElem_SeekID, MkvElem_Tracks)
+ .serializeUnsignedInteger(MkvElem_SeekPosition, CurSeg.offTracks - CurSeg.offStart, 8)
+ .subEnd(MkvElem_Seek);
+
+ if (CurSeg.offCues)
+ LogFunc(("Updating Cues @ %RU64\n", CurSeg.offCues));
+
+ subStart(MkvElem_Seek)
+ .serializeUnsignedInteger(MkvElem_SeekID, MkvElem_Cues)
+ .serializeUnsignedInteger(MkvElem_SeekPosition, CurSeg.offCues - CurSeg.offStart, 8)
+ .subEnd(MkvElem_Seek);
+
+ subStart(MkvElem_Seek)
+ .serializeUnsignedInteger(MkvElem_SeekID, MkvElem_Info)
+ .serializeUnsignedInteger(MkvElem_SeekPosition, CurSeg.offInfo - CurSeg.offStart, 8)
+ .subEnd(MkvElem_Seek);
+
+ subEnd(MkvElem_SeekHead);
+
+ /*
+ * Write the segment's info element.
+ */
+
+ /* Save offset of the segment's info element. */
+ CurSeg.offInfo = RTFileTell(getFile());
+
+ LogFunc(("Info @ %RU64\n", CurSeg.offInfo));
+
+ char szMux[64];
+ RTStrPrintf(szMux, sizeof(szMux),
+#ifdef VBOX_WITH_LIBVPX
+ "vpxenc%s", vpx_codec_version_str());
+#else
+ "unknown");
+#endif
+ char szApp[64];
+ RTStrPrintf(szApp, sizeof(szApp), VBOX_PRODUCT " %sr%u", VBOX_VERSION_STRING, RTBldCfgRevision());
+
+ const WebMTimecodeAbs tcAbsDurationMs = CurSeg.tcAbsLastWrittenMs - CurSeg.tcAbsStartMs;
+
+ if (!CurSeg.lstCuePoints.empty())
+ {
+ LogFunc(("tcAbsDurationMs=%RU64\n", tcAbsDurationMs));
+ AssertMsg(tcAbsDurationMs, ("Segment seems to be empty (duration is 0)\n"));
+ }
+
+ subStart(MkvElem_Info)
+ .serializeUnsignedInteger(MkvElem_TimecodeScale, CurSeg.uTimecodeScaleFactor)
+ .serializeFloat(MkvElem_Segment_Duration, tcAbsDurationMs)
+ .serializeString(MkvElem_MuxingApp, szMux)
+ .serializeString(MkvElem_WritingApp, szApp)
+ .subEnd(MkvElem_Info);
+}
+