Output Empty WebVtt Segments

According to the HLS+WebVTT spec, if there is no text in a segment
a webvtt file with no cues can be added to the manifest. By outputting
empty segments it allows the accumulated duration in the Master Playlist
to better represent the duration of the text stream.

Spec Reference: //tools.ietf.org/html/draft-pantos-http-live-streaming-23
  Section 3.5. WebVTT

Bug: #205

Change-Id: I5de01200fd9fa99c57949c773e8ee926b0f6ba8a
This commit is contained in:
Aaron Vaage 2018-01-31 11:10:13 -08:00
parent ea96b72d6a
commit 771944a3f5
4 changed files with 35 additions and 22 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

@ -35,7 +35,10 @@ 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) {
Status status; Status status;
while (status.ok() && samples_.size()) { while (status.ok() && samples_.size()) {
status.Update(OnSegmentEnd()); // It is not possible for there to be any gaps, or else we would have
// already ended the segments before them. So just close the last remaining
// open segments.
OnSegmentEnd(samples_.top().segment);
} }
return status.ok() ? FlushAllDownstreams() : status; return status.ok() ? FlushAllDownstreams() : status;
} }
@ -68,23 +71,19 @@ Status WebVttSegmenter::OnTextSample(std::shared_ptr<const TextSample> sample) {
samples_.push(seg_sample); samples_.push(seg_sample);
} }
Status status; // Output all the segments that come before the start of this cue's first
// segment.
while (status.ok() && samples_.size() && for (; current_segment_ < start_segment; current_segment_++) {
samples_.top().segment < start_segment) { Status status = OnSegmentEnd(current_segment_);
// WriteNextSegment will pop elements from |samples_| which will if (!status.ok()) {
// eventually allow the loop to exit. return status;
status.Update(OnSegmentEnd()); }
} }
return status; return Status::OK;
} }
Status WebVttSegmenter::OnSegmentEnd() { Status WebVttSegmenter::OnSegmentEnd(uint64_t segment) {
DCHECK(samples_.size());
const uint64_t segment = samples_.top().segment;
Status status; Status status;
while (status.ok() && samples_.size() && samples_.top().segment == segment) { while (status.ok() && samples_.size() && samples_.top().segment == segment) {
status.Update( status.Update(

View File

@ -56,8 +56,10 @@ class WebVttSegmenter : public MediaHandler {
Status InitializeInternal() override; Status InitializeInternal() override;
Status OnTextSample(std::shared_ptr<const TextSample> sample); Status OnTextSample(std::shared_ptr<const TextSample> sample);
Status OnSegmentEnd();
Status OnSegmentEnd(uint64_t segment);
uint64_t current_segment_ = 0;
uint64_t segment_duration_ms_; uint64_t segment_duration_ms_;
std::priority_queue<WebVttSegmentedTextSample, std::priority_queue<WebVttSegmentedTextSample,
std::vector<WebVttSegmentedTextSample>, std::vector<WebVttSegmentedTextSample>,

View File

@ -142,7 +142,7 @@ TEST_F(WebVttSegmenterTest, CreatesSegmentsForCues) {
// | | // | |
// | | [---B---] // | | [---B---]
// | | // | |
TEST_F(WebVttSegmenterTest, SkipsEmptySegments) { TEST_F(WebVttSegmenterTest, DoesntSkipsEmptySegments) {
const uint64_t kSampleDuration = kSegmentDuration / 2; const uint64_t kSampleDuration = kSegmentDuration / 2;
{ {
@ -160,7 +160,11 @@ TEST_F(WebVttSegmenterTest, SkipsEmptySegments) {
OnProcess(IsSegmentInfo(kStreamIndex, kStartTimeSigned, OnProcess(IsSegmentInfo(kStreamIndex, kStartTimeSigned,
kSegmentDuration, !kSubSegment, !kEncrypted))); kSegmentDuration, !kSubSegment, !kEncrypted)));
// There is no segment two // Segment two (empty)
EXPECT_CALL(*Output(kOutputIndex),
OnProcess(IsSegmentInfo(
kStreamIndex, kStartTimeSigned + kSegmentDuration,
kSegmentDuration, !kSubSegment, !kEncrypted)));
// Segment Three // Segment Three
EXPECT_CALL(*Output(kOutputIndex), EXPECT_CALL(*Output(kOutputIndex),