vttdemux: initial revision

vttdemux is a tool for demuxing a webm file containing
WebVTT metadata tracks, extracting the embedded metadata
from each track and storing it as a standalone WebVTT file.

Change-Id: I8897b3dc502c49c92f5b79925939baa5a9490aaa
diff --git a/Makefile b/Makefile
index 6eb82f4..a2186a8 100644
--- a/Makefile
+++ b/Makefile
@@ -8,8 +8,9 @@
 OBJECTS1  := sample.o
 OBJECTS2  := sample_muxer.o vttreader.o webvttparser.o sample_muxer_metadata.o
 OBJECTS3  := dumpvtt.o vttreader.o webvttparser.o
+OBJECTS4  := vttdemux.o webvttparser.o
 INCLUDES  := -I.
-EXES      := samplemuxer sample dumpvtt
+EXES      := samplemuxer sample dumpvtt vttdemux
 
 all: $(EXES)
 
@@ -24,6 +25,9 @@
 
 shared: $(LIBWEBMSO)
 
+vttdemux: $(OBJECTS4) $(LIBWEBMA)
+	$(CXX) $^ -o $@
+
 libwebm.a: $(OBJSA)
 	$(AR) rcs $@ $^
 
@@ -40,4 +44,4 @@
 	$(CXX) -c $(CXXFLAGS) -fPIC $(INCLUDES) $< -o $@
 
 clean:
-	$(RM) -f $(OBJECTS1) $(OBJECTS2) $(OBJECTS3) $(OBJSA) $(OBJSSO) $(LIBWEBMA) $(LIBWEBMSO) $(EXES) Makefile.bak
+	$(RM) -f $(OBJECTS1) $(OBJECTS2) $(OBJECTS3) $(OBJECTS4) $(OBJSA) $(OBJSSO) $(LIBWEBMA) $(LIBWEBMSO) $(EXES) Makefile.bak
diff --git a/vttdemux.cc b/vttdemux.cc
new file mode 100644
index 0000000..fe0584f
--- /dev/null
+++ b/vttdemux.cc
@@ -0,0 +1,736 @@
+// Copyright (c) 2012 The WebM project authors. All Rights Reserved.
+//
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file in the root of the source
+// tree. An additional intellectual property rights grant can be found
+// in the file PATENTS.  All contributing project authors may
+// be found in the AUTHORS file in the root of the source tree.
+
+#include <cstdio>
+#include <cstring>
+#include <map>
+#include <memory>
+#include <string>
+#include "./mkvparser.hpp"
+#include "./mkvreader.hpp"
+#include "./webvttparser.h"
+
+using std::string;
+
+namespace vttdemux {
+
+typedef long long mkvtime_t;  // NOLINT
+typedef long long mkvpos_t;  // NOLINT
+typedef std::auto_ptr<mkvparser::Segment> segment_ptr_t;
+
+// WebVTT metadata tracks have a type (encoded in the CodecID for the track).
+// We use |type| to synthesize a filename for the out-of-band WebVTT |file|.
+struct MetadataInfo {
+  enum Type { kSubtitles, kCaptions, kDescriptions, kMetadata } type;
+  FILE* file;
+};
+
+// We use a map, indexed by track number, to collect information about
+// each track in the input file.
+typedef std::map<long, MetadataInfo> metadata_map_t;  // NOLINT
+
+// The data from the original WebVTT Cue is stored as a WebM block.
+// The FrameParser is used to parse the lines of text out from the
+// block, in order to reconstruct the original WebVTT Cue.
+class FrameParser : public libwebvtt::LineReader {
+ public:
+  //  Bind the FrameParser instance to a WebM block.
+  explicit FrameParser(const mkvparser::BlockGroup* block_group);
+  virtual ~FrameParser();
+
+  // The Webm block (group) to which this instance is bound.  We
+  // treat the payload of the block as a stream of characters.
+  const mkvparser::BlockGroup* const block_group_;
+
+ protected:
+  // Read the next character from the character stream (the payload
+  // of the WebM block).  We increment the stream pointer |pos_| as
+  // each character from the stream is consumed.
+  virtual int GetChar(char* c);
+
+  // End-of-line handling requires that we put a character back into
+  // the stream.  Here we need only decrement the stream pointer |pos_|
+  // to unconsume the character.
+  virtual void UngetChar(char c);
+
+  // The current position in the character stream (the payload of the block).
+  mkvpos_t pos_;
+
+  // The position of the end of the character stream. When the current
+  // position |pos_| equals the end position |pos_end_|, the entire
+  // stream (block payload) has been consumed and end-of-stream is indicated.
+  mkvpos_t pos_end_;
+
+ private:
+  // Disable copy ctor and copy assign
+  FrameParser(const FrameParser&);
+  FrameParser& operator=(const FrameParser&);
+};
+
+// Parse the EBML header of the WebM input file, to determine whether we
+// actually have a WebM file.  Returns false if this is not a WebM file.
+bool ParseHeader(mkvparser::IMkvReader* reader, mkvpos_t* pos);
+
+// Parse the Segment of the input file and load all of its clusters.
+// Returns false if there was an error parsing the file.
+bool ParseSegment(
+    mkvparser::IMkvReader* reader,
+    mkvpos_t pos,
+    segment_ptr_t* segment);
+
+// Iterate over the tracks of the input file and cache information about
+// each metadata track.
+void BuildMap(const mkvparser::Segment* segment, metadata_map_t* metadata_map);
+
+// For each track listed in the cache, synthesize its output filename
+// and open a file handle that designates the out-of-band file.
+// Returns false if we were unable to open an output file for a track.
+bool OpenFiles(metadata_map_t* metadata_map, const char* filename);
+
+// Close the file handle for each track in the cache.
+void CloseFiles(metadata_map_t* metadata_map);
+
+// Iterate over the clusters of the input file, and write a WebVTT cue
+// for each metadata block.  Returns false if processing of a cluster
+// failed.
+bool WriteFiles(const metadata_map_t& m, mkvparser::Segment* s);
+
+// Write the WebVTT header for each track in the cache.  We do this
+// immediately before writing the actual WebVTT cues.  Returns false
+// if the write failed.
+bool InitializeFiles(const metadata_map_t& metadata_map);
+
+// Iterate over the blocks of the |cluster|, writing a WebVTT cue to
+// its associated output file for each block of metadata.  Returns
+// false if processing a block failed, or there was a parse error.
+bool ProcessCluster(
+    const metadata_map_t& metadata_map,
+    const mkvparser::Cluster* cluster);
+
+// Look up this track number in the cache, and if found (meaning this
+// is a metadata track), write a WebVTT cue to the associated output
+// file.  Returns false if writing the WebVTT cue failed.
+bool ProcessBlockEntry(
+    const metadata_map_t& metadata_map,
+    const mkvparser::BlockEntry* block_entry);
+
+// Parse the lines of text from the |block_group| to reconstruct the
+// original WebVTT cue, and write it to the associated output |file|.
+// Returns false if there was an error writing to the output file.
+bool WriteCue(FILE* file, const mkvparser::BlockGroup* block_group);
+
+// Consume a line of text from the character stream, and if the line
+// is not empty write the cue identifier to the associated output
+// file.  Returns false if there was an error writing to the file.
+bool WriteCueIdentifier(FILE* f, FrameParser* parser);
+
+// Consume a line of text from the character stream (which holds any
+// cue settings) and write the cue timings line for this cue to the
+// associated output file.  Returns false if there was an error
+// writing to the file.
+bool WriteCueTimings(
+    FILE* f,
+    FrameParser* parser);
+
+// Write the timestamp (representating either the start time or stop
+// time of the cue) to the output file.  Returns false if there was an
+// error writing to the file.
+bool WriteCueTime(
+    FILE* f,
+    mkvtime_t time_ns);
+
+// Consume the remaining lines of text from the character stream
+// (these lines are the actual payload of the WebVTT cue), and write
+// them to the associated output file.  Returns false if there was an
+// error writing to the file.
+bool WriteCuePayload(
+    FILE* f,
+    FrameParser* parser);
+}  // namespace vttdemux
+
+int main(int argc, const char* argv[]) {
+  if (argc != 2) {
+    printf("usage: vttdemux <webmfile>\n");
+    return EXIT_SUCCESS;
+  }
+
+  const char* const filename = argv[1];
+  mkvparser::MkvReader reader;
+
+  int e = reader.Open(filename);
+
+  if (e) {  // error
+    printf("unable to open file\n");
+    return EXIT_FAILURE;
+  }
+
+  vttdemux::mkvpos_t pos;
+
+  if (!vttdemux::ParseHeader(&reader, &pos))
+    return EXIT_FAILURE;
+
+  vttdemux::segment_ptr_t segment_ptr;
+
+  if (!vttdemux::ParseSegment(&reader, pos, &segment_ptr))
+    return EXIT_FAILURE;
+
+  vttdemux::metadata_map_t metadata_map;
+
+  BuildMap(segment_ptr.get(), &metadata_map);
+
+  if (metadata_map.empty()) {
+    printf("no metadata tracks found\n");
+    return EXIT_FAILURE;  // TODO(matthewjheaney): correct result?
+  }
+
+  if (!OpenFiles(&metadata_map, filename)) {
+    CloseFiles(&metadata_map);  // nothing to flush, so not strictly necessary
+    return EXIT_FAILURE;
+  }
+
+  if (!WriteFiles(metadata_map, segment_ptr.get())) {
+    CloseFiles(&metadata_map);  // might as well flush what we do have
+    return EXIT_FAILURE;
+  }
+
+  CloseFiles(&metadata_map);
+
+  return EXIT_SUCCESS;
+}
+
+namespace vttdemux {
+
+FrameParser::FrameParser(const mkvparser::BlockGroup* block_group)
+    : block_group_(block_group) {
+  const mkvparser::Block* const block = block_group->GetBlock();
+  const mkvparser::Block::Frame& f = block->GetFrame(0);
+
+  // The beginning and end of the character stream corresponds to the
+  // position of this block's frame within the WebM input file.
+
+  pos_ = f.pos;
+  pos_end_ = f.pos + f.len;
+}
+
+FrameParser::~FrameParser() {
+}
+
+int FrameParser::GetChar(char* c) {
+  if (pos_ >= pos_end_)  // end-of-stream
+    return 1;  // per the semantics of libwebvtt::Reader::GetChar
+
+  const mkvparser::Cluster* const cluster = block_group_->GetCluster();
+  const mkvparser::Segment* const segment = cluster->m_pSegment;
+  mkvparser::IMkvReader* const reader = segment->m_pReader;
+
+  unsigned char* const buf = reinterpret_cast<unsigned char*>(c);
+  const int result = reader->Read(pos_, 1, buf);
+
+  if (result < 0)  // error
+    return -1;
+
+  ++pos_;  // consume this character in the stream
+  return 0;
+}
+
+void FrameParser::UngetChar(char /* c */ ) {
+  // All we need to do here is decrement the position in the stream.
+  // The next time GetChar is called the same character will be
+  // re-read from the input file.
+  --pos_;
+}
+
+}  // namespace vttdemux
+
+bool vttdemux::ParseHeader(
+    mkvparser::IMkvReader* reader,
+    mkvpos_t* pos) {
+  mkvparser::EBMLHeader h;
+  const mkvpos_t status = h.Parse(reader, *pos);
+
+  if (status) {
+    printf("error parsing EBML header\n");
+    return false;
+  }
+
+  if (strcmp(h.m_docType, "webm") != 0) {
+    printf("bad doctype\n");
+    return false;
+  }
+
+  return true;  // success
+}
+
+bool vttdemux::ParseSegment(
+    mkvparser::IMkvReader* reader,
+    mkvpos_t pos,
+    segment_ptr_t* segment_ptr) {
+  // We first create the segment object.
+
+  mkvparser::Segment* p;
+  const mkvpos_t create = mkvparser::Segment::CreateInstance(reader, pos, p);
+
+  if (create) {
+    printf("error parsing segment element\n");
+    return false;
+  }
+
+  segment_ptr->reset(p);
+
+  // Now parse all of the segment's sub-elements, in toto.
+
+  const long status = p->Load();  // NOLINT
+
+  if (status < 0) {
+    printf("error loading segment\n");
+    return false;
+  }
+
+  return true;
+}
+
+void vttdemux::BuildMap(
+    const mkvparser::Segment* segment,
+    metadata_map_t* map_ptr) {
+  const mkvparser::Tracks* const tt = segment->GetTracks();
+  if (tt == NULL)
+    return;
+
+  const long tc = tt->GetTracksCount();  // NOLINT
+  if (tc <= 0)
+    return;
+
+  metadata_map_t& m = *map_ptr;
+
+  // Iterate over the tracks in the intput file.  We determine whether
+  // a track holds metadata by inspecting its CodecID.
+
+  for (long idx = 0; idx < tc; ++idx) {  // NOLINT
+    const mkvparser::Track* const t = tt->GetTrackByIndex(idx);
+
+    if (t == NULL)  // weird
+      continue;
+
+    const char* const codec_id = t->GetCodecId();
+
+    if (codec_id == NULL)  // weird
+      continue;
+
+    MetadataInfo info;
+    info.file = NULL;
+
+    if (strcmp(codec_id, "D_WEBVTT/SUBTITLES") == 0) {
+      info.type = MetadataInfo::kSubtitles;
+    } else if (strcmp(codec_id, "D_WEBVTT/CAPTIONS") == 0) {
+      info.type = MetadataInfo::kCaptions;
+    } else if (strcmp(codec_id, "D_WEBVTT/DESCRIPTIONS") == 0) {
+      info.type = MetadataInfo::kDescriptions;
+    } else if (strcmp(codec_id, "D_WEBVTT/METADATA") == 0) {
+      info.type = MetadataInfo::kMetadata;
+    } else {
+      continue;
+    }
+
+    const long tn = t->GetNumber();  // NOLINT
+    m[tn] = info;  // create an entry in the cache for this track
+  }
+}
+
+bool vttdemux::OpenFiles(metadata_map_t* metadata_map, const char* filename) {
+  if (metadata_map == NULL || metadata_map->empty())
+    return false;
+
+  if (filename == NULL)
+    return false;
+
+  // Find the position of the filename extension.  We synthesize the
+  // output filename from the directory path and basename of the input
+  // filename.
+
+  const char* const ext = strrchr(filename, '.');
+
+  if (ext == NULL)  // TODO(matthewjheaney): liberalize?
+    return false;
+
+  // Remember whether a track of this type has already been seen (the
+  // map key) by keeping a count (the map item).  We quality the
+  // output filename with the track number if there is more than one
+  // track having a given type.
+
+  std::map<MetadataInfo::Type, int> exists;
+
+  typedef metadata_map_t::iterator iter_t;
+
+  metadata_map_t& m = *metadata_map;
+  const iter_t ii = m.begin();
+  const iter_t j = m.end();
+
+  // Make a first pass over the cache to determine whether there is
+  // more than one track corresponding to a given metadata type.
+
+  iter_t i = ii;
+  while (i != j) {
+    const metadata_map_t::value_type& v = *i++;
+    const MetadataInfo& info = v.second;
+    const MetadataInfo::Type type = info.type;
+    ++exists[type];
+  }
+
+  // Make a second pass over the cache, synthesizing the filename of
+  // each output file (from the input file basename, the input track
+  // metadata type, and its track number if necessary), and then
+  // opening a WebVTT output file having that filename.
+
+  i = ii;
+  while (i != j) {
+    metadata_map_t::value_type& v = *i++;
+    MetadataInfo& info = v.second;
+    const MetadataInfo::Type type = info.type;
+
+    // Start with the basename of the input file.
+
+    string name(filename, ext);
+
+    // Next append the metadata kind.
+
+    switch (type) {
+      case MetadataInfo::kSubtitles:
+        name += "_SUBTITLES";
+        break;
+
+      case MetadataInfo::kCaptions:
+        name += "_CAPTIONS";
+        break;
+
+      case MetadataInfo::kDescriptions:
+        name += "_DESCRIPTIONS";
+        break;
+
+      case MetadataInfo::kMetadata:
+        name += "_METADATA";
+        break;
+
+      default:
+        return false;
+    }
+
+    // If there is more than one metadata track having a given type
+    // (the WebVTT-in-WebM spec doesn't preclude this), then qualify
+    // the output filename with the input track number.
+
+    if (exists[type] > 1) {
+      enum { kLen = 33 };
+      char str[kLen];  // max 126 tracks, so only 4 chars really needed
+      snprintf(str, kLen, "%ld", v.first);  // track number
+      name += str;
+    }
+
+    // Finally append the output filename extension.
+
+    name += ".vtt";
+
+    // We have synthesized the full output filename, so attempt to
+    // open the WebVTT output file.
+
+    info.file = fopen(name.c_str(), "wb");
+
+    if (info.file == NULL) {
+      printf("unable to open output file %s\n", name.c_str());
+      return false;
+    }
+  }
+
+  return true;
+}
+
+void vttdemux::CloseFiles(metadata_map_t* metadata_map) {
+  if (metadata_map == NULL)
+    return;
+
+  metadata_map_t& m = *metadata_map;
+
+  typedef metadata_map_t::iterator iter_t;
+
+  iter_t i = m.begin();
+  const iter_t j = m.end();
+
+  // Gracefully close each output file, to ensure all output gets
+  // propertly flushed.
+
+  while (i != j) {
+    metadata_map_t::value_type& v = *i++;
+    MetadataInfo& info = v.second;
+
+    fclose(info.file);
+    info.file = NULL;
+  }
+}
+
+bool vttdemux::WriteFiles(const metadata_map_t& m, mkvparser::Segment* s) {
+  // First write the WebVTT header.
+
+  InitializeFiles(m);
+
+  // Now iterate over the clusters, writing the WebVTT cue as we parse
+  // each metadata block.
+
+  const mkvparser::Cluster* cluster = s->GetFirst();
+
+  while (cluster != NULL && !cluster->EOS()) {
+    if (!ProcessCluster(m, cluster))
+      return false;
+
+    cluster = s->GetNext(cluster);
+  }
+
+  return true;
+}
+
+bool vttdemux::InitializeFiles(const metadata_map_t& m) {
+  // Write the WebVTT header for each output file in the cache.
+
+  typedef metadata_map_t::const_iterator iter_t;
+  iter_t i = m.begin();
+  const iter_t j = m.end();
+
+  while (i != j) {
+    const metadata_map_t::value_type& v = *i++;
+    const MetadataInfo& info = v.second;
+    FILE* const f = info.file;
+
+    if (fputs("WEBVTT\n", f) < 0) {
+      printf("unable to initialize output file\n");
+      return false;
+    }
+  }
+
+  return true;
+}
+
+bool vttdemux::ProcessCluster(
+    const metadata_map_t& m,
+    const mkvparser::Cluster* c) {
+  // Visit the blocks in this cluster, writing a WebVTT cue for each
+  // metadata block.
+
+  const mkvparser::BlockEntry* block_entry;
+
+  long result = c->GetFirst(block_entry);  // NOLINT
+  if (result < 0) {  // error
+    printf("bad cluster (unable to get first block)\n");
+    return false;
+  }
+
+  while (block_entry != NULL && !block_entry->EOS()) {
+    if (!ProcessBlockEntry(m, block_entry))
+      return false;
+
+    result = c->GetNext(block_entry, block_entry);
+    if (result < 0) {  // error
+      printf("bad cluster (unable to get next block)\n");
+      return false;
+    }
+  }
+
+  return true;
+}
+
+bool vttdemux::ProcessBlockEntry(
+    const metadata_map_t& m,
+    const mkvparser::BlockEntry* block_entry) {
+  // If the track number for this block is in the cache, then we have
+  // a metadata block, so write the WebVTT cue to the output file.
+
+  const mkvparser::Block* const block = block_entry->GetBlock();
+  const long long tn = block->GetTrackNumber();  // NOLINT
+
+  typedef metadata_map_t::const_iterator iter_t;
+  const iter_t i = m.find(tn);
+
+  if (i == m.end())  // not a metadata track
+    return true;     // nothing else to do
+
+  if (block_entry->GetKind() != mkvparser::BlockEntry::kBlockGroup)
+    return false;  // weird
+
+  typedef mkvparser::BlockGroup BG;
+  const BG* const block_group = static_cast<const BG*>(block_entry);
+
+  const MetadataInfo& info = i->second;
+  FILE* const f = info.file;
+
+  return WriteCue(f, block_group);
+}
+
+bool vttdemux::WriteCue(
+    FILE* f,
+    const mkvparser::BlockGroup* block_group) {
+  // Bind a FrameParser object to the block, which allows us to
+  // extract each line of text from the payload of the block.
+  FrameParser parser(block_group);
+
+  // We start a new cue by writing a cue separator (an empty line)
+  // into the stream.
+
+  if (fputc('\n', f) < 0)
+    return false;
+
+  // A WebVTT Cue comprises 3 things: a cue identifier, followed by
+  // the cue timings, followed by the payload of the cue.  We write
+  // each part of the cue in sequence.
+
+  if (!WriteCueIdentifier(f, &parser))
+    return false;
+
+  if (!WriteCueTimings(f, &parser))
+    return false;
+
+  if (!WriteCuePayload(f, &parser))
+    return false;
+
+  return true;
+}
+
+bool vttdemux::WriteCueIdentifier(
+    FILE* f,
+    FrameParser* parser) {
+  string line;
+  int e = parser->GetLine(&line);
+
+  if (e)  // error or EOS
+    return false;
+
+  // If the cue identifier line is empty, this means that the original
+  // WebVTT cue did not have a cue identifier, so we don't bother
+  // writing an extra line terminator to the output file (though doing
+  // so would be harmless).
+
+  if (!line.empty()) {
+    if (fputs(line.c_str(), f) < 0)
+      return false;
+
+    if (fputc('\n', f) < 0)
+      return false;
+  }
+
+  return true;
+}
+
+bool vttdemux::WriteCueTimings(
+    FILE* f,
+    FrameParser* parser) {
+  const mkvparser::BlockGroup* const block_group = parser->block_group_;
+  const mkvparser::Cluster* const cluster = block_group->GetCluster();
+  const mkvparser::Block* const block = block_group->GetBlock();
+
+  // A WebVTT Cue "timings" line comprises two parts: the start and
+  // stop time for this cue, followed by the (optional) cue settings,
+  // such as orientation of the rendered text or its size.  Only the
+  // settings part of the cue timings line is stored in the WebM
+  // block.  We reconstruct the start and stop times of the WebVTT cue
+  // from the timestamp and duration of the WebM block.
+
+  const mkvtime_t start_ns = block->GetTime(cluster);
+
+  if (!WriteCueTime(f, start_ns))
+    return false;
+
+  if (fputs(" --> ", f) < 0)
+    return false;
+
+  const mkvtime_t duration_timecode = block_group->GetDurationTimeCode();
+
+  if (duration_timecode < 0)
+    return false;
+
+  const mkvparser::Segment* const segment = cluster->m_pSegment;
+  const mkvparser::SegmentInfo* const info = segment->GetInfo();
+
+  if (info == NULL)
+    return false;
+
+  const mkvtime_t timecode_scale = info->GetTimeCodeScale();
+
+  if (timecode_scale <= 0)
+    return false;
+
+  const mkvtime_t duration_ns = duration_timecode * timecode_scale;
+  const mkvtime_t stop_ns = start_ns + duration_ns;
+
+  if (!WriteCueTime(f, stop_ns))
+    return false;
+
+  string line;
+  int e = parser->GetLine(&line);
+
+  if (e)  // error or EOS
+    return false;
+
+  if (!line.empty()) {
+    if (fputc(' ', f) < 0)
+      return false;
+
+    if (fputs(line.c_str(), f) < 0)
+      return false;
+  }
+
+  if (fputc('\n', f) < 0)
+    return false;
+
+  return true;
+}
+
+bool vttdemux::WriteCueTime(
+    FILE* f,
+    mkvtime_t time_ns) {
+  mkvtime_t ms = time_ns / 1000000;  // WebVTT time has millisecond resolution
+
+  mkvtime_t sec = ms / 1000;
+  ms -= sec * 1000;
+
+  mkvtime_t min = sec / 60;
+  sec -= 60 * min;
+
+  mkvtime_t hr = min / 60;
+  min -= 60 * hr;
+
+  if (hr > 0) {
+    if (fprintf(f, "%02lld:", hr) < 0)
+      return false;
+  }
+
+  if (fprintf(f, "%02lld:%02lld.%03lld", min, sec, ms) < 0)
+      return false;
+
+  return true;
+}
+
+bool vttdemux::WriteCuePayload(
+    FILE* f,
+    FrameParser* parser) {
+  int count = 0;  // count of lines of payload text written to output file
+  for (string line;;) {
+    const int e = parser->GetLine(&line);
+
+    if (e < 0)  // error (only -- we allow EOS here)
+      return false;
+
+    if (line.empty())  // TODO(matthewjheaney): retain this check?
+      break;
+
+    if (fprintf(f, "%s\n", line.c_str()) < 0)
+      return false;
+
+    ++count;
+  }
+
+  if (count <= 0)  // WebVTT cue requires non-empty payload
+    return false;
+
+  return true;
+}