VirtualBox

source: vbox/trunk/src/VBox/Main/src-client/WebMWriter.cpp@ 82422

Last change on this file since 82422 was 82422, checked in by vboxsync, 5 years ago

Main/Recording: Cleanup; greatly reduced the include maze.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 28.8 KB
Line 
1/* $Id: WebMWriter.cpp 82422 2019-12-05 15:59:34Z vboxsync $ */
2/** @file
3 * WebMWriter.cpp - WebM container handling.
4 */
5
6/*
7 * Copyright (C) 2013-2019 Oracle Corporation
8 *
9 * This file is part of VirtualBox Open Source Edition (OSE), as
10 * available from http://www.virtualbox.org. This file is free software;
11 * you can redistribute it and/or modify it under the terms of the GNU
12 * General Public License (GPL) as published by the Free Software
13 * Foundation, in version 2 as it comes in the "COPYING" file of the
14 * VirtualBox OSE distribution. VirtualBox OSE is distributed in the
15 * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
16 */
17
18/**
19 * For more information, see:
20 * - https://w3c.github.io/media-source/webm-byte-stream-format.html
21 * - https://www.webmproject.org/docs/container/#muxer-guidelines
22 */
23
24#define LOG_GROUP LOG_GROUP_MAIN_DISPLAY
25#include "LoggingNew.h"
26
27#include <iprt/buildconfig.h>
28#include <VBox/version.h>
29
30#include "WebMWriter.h"
31
32
33WebMWriter::WebMWriter(void)
34{
35 /* Size (in bytes) of time code to write. We use 2 bytes (16 bit) by default. */
36 m_cbTimecode = 2;
37 m_uTimecodeMax = UINT16_MAX;
38
39 m_fInTracksSection = false;
40}
41
42WebMWriter::~WebMWriter(void)
43{
44 Close();
45}
46
47/**
48 * Opens (creates) an output file using an already open file handle.
49 *
50 * @returns VBox status code.
51 * @param a_pszFilename Name of the file the file handle points at.
52 * @param a_phFile Pointer to open file handle to use.
53 * @param a_enmAudioCodec Audio codec to use.
54 * @param a_enmVideoCodec Video codec to use.
55 */
56int WebMWriter::OpenEx(const char *a_pszFilename, PRTFILE a_phFile,
57 WebMWriter::AudioCodec a_enmAudioCodec, WebMWriter::VideoCodec a_enmVideoCodec)
58{
59 try
60 {
61 m_enmAudioCodec = a_enmAudioCodec;
62 m_enmVideoCodec = a_enmVideoCodec;
63
64 LogFunc(("Creating '%s'\n", a_pszFilename));
65
66 int rc = createEx(a_pszFilename, a_phFile);
67 if (RT_SUCCESS(rc))
68 {
69 rc = init();
70 if (RT_SUCCESS(rc))
71 rc = writeHeader();
72 }
73 }
74 catch(int rc)
75 {
76 return rc;
77 }
78 return VINF_SUCCESS;
79}
80
81/**
82 * Opens an output file.
83 *
84 * @returns VBox status code.
85 * @param a_pszFilename Name of the file to create.
86 * @param a_fOpen File open mode of type RTFILE_O_.
87 * @param a_enmAudioCodec Audio codec to use.
88 * @param a_enmVideoCodec Video codec to use.
89 */
90int WebMWriter::Open(const char *a_pszFilename, uint64_t a_fOpen,
91 WebMWriter::AudioCodec a_enmAudioCodec, WebMWriter::VideoCodec a_enmVideoCodec)
92{
93 try
94 {
95 m_enmAudioCodec = a_enmAudioCodec;
96 m_enmVideoCodec = a_enmVideoCodec;
97
98 LogFunc(("Creating '%s'\n", a_pszFilename));
99
100 int rc = create(a_pszFilename, a_fOpen);
101 if (RT_SUCCESS(rc))
102 {
103 rc = init();
104 if (RT_SUCCESS(rc))
105 rc = writeHeader();
106 }
107 }
108 catch(int rc)
109 {
110 return rc;
111 }
112 return VINF_SUCCESS;
113}
114
115/**
116 * Closes the WebM file and drains all queues.
117 *
118 * @returns IPRT status code.
119 */
120int WebMWriter::Close(void)
121{
122 LogFlowFuncEnter();
123
124 if (!isOpen())
125 return VINF_SUCCESS;
126
127 /* Make sure to drain all queues. */
128 processQueue(&CurSeg.queueBlocks, true /* fForce */);
129
130 writeFooter();
131
132 WebMTracks::iterator itTrack = CurSeg.mapTracks.begin();
133 while (itTrack != CurSeg.mapTracks.end())
134 {
135 WebMTrack *pTrack = itTrack->second;
136 if (pTrack) /* Paranoia. */
137 delete pTrack;
138
139 CurSeg.mapTracks.erase(itTrack);
140
141 itTrack = CurSeg.mapTracks.begin();
142 }
143
144 Assert(CurSeg.queueBlocks.Map.size() == 0);
145 Assert(CurSeg.mapTracks.size() == 0);
146
147 com::Utf8Str strFileName = getFileName().c_str();
148
149 close();
150
151 int rc = VINF_SUCCESS;
152
153 /* If no clusters (= data) was written, delete the file again. */
154 if (!CurSeg.cClusters)
155 rc = RTFileDelete(strFileName.c_str());
156
157 LogFlowFuncLeaveRC(rc);
158 return rc;
159}
160
161/**
162 * Adds an audio track.
163 *
164 * @returns IPRT status code.
165 * @param uHz Input sampling rate.
166 * Must be supported by the selected audio codec.
167 * @param cChannels Number of input audio channels.
168 * @param cBits Number of input bits per channel.
169 * @param puTrack Track number on successful creation. Optional.
170 */
171int WebMWriter::AddAudioTrack(uint16_t uHz, uint8_t cChannels, uint8_t cBits, uint8_t *puTrack)
172{
173#ifdef VBOX_WITH_LIBOPUS
174 AssertReturn(uHz, VERR_INVALID_PARAMETER);
175 AssertReturn(cBits, VERR_INVALID_PARAMETER);
176 AssertReturn(cChannels, VERR_INVALID_PARAMETER);
177
178 /*
179 * Adjust the handed-in Hz rate to values which are supported by the Opus codec.
180 *
181 * Only the following values are supported by an Opus standard build
182 * -- every other rate only is supported by a custom build.
183 *
184 * See opus_encoder_create() for more information.
185 */
186 if (uHz > 24000) uHz = 48000;
187 else if (uHz > 16000) uHz = 24000;
188 else if (uHz > 12000) uHz = 16000;
189 else if (uHz > 8000 ) uHz = 12000;
190 else uHz = 8000;
191
192 /* Some players (e.g. Firefox with Nestegg) rely on track numbers starting at 1.
193 * Using a track number 0 will show those files as being corrupted. */
194 const uint8_t uTrack = (uint8_t)CurSeg.mapTracks.size() + 1;
195
196 subStart(MkvElem_TrackEntry);
197
198 serializeUnsignedInteger(MkvElem_TrackNumber, (uint8_t)uTrack);
199 serializeString (MkvElem_Language, "und" /* "Undefined"; see ISO-639-2. */);
200 serializeUnsignedInteger(MkvElem_FlagLacing, (uint8_t)0);
201
202 WebMTrack *pTrack = new WebMTrack(WebMTrackType_Audio, uTrack, RTFileTell(getFile()));
203
204 pTrack->Audio.uHz = uHz;
205 pTrack->Audio.msPerBlock = 20; /** Opus uses 20ms by default. Make this configurable? */
206 pTrack->Audio.framesPerBlock = uHz / (1000 /* s in ms */ / pTrack->Audio.msPerBlock);
207
208 WEBMOPUSPRIVDATA opusPrivData(uHz, cChannels);
209
210 LogFunc(("Opus @ %RU16Hz (%RU16ms + %RU16 frames per block)\n",
211 pTrack->Audio.uHz, pTrack->Audio.msPerBlock, pTrack->Audio.framesPerBlock));
212
213 serializeUnsignedInteger(MkvElem_TrackUID, pTrack->uUUID, 4)
214 .serializeUnsignedInteger(MkvElem_TrackType, 2 /* Audio */)
215 .serializeString(MkvElem_CodecID, "A_OPUS")
216 .serializeData(MkvElem_CodecPrivate, &opusPrivData, sizeof(opusPrivData))
217 .serializeUnsignedInteger(MkvElem_CodecDelay, 0)
218 .serializeUnsignedInteger(MkvElem_SeekPreRoll, 80 * 1000000) /* 80ms in ns. */
219 .subStart(MkvElem_Audio)
220 .serializeFloat(MkvElem_SamplingFrequency, (float)uHz)
221 .serializeUnsignedInteger(MkvElem_Channels, cChannels)
222 .serializeUnsignedInteger(MkvElem_BitDepth, cBits)
223 .subEnd(MkvElem_Audio)
224 .subEnd(MkvElem_TrackEntry);
225
226 CurSeg.mapTracks[uTrack] = pTrack;
227
228 if (puTrack)
229 *puTrack = uTrack;
230
231 return VINF_SUCCESS;
232#else
233 RT_NOREF(uHz, cChannels, cBits, puTrack);
234 return VERR_NOT_SUPPORTED;
235#endif
236}
237
238/**
239 * Adds a video track.
240 *
241 * @returns IPRT status code.
242 * @param uWidth Width (in pixels) of the video track.
243 * @param uHeight Height (in pixels) of the video track.
244 * @param uFPS FPS (Frames Per Second) of the video track.
245 * @param puTrack Track number of the added video track on success. Optional.
246 */
247int WebMWriter::AddVideoTrack(uint16_t uWidth, uint16_t uHeight, uint32_t uFPS, uint8_t *puTrack)
248{
249#ifdef VBOX_WITH_LIBVPX
250 /* Some players (e.g. Firefox with Nestegg) rely on track numbers starting at 1.
251 * Using a track number 0 will show those files as being corrupted. */
252 const uint8_t uTrack = (uint8_t)CurSeg.mapTracks.size() + 1;
253
254 subStart(MkvElem_TrackEntry);
255
256 serializeUnsignedInteger(MkvElem_TrackNumber, (uint8_t)uTrack);
257 serializeString (MkvElem_Language, "und" /* "Undefined"; see ISO-639-2. */);
258 serializeUnsignedInteger(MkvElem_FlagLacing, (uint8_t)0);
259
260 WebMTrack *pTrack = new WebMTrack(WebMTrackType_Video, uTrack, RTFileTell(getFile()));
261
262 /** @todo Resolve codec type. */
263 serializeUnsignedInteger(MkvElem_TrackUID, pTrack->uUUID /* UID */, 4)
264 .serializeUnsignedInteger(MkvElem_TrackType, 1 /* Video */)
265 .serializeString(MkvElem_CodecID, "V_VP8")
266 .subStart(MkvElem_Video)
267 .serializeUnsignedInteger(MkvElem_PixelWidth, uWidth)
268 .serializeUnsignedInteger(MkvElem_PixelHeight, uHeight)
269 /* Some players rely on the FPS rate for timing calculations.
270 * So make sure to *always* include that. */
271 .serializeFloat (MkvElem_FrameRate, (float)uFPS)
272 .subEnd(MkvElem_Video);
273
274 subEnd(MkvElem_TrackEntry);
275
276 CurSeg.mapTracks[uTrack] = pTrack;
277
278 if (puTrack)
279 *puTrack = uTrack;
280
281 return VINF_SUCCESS;
282#else
283 RT_NOREF(uWidth, uHeight, dbFPS, puTrack);
284 return VERR_NOT_SUPPORTED;
285#endif
286}
287
288/**
289 * Gets file name.
290 *
291 * @returns File name as UTF-8 string.
292 */
293const com::Utf8Str& WebMWriter::GetFileName(void)
294{
295 return getFileName();
296}
297
298/**
299 * Gets current output file size.
300 *
301 * @returns File size in bytes.
302 */
303uint64_t WebMWriter::GetFileSize(void)
304{
305 return getFileSize();
306}
307
308/**
309 * Gets current free storage space available for the file.
310 *
311 * @returns Available storage free space.
312 */
313uint64_t WebMWriter::GetAvailableSpace(void)
314{
315 return getAvailableSpace();
316}
317
318/**
319 * Takes care of the initialization of the instance.
320 *
321 * @returns IPRT status code.
322 */
323int WebMWriter::init(void)
324{
325 return CurSeg.init();
326}
327
328/**
329 * Takes care of the destruction of the instance.
330 */
331void WebMWriter::destroy(void)
332{
333 CurSeg.uninit();
334}
335
336/**
337 * Writes the WebM file header.
338 *
339 * @returns IPRT status code.
340 */
341int WebMWriter::writeHeader(void)
342{
343 LogFunc(("Header @ %RU64\n", RTFileTell(getFile())));
344
345 subStart(MkvElem_EBML)
346 .serializeUnsignedInteger(MkvElem_EBMLVersion, 1)
347 .serializeUnsignedInteger(MkvElem_EBMLReadVersion, 1)
348 .serializeUnsignedInteger(MkvElem_EBMLMaxIDLength, 4)
349 .serializeUnsignedInteger(MkvElem_EBMLMaxSizeLength, 8)
350 .serializeString(MkvElem_DocType, "webm")
351 .serializeUnsignedInteger(MkvElem_DocTypeVersion, 2)
352 .serializeUnsignedInteger(MkvElem_DocTypeReadVersion, 2)
353 .subEnd(MkvElem_EBML);
354
355 subStart(MkvElem_Segment);
356
357 /* Save offset of current segment. */
358 CurSeg.offStart = RTFileTell(getFile());
359
360 writeSeekHeader();
361
362 /* Save offset of upcoming tracks segment. */
363 CurSeg.offTracks = RTFileTell(getFile());
364
365 /* The tracks segment starts right after this header. */
366 subStart(MkvElem_Tracks);
367 m_fInTracksSection = true;
368
369 return VINF_SUCCESS;
370}
371
372/**
373 * Writes a simple block into the EBML structure.
374 *
375 * @returns IPRT status code.
376 * @param a_pTrack Track the simple block is assigned to.
377 * @param a_pBlock Simple block to write.
378 */
379int WebMWriter::writeSimpleBlockEBML(WebMTrack *a_pTrack, WebMSimpleBlock *a_pBlock)
380{
381#ifdef LOG_ENABLED
382 WebMCluster &Cluster = CurSeg.CurCluster;
383
384 Log3Func(("[T%RU8C%RU64] Off=%RU64, AbsPTSMs=%RU64, RelToClusterMs=%RU16, %zu bytes\n",
385 a_pTrack->uTrack, Cluster.uID, RTFileTell(getFile()),
386 a_pBlock->Data.tcAbsPTSMs, a_pBlock->Data.tcRelToClusterMs, a_pBlock->Data.cb));
387#endif
388 /*
389 * Write a "Simple Block".
390 */
391 writeClassId(MkvElem_SimpleBlock);
392 /* Block size. */
393 writeUnsignedInteger(0x10000000u | ( 1 /* Track number size. */
394 + m_cbTimecode /* Timecode size .*/
395 + 1 /* Flags size. */
396 + a_pBlock->Data.cb /* Actual frame data size. */), 4);
397 /* Track number. */
398 writeSize(a_pTrack->uTrack);
399 /* Timecode (relative to cluster opening timecode). */
400 writeUnsignedInteger(a_pBlock->Data.tcRelToClusterMs, m_cbTimecode);
401 /* Flags. */
402 writeUnsignedInteger(a_pBlock->Data.fFlags, 1);
403 /* Frame data. */
404 write(a_pBlock->Data.pv, a_pBlock->Data.cb);
405
406 return VINF_SUCCESS;
407}
408
409/**
410 * Writes a simple block and enqueues it into the segment's render queue.
411 *
412 * @returns IPRT status code.
413 * @param a_pTrack Track the simple block is assigned to.
414 * @param a_pBlock Simple block to write and enqueue.
415 */
416int WebMWriter::writeSimpleBlockQueued(WebMTrack *a_pTrack, WebMSimpleBlock *a_pBlock)
417{
418 RT_NOREF(a_pTrack);
419
420 int rc = VINF_SUCCESS;
421
422 try
423 {
424 const WebMTimecodeAbs tcAbsPTS = a_pBlock->Data.tcAbsPTSMs;
425
426 /* See if we already have an entry for the specified timecode in our queue. */
427 WebMBlockMap::iterator itQueue = CurSeg.queueBlocks.Map.find(tcAbsPTS);
428 if (itQueue != CurSeg.queueBlocks.Map.end()) /* Use existing queue. */
429 {
430 WebMTimecodeBlocks &Blocks = itQueue->second;
431 Blocks.Enqueue(a_pBlock);
432 }
433 else /* Create a new timecode entry. */
434 {
435 WebMTimecodeBlocks Blocks;
436 Blocks.Enqueue(a_pBlock);
437
438 CurSeg.queueBlocks.Map[tcAbsPTS] = Blocks;
439 }
440
441 rc = processQueue(&CurSeg.queueBlocks, false /* fForce */);
442 }
443 catch(...)
444 {
445 delete a_pBlock;
446 a_pBlock = NULL;
447
448 rc = VERR_NO_MEMORY;
449 }
450
451 return rc;
452}
453
454#ifdef VBOX_WITH_LIBVPX
455/**
456 * Writes VPX (VP8 video) simple data block.
457 *
458 * @returns IPRT status code.
459 * @param a_pTrack Track ID to write data to.
460 * @param a_pCfg VPX encoder configuration to use.
461 * @param a_pPkt VPX packet video data packet to write.
462 */
463int WebMWriter::writeSimpleBlockVP8(WebMTrack *a_pTrack, const vpx_codec_enc_cfg_t *a_pCfg, const vpx_codec_cx_pkt_t *a_pPkt)
464{
465 RT_NOREF(a_pTrack);
466
467 /* Calculate the absolute PTS of this frame (in ms). */
468 WebMTimecodeAbs tcAbsPTSMs = a_pPkt->data.frame.pts * 1000
469 * (uint64_t) a_pCfg->g_timebase.num / a_pCfg->g_timebase.den;
470 if ( tcAbsPTSMs
471 && tcAbsPTSMs <= a_pTrack->tcAbsLastWrittenMs)
472 {
473 AssertFailed(); /* Should never happen. */
474 tcAbsPTSMs = a_pTrack->tcAbsLastWrittenMs + 1;
475 }
476
477 const bool fKeyframe = RT_BOOL(a_pPkt->data.frame.flags & VPX_FRAME_IS_KEY);
478
479 uint8_t fFlags = VBOX_WEBM_BLOCK_FLAG_NONE;
480 if (fKeyframe)
481 fFlags |= VBOX_WEBM_BLOCK_FLAG_KEY_FRAME;
482 if (a_pPkt->data.frame.flags & VPX_FRAME_IS_INVISIBLE)
483 fFlags |= VBOX_WEBM_BLOCK_FLAG_INVISIBLE;
484
485 return writeSimpleBlockQueued(a_pTrack,
486 new WebMSimpleBlock(a_pTrack,
487 tcAbsPTSMs, a_pPkt->data.frame.buf, a_pPkt->data.frame.sz, fFlags));
488}
489#endif /* VBOX_WITH_LIBVPX */
490
491#ifdef VBOX_WITH_LIBOPUS
492/**
493 * Writes an Opus (audio) simple data block.
494 *
495 * @returns IPRT status code.
496 * @param a_pTrack Track ID to write data to.
497 * @param pvData Pointer to simple data block to write.
498 * @param cbData Size (in bytes) of simple data block to write.
499 * @param tcAbsPTSMs Absolute PTS of simple data block.
500 *
501 * @remarks Audio blocks that have same absolute timecode as video blocks SHOULD be written before the video blocks.
502 */
503int WebMWriter::writeSimpleBlockOpus(WebMTrack *a_pTrack, const void *pvData, size_t cbData, WebMTimecodeAbs tcAbsPTSMs)
504{
505 AssertPtrReturn(a_pTrack, VERR_INVALID_POINTER);
506 AssertPtrReturn(pvData, VERR_INVALID_POINTER);
507 AssertReturn(cbData, VERR_INVALID_PARAMETER);
508
509 /* Every Opus frame is a key frame. */
510 const uint8_t fFlags = VBOX_WEBM_BLOCK_FLAG_KEY_FRAME;
511
512 return writeSimpleBlockQueued(a_pTrack,
513 new WebMSimpleBlock(a_pTrack, tcAbsPTSMs, pvData, cbData, fFlags));
514}
515#endif /* VBOX_WITH_LIBOPUS */
516
517/**
518 * Writes a data block to the specified track.
519 *
520 * @returns IPRT status code.
521 * @param uTrack Track ID to write data to.
522 * @param pvData Pointer to data block to write.
523 * @param cbData Size (in bytes) of data block to write.
524 */
525int WebMWriter::WriteBlock(uint8_t uTrack, const void *pvData, size_t cbData)
526{
527 RT_NOREF(cbData); /* Only needed for assertions for now. */
528
529 int rc = RTCritSectEnter(&CurSeg.CritSect);
530 AssertRC(rc);
531
532 WebMTracks::iterator itTrack = CurSeg.mapTracks.find(uTrack);
533 if (itTrack == CurSeg.mapTracks.end())
534 {
535 RTCritSectLeave(&CurSeg.CritSect);
536 return VERR_NOT_FOUND;
537 }
538
539 WebMTrack *pTrack = itTrack->second;
540 AssertPtr(pTrack);
541
542 if (m_fInTracksSection)
543 {
544 subEnd(MkvElem_Tracks);
545 m_fInTracksSection = false;
546 }
547
548 switch (pTrack->enmType)
549 {
550
551 case WebMTrackType_Audio:
552 {
553#ifdef VBOX_WITH_LIBOPUS
554 if (m_enmAudioCodec == WebMWriter::AudioCodec_Opus)
555 {
556 Assert(cbData == sizeof(WebMWriter::BlockData_Opus));
557 WebMWriter::BlockData_Opus *pData = (WebMWriter::BlockData_Opus *)pvData;
558 rc = writeSimpleBlockOpus(pTrack, pData->pvData, pData->cbData, pData->uPTSMs);
559 }
560 else
561#endif /* VBOX_WITH_LIBOPUS */
562 rc = VERR_NOT_SUPPORTED;
563 break;
564 }
565
566 case WebMTrackType_Video:
567 {
568#ifdef VBOX_WITH_LIBVPX
569 if (m_enmVideoCodec == WebMWriter::VideoCodec_VP8)
570 {
571 Assert(cbData == sizeof(WebMWriter::BlockData_VP8));
572 WebMWriter::BlockData_VP8 *pData = (WebMWriter::BlockData_VP8 *)pvData;
573 rc = writeSimpleBlockVP8(pTrack, pData->pCfg, pData->pPkt);
574 }
575 else
576#endif /* VBOX_WITH_LIBVPX */
577 rc = VERR_NOT_SUPPORTED;
578 break;
579 }
580
581 default:
582 rc = VERR_NOT_SUPPORTED;
583 break;
584 }
585
586 int rc2 = RTCritSectLeave(&CurSeg.CritSect);
587 AssertRC(rc2);
588
589 return rc;
590}
591
592/**
593 * Processes a render queue.
594 *
595 * @returns IPRT status code.
596 * @param pQueue Queue to process.
597 * @param fForce Whether forcing to process the render queue or not.
598 * Needed to drain the queues when terminating.
599 */
600int WebMWriter::processQueue(WebMQueue *pQueue, bool fForce)
601{
602 if (pQueue->tsLastProcessedMs == 0)
603 pQueue->tsLastProcessedMs = RTTimeMilliTS();
604
605 if (!fForce)
606 {
607 /* Only process when we reached a certain threshold. */
608 if (RTTimeMilliTS() - pQueue->tsLastProcessedMs < 5000 /* ms */ /** @todo Make this configurable */)
609 return VINF_SUCCESS;
610 }
611
612 WebMCluster &Cluster = CurSeg.CurCluster;
613
614 /* Iterate through the block map. */
615 WebMBlockMap::iterator it = pQueue->Map.begin();
616 while (it != CurSeg.queueBlocks.Map.end())
617 {
618 WebMTimecodeAbs mapAbsPTSMs = it->first;
619 WebMTimecodeBlocks mapBlocks = it->second;
620
621 /* Whether to start a new cluster or not. */
622 bool fClusterStart = false;
623
624 /* If the current segment does not have any clusters (yet),
625 * take the first absolute PTS as the starting point for that segment. */
626 if (CurSeg.cClusters == 0)
627 {
628 CurSeg.tcAbsStartMs = mapAbsPTSMs;
629 fClusterStart = true;
630 }
631
632 /* Determine if we need to start a new cluster. */
633 /* No blocks written yet? Start a new cluster. */
634 if ( Cluster.cBlocks == 0
635 /* Did we reach the maximum a cluster can hold? Use a new cluster then. */
636 || mapAbsPTSMs - Cluster.tcAbsStartMs > VBOX_WEBM_CLUSTER_MAX_LEN_MS
637 /* If the block map indicates that a cluster is needed for this timecode, create one. */
638 || mapBlocks.fClusterNeeded)
639 {
640 fClusterStart = true;
641 }
642
643 if ( fClusterStart
644 && !mapBlocks.fClusterStarted)
645 {
646 /* Last written timecode of the current cluster. */
647 uint64_t tcAbsClusterLastWrittenMs;
648
649 if (Cluster.fOpen) /* Close current cluster first. */
650 {
651 Log2Func(("[C%RU64] End @ %RU64ms (duration = %RU64ms)\n",
652 Cluster.uID, Cluster.tcAbsLastWrittenMs, Cluster.tcAbsLastWrittenMs - Cluster.tcAbsStartMs));
653
654 /* Make sure that the current cluster contained some data. */
655 Assert(Cluster.offStart);
656 Assert(Cluster.cBlocks);
657
658 /* Save the last written timecode of the current cluster before closing it. */
659 tcAbsClusterLastWrittenMs = Cluster.tcAbsLastWrittenMs;
660
661 subEnd(MkvElem_Cluster);
662 Cluster.fOpen = false;
663 }
664 else /* First cluster ever? Use the segment's starting timecode. */
665 tcAbsClusterLastWrittenMs = CurSeg.tcAbsStartMs;
666
667 Cluster.fOpen = true;
668 Cluster.uID = CurSeg.cClusters;
669 /* Use the block map's currently processed TC as the cluster's starting TC. */
670 Cluster.tcAbsStartMs = mapAbsPTSMs;
671 Cluster.tcAbsLastWrittenMs = Cluster.tcAbsStartMs;
672 Cluster.offStart = RTFileTell(getFile());
673 Cluster.cBlocks = 0;
674
675 AssertMsg(Cluster.tcAbsStartMs <= mapAbsPTSMs,
676 ("Cluster #%RU64 start TC (%RU64) must not bigger than the block map's currently processed TC (%RU64)\n",
677 Cluster.uID, Cluster.tcAbsStartMs, mapAbsPTSMs));
678
679 Log2Func(("[C%RU64] Start @ %RU64ms (map TC is %RU64) / %RU64 bytes\n",
680 Cluster.uID, Cluster.tcAbsStartMs, mapAbsPTSMs, Cluster.offStart));
681
682 /* Insert cue points for all tracks if a new cluster has been started. */
683 WebMCuePoint *pCuePoint = new WebMCuePoint(Cluster.tcAbsStartMs);
684
685 WebMTracks::iterator itTrack = CurSeg.mapTracks.begin();
686 while (itTrack != CurSeg.mapTracks.end())
687 {
688 pCuePoint->Pos[itTrack->first] = new WebMCueTrackPosEntry(Cluster.offStart);
689 ++itTrack;
690 }
691
692 CurSeg.lstCuePoints.push_back(pCuePoint);
693
694 subStart(MkvElem_Cluster)
695 .serializeUnsignedInteger(MkvElem_Timecode, Cluster.tcAbsStartMs - CurSeg.tcAbsStartMs);
696
697 CurSeg.cClusters++;
698
699 mapBlocks.fClusterStarted = true;
700 }
701
702 Log2Func(("[C%RU64] SegTcAbsStartMs=%RU64, ClusterTcAbsStartMs=%RU64, ClusterTcAbsLastWrittenMs=%RU64, mapAbsPTSMs=%RU64\n",
703 Cluster.uID, CurSeg.tcAbsStartMs, Cluster.tcAbsStartMs, Cluster.tcAbsLastWrittenMs, mapAbsPTSMs));
704
705 /* Iterate through all blocks related to the current timecode. */
706 while (!mapBlocks.Queue.empty())
707 {
708 WebMSimpleBlock *pBlock = mapBlocks.Queue.front();
709 AssertPtr(pBlock);
710
711 WebMTrack *pTrack = pBlock->pTrack;
712 AssertPtr(pTrack);
713
714 /* Calculate the block's relative time code to the current cluster's starting time code. */
715 Assert(pBlock->Data.tcAbsPTSMs >= Cluster.tcAbsStartMs);
716 pBlock->Data.tcRelToClusterMs = pBlock->Data.tcAbsPTSMs - Cluster.tcAbsStartMs;
717
718 int rc2 = writeSimpleBlockEBML(pTrack, pBlock);
719 AssertRC(rc2);
720
721 Cluster.cBlocks++;
722 Cluster.tcAbsLastWrittenMs = pBlock->Data.tcAbsPTSMs;
723
724 pTrack->cTotalBlocks++;
725 pTrack->tcAbsLastWrittenMs = Cluster.tcAbsLastWrittenMs;
726
727 if (CurSeg.tcAbsLastWrittenMs < pTrack->tcAbsLastWrittenMs)
728 CurSeg.tcAbsLastWrittenMs = pTrack->tcAbsLastWrittenMs;
729
730 /* Save a cue point if this is a keyframe (if no new cluster has been started,
731 * as this implies that a cue point already is present. */
732 if ( !fClusterStart
733 && (pBlock->Data.fFlags & VBOX_WEBM_BLOCK_FLAG_KEY_FRAME))
734 {
735 /* Insert cue points for all tracks if a new cluster has been started. */
736 WebMCuePoint *pCuePoint = new WebMCuePoint(Cluster.tcAbsLastWrittenMs);
737
738 WebMTracks::iterator itTrack = CurSeg.mapTracks.begin();
739 while (itTrack != CurSeg.mapTracks.end())
740 {
741 pCuePoint->Pos[itTrack->first] = new WebMCueTrackPosEntry(Cluster.offStart);
742 ++itTrack;
743 }
744
745 CurSeg.lstCuePoints.push_back(pCuePoint);
746 }
747
748 delete pBlock;
749 pBlock = NULL;
750
751 mapBlocks.Queue.pop();
752 }
753
754 Assert(mapBlocks.Queue.empty());
755
756 CurSeg.queueBlocks.Map.erase(it);
757
758 it = CurSeg.queueBlocks.Map.begin();
759 }
760
761 Assert(CurSeg.queueBlocks.Map.empty());
762
763 pQueue->tsLastProcessedMs = RTTimeMilliTS();
764
765 return VINF_SUCCESS;
766}
767
768/**
769 * Writes the WebM footer.
770 *
771 * @returns IPRT status code.
772 */
773int WebMWriter::writeFooter(void)
774{
775 AssertReturn(isOpen(), VERR_WRONG_ORDER);
776
777 if (m_fInTracksSection)
778 {
779 subEnd(MkvElem_Tracks);
780 m_fInTracksSection = false;
781 }
782
783 if (CurSeg.CurCluster.fOpen)
784 {
785 subEnd(MkvElem_Cluster);
786 CurSeg.CurCluster.fOpen = false;
787 }
788
789 /*
790 * Write Cues element.
791 */
792 CurSeg.offCues = RTFileTell(getFile());
793 LogFunc(("Cues @ %RU64\n", CurSeg.offCues));
794
795 subStart(MkvElem_Cues);
796
797 WebMCuePointList::iterator itCuePoint = CurSeg.lstCuePoints.begin();
798 while (itCuePoint != CurSeg.lstCuePoints.end())
799 {
800 WebMCuePoint *pCuePoint = (*itCuePoint);
801 AssertPtr(pCuePoint);
802
803 LogFunc(("CuePoint @ %RU64: %zu tracks, tcAbs=%RU64)\n",
804 RTFileTell(getFile()), pCuePoint->Pos.size(), pCuePoint->tcAbs));
805
806 subStart(MkvElem_CuePoint)
807 .serializeUnsignedInteger(MkvElem_CueTime, pCuePoint->tcAbs);
808
809 WebMCueTrackPosMap::iterator itTrackPos = pCuePoint->Pos.begin();
810 while (itTrackPos != pCuePoint->Pos.end())
811 {
812 WebMCueTrackPosEntry *pTrackPos = itTrackPos->second;
813 AssertPtr(pTrackPos);
814
815 LogFunc(("TrackPos (track #%RU32) @ %RU64, offCluster=%RU64)\n",
816 itTrackPos->first, RTFileTell(getFile()), pTrackPos->offCluster));
817
818 subStart(MkvElem_CueTrackPositions)
819 .serializeUnsignedInteger(MkvElem_CueTrack, itTrackPos->first)
820 .serializeUnsignedInteger(MkvElem_CueClusterPosition, pTrackPos->offCluster - CurSeg.offStart, 8)
821 .subEnd(MkvElem_CueTrackPositions);
822
823 ++itTrackPos;
824 }
825
826 subEnd(MkvElem_CuePoint);
827
828 ++itCuePoint;
829 }
830
831 subEnd(MkvElem_Cues);
832 subEnd(MkvElem_Segment);
833
834 /*
835 * Re-Update seek header with final information.
836 */
837
838 writeSeekHeader();
839
840 return RTFileSeek(getFile(), 0, RTFILE_SEEK_END, NULL);
841}
842
843/**
844 * Writes the segment's seek header.
845 */
846void WebMWriter::writeSeekHeader(void)
847{
848 if (CurSeg.offSeekInfo)
849 RTFileSeek(getFile(), CurSeg.offSeekInfo, RTFILE_SEEK_BEGIN, NULL);
850 else
851 CurSeg.offSeekInfo = RTFileTell(getFile());
852
853 LogFunc(("Seek Header @ %RU64\n", CurSeg.offSeekInfo));
854
855 subStart(MkvElem_SeekHead);
856
857 subStart(MkvElem_Seek)
858 .serializeUnsignedInteger(MkvElem_SeekID, MkvElem_Tracks)
859 .serializeUnsignedInteger(MkvElem_SeekPosition, CurSeg.offTracks - CurSeg.offStart, 8)
860 .subEnd(MkvElem_Seek);
861
862 if (CurSeg.offCues)
863 LogFunc(("Updating Cues @ %RU64\n", CurSeg.offCues));
864
865 subStart(MkvElem_Seek)
866 .serializeUnsignedInteger(MkvElem_SeekID, MkvElem_Cues)
867 .serializeUnsignedInteger(MkvElem_SeekPosition, CurSeg.offCues - CurSeg.offStart, 8)
868 .subEnd(MkvElem_Seek);
869
870 subStart(MkvElem_Seek)
871 .serializeUnsignedInteger(MkvElem_SeekID, MkvElem_Info)
872 .serializeUnsignedInteger(MkvElem_SeekPosition, CurSeg.offInfo - CurSeg.offStart, 8)
873 .subEnd(MkvElem_Seek);
874
875 subEnd(MkvElem_SeekHead);
876
877 /*
878 * Write the segment's info element.
879 */
880
881 /* Save offset of the segment's info element. */
882 CurSeg.offInfo = RTFileTell(getFile());
883
884 LogFunc(("Info @ %RU64\n", CurSeg.offInfo));
885
886 char szMux[64];
887 RTStrPrintf(szMux, sizeof(szMux),
888#ifdef VBOX_WITH_LIBVPX
889 "vpxenc%s", vpx_codec_version_str());
890#else
891 "unknown");
892#endif
893 char szApp[64];
894 RTStrPrintf(szApp, sizeof(szApp), VBOX_PRODUCT " %sr%u", VBOX_VERSION_STRING, RTBldCfgRevision());
895
896 const WebMTimecodeAbs tcAbsDurationMs = CurSeg.tcAbsLastWrittenMs - CurSeg.tcAbsStartMs;
897
898 if (!CurSeg.lstCuePoints.empty())
899 {
900 LogFunc(("tcAbsDurationMs=%RU64\n", tcAbsDurationMs));
901 AssertMsg(tcAbsDurationMs, ("Segment seems to be empty (duration is 0)\n"));
902 }
903
904 subStart(MkvElem_Info)
905 .serializeUnsignedInteger(MkvElem_TimecodeScale, CurSeg.uTimecodeScaleFactor)
906 .serializeFloat(MkvElem_Segment_Duration, tcAbsDurationMs)
907 .serializeString(MkvElem_MuxingApp, szMux)
908 .serializeString(MkvElem_WritingApp, szApp)
909 .subEnd(MkvElem_Info);
910}
911
Note: See TracBrowser for help on using the repository browser.

© 2025 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette