diff options
Diffstat (limited to 'src/VBox/Main/src-client/WebMWriter.cpp')
-rw-r--r-- | src/VBox/Main/src-client/WebMWriter.cpp | 918 |
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); +} + |