diff --git a/packager/media/chunking/text_chunker.cc b/packager/media/chunking/text_chunker.cc index 8aafe096b5..54513efe76 100644 --- a/packager/media/chunking/text_chunker.cc +++ b/packager/media/chunking/text_chunker.cc @@ -12,27 +12,10 @@ 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(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; -} +TextChunker::TextChunker(double segment_duration_in_seconds) + : segment_duration_in_seconds_(segment_duration_in_seconds){}; Status TextChunker::Process(std::unique_ptr data) { switch (data->stream_data_type) { @@ -49,75 +32,89 @@ Status TextChunker::Process(std::unique_ptr data) { } Status TextChunker::OnFlushRequest(size_t input_stream_index) { - // Keep outputting segments until all the samples leave the system. - while (segment_samples_.size()) { - RETURN_IF_ERROR(EndSegment(segment_expected_end_ms_)); + // Keep outputting segments until all the samples leave the system. Calling + // |DispatchSegment| will remove samples over time. + while (samples_in_current_segment_.size()) { + RETURN_IF_ERROR(DispatchSegment(segment_duration_)); } return FlushAllDownstreams(); } Status TextChunker::OnStreamInfo(std::shared_ptr info) { - // There is no information we need from the stream info, so just pass it - // downstream. + time_scale_ = info->time_scale(); + segment_duration_ = ScaleTime(segment_duration_in_seconds_); + return DispatchStreamInfo(kStreamIndex, std::move(info)); } 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; + // We are going to end the current segment prematurely using the cue event's + // time as the new segment end. - // 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_)); + // Because the cue should have been inserted into the stream such that no + // later sample could start before it does, we know that there should + // be no later samples starting before the cue event. + + // Convert the event's time to be scaled to the time of each sample. + const int64_t event_time = ScaleTime(event->time_in_seconds); + + // Output all full segments before the segment that the cue event interupts. + while (segment_start_ + segment_duration_ < event_time) { + RETURN_IF_ERROR(DispatchSegment(segment_duration_)); } - RETURN_IF_ERROR(EndSegment(cue_time_in_ms)); - RETURN_IF_ERROR(DispatchCueEvent(kStreamIndex, std::move(event))); + const int64_t shorten_duration = event_time - segment_start_; - return Status::OK; + RETURN_IF_ERROR(DispatchSegment(shorten_duration)); + return DispatchCueEvent(kStreamIndex, std::move(event)); } 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_)); + const int64_t sample_start = sample->start_time(); + while (sample_start >= segment_start_ + segment_duration_) { + RETURN_IF_ERROR(DispatchSegment(segment_duration_)); } - segment_samples_.push_back(std::move(sample)); + samples_in_current_segment_.push_back(std::move(sample)); return Status::OK; } -Status TextChunker::EndSegment(int64_t segment_actual_end_ms) { +Status TextChunker::DispatchSegment(int64_t duration) { + DCHECK_GT(duration, 0) << "Segment duration should always be positive"; + // Output all the samples that are part of the segment. - for (const auto& sample : segment_samples_) { + for (const auto& sample : samples_in_current_segment_) { RETURN_IF_ERROR(DispatchTextSample(kStreamIndex, sample)); } - RETURN_IF_ERROR(DispatchSegmentInfo( - kStreamIndex, MakeSegmentInfo(segment_start_ms_, segment_actual_end_ms))); + // Output the segment info. + std::shared_ptr info = std::make_shared(); + info->start_timestamp = segment_start_; + info->duration = duration; + RETURN_IF_ERROR(DispatchSegmentInfo(kStreamIndex, std::move(info))); - // 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); + // Move onto the next segment. + const int64_t new_segment_start = segment_start_ + duration; + segment_start_ = new_segment_start; + + // Remove all samples that end before the (new) current segment started. + samples_in_current_segment_.remove_if( + [new_segment_start](const std::shared_ptr& sample) { + // For the sample to even be in this list, it should have started + // before the (new) current segment. + DCHECK_LT(sample->start_time(), new_segment_start); + return sample->EndTime() <= new_segment_start; + }); 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; - }); +int64_t TextChunker::ScaleTime(double seconds) const { + DCHECK_GT(time_scale_, 0) << "Need positive time scale to scale time."; + return static_cast(seconds * time_scale_); } - } // namespace media } // namespace shaka diff --git a/packager/media/chunking/text_chunker.h b/packager/media/chunking/text_chunker.h index 13bee2e946..91879993b2 100644 --- a/packager/media/chunking/text_chunker.h +++ b/packager/media/chunking/text_chunker.h @@ -14,15 +14,19 @@ namespace shaka { namespace media { +// Media handler for taking a single stream of text samples and inserting +// segment info based on a fixed segment duration and on cue events. The +// only time a segment's duration will not match the fixed segment duration +// is when a cue event is seen. class TextChunker : public MediaHandler { public: - explicit TextChunker(int64_t segment_duration_ms); + explicit TextChunker(double segment_duration_in_seconds); private: TextChunker(const TextChunker&) = delete; TextChunker& operator=(const TextChunker&) = delete; - Status InitializeInternal() override; + Status InitializeInternal() override { return Status::OK; } Status Process(std::unique_ptr stream_data) override; Status OnFlushRequest(size_t input_stream_index) override; @@ -31,17 +35,27 @@ class TextChunker : public MediaHandler { Status OnCueEvent(std::shared_ptr cue); Status OnTextSample(std::shared_ptr sample); - Status EndSegment(int64_t segment_actual_end_ms); - void StartNewSegment(int64_t start_ms); + // This does two things that should always happen together: + // 1. Dispatch all the samples and a segment info for the time range + // segment_start_ to segment_start_ + duration + // 2. Set the next segment to start at segment_start_ + duration and + // remove all samples that don't last into that segment. + Status DispatchSegment(int64_t duration); - int64_t segment_duration_ms_; + int64_t ScaleTime(double seconds) const; - // 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_; + double segment_duration_in_seconds_; + + int64_t time_scale_ = -1; // Set in OnStreamInfo + + // Time values are in scaled units. + int64_t segment_start_ = 0; + int64_t segment_duration_ = -1; // Set in OnStreamInfo + + // All samples that make up the current segment. We must store the samples + // until the segment ends because a cue event may end the segment sooner + // than we expected. + std::list> samples_in_current_segment_; }; } // namespace media diff --git a/packager/media/chunking/text_chunker_unittest.cc b/packager/media/chunking/text_chunker_unittest.cc index 92be56dc90..4b7a87c932 100644 --- a/packager/media/chunking/text_chunker_unittest.cc +++ b/packager/media/chunking/text_chunker_unittest.cc @@ -27,7 +27,7 @@ const size_t kOutput = 0; const bool kEncrypted = true; const bool kSubSegment = true; -const int64_t kTick = 50; +const uint64_t kTimescaleMs = 1000; const char* kNoId = ""; const char* kNoSettings = ""; @@ -36,25 +36,33 @@ const char* kNoPayload = ""; class TextChunkerTest : public MediaHandlerTestBase { protected: - void Init(int64_t segment_duration) { + void Init(double segment_duration) { ASSERT_OK(SetUpAndInitializeGraph( std::make_shared(segment_duration), kInputs, kOutputs)); } }; -// S0 S1 -// | | -// |[---A---]| -// | | +// Verify that when a sample elapses a full segment, that it only appears +// in the one segment. +// +// Segment Duration = 100 MS +// +// TIME (ms):0 5 1 +// 0 0 +// 0 +// SAMPLES :[-----A-----] +// SEGMENTS : ^ +// TEST_F(TextChunkerTest, SampleEndingOnSegmentStart) { - const int64_t kSegmentStart = 0; - const int64_t kSegmentDuration = kTick; + const double kSegmentDurationSec = 0.1; + const int64_t kSegmentDurationMs = 100; - const int64_t kSampleStart = 0; - const int64_t kSampleDuration = kTick; - const int64_t kSampleEnd = kSampleStart + kSampleDuration; + const int64_t kSegment0Start = 0; - Init(kSegmentDuration); + const int64_t kSampleAStart = 0; + const int64_t kSampleAEnd = 100; + + Init(kSegmentDurationSec); { testing::InSequence s; @@ -62,12 +70,12 @@ TEST_F(TextChunkerTest, SampleEndingOnSegmentStart) { EXPECT_CALL(*Output(kOutput), OnProcess(IsStreamInfo(kStreamIndex))); EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kNoId, kSampleStart, kSampleEnd, + OnProcess(IsTextSample(kNoId, kSampleAStart, kSampleAEnd, kNoSettings, kNoPayload))); - EXPECT_CALL( - *Output(kOutput), - OnProcess(IsSegmentInfo(kStreamIndex, kSegmentStart, kSegmentDuration, - !kSubSegment, !kEncrypted))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment0Start, + kSegmentDurationMs, !kSubSegment, + !kEncrypted))); EXPECT_CALL(*Output(kOutput), OnFlush(kStreamIndex)); } @@ -75,29 +83,36 @@ TEST_F(TextChunkerTest, SampleEndingOnSegmentStart) { kStreamIndex, GetTextStreamInfo(kMsTimeScale)))); ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( kStreamIndex, - GetTextSample(kNoId, kSampleStart, kSampleEnd, kNoPayload)))); + GetTextSample(kNoId, kSampleAStart, kSampleAEnd, kNoPayload)))); ASSERT_OK(Input(kInput)->FlushAllDownstreams()); } -// S0 S1 S2 -// | | | -// |[-A-] | | -// | |[-B-] | -// | | | +// Verify that samples only appear in the correct segments when they only exist +// in one segment. +// +// Segment Duration = 100 MS +// +// TIME (ms):0 5 1 1 2 +// 0 0 5 0 +// 0 0 0 +// SAMPLES : [--A--] +// [--B--] +// SEGMENTS : ^ ^ +// TEST_F(TextChunkerTest, CreatesSegmentsForSamples) { - const int64_t kSegmentDuration = 2 * kTick; + const double kSegmentDurationSec = 0.1; + const int64_t kSegmentDurationMs = 100; + const int64_t kSegment0Start = 0; - const int64_t kSegment1Start = kSegment0Start + kSegmentDuration; + const int64_t kSegment1Start = 100; - const int64_t kSample0Start = 0; - const int64_t kSample0Duration = kTick; - const int64_t kSample0End = kSample0Start + kSample0Duration; + const int64_t kSampleAStart = 25; + const int64_t kSampleAEnd = 75; - const int64_t kSample1Start = 3 * kTick; - const int64_t kSample1Duration = kTick; - const int64_t kSample1End = kSample1Start + kSample1Duration; + const int64_t kSampleBStart = 125; + const int64_t kSampleBEnd = 175; - Init(kSegmentDuration); + Init(kSegmentDurationSec); { testing::InSequence s; @@ -106,21 +121,21 @@ TEST_F(TextChunkerTest, CreatesSegmentsForSamples) { // Segment One EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kNoId, kSample0Start, kSample0End, + OnProcess(IsTextSample(kNoId, kSampleAStart, kSampleAEnd, kNoSettings, kNoPayload))); - EXPECT_CALL( - *Output(kOutput), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment0Start, kSegmentDuration, - !kSubSegment, !kEncrypted))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment0Start, + kSegmentDurationMs, !kSubSegment, + !kEncrypted))); // Segment Two EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kNoId, kSample1Start, kSample1End, + OnProcess(IsTextSample(kNoId, kSampleBStart, kSampleBEnd, kNoSettings, kNoPayload))); - EXPECT_CALL( - *Output(kOutput), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, kSegmentDuration, - !kSubSegment, !kEncrypted))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, + kSegmentDurationMs, !kSubSegment, + !kEncrypted))); EXPECT_CALL(*Output(kOutput), OnFlush(kStreamIndex)); } @@ -129,33 +144,40 @@ TEST_F(TextChunkerTest, CreatesSegmentsForSamples) { kStreamIndex, GetTextStreamInfo(kMsTimeScale)))); ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( kStreamIndex, - GetTextSample(kNoId, kSample0Start, kSample0End, kNoPayload)))); + GetTextSample(kNoId, kSampleAStart, kSampleAEnd, kNoPayload)))); ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( kStreamIndex, - GetTextSample(kNoId, kSample1Start, kSample1End, kNoPayload)))); + GetTextSample(kNoId, kSampleBStart, kSampleBEnd, kNoPayload)))); ASSERT_OK(Input(kInput)->FlushAllDownstreams()); } -// S0 S1 S2 S3 -// | | | | -// |[-A-] | | | -// | | |[-B-] | -// | | | | +// Verify that a segment will get outputted even if there are no samples +// overlapping with it. +// +// Segment Duration = 100 MS +// +// TIME (ms):0 5 1 1 2 2 3 +// 0 0 5 0 5 0 +// 0 0 0 0 0 +// SAMPLES : [--A--] +// [--B--] +// SEGMENTS : ^ ^ ^ +// TEST_F(TextChunkerTest, OutputsEmptySegments) { - const int64_t kSegmentDuration = 2 * kTick; + const double kSegmentDurationSec = 0.1; + const int64_t kSegmentDurationMs = 100; + const int64_t kSegment0Start = 0; - const int64_t kSegment1Start = kSegment0Start + kSegmentDuration; - const int64_t kSegment2Start = kSegment1Start + kSegmentDuration; + const int64_t kSegment1Start = 100; + const int64_t kSegment2Start = 200; - const int64_t kSample0Start = 0; - const int64_t kSample0Duration = kTick; - const int64_t kSample0End = kSample0Start + kSample0Duration; + const int64_t kSampleAStart = 25; + const int64_t kSampleAEnd = 75; - const int64_t kSample1Start = 4 * kTick; - const int64_t kSample1Duration = kTick; - const int64_t kSample1End = kSample1Start + kSample1Duration; + const int64_t kSampleBStart = 225; + const int64_t kSampleBEnd = 275; - Init(kSegmentDuration); + Init(kSegmentDurationSec); { testing::InSequence s; @@ -164,27 +186,27 @@ TEST_F(TextChunkerTest, OutputsEmptySegments) { // Segment One EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kNoId, kSample0Start, kSample0End, + OnProcess(IsTextSample(kNoId, kSampleAStart, kSampleAEnd, kNoSettings, kNoPayload))); - EXPECT_CALL( - *Output(kOutput), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment0Start, kSegmentDuration, - !kSubSegment, !kEncrypted))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment0Start, + kSegmentDurationMs, !kSubSegment, + !kEncrypted))); // Segment Two (empty segment) - EXPECT_CALL( - *Output(kOutput), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, kSegmentDuration, - !kSubSegment, !kEncrypted))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, + kSegmentDurationMs, !kSubSegment, + !kEncrypted))); // Segment Three EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kNoId, kSample1Start, kSample1End, + OnProcess(IsTextSample(kNoId, kSampleBStart, kSampleBEnd, kNoSettings, kNoPayload))); - EXPECT_CALL( - *Output(kOutput), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment2Start, kSegmentDuration, - !kSubSegment, !kEncrypted))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment2Start, + kSegmentDurationMs, !kSubSegment, + !kEncrypted))); EXPECT_CALL(*Output(kOutput), OnFlush(kStreamIndex)); } @@ -193,27 +215,35 @@ TEST_F(TextChunkerTest, OutputsEmptySegments) { kStreamIndex, GetTextStreamInfo(kMsTimeScale)))); ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( kStreamIndex, - GetTextSample(kNoId, kSample0Start, kSample0End, kNoPayload)))); + GetTextSample(kNoId, kSampleAStart, kSampleAEnd, kNoPayload)))); ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( kStreamIndex, - GetTextSample(kNoId, kSample1Start, kSample1End, kNoPayload)))); + GetTextSample(kNoId, kSampleBStart, kSampleBEnd, kNoPayload)))); ASSERT_OK(Input(kInput)->FlushAllDownstreams()); } -// S0 S1 S2 -// | | | -// | [---A---] | -// | | | +// Verify that samples that overlap multiple samples get dispatch in all +// segments. +// +// Segment Duration = 100 MS +// +// TIME (ms):0 5 1 1 +// 0 0 5 +// 0 0 +// SAMPLES : [-----A-----] +// SEGMENTS : ^ +// TEST_F(TextChunkerTest, SampleCrossesSegments) { - const int64_t kSegmentDuration = 2 * kTick; + const double kSegmentDurationSec = 0.1; + const int64_t kSegmentDurationMs = 100; + const int64_t kSegment0Start = 0; - const int64_t kSegment1Start = kSegment0Start + kSegmentDuration; + const int64_t kSegment1Start = 100; - const int64_t kSampleStart = kTick; - const int64_t kSampleDuration = 2 * kTick; - const int64_t kSampleEnd = kSampleStart + kSampleDuration; + const int64_t kSampleAStart = 50; + const int64_t kSampleAEnd = 150; - Init(kSegmentDuration); + Init(kSegmentDurationSec); { testing::InSequence s; @@ -222,63 +252,68 @@ TEST_F(TextChunkerTest, SampleCrossesSegments) { // Segment One EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kNoId, kSampleStart, kSampleEnd, + OnProcess(IsTextSample(kNoId, kSampleAStart, kSampleAEnd, kNoSettings, kNoPayload))); - EXPECT_CALL( - *Output(kOutput), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment0Start, kSegmentDuration, - !kSubSegment, !kEncrypted))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment0Start, + kSegmentDurationMs, !kSubSegment, + !kEncrypted))); // Segment Two EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kNoId, kSampleStart, kSampleEnd, + OnProcess(IsTextSample(kNoId, kSampleAStart, kSampleAEnd, kNoSettings, kNoPayload))); - EXPECT_CALL( - *Output(kOutput), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, kSegmentDuration, - !kSubSegment, !kEncrypted))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, + kSegmentDurationMs, !kSubSegment, + !kEncrypted))); EXPECT_CALL(*Output(kOutput), OnFlush(kStreamIndex)); } ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromStreamInfo( - kStreamIndex, GetTextStreamInfo(kMsTimeScale)))); + kStreamIndex, GetTextStreamInfo(kTimescaleMs)))); ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( kStreamIndex, - GetTextSample(kNoId, kSampleStart, kSampleEnd, kNoPayload)))); + GetTextSample(kNoId, kSampleAStart, kSampleAEnd, kNoPayload)))); ASSERT_OK(Input(kInput)->FlushAllDownstreams()); } -// S0 S1 S2 S3 -// | | | | -// | [-A----|----] | | -// | [-B----|----] | | -// | [-C----|----------|----] | -// | | | | +// Verify that samples that overlap multiple samples get dispatch in all +// segments, even if different samples elapse different number of segments. +// +// Segment Duration = 100 MS +// +// TIME (ms):0 5 1 1 2 2 3 +// 0 0 5 0 5 0 +// 0 0 0 0 0 +// SAMPLES : [-----A-----] +// [-----B-----] +// [-----------C-----------] +// SEGMENTS : ^ ^ ^ +// TEST_F(TextChunkerTest, PreservesOrder) { - const int64_t kSegmentDuration = 2 * kTick; + const double kSegmentDurationSec = 0.1; + const int64_t kSegmentDurationMs = 100; const int64_t kSegment0Start = 0; - const int64_t kSegment1Start = kSegment0Start + kSegmentDuration; - const int64_t kSegment2Start = kSegment1Start + kSegmentDuration; + const int64_t kSegment1Start = 100; + const int64_t kSegment2Start = 200; - const int64_t kSample0Start = kTick; - const int64_t kSample0Duration = 2 * kTick; - const int64_t kSample0End = kSample0Start + kSample0Duration; + const int64_t kSampleAStart = 50; + const int64_t kSampleAEnd = 150; - const int64_t kSample1Start = kTick; - const int64_t kSample1Duration = 2 * kTick; - const int64_t kSample1End = kSample1Start + kSample1Duration; + const int64_t kSampleBStart = 50; + const int64_t kSampleBEnd = 150; - const int64_t kSample2Start = kTick; - const int64_t kSample2Duration = 4 * kTick; - const int64_t kSample2End = kSample2Start + kSample2Duration; + const int64_t kSampleCStart = 50; + const int64_t kSampleCEnd = 250; - const char* kSample0Id = "sample 0"; - const char* kSample1Id = "sample 1"; - const char* kSample2Id = "sample 2"; + const char* kSampleAId = "sample 0"; + const char* kSampleBId = "sample 1"; + const char* kSampleCId = "sample 2"; - Init(kSegmentDuration); + Init(kSegmentDurationSec); { testing::InSequence s; @@ -287,42 +322,42 @@ TEST_F(TextChunkerTest, PreservesOrder) { // Segment One EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kSample0Id, kSample0Start, kSample0End, + OnProcess(IsTextSample(kSampleAId, kSampleAStart, kSampleAEnd, kNoSettings, kNoPayload))); EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kSample1Id, kSample1Start, kSample1End, + OnProcess(IsTextSample(kSampleBId, kSampleBStart, kSampleBEnd, kNoSettings, kNoPayload))); EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kSample2Id, kSample2Start, kSample2End, + OnProcess(IsTextSample(kSampleCId, kSampleCStart, kSampleCEnd, kNoSettings, kNoPayload))); - EXPECT_CALL( - *Output(kOutput), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment0Start, kSegmentDuration, - !kSubSegment, !kEncrypted))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment0Start, + kSegmentDurationMs, !kSubSegment, + !kEncrypted))); // Segment Two EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kSample0Id, kSample0Start, kSample0End, + OnProcess(IsTextSample(kSampleAId, kSampleAStart, kSampleAEnd, kNoSettings, kNoPayload))); EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kSample1Id, kSample1Start, kSample1End, + OnProcess(IsTextSample(kSampleBId, kSampleBStart, kSampleBEnd, kNoSettings, kNoPayload))); EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kSample2Id, kSample2Start, kSample2End, + OnProcess(IsTextSample(kSampleCId, kSampleCStart, kSampleCEnd, kNoSettings, kNoPayload))); - EXPECT_CALL( - *Output(kOutput), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, kSegmentDuration, - !kSubSegment, !kEncrypted))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, + kSegmentDurationMs, !kSubSegment, + !kEncrypted))); // Segment Two EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kSample2Id, kSample2Start, kSample2End, + OnProcess(IsTextSample(kSampleCId, kSampleCStart, kSampleCEnd, kNoSettings, kNoPayload))); - EXPECT_CALL( - *Output(kOutput), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment2Start, kSegmentDuration, - !kSubSegment, !kEncrypted))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment2Start, + kSegmentDurationMs, !kSubSegment, + !kEncrypted))); EXPECT_CALL(*Output(kOutput), OnFlush(kStreamIndex)); } @@ -331,39 +366,45 @@ TEST_F(TextChunkerTest, PreservesOrder) { kStreamIndex, GetTextStreamInfo(kMsTimeScale)))); ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( kStreamIndex, - GetTextSample(kSample0Id, kSample0Start, kSample0End, kNoPayload)))); + GetTextSample(kSampleAId, kSampleAStart, kSampleAEnd, kNoPayload)))); ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( kStreamIndex, - GetTextSample(kSample1Id, kSample1Start, kSample1End, kNoPayload)))); + GetTextSample(kSampleBId, kSampleBStart, kSampleBEnd, kNoPayload)))); ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( kStreamIndex, - GetTextSample(kSample2Id, kSample2Start, kSample2End, kNoPayload)))); + GetTextSample(kSampleCId, kSampleCStart, kSampleCEnd, kNoPayload)))); ASSERT_OK(Input(kInput)->FlushAllDownstreams()); } -// S0 S1 S2 S3 S4 S5 -// | | | | | | -// | [--|-----|--A--|-----|--] | -// | | [--|--B--|--] | | -// | | | | | | +// Check that when samples overlap/contain other samples, that they still +// get outputted in the correct segments. +// +// Segment Duration = 50 MS +// +// TIME (ms):0 5 1 1 2 2 +// 0 0 5 0 5 +// 0 0 0 0 +// SAMPLES : [-----------A-----------] +// [-----B------] +// SEGMENTS : ^ ^ ^ ^ ^ +// TEST_F(TextChunkerTest, NestedSamples) { - const int64_t kSegmentDuration = 2 * kTick; + const double kSegmentDurationSec = 0.05; + const int64_t kSegmentDurationMs = 50; - const int64_t kSample0Start = 1 * kTick; - const int64_t kSample0Duration = 8 * kTick; - const int64_t kSample0End = kSample0Start + kSample0Duration; + const int64_t kSampleAStart = 25; + const int64_t kSampleAEnd = 225; - const int64_t kSample1Start = 3 * kTick; - const int64_t kSample1Duration = 4 * kTick; - const int64_t kSample1End = kSample1Start + kSample1Duration; + const int64_t kSampleBStart = 75; + const int64_t kSampleBEnd = 175; 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; + const int64_t kSegment1Start = 50; + const int64_t kSegment2Start = 100; + const int64_t kSegment3Start = 150; + const int64_t kSegment4Start = 200; - Init(kSegmentDuration); + Init(kSegmentDurationSec); { testing::InSequence s; @@ -372,57 +413,57 @@ TEST_F(TextChunkerTest, NestedSamples) { // Segment 0 EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kNoId, kSample0Start, kSample0End, + OnProcess(IsTextSample(kNoId, kSampleAStart, kSampleAEnd, kNoSettings, kNoPayload))); - EXPECT_CALL( - *Output(kOutput), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment0Start, kSegmentDuration, - !kSubSegment, !kEncrypted))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment0Start, + kSegmentDurationMs, !kSubSegment, + !kEncrypted))); // Segment 1 EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kNoId, kSample0Start, kSample0End, + OnProcess(IsTextSample(kNoId, kSampleAStart, kSampleAEnd, kNoSettings, kNoPayload))); EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kNoId, kSample1Start, kSample1End, + OnProcess(IsTextSample(kNoId, kSampleBStart, kSampleBEnd, kNoSettings, kNoPayload))); - EXPECT_CALL( - *Output(kOutput), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, kSegmentDuration, - !kSubSegment, !kEncrypted))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, + kSegmentDurationMs, !kSubSegment, + !kEncrypted))); // Segment 2 EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kNoId, kSample0Start, kSample0End, + OnProcess(IsTextSample(kNoId, kSampleAStart, kSampleAEnd, kNoSettings, kNoPayload))); EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kNoId, kSample1Start, kSample1End, + OnProcess(IsTextSample(kNoId, kSampleBStart, kSampleBEnd, kNoSettings, kNoPayload))); - EXPECT_CALL( - *Output(kOutput), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment2Start, kSegmentDuration, - !kSubSegment, !kEncrypted))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment2Start, + kSegmentDurationMs, !kSubSegment, + !kEncrypted))); // Segment 3 EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kNoId, kSample0Start, kSample0End, + OnProcess(IsTextSample(kNoId, kSampleAStart, kSampleAEnd, kNoSettings, kNoPayload))); EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kNoId, kSample1Start, kSample1End, + OnProcess(IsTextSample(kNoId, kSampleBStart, kSampleBEnd, kNoSettings, kNoPayload))); - EXPECT_CALL( - *Output(kOutput), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment3Start, kSegmentDuration, - !kSubSegment, !kEncrypted))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment3Start, + kSegmentDurationMs, !kSubSegment, + !kEncrypted))); // Segment 4 EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kNoId, kSample0Start, kSample0End, + OnProcess(IsTextSample(kNoId, kSampleAStart, kSampleAEnd, kNoSettings, kNoPayload))); - EXPECT_CALL( - *Output(kOutput), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment4Start, kSegmentDuration, - !kSubSegment, !kEncrypted))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment4Start, + kSegmentDurationMs, !kSubSegment, + !kEncrypted))); EXPECT_CALL(*Output(kOutput), OnFlush(kStreamIndex)); } @@ -431,33 +472,40 @@ TEST_F(TextChunkerTest, NestedSamples) { kStreamIndex, GetTextStreamInfo(kMsTimeScale)))); ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( kStreamIndex, - GetTextSample(kNoId, kSample0Start, kSample0End, kNoPayload)))); + GetTextSample(kNoId, kSampleAStart, kSampleAEnd, kNoPayload)))); ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( kStreamIndex, - GetTextSample(kNoId, kSample1Start, kSample1End, kNoPayload)))); + GetTextSample(kNoId, kSampleBStart, kSampleBEnd, kNoPayload)))); ASSERT_OK(Input(kInput)->FlushAllDownstreams()); } -// S0 S1 S2 S3 -// | | | | -// | [------A--------]| | -// | | |[--B--] | -// | | | | +// Make sure that a sample that extends multiple segments is dropped when +// it no longer overlaps with a later segment. +// +// Segment Duration = 100 MS +// +// TIME (ms):0 5 1 1 2 2 3 +// 0 0 5 0 5 0 +// 0 0 0 0 0 +// SAMPLES : [--------A--------] +// [--B--] +// SEGMENTS : ^ ^ ^ +// TEST_F(TextChunkerTest, SecondSampleStartsAfterMultiSegmentSampleEnds) { - const int64_t kSegmentDuration = 2 * kTick; + const double kSegmentDurationSec = 0.1; + const int64_t kSegmentDurationMs = 100; + const int64_t kSegment0Start = 0; - const int64_t kSegment1Start = kSegment0Start + kSegmentDuration; - const int64_t kSegment2Start = kSegment1Start + kSegmentDuration; + const int64_t kSegment1Start = 100; + const int64_t kSegment2Start = 200; - const int64_t kSample0Start = kTick; - const int64_t kSample0Duration = 3 * kTick; - const int64_t kSample0End = kSample0Start + kSample0Duration; + const int64_t kSampleAStart = 50; + const int64_t kSampleAEnd = 200; - const int64_t kSample1Start = 4 * kTick; - const int64_t kSample1Duration = kTick; - const int64_t kSample1End = kSample1Start + kSample1Duration; + const int64_t kSampleBStart = 200; + const int64_t kSampleBEnd = 250; - Init(kSegmentDuration); + Init(kSegmentDurationSec); { testing::InSequence s; @@ -466,72 +514,77 @@ TEST_F(TextChunkerTest, SecondSampleStartsAfterMultiSegmentSampleEnds) { // Segment One EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kNoId, kSample0Start, kSample0End, + OnProcess(IsTextSample(kNoId, kSampleAStart, kSampleAEnd, kNoSettings, kNoPayload))); - EXPECT_CALL( - *Output(kOutput), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment0Start, kSegmentDuration, - !kSubSegment, !kEncrypted))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment0Start, + kSegmentDurationMs, !kSubSegment, + !kEncrypted))); // Segment Two EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kNoId, kSample0Start, kSample0End, + OnProcess(IsTextSample(kNoId, kSampleAStart, kSampleAEnd, kNoSettings, kNoPayload))); - EXPECT_CALL( - *Output(kOutput), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, kSegmentDuration, - !kSubSegment, !kEncrypted))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, + kSegmentDurationMs, !kSubSegment, + !kEncrypted))); // Segment Three EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kNoId, kSample1Start, kSample1End, + OnProcess(IsTextSample(kNoId, kSampleBStart, kSampleBEnd, kNoSettings, kNoPayload))); - EXPECT_CALL( - *Output(kOutput), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment2Start, kSegmentDuration, - !kSubSegment, !kEncrypted))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment2Start, + kSegmentDurationMs, !kSubSegment, + !kEncrypted))); EXPECT_CALL(*Output(kOutput), OnFlush(kStreamIndex)); } ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromStreamInfo( - kStreamIndex, GetTextStreamInfo(kMsTimeScale)))); + kStreamIndex, GetTextStreamInfo(kTimescaleMs)))); ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( kStreamIndex, - GetTextSample(kNoId, kSample0Start, kSample0End, kNoPayload)))); + GetTextSample(kNoId, kSampleAStart, kSampleAEnd, kNoPayload)))); ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( kStreamIndex, - GetTextSample(kNoId, kSample1Start, kSample1End, kNoPayload)))); + GetTextSample(kNoId, kSampleBStart, kSampleBEnd, 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. +// Check that segments will be injected when a cue event comes down the +// pipeline and that the segment duration will get reset after the cues +// are dispatched. +// +// Segment Duration = 300 MS +// +// TIME (ms):0 5 1 1 2 2 3 3 4 5 +// 0 0 5 0 5 0 5 5 0 +// 0 0 0 0 0 0 0 0 +// SAMPLES : [-----------A-----------] +// CUES : ^ ^ +// SEGMENTS : ^ ^ ^ +// TEST_F(TextChunkerTest, SampleSpanningMultipleCues) { - const int64_t kSegmentDuration = 8 * kTick; + const double kSegmentDurationSec = 0.3; + const int64_t kSegmentDurationMs = 300; - const int64_t kSampleStart = kTick; - const int64_t kSampleDuration = 4 * kTick; - const int64_t kSampleEnd = kSampleStart + kSampleDuration; + const int64_t kSampleAStart = 50; + const int64_t kSampleAEnd = 250; - 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 double kC0 = 0.1; + const double kC1 = 0.2; 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; + const int64_t kSegment1Start = 100; + const int64_t kSegment2Start = 200; + ; - Init(kSegmentDuration); + const double kSegment0StartLength = 100; + const double kSegment1StartLength = 100; + + Init(kSegmentDurationSec); { testing::InSequence s; @@ -540,45 +593,43 @@ TEST_F(TextChunkerTest, SampleSpanningMultipleCues) { // Segment 0 and Cue 0 EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kNoId, kSampleStart, kSampleEnd, + OnProcess(IsTextSample(kNoId, kSampleAStart, kSampleAEnd, kNoSettings, kNoPayload))); - EXPECT_CALL( - *Output(kOutput), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment0Start, kSegment0Duration, - !kSubSegment, !kEncrypted))); EXPECT_CALL(*Output(kOutput), - OnProcess(IsCueEvent(kStreamIndex, kCue0TimeInSeconds))); + OnProcess(IsSegmentInfo(kStreamIndex, kSegment0Start, + kSegment0StartLength, !kSubSegment, + !kEncrypted))); + EXPECT_CALL(*Output(kOutput), OnProcess(IsCueEvent(kStreamIndex, kC0))); // Segment 1 and Cue 1 EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kNoId, kSampleStart, kSampleEnd, + OnProcess(IsTextSample(kNoId, kSampleAStart, kSampleAEnd, kNoSettings, kNoPayload))); - EXPECT_CALL( - *Output(kOutput), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, kSegment1Duration, - !kSubSegment, !kEncrypted))); EXPECT_CALL(*Output(kOutput), - OnProcess(IsCueEvent(kStreamIndex, kCue1TimeInSeconds))); + OnProcess(IsSegmentInfo(kStreamIndex, kSegment1Start, + kSegment1StartLength, !kSubSegment, + !kEncrypted))); + EXPECT_CALL(*Output(kOutput), OnProcess(IsCueEvent(kStreamIndex, kC1))); // Segment 2 EXPECT_CALL(*Output(kOutput), - OnProcess(IsTextSample(kNoId, kSampleStart, kSampleEnd, + OnProcess(IsTextSample(kNoId, kSampleAStart, kSampleAEnd, kNoSettings, kNoPayload))); - EXPECT_CALL( - *Output(kOutput), - OnProcess(IsSegmentInfo(kStreamIndex, kSegment2Start, kSegment2Duration, - !kSubSegment, !kEncrypted))); + EXPECT_CALL(*Output(kOutput), + OnProcess(IsSegmentInfo(kStreamIndex, kSegment2Start, + kSegmentDurationMs, !kSubSegment, + !kEncrypted))); } ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromStreamInfo( - kStreamIndex, GetTextStreamInfo(kMsTimeScale)))); + kStreamIndex, GetTextStreamInfo(kTimescaleMs)))); ASSERT_OK(Input(kInput)->Dispatch(StreamData::FromTextSample( kStreamIndex, - GetTextSample(kNoId, kSampleStart, kSampleEnd, kNoPayload)))); + GetTextSample(kNoId, kSampleAStart, kSampleAEnd, kNoPayload)))); ASSERT_OK(Input(kInput)->Dispatch( - StreamData::FromCueEvent(kStreamIndex, GetCueEvent(kCue0TimeInSeconds)))); + StreamData::FromCueEvent(kStreamIndex, GetCueEvent(kC0)))); ASSERT_OK(Input(kInput)->Dispatch( - StreamData::FromCueEvent(kStreamIndex, GetCueEvent(kCue1TimeInSeconds)))); + StreamData::FromCueEvent(kStreamIndex, GetCueEvent(kC1)))); ASSERT_OK(Input(kInput)->FlushAllDownstreams()); } diff --git a/packager/packager.cc b/packager/packager.cc index 2c107522ca..e73687c402 100644 --- a/packager/packager.cc +++ b/packager/packager.cc @@ -456,10 +456,8 @@ std::unique_ptr CreateTextChunker( const ChunkingParams& chunking_params) { const float segment_length_in_seconds = chunking_params.segment_duration_in_seconds; - const uint64_t segment_length_in_ms = - static_cast(segment_length_in_seconds * 1000); - - return std::unique_ptr(new TextChunker(segment_length_in_ms)); + return std::unique_ptr( + new TextChunker(segment_length_in_seconds)); } Status CreateHlsTextJob(const StreamDescriptor& stream,