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