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