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