Commit d749be45 authored by acolwell@chromium.org's avatar acolwell@chromium.org

Fix muxed MP4 parsing so it won't crash on partial media segment appends.

Partial appends could cause the MP4StreamParser to emit buffers in
non-monotonically increasing timestamp order because of how we process
trun boxes. This patch makes sure that processing of trun boxes is
deferred until we have all the sample & aux_info data before emitting
samples. This prevents the parser from emitting samples in such a way
that will break downstream code. This is a minimal impact fix to
avoid bad behavior at the expense of buffering more data.

BUG=None
TEST=All MP4StreamParserTests that do partial appends now verify the fix.

Review URL: https://codereview.chromium.org/348623003

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@278650 0039d316-1c4b-4281-b951-d872f2087c98
parent fc43f5a2
...@@ -92,18 +92,33 @@ bool MP4StreamParser::Parse(const uint8* buf, int size) { ...@@ -92,18 +92,33 @@ bool MP4StreamParser::Parse(const uint8* buf, int size) {
BufferQueue audio_buffers; BufferQueue audio_buffers;
BufferQueue video_buffers; BufferQueue video_buffers;
bool result, err = false; bool result = false;
bool err = false;
do { do {
if (state_ == kParsingBoxes) { switch (state_) {
result = ParseBox(&err); case kWaitingForInit:
} else { case kError:
DCHECK_EQ(kEmittingSamples, state_); NOTREACHED();
result = EnqueueSample(&audio_buffers, &video_buffers, &err); return false;
if (result) {
int64 max_clear = runs_->GetMaxClearOffset() + moof_head_; case kParsingBoxes:
err = !ReadAndDiscardMDATsUntil(max_clear); result = ParseBox(&err);
} break;
case kWaitingForSampleData:
result = HaveEnoughDataToEnqueueSamples();
if (result)
ChangeState(kEmittingSamples);
break;
case kEmittingSamples:
result = EnqueueSample(&audio_buffers, &video_buffers, &err);
if (result) {
int64 max_clear = runs_->GetMaxClearOffset() + moof_head_;
err = !ReadAndDiscardMDATsUntil(max_clear);
}
break;
} }
} while (result && !err); } while (result && !err);
...@@ -312,9 +327,10 @@ bool MP4StreamParser::ParseMoof(BoxReader* reader) { ...@@ -312,9 +327,10 @@ bool MP4StreamParser::ParseMoof(BoxReader* reader) {
if (!runs_) if (!runs_)
runs_.reset(new TrackRunIterator(moov_.get(), log_cb_)); runs_.reset(new TrackRunIterator(moov_.get(), log_cb_));
RCHECK(runs_->Init(moof)); RCHECK(runs_->Init(moof));
RCHECK(ComputeHighestEndOffset(moof));
EmitNeedKeyIfNecessary(moof.pssh); EmitNeedKeyIfNecessary(moof.pssh);
new_segment_cb_.Run(); new_segment_cb_.Run();
ChangeState(kEmittingSamples); ChangeState(kWaitingForSampleData);
return true; return true;
} }
...@@ -393,6 +409,8 @@ bool MP4StreamParser::PrepareAACBuffer( ...@@ -393,6 +409,8 @@ bool MP4StreamParser::PrepareAACBuffer(
bool MP4StreamParser::EnqueueSample(BufferQueue* audio_buffers, bool MP4StreamParser::EnqueueSample(BufferQueue* audio_buffers,
BufferQueue* video_buffers, BufferQueue* video_buffers,
bool* err) { bool* err) {
DCHECK_EQ(state_, kEmittingSamples);
if (!runs_->IsRunValid()) { if (!runs_->IsRunValid()) {
// Flush any buffers we've gotten in this chunk so that buffers don't // Flush any buffers we've gotten in this chunk so that buffers don't
// cross NewSegment() calls // cross NewSegment() calls
...@@ -400,7 +418,7 @@ bool MP4StreamParser::EnqueueSample(BufferQueue* audio_buffers, ...@@ -400,7 +418,7 @@ bool MP4StreamParser::EnqueueSample(BufferQueue* audio_buffers,
if (*err) if (*err)
return false; return false;
// Remain in kEnqueueingSamples state, discarding data, until the end of // Remain in kEmittingSamples state, discarding data, until the end of
// the current 'mdat' box has been appended to the queue. // the current 'mdat' box has been appended to the queue.
if (!queue_.Trim(mdat_tail_)) if (!queue_.Trim(mdat_tail_))
return false; return false;
...@@ -426,8 +444,10 @@ bool MP4StreamParser::EnqueueSample(BufferQueue* audio_buffers, ...@@ -426,8 +444,10 @@ bool MP4StreamParser::EnqueueSample(BufferQueue* audio_buffers,
bool video = has_video_ && video_track_id_ == runs_->track_id(); bool video = has_video_ && video_track_id_ == runs_->track_id();
// Skip this entire track if it's not one we're interested in // Skip this entire track if it's not one we're interested in
if (!audio && !video) if (!audio && !video) {
runs_->AdvanceRun(); runs_->AdvanceRun();
return true;
}
// Attempt to cache the auxiliary information first. Aux info is usually // Attempt to cache the auxiliary information first. Aux info is usually
// placed in a contiguous block before the sample data, rather than being // placed in a contiguous block before the sample data, rather than being
...@@ -577,5 +597,40 @@ void MP4StreamParser::ChangeState(State new_state) { ...@@ -577,5 +597,40 @@ void MP4StreamParser::ChangeState(State new_state) {
state_ = new_state; state_ = new_state;
} }
bool MP4StreamParser::HaveEnoughDataToEnqueueSamples() {
DCHECK_EQ(state_, kWaitingForSampleData);
// For muxed content, make sure we have data up to |highest_end_offset_|
// so we can ensure proper enqueuing behavior. Otherwise assume we have enough
// data and allow per sample offset checks to meter sample enqueuing.
// TODO(acolwell): Fix trun box handling so we don't have to special case
// muxed content.
return !(has_audio_ && has_video_ &&
queue_.tail() < highest_end_offset_ + moof_head_);
}
bool MP4StreamParser::ComputeHighestEndOffset(const MovieFragment& moof) {
highest_end_offset_ = 0;
TrackRunIterator runs(moov_.get(), log_cb_);
RCHECK(runs.Init(moof));
while (runs.IsRunValid()) {
int64 aux_info_end_offset = runs.aux_info_offset() + runs.aux_info_size();
if (aux_info_end_offset > highest_end_offset_)
highest_end_offset_ = aux_info_end_offset;
while (runs.IsSampleValid()) {
int64 sample_end_offset = runs.sample_offset() + runs.sample_size();
if (sample_end_offset > highest_end_offset_)
highest_end_offset_ = sample_end_offset;
runs.AdvanceSample();
}
runs.AdvanceRun();
}
return true;
}
} // namespace mp4 } // namespace mp4
} // namespace media } // namespace media
...@@ -42,6 +42,7 @@ class MEDIA_EXPORT MP4StreamParser : public StreamParser { ...@@ -42,6 +42,7 @@ class MEDIA_EXPORT MP4StreamParser : public StreamParser {
enum State { enum State {
kWaitingForInit, kWaitingForInit,
kParsingBoxes, kParsingBoxes,
kWaitingForSampleData,
kEmittingSamples, kEmittingSamples,
kError kError
}; };
...@@ -78,6 +79,15 @@ class MEDIA_EXPORT MP4StreamParser : public StreamParser { ...@@ -78,6 +79,15 @@ class MEDIA_EXPORT MP4StreamParser : public StreamParser {
void Reset(); void Reset();
// Checks to see if we have enough data in |queue_| to transition to
// kEmittingSamples and start enqueuing samples.
bool HaveEnoughDataToEnqueueSamples();
// Sets |highest_end_offset_| based on the data in |moov_|
// and |moof|. Returns true if |highest_end_offset_| was successfully
// computed.
bool ComputeHighestEndOffset(const MovieFragment& moof);
State state_; State state_;
InitCB init_cb_; InitCB init_cb_;
NewConfigCB config_cb_; NewConfigCB config_cb_;
...@@ -99,6 +109,11 @@ class MEDIA_EXPORT MP4StreamParser : public StreamParser { ...@@ -99,6 +109,11 @@ class MEDIA_EXPORT MP4StreamParser : public StreamParser {
// Valid iff it is greater than the head of the queue. // Valid iff it is greater than the head of the queue.
int64 mdat_tail_; int64 mdat_tail_;
// The highest end offset in the current moof. This offset is
// relative to |moof_head_|. This value is used to make sure we have collected
// enough bytes to parse all samples and aux_info in the current moof.
int64 highest_end_offset_;
scoped_ptr<mp4::Movie> moov_; scoped_ptr<mp4::Movie> moov_;
scoped_ptr<mp4::TrackRunIterator> runs_; scoped_ptr<mp4::TrackRunIterator> runs_;
......
...@@ -31,7 +31,8 @@ static const char kMp4InitDataType[] = "video/mp4"; ...@@ -31,7 +31,8 @@ static const char kMp4InitDataType[] = "video/mp4";
class MP4StreamParserTest : public testing::Test { class MP4StreamParserTest : public testing::Test {
public: public:
MP4StreamParserTest() MP4StreamParserTest()
: configs_received_(false) { : configs_received_(false),
lower_bound_(base::TimeDelta::Max()) {
std::set<int> audio_object_types; std::set<int> audio_object_types;
audio_object_types.insert(kISO_14496_3); audio_object_types.insert(kISO_14496_3);
parser_.reset(new MP4StreamParser(audio_object_types, false)); parser_.reset(new MP4StreamParser(audio_object_types, false));
...@@ -40,6 +41,7 @@ class MP4StreamParserTest : public testing::Test { ...@@ -40,6 +41,7 @@ class MP4StreamParserTest : public testing::Test {
protected: protected:
scoped_ptr<MP4StreamParser> parser_; scoped_ptr<MP4StreamParser> parser_;
bool configs_received_; bool configs_received_;
base::TimeDelta lower_bound_;
bool AppendData(const uint8* data, size_t length) { bool AppendData(const uint8* data, size_t length) {
return parser_->Parse(data, length); return parser_->Parse(data, length);
...@@ -73,7 +75,6 @@ class MP4StreamParserTest : public testing::Test { ...@@ -73,7 +75,6 @@ class MP4StreamParserTest : public testing::Test {
return true; return true;
} }
void DumpBuffers(const std::string& label, void DumpBuffers(const std::string& label,
const StreamParser::BufferQueue& buffers) { const StreamParser::BufferQueue& buffers) {
DVLOG(2) << "DumpBuffers: " << label << " size " << buffers.size(); DVLOG(2) << "DumpBuffers: " << label << " size " << buffers.size();
...@@ -96,6 +97,24 @@ class MP4StreamParserTest : public testing::Test { ...@@ -96,6 +97,24 @@ class MP4StreamParserTest : public testing::Test {
if (!text_map.empty()) if (!text_map.empty())
return false; return false;
// Find the second highest timestamp so that we know what the
// timestamps on the next set of buffers must be >= than.
base::TimeDelta audio = !audio_buffers.empty() ?
audio_buffers.back()->GetDecodeTimestamp() : kNoTimestamp();
base::TimeDelta video = !video_buffers.empty() ?
video_buffers.back()->GetDecodeTimestamp() : kNoTimestamp();
base::TimeDelta second_highest_timestamp =
(audio == kNoTimestamp() ||
(video != kNoTimestamp() && audio > video)) ? video : audio;
DCHECK(second_highest_timestamp != kNoTimestamp());
if (lower_bound_ != kNoTimestamp() &&
second_highest_timestamp < lower_bound_) {
return false;
}
lower_bound_ = second_highest_timestamp;
return true; return true;
} }
...@@ -108,10 +127,12 @@ class MP4StreamParserTest : public testing::Test { ...@@ -108,10 +127,12 @@ class MP4StreamParserTest : public testing::Test {
void NewSegmentF() { void NewSegmentF() {
DVLOG(1) << "NewSegmentF"; DVLOG(1) << "NewSegmentF";
lower_bound_ = kNoTimestamp();
} }
void EndOfSegmentF() { void EndOfSegmentF() {
DVLOG(1) << "EndOfSegmentF()"; DVLOG(1) << "EndOfSegmentF()";
lower_bound_ = base::TimeDelta::Max();
} }
void InitializeParser() { void InitializeParser() {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment