VirtualBox

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

Last change on this file since 107504 was 107504, checked in by vboxsync, 4 months ago

src/VBox/Main/src-client/WebMWriter.cpp: Fixed warning found by Parfait (uninitialized attributes). jiraref:VBP-1424

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