VirtualBox

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

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

Recording: Implemented support for Vorbis codec (provided by libvorbis, not enabled by default yet). This also makes all the codec handling more abstract by using a simple codec wrapper, to keep other places free from codec-specific as much as possible. Initial implementation works and output files are being recognized by media players, but there still are some timing bugs to resolve, as well as optimizing the performance. bugref:10275

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

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