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