VirtualBox

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

Last change on this file since 94521 was 93115, checked in by vboxsync, 2 years ago

scm --update-copyright-year

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

© 2023 Oracle
ContactPrivacy policyTerms of Use