diff --git a/packager/media/chunking/text_chunker.cc b/packager/media/chunking/text_chunker.cc index 43161a18ef..8aafe096b5 100644 --- a/packager/media/chunking/text_chunker.cc +++ b/packager/media/chunking/text_chunker.cc @@ -6,26 +6,42 @@ #include "packager/media/chunking/text_chunker.h" +#include "packager/status_macros.h" + namespace shaka { namespace media { namespace { const size_t kStreamIndex = 0; + +std::shared_ptr MakeSegmentInfo(int64_t start_ms, + int64_t end_ms) { + DCHECK_LT(start_ms, end_ms); + + std::shared_ptr info = std::make_shared(); + info->start_timestamp = start_ms; + info->duration = end_ms - start_ms; + + return info; +} } // namespace -TextChunker::TextChunker(uint64_t segment_duration_ms) - : segment_duration_ms_(segment_duration_ms) {} +TextChunker::TextChunker(int64_t segment_duration_ms) + : segment_duration_ms_(segment_duration_ms), + segment_start_ms_(0), + segment_expected_end_ms_(segment_duration_ms) {} Status TextChunker::InitializeInternal() { return Status::OK; } -Status TextChunker::Process(std::unique_ptr stream_data) { - switch (stream_data->stream_data_type) { +Status TextChunker::Process(std::unique_ptr data) { + switch (data->stream_data_type) { case StreamDataType::kStreamInfo: - return DispatchStreamInfo(kStreamIndex, - std::move(stream_data->stream_info)); + return OnStreamInfo(std::move(data->stream_info)); case StreamDataType::kTextSample: - return OnTextSample(stream_data->text_sample); + return OnTextSample(data->text_sample); + case StreamDataType::kCueEvent: + return OnCueEvent(data->cue_event); default: return Status(error::INTERNAL_ERROR, "Invalid stream data type for this handler"); @@ -33,92 +49,75 @@ Status TextChunker::Process(std::unique_ptr stream_data) { } Status TextChunker::OnFlushRequest(size_t input_stream_index) { - // At this point we know that there is a single series of consecutive - // 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); - - if (!status.ok()) { - return status; - } + // Keep outputting segments until all the samples leave the system. + while (segment_samples_.size()) { + RETURN_IF_ERROR(EndSegment(segment_expected_end_ms_)); } - segment_map_.clear(); - return FlushAllDownstreams(); } -Status TextChunker::OnTextSample(std::shared_ptr sample) { - const uint64_t start_segment = sample->start_time() / segment_duration_ms_; +Status TextChunker::OnStreamInfo(std::shared_ptr info) { + // There is no information we need from the stream info, so just pass it + // downstream. + return DispatchStreamInfo(kStreamIndex, std::move(info)); +} - // Find the last segment that overlaps the sample. Adjust the sample by one - // ms (smallest time unit) in case |EndTime| falls on the segment boundary. - DCHECK_GT(sample->duration(), 0u); - const uint64_t ending_segment = - (sample->EndTime() - 1) / segment_duration_ms_; +Status TextChunker::OnCueEvent(std::shared_ptr event) { + // We are going to cut the current segment into two using the event's time as + // the division. + const int64_t cue_time_in_ms = event->time_in_seconds * 1000; - DCHECK_GE(ending_segment, start_segment); - - // Samples must always be advancing. If a sample comes in out of order, - // skip the sample. - if (head_segment_ > start_segment) { - LOG(WARNING) << "New sample has arrived out of order. Skipping sample " - << "as segment start is " << start_segment << " and segment " - << "head is " << head_segment_ << "."; - return Status::OK; + // In the case that there is a gap with no samples between the last sample + // and the cue event, output all the segments until we get to the segment that + // the cue event interrupts. + while (segment_expected_end_ms_ < cue_time_in_ms) { + RETURN_IF_ERROR(EndSegment(segment_expected_end_ms_)); } - // Add the sample to each segment it spans. - for (uint64_t segment = start_segment; segment <= ending_segment; segment++) { - segment_map_[segment].push_back(sample); - } - - // Move forward segment-by-segment so that we output empty segments to fill - // any segments with no cues. - for (uint64_t segment = head_segment_; segment < start_segment; segment++) { - auto it = segment_map_.find(segment); - - Status status; - if (it == segment_map_.end()) { - const SegmentSamples 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); - } - - // If we fail to output a single sample, just stop. - if (!status.ok()) { - 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_IF_ERROR(EndSegment(cue_time_in_ms)); + RETURN_IF_ERROR(DispatchCueEvent(kStreamIndex, std::move(event))); return Status::OK; } -Status TextChunker::DispatchSegmentWithSamples(uint64_t segment, - const SegmentSamples& samples) { - Status status; - for (const auto& sample : samples) { - status.Update(DispatchTextSample(kStreamIndex, sample)); +Status TextChunker::OnTextSample(std::shared_ptr sample) { + // Output all segments that come before our new sample. + while (segment_expected_end_ms_ <= sample->start_time()) { + RETURN_IF_ERROR(EndSegment(segment_expected_end_ms_)); } - // Only send the segment info if all the samples were successful. - if (!status.ok()) { - return status; - } + segment_samples_.push_back(std::move(sample)); - std::shared_ptr info = std::make_shared(); - info->start_timestamp = segment * segment_duration_ms_; - info->duration = segment_duration_ms_; - - return DispatchSegmentInfo(kStreamIndex, std::move(info)); + return Status::OK; } + +Status TextChunker::EndSegment(int64_t segment_actual_end_ms) { + // Output all the samples that are part of the segment. + for (const auto& sample : segment_samples_) { + RETURN_IF_ERROR(DispatchTextSample(kStreamIndex, sample)); + } + + RETURN_IF_ERROR(DispatchSegmentInfo( + kStreamIndex, MakeSegmentInfo(segment_start_ms_, segment_actual_end_ms))); + + // Create a new segment that comes right after the old segment and remove all + // samples that don't cross over into the new segment. + StartNewSegment(segment_actual_end_ms); + + return Status::OK; +} + +void TextChunker::StartNewSegment(int64_t start_ms) { + segment_start_ms_ = start_ms; + segment_expected_end_ms_ = start_ms + segment_duration_ms_; + + // Remove all samples that no longer overlap with the new segment. + segment_samples_.remove_if( + [start_ms](const std::shared_ptr& sample) { + return sample->EndTime() <= start_ms; + }); +} + } // namespace media } // namespace shaka diff --git a/packager/media/chunking/text_chunker.h b/packager/media/chunking/text_chunker.h index bd8f05f018..13bee2e946 100644 --- a/packager/media/chunking/text_chunker.h +++ b/packager/media/chunking/text_chunker.h @@ -7,43 +7,41 @@ #ifndef PACKAGER_MEDIA_CHUNKING_TEXT_CHUNKER_H_ #define PACKAGER_MEDIA_CHUNKING_TEXT_CHUNKER_H_ -#include - -#include -#include +#include #include "packager/media/base/media_handler.h" -#include "packager/status.h" namespace shaka { namespace media { class TextChunker : public MediaHandler { public: - explicit TextChunker(uint64_t segment_duration_ms); - - protected: - Status Process(std::unique_ptr stream_data) override; - Status OnFlushRequest(size_t input_stream_index) override; + explicit TextChunker(int64_t segment_duration_ms); private: TextChunker(const TextChunker&) = delete; TextChunker& operator=(const TextChunker&) = delete; - using SegmentSamples = std::vector>; - Status InitializeInternal() override; + Status Process(std::unique_ptr stream_data) override; + Status OnFlushRequest(size_t input_stream_index) override; + + Status OnStreamInfo(std::shared_ptr info); + Status OnCueEvent(std::shared_ptr cue); Status OnTextSample(std::shared_ptr sample); - Status DispatchSegmentWithSamples(uint64_t segment, - const SegmentSamples& samples); + Status EndSegment(int64_t segment_actual_end_ms); + void StartNewSegment(int64_t start_ms); - uint64_t segment_duration_ms_; + int64_t segment_duration_ms_; - // Mapping of segment number to segment. - std::map segment_map_; - uint64_t head_segment_ = 0; + // The segment that we are currently outputting samples for. The segment + // will end once a new sample with start time greater or equal to the + // segment's end time arrives. + int64_t segment_start_ms_; + int64_t segment_expected_end_ms_; + std::list> segment_samples_; }; } // namespace media diff --git a/packager/media/chunking/text_chunker_unittest.cc b/packager/media/chunking/text_chunker_unittest.cc index 1ec6f9ca42..83b773c035 100644 --- a/packager/media/chunking/text_chunker_unittest.cc +++ b/packager/media/chunking/text_chunker_unittest.cc @@ -15,298 +15,570 @@ namespace shaka { namespace media { namespace { -const int64_t kStartTime = 0; -const int64_t kSegmentDuration = 10000; // 10 seconds +const uint64_t kStreamIndex = 0; -const size_t kStreamIndex = 0; +const size_t kInputs = 1; +const size_t kOutputs = 1; -const size_t kInputCount = 1; -const size_t kOutputCount = 1; -const size_t kInputIndex = 0; -const size_t kOutputIndex = 0; +const size_t kInput = 0; +const size_t kOutput = 0; const bool kEncrypted = true; const bool kSubSegment = true; -const char* kId[] = {"cue 1 id", "cue 2 id"}; -const char* kPayload[] = {"cue 1 payload", "cue 2 payload"}; +const int64_t kTick = 50; -const std::string kNoSettings = ""; +const char* kNoId = ""; +const char* kNoSettings = ""; +const char* kNoPayload = ""; } // namespace class TextChunkerTest : public MediaHandlerTestBase { protected: - void SetUp() { - ASSERT_OK( - SetUpAndInitializeGraph(std::make_shared(kSegmentDuration), - kInputCount, kOutputCount)); + void Init(int64_t segment_duration) { + ASSERT_OK(SetUpAndInitializeGraph( + std::make_shared(segment_duration), kInputs, kOutputs)); } }; -// When a cue ends on a segment boundry, it does not create a cue with a 0 ms -// duration +// S0 S1 // | | // |[---A---]| // | | -TEST_F(TextChunkerTest, CueEndingOnSegmentStart) { - const int64_t kSampleDuration = kSegmentDuration; +TEST_F(TextChunkerTest, SampleEndingOnSegmentStart) { + const int64_t kSegmentStart = 0; + const int64_t kSegmentDuration = kTick; + + const int64_t kSampleStart = 0; + const int64_t kSampleDuration = kTick; + const int64_t kSampleEnd = kSampleStart + kSampleDuration; + + Init(kSegmentDuration); { testing::InSequence s; - EXPECT_CALL(*Output(kOutputIndex), OnProcess(IsStreamInfo(kStreamIndex))); + EXPECT_CALL(*Output(kOutput), OnProcess(IsStreamInfo(kStreamIndex))); - // Segment One + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kNoId, kSampleStart, kSampleEnd, + kNoSettings, kNoPayload))); EXPECT_CALL( - *Output(kOutputIndex), - OnProcess(IsTextSample(kId[0], kStartTime, kStartTime + kSampleDuration, - kNoSettings, kPayload[0]))); - EXPECT_CALL( - *Output(kOutputIndex), - OnProcess(IsSegmentInfo(kStreamIndex, kStartTime, kSegmentDuration, + *Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegmentStart, kSegmentDuration, !kSubSegment, !kEncrypted))); - EXPECT_CALL(*Output(kOutputIndex), OnFlush(kStreamIndex)); + EXPECT_CALL(*Output(kOutput), OnFlush(kStreamIndex)); } - ASSERT_OK(Input(kInputIndex) - ->Dispatch(StreamData::FromStreamInfo(kStreamIndex, - GetTextStreamInfo()))); - ASSERT_OK(Input(kInputIndex) - ->Dispatch(StreamData::FromTextSample( - kStreamIndex, - GetTextSample(kId[0], kStartTime, - kStartTime + kSampleDuration, kPayload[0])))); - ASSERT_OK(Input(kInputIndex)->FlushAllDownstreams()); + ASSERT_OK(Input(kInput)->Dispatch( + StreamData::FromStreamInfo(kStreamIndex, GetTextStreamInfo()))); + ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( + kStreamIndex, + GetTextSample(kNoId, kSampleStart, kSampleEnd, kNoPayload)))); + ASSERT_OK(Input(kInput)->FlushAllDownstreams()); } -// Each cue belongs in its own segment, so before each cue is passed -// downstream, a 'input of segment' message should be passed downstream. -// | -// [---A---] | -// | [---B---] -// | -TEST_F(TextChunkerTest, CreatesSegmentsForCues) { - // Divide segment duration by 2 so that the sample duration won't be a full - // segment. - const int64_t kSampleDuration = kSegmentDuration / 2; +// S0 S1 S2 +// | | | +// |[-A-] | | +// | |[-B-] | +// | | | +TEST_F(TextChunkerTest, CreatesSegmentsForSamples) { + const int64_t kSegmentDuration = 2 * kTick; + const int64_t kSegment0Start = 0; + const int64_t kSegment1Start = kSegment0Start + kSegmentDuration; + + const int64_t kSample0Start = 0; + const int64_t kSample0Duration = kTick; + const int64_t kSample0End = kSample0Start + kSample0Duration; + + const int64_t kSample1Start = 3 * kTick; + const int64_t kSample1Duration = kTick; + const int64_t kSample1End = kSample1Start + kSample1Duration; + + Init(kSegmentDuration); { testing::InSequence s; - EXPECT_CALL(*Output(kOutputIndex), OnProcess(IsStreamInfo(kStreamIndex))); + EXPECT_CALL(*Output(kOutput), OnProcess(IsStreamInfo(kStreamIndex))); // Segment One + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kNoId, kSample0Start, kSample0End, + kNoSettings, kNoPayload))); EXPECT_CALL( - *Output(kOutputIndex), - OnProcess(IsTextSample(kId[0], kStartTime, kStartTime + kSampleDuration, - kNoSettings, kPayload[0]))); - EXPECT_CALL( - *Output(kOutputIndex), - OnProcess(IsSegmentInfo(kStreamIndex, kStartTime, kSegmentDuration, + *Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment0Start, kSegmentDuration, !kSubSegment, !kEncrypted))); // Segment Two + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kNoId, kSample1Start, kSample1End, + kNoSettings, kNoPayload))); EXPECT_CALL( - *Output(kOutputIndex), - OnProcess(IsTextSample(kId[1], kStartTime + kSegmentDuration, - kStartTime + kSegmentDuration + kSampleDuration, - kNoSettings, kPayload[1]))); - EXPECT_CALL( - *Output(kOutputIndex), - OnProcess(IsSegmentInfo(kStreamIndex, kStartTime + kSegmentDuration, - kSegmentDuration, !kSubSegment, !kEncrypted))); + *Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, kSegmentDuration, + !kSubSegment, !kEncrypted))); - EXPECT_CALL(*Output(kOutputIndex), OnFlush(kStreamIndex)); + EXPECT_CALL(*Output(kOutput), OnFlush(kStreamIndex)); } - ASSERT_OK(Input(kInputIndex) - ->Dispatch(StreamData::FromStreamInfo(kStreamIndex, - GetTextStreamInfo()))); - ASSERT_OK(Input(kInputIndex) - ->Dispatch(StreamData::FromTextSample( - kStreamIndex, - GetTextSample(kId[0], kStartTime, - kStartTime + kSampleDuration, kPayload[0])))); - ASSERT_OK( - Input(kInputIndex) - ->Dispatch(StreamData::FromTextSample( - kStreamIndex, - GetTextSample(kId[1], kStartTime + kSegmentDuration, - kStartTime + kSegmentDuration + kSampleDuration, - kPayload[1])))); - ASSERT_OK(Input(kInputIndex)->FlushAllDownstreams()); + ASSERT_OK(Input(kInput)->Dispatch( + StreamData::FromStreamInfo(kStreamIndex, GetTextStreamInfo()))); + ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( + kStreamIndex, + GetTextSample(kNoId, kSample0Start, kSample0End, kNoPayload)))); + ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( + kStreamIndex, + GetTextSample(kNoId, kSample1Start, kSample1End, kNoPayload)))); + ASSERT_OK(Input(kInput)->FlushAllDownstreams()); } -// [---A---] | | -// | | -// | | [---B---] -// | | +// S0 S1 S2 S3 +// | | | | +// |[-A-] | | | +// | | |[-B-] | +// | | | | TEST_F(TextChunkerTest, OutputsEmptySegments) { - const int64_t kSampleDuration = kSegmentDuration / 2; - - const int64_t kSegment1Start = kStartTime; + const int64_t kSegmentDuration = 2 * kTick; + const int64_t kSegment0Start = 0; + const int64_t kSegment1Start = kSegment0Start + kSegmentDuration; const int64_t kSegment2Start = kSegment1Start + kSegmentDuration; - const int64_t kSegment3Start = kSegment2Start + kSegmentDuration; + + const int64_t kSample0Start = 0; + const int64_t kSample0Duration = kTick; + const int64_t kSample0End = kSample0Start + kSample0Duration; + + const int64_t kSample1Start = 4 * kTick; + const int64_t kSample1Duration = kTick; + const int64_t kSample1End = kSample1Start + kSample1Duration; + + Init(kSegmentDuration); { testing::InSequence s; - EXPECT_CALL(*Output(kOutputIndex), OnProcess(IsStreamInfo(kStreamIndex))); + EXPECT_CALL(*Output(kOutput), OnProcess(IsStreamInfo(kStreamIndex))); // Segment One + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kNoId, kSample0Start, kSample0End, + kNoSettings, kNoPayload))); EXPECT_CALL( - *Output(kOutputIndex), - OnProcess(IsTextSample(kId[0], kStartTime, kStartTime + kSampleDuration, - kNoSettings, kPayload[0]))); - EXPECT_CALL( - *Output(kOutputIndex), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, kSegmentDuration, + *Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment0Start, kSegmentDuration, !kSubSegment, !kEncrypted))); // Segment Two (empty segment) EXPECT_CALL( - *Output(kOutputIndex), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment2Start, kSegmentDuration, + *Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, kSegmentDuration, !kSubSegment, !kEncrypted))); // Segment Three - EXPECT_CALL(*Output(kOutputIndex), - OnProcess(IsTextSample( - kId[1], kStartTime + 2 * kSegmentDuration, - kStartTime + 2 * kSegmentDuration + kSampleDuration, - kNoSettings, kPayload[1]))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kNoId, kSample1Start, kSample1End, + kNoSettings, kNoPayload))); EXPECT_CALL( - *Output(kOutputIndex), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment3Start, kSegmentDuration, + *Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment2Start, kSegmentDuration, !kSubSegment, !kEncrypted))); - EXPECT_CALL(*Output(kOutputIndex), OnFlush(kStreamIndex)); - } - - ASSERT_OK(Input(kInputIndex) - ->Dispatch(StreamData::FromStreamInfo(kStreamIndex, - GetTextStreamInfo()))); - ASSERT_OK(Input(kInputIndex) - ->Dispatch(StreamData::FromTextSample( - kStreamIndex, - GetTextSample(kId[0], kStartTime, - kStartTime + kSampleDuration, kPayload[0])))); - ASSERT_OK( - Input(kInputIndex) - ->Dispatch(StreamData::FromTextSample( - kStreamIndex, - GetTextSample(kId[1], kStartTime + 2 * kSegmentDuration, - kStartTime + 2 * kSegmentDuration + kSampleDuration, - kPayload[1])))); - ASSERT_OK(Input(kInputIndex)->FlushAllDownstreams()); -} - -// When a cue crossing the segment boundary, the cue should be included in -// both segments. -// | -// [-----A-----|---------] -// | -TEST_F(TextChunkerTest, CueCrossesSegments) { - const int64_t kSampleDuration = 2 * kSegmentDuration; - - { - testing::InSequence s; - - EXPECT_CALL(*Output(kOutputIndex), OnProcess(IsStreamInfo(kStreamIndex))); - - // Segment One - EXPECT_CALL( - *Output(kOutputIndex), - OnProcess(IsTextSample(kId[0], kStartTime, kStartTime + kSampleDuration, - kNoSettings, kPayload[0]))); - EXPECT_CALL( - *Output(kOutputIndex), - OnProcess(IsSegmentInfo(kStreamIndex, kStartTime, kSegmentDuration, - !kSubSegment, !kEncrypted))); - - // Segment Two - EXPECT_CALL( - *Output(kOutputIndex), - OnProcess(IsTextSample(kId[0], kStartTime, kStartTime + kSampleDuration, - kNoSettings, kPayload[0]))); - EXPECT_CALL( - *Output(kOutputIndex), - OnProcess(IsSegmentInfo(kStreamIndex, kStartTime + kSegmentDuration, - kSegmentDuration, !kSubSegment, !kEncrypted))); - - EXPECT_CALL(*Output(kOutputIndex), OnFlush(kStreamIndex)); - } - - ASSERT_OK(Input(kInputIndex) - ->Dispatch(StreamData::FromStreamInfo(kStreamIndex, - GetTextStreamInfo()))); - ASSERT_OK(Input(kInputIndex) - ->Dispatch(StreamData::FromTextSample( - kStreamIndex, - GetTextSample(kId[0], kStartTime, - kStartTime + kSampleDuration, kPayload[0])))); - ASSERT_OK(Input(kInputIndex)->FlushAllDownstreams()); -} - -class TextChunkerOrderTest : public MediaHandlerTestBase {}; - -TEST_F(TextChunkerOrderTest, PreservesOrder) { - const size_t kInputs = 1; - const size_t kOutputs = 1; - - const size_t kInput = 0; - const size_t kOutput = 0; - - const int64_t kDuration = 10000; - const int64_t kSegmentStart1 = 0; - const int64_t kSegmentStart2 = kDuration; - - ASSERT_OK(SetUpAndInitializeGraph(std::make_shared(kDuration), - kInputs, kOutputs)); - - { - testing::InSequence s; - - EXPECT_CALL(*Output(kOutput), OnProcess(IsStreamInfo(kInput))); - - // Segment One - EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample("1", 5000u, 8500u, "", - "WebVtt testing Line 1 (5.0 - 8.5)"))); - EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample("2", 5000u, 8500u, "", - "WebVtt testing Line 2 (5.0 - 8.5)"))); - EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample("3", 5000u, 12500u, "", - "WebVtt testing (5.0 - 12.5)"))); - EXPECT_CALL(*Output(kOutput), OnProcess(IsSegmentInfo( - kInput, kSegmentStart1, kSegmentDuration, - !kSubSegment, !kEncrypted))); - - // Segment Two - EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample("3", 5000u, 12500u, "", - "WebVtt testing (5.0 - 12.5)"))); - EXPECT_CALL(*Output(kOutput), OnProcess(IsSegmentInfo( - kInput, kSegmentStart2, kSegmentDuration, - !kSubSegment, !kEncrypted))); - - EXPECT_CALL(*Output(kOutput), OnFlush(kInput)); + EXPECT_CALL(*Output(kOutput), OnFlush(kStreamIndex)); } ASSERT_OK(Input(kInput)->Dispatch( - StreamData::FromStreamInfo(0, GetTextStreamInfo()))); + StreamData::FromStreamInfo(kStreamIndex, GetTextStreamInfo()))); ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( - kOutput, - GetTextSample("1", 5000, 8500, "WebVtt testing Line 1 (5.0 - 8.5)")))); + kStreamIndex, + GetTextSample(kNoId, kSample0Start, kSample0End, kNoPayload)))); ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( - kOutput, - GetTextSample("2", 5000, 8500, "WebVtt testing Line 2 (5.0 - 8.5)")))); - ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( - kOutput, - GetTextSample("3", 5000, 12500, "WebVtt testing (5.0 - 12.5)")))); + kStreamIndex, + GetTextSample(kNoId, kSample1Start, kSample1End, kNoPayload)))); + ASSERT_OK(Input(kInput)->FlushAllDownstreams()); +} - ASSERT_OK(Input(kInputIndex)->FlushAllDownstreams()); +// S0 S1 S2 +// | | | +// | [---A---] | +// | | | +TEST_F(TextChunkerTest, SampleCrossesSegments) { + const int64_t kSegmentDuration = 2 * kTick; + const int64_t kSegment0Start = 0; + const int64_t kSegment1Start = kSegment0Start + kSegmentDuration; + + const int64_t kSampleStart = kTick; + const int64_t kSampleDuration = 2 * kTick; + const int64_t kSampleEnd = kSampleStart + kSampleDuration; + + Init(kSegmentDuration); + + { + testing::InSequence s; + + EXPECT_CALL(*Output(kOutput), OnProcess(IsStreamInfo(kStreamIndex))); + + // Segment One + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kNoId, kSampleStart, kSampleEnd, + kNoSettings, kNoPayload))); + EXPECT_CALL( + *Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment0Start, kSegmentDuration, + !kSubSegment, !kEncrypted))); + + // Segment Two + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kNoId, kSampleStart, kSampleEnd, + kNoSettings, kNoPayload))); + EXPECT_CALL( + *Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, kSegmentDuration, + !kSubSegment, !kEncrypted))); + + EXPECT_CALL(*Output(kOutput), OnFlush(kStreamIndex)); + } + + ASSERT_OK(Input(kInput)->Dispatch( + StreamData::FromStreamInfo(kStreamIndex, GetTextStreamInfo()))); + ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( + kStreamIndex, + GetTextSample(kNoId, kSampleStart, kSampleEnd, kNoPayload)))); + ASSERT_OK(Input(kInput)->FlushAllDownstreams()); +} + +// S0 S1 S2 S3 +// | | | | +// | [-A----|----] | | +// | [-B----|----] | | +// | [-C----|----------|----] | +// | | | | +TEST_F(TextChunkerTest, PreservesOrder) { + const int64_t kSegmentDuration = 2 * kTick; + + const int64_t kSegment0Start = 0; + const int64_t kSegment1Start = kSegment0Start + kSegmentDuration; + const int64_t kSegment2Start = kSegment1Start + kSegmentDuration; + + const int64_t kSample0Start = kTick; + const int64_t kSample0Duration = 2 * kTick; + const int64_t kSample0End = kSample0Start + kSample0Duration; + + const int64_t kSample1Start = kTick; + const int64_t kSample1Duration = 2 * kTick; + const int64_t kSample1End = kSample1Start + kSample1Duration; + + const int64_t kSample2Start = kTick; + const int64_t kSample2Duration = 4 * kTick; + const int64_t kSample2End = kSample2Start + kSample2Duration; + + const char* kSample0Id = "sample 0"; + const char* kSample1Id = "sample 1"; + const char* kSample2Id = "sample 2"; + + Init(kSegmentDuration); + + { + testing::InSequence s; + + EXPECT_CALL(*Output(kOutput), OnProcess(IsStreamInfo(kStreamIndex))); + + // Segment One + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kSample0Id, kSample0Start, kSample0End, + kNoSettings, kNoPayload))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kSample1Id, kSample1Start, kSample1End, + kNoSettings, kNoPayload))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kSample2Id, kSample2Start, kSample2End, + kNoSettings, kNoPayload))); + EXPECT_CALL( + *Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment0Start, kSegmentDuration, + !kSubSegment, !kEncrypted))); + + // Segment Two + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kSample0Id, kSample0Start, kSample0End, + kNoSettings, kNoPayload))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kSample1Id, kSample1Start, kSample1End, + kNoSettings, kNoPayload))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kSample2Id, kSample2Start, kSample2End, + kNoSettings, kNoPayload))); + EXPECT_CALL( + *Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, kSegmentDuration, + !kSubSegment, !kEncrypted))); + + // Segment Two + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kSample2Id, kSample2Start, kSample2End, + kNoSettings, kNoPayload))); + EXPECT_CALL( + *Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment2Start, kSegmentDuration, + !kSubSegment, !kEncrypted))); + + EXPECT_CALL(*Output(kOutput), OnFlush(kStreamIndex)); + } + + ASSERT_OK(Input(kInput)->Dispatch( + StreamData::FromStreamInfo(kStreamIndex, GetTextStreamInfo()))); + ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( + kStreamIndex, + GetTextSample(kSample0Id, kSample0Start, kSample0End, kNoPayload)))); + ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( + kStreamIndex, + GetTextSample(kSample1Id, kSample1Start, kSample1End, kNoPayload)))); + ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( + kStreamIndex, + GetTextSample(kSample2Id, kSample2Start, kSample2End, kNoPayload)))); + ASSERT_OK(Input(kInput)->FlushAllDownstreams()); +} + +// S0 S1 S2 S3 S4 S5 +// | | | | | | +// | [--|-----|--A--|-----|--] | +// | | [--|--B--|--] | | +// | | | | | | +TEST_F(TextChunkerTest, NestedSamples) { + const int64_t kSegmentDuration = 2 * kTick; + + const int64_t kSample0Start = 1 * kTick; + const int64_t kSample0Duration = 8 * kTick; + const int64_t kSample0End = kSample0Start + kSample0Duration; + + const int64_t kSample1Start = 3 * kTick; + const int64_t kSample1Duration = 4 * kTick; + const int64_t kSample1End = kSample1Start + kSample1Duration; + + const int64_t kSegment0Start = 0; + const int64_t kSegment1Start = kSegment0Start + kSegmentDuration; + const int64_t kSegment2Start = kSegment1Start + kSegmentDuration; + const int64_t kSegment3Start = kSegment2Start + kSegmentDuration; + const int64_t kSegment4Start = kSegment3Start + kSegmentDuration; + + Init(kSegmentDuration); + + { + testing::InSequence s; + + EXPECT_CALL(*Output(kOutput), OnProcess(IsStreamInfo(kStreamIndex))); + + // Segment 0 + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kNoId, kSample0Start, kSample0End, + kNoSettings, kNoPayload))); + EXPECT_CALL( + *Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment0Start, kSegmentDuration, + !kSubSegment, !kEncrypted))); + + // Segment 1 + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kNoId, kSample0Start, kSample0End, + kNoSettings, kNoPayload))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kNoId, kSample1Start, kSample1End, + kNoSettings, kNoPayload))); + EXPECT_CALL( + *Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, kSegmentDuration, + !kSubSegment, !kEncrypted))); + + // Segment 2 + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kNoId, kSample0Start, kSample0End, + kNoSettings, kNoPayload))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kNoId, kSample1Start, kSample1End, + kNoSettings, kNoPayload))); + EXPECT_CALL( + *Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment2Start, kSegmentDuration, + !kSubSegment, !kEncrypted))); + + // Segment 3 + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kNoId, kSample0Start, kSample0End, + kNoSettings, kNoPayload))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kNoId, kSample1Start, kSample1End, + kNoSettings, kNoPayload))); + EXPECT_CALL( + *Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment3Start, kSegmentDuration, + !kSubSegment, !kEncrypted))); + + // Segment 4 + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kNoId, kSample0Start, kSample0End, + kNoSettings, kNoPayload))); + EXPECT_CALL( + *Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment4Start, kSegmentDuration, + !kSubSegment, !kEncrypted))); + + EXPECT_CALL(*Output(kOutput), OnFlush(kStreamIndex)); + } + + ASSERT_OK(Input(kInput)->Dispatch( + StreamData::FromStreamInfo(kStreamIndex, GetTextStreamInfo()))); + ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( + kStreamIndex, + GetTextSample(kNoId, kSample0Start, kSample0End, kNoPayload)))); + ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( + kStreamIndex, + GetTextSample(kNoId, kSample1Start, kSample1End, kNoPayload)))); + ASSERT_OK(Input(kInput)->FlushAllDownstreams()); +} + +// S0 S1 S2 S3 +// | | | | +// | [------A--------]| | +// | | |[--B--] | +// | | | | +TEST_F(TextChunkerTest, SecondSampleStartsAfterMultiSegmentSampleEnds) { + const int64_t kSegmentDuration = 2 * kTick; + const int64_t kSegment0Start = 0; + const int64_t kSegment1Start = kSegment0Start + kSegmentDuration; + const int64_t kSegment2Start = kSegment1Start + kSegmentDuration; + + const int64_t kSample0Start = kTick; + const int64_t kSample0Duration = 3 * kTick; + const int64_t kSample0End = kSample0Start + kSample0Duration; + + const int64_t kSample1Start = 4 * kTick; + const int64_t kSample1Duration = kTick; + const int64_t kSample1End = kSample1Start + kSample1Duration; + + Init(kSegmentDuration); + + { + testing::InSequence s; + + EXPECT_CALL(*Output(kOutput), OnProcess(IsStreamInfo(kStreamIndex))); + + // Segment One + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kNoId, kSample0Start, kSample0End, + kNoSettings, kNoPayload))); + EXPECT_CALL( + *Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment0Start, kSegmentDuration, + !kSubSegment, !kEncrypted))); + + // Segment Two + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kNoId, kSample0Start, kSample0End, + kNoSettings, kNoPayload))); + EXPECT_CALL( + *Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, kSegmentDuration, + !kSubSegment, !kEncrypted))); + + // Segment Three + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kNoId, kSample1Start, kSample1End, + kNoSettings, kNoPayload))); + EXPECT_CALL( + *Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment2Start, kSegmentDuration, + !kSubSegment, !kEncrypted))); + + EXPECT_CALL(*Output(kOutput), OnFlush(kStreamIndex)); + } + + ASSERT_OK(Input(kInput)->Dispatch( + StreamData::FromStreamInfo(kStreamIndex, GetTextStreamInfo()))); + ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( + kStreamIndex, + GetTextSample(kNoId, kSample0Start, kSample0End, kNoPayload)))); + ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( + kStreamIndex, + GetTextSample(kNoId, kSample1Start, kSample1End, kNoPayload)))); + ASSERT_OK(Input(kInput)->FlushAllDownstreams()); +} + +// S0 C0 C1 S1 * * +// s0 s1 s2 * * s3 +// | | | | +// | [---|-----A----|---] | +// | | | | +// The segment duration is 8 ticks, but with the cues being injected, c0 will +// become s1, c1 will become s3, and S1 will become s3. +TEST_F(TextChunkerTest, SampleSpanningMultipleCues) { + const int64_t kSegmentDuration = 8 * kTick; + + const int64_t kSampleStart = kTick; + const int64_t kSampleDuration = 4 * kTick; + const int64_t kSampleEnd = kSampleStart + kSampleDuration; + + const int64_t kCue0Time = 2 * kTick; + const double kCue0TimeInSeconds = kCue0Time / 1000.0; + const int64_t kCue1Time = 4 * kTick; + const double kCue1TimeInSeconds = kCue1Time / 1000.0; + + const int64_t kSegment0Start = 0; + const int64_t kSegment0Duration = 2 * kTick; + const int64_t kSegment1Start = kSegment0Start + kSegment0Duration; + const int64_t kSegment1Duration = 2 * kTick; + const int64_t kSegment2Start = kSegment1Start + kSegment1Duration; + const int64_t kSegment2Duration = 8 * kTick; + + Init(kSegmentDuration); + + { + testing::InSequence s; + + EXPECT_CALL(*Output(kOutput), OnProcess(IsStreamInfo(kStreamIndex))); + + // Segment 0 and Cue 0 + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kNoId, kSampleStart, kSampleEnd, + kNoSettings, kNoPayload))); + EXPECT_CALL( + *Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment0Start, kSegment0Duration, + !kSubSegment, !kEncrypted))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsCueEvent(kStreamIndex, kCue0TimeInSeconds))); + + // Segment 1 and Cue 1 + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kNoId, kSampleStart, kSampleEnd, + kNoSettings, kNoPayload))); + EXPECT_CALL( + *Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, kSegment1Duration, + !kSubSegment, !kEncrypted))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsCueEvent(kStreamIndex, kCue1TimeInSeconds))); + + // Segment 2 + EXPECT_CALL(*Output(kOutput), + OnProcess(IsTextSample(kNoId, kSampleStart, kSampleEnd, + kNoSettings, kNoPayload))); + EXPECT_CALL( + *Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment2Start, kSegment2Duration, + !kSubSegment, !kEncrypted))); + } + + ASSERT_OK(Input(kInput)->Dispatch( + StreamData::FromStreamInfo(kStreamIndex, GetTextStreamInfo()))); + ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( + kStreamIndex, + GetTextSample(kNoId, kSampleStart, kSampleEnd, kNoPayload)))); + ASSERT_OK(Input(kInput)->Dispatch( + StreamData::FromCueEvent(kStreamIndex, GetCueEvent(kCue0TimeInSeconds)))); + ASSERT_OK(Input(kInput)->Dispatch( + StreamData::FromCueEvent(kStreamIndex, GetCueEvent(kCue1TimeInSeconds)))); + ASSERT_OK(Input(kInput)->FlushAllDownstreams()); } } // namespace media