xref: /aosp_15_r20/external/libwebm/vttdemux.cc (revision 103e46e4cd4b6efcf6001f23fa8665fb110abf8d)
1 // Copyright (c) 2012 The WebM project authors. All Rights Reserved.
2 //
3 // Use of this source code is governed by a BSD-style license
4 // that can be found in the LICENSE file in the root of the source
5 // tree. An additional intellectual property rights grant can be found
6 // in the file PATENTS.  All contributing project authors may
7 // be found in the AUTHORS file in the root of the source tree.
8 
9 #include <cstdio>
10 #include <cstdlib>
11 #include <cstring>
12 #include <map>
13 #include <memory>
14 #include <string>
15 #include <utility>
16 
17 #include "mkvparser/mkvparser.h"
18 #include "mkvparser/mkvreader.h"
19 #include "webvtt/webvttparser.h"
20 
21 using std::string;
22 
23 namespace libwebm {
24 namespace vttdemux {
25 
26 typedef long long mkvtime_t;  // NOLINT
27 typedef long long mkvpos_t;  // NOLINT
28 typedef std::unique_ptr<mkvparser::Segment> segment_ptr_t;
29 
30 // WebVTT metadata tracks have a type (encoded in the CodecID for the track).
31 // We use |type| to synthesize a filename for the out-of-band WebVTT |file|.
32 struct MetadataInfo {
33   enum Type { kSubtitles, kCaptions, kDescriptions, kMetadata, kChapters } type;
34   FILE* file;
35 };
36 
37 // We use a map, indexed by track number, to collect information about
38 // each track in the input file.
39 typedef std::map<long, MetadataInfo> metadata_map_t;  // NOLINT
40 
41 // The distinguished key value we use to store the chapters
42 // information in the metadata map.
43 enum { kChaptersKey = 0 };
44 
45 // The data from the original WebVTT Cue is stored as a WebM block.
46 // The FrameParser is used to parse the lines of text out from the
47 // block, in order to reconstruct the original WebVTT Cue.
48 class FrameParser : public libwebvtt::LineReader {
49  public:
50   //  Bind the FrameParser instance to a WebM block.
51   explicit FrameParser(const mkvparser::BlockGroup* block_group);
52   virtual ~FrameParser();
53 
54   // The Webm block (group) to which this instance is bound.  We
55   // treat the payload of the block as a stream of characters.
56   const mkvparser::BlockGroup* const block_group_;
57 
58  protected:
59   // Read the next character from the character stream (the payload
60   // of the WebM block).  We increment the stream pointer |pos_| as
61   // each character from the stream is consumed.
62   virtual int GetChar(char* c);
63 
64   // End-of-line handling requires that we put a character back into
65   // the stream.  Here we need only decrement the stream pointer |pos_|
66   // to unconsume the character.
67   virtual void UngetChar(char c);
68 
69   // The current position in the character stream (the payload of the block).
70   mkvpos_t pos_;
71 
72   // The position of the end of the character stream. When the current
73   // position |pos_| equals the end position |pos_end_|, the entire
74   // stream (block payload) has been consumed and end-of-stream is indicated.
75   mkvpos_t pos_end_;
76 
77  private:
78   // Disable copy ctor and copy assign
79   FrameParser(const FrameParser&);
80   FrameParser& operator=(const FrameParser&);
81 };
82 
83 // The data from the original WebVTT Cue is stored as an MKV Chapters
84 // Atom element (the cue payload is stored as a Display sub-element).
85 // The ChapterAtomParser is used to parse the lines of text out from
86 // the String sub-element of the Display element (though it would be
87 // admittedly odd if there were more than one line).
88 class ChapterAtomParser : public libwebvtt::LineReader {
89  public:
90   explicit ChapterAtomParser(const mkvparser::Chapters::Display* display);
91   virtual ~ChapterAtomParser();
92 
93   const mkvparser::Chapters::Display* const display_;
94 
95  protected:
96   // Read the next character from the character stream (the title
97   // member of the atom's display).  We increment the stream pointer
98   // |str_| as each character from the stream is consumed.
99   virtual int GetChar(char* c);
100 
101   // End-of-line handling requires that we put a character back into
102   // the stream.  Here we need only decrement the stream pointer |str_|
103   // to unconsume the character.
104   virtual void UngetChar(char c);
105 
106   // The current position in the character stream (the title of the
107   // atom's display).
108   const char* str_;
109 
110   // The position of the end of the character stream. When the current
111   // position |str_| equals the end position |str_end_|, the entire
112   // stream (title of the display) has been consumed and end-of-stream
113   // is indicated.
114   const char* str_end_;
115 
116  private:
117   ChapterAtomParser(const ChapterAtomParser&);
118   ChapterAtomParser& operator=(const ChapterAtomParser&);
119 };
120 
121 // Parse the EBML header of the WebM input file, to determine whether we
122 // actually have a WebM file.  Returns false if this is not a WebM file.
123 bool ParseHeader(mkvparser::IMkvReader* reader, mkvpos_t* pos);
124 
125 // Parse the Segment of the input file and load all of its clusters.
126 // Returns false if there was an error parsing the file.
127 bool ParseSegment(mkvparser::IMkvReader* reader, mkvpos_t pos,
128                   segment_ptr_t* segment);
129 
130 // If |segment| has a Chapters element (in which case, there will be a
131 // corresponding entry in |metadata_map|), convert the MKV chapters to
132 // WebVTT chapter cues and write them to the output file.  Returns
133 // false on error.
134 bool WriteChaptersFile(const metadata_map_t& metadata_map,
135                        const mkvparser::Segment* segment);
136 
137 // Convert an MKV Chapters Atom to a WebVTT cue and write it to the
138 // output |file|.  Returns false on error.
139 bool WriteChaptersCue(FILE* file, const mkvparser::Chapters* chapters,
140                       const mkvparser::Chapters::Atom* atom,
141                       const mkvparser::Chapters::Display* display);
142 
143 // Write the Cue Identifier line of the WebVTT cue, if it's present.
144 // Returns false on error.
145 bool WriteChaptersCueIdentifier(FILE* file,
146                                 const mkvparser::Chapters::Atom* atom);
147 
148 // Use the timecodes from the chapters |atom| to write just the
149 // timings line of the WebVTT cue.  Returns false on error.
150 bool WriteChaptersCueTimings(FILE* file, const mkvparser::Chapters* chapters,
151                              const mkvparser::Chapters::Atom* atom);
152 
153 // Parse the String sub-element of the |display| and write the payload
154 // of the WebVTT cue.  Returns false on error.
155 bool WriteChaptersCuePayload(FILE* file,
156                              const mkvparser::Chapters::Display* display);
157 
158 // Iterate over the tracks of the input file (and any chapters
159 // element) and cache information about each metadata track.
160 void BuildMap(const mkvparser::Segment* segment, metadata_map_t* metadata_map);
161 
162 // For each track listed in the cache, synthesize its output filename
163 // and open a file handle that designates the out-of-band file.
164 // Returns false if we were unable to open an output file for a track.
165 bool OpenFiles(metadata_map_t* metadata_map, const char* filename);
166 
167 // Close the file handle for each track in the cache.
168 void CloseFiles(metadata_map_t* metadata_map);
169 
170 // Iterate over the clusters of the input file, and write a WebVTT cue
171 // for each metadata block.  Returns false if processing of a cluster
172 // failed.
173 bool WriteFiles(const metadata_map_t& m, mkvparser::Segment* s);
174 
175 // Write the WebVTT header for each track in the cache.  We do this
176 // immediately before writing the actual WebVTT cues.  Returns false
177 // if the write failed.
178 bool InitializeFiles(const metadata_map_t& metadata_map);
179 
180 // Iterate over the blocks of the |cluster|, writing a WebVTT cue to
181 // its associated output file for each block of metadata.  Returns
182 // false if processing a block failed, or there was a parse error.
183 bool ProcessCluster(const metadata_map_t& metadata_map,
184                     const mkvparser::Cluster* cluster);
185 
186 // Look up this track number in the cache, and if found (meaning this
187 // is a metadata track), write a WebVTT cue to the associated output
188 // file.  Returns false if writing the WebVTT cue failed.
189 bool ProcessBlockEntry(const metadata_map_t& metadata_map,
190                        const mkvparser::BlockEntry* block_entry);
191 
192 // Parse the lines of text from the |block_group| to reconstruct the
193 // original WebVTT cue, and write it to the associated output |file|.
194 // Returns false if there was an error writing to the output file.
195 bool WriteCue(FILE* file, const mkvparser::BlockGroup* block_group);
196 
197 // Consume a line of text from the character stream, and if the line
198 // is not empty write the cue identifier to the associated output
199 // file.  Returns false if there was an error writing to the file.
200 bool WriteCueIdentifier(FILE* f, FrameParser* parser);
201 
202 // Consume a line of text from the character stream (which holds any
203 // cue settings) and write the cue timings line for this cue to the
204 // associated output file.  Returns false if there was an error
205 // writing to the file.
206 bool WriteCueTimings(FILE* f, FrameParser* parser);
207 
208 // Write the timestamp (representating either the start time or stop
209 // time of the cue) to the output file.  Returns false if there was an
210 // error writing to the file.
211 bool WriteCueTime(FILE* f, mkvtime_t time_ns);
212 
213 // Consume the remaining lines of text from the character stream
214 // (these lines are the actual payload of the WebVTT cue), and write
215 // them to the associated output file.  Returns false if there was an
216 // error writing to the file.
217 bool WriteCuePayload(FILE* f, FrameParser* parser);
218 }  // namespace vttdemux
219 
220 namespace vttdemux {
221 
FrameParser(const mkvparser::BlockGroup * block_group)222 FrameParser::FrameParser(const mkvparser::BlockGroup* block_group)
223     : block_group_(block_group) {
224   const mkvparser::Block* const block = block_group->GetBlock();
225   const mkvparser::Block::Frame& f = block->GetFrame(0);
226 
227   // The beginning and end of the character stream corresponds to the
228   // position of this block's frame within the WebM input file.
229 
230   pos_ = f.pos;
231   pos_end_ = f.pos + f.len;
232 }
233 
~FrameParser()234 FrameParser::~FrameParser() {}
235 
GetChar(char * c)236 int FrameParser::GetChar(char* c) {
237   if (pos_ >= pos_end_)  // end-of-stream
238     return 1;  // per the semantics of libwebvtt::Reader::GetChar
239 
240   const mkvparser::Cluster* const cluster = block_group_->GetCluster();
241   const mkvparser::Segment* const segment = cluster->m_pSegment;
242   mkvparser::IMkvReader* const reader = segment->m_pReader;
243 
244   unsigned char* const buf = reinterpret_cast<unsigned char*>(c);
245   const int result = reader->Read(pos_, 1, buf);
246 
247   if (result < 0)  // error
248     return -1;
249 
250   ++pos_;  // consume this character in the stream
251   return 0;
252 }
253 
UngetChar(char)254 void FrameParser::UngetChar(char /* c */) {
255   // All we need to do here is decrement the position in the stream.
256   // The next time GetChar is called the same character will be
257   // re-read from the input file.
258   --pos_;
259 }
260 
ChapterAtomParser(const mkvparser::Chapters::Display * display)261 ChapterAtomParser::ChapterAtomParser(
262     const mkvparser::Chapters::Display* display)
263     : display_(display) {
264   str_ = display->GetString();
265   if (str_ == NULL)
266     return;
267   const size_t len = strlen(str_);
268   str_end_ = str_ + len;
269 }
270 
~ChapterAtomParser()271 ChapterAtomParser::~ChapterAtomParser() {}
272 
GetChar(char * c)273 int ChapterAtomParser::GetChar(char* c) {
274   if (str_ == NULL || str_ >= str_end_)  // end-of-stream
275     return 1;  // per the semantics of libwebvtt::Reader::GetChar
276 
277   *c = *str_++;  // consume this character in the stream
278   return 0;
279 }
280 
UngetChar(char)281 void ChapterAtomParser::UngetChar(char /* c */) {
282   // All we need to do here is decrement the position in the stream.
283   // The next time GetChar is called the same character will be
284   // re-read from the input file.
285   --str_;
286 }
287 
288 }  // namespace vttdemux
289 
ParseHeader(mkvparser::IMkvReader * reader,mkvpos_t * pos)290 bool vttdemux::ParseHeader(mkvparser::IMkvReader* reader, mkvpos_t* pos) {
291   mkvparser::EBMLHeader h;
292   const mkvpos_t status = h.Parse(reader, *pos);
293 
294   if (status) {
295     printf("error parsing EBML header\n");
296     return false;
297   }
298 
299   if (h.m_docType == NULL || strcmp(h.m_docType, "webm") != 0) {
300     printf("bad doctype\n");
301     return false;
302   }
303 
304   return true;  // success
305 }
306 
ParseSegment(mkvparser::IMkvReader * reader,mkvpos_t pos,segment_ptr_t * segment_ptr)307 bool vttdemux::ParseSegment(mkvparser::IMkvReader* reader, mkvpos_t pos,
308                             segment_ptr_t* segment_ptr) {
309   // We first create the segment object.
310 
311   mkvparser::Segment* p;
312   const mkvpos_t create = mkvparser::Segment::CreateInstance(reader, pos, p);
313 
314   if (create) {
315     printf("error parsing segment element\n");
316     return false;
317   }
318 
319   segment_ptr->reset(p);
320 
321   // Now parse all of the segment's sub-elements, in toto.
322 
323   const long status = p->Load();  // NOLINT
324 
325   if (status < 0) {
326     printf("error loading segment\n");
327     return false;
328   }
329 
330   return true;
331 }
332 
BuildMap(const mkvparser::Segment * segment,metadata_map_t * map_ptr)333 void vttdemux::BuildMap(const mkvparser::Segment* segment,
334                         metadata_map_t* map_ptr) {
335   metadata_map_t& m = *map_ptr;
336   m.clear();
337 
338   if (segment->GetChapters()) {
339     MetadataInfo info;
340     info.file = NULL;
341     info.type = MetadataInfo::kChapters;
342 
343     m[kChaptersKey] = info;
344   }
345 
346   const mkvparser::Tracks* const tt = segment->GetTracks();
347   if (tt == NULL)
348     return;
349 
350   const long tc = tt->GetTracksCount();  // NOLINT
351   if (tc <= 0)
352     return;
353 
354   // Iterate over the tracks in the intput file.  We determine whether
355   // a track holds metadata by inspecting its CodecID.
356 
357   for (long idx = 0; idx < tc; ++idx) {  // NOLINT
358     const mkvparser::Track* const t = tt->GetTrackByIndex(idx);
359 
360     if (t == NULL)  // weird
361       continue;
362 
363     const long tn = t->GetNumber();  // NOLINT
364 
365     if (tn <= 0)  // weird
366       continue;
367 
368     const char* const codec_id = t->GetCodecId();
369 
370     if (codec_id == NULL)  // weird
371       continue;
372 
373     MetadataInfo info;
374     info.file = NULL;
375 
376     if (strcmp(codec_id, "D_WEBVTT/SUBTITLES") == 0) {
377       info.type = MetadataInfo::kSubtitles;
378     } else if (strcmp(codec_id, "D_WEBVTT/CAPTIONS") == 0) {
379       info.type = MetadataInfo::kCaptions;
380     } else if (strcmp(codec_id, "D_WEBVTT/DESCRIPTIONS") == 0) {
381       info.type = MetadataInfo::kDescriptions;
382     } else if (strcmp(codec_id, "D_WEBVTT/METADATA") == 0) {
383       info.type = MetadataInfo::kMetadata;
384     } else {
385       continue;
386     }
387 
388     m[tn] = info;  // create an entry in the cache for this track
389   }
390 }
391 
OpenFiles(metadata_map_t * metadata_map,const char * filename)392 bool vttdemux::OpenFiles(metadata_map_t* metadata_map, const char* filename) {
393   if (metadata_map == NULL || metadata_map->empty())
394     return false;
395 
396   if (filename == NULL)
397     return false;
398 
399   // Find the position of the filename extension.  We synthesize the
400   // output filename from the directory path and basename of the input
401   // filename.
402 
403   const char* const ext = strrchr(filename, '.');
404 
405   if (ext == NULL)  // TODO(matthewjheaney): liberalize?
406     return false;
407 
408   // Remember whether a track of this type has already been seen (the
409   // map key) by keeping a count (the map item).  We quality the
410   // output filename with the track number if there is more than one
411   // track having a given type.
412 
413   std::map<MetadataInfo::Type, int> exists;
414 
415   typedef metadata_map_t::iterator iter_t;
416 
417   metadata_map_t& m = *metadata_map;
418   const iter_t ii = m.begin();
419   const iter_t j = m.end();
420 
421   // Make a first pass over the cache to determine whether there is
422   // more than one track corresponding to a given metadata type.
423 
424   iter_t i = ii;
425   while (i != j) {
426     const metadata_map_t::value_type& v = *i++;
427     const MetadataInfo& info = v.second;
428     const MetadataInfo::Type type = info.type;
429     ++exists[type];
430   }
431 
432   // Make a second pass over the cache, synthesizing the filename of
433   // each output file (from the input file basename, the input track
434   // metadata type, and its track number if necessary), and then
435   // opening a WebVTT output file having that filename.
436 
437   i = ii;
438   while (i != j) {
439     metadata_map_t::value_type& v = *i++;
440     MetadataInfo& info = v.second;
441     const MetadataInfo::Type type = info.type;
442 
443     // Start with the basename of the input file.
444 
445     string name(filename, ext);
446 
447     // Next append the metadata kind.
448 
449     switch (type) {
450       case MetadataInfo::kSubtitles:
451         name += "_SUBTITLES";
452         break;
453 
454       case MetadataInfo::kCaptions:
455         name += "_CAPTIONS";
456         break;
457 
458       case MetadataInfo::kDescriptions:
459         name += "_DESCRIPTIONS";
460         break;
461 
462       case MetadataInfo::kMetadata:
463         name += "_METADATA";
464         break;
465 
466       case MetadataInfo::kChapters:
467         name += "_CHAPTERS";
468         break;
469 
470       default:
471         return false;
472     }
473 
474     // If there is more than one metadata track having a given type
475     // (the WebVTT-in-WebM spec doesn't preclude this), then qualify
476     // the output filename with the input track number.
477 
478     if (exists[type] > 1) {
479       enum { kLen = 33 };
480       char str[kLen];  // max 126 tracks, so only 4 chars really needed
481 #ifndef _MSC_VER
482       snprintf(str, kLen, "%ld", v.first);  // track number
483 #else
484       _snprintf_s(str, sizeof(str), kLen, "%ld", v.first);  // track number
485 #endif
486       name += str;
487     }
488 
489     // Finally append the output filename extension.
490 
491     name += ".vtt";
492 
493     // We have synthesized the full output filename, so attempt to
494     // open the WebVTT output file.
495 
496     info.file = fopen(name.c_str(), "wb");
497     const bool success = (info.file != NULL);
498 
499     if (!success) {
500       printf("unable to open output file %s\n", name.c_str());
501       return false;
502     }
503   }
504 
505   return true;
506 }
507 
CloseFiles(metadata_map_t * metadata_map)508 void vttdemux::CloseFiles(metadata_map_t* metadata_map) {
509   if (metadata_map == NULL)
510     return;
511 
512   metadata_map_t& m = *metadata_map;
513 
514   typedef metadata_map_t::iterator iter_t;
515 
516   iter_t i = m.begin();
517   const iter_t j = m.end();
518 
519   // Gracefully close each output file, to ensure all output gets
520   // propertly flushed.
521 
522   while (i != j) {
523     metadata_map_t::value_type& v = *i++;
524     MetadataInfo& info = v.second;
525 
526     if (info.file != NULL) {
527       fclose(info.file);
528       info.file = NULL;
529     }
530   }
531 }
532 
WriteFiles(const metadata_map_t & m,mkvparser::Segment * s)533 bool vttdemux::WriteFiles(const metadata_map_t& m, mkvparser::Segment* s) {
534   // First write the WebVTT header.
535 
536   InitializeFiles(m);
537 
538   if (!WriteChaptersFile(m, s))
539     return false;
540 
541   // Now iterate over the clusters, writing the WebVTT cue as we parse
542   // each metadata block.
543 
544   const mkvparser::Cluster* cluster = s->GetFirst();
545 
546   while (cluster != NULL && !cluster->EOS()) {
547     if (!ProcessCluster(m, cluster))
548       return false;
549 
550     cluster = s->GetNext(cluster);
551   }
552 
553   return true;
554 }
555 
InitializeFiles(const metadata_map_t & m)556 bool vttdemux::InitializeFiles(const metadata_map_t& m) {
557   // Write the WebVTT header for each output file in the cache.
558 
559   typedef metadata_map_t::const_iterator iter_t;
560   iter_t i = m.begin();
561   const iter_t j = m.end();
562 
563   while (i != j) {
564     const metadata_map_t::value_type& v = *i++;
565     const MetadataInfo& info = v.second;
566     FILE* const f = info.file;
567 
568     if (fputs("WEBVTT\n", f) < 0) {
569       printf("unable to initialize output file\n");
570       return false;
571     }
572   }
573 
574   return true;
575 }
576 
WriteChaptersFile(const metadata_map_t & m,const mkvparser::Segment * s)577 bool vttdemux::WriteChaptersFile(const metadata_map_t& m,
578                                  const mkvparser::Segment* s) {
579   const metadata_map_t::const_iterator info_iter = m.find(kChaptersKey);
580   if (info_iter == m.end())  // no chapters, so nothing to do
581     return true;
582 
583   const mkvparser::Chapters* const chapters = s->GetChapters();
584   if (chapters == NULL)  // weird
585     return true;
586 
587   const MetadataInfo& info = info_iter->second;
588   FILE* const file = info.file;
589 
590   const int edition_count = chapters->GetEditionCount();
591 
592   if (edition_count <= 0)  // weird
593     return true;  // nothing to do
594 
595   if (edition_count > 1) {
596     // TODO(matthewjheaney): figure what to do here
597     printf("more than one chapter edition detected\n");
598     return false;
599   }
600 
601   const mkvparser::Chapters::Edition* const edition = chapters->GetEdition(0);
602 
603   const int atom_count = edition->GetAtomCount();
604 
605   for (int idx = 0; idx < atom_count; ++idx) {
606     const mkvparser::Chapters::Atom* const atom = edition->GetAtom(idx);
607     const int display_count = atom->GetDisplayCount();
608 
609     if (display_count <= 0)
610       continue;
611 
612     if (display_count > 1) {
613       // TODO(matthewjheaney): handle case of multiple languages
614       printf("more than 1 display in atom detected\n");
615       return false;
616     }
617 
618     const mkvparser::Chapters::Display* const display = atom->GetDisplay(0);
619 
620     if (const char* language = display->GetLanguage()) {
621       if (strcmp(language, "eng") != 0) {
622         // TODO(matthewjheaney): handle case of multiple languages.
623 
624         // We must create a separate webvtt file for each language.
625         // This isn't a simple problem (which is why we defer it for
626         // now), because there's nothing in the header that tells us
627         // what languages we have as cues.  We must parse the displays
628         // of each atom to determine that.
629 
630         // One solution is to make two passes over the input data.
631         // First parse the displays, creating an in-memory cache of
632         // all the chapter cues, sorted according to their language.
633         // After we have read all of the chapter atoms from the input
634         // file, we can then write separate output files for each
635         // language.
636 
637         printf("only English-language chapter cues are supported\n");
638         return false;
639       }
640     }
641 
642     if (!WriteChaptersCue(file, chapters, atom, display))
643       return false;
644   }
645 
646   return true;
647 }
648 
WriteChaptersCue(FILE * f,const mkvparser::Chapters * chapters,const mkvparser::Chapters::Atom * atom,const mkvparser::Chapters::Display * display)649 bool vttdemux::WriteChaptersCue(FILE* f, const mkvparser::Chapters* chapters,
650                                 const mkvparser::Chapters::Atom* atom,
651                                 const mkvparser::Chapters::Display* display) {
652   // We start a new cue by writing a cue separator (an empty line)
653   // into the stream.
654 
655   if (fputc('\n', f) < 0)
656     return false;
657 
658   // A WebVTT Cue comprises 3 things: a cue identifier, followed by
659   // the cue timings, followed by the payload of the cue.  We write
660   // each part of the cue in sequence.
661 
662   if (!WriteChaptersCueIdentifier(f, atom))
663     return false;
664 
665   if (!WriteChaptersCueTimings(f, chapters, atom))
666     return false;
667 
668   if (!WriteChaptersCuePayload(f, display))
669     return false;
670 
671   return true;
672 }
673 
WriteChaptersCueIdentifier(FILE * f,const mkvparser::Chapters::Atom * atom)674 bool vttdemux::WriteChaptersCueIdentifier(
675     FILE* f, const mkvparser::Chapters::Atom* atom) {
676   const char* const identifier = atom->GetStringUID();
677 
678   if (identifier == NULL)
679     return true;  // nothing else to do
680 
681   if (fprintf(f, "%s\n", identifier) < 0)
682     return false;
683 
684   return true;
685 }
686 
WriteChaptersCueTimings(FILE * f,const mkvparser::Chapters * chapters,const mkvparser::Chapters::Atom * atom)687 bool vttdemux::WriteChaptersCueTimings(FILE* f,
688                                        const mkvparser::Chapters* chapters,
689                                        const mkvparser::Chapters::Atom* atom) {
690   const mkvtime_t start_ns = atom->GetStartTime(chapters);
691 
692   if (start_ns < 0)
693     return false;
694 
695   const mkvtime_t stop_ns = atom->GetStopTime(chapters);
696 
697   if (stop_ns < 0)
698     return false;
699 
700   if (!WriteCueTime(f, start_ns))
701     return false;
702 
703   if (fputs(" --> ", f) < 0)
704     return false;
705 
706   if (!WriteCueTime(f, stop_ns))
707     return false;
708 
709   if (fputc('\n', f) < 0)
710     return false;
711 
712   return true;
713 }
714 
WriteChaptersCuePayload(FILE * f,const mkvparser::Chapters::Display * display)715 bool vttdemux::WriteChaptersCuePayload(
716     FILE* f, const mkvparser::Chapters::Display* display) {
717   // Bind a Chapter parser object to the display, which allows us to
718   // extract each line of text from the title-part of the display.
719   ChapterAtomParser parser(display);
720 
721   int count = 0;  // count of lines of payload text written to output file
722   for (string line;;) {
723     const int e = parser.GetLine(&line);
724 
725     if (e < 0)  // error (only -- we allow EOS here)
726       return false;
727 
728     if (line.empty())  // TODO(matthewjheaney): retain this check?
729       break;
730 
731     if (fprintf(f, "%s\n", line.c_str()) < 0)
732       return false;
733 
734     ++count;
735   }
736 
737   if (count <= 0)  // WebVTT cue requires non-empty payload
738     return false;
739 
740   return true;
741 }
742 
ProcessCluster(const metadata_map_t & m,const mkvparser::Cluster * c)743 bool vttdemux::ProcessCluster(const metadata_map_t& m,
744                               const mkvparser::Cluster* c) {
745   // Visit the blocks in this cluster, writing a WebVTT cue for each
746   // metadata block.
747 
748   const mkvparser::BlockEntry* block_entry;
749 
750   long result = c->GetFirst(block_entry);  // NOLINT
751   if (result < 0) {
752     printf("bad cluster (unable to get first block)\n");
753     return false;
754   }
755 
756   while (block_entry != NULL && !block_entry->EOS()) {
757     if (!ProcessBlockEntry(m, block_entry))
758       return false;
759 
760     result = c->GetNext(block_entry, block_entry);
761     if (result < 0) {  // error
762       printf("bad cluster (unable to get next block)\n");
763       return false;
764     }
765   }
766 
767   return true;
768 }
769 
ProcessBlockEntry(const metadata_map_t & m,const mkvparser::BlockEntry * block_entry)770 bool vttdemux::ProcessBlockEntry(const metadata_map_t& m,
771                                  const mkvparser::BlockEntry* block_entry) {
772   // If the track number for this block is in the cache, then we have
773   // a metadata block, so write the WebVTT cue to the output file.
774 
775   const mkvparser::Block* const block = block_entry->GetBlock();
776   const long long tn = block->GetTrackNumber();  // NOLINT
777 
778   typedef metadata_map_t::const_iterator iter_t;
779   const iter_t i = m.find(static_cast<metadata_map_t::key_type>(tn));
780 
781   if (i == m.end())  // not a metadata track
782     return true;  // nothing else to do
783 
784   if (block_entry->GetKind() != mkvparser::BlockEntry::kBlockGroup)
785     return false;  // weird
786 
787   typedef mkvparser::BlockGroup BG;
788   const BG* const block_group = static_cast<const BG*>(block_entry);
789 
790   const MetadataInfo& info = i->second;
791   FILE* const f = info.file;
792 
793   return WriteCue(f, block_group);
794 }
795 
WriteCue(FILE * f,const mkvparser::BlockGroup * block_group)796 bool vttdemux::WriteCue(FILE* f, const mkvparser::BlockGroup* block_group) {
797   // Bind a FrameParser object to the block, which allows us to
798   // extract each line of text from the payload of the block.
799   FrameParser parser(block_group);
800 
801   // We start a new cue by writing a cue separator (an empty line)
802   // into the stream.
803 
804   if (fputc('\n', f) < 0)
805     return false;
806 
807   // A WebVTT Cue comprises 3 things: a cue identifier, followed by
808   // the cue timings, followed by the payload of the cue.  We write
809   // each part of the cue in sequence.
810 
811   if (!WriteCueIdentifier(f, &parser))
812     return false;
813 
814   if (!WriteCueTimings(f, &parser))
815     return false;
816 
817   if (!WriteCuePayload(f, &parser))
818     return false;
819 
820   return true;
821 }
822 
WriteCueIdentifier(FILE * f,FrameParser * parser)823 bool vttdemux::WriteCueIdentifier(FILE* f, FrameParser* parser) {
824   string line;
825   int e = parser->GetLine(&line);
826 
827   if (e)  // error or EOS
828     return false;
829 
830   // If the cue identifier line is empty, this means that the original
831   // WebVTT cue did not have a cue identifier, so we don't bother
832   // writing an extra line terminator to the output file (though doing
833   // so would be harmless).
834 
835   if (!line.empty()) {
836     if (fputs(line.c_str(), f) < 0)
837       return false;
838 
839     if (fputc('\n', f) < 0)
840       return false;
841   }
842 
843   return true;
844 }
845 
WriteCueTimings(FILE * f,FrameParser * parser)846 bool vttdemux::WriteCueTimings(FILE* f, FrameParser* parser) {
847   const mkvparser::BlockGroup* const block_group = parser->block_group_;
848   const mkvparser::Cluster* const cluster = block_group->GetCluster();
849   const mkvparser::Block* const block = block_group->GetBlock();
850 
851   // A WebVTT Cue "timings" line comprises two parts: the start and
852   // stop time for this cue, followed by the (optional) cue settings,
853   // such as orientation of the rendered text or its size.  Only the
854   // settings part of the cue timings line is stored in the WebM
855   // block.  We reconstruct the start and stop times of the WebVTT cue
856   // from the timestamp and duration of the WebM block.
857 
858   const mkvtime_t start_ns = block->GetTime(cluster);
859 
860   if (!WriteCueTime(f, start_ns))
861     return false;
862 
863   if (fputs(" --> ", f) < 0)
864     return false;
865 
866   const mkvtime_t duration_timecode = block_group->GetDurationTimeCode();
867 
868   if (duration_timecode < 0)
869     return false;
870 
871   const mkvparser::Segment* const segment = cluster->m_pSegment;
872   const mkvparser::SegmentInfo* const info = segment->GetInfo();
873 
874   if (info == NULL)
875     return false;
876 
877   const mkvtime_t timecode_scale = info->GetTimeCodeScale();
878 
879   if (timecode_scale <= 0)
880     return false;
881 
882   const mkvtime_t duration_ns = duration_timecode * timecode_scale;
883   const mkvtime_t stop_ns = start_ns + duration_ns;
884 
885   if (!WriteCueTime(f, stop_ns))
886     return false;
887 
888   string line;
889   int e = parser->GetLine(&line);
890 
891   if (e)  // error or EOS
892     return false;
893 
894   if (!line.empty()) {
895     if (fputc(' ', f) < 0)
896       return false;
897 
898     if (fputs(line.c_str(), f) < 0)
899       return false;
900   }
901 
902   if (fputc('\n', f) < 0)
903     return false;
904 
905   return true;
906 }
907 
WriteCueTime(FILE * f,mkvtime_t time_ns)908 bool vttdemux::WriteCueTime(FILE* f, mkvtime_t time_ns) {
909   mkvtime_t ms = time_ns / 1000000;  // WebVTT time has millisecond resolution
910 
911   mkvtime_t sec = ms / 1000;
912   ms -= sec * 1000;
913 
914   mkvtime_t min = sec / 60;
915   sec -= 60 * min;
916 
917   mkvtime_t hr = min / 60;
918   min -= 60 * hr;
919 
920   if (hr > 0) {
921     if (fprintf(f, "%02lld:", hr) < 0)
922       return false;
923   }
924 
925   if (fprintf(f, "%02lld:%02lld.%03lld", min, sec, ms) < 0)
926     return false;
927 
928   return true;
929 }
930 
WriteCuePayload(FILE * f,FrameParser * parser)931 bool vttdemux::WriteCuePayload(FILE* f, FrameParser* parser) {
932   int count = 0;  // count of lines of payload text written to output file
933   for (string line;;) {
934     const int e = parser->GetLine(&line);
935 
936     if (e < 0)  // error (only -- we allow EOS here)
937       return false;
938 
939     if (line.empty())  // TODO(matthewjheaney): retain this check?
940       break;
941 
942     if (fprintf(f, "%s\n", line.c_str()) < 0)
943       return false;
944 
945     ++count;
946   }
947 
948   if (count <= 0)  // WebVTT cue requires non-empty payload
949     return false;
950 
951   return true;
952 }
953 
954 }  // namespace libwebm
955 
main(int argc,const char * argv[])956 int main(int argc, const char* argv[]) {
957   if (argc != 2) {
958     printf("usage: vttdemux <webmfile>\n");
959     return EXIT_SUCCESS;
960   }
961 
962   const char* const filename = argv[1];
963   mkvparser::MkvReader reader;
964 
965   int e = reader.Open(filename);
966 
967   if (e) {  // error
968     printf("unable to open file\n");
969     return EXIT_FAILURE;
970   }
971 
972   libwebm::vttdemux::mkvpos_t pos;
973 
974   if (!libwebm::vttdemux::ParseHeader(&reader, &pos))
975     return EXIT_FAILURE;
976 
977   libwebm::vttdemux::segment_ptr_t segment_ptr;
978 
979   if (!libwebm::vttdemux::ParseSegment(&reader, pos, &segment_ptr))
980     return EXIT_FAILURE;
981 
982   libwebm::vttdemux::metadata_map_t metadata_map;
983 
984   BuildMap(segment_ptr.get(), &metadata_map);
985 
986   if (metadata_map.empty()) {
987     printf("no WebVTT metadata found\n");
988     return EXIT_FAILURE;
989   }
990 
991   if (!OpenFiles(&metadata_map, filename)) {
992     CloseFiles(&metadata_map);  // nothing to flush, so not strictly necessary
993     return EXIT_FAILURE;
994   }
995 
996   if (!WriteFiles(metadata_map, segment_ptr.get())) {
997     CloseFiles(&metadata_map);  // might as well flush what we do have
998     return EXIT_FAILURE;
999   }
1000 
1001   CloseFiles(&metadata_map);
1002 
1003   return EXIT_SUCCESS;
1004 }
1005