Output Empty WebVTT Text Segments

Have the text webvtt pipeline output empty segments rather than skipping
them.

Bug: #324
Change-Id: I175cf22b65b241f6bf566aba2440518f3df95277
This commit is contained in:
Aaron Vaage 2018-02-05 11:16:00 -08:00
parent f35410deb1
commit c7eb2b4a7e
4 changed files with 76 additions and 55 deletions

View File

@ -26,6 +26,7 @@ TEST(WebVttTextPipelineTest, SegmentedOutput) {
const char* kOutput1 = "memory://output/template-1.vtt"; const char* kOutput1 = "memory://output/template-1.vtt";
const char* kOutput2 = "memory://output/template-2.vtt"; const char* kOutput2 = "memory://output/template-2.vtt";
const char* kOutput3 = "memory://output/template-3.vtt"; const char* kOutput3 = "memory://output/template-3.vtt";
const char* kOutput4 = "memory://output/template-4.vtt";
const char* kInputFile = "memory://input.vtt"; const char* kInputFile = "memory://input.vtt";
const char* kInput = const char* kInput =
@ -53,8 +54,14 @@ TEST(WebVttTextPipelineTest, SegmentedOutput) {
"Thank you.\n"; "Thank you.\n";
// Segment One // Segment One
// 00:00:10.000 to 00:00:20.000 // 00:00:00.000 to 00:00:10.000
const char* kExpectedOutput1 = const char* kExpectedOutput1 =
"WEBVTT\n"
"\n";
// Segment Two
// 00:00:10.000 to 00:00:20.000
const char* kExpectedOutput2 =
"WEBVTT\n" "WEBVTT\n"
"\n" "\n"
"1\n" "1\n"
@ -62,9 +69,9 @@ TEST(WebVttTextPipelineTest, SegmentedOutput) {
"This blade has a dark past.\n" "This blade has a dark past.\n"
"\n"; "\n";
// Segment Two // Segment Three
// 00:00:20.000 to 00:00:30.000 // 00:00:20.000 to 00:00:30.000
const char* kExpectedOutput2 = const char* kExpectedOutput3 =
"WEBVTT\n" "WEBVTT\n"
"\n" "\n"
"1\n" "1\n"
@ -80,9 +87,9 @@ TEST(WebVttTextPipelineTest, SegmentedOutput) {
"You're a fool for traveling alone,\nso completely unprepared.\n" "You're a fool for traveling alone,\nso completely unprepared.\n"
"\n"; "\n";
// Segment Three // Segment Four
// 00:00:30.000 to 00:00:40.000 // 00:00:30.000 to 00:00:40.000
const char* kExpectedOutput3 = const char* kExpectedOutput4 =
"WEBVTT\n" "WEBVTT\n"
"\n" "\n"
"3\n" "3\n"
@ -128,6 +135,7 @@ TEST(WebVttTextPipelineTest, SegmentedOutput) {
ASSERT_FILE_STREQ(kOutput1, kExpectedOutput1); ASSERT_FILE_STREQ(kOutput1, kExpectedOutput1);
ASSERT_FILE_STREQ(kOutput2, kExpectedOutput2); ASSERT_FILE_STREQ(kOutput2, kExpectedOutput2);
ASSERT_FILE_STREQ(kOutput3, kExpectedOutput3); ASSERT_FILE_STREQ(kOutput3, kExpectedOutput3);
ASSERT_FILE_STREQ(kOutput4, kExpectedOutput4);
} }
} // namespace media } // namespace media
} // namespace shaka } // namespace shaka

View File

@ -10,7 +10,7 @@ namespace shaka {
namespace media { namespace media {
namespace { namespace {
const size_t kStreamIndex = 0; const size_t kStreamIndex = 0;
} } // namespace
WebVttSegmenter::WebVttSegmenter(uint64_t segment_duration_ms) WebVttSegmenter::WebVttSegmenter(uint64_t segment_duration_ms)
: segment_duration_ms_(segment_duration_ms) {} : segment_duration_ms_(segment_duration_ms) {}
@ -33,16 +33,18 @@ Status WebVttSegmenter::Process(std::unique_ptr<StreamData> stream_data) {
} }
Status WebVttSegmenter::OnFlushRequest(size_t input_stream_index) { Status WebVttSegmenter::OnFlushRequest(size_t input_stream_index) {
while (segment_map_.size()) { // At this point we know that there is a single series of consecutive
uint64_t segment = segment_map_.begin()->first; // segments, all we need to do is run through all of them.
for (const auto& pair : segment_map_) {
Status status = DispatchSegmentWithSamples(pair.first, pair.second);
// OnSegmentEnd will remove the segment from the map.
Status status = OnSegmentEnd(segment);
if (!status.ok()) { if (!status.ok()) {
return status; return status;
} }
} }
segment_map_.clear();
return FlushAllDownstreams(); return FlushAllDownstreams();
} }
@ -66,58 +68,58 @@ Status WebVttSegmenter::OnTextSample(std::shared_ptr<const TextSample> sample) {
return Status::OK; return Status::OK;
} }
// Now that we are accepting this new segment, its starting segment is our
// new head segment.
head_segment_ = start_segment;
// Add the sample to each segment it spans. // Add the sample to each segment it spans.
for (uint64_t segment = start_segment; segment <= ending_segment; segment++) { for (uint64_t segment = start_segment; segment <= ending_segment; segment++) {
segment_map_[segment].push_back(sample); segment_map_[segment].push_back(sample);
} }
// Output all the segments that come before the start of this cue's first // Move forward segment-by-segment so that we output empty segments to fill
// segment. // any segments with no cues.
while (segment_map_.size()) { for (uint64_t segment = head_segment_; segment < start_segment; segment++) {
// Since the segments are in accending order, we can break out of the loop auto it = segment_map_.find(segment);
// once we catch-up to the new samples starting segment.
const uint64_t segment = segment_map_.begin()->first; Status status;
if (segment >= start_segment) { if (it == segment_map_.end()) {
break; const WebVttSegmentSamples kNoSamples;
status.Update(DispatchSegmentWithSamples(segment, kNoSamples));
} else {
// We found a segment, output all the samples. Remove it from the map as
// we should never need to write to it again.
status.Update(DispatchSegmentWithSamples(segment, it->second));
segment_map_.erase(it);
} }
// Output the segment. If there is an error, there is no reason to continue. // If we fail to output a single sample, just stop.
Status status = OnSegmentEnd(segment);
if (!status.ok()) { if (!status.ok()) {
return status; return status;
} }
} }
// Jump ahead to the start of this segment as we should never have any samples
// start before |start_segment|.
head_segment_ = start_segment;
return Status::OK; return Status::OK;
} }
Status WebVttSegmenter::OnSegmentEnd(uint64_t segment) { Status WebVttSegmenter::DispatchSegmentWithSamples(
uint64_t segment,
const WebVttSegmentSamples& samples) {
Status status; Status status;
for (const auto& sample : samples) {
const auto found = segment_map_.find(segment); status.Update(DispatchTextSample(kStreamIndex, sample));
if (found != segment_map_.end()) {
for (const auto& sample : found->second) {
status.Update(DispatchTextSample(kStreamIndex, sample));
}
// Stop storing the segment.
segment_map_.erase(found);
} }
// Only send the segment info if all the samples were accepted. // Only send the segment info if all the samples were successful.
if (status.ok()) { if (!status.ok()) {
std::shared_ptr<SegmentInfo> info = std::make_shared<SegmentInfo>(); return status;
info->start_timestamp = segment * segment_duration_ms_;
info->duration = segment_duration_ms_;
status.Update(DispatchSegmentInfo(kStreamIndex, std::move(info)));
} }
return status; std::shared_ptr<SegmentInfo> info = std::make_shared<SegmentInfo>();
info->start_timestamp = segment * segment_duration_ms_;
info->duration = segment_duration_ms_;
return DispatchSegmentInfo(kStreamIndex, std::move(info));
} }
} // namespace media } // namespace media
} // namespace shaka } // namespace shaka

View File

@ -30,19 +30,20 @@ class WebVttSegmenter : public MediaHandler {
WebVttSegmenter(const WebVttSegmenter&) = delete; WebVttSegmenter(const WebVttSegmenter&) = delete;
WebVttSegmenter& operator=(const WebVttSegmenter&) = delete; WebVttSegmenter& operator=(const WebVttSegmenter&) = delete;
using WebVttSample = std::shared_ptr<const TextSample>;
using WebVttSegmentSamples = std::vector<WebVttSample>;
Status InitializeInternal() override; Status InitializeInternal() override;
Status OnTextSample(std::shared_ptr<const TextSample> sample); Status OnTextSample(std::shared_ptr<const TextSample> sample);
Status OnSegmentEnd(uint64_t segment); Status DispatchSegmentWithSamples(uint64_t segment,
const WebVttSegmentSamples& samples);
uint64_t segment_duration_ms_; uint64_t segment_duration_ms_;
using WebVttSample = std::shared_ptr<const TextSample>;
using WebVttSegment = std::vector<WebVttSample>;
// Mapping of segment number to segment. // Mapping of segment number to segment.
std::map<uint64_t, WebVttSegment> segment_map_; std::map<uint64_t, WebVttSegmentSamples> segment_map_;
uint64_t head_segment_ = 0; uint64_t head_segment_ = 0;
}; };

View File

@ -142,9 +142,13 @@ TEST_F(WebVttSegmenterTest, CreatesSegmentsForCues) {
// | | // | |
// | | [---B---] // | | [---B---]
// | | // | |
TEST_F(WebVttSegmenterTest, SkipsEmptySegments) { TEST_F(WebVttSegmenterTest, OutputsEmptySegments) {
const uint64_t kSampleDuration = kSegmentDuration / 2; const uint64_t kSampleDuration = kSegmentDuration / 2;
const int64_t kSegment1Start = kStartTime;
const int64_t kSegment2Start = kSegment1Start + kSegmentDuration;
const int64_t kSegment3Start = kSegment2Start + kSegmentDuration;
{ {
testing::InSequence s; testing::InSequence s;
@ -157,19 +161,25 @@ TEST_F(WebVttSegmenterTest, SkipsEmptySegments) {
kNoSettings, kPayload[0]))); kNoSettings, kPayload[0])));
EXPECT_CALL( EXPECT_CALL(
*Output(kOutputIndex), *Output(kOutputIndex),
OnProcess(IsSegmentInfo(kStreamIndex, kStartTimeSigned, OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, kSegmentDuration,
kSegmentDuration, !kSubSegment, !kEncrypted))); !kSubSegment, !kEncrypted)));
// Segment Two // Segment Two (empty segment)
EXPECT_CALL(
*Output(kOutputIndex),
OnProcess(IsSegmentInfo(kStreamIndex, kSegment2Start, kSegmentDuration,
!kSubSegment, !kEncrypted)));
// Segment Three
EXPECT_CALL(*Output(kOutputIndex), EXPECT_CALL(*Output(kOutputIndex),
OnProcess(IsTextSample( OnProcess(IsTextSample(
kId[1], kStartTime + 2 * kSegmentDuration, kId[1], kStartTime + 2 * kSegmentDuration,
kStartTime + 2 * kSegmentDuration + kSampleDuration, kStartTime + 2 * kSegmentDuration + kSampleDuration,
kNoSettings, kPayload[1]))); kNoSettings, kPayload[1])));
EXPECT_CALL(*Output(kOutputIndex), EXPECT_CALL(
OnProcess(IsSegmentInfo( *Output(kOutputIndex),
kStreamIndex, kStartTimeSigned + 2 * kSegmentDuration, OnProcess(IsSegmentInfo(kStreamIndex, kSegment3Start, kSegmentDuration,
kSegmentDuration, !kSubSegment, !kEncrypted))); !kSubSegment, !kEncrypted)));
EXPECT_CALL(*Output(kOutputIndex), OnFlush(kStreamIndex)); EXPECT_CALL(*Output(kOutputIndex), OnFlush(kStreamIndex));
} }