From c88fe545538611d50b333a4159bc4935f39db931 Mon Sep 17 00:00:00 2001 From: Aaron Vaage Date: Mon, 22 May 2017 09:35:49 -0700 Subject: [PATCH] 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 --- packager/media/base/text_sample.h | 5 +- packager/media/event/muxer_listener.h | 3 +- .../media/event/muxer_listener_internal.cc | 3 + packager/media/formats/webvtt/webvtt.gyp | 3 + .../formats/webvtt/webvtt_output_handler.cc | 166 ++++++++++++++++++ .../formats/webvtt/webvtt_output_handler.h | 86 +++++++++ .../webvtt/webvtt_output_handler_unittest.cc | 155 ++++++++++++++++ 7 files changed, 418 insertions(+), 3 deletions(-) create mode 100644 packager/media/formats/webvtt/webvtt_output_handler.cc create mode 100644 packager/media/formats/webvtt/webvtt_output_handler.h create mode 100644 packager/media/formats/webvtt/webvtt_output_handler_unittest.cc diff --git a/packager/media/base/text_sample.h b/packager/media/base/text_sample.h index 6ad8d2403a..691f3e5476 100644 --- a/packager/media/base/text_sample.h +++ b/packager/media/base/text_sample.h @@ -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; diff --git a/packager/media/event/muxer_listener.h b/packager/media/event/muxer_listener.h index 1b489d819e..2078f2aa53 100644 --- a/packager/media/event/muxer_listener.h +++ b/packager/media/event/muxer_listener.h @@ -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 diff --git a/packager/media/event/muxer_listener_internal.cc b/packager/media/event/muxer_listener_internal.cc index 6c087d6e88..04a4e1a19e 100644 --- a/packager/media/event/muxer_listener_internal.cc +++ b/packager/media/event/muxer_listener_internal.cc @@ -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; } diff --git a/packager/media/formats/webvtt/webvtt.gyp b/packager/media/formats/webvtt/webvtt.gyp index 9d479ed915..fab0bc77c4 100644 --- a/packager/media/formats/webvtt/webvtt.gyp +++ b/packager/media/formats/webvtt/webvtt.gyp @@ -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', diff --git a/packager/media/formats/webvtt/webvtt_output_handler.cc b/packager/media/formats/webvtt/webvtt_output_handler.cc new file mode 100644 index 0000000000..db8893a4ce --- /dev/null +++ b/packager/media/formats/webvtt/webvtt_output_handler.cc @@ -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 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 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(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 diff --git a/packager/media/formats/webvtt/webvtt_output_handler.h b/packager/media/formats/webvtt/webvtt_output_handler.h new file mode 100644 index 0000000000..54e3702669 --- /dev/null +++ b/packager/media/formats/webvtt/webvtt_output_handler.h @@ -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 + +#include + +#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 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 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 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_ diff --git a/packager/media/formats/webvtt/webvtt_output_handler_unittest.cc b/packager/media/formats/webvtt/webvtt_output_handler_unittest.cc new file mode 100644 index 0000000000..5741d3748e --- /dev/null +++ b/packager/media/formats/webvtt/webvtt_output_handler_unittest.cc @@ -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 +#include + +#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 muxer_listener(new CombinedMuxerListener()); + + out_ = std::make_shared( + muxer_options, std::move(muxer_listener)); + + ASSERT_OK(SetUpAndInitializeGraph(out_, kInputCount, kOutputCount)); + } + + std::shared_ptr 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