Output Empty MP4 WebVtt Cues

When a gap is found in the text stream, the WebVtt to Mp4 converter will
now output the special empty vtt cue.

Change-Id: I8be88c6b7589aa120a2215e1e4b8e98031fe326d
Closes: #324
This commit is contained in:
Aaron Vaage 2018-02-05 13:08:20 -08:00
parent 8257eea804
commit f35410deb1
2 changed files with 118 additions and 32 deletions

View File

@ -129,14 +129,10 @@ Status WebVttToMp4Handler::ProcessUpToTime(uint64_t cutoff_time) {
// Get the time range for which the current active state is valid. // Get the time range for which the current active state is valid.
const uint64_t previous_change = next_change_; const uint64_t previous_change = next_change_;
next_change_ = actions_.top()->time(); next_change_ = actions_.top()->time();
// The only time that |previous_change| and |next_change_| should ever break
// the rule |next_change_ > previous_change| is at the start where
// |previous_change| and |next_change_| are both zero.
DCHECK((previous_change == 0 && next_change_ == 0) ||
next_change_ > previous_change);
// Send out the active group. If there is nothing in the active group, then if (next_change_ > previous_change) {
// an empty cue is sent. // Send out the active group. If there is nothing in the active group,
// then an empty cue is sent.
Status status = Status status =
active_.size() active_.size()
? MergeAndSendSamples(active_, previous_change, next_change_) ? MergeAndSendSamples(active_, previous_change, next_change_)
@ -145,6 +141,13 @@ Status WebVttToMp4Handler::ProcessUpToTime(uint64_t cutoff_time) {
if (!status.ok()) { if (!status.ok()) {
return status; return status;
} }
} else {
// The only time that |previous_change| and |next_change_| should ever
// break the rule |next_change_ > previous_change| is at the start where
// |previous_change| and |next_change_| are both zero.
DCHECK_EQ(previous_change, 0u);
DCHECK_EQ(next_change_, 0u);
}
// STAGE 2: Move to the next state. // STAGE 2: Move to the next state.
while (actions_.size() && actions_.top()->time() == next_change_) { while (actions_.size() && actions_.top()->time() == next_change_) {
@ -180,8 +183,19 @@ Status WebVttToMp4Handler::MergeAndSendSamples(
Status WebVttToMp4Handler::SendEmptySample(uint64_t start_time, Status WebVttToMp4Handler::SendEmptySample(uint64_t start_time,
uint64_t end_time) { uint64_t end_time) {
// TODO(vaage): Dispatch an empty vtt sample. DCHECK_GT(end_time, start_time);
return Status::OK;
box_writer_.Clear();
mp4::VTTEmptyCueBox box;
box.Write(&box_writer_);
std::shared_ptr<MediaSample> sample =
MediaSample::CopyFrom(box_writer_.Buffer(), box_writer_.Size(), true);
sample->set_pts(start_time);
sample->set_dts(start_time);
sample->set_duration(end_time - start_time);
return DispatchMediaSample(kTrackId, std::move(sample));
} }
uint64_t WebVttToMp4Handler::NextActionId() { uint64_t WebVttToMp4Handler::NextActionId() {

View File

@ -63,32 +63,104 @@ class WebVttToMp4HandlerTest : public MediaHandlerTestBase {
std::shared_ptr<TestableWebVttToMp4Handler> mp4_handler_; std::shared_ptr<TestableWebVttToMp4Handler> mp4_handler_;
}; };
// Verify the cues are grouped correctly when the cues do not overlap at all. // Verify that when the stream starts at a non-zero value, the gap at the
// // start will be filled.
// [----A---] [---B---] // | [----A----]
TEST_F(WebVttToMp4HandlerTest, NoOverlap) { TEST_F(WebVttToMp4HandlerTest, NonZeroStartTime) {
const int64_t kStart[] = {0, 1100}; const int64_t kGapStart = 0;
const int64_t kEnd[] = {1000, 2100}; const int64_t kGapEnd = 100;
const int64_t kGapDuration = kGapEnd - kGapStart;
const char* kSampleId = kId[0];
const char* kSamplePayload = kPayload[0];
const int64_t kSampleStart = kGapEnd;
const int64_t kSampleDuration = 500;
const int64_t kSampleEnd = kSampleStart + kSampleDuration;
{ {
testing::InSequence s; testing::InSequence s;
for (size_t i = kA; i <= kB; i++) { // Empty Cue to fill gap
EXPECT_CALL(*mp4_handler_, OnWriteCue(kId[i], kNoSettings, kPayload[i]));
EXPECT_CALL(*Output(kOutputIndex), EXPECT_CALL(*Output(kOutputIndex),
OnProcess(IsMediaSample(kStreamIndex, kStart[i], OnProcess(IsMediaSample(kStreamIndex, kGapStart, kGapDuration,
kEnd[i] - kStart[i], !kEncrypted))); !kEncrypted)));
}
// Sample
EXPECT_CALL(*mp4_handler_,
OnWriteCue(kSampleId, kNoSettings, kSamplePayload));
EXPECT_CALL(*Output(kOutputIndex),
OnProcess(IsMediaSample(kStreamIndex, kSampleStart,
kSampleDuration, !kEncrypted)));
EXPECT_CALL(*Output(kOutputIndex), OnFlush(kStreamIndex)); EXPECT_CALL(*Output(kOutputIndex), OnFlush(kStreamIndex));
} }
for (size_t i = kA; i <= kB; i++) {
ASSERT_OK(Input(kInputIndex) ASSERT_OK(Input(kInputIndex)
->Dispatch(StreamData::FromTextSample( ->Dispatch(StreamData::FromTextSample(
kStreamIndex, kStreamIndex, GetTextSample(kSampleId, kSampleStart,
GetTextSample(kId[i], kStart[i], kEnd[i], kPayload[i])))); kSampleEnd, kSamplePayload))));
ASSERT_OK(Input(kInputIndex)->FlushAllDownstreams());
} }
// Verify the cues are grouped correctly when the cues do not overlap at all.
// An empty cue should be inserted between the two as there is a gap.
//
// [----A---] [---B---]
TEST_F(WebVttToMp4HandlerTest, NoOverlap) {
const int64_t kDuration = 1000;
const char* kSample1Id = kId[0];
const char* kSample1Payload = kPayload[0];
const int64_t kSample1Start = 0;
const int64_t kSample1End = kSample1Start + kDuration;
// Make sample 2 be just a little after sample 1.
const char* kSample2Id = kId[1];
const char* kSample2Payload = kPayload[1];
const int64_t kSample2Start = kSample1End + 100;
const int64_t kSample2End = kSample2Start + kDuration;
const int64_t kGapStart = kSample1End;
const int64_t kGapDuration = kSample2Start - kSample1End;
{
testing::InSequence s;
// Sample 1
EXPECT_CALL(*mp4_handler_,
OnWriteCue(kSample1Id, kNoSettings, kSample1Payload));
EXPECT_CALL(*Output(kOutputIndex),
OnProcess(IsMediaSample(kStreamIndex, kSample1Start, kDuration,
!kEncrypted)));
// Empty Cue to fill gap
EXPECT_CALL(*Output(kOutputIndex),
OnProcess(IsMediaSample(kStreamIndex, kGapStart, kGapDuration,
!kEncrypted)));
// Sample 2
EXPECT_CALL(*mp4_handler_,
OnWriteCue(kSample2Id, kNoSettings, kSample2Payload));
EXPECT_CALL(*Output(kOutputIndex),
OnProcess(IsMediaSample(kStreamIndex, kSample2Start, kDuration,
!kEncrypted)));
EXPECT_CALL(*Output(kOutputIndex), OnFlush(kStreamIndex));
}
ASSERT_OK(
Input(kInputIndex)
->Dispatch(StreamData::FromTextSample(
kStreamIndex, GetTextSample(kSample1Id, kSample1Start,
kSample1End, kSample1Payload))));
ASSERT_OK(
Input(kInputIndex)
->Dispatch(StreamData::FromTextSample(
kStreamIndex, GetTextSample(kSample2Id, kSample2Start,
kSample2End, kSample2Payload))));
ASSERT_OK(Input(kInputIndex)->FlushAllDownstreams()); ASSERT_OK(Input(kInputIndex)->FlushAllDownstreams());
} }