WebVTT Output

This change introduces handlers to output WebVtt text files. There is
only one output but there is a common base to support others.

WebVttOutputHandler which handles all communication with other handlers
and WebVttSegmentedOutputHandler is responsible for listening for events
and choosing when and where to write cues and headers.

Bug: 36138902
Change-Id: I2b13a94262554398e66fee8cf024aa21041ddbab
This commit is contained in:
Aaron Vaage 2017-05-22 09:35:49 -07:00
parent 9e7b5c1ca9
commit c88fe54553
7 changed files with 418 additions and 3 deletions

View File

@ -31,8 +31,9 @@ class TextSample {
void AppendPayload(const std::string& payload);
private:
TextSample(const TextSample&) = delete;
TextSample& operator=(const TextSample&) = delete;
// Allow the compiler generated copy constructor and assignment operator
// intentionally. Since the text data is typically small, the performance
// impact is minimal.
std::string id_;
uint64_t start_time_ = 0;

View File

@ -35,7 +35,8 @@ class MuxerListener {
kContainerUnknown = 0,
kContainerMp4,
kContainerMpeg2ts,
kContainerWebM
kContainerWebM,
kContainerText
};
/// Structure for specifying ranges within a media file. This is mainly for

View File

@ -60,6 +60,9 @@ void SetMediaInfoContainerType(MuxerListener::ContainerType container_type,
case MuxerListener::kContainerWebM:
media_info->set_container_type(MediaInfo::CONTAINER_WEBM);
break;
case MuxerListener::kContainerText:
media_info->set_container_type(MediaInfo::CONTAINER_TEXT);
break;
default:
NOTREACHED() << "Unknown container type " << container_type;
}

View File

@ -19,6 +19,8 @@
'text_readers.h',
'webvtt_media_parser.cc',
'webvtt_media_parser.h',
'webvtt_output_handler.cc',
'webvtt_output_handler.h',
'webvtt_parser.cc',
'webvtt_parser.h',
'webvtt_sample_converter.cc',
@ -43,6 +45,7 @@
'sources': [
'text_readers_unittest.cc',
'webvtt_media_parser_unittest.cc',
'webvtt_output_handler_unittest.cc',
'webvtt_parser_unittest.cc',
'webvtt_sample_converter_unittest.cc',
'webvtt_segmenter_unittest.cc',

View File

@ -0,0 +1,166 @@
// Copyright 2017 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#include "packager/media/formats/webvtt/webvtt_output_handler.h"
#include "packager/base/logging.h"
#include "packager/file/file.h"
#include "packager/media/base/muxer_util.h"
#include "packager/media/formats/webvtt/webvtt_timestamp.h"
namespace shaka {
namespace media {
void WebVttOutputHandler::WriteCue(const std::string& id,
uint64_t start_ms,
uint64_t end_ms,
const std::string& settings,
const std::string& payload) {
// Build a block of text that makes up the cue so that we can use a loop to
// write all the lines.
const std::string start = MsToWebVttTimestamp(start_ms);
const std::string end = MsToWebVttTimestamp(end_ms);
// Ids are optional
if (id.length()) {
buffer_.append(id);
buffer_.append("\n"); // end of id
}
buffer_.append(start);
buffer_.append(" --> ");
buffer_.append(end);
// Settings are optional
if (settings.length()) {
buffer_.append(" ");
buffer_.append(settings);
}
buffer_.append("\n"); // end of time & settings
buffer_.append(payload);
buffer_.append("\n"); // end of payload
buffer_.append("\n"); // end of cue
}
Status WebVttOutputHandler::WriteSegmentToFile(const std::string& filename) {
// Need blank line between "WEBVTT" and the first cue
const std::string WEBVTT_HEADER = "WEBVTT\n\n";
File* file = File::Open(filename.c_str(), "w");
if (file == nullptr) {
return Status(error::FILE_FAILURE, "Failed to open " + filename);
}
size_t written;
written = file->Write(WEBVTT_HEADER.c_str(), WEBVTT_HEADER.size());
if (written != WEBVTT_HEADER.size()) {
return Status(error::FILE_FAILURE, "Failed to write webvtt header to file");
}
written = file->Write(buffer_.c_str(), buffer_.size());
if (written != buffer_.size()) {
return Status(error::FILE_FAILURE,
"Failed to write webvtt cotnent to file");
}
// Since all the cues have been written to disk, there is no reason to hold
// onto that information anymore.
buffer_.clear();
bool closed = file->Close();
if (!closed) {
return Status(error::FILE_FAILURE, "Failed to close " + filename);
}
return Status::OK;
}
Status WebVttOutputHandler::InitializeInternal() {
return Status::OK;
}
Status WebVttOutputHandler::Process(std::unique_ptr<StreamData> stream_data) {
switch (stream_data->stream_data_type) {
case StreamDataType::kStreamInfo:
return OnStreamInfo(*stream_data->stream_info);
case StreamDataType::kSegmentInfo:
return OnSegmentInfo(*stream_data->segment_info);
case StreamDataType::kTextSample:
return OnTextSample(*stream_data->text_sample);
default:
return Status(error::INTERNAL_ERROR,
"Invalid stream data type for this handler");
}
}
Status WebVttOutputHandler::OnFlushRequest(size_t input_stream_index) {
OnStreamEnd();
return Status::OK;
}
WebVttSegmentedOutputHandler::WebVttSegmentedOutputHandler(
const MuxerOptions& muxer_options,
std::unique_ptr<MuxerListener> muxer_listener)
: muxer_options_(muxer_options),
muxer_listener_(std::move(muxer_listener)) {}
Status WebVttSegmentedOutputHandler::OnStreamInfo(const StreamInfo& info) {
muxer_listener_->OnMediaStart(muxer_options_, info, info.time_scale(),
MuxerListener::kContainerText);
return Status::OK;
}
Status WebVttSegmentedOutputHandler::OnSegmentInfo(const SegmentInfo& info) {
total_duration_ms_ += info.duration;
const std::string& segment_template = muxer_options_.segment_template;
const uint32_t index = segment_index_++;
const uint64_t start = info.start_timestamp;
const uint64_t duration = info.duration;
const uint32_t bandwidth = 0;
// Write all the samples to the file.
const std::string filename =
GetSegmentName(segment_template, start, index, bandwidth);
// Write everything to the file before telling the manifest so that the
// file will exist on disk.
Status write_status = WriteSegmentToFile(filename);
if (!write_status.ok()) {
return write_status;
}
// Update the manifest with our new file.
const uint64_t size = File::GetFileSize(filename.c_str());
muxer_listener_->OnNewSegment(filename, start, duration, size);
return Status::OK;
}
Status WebVttSegmentedOutputHandler::OnTextSample(const TextSample& sample) {
const std::string& id = sample.id();
const uint64_t start_ms = sample.start_time();
const uint64_t end_ms = sample.EndTime();
const std::string& settings = sample.settings();
const std::string& payload = sample.payload();
WriteCue(id, start_ms, end_ms, settings, payload);
return Status::OK;
}
Status WebVttSegmentedOutputHandler::OnStreamEnd() {
const float duration_ms = static_cast<float>(total_duration_ms_);
const float duration_seconds = duration_ms / 1000.0f;
MuxerListener::MediaRanges empty_ranges;
muxer_listener_->OnMediaEnd(empty_ranges, duration_seconds);
return Status::OK;
}
} // namespace media
} // namespace shaka

View File

@ -0,0 +1,86 @@
// Copyright 2017 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#ifndef PACKAGER_MEDIA_FORMATS_WEBVTT_WEBVTT_TEXT_HANDLER_H_
#define PACKAGER_MEDIA_FORMATS_WEBVTT_WEBVTT_TEXT_HANDLER_H_
#include <stdint.h>
#include <vector>
#include "packager/media/base/media_handler.h"
#include "packager/media/base/muxer_options.h"
#include "packager/media/event/muxer_listener.h"
namespace shaka {
namespace media {
// WebVttOutputHandler is the base class for all WebVtt text output handlers.
// It handles taking in the samples and writing the text out, but relies on
// sub classes to handle the logic of when and where to write the information.
class WebVttOutputHandler : public MediaHandler {
public:
WebVttOutputHandler() = default;
virtual ~WebVttOutputHandler() = default;
protected:
virtual Status OnStreamInfo(const StreamInfo& info) = 0;
virtual Status OnSegmentInfo(const SegmentInfo& info) = 0;
virtual Status OnTextSample(const TextSample& sample) = 0;
virtual Status OnStreamEnd() = 0;
// Top level functions for output. These functions should be used by
// subclasses to write to files.
void WriteCue(const std::string& id,
uint64_t start,
uint64_t end,
const std::string& settings,
const std::string& payload);
// Writes the current state of the current segment to disk. This will
// reset the internal state and set it up for the next segment.
Status WriteSegmentToFile(const std::string& filename);
private:
WebVttOutputHandler(const WebVttOutputHandler&) = delete;
WebVttOutputHandler& operator=(const WebVttOutputHandler&) = delete;
Status InitializeInternal() override;
Status Process(std::unique_ptr<StreamData> stream_data) override;
Status OnFlushRequest(size_t input_stream_index) override;
// A buffer of characters waiting to be written to a file.
std::string buffer_;
};
// This WebVttt output handler should only be used when the source WebVTT
// content needs to be segmented across multiple files.
class WebVttSegmentedOutputHandler : public WebVttOutputHandler {
public:
WebVttSegmentedOutputHandler(const MuxerOptions& muxer_options,
std::unique_ptr<MuxerListener> muxer_listener);
private:
Status OnStreamInfo(const StreamInfo& info) override;
Status OnSegmentInfo(const SegmentInfo& info) override;
Status OnTextSample(const TextSample& sample) override;
Status OnStreamEnd() override;
Status OnSegmentEnded();
void GoToNextSegment(uint64_t start_time_ms);
const MuxerOptions muxer_options_;
std::unique_ptr<MuxerListener> muxer_listener_;
// Sum together all segment durations so we know how long the stream is.
uint64_t total_duration_ms_ = 0;
uint32_t segment_index_ = 0;
};
} // namespace media
} // namespace shaka
#endif // PACKAGER_MEDIA_FORMATS_WEBVTT_WEBVTT_TEXT_HANDLER_H_

View File

@ -0,0 +1,155 @@
// Copyright 2017 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "packager/file/file_test_util.h"
#include "packager/media/base/media_handler_test_base.h"
#include "packager/media/base/text_stream_info.h"
#include "packager/media/event/combined_muxer_listener.h"
#include "packager/media/formats/webvtt/webvtt_output_handler.h"
#include "packager/status_test_util.h"
namespace shaka {
namespace media {
namespace {
const size_t kInputCount = 1;
const size_t kOutputCount = 0;
const size_t kInputIndex = 0;
const size_t kStreamIndex = 0;
const bool kEncrypted = true;
const char* kNoId = "";
const char* kSegmentedFileTemplate = "memory://output/template-$Number$.vtt";
const char* kSegmentedFileOutput1 = "memory://output/template-1.vtt";
const char* kSegmentedFileOutput2 = "memory://output/template-2.vtt";
} // namespace
class WebVttSegmentedOutputTest : public MediaHandlerTestBase {
protected:
void SetUp() {
MuxerOptions muxer_options;
muxer_options.segment_template = kSegmentedFileTemplate;
std::unique_ptr<MuxerListener> muxer_listener(new CombinedMuxerListener());
out_ = std::make_shared<WebVttSegmentedOutputHandler>(
muxer_options, std::move(muxer_listener));
ASSERT_OK(SetUpAndInitializeGraph(out_, kInputCount, kOutputCount));
}
std::shared_ptr<WebVttSegmentedOutputHandler> out_;
};
TEST_F(WebVttSegmentedOutputTest, WithNoSegmentAndWithNoSamples) {
// Expected output - No files should be created as there were no
// samples.
ASSERT_OK(Input(kInputIndex)
->Dispatch(StreamData::FromStreamInfo(kStreamIndex,
GetTextStreamInfo())));
ASSERT_OK(
Input(kInputIndex)
->Dispatch(StreamData::FromSegmentInfo(
kStreamIndex, GetSegmentInfo(kStreamIndex, 10000, !kEncrypted))));
ASSERT_OK(Input(kInputIndex)->FlushAllDownstreams());
}
TEST_F(WebVttSegmentedOutputTest, WithOneSegmentAndWithOneSample) {
const char* kExpectedOutput =
"WEBVTT\n"
"\n"
"00:00:05.000 --> 00:00:06.000\n"
"payload\n"
"\n";
ASSERT_OK(Input(kInputIndex)
->Dispatch(StreamData::FromStreamInfo(kStreamIndex,
GetTextStreamInfo())));
ASSERT_OK(
Input(kInputIndex)
->Dispatch(StreamData::FromTextSample(
kStreamIndex, GetTextSample(kNoId, 5000, 6000, "payload"))));
ASSERT_OK(Input(kInputIndex)
->Dispatch(StreamData::FromSegmentInfo(
kStreamIndex, GetSegmentInfo(0, 10000, !kEncrypted))));
ASSERT_OK(Input(kInputIndex)->FlushAllDownstreams());
ASSERT_FILE_STREQ(kSegmentedFileOutput1, kExpectedOutput);
}
TEST_F(WebVttSegmentedOutputTest, WithTwoSegmentAndWithOneSample) {
const char* kExpectedOutput1 =
"WEBVTT\n"
"\n"
"00:00:05.000 --> 00:00:06.000\n"
"payload 1\n"
"\n";
const char* kExpectedOutput2 =
"WEBVTT\n"
"\n"
"00:00:15.000 --> 00:00:16.000\n"
"payload 2\n"
"\n";
ASSERT_OK(Input(kInputIndex)
->Dispatch(StreamData::FromStreamInfo(0, GetTextStreamInfo())));
// Segment One
ASSERT_OK(
Input(kInputIndex)
->Dispatch(StreamData::FromTextSample(
kStreamIndex, GetTextSample(kNoId, 5000, 6000, "payload 1"))));
ASSERT_OK(Input(kInputIndex)
->Dispatch(StreamData::FromSegmentInfo(
kStreamIndex, GetSegmentInfo(0, 10000, !kEncrypted))));
// Segment Two
ASSERT_OK(
Input(kInputIndex)
->Dispatch(StreamData::FromTextSample(
kStreamIndex, GetTextSample(kNoId, 15000, 16000, "payload 2"))));
ASSERT_OK(Input(kInputIndex)
->Dispatch(StreamData::FromSegmentInfo(
kStreamIndex, GetSegmentInfo(10000, 10000, !kEncrypted))));
ASSERT_OK(Input(kInputIndex)->FlushAllDownstreams());
ASSERT_FILE_STREQ(kSegmentedFileOutput1, kExpectedOutput1);
ASSERT_FILE_STREQ(kSegmentedFileOutput2, kExpectedOutput2);
}
TEST_F(WebVttSegmentedOutputTest, WithAnEmptySegment) {
const char* kExpectedOutput =
"WEBVTT\n"
"\n"
"00:00:15.000 --> 00:00:16.000\n"
"payload 2\n"
"\n";
ASSERT_OK(Input(kInputIndex)
->Dispatch(StreamData::FromStreamInfo(0, GetTextStreamInfo())));
// Segment One
ASSERT_OK(Input(kInputIndex)
->Dispatch(StreamData::FromSegmentInfo(
kStreamIndex, GetSegmentInfo(0, 10000, !kEncrypted))));
// Segment Two
ASSERT_OK(
Input(kInputIndex)
->Dispatch(StreamData::FromTextSample(
kStreamIndex, GetTextSample(kNoId, 15000, 16000, "payload 2"))));
ASSERT_OK(Input(kInputIndex)
->Dispatch(StreamData::FromSegmentInfo(
kStreamIndex, GetSegmentInfo(10000, 10000, !kEncrypted))));
ASSERT_OK(Input(kInputIndex)->FlushAllDownstreams());
// The empty segment will not write to disk, but it will use segment's
// filename.
ASSERT_FILE_STREQ(kSegmentedFileOutput2, kExpectedOutput);
}
} // namespace media
} // namespace shaka