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