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