Commit 950c3b3d authored by vrk@google.com's avatar vrk@google.com

Chrome-side implementation of media source timestamp offset

Adds functionality to signal an offset to be applied to the
buffers in ChunkDemuxer. Is not triggerable from Chrome yet.

BUG=139044
TEST=media_unittests

Review URL: https://chromiumcodereview.appspot.com/10803019

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@148810 0039d316-1c4b-4281-b951-d872f2087c98
parent 3b693bdd
......@@ -74,7 +74,8 @@ class MEDIA_EXPORT StreamParser {
const NewBuffersCB& audio_cb,
const NewBuffersCB& video_cb,
const NeedKeyCB& need_key_cb,
const NewMediaSegmentCB& new_segment_cb) = 0;
const NewMediaSegmentCB& new_segment_cb,
const base::Closure& end_of_segment_cb) = 0;
// Called when a seek occurs. This flushes the current parser state
// and puts the parser in a state where it can receive data for the new seek
......
......@@ -111,7 +111,7 @@ static bool IsSupported(const std::string& type,
*has_audio = false;
*has_video = false;
// Search for the SupportedTypeInfo for |type|
// Search for the SupportedTypeInfo for |type|.
for (size_t i = 0; i < arraysize(kSupportedTypeInfo); ++i) {
const SupportedTypeInfo& type_info = kSupportedTypeInfo[i];
if (type == type_info.type) {
......@@ -637,19 +637,24 @@ ChunkDemuxer::Status ChunkDemuxer::AddId(const std::string& id,
audio_cb,
video_cb,
base::Bind(&ChunkDemuxer::OnNeedKey, base::Unretained(this)),
base::Bind(&ChunkDemuxer::OnNewMediaSegment, base::Unretained(this), id));
base::Bind(&ChunkDemuxer::OnNewMediaSegment, base::Unretained(this), id),
base::Bind(&ChunkDemuxer::OnEndOfMediaSegment,
base::Unretained(this), id));
stream_parser_map_[id] = stream_parser.release();
SourceInfo info = { base::TimeDelta(), true };
source_info_map_[id] = info;
return kOk;
}
void ChunkDemuxer::RemoveId(const std::string& id) {
CHECK_GT(stream_parser_map_.count(id), 0u);
CHECK(IsValidId(id));
base::AutoLock auto_lock(lock_);
delete stream_parser_map_[id];
stream_parser_map_.erase(id);
source_info_map_.erase(id);
if (source_id_audio_ == id && audio_)
audio_->Shutdown();
......@@ -660,7 +665,7 @@ void ChunkDemuxer::RemoveId(const std::string& id) {
Ranges<TimeDelta> ChunkDemuxer::GetBufferedRanges(const std::string& id) const {
DCHECK(!id.empty());
DCHECK_GT(stream_parser_map_.count(id), 0u);
DCHECK(IsValidId(id));
DCHECK(id == source_id_audio_ || id == source_id_video_);
base::AutoLock auto_lock(lock_);
......@@ -730,7 +735,7 @@ bool ChunkDemuxer::AppendData(const std::string& id,
switch (state_) {
case INITIALIZING:
case WAITING_FOR_START_TIME:
DCHECK_GT(stream_parser_map_.count(id), 0u);
DCHECK(IsValidId(id));
if (!stream_parser_map_[id]->Parse(data, length)) {
ReportError_Locked(DEMUXER_ERROR_COULD_NOT_OPEN);
return true;
......@@ -738,7 +743,7 @@ bool ChunkDemuxer::AppendData(const std::string& id,
break;
case INITIALIZED: {
DCHECK_GT(stream_parser_map_.count(id), 0u);
DCHECK(IsValidId(id));
if (!stream_parser_map_[id]->Parse(data, length)) {
ReportError_Locked(PIPELINE_ERROR_DECODE);
return true;
......@@ -780,11 +785,24 @@ bool ChunkDemuxer::AppendData(const std::string& id,
void ChunkDemuxer::Abort(const std::string& id) {
DVLOG(1) << "Abort(" << id << ")";
DCHECK(!id.empty());
DCHECK_GT(stream_parser_map_.count(id), 0u);
CHECK(IsValidId(id));
stream_parser_map_[id]->Flush();
}
bool ChunkDemuxer::SetTimestampOffset(const std::string& id, double offset) {
DVLOG(1) << "SetTimestampOffset(" << id << ", " << offset << ")";
CHECK(IsValidId(id));
if (!source_info_map_[id].can_update_offset)
return false;
TimeDelta time_offset = TimeDelta::FromMicroseconds(
offset * base::Time::kMicrosecondsPerSecond);
source_info_map_[id].timestamp_offset = time_offset;
return true;
}
bool ChunkDemuxer::EndOfStream(PipelineStatus status) {
DVLOG(1) << "EndOfStream(" << status << ")";
base::AutoLock auto_lock(lock_);
......@@ -983,6 +1001,10 @@ bool ChunkDemuxer::OnAudioBuffers(const StreamParser::BufferQueue& buffers) {
if (!audio_)
return false;
CHECK(IsValidId(source_id_audio_));
AdjustBufferTimestamps(
buffers, source_info_map_[source_id_audio_].timestamp_offset);
return audio_->Append(buffers);
}
......@@ -993,6 +1015,10 @@ bool ChunkDemuxer::OnVideoBuffers(const StreamParser::BufferQueue& buffers) {
if (!video_)
return false;
CHECK(IsValidId(source_id_video_));
AdjustBufferTimestamps(
buffers, source_info_map_[source_id_video_].timestamp_offset);
return video_->Append(buffers);
}
......@@ -1003,11 +1029,16 @@ bool ChunkDemuxer::OnNeedKey(scoped_array<uint8> init_data,
}
void ChunkDemuxer::OnNewMediaSegment(const std::string& source_id,
TimeDelta start_timestamp) {
TimeDelta timestamp) {
DVLOG(2) << "OnNewMediaSegment(" << source_id << ", "
<< start_timestamp.InSecondsF() << ")";
<< timestamp.InSecondsF() << ")";
lock_.AssertAcquired();
CHECK(IsValidId(source_id));
source_info_map_[source_id].can_update_offset = false;
base::TimeDelta start_timestamp =
timestamp + source_info_map_[source_id].timestamp_offset;
if (start_time_ == kNoTimestamp()) {
DCHECK(state_ == INITIALIZING || state_ == WAITING_FOR_START_TIME);
// Use the first reported media segment start time as the |start_time_|
......@@ -1037,4 +1068,29 @@ void ChunkDemuxer::OnNewMediaSegment(const std::string& source_id,
base::ResetAndReturn(&init_cb_).Run(PIPELINE_OK);
}
void ChunkDemuxer::OnEndOfMediaSegment(const std::string& source_id) {
DVLOG(2) << "OnEndOfMediaSegment(" << source_id << ")";
CHECK(IsValidId(source_id));
source_info_map_[source_id].can_update_offset = true;
}
void ChunkDemuxer::AdjustBufferTimestamps(
const StreamParser::BufferQueue& buffers,
base::TimeDelta timestamp_offset) {
if (timestamp_offset == base::TimeDelta())
return;
for (StreamParser::BufferQueue::const_iterator itr = buffers.begin();
itr != buffers.end(); ++itr) {
(*itr)->SetDecodeTimestamp(
(*itr)->GetDecodeTimestamp() + timestamp_offset);
(*itr)->SetTimestamp((*itr)->GetTimestamp() + timestamp_offset);
}
}
bool ChunkDemuxer::IsValidId(const std::string& source_id) const {
return source_info_map_.count(source_id) > 0u &&
stream_parser_map_.count(source_id) > 0u;
}
} // namespace media
......@@ -74,6 +74,12 @@ class MEDIA_EXPORT ChunkDemuxer : public Demuxer {
// it can accept a new segment.
void Abort(const std::string& id);
// Sets a time |offset| in seconds to be applied to subsequent buffers
// appended to the source buffer assicated with |id|. Returns true if the
// offset is set properly, false if the offset cannot be applied because we're
// in the middle of parsing a media segment.
bool SetTimestampOffset(const std::string& id, double offset);
// Signals an EndOfStream request.
// Returns false if called in an unexpected state or if there is a gap between
// the current position and the end of the buffered data.
......@@ -117,11 +123,19 @@ class MEDIA_EXPORT ChunkDemuxer : public Demuxer {
bool OnNeedKey(scoped_array<uint8> init_data, int init_data_size);
void OnNewMediaSegment(const std::string& source_id,
base::TimeDelta start_timestamp);
void OnEndOfMediaSegment(const std::string& source_id);
// Computes the intersection between the video & audio
// buffered ranges.
Ranges<base::TimeDelta> ComputeIntersection() const;
// Applies |time_offset| to the timestamps of |buffers|.
void AdjustBufferTimestamps(const StreamParser::BufferQueue& buffers,
base::TimeDelta timestamp_offset);
// Returns true if |source_id| is valid, false otherwise.
bool IsValidId(const std::string& source_id) const;
mutable base::Lock lock_;
State state_;
......@@ -138,6 +152,14 @@ class MEDIA_EXPORT ChunkDemuxer : public Demuxer {
typedef std::map<std::string, StreamParser*> StreamParserMap;
StreamParserMap stream_parser_map_;
// Contains state belonging to a source id.
struct SourceInfo {
base::TimeDelta timestamp_offset;
bool can_update_offset;
};
typedef std::map<std::string, SourceInfo> SourceInfoMap;
SourceInfoMap source_info_map_;
// Used to ensure that (1) config data matches the type and codec provided in
// AddId(), (2) only 1 audio and 1 video sources are added, and (3) ids may be
// removed with RemoveID() but can not be re-added (yet).
......
......@@ -2064,4 +2064,70 @@ TEST_F(ChunkDemuxerTest, TestConfigChange_Seek) {
ASSERT_TRUE(video_config_1.Matches(stream->video_decoder_config()));
}
TEST_F(ChunkDemuxerTest, TestTimestampPositiveOffset) {
ASSERT_TRUE(InitDemuxer(true, true, false));
ASSERT_TRUE(demuxer_->SetTimestampOffset(kSourceId, 30));
scoped_ptr<Cluster> cluster(GenerateCluster(0, 2));
ASSERT_TRUE(AppendData(cluster->data(), cluster->size()));
scoped_refptr<DemuxerStream> audio =
demuxer_->GetStream(DemuxerStream::AUDIO);
scoped_refptr<DemuxerStream> video =
demuxer_->GetStream(DemuxerStream::VIDEO);
GenerateExpectedReads(30000, 2, audio, video);
}
TEST_F(ChunkDemuxerTest, TestTimestampNegativeOffset) {
ASSERT_TRUE(InitDemuxer(true, true, false));
ASSERT_TRUE(demuxer_->SetTimestampOffset(kSourceId, -1));
scoped_ptr<Cluster> cluster = GenerateCluster(1000, 2);
ASSERT_TRUE(AppendData(cluster->data(), cluster->size()));
scoped_refptr<DemuxerStream> audio =
demuxer_->GetStream(DemuxerStream::AUDIO);
scoped_refptr<DemuxerStream> video =
demuxer_->GetStream(DemuxerStream::VIDEO);
GenerateExpectedReads(0, 2, audio, video);
}
TEST_F(ChunkDemuxerTest, TestTimestampOffsetSeparateStreams) {
std::string audio_id = "audio1";
std::string video_id = "video1";
ASSERT_TRUE(InitDemuxerAudioAndVideoSources(audio_id, video_id));
scoped_refptr<DemuxerStream> audio =
demuxer_->GetStream(DemuxerStream::AUDIO);
scoped_refptr<DemuxerStream> video =
demuxer_->GetStream(DemuxerStream::VIDEO);
scoped_ptr<Cluster> cluster_a(
GenerateSingleStreamCluster(
2500, 2500 + kAudioBlockDuration * 4, kAudioTrackNum,
kAudioBlockDuration));
scoped_ptr<Cluster> cluster_v(
GenerateSingleStreamCluster(
0, kVideoBlockDuration * 4, kVideoTrackNum, kVideoBlockDuration));
ASSERT_TRUE(demuxer_->SetTimestampOffset(audio_id, -2.5));
ASSERT_TRUE(AppendData(audio_id, cluster_a->data(), cluster_a->size()));
GenerateSingleStreamExpectedReads(0, 4, audio, kAudioBlockDuration);
ASSERT_TRUE(demuxer_->SetTimestampOffset(video_id, 27.3));
ASSERT_TRUE(AppendData(video_id, cluster_v->data(), cluster_v->size()));
GenerateSingleStreamExpectedReads(27300, 4, video, kVideoBlockDuration);
}
TEST_F(ChunkDemuxerTest, TestTimestampOffsetMidParse) {
ASSERT_TRUE(InitDemuxer(true, true, false));
scoped_ptr<Cluster> cluster = GenerateCluster(0, 2);
// Append only part of the cluster data.
ASSERT_TRUE(AppendData(cluster->data(), cluster->size() - 13));
// Setting a timestamp should fail because we're in the middle of a cluster.
ASSERT_FALSE(demuxer_->SetTimestampOffset(kSourceId, 25));
}
} // namespace media
......@@ -982,7 +982,7 @@ SourceBufferRange* SourceBufferRange::SplitRange(base::TimeDelta timestamp) {
SourceBufferRange::KeyframeMap::iterator
SourceBufferRange::GetFirstKeyframeAt(base::TimeDelta timestamp,
bool skip_given_timestamp) {
bool skip_given_timestamp) {
KeyframeMap::iterator result = keyframe_map_.lower_bound(timestamp);
// lower_bound() returns the first element >= |timestamp|, so if we don't want
// to include keyframes == |timestamp|, we have to increment the iterator
......
......@@ -37,13 +37,15 @@ void MP4StreamParser::Init(const InitCB& init_cb,
const NewBuffersCB& audio_cb,
const NewBuffersCB& video_cb,
const NeedKeyCB& need_key_cb,
const NewMediaSegmentCB& new_segment_cb) {
const NewMediaSegmentCB& new_segment_cb,
const base::Closure& end_of_segment_cb) {
DCHECK_EQ(state_, kWaitingForInit);
DCHECK(init_cb_.is_null());
DCHECK(!init_cb.is_null());
DCHECK(!config_cb.is_null());
DCHECK(!audio_cb.is_null() || !video_cb.is_null());
DCHECK(!need_key_cb.is_null());
DCHECK(!end_of_segment_cb.is_null());
ChangeState(kParsingBoxes);
init_cb_ = init_cb;
......@@ -52,6 +54,7 @@ void MP4StreamParser::Init(const InitCB& init_cb,
video_cb_ = video_cb;
need_key_cb_ = need_key_cb;
new_segment_cb_ = new_segment_cb;
end_of_segment_cb_ = end_of_segment_cb;
}
void MP4StreamParser::Flush() {
......@@ -316,8 +319,11 @@ bool MP4StreamParser::EnqueueSample(BufferQueue* audio_buffers,
// Flush any buffers we've gotten in this chunk so that buffers don't
// cross NewSegment() calls
*err = !SendAndFlushSamples(audio_buffers, video_buffers);
if (*err) return false;
if (*err)
return false;
ChangeState(kParsingBoxes);
end_of_segment_cb_.Run();
return true;
}
......@@ -337,7 +343,8 @@ bool MP4StreamParser::EnqueueSample(BufferQueue* audio_buffers,
bool video = has_video_ && video_track_id_ == runs_->track_id();
// Skip this entire track if it's not one we're interested in
if (!audio && !video) runs_->AdvanceRun();
if (!audio && !video)
runs_->AdvanceRun();
// Attempt to cache the auxiliary information first. Aux info is usually
// placed in a contiguous block before the sample data, rather than being
......
......@@ -31,7 +31,8 @@ class MEDIA_EXPORT MP4StreamParser : public StreamParser {
const NewBuffersCB& audio_cb,
const NewBuffersCB& video_cb,
const NeedKeyCB& need_key_cb,
const NewMediaSegmentCB& new_segment_cb) OVERRIDE;
const NewMediaSegmentCB& new_segment_cb,
const base::Closure& end_of_segment_cb) OVERRIDE;
virtual void Flush() OVERRIDE;
virtual bool Parse(const uint8* buf, int size) OVERRIDE;
......@@ -70,6 +71,7 @@ class MEDIA_EXPORT MP4StreamParser : public StreamParser {
NewBuffersCB video_cb_;
NeedKeyCB need_key_cb_;
NewMediaSegmentCB new_segment_cb_;
base::Closure end_of_segment_cb_;
OffsetByteQueue queue_;
......
......@@ -93,6 +93,10 @@ class MP4StreamParserTest : public testing::Test {
segment_start_ = start_dts;
}
void EndOfSegmentF() {
DVLOG(1) << "EndOfSegmentF()";
}
void InitializeParser() {
parser_->Init(
base::Bind(&MP4StreamParserTest::InitF, base::Unretained(this)),
......@@ -100,7 +104,9 @@ class MP4StreamParserTest : public testing::Test {
base::Bind(&MP4StreamParserTest::NewBuffersF, base::Unretained(this)),
base::Bind(&MP4StreamParserTest::NewBuffersF, base::Unretained(this)),
base::Bind(&MP4StreamParserTest::KeyNeededF, base::Unretained(this)),
base::Bind(&MP4StreamParserTest::NewSegmentF, base::Unretained(this)));
base::Bind(&MP4StreamParserTest::NewSegmentF, base::Unretained(this)),
base::Bind(&MP4StreamParserTest::EndOfSegmentF,
base::Unretained(this)));
}
bool ParseMP4File(const std::string& filename, int append_bytes) {
......
......@@ -43,6 +43,7 @@ WebMClusterParser::WebMClusterParser(int64 timecode_scale,
block_duration_(-1),
cluster_timecode_(-1),
cluster_start_time_(kNoTimestamp()),
cluster_ended_(false),
audio_(audio_track_num),
video_(video_track_num) {
CHECK_GE(video_encryption_key_id_size, 0);
......@@ -59,6 +60,7 @@ void WebMClusterParser::Reset() {
last_block_timecode_ = -1;
cluster_timecode_ = -1;
cluster_start_time_ = kNoTimestamp();
cluster_ended_ = false;
parser_.Reset();
audio_.Reset();
video_.Reset();
......@@ -70,10 +72,13 @@ int WebMClusterParser::Parse(const uint8* buf, int size) {
int result = parser_.Parse(buf, size);
if (result <= 0)
if (result <= 0) {
cluster_ended_ = false;
return result;
}
if (parser_.IsParsingComplete()) {
cluster_ended_ = parser_.IsParsingComplete();
if (cluster_ended_) {
// If there were no buffers in this cluster, set the cluster start time to
// be the |cluster_timecode_|.
if (cluster_start_time_ == kNoTimestamp()) {
......
......@@ -44,6 +44,9 @@ class MEDIA_EXPORT WebMClusterParser : public WebMParserClient {
const BufferQueue& audio_buffers() const { return audio_.buffers(); }
const BufferQueue& video_buffers() const { return video_.buffers(); }
// Returns true if the last Parse() call stopped at the end of a cluster.
bool cluster_ended() const { return cluster_ended_; }
private:
// Helper class that manages per-track state.
class Track {
......@@ -88,6 +91,7 @@ class MEDIA_EXPORT WebMClusterParser : public WebMParserClient {
int64 cluster_timecode_;
base::TimeDelta cluster_start_time_;
bool cluster_ended_;
Track audio_;
Track video_;
......
......@@ -192,7 +192,8 @@ void WebMStreamParser::Init(const InitCB& init_cb,
const NewBuffersCB& audio_cb,
const NewBuffersCB& video_cb,
const NeedKeyCB& need_key_cb,
const NewMediaSegmentCB& new_segment_cb) {
const NewMediaSegmentCB& new_segment_cb,
const base::Closure& end_of_segment_cb) {
DCHECK_EQ(state_, kWaitingForInit);
DCHECK(init_cb_.is_null());
DCHECK(!init_cb.is_null());
......@@ -200,6 +201,7 @@ void WebMStreamParser::Init(const InitCB& init_cb,
DCHECK(!audio_cb.is_null() || !video_cb.is_null());
DCHECK(!need_key_cb.is_null());
DCHECK(!new_segment_cb.is_null());
DCHECK(!end_of_segment_cb.is_null());
ChangeState(kParsingHeaders);
init_cb_ = init_cb;
......@@ -208,6 +210,7 @@ void WebMStreamParser::Init(const InitCB& init_cb,
video_cb_ = video_cb;
need_key_cb_ = need_key_cb;
new_segment_cb_ = new_segment_cb;
end_of_segment_cb_ = end_of_segment_cb;
}
void WebMStreamParser::Flush() {
......@@ -416,6 +419,7 @@ int WebMStreamParser::ParseCluster(const uint8* data, int size) {
const BufferQueue& audio_buffers = cluster_parser_->audio_buffers();
const BufferQueue& video_buffers = cluster_parser_->video_buffers();
base::TimeDelta cluster_start_time = cluster_parser_->cluster_start_time();
bool cluster_ended = cluster_parser_->cluster_ended();
if (waiting_for_buffers_ && cluster_start_time != kNoTimestamp()) {
new_segment_cb_.Run(cluster_start_time);
......@@ -428,6 +432,9 @@ int WebMStreamParser::ParseCluster(const uint8* data, int size) {
if (!video_buffers.empty() && !video_cb_.Run(video_buffers))
return -1;
if (cluster_ended)
end_of_segment_cb_.Run();
return bytes_parsed;
}
......
......@@ -26,7 +26,8 @@ class WebMStreamParser : public StreamParser {
const NewBuffersCB& audio_cb,
const NewBuffersCB& video_cb,
const NeedKeyCB& need_key_cb,
const NewMediaSegmentCB& new_segment_cb) OVERRIDE;
const NewMediaSegmentCB& new_segment_cb,
const base::Closure& end_of_segment_cb) OVERRIDE;
virtual void Flush() OVERRIDE;
virtual bool Parse(const uint8* buf, int size) OVERRIDE;
......@@ -66,6 +67,7 @@ class WebMStreamParser : public StreamParser {
NewBuffersCB video_cb_;
NeedKeyCB need_key_cb_;
NewMediaSegmentCB new_segment_cb_;
base::Closure end_of_segment_cb_;
// True if a new cluster id has been seen, but no audio or video buffers have
// been parsed yet.
......
......@@ -653,6 +653,11 @@ WebKit::WebMediaPlayer::AddIdStatus WebMediaPlayerImpl::sourceAddId(
new_codecs));
}
bool WebMediaPlayerImpl::sourceTimestampOffset(
const WebKit::WebString& id, double offset) {
return proxy_->DemuxerSetTimestampOffset(id.utf8().data(), offset);
}
bool WebMediaPlayerImpl::sourceRemoveId(const WebKit::WebString& id) {
DCHECK(!id.isEmpty());
proxy_->DemuxerRemoveId(id.utf8().data());
......
......@@ -208,6 +208,8 @@ class WebMediaPlayerImpl
unsigned length);
virtual bool sourceAbort(const WebKit::WebString& id);
virtual void sourceEndOfStream(EndOfStreamStatus status);
virtual bool sourceTimestampOffset(
const WebKit::WebString& id, double offset);
virtual MediaKeyException generateKeyRequest(
const WebKit::WebString& key_system,
......
......@@ -196,6 +196,11 @@ media::ChunkDemuxer::Status WebMediaPlayerProxy::DemuxerAddId(
return chunk_demuxer_->AddId(id, type, codecs);
}
bool WebMediaPlayerProxy::DemuxerSetTimestampOffset(
const std::string& id, double offset) {
return chunk_demuxer_->SetTimestampOffset(id, offset);
}
void WebMediaPlayerProxy::DemuxerRemoveId(const std::string& id) {
chunk_demuxer_->RemoveId(id);
}
......
......@@ -105,6 +105,7 @@ class WebMediaPlayerProxy
void DemuxerAbort(const std::string& id);
void DemuxerEndOfStream(media::PipelineStatus status);
void DemuxerShutdown();
bool DemuxerSetTimestampOffset(const std::string& id, double offset);
// DecryptorClient implementation.
virtual void KeyAdded(const std::string& key_system,
......
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