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