summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormatthewjheaney@chromium.org <matthewjheaney@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-11-22 01:19:31 +0000
committermatthewjheaney@chromium.org <matthewjheaney@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-11-22 01:19:31 +0000
commit8a5610641873c139760a68ab3bd05e20e62df3ae (patch)
tree793df094179d5c9db661876e62a17cd9ca0e750b
parent49728365a1847baa284259504aaade201bfdccb7 (diff)
downloadchromium_src-8a5610641873c139760a68ab3bd05e20e62df3ae.zip
chromium_src-8a5610641873c139760a68ab3bd05e20e62df3ae.tar.gz
chromium_src-8a5610641873c139760a68ab3bd05e20e62df3ae.tar.bz2
Render inband text tracks in the media pipeline
This change modifies the FFmpeg demuxer to recognize text streams embedded in the source media (Webm). Text decoder and text renderer filters have been added to the pipeline, to process the text frames as they are pulled downstream. BUG=230708 Review URL: https://codereview.chromium.org/23702007 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@236660 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--content/renderer/media/android/media_source_delegate.cc24
-rw-r--r--content/renderer/media/android/media_source_delegate.h3
-rw-r--r--content/renderer/media/texttrack_impl.cc45
-rw-r--r--content/renderer/media/texttrack_impl.h21
-rw-r--r--content/renderer/media/webmediaplayer_impl.cc49
-rw-r--r--content/renderer/media/webmediaplayer_impl.h6
-rw-r--r--media/base/demuxer.h15
-rw-r--r--media/base/demuxer_perftest.cc8
-rw-r--r--media/base/demuxer_stream.h1
-rw-r--r--media/base/fake_text_track_stream.cc83
-rw-r--r--media/base/fake_text_track_stream.h47
-rw-r--r--media/base/filter_collection.cc10
-rw-r--r--media/base/filter_collection.h5
-rw-r--r--media/base/media_log.cc2
-rw-r--r--media/base/media_log_event.h3
-rw-r--r--media/base/mock_demuxer_host.h6
-rw-r--r--media/base/mock_filters.cc4
-rw-r--r--media/base/mock_filters.h19
-rw-r--r--media/base/pipeline.cc80
-rw-r--r--media/base/pipeline.h22
-rw-r--r--media/base/pipeline_unittest.cc108
-rw-r--r--media/base/stream_parser.h15
-rw-r--r--media/base/text_cue.cc23
-rw-r--r--media/base/text_cue.h48
-rw-r--r--media/base/text_renderer.cc369
-rw-r--r--media/base/text_renderer.h145
-rw-r--r--media/base/text_renderer_unittest.cc1382
-rw-r--r--media/base/text_track.h19
-rw-r--r--media/base/text_track_config.cc27
-rw-r--r--media/base/text_track_config.h45
-rw-r--r--media/filters/chunk_demuxer.cc252
-rw-r--r--media/filters/chunk_demuxer.h15
-rw-r--r--media/filters/chunk_demuxer_unittest.cc217
-rw-r--r--media/filters/ffmpeg_demuxer.cc139
-rw-r--r--media/filters/ffmpeg_demuxer.h17
-rw-r--r--media/filters/ffmpeg_demuxer_unittest.cc134
-rw-r--r--media/filters/pipeline_integration_test.cc11
-rw-r--r--media/filters/source_buffer_stream.cc25
-rw-r--r--media/filters/source_buffer_stream.h7
-rw-r--r--media/filters/webvtt_util.h30
-rw-r--r--media/media.gyp10
-rw-r--r--media/mp2t/mp2t_stream_parser.cc10
-rw-r--r--media/mp2t/mp2t_stream_parser.h1
-rw-r--r--media/mp2t/mp2t_stream_parser_unittest.cc22
-rw-r--r--media/mp3/mp3_stream_parser.cc4
-rw-r--r--media/mp3/mp3_stream_parser.h1
-rw-r--r--media/mp3/mp3_stream_parser_unittest.cc20
-rw-r--r--media/mp4/mp4_stream_parser.cc4
-rw-r--r--media/mp4/mp4_stream_parser.h1
-rw-r--r--media/mp4/mp4_stream_parser_unittest.cc21
-rw-r--r--media/webm/webm_cluster_parser.cc29
-rw-r--r--media/webm/webm_cluster_parser_unittest.cc17
-rw-r--r--media/webm/webm_stream_parser.cc43
-rw-r--r--media/webm/webm_stream_parser.h7
-rw-r--r--media/webm/webm_tracks_parser.cc7
-rw-r--r--media/webm/webm_tracks_parser.h10
-rw-r--r--media/webm/webm_tracks_parser_unittest.cc8
57 files changed, 3331 insertions, 365 deletions
diff --git a/content/renderer/media/android/media_source_delegate.cc b/content/renderer/media/android/media_source_delegate.cc
index 7d7e917..57fc015 100644
--- a/content/renderer/media/android/media_source_delegate.cc
+++ b/content/renderer/media/android/media_source_delegate.cc
@@ -40,13 +40,6 @@ const uint8 kVorbisPadding[] = { 0xff, 0xff, 0xff, 0xff };
namespace content {
-static scoped_ptr<media::TextTrack> ReturnNullTextTrack(
- media::TextKind kind,
- const std::string& label,
- const std::string& language) {
- return scoped_ptr<media::TextTrack>();
-}
-
static void LogMediaSourceError(const scoped_refptr<media::MediaLog>& media_log,
const std::string& error) {
media_log->AddEvent(media_log->CreateMediaSourceErrorEvent(error));
@@ -164,7 +157,6 @@ void MediaSourceDelegate::InitializeMediaSource(
&MediaSourceDelegate::OnDemuxerOpened, main_weak_this_)),
media::BindToCurrentLoop(base::Bind(
&MediaSourceDelegate::OnNeedKey, main_weak_this_)),
- base::Bind(&ReturnNullTextTrack),
base::Bind(&LogMediaSourceError, media_log_)));
demuxer_ = chunk_demuxer_.get();
@@ -179,7 +171,8 @@ void MediaSourceDelegate::InitializeDemuxer() {
DCHECK(media_loop_->BelongsToCurrentThread());
demuxer_client_->AddDelegate(demuxer_client_id_, this);
demuxer_->Initialize(this, base::Bind(&MediaSourceDelegate::OnDemuxerInitDone,
- media_weak_factory_.GetWeakPtr()));
+ media_weak_factory_.GetWeakPtr()),
+ false);
}
#if defined(GOOGLE_TV)
@@ -506,6 +499,19 @@ void MediaSourceDelegate::OnDemuxerError(media::PipelineStatus status) {
update_network_state_cb_.Run(PipelineErrorToNetworkState(status));
}
+void MediaSourceDelegate::AddTextStream(
+ media::DemuxerStream* /* text_stream */ ,
+ const media::TextTrackConfig& /* config */ ) {
+ // TODO(matthewjheaney): add text stream (http://crbug/322115).
+ NOTIMPLEMENTED();
+}
+
+void MediaSourceDelegate::RemoveTextStream(
+ media::DemuxerStream* /* text_stream */ ) {
+ // TODO(matthewjheaney): remove text stream (http://crbug/322115).
+ NOTIMPLEMENTED();
+}
+
void MediaSourceDelegate::OnDemuxerInitDone(media::PipelineStatus status) {
DCHECK(media_loop_->BelongsToCurrentThread());
DVLOG(1) << __FUNCTION__ << "(" << status << ") : " << demuxer_client_id_;
diff --git a/content/renderer/media/android/media_source_delegate.h b/content/renderer/media/android/media_source_delegate.h
index 65e4b5b..317ef65 100644
--- a/content/renderer/media/android/media_source_delegate.h
+++ b/content/renderer/media/android/media_source_delegate.h
@@ -127,6 +127,9 @@ class MediaSourceDelegate : public media::DemuxerHost {
base::TimeDelta end) OVERRIDE;
virtual void SetDuration(base::TimeDelta duration) OVERRIDE;
virtual void OnDemuxerError(media::PipelineStatus status) OVERRIDE;
+ virtual void AddTextStream(media::DemuxerStream* text_stream,
+ const media::TextTrackConfig& config) OVERRIDE;
+ virtual void RemoveTextStream(media::DemuxerStream* text_stream) OVERRIDE;
// Notifies |demuxer_client_| and fires |duration_changed_cb_|.
void OnDurationChanged(const base::TimeDelta& duration);
diff --git a/content/renderer/media/texttrack_impl.cc b/content/renderer/media/texttrack_impl.cc
index e50c390..3df473b 100644
--- a/content/renderer/media/texttrack_impl.cc
+++ b/content/renderer/media/texttrack_impl.cc
@@ -4,21 +4,32 @@
#include "content/renderer/media/texttrack_impl.h"
+#include "base/bind.h"
+#include "base/location.h"
+#include "base/message_loop/message_loop_proxy.h"
#include "content/renderer/media/webinbandtexttrack_impl.h"
+#include "media/base/bind_to_loop.h"
#include "third_party/WebKit/public/platform/WebInbandTextTrackClient.h"
#include "third_party/WebKit/public/platform/WebMediaPlayerClient.h"
namespace content {
-TextTrackImpl::TextTrackImpl(blink::WebMediaPlayerClient* client,
- WebInbandTextTrackImpl* text_track)
- : client_(client), text_track_(text_track) {
+TextTrackImpl::TextTrackImpl(
+ const scoped_refptr<base::MessageLoopProxy>& message_loop,
+ blink::WebMediaPlayerClient* client,
+ scoped_ptr<WebInbandTextTrackImpl> text_track)
+ : message_loop_(message_loop),
+ client_(client),
+ text_track_(text_track.Pass()) {
client_->addTextTrack(text_track_.get());
}
TextTrackImpl::~TextTrackImpl() {
- if (text_track_->client())
- client_->removeTextTrack(text_track_.get());
+ message_loop_->PostTask(
+ FROM_HERE,
+ base::Bind(&TextTrackImpl::OnRemoveTrack,
+ client_,
+ base::Passed(&text_track_)));
}
void TextTrackImpl::addWebVTTCue(const base::TimeDelta& start,
@@ -26,12 +37,34 @@ void TextTrackImpl::addWebVTTCue(const base::TimeDelta& start,
const std::string& id,
const std::string& content,
const std::string& settings) {
- if (blink::WebInbandTextTrackClient* client = text_track_->client())
+ message_loop_->PostTask(
+ FROM_HERE,
+ base::Bind(&TextTrackImpl::OnAddCue,
+ text_track_.get(),
+ start, end,
+ id, content, settings));
+}
+
+void TextTrackImpl::OnAddCue(WebInbandTextTrackImpl* text_track,
+ const base::TimeDelta& start,
+ const base::TimeDelta& end,
+ const std::string& id,
+ const std::string& content,
+ const std::string& settings) {
+ if (blink::WebInbandTextTrackClient* client = text_track->client()) {
client->addWebVTTCue(start.InSecondsF(),
end.InSecondsF(),
blink::WebString::fromUTF8(id),
blink::WebString::fromUTF8(content),
blink::WebString::fromUTF8(settings));
+ }
+}
+
+void TextTrackImpl::OnRemoveTrack(
+ blink::WebMediaPlayerClient* client,
+ scoped_ptr<WebInbandTextTrackImpl> text_track) {
+ if (text_track->client())
+ client->removeTextTrack(text_track.get());
}
} // namespace content
diff --git a/content/renderer/media/texttrack_impl.h b/content/renderer/media/texttrack_impl.h
index d7b5961..3285b60 100644
--- a/content/renderer/media/texttrack_impl.h
+++ b/content/renderer/media/texttrack_impl.h
@@ -11,7 +11,12 @@
#include "base/memory/scoped_ptr.h"
#include "media/base/text_track.h"
+namespace base {
+class MessageLoopProxy;
+}
+
namespace blink {
+class WebInbandTextTrackClient;
class WebMediaPlayerClient;
}
@@ -22,8 +27,9 @@ class WebInbandTextTrackImpl;
class TextTrackImpl : public media::TextTrack {
public:
// Constructor assumes ownership of the |text_track| object.
- TextTrackImpl(blink::WebMediaPlayerClient* client,
- WebInbandTextTrackImpl* text_track);
+ TextTrackImpl(const scoped_refptr<base::MessageLoopProxy>& message_loop,
+ blink::WebMediaPlayerClient* client,
+ scoped_ptr<WebInbandTextTrackImpl> text_track);
virtual ~TextTrackImpl();
@@ -34,6 +40,17 @@ class TextTrackImpl : public media::TextTrack {
const std::string& settings) OVERRIDE;
private:
+ static void OnAddCue(WebInbandTextTrackImpl* text_track,
+ const base::TimeDelta& start,
+ const base::TimeDelta& end,
+ const std::string& id,
+ const std::string& content,
+ const std::string& settings);
+
+ static void OnRemoveTrack(blink::WebMediaPlayerClient* client,
+ scoped_ptr<WebInbandTextTrackImpl> text_track);
+
+ scoped_refptr<base::MessageLoopProxy> message_loop_;
blink::WebMediaPlayerClient* client_;
scoped_ptr<WebInbandTextTrackImpl> text_track_;
DISALLOW_COPY_AND_ASSIGN(TextTrackImpl);
diff --git a/content/renderer/media/webmediaplayer_impl.cc b/content/renderer/media/webmediaplayer_impl.cc
index 989d322..60bd397 100644
--- a/content/renderer/media/webmediaplayer_impl.cc
+++ b/content/renderer/media/webmediaplayer_impl.cc
@@ -38,6 +38,7 @@
#include "media/base/media_log.h"
#include "media/base/media_switches.h"
#include "media/base/pipeline.h"
+#include "media/base/text_renderer.h"
#include "media/base/video_frame.h"
#include "media/filters/audio_renderer_impl.h"
#include "media/filters/chunk_demuxer.h"
@@ -993,21 +994,26 @@ void WebMediaPlayerImpl::OnNeedKey(const std::string& type,
init_data.size());
}
-scoped_ptr<media::TextTrack>
-WebMediaPlayerImpl::OnTextTrack(media::TextKind kind,
- const std::string& label,
- const std::string& language) {
- typedef WebInbandTextTrackImpl::Kind webkind_t;
- const webkind_t webkind = static_cast<webkind_t>(kind);
- const blink::WebString weblabel = blink::WebString::fromUTF8(label);
- const blink::WebString weblanguage = blink::WebString::fromUTF8(language);
+void WebMediaPlayerImpl::OnAddTextTrack(
+ const media::TextTrackConfig& config,
+ const media::AddTextTrackDoneCB& done_cb) {
+ DCHECK(main_loop_->BelongsToCurrentThread());
+
+ const WebInbandTextTrackImpl::Kind web_kind =
+ static_cast<WebInbandTextTrackImpl::Kind>(config.kind());
+ const blink::WebString web_label =
+ blink::WebString::fromUTF8(config.label());
+ const blink::WebString web_language =
+ blink::WebString::fromUTF8(config.language());
+
+ scoped_ptr<WebInbandTextTrackImpl> web_inband_text_track(
+ new WebInbandTextTrackImpl(web_kind, web_label, web_language,
+ text_track_index_++));
- WebInbandTextTrackImpl* const text_track =
- new WebInbandTextTrackImpl(webkind, weblabel, weblanguage,
- text_track_index_++);
+ scoped_ptr<media::TextTrack> text_track(
+ new TextTrackImpl(main_loop_, GetClient(), web_inband_text_track.Pass()));
- return scoped_ptr<media::TextTrack>(new TextTrackImpl(GetClient(),
- text_track));
+ done_cb.Run(text_track.Pass());
}
void WebMediaPlayerImpl::OnKeyError(const std::string& session_id,
@@ -1091,17 +1097,9 @@ void WebMediaPlayerImpl::StartPipeline() {
DCHECK(!chunk_demuxer_);
DCHECK(!data_source_);
- media::AddTextTrackCB add_text_track_cb;
-
- if (cmd_line->HasSwitch(switches::kEnableInbandTextTracks)) {
- add_text_track_cb =
- base::Bind(&WebMediaPlayerImpl::OnTextTrack, base::Unretained(this));
- }
-
chunk_demuxer_ = new media::ChunkDemuxer(
BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnDemuxerOpened),
BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnNeedKey),
- add_text_track_cb,
base::Bind(&LogMediaSourceError, media_log_));
demuxer_.reset(chunk_demuxer_);
@@ -1169,6 +1167,15 @@ void WebMediaPlayerImpl::StartPipeline() {
true));
filter_collection->SetVideoRenderer(video_renderer.Pass());
+ if (cmd_line->HasSwitch(switches::kEnableInbandTextTracks)) {
+ scoped_ptr<media::TextRenderer> text_renderer(
+ new media::TextRenderer(
+ media_loop_,
+ BIND_TO_RENDER_LOOP(&WebMediaPlayerImpl::OnAddTextTrack)));
+
+ filter_collection->SetTextRenderer(text_renderer.Pass());
+ }
+
// ... and we're ready to go!
starting_ = true;
pipeline_->Start(
diff --git a/content/renderer/media/webmediaplayer_impl.h b/content/renderer/media/webmediaplayer_impl.h
index 47ff1d6..df3657f 100644
--- a/content/renderer/media/webmediaplayer_impl.h
+++ b/content/renderer/media/webmediaplayer_impl.h
@@ -55,7 +55,6 @@ class MessageLoopProxy;
namespace media {
class ChunkDemuxer;
-class FFmpegDemuxer;
class GpuVideoAcceleratorFactories;
class MediaLog;
}
@@ -196,9 +195,8 @@ class WebMediaPlayerImpl
const std::string& default_url);
void OnNeedKey(const std::string& type,
const std::vector<uint8>& init_data);
- scoped_ptr<media::TextTrack> OnTextTrack(media::TextKind kind,
- const std::string& label,
- const std::string& language);
+ void OnAddTextTrack(const media::TextTrackConfig& config,
+ const media::AddTextTrackDoneCB& done_cb);
void SetOpaque(bool);
private:
diff --git a/media/base/demuxer.h b/media/base/demuxer.h
index a2dad22..9b671f0 100644
--- a/media/base/demuxer.h
+++ b/media/base/demuxer.h
@@ -15,6 +15,8 @@
namespace media {
+class TextTrackConfig;
+
class MEDIA_EXPORT DemuxerHost : public DataSourceHost {
public:
// Sets the duration of the media in microseconds.
@@ -25,6 +27,13 @@ class MEDIA_EXPORT DemuxerHost : public DataSourceHost {
// method with PIPELINE_OK.
virtual void OnDemuxerError(PipelineStatus error) = 0;
+ // Add |text_stream| to the collection managed by the text renderer.
+ virtual void AddTextStream(DemuxerStream* text_stream,
+ const TextTrackConfig& config) = 0;
+
+ // Remove |text_stream| from the presentation.
+ virtual void RemoveTextStream(DemuxerStream* text_stream) = 0;
+
protected:
virtual ~DemuxerHost();
};
@@ -45,7 +54,8 @@ class MEDIA_EXPORT Demuxer {
// The demuxer does not own |host| as it is guaranteed to outlive the
// lifetime of the demuxer. Don't delete it!
virtual void Initialize(DemuxerHost* host,
- const PipelineStatusCB& status_cb) = 0;
+ const PipelineStatusCB& status_cb,
+ bool enable_text_tracks) = 0;
// Carry out any actions required to seek to the given time, executing the
// callback upon completion.
@@ -66,7 +76,8 @@ class MEDIA_EXPORT Demuxer {
// TODO(scherkus): this might not be needed http://crbug.com/234708
virtual void OnAudioRendererDisabled() = 0;
- // Returns the given stream type, or NULL if that type is not present.
+ // Returns the first stream of the given stream type (which is not allowed
+ // to be DemuxerStream::TEXT), or NULL if that type of stream is not present.
virtual DemuxerStream* GetStream(DemuxerStream::Type type) = 0;
// Returns the starting time for the media file.
diff --git a/media/base/demuxer_perftest.cc b/media/base/demuxer_perftest.cc
index 8681e96..8e791ee 100644
--- a/media/base/demuxer_perftest.cc
+++ b/media/base/demuxer_perftest.cc
@@ -33,6 +33,9 @@ class DemuxerHostImpl : public media::DemuxerHost {
// DemuxerHost implementation.
virtual void SetDuration(base::TimeDelta duration) OVERRIDE {}
virtual void OnDemuxerError(media::PipelineStatus error) OVERRIDE {}
+ virtual void AddTextStream(media::DemuxerStream* text_stream,
+ const media::TextTrackConfig& config) OVERRIDE {}
+ virtual void RemoveTextStream(media::DemuxerStream* text_stream) OVERRIDE {}
private:
DISALLOW_COPY_AND_ASSIGN(DemuxerHostImpl);
@@ -182,8 +185,9 @@ static void RunDemuxerBenchmark(const std::string& filename) {
need_key_cb,
new MediaLog());
- demuxer.Initialize(&demuxer_host, base::Bind(
- &QuitLoopWithStatus, &message_loop));
+ demuxer.Initialize(&demuxer_host,
+ base::Bind(&QuitLoopWithStatus, &message_loop),
+ false);
message_loop.Run();
StreamReader stream_reader(&demuxer, false);
diff --git a/media/base/demuxer_stream.h b/media/base/demuxer_stream.h
index bb45344..4e07c66 100644
--- a/media/base/demuxer_stream.h
+++ b/media/base/demuxer_stream.h
@@ -21,6 +21,7 @@ class MEDIA_EXPORT DemuxerStream {
UNKNOWN,
AUDIO,
VIDEO,
+ TEXT,
NUM_TYPES, // Always keep this entry as the last one!
};
diff --git a/media/base/fake_text_track_stream.cc b/media/base/fake_text_track_stream.cc
new file mode 100644
index 0000000..3136c47
--- /dev/null
+++ b/media/base/fake_text_track_stream.cc
@@ -0,0 +1,83 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "media/base/fake_text_track_stream.h"
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "media/base/decoder_buffer.h"
+#include "media/filters/webvtt_util.h"
+
+namespace media {
+
+FakeTextTrackStream::FakeTextTrackStream()
+ : message_loop_(base::MessageLoopProxy::current()),
+ stopping_(false) {
+}
+
+FakeTextTrackStream::~FakeTextTrackStream() {
+ DCHECK(read_cb_.is_null());
+}
+
+void FakeTextTrackStream::Read(const ReadCB& read_cb) {
+ DCHECK(!read_cb.is_null());
+ DCHECK(read_cb_.is_null());
+ OnRead();
+ read_cb_ = read_cb;
+
+ if (stopping_) {
+ message_loop_->PostTask(FROM_HERE, base::Bind(
+ &FakeTextTrackStream::AbortPendingRead, base::Unretained(this)));
+ }
+}
+
+DemuxerStream::Type FakeTextTrackStream::type() {
+ return DemuxerStream::TEXT;
+}
+
+void FakeTextTrackStream::SatisfyPendingRead(
+ const base::TimeDelta& start,
+ const base::TimeDelta& duration,
+ const std::string& id,
+ const std::string& content,
+ const std::string& settings) {
+ DCHECK(!read_cb_.is_null());
+
+ const uint8* const data_buf = reinterpret_cast<const uint8*>(content.data());
+ const int data_len = static_cast<int>(content.size());
+
+ std::vector<uint8> side_data;
+ MakeSideData(id.begin(), id.end(),
+ settings.begin(), settings.end(),
+ &side_data);
+
+ const uint8* const sd_buf = &side_data[0];
+ const int sd_len = static_cast<int>(side_data.size());
+
+ scoped_refptr<DecoderBuffer> buffer;
+ buffer = DecoderBuffer::CopyFrom(data_buf, data_len, sd_buf, sd_len);
+
+ buffer->set_timestamp(start);
+ buffer->set_duration(duration);
+
+ base::ResetAndReturn(&read_cb_).Run(kOk, buffer);
+}
+
+void FakeTextTrackStream::AbortPendingRead() {
+ DCHECK(!read_cb_.is_null());
+ base::ResetAndReturn(&read_cb_).Run(kAborted, NULL);
+}
+
+void FakeTextTrackStream::SendEosNotification() {
+ DCHECK(!read_cb_.is_null());
+ base::ResetAndReturn(&read_cb_).Run(kOk, DecoderBuffer::CreateEOSBuffer());
+}
+
+void FakeTextTrackStream::Stop() {
+ stopping_ = true;
+ if (!read_cb_.is_null())
+ AbortPendingRead();
+}
+
+} // namespace media
diff --git a/media/base/fake_text_track_stream.h b/media/base/fake_text_track_stream.h
new file mode 100644
index 0000000..33c74ef
--- /dev/null
+++ b/media/base/fake_text_track_stream.h
@@ -0,0 +1,47 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "base/message_loop/message_loop.h"
+#include "media/base/audio_decoder_config.h"
+#include "media/base/demuxer_stream.h"
+#include "media/base/video_decoder_config.h"
+#include "testing/gmock/include/gmock/gmock.h"
+
+namespace media {
+
+// Fake implementation of the DemuxerStream. These are the stream objects
+// we pass to the text renderer object when streams are added and removed.
+class FakeTextTrackStream : public DemuxerStream {
+ public:
+ FakeTextTrackStream();
+ virtual ~FakeTextTrackStream();
+
+ // DemuxerStream implementation.
+ virtual void Read(const ReadCB&) OVERRIDE;
+ MOCK_METHOD0(audio_decoder_config, AudioDecoderConfig());
+ MOCK_METHOD0(video_decoder_config, VideoDecoderConfig());
+ virtual Type type() OVERRIDE;
+ MOCK_METHOD0(EnableBitstreamConverter, void());
+
+ void SatisfyPendingRead(const base::TimeDelta& start,
+ const base::TimeDelta& duration,
+ const std::string& id,
+ const std::string& content,
+ const std::string& settings);
+ void AbortPendingRead();
+ void SendEosNotification();
+
+ void Stop();
+
+ MOCK_METHOD0(OnRead, void());
+
+ private:
+ scoped_refptr<base::MessageLoopProxy> message_loop_;
+ ReadCB read_cb_;
+ bool stopping_;
+
+ DISALLOW_COPY_AND_ASSIGN(FakeTextTrackStream);
+};
+
+} // namespace media
diff --git a/media/base/filter_collection.cc b/media/base/filter_collection.cc
index 730835f..da5042f 100644
--- a/media/base/filter_collection.cc
+++ b/media/base/filter_collection.cc
@@ -6,6 +6,7 @@
#include "media/base/audio_renderer.h"
#include "media/base/demuxer.h"
+#include "media/base/text_renderer.h"
#include "media/base/video_renderer.h"
namespace media {
@@ -40,4 +41,13 @@ scoped_ptr<VideoRenderer> FilterCollection::GetVideoRenderer() {
return video_renderer_.Pass();
}
+void FilterCollection::SetTextRenderer(
+ scoped_ptr<TextRenderer> text_renderer) {
+ text_renderer_ = text_renderer.Pass();
+}
+
+scoped_ptr<TextRenderer> FilterCollection::GetTextRenderer() {
+ return text_renderer_.Pass();
+}
+
} // namespace media
diff --git a/media/base/filter_collection.h b/media/base/filter_collection.h
index 90ea066..a0aee76 100644
--- a/media/base/filter_collection.h
+++ b/media/base/filter_collection.h
@@ -12,6 +12,7 @@ namespace media {
class AudioRenderer;
class Demuxer;
+class TextRenderer;
class VideoRenderer;
// Represents a set of uninitialized demuxer and audio/video decoders and
@@ -33,10 +34,14 @@ class MEDIA_EXPORT FilterCollection {
void SetVideoRenderer(scoped_ptr<VideoRenderer> video_renderer);
scoped_ptr<VideoRenderer> GetVideoRenderer();
+ void SetTextRenderer(scoped_ptr<TextRenderer> text_renderer);
+ scoped_ptr<TextRenderer> GetTextRenderer();
+
private:
Demuxer* demuxer_;
scoped_ptr<AudioRenderer> audio_renderer_;
scoped_ptr<VideoRenderer> video_renderer_;
+ scoped_ptr<TextRenderer> text_renderer_;
DISALLOW_COPY_AND_ASSIGN(FilterCollection);
};
diff --git a/media/base/media_log.cc b/media/base/media_log.cc
index 8a07b02..e791b44 100644
--- a/media/base/media_log.cc
+++ b/media/base/media_log.cc
@@ -50,6 +50,8 @@ const char* MediaLog::EventTypeToString(MediaLogEvent::Type type) {
return "AUDIO_ENDED";
case MediaLogEvent::VIDEO_ENDED:
return "VIDEO_ENDED";
+ case MediaLogEvent::TEXT_ENDED:
+ return "TEXT_ENDED";
case MediaLogEvent::AUDIO_RENDERER_DISABLED:
return "AUDIO_RENDERER_DISABLED";
case MediaLogEvent::BUFFERED_EXTENTS_CHANGED:
diff --git a/media/base/media_log_event.h b/media/base/media_log_event.h
index 811d113..3052d41 100644
--- a/media/base/media_log_event.h
+++ b/media/base/media_log_event.h
@@ -70,9 +70,10 @@ struct MediaLogEvent {
TOTAL_BYTES_SET,
NETWORK_ACTIVITY_SET,
- // Audio/Video stream playback has ended.
+ // Audio/Video/Text stream playback has ended.
AUDIO_ENDED,
VIDEO_ENDED,
+ TEXT_ENDED,
// The audio renderer has been disabled.
// params: none.
diff --git a/media/base/mock_demuxer_host.h b/media/base/mock_demuxer_host.h
index 597c132..61761a8 100644
--- a/media/base/mock_demuxer_host.h
+++ b/media/base/mock_demuxer_host.h
@@ -5,9 +5,8 @@
#ifndef MEDIA_BASE_MOCK_DEMUXER_HOST_H_
#define MEDIA_BASE_MOCK_DEMUXER_HOST_H_
-#include <string>
-
#include "media/base/demuxer.h"
+#include "media/base/text_track_config.h"
#include "testing/gmock/include/gmock/gmock.h"
namespace media {
@@ -26,6 +25,9 @@ class MockDemuxerHost : public DemuxerHost {
// DemuxerHost implementation.
MOCK_METHOD1(OnDemuxerError, void(PipelineStatus error));
MOCK_METHOD1(SetDuration, void(base::TimeDelta duration));
+ MOCK_METHOD2(AddTextStream, void(DemuxerStream*,
+ const TextTrackConfig&));
+ MOCK_METHOD1(RemoveTextStream, void(DemuxerStream*));
private:
DISALLOW_COPY_AND_ASSIGN(MockDemuxerHost);
diff --git a/media/base/mock_filters.cc b/media/base/mock_filters.cc
index eaf5201..e4faf70 100644
--- a/media/base/mock_filters.cc
+++ b/media/base/mock_filters.cc
@@ -66,6 +66,10 @@ MockAudioRenderer::MockAudioRenderer() {}
MockAudioRenderer::~MockAudioRenderer() {}
+MockTextTrack::MockTextTrack() {}
+
+MockTextTrack::~MockTextTrack() {}
+
MockDecryptor::MockDecryptor() {}
MockDecryptor::~MockDecryptor() {}
diff --git a/media/base/mock_filters.h b/media/base/mock_filters.h
index fb5e8a0..c71590d 100644
--- a/media/base/mock_filters.h
+++ b/media/base/mock_filters.h
@@ -16,6 +16,7 @@
#include "media/base/demuxer.h"
#include "media/base/filter_collection.h"
#include "media/base/pipeline_status.h"
+#include "media/base/text_track.h"
#include "media/base/video_decoder.h"
#include "media/base/video_decoder_config.h"
#include "media/base/video_frame.h"
@@ -30,7 +31,8 @@ class MockDemuxer : public Demuxer {
virtual ~MockDemuxer();
// Demuxer implementation.
- MOCK_METHOD2(Initialize, void(DemuxerHost* host, const PipelineStatusCB& cb));
+ MOCK_METHOD3(Initialize,
+ void(DemuxerHost* host, const PipelineStatusCB& cb, bool));
MOCK_METHOD1(SetPlaybackRate, void(float playback_rate));
MOCK_METHOD2(Seek, void(base::TimeDelta time, const PipelineStatusCB& cb));
MOCK_METHOD1(Stop, void(const base::Closure& callback));
@@ -155,6 +157,21 @@ class MockAudioRenderer : public AudioRenderer {
DISALLOW_COPY_AND_ASSIGN(MockAudioRenderer);
};
+class MockTextTrack : public TextTrack {
+ public:
+ MockTextTrack();
+ virtual ~MockTextTrack();
+
+ MOCK_METHOD5(addWebVTTCue, void(const base::TimeDelta& start,
+ const base::TimeDelta& end,
+ const std::string& id,
+ const std::string& content,
+ const std::string& settings));
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(MockTextTrack);
+};
+
class MockDecryptor : public Decryptor {
public:
MockDecryptor();
diff --git a/media/base/pipeline.cc b/media/base/pipeline.cc
index 5454fa7..e82f88d 100644
--- a/media/base/pipeline.cc
+++ b/media/base/pipeline.cc
@@ -21,6 +21,8 @@
#include "media/base/clock.h"
#include "media/base/filter_collection.h"
#include "media/base/media_log.h"
+#include "media/base/text_renderer.h"
+#include "media/base/text_track_config.h"
#include "media/base/video_decoder.h"
#include "media/base/video_decoder_config.h"
#include "media/base/video_renderer.h"
@@ -47,6 +49,7 @@ Pipeline::Pipeline(const scoped_refptr<base::MessageLoopProxy>& message_loop,
state_(kCreated),
audio_ended_(false),
video_ended_(false),
+ text_ended_(false),
audio_disabled_(false),
demuxer_(NULL),
creation_time_(default_tick_clock_.NowTicks()) {
@@ -293,6 +296,19 @@ void Pipeline::OnDemuxerError(PipelineStatus error) {
SetError(error);
}
+void Pipeline::AddTextStream(DemuxerStream* text_stream,
+ const TextTrackConfig& config) {
+ message_loop_->PostTask(FROM_HERE, base::Bind(
+ &Pipeline::AddTextStreamTask, base::Unretained(this),
+ text_stream, config));
+}
+
+void Pipeline::RemoveTextStream(DemuxerStream* text_stream) {
+ message_loop_->PostTask(FROM_HERE, base::Bind(
+ &Pipeline::RemoveTextStreamTask, base::Unretained(this),
+ text_stream));
+}
+
void Pipeline::SetError(PipelineStatus error) {
DCHECK(IsRunning());
DCHECK_NE(PIPELINE_OK, error);
@@ -537,6 +553,10 @@ void Pipeline::DoSeek(
bound_fns.Push(base::Bind(
&VideoRenderer::Pause, base::Unretained(video_renderer_.get())));
}
+ if (text_renderer_) {
+ bound_fns.Push(base::Bind(
+ &TextRenderer::Pause, base::Unretained(text_renderer_.get())));
+ }
// Flush.
if (audio_renderer_) {
@@ -547,6 +567,10 @@ void Pipeline::DoSeek(
bound_fns.Push(base::Bind(
&VideoRenderer::Flush, base::Unretained(video_renderer_.get())));
}
+ if (text_renderer_) {
+ bound_fns.Push(base::Bind(
+ &TextRenderer::Flush, base::Unretained(text_renderer_.get())));
+ }
// Seek demuxer.
bound_fns.Push(base::Bind(
@@ -586,6 +610,11 @@ void Pipeline::DoPlay(const PipelineStatusCB& done_cb) {
&VideoRenderer::Play, base::Unretained(video_renderer_.get())));
}
+ if (text_renderer_) {
+ bound_fns.Push(base::Bind(
+ &TextRenderer::Play, base::Unretained(text_renderer_.get())));
+ }
+
pending_callbacks_ = SerialRunner::Run(bound_fns, done_cb);
}
@@ -609,6 +638,11 @@ void Pipeline::DoStop(const PipelineStatusCB& done_cb) {
&VideoRenderer::Stop, base::Unretained(video_renderer_.get())));
}
+ if (text_renderer_) {
+ bound_fns.Push(base::Bind(
+ &TextRenderer::Stop, base::Unretained(text_renderer_.get())));
+ }
+
pending_callbacks_ = SerialRunner::Run(bound_fns, done_cb);
}
@@ -625,6 +659,7 @@ void Pipeline::OnStopCompleted(PipelineStatus status) {
filter_collection_.reset();
audio_renderer_.reset();
video_renderer_.reset();
+ text_renderer_.reset();
demuxer_ = NULL;
// If we stop during initialization/seeking we want to run |seek_cb_|
@@ -685,6 +720,13 @@ void Pipeline::OnVideoRendererEnded() {
media_log_->AddEvent(media_log_->CreateEvent(MediaLogEvent::VIDEO_ENDED));
}
+void Pipeline::OnTextRendererEnded() {
+ // Force post to process ended messages after current execution frame.
+ message_loop_->PostTask(FROM_HERE, base::Bind(
+ &Pipeline::DoTextRendererEnded, base::Unretained(this)));
+ media_log_->AddEvent(media_log_->CreateEvent(MediaLogEvent::TEXT_ENDED));
+}
+
// Called from any thread.
void Pipeline::OnUpdateStatistics(const PipelineStatistics& stats) {
base::AutoLock auto_lock(lock_);
@@ -711,6 +753,13 @@ void Pipeline::StartTask(scoped_ptr<FilterCollection> filter_collection,
buffering_state_cb_ = buffering_state_cb;
duration_change_cb_ = duration_change_cb;
+ text_renderer_ = filter_collection_->GetTextRenderer();
+
+ if (text_renderer_) {
+ text_renderer_->Initialize(
+ base::Bind(&Pipeline::OnTextRendererEnded, base::Unretained(this)));
+ }
+
StateTransitionTask(PIPELINE_OK);
}
@@ -800,6 +849,7 @@ void Pipeline::SeekTask(TimeDelta time, const PipelineStatusCB& seek_cb) {
seek_cb_ = seek_cb;
audio_ended_ = false;
video_ended_ = false;
+ text_ended_ = false;
// Kick off seeking!
{
@@ -843,6 +893,18 @@ void Pipeline::DoVideoRendererEnded() {
RunEndedCallbackIfNeeded();
}
+void Pipeline::DoTextRendererEnded() {
+ DCHECK(message_loop_->BelongsToCurrentThread());
+
+ if (state_ != kStarted)
+ return;
+
+ DCHECK(!text_ended_);
+ text_ended_ = true;
+
+ RunEndedCallbackIfNeeded();
+}
+
void Pipeline::RunEndedCallbackIfNeeded() {
DCHECK(message_loop_->BelongsToCurrentThread());
@@ -852,6 +914,9 @@ void Pipeline::RunEndedCallbackIfNeeded() {
if (video_renderer_ && !video_ended_)
return;
+ if (text_renderer_ && text_renderer_->HasTracks() && !text_ended_)
+ return;
+
{
base::AutoLock auto_lock(lock_);
clock_->EndOfStream();
@@ -876,11 +941,24 @@ void Pipeline::AudioDisabledTask() {
StartClockIfWaitingForTimeUpdate_Locked();
}
+void Pipeline::AddTextStreamTask(DemuxerStream* text_stream,
+ const TextTrackConfig& config) {
+ DCHECK(message_loop_->BelongsToCurrentThread());
+ // TODO(matthewjheaney): fix up text_ended_ when text stream
+ // is added (http://crbug.com/321446).
+ text_renderer_->AddTextStream(text_stream, config);
+}
+
+void Pipeline::RemoveTextStreamTask(DemuxerStream* text_stream) {
+ DCHECK(message_loop_->BelongsToCurrentThread());
+ text_renderer_->RemoveTextStream(text_stream);
+}
+
void Pipeline::InitializeDemuxer(const PipelineStatusCB& done_cb) {
DCHECK(message_loop_->BelongsToCurrentThread());
demuxer_ = filter_collection_->GetDemuxer();
- demuxer_->Initialize(this, done_cb);
+ demuxer_->Initialize(this, done_cb, text_renderer_);
}
void Pipeline::InitializeAudioRenderer(const PipelineStatusCB& done_cb) {
diff --git a/media/base/pipeline.h b/media/base/pipeline.h
index 09ff904..222091f 100644
--- a/media/base/pipeline.h
+++ b/media/base/pipeline.h
@@ -30,6 +30,8 @@ namespace media {
class Clock;
class FilterCollection;
class MediaLog;
+class TextRenderer;
+class TextTrackConfig;
class VideoRenderer;
// Pipeline runs the media pipeline. Filters are created and called on the
@@ -232,6 +234,9 @@ class MEDIA_EXPORT Pipeline : public DemuxerHost {
// DemuxerHost implementaion.
virtual void SetDuration(base::TimeDelta duration) OVERRIDE;
virtual void OnDemuxerError(PipelineStatus error) OVERRIDE;
+ virtual void AddTextStream(DemuxerStream* text_stream,
+ const TextTrackConfig& config) OVERRIDE;
+ virtual void RemoveTextStream(DemuxerStream* text_stream) OVERRIDE;
// Initiates teardown sequence in response to a runtime error.
//
@@ -244,6 +249,7 @@ class MEDIA_EXPORT Pipeline : public DemuxerHost {
// Callbacks executed when a renderer has ended.
void OnAudioRendererEnded();
void OnVideoRendererEnded();
+ void OnTextRendererEnded();
// Callback executed by filters to update statistics.
void OnUpdateStatistics(const PipelineStatistics& stats);
@@ -283,14 +289,22 @@ class MEDIA_EXPORT Pipeline : public DemuxerHost {
// Carries out notifying filters that we are seeking to a new timestamp.
void SeekTask(base::TimeDelta time, const PipelineStatusCB& seek_cb);
- // Handles audio/video ended logic and running |ended_cb_|.
+ // Handles audio/video/text ended logic and running |ended_cb_|.
void DoAudioRendererEnded();
void DoVideoRendererEnded();
+ void DoTextRendererEnded();
void RunEndedCallbackIfNeeded();
// Carries out disabling the audio renderer.
void AudioDisabledTask();
+ // Carries out adding a new text stream to the text renderer.
+ void AddTextStreamTask(DemuxerStream* text_stream,
+ const TextTrackConfig& config);
+
+ // Carries out removing a text stream from the text renderer.
+ void RemoveTextStreamTask(DemuxerStream* text_stream);
+
// Kicks off initialization for each media object, executing |done_cb| with
// the result when completed.
void InitializeDemuxer(const PipelineStatusCB& done_cb);
@@ -392,7 +406,7 @@ class MEDIA_EXPORT Pipeline : public DemuxerHost {
// reset the pipeline state, and restore this to PIPELINE_OK.
PipelineStatus status_;
- // Whether the media contains rendered audio and video streams.
+ // Whether the media contains rendered audio or video streams.
// TODO(fischman,scherkus): replace these with checks for
// {audio,video}_decoder_ once extraction of {Audio,Video}Decoder from the
// Filter heirarchy is done.
@@ -405,9 +419,10 @@ class MEDIA_EXPORT Pipeline : public DemuxerHost {
// Member that tracks the current state.
State state_;
- // Whether we've received the audio/video ended events.
+ // Whether we've received the audio/video/text ended events.
bool audio_ended_;
bool video_ended_;
+ bool text_ended_;
// Set to true in DisableAudioRendererTask().
bool audio_disabled_;
@@ -434,6 +449,7 @@ class MEDIA_EXPORT Pipeline : public DemuxerHost {
// playback rate, and determining when playback has finished.
scoped_ptr<AudioRenderer> audio_renderer_;
scoped_ptr<VideoRenderer> video_renderer_;
+ scoped_ptr<TextRenderer> text_renderer_;
PipelineStatistics statistics_;
diff --git a/media/base/pipeline_unittest.cc b/media/base/pipeline_unittest.cc
index 1506c21..ccd7e4e 100644
--- a/media/base/pipeline_unittest.cc
+++ b/media/base/pipeline_unittest.cc
@@ -11,11 +11,14 @@
#include "base/threading/simple_thread.h"
#include "base/time/clock.h"
#include "media/base/clock.h"
+#include "media/base/fake_text_track_stream.h"
#include "media/base/gmock_callback_support.h"
#include "media/base/media_log.h"
#include "media/base/mock_filters.h"
#include "media/base/pipeline.h"
#include "media/base/test_helpers.h"
+#include "media/base/text_renderer.h"
+#include "media/base/text_track_config.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/size.h"
@@ -93,6 +96,13 @@ class PipelineTest : public ::testing::Test {
scoped_ptr<AudioRenderer> audio_renderer(audio_renderer_);
filter_collection_->SetAudioRenderer(audio_renderer.Pass());
+ text_renderer_ = new TextRenderer(
+ message_loop_.message_loop_proxy(),
+ base::Bind(&PipelineTest::OnAddTextTrack,
+ base::Unretained(this)));
+ scoped_ptr<TextRenderer> text_renderer(text_renderer_);
+ filter_collection_->SetTextRenderer(text_renderer.Pass());
+
// InitializeDemuxer() adds overriding expectations for expected non-NULL
// streams.
DemuxerStream* null_pointer = NULL;
@@ -109,6 +119,13 @@ class PipelineTest : public ::testing::Test {
ExpectStop();
+ // The mock demuxer doesn't stop the fake text track stream,
+ // so just stop it manually.
+ if (text_stream_) {
+ text_stream_->Stop();
+ message_loop_.RunUntilIdle();
+ }
+
// Expect a stop callback if we were started.
EXPECT_CALL(callbacks_, OnStop());
pipeline_->Stop(base::Bind(&CallbackHelper::OnStop,
@@ -122,7 +139,7 @@ class PipelineTest : public ::testing::Test {
void InitializeDemuxer(MockDemuxerStreamVector* streams,
const base::TimeDelta& duration) {
EXPECT_CALL(callbacks_, OnDurationChange());
- EXPECT_CALL(*demuxer_, Initialize(_, _))
+ EXPECT_CALL(*demuxer_, Initialize(_, _, _))
.WillOnce(DoAll(SetDemuxerProperties(duration),
RunCallback<1>(PIPELINE_OK)));
@@ -173,6 +190,13 @@ class PipelineTest : public ::testing::Test {
}
}
+ void AddTextStream() {
+ EXPECT_CALL(*this, OnAddTextTrack(_,_))
+ .WillOnce(Invoke(this, &PipelineTest::DoOnAddTextTrack));
+ static_cast<DemuxerHost*>(pipeline_.get())->AddTextStream(text_stream(),
+ TextTrackConfig(kTextSubtitles, "", ""));
+ }
+
// Sets up expectations on the callback and initializes the pipeline. Called
// after tests have set expectations any filters they wish to use.
void InitializePipeline(PipelineStatus start_status) {
@@ -215,6 +239,11 @@ class PipelineTest : public ::testing::Test {
video_stream_->set_video_decoder_config(video_decoder_config_);
}
+ void CreateTextStream() {
+ scoped_ptr<FakeTextTrackStream> text_stream(new FakeTextTrackStream);
+ text_stream_ = text_stream.Pass();
+ }
+
MockDemuxerStream* audio_stream() {
return audio_stream_.get();
}
@@ -223,6 +252,10 @@ class PipelineTest : public ::testing::Test {
return video_stream_.get();
}
+ FakeTextTrackStream* text_stream() {
+ return text_stream_.get();
+ }
+
void ExpectSeek(const base::TimeDelta& seek_time) {
// Every filter should receive a call to Seek().
EXPECT_CALL(*demuxer_, Seek(seek_time, _))
@@ -281,6 +314,15 @@ class PipelineTest : public ::testing::Test {
EXPECT_CALL(*video_renderer_, Stop(_)).WillOnce(RunClosure<0>());
}
+ MOCK_METHOD2(OnAddTextTrack, void(const TextTrackConfig&,
+ const AddTextTrackDoneCB&));
+
+ void DoOnAddTextTrack(const TextTrackConfig& config,
+ const AddTextTrackDoneCB& done_cb) {
+ scoped_ptr<TextTrack> text_track(new MockTextTrack);
+ done_cb.Run(text_track.Pass());
+ }
+
// Fixture members.
StrictMock<CallbackHelper> callbacks_;
base::SimpleTestTickClock test_tick_clock_;
@@ -291,8 +333,11 @@ class PipelineTest : public ::testing::Test {
scoped_ptr<MockDemuxer> demuxer_;
MockVideoRenderer* video_renderer_;
MockAudioRenderer* audio_renderer_;
+ StrictMock<CallbackHelper> text_renderer_callbacks_;
+ TextRenderer* text_renderer_;
scoped_ptr<StrictMock<MockDemuxerStream> > audio_stream_;
scoped_ptr<StrictMock<MockDemuxerStream> > video_stream_;
+ scoped_ptr<FakeTextTrackStream> text_stream_;
AudioRenderer::TimeCB audio_time_cb_;
VideoDecoderConfig video_decoder_config_;
@@ -338,7 +383,7 @@ TEST_F(PipelineTest, NotStarted) {
TEST_F(PipelineTest, NeverInitializes) {
// Don't execute the callback passed into Initialize().
- EXPECT_CALL(*demuxer_, Initialize(_, _));
+ EXPECT_CALL(*demuxer_, Initialize(_, _, _));
// This test hangs during initialization by never calling
// InitializationComplete(). StrictMock<> will ensure that the callback is
@@ -363,7 +408,7 @@ TEST_F(PipelineTest, NeverInitializes) {
}
TEST_F(PipelineTest, URLNotFound) {
- EXPECT_CALL(*demuxer_, Initialize(_, _))
+ EXPECT_CALL(*demuxer_, Initialize(_, _, _))
.WillOnce(RunCallback<1>(PIPELINE_ERROR_URL_NOT_FOUND));
EXPECT_CALL(*demuxer_, Stop(_))
.WillOnce(RunClosure<0>());
@@ -372,7 +417,7 @@ TEST_F(PipelineTest, URLNotFound) {
}
TEST_F(PipelineTest, NoStreams) {
- EXPECT_CALL(*demuxer_, Initialize(_, _))
+ EXPECT_CALL(*demuxer_, Initialize(_, _, _))
.WillOnce(RunCallback<1>(PIPELINE_OK));
EXPECT_CALL(*demuxer_, Stop(_))
.WillOnce(RunClosure<0>());
@@ -422,9 +467,47 @@ TEST_F(PipelineTest, AudioVideoStream) {
EXPECT_TRUE(pipeline_->HasVideo());
}
+TEST_F(PipelineTest, VideoTextStream) {
+ CreateVideoStream();
+ CreateTextStream();
+ MockDemuxerStreamVector streams;
+ streams.push_back(video_stream());
+
+ InitializeDemuxer(&streams);
+ InitializeVideoRenderer(video_stream());
+
+ InitializePipeline(PIPELINE_OK);
+ EXPECT_FALSE(pipeline_->HasAudio());
+ EXPECT_TRUE(pipeline_->HasVideo());
+
+ AddTextStream();
+ message_loop_.RunUntilIdle();
+}
+
+TEST_F(PipelineTest, VideoAudioTextStream) {
+ CreateVideoStream();
+ CreateAudioStream();
+ CreateTextStream();
+ MockDemuxerStreamVector streams;
+ streams.push_back(video_stream());
+ streams.push_back(audio_stream());
+
+ InitializeDemuxer(&streams);
+ InitializeVideoRenderer(video_stream());
+ InitializeAudioRenderer(audio_stream(), false);
+
+ InitializePipeline(PIPELINE_OK);
+ EXPECT_TRUE(pipeline_->HasAudio());
+ EXPECT_TRUE(pipeline_->HasVideo());
+
+ AddTextStream();
+ message_loop_.RunUntilIdle();
+}
+
TEST_F(PipelineTest, Seek) {
CreateAudioStream();
CreateVideoStream();
+ CreateTextStream();
MockDemuxerStreamVector streams;
streams.push_back(audio_stream());
streams.push_back(video_stream());
@@ -436,6 +519,9 @@ TEST_F(PipelineTest, Seek) {
// Initialize then seek!
InitializePipeline(PIPELINE_OK);
+ AddTextStream();
+ message_loop_.RunUntilIdle();
+
// Every filter should receive a call to Seek().
base::TimeDelta expected = base::TimeDelta::FromSeconds(2000);
ExpectSeek(expected);
@@ -574,6 +660,7 @@ TEST_F(PipelineTest, DisableAudioRendererDuringInit) {
TEST_F(PipelineTest, EndedCallback) {
CreateAudioStream();
CreateVideoStream();
+ CreateTextStream();
MockDemuxerStreamVector streams;
streams.push_back(audio_stream());
streams.push_back(video_stream());
@@ -583,13 +670,18 @@ TEST_F(PipelineTest, EndedCallback) {
InitializeVideoRenderer(video_stream());
InitializePipeline(PIPELINE_OK);
- // The ended callback shouldn't run until both renderers have ended.
+ AddTextStream();
+
+ // The ended callback shouldn't run until all renderers have ended.
pipeline_->OnAudioRendererEnded();
message_loop_.RunUntilIdle();
- EXPECT_CALL(callbacks_, OnEnded());
pipeline_->OnVideoRendererEnded();
message_loop_.RunUntilIdle();
+
+ EXPECT_CALL(callbacks_, OnEnded());
+ text_stream()->SendEosNotification();
+ message_loop_.RunUntilIdle();
}
TEST_F(PipelineTest, AudioStreamShorterThanVideo) {
@@ -923,13 +1015,13 @@ class PipelineTeardownTest : public PipelineTest {
if (state == kInitDemuxer) {
if (stop_or_error == kStop) {
- EXPECT_CALL(*demuxer_, Initialize(_, _))
+ EXPECT_CALL(*demuxer_, Initialize(_, _, _))
.WillOnce(DoAll(Stop(pipeline_.get(), stop_cb),
RunCallback<1>(PIPELINE_OK)));
EXPECT_CALL(callbacks_, OnStop());
} else {
status = DEMUXER_ERROR_COULD_NOT_OPEN;
- EXPECT_CALL(*demuxer_, Initialize(_, _))
+ EXPECT_CALL(*demuxer_, Initialize(_, _, _))
.WillOnce(RunCallback<1>(status));
}
diff --git a/media/base/stream_parser.h b/media/base/stream_parser.h
index 33a336d..101ce4e 100644
--- a/media/base/stream_parser.h
+++ b/media/base/stream_parser.h
@@ -6,6 +6,7 @@
#define MEDIA_BASE_STREAM_PARSER_H_
#include <deque>
+#include <map>
#include <string>
#include "base/callback_forward.h"
@@ -14,18 +15,19 @@
#include "base/time/time.h"
#include "media/base/media_export.h"
#include "media/base/media_log.h"
-#include "media/base/text_track.h"
namespace media {
class AudioDecoderConfig;
class StreamParserBuffer;
+class TextTrackConfig;
class VideoDecoderConfig;
// Abstract interface for parsing media byte streams.
class MEDIA_EXPORT StreamParser {
public:
typedef std::deque<scoped_refptr<StreamParserBuffer> > BufferQueue;
+ typedef std::map<int, TextTrackConfig> TextTrackConfigMap;
StreamParser();
virtual ~StreamParser();
@@ -43,11 +45,14 @@ class MEDIA_EXPORT StreamParser {
// then it means that there isn't an audio stream.
// Second parameter - The new video configuration. If the config is not valid
// then it means that there isn't an audio stream.
+ // Third parameter - The new text tracks configuration. If the map is empty,
+ // then no text tracks were parsed from the stream.
// Return value - True if the new configurations are accepted.
// False if the new configurations are not supported
// and indicates that a parsing error should be signalled.
typedef base::Callback<bool(const AudioDecoderConfig&,
- const VideoDecoderConfig&)> NewConfigCB;
+ const VideoDecoderConfig&,
+ const TextTrackConfigMap&)> NewConfigCB;
// New stream buffers have been parsed.
// First parameter - A queue of newly parsed audio buffers.
@@ -59,12 +64,13 @@ class MEDIA_EXPORT StreamParser {
const BufferQueue&)> NewBuffersCB;
// New stream buffers of inband text have been parsed.
- // First parameter - The text track to which these cues will be added.
+ // First parameter - The id of the text track to which these cues will
+ // be added.
// Second parameter - A queue of newly parsed buffers.
// Return value - True indicates that the buffers are accepted.
// False if something was wrong with the buffers and a parsing
// error should be signalled.
- typedef base::Callback<bool(TextTrack*, const BufferQueue&)> NewTextBuffersCB;
+ typedef base::Callback<bool(int, const BufferQueue&)> NewTextBuffersCB;
// Signals the beginning of a new media segment.
typedef base::Callback<void()> NewMediaSegmentCB;
@@ -85,7 +91,6 @@ class MEDIA_EXPORT StreamParser {
const NewBuffersCB& new_buffers_cb,
const NewTextBuffersCB& text_cb,
const NeedKeyCB& need_key_cb,
- const AddTextTrackCB& add_text_track_cb,
const NewMediaSegmentCB& new_segment_cb,
const base::Closure& end_of_segment_cb,
const LogCB& log_cb) = 0;
diff --git a/media/base/text_cue.cc b/media/base/text_cue.cc
new file mode 100644
index 0000000..3d8a892
--- /dev/null
+++ b/media/base/text_cue.cc
@@ -0,0 +1,23 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "media/base/text_cue.h"
+
+namespace media {
+
+TextCue::TextCue(const base::TimeDelta& timestamp,
+ const base::TimeDelta& duration,
+ const std::string& id,
+ const std::string& settings,
+ const std::string& text)
+ : timestamp_(timestamp),
+ duration_(duration),
+ id_(id),
+ settings_(settings),
+ text_(text) {
+}
+
+TextCue::~TextCue() {}
+
+} // namespace media
diff --git a/media/base/text_cue.h b/media/base/text_cue.h
new file mode 100644
index 0000000..2afae8d
--- /dev/null
+++ b/media/base/text_cue.h
@@ -0,0 +1,48 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef MEDIA_BASE_TEXT_CUE_H_
+#define MEDIA_BASE_TEXT_CUE_H_
+
+#include <string>
+
+#include "base/memory/ref_counted.h"
+#include "base/time/time.h"
+#include "media/base/media_export.h"
+
+namespace media {
+
+// A text buffer to carry the components of a text track cue.
+class MEDIA_EXPORT TextCue
+ : public base::RefCountedThreadSafe<TextCue> {
+ public:
+ TextCue(const base::TimeDelta& timestamp,
+ const base::TimeDelta& duration,
+ const std::string& id,
+ const std::string& settings,
+ const std::string& text);
+
+ // Access to constructor parameters.
+ base::TimeDelta timestamp() const { return timestamp_; }
+ base::TimeDelta duration() const { return duration_; }
+ const std::string& id() const { return id_; }
+ const std::string& settings() const { return settings_; }
+ const std::string& text() const { return text_; }
+
+ private:
+ friend class base::RefCountedThreadSafe<TextCue>;
+ ~TextCue();
+
+ base::TimeDelta timestamp_;
+ base::TimeDelta duration_;
+ std::string id_;
+ std::string settings_;
+ std::string text_;
+
+ DISALLOW_IMPLICIT_CONSTRUCTORS(TextCue);
+};
+
+} // namespace media
+
+#endif // MEDIA_BASE_TEXT_CUE_H_
diff --git a/media/base/text_renderer.cc b/media/base/text_renderer.cc
new file mode 100644
index 0000000..91f9a33
--- /dev/null
+++ b/media/base/text_renderer.cc
@@ -0,0 +1,369 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "media/base/text_renderer.h"
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/logging.h"
+#include "base/message_loop/message_loop_proxy.h"
+#include "base/stl_util.h"
+#include "media/base/bind_to_loop.h"
+#include "media/base/decoder_buffer.h"
+#include "media/base/demuxer.h"
+#include "media/base/demuxer_stream.h"
+#include "media/base/text_cue.h"
+
+namespace media {
+
+TextRenderer::TextRenderer(
+ const scoped_refptr<base::MessageLoopProxy>& message_loop,
+ const AddTextTrackCB& add_text_track_cb)
+ : message_loop_(message_loop),
+ weak_factory_(this),
+ add_text_track_cb_(add_text_track_cb),
+ state_(kUninitialized),
+ pending_read_count_(0) {
+}
+
+TextRenderer::~TextRenderer() {
+ DCHECK(state_ == kUninitialized ||
+ state_ == kStopped) << "state_ " << state_;
+ DCHECK_EQ(pending_read_count_, 0);
+ STLDeleteValues(&text_track_state_map_);
+}
+
+void TextRenderer::Initialize(const base::Closure& ended_cb) {
+ DCHECK(message_loop_->BelongsToCurrentThread());
+ DCHECK(!ended_cb.is_null());
+ DCHECK_EQ(kUninitialized, state_) << "state_ " << state_;
+ DCHECK(text_track_state_map_.empty());
+ DCHECK_EQ(pending_read_count_, 0);
+ DCHECK(pending_eos_set_.empty());
+ DCHECK(ended_cb_.is_null());
+
+ weak_this_ = weak_factory_.GetWeakPtr();
+ ended_cb_ = ended_cb;
+ state_ = kPaused;
+}
+
+void TextRenderer::Play(const base::Closure& callback) {
+ DCHECK(message_loop_->BelongsToCurrentThread());
+ DCHECK_EQ(state_, kPaused) << "state_ " << state_;
+
+ for (TextTrackStateMap::iterator itr = text_track_state_map_.begin();
+ itr != text_track_state_map_.end(); ++itr) {
+ TextTrackState* state = itr->second;
+ if (state->read_state == TextTrackState::kReadPending) {
+ DCHECK_GT(pending_read_count_, 0);
+ continue;
+ }
+
+ Read(state, itr->first);
+ }
+
+ state_ = kPlaying;
+ callback.Run();
+}
+
+void TextRenderer::Pause(const base::Closure& callback) {
+ DCHECK(message_loop_->BelongsToCurrentThread());
+ DCHECK(state_ == kPlaying || state_ == kEnded) << "state_ " << state_;
+ DCHECK_GE(pending_read_count_, 0);
+ pause_cb_ = callback;
+
+ if (pending_read_count_ == 0) {
+ state_ = kPaused;
+ base::ResetAndReturn(&pause_cb_).Run();
+ return;
+ }
+
+ state_ = kPausePending;
+}
+
+void TextRenderer::Flush(const base::Closure& callback) {
+ DCHECK(message_loop_->BelongsToCurrentThread());
+ DCHECK_EQ(pending_read_count_, 0);
+ DCHECK(state_ == kPaused) << "state_ " << state_;
+
+ for (TextTrackStateMap::iterator itr = text_track_state_map_.begin();
+ itr != text_track_state_map_.end(); ++itr) {
+ pending_eos_set_.insert(itr->first);
+ }
+ DCHECK_EQ(pending_eos_set_.size(), text_track_state_map_.size());
+ callback.Run();
+}
+
+void TextRenderer::Stop(const base::Closure& cb) {
+ DCHECK(message_loop_->BelongsToCurrentThread());
+ DCHECK(!cb.is_null());
+ DCHECK(state_ == kPlaying ||
+ state_ == kPausePending ||
+ state_ == kPaused ||
+ state_ == kEnded) << "state_ " << state_;
+ DCHECK_GE(pending_read_count_, 0);
+
+ stop_cb_ = cb;
+
+ if (pending_read_count_ == 0) {
+ state_ = kStopped;
+ base::ResetAndReturn(&stop_cb_).Run();
+ return;
+ }
+
+ state_ = kStopPending;
+}
+
+void TextRenderer::AddTextStream(DemuxerStream* text_stream,
+ const TextTrackConfig& config) {
+ DCHECK(message_loop_->BelongsToCurrentThread());
+ DCHECK(state_ != kUninitialized) << "state_ " << state_;
+ DCHECK_NE(state_, kStopPending);
+ DCHECK_NE(state_, kStopped);
+ DCHECK(text_track_state_map_.find(text_stream) ==
+ text_track_state_map_.end());
+ DCHECK(pending_eos_set_.find(text_stream) ==
+ pending_eos_set_.end());
+
+ media::AddTextTrackDoneCB done_cb =
+ media::BindToLoop(message_loop_,
+ base::Bind(&TextRenderer::OnAddTextTrackDone,
+ weak_this_,
+ text_stream));
+
+ add_text_track_cb_.Run(config, done_cb);
+}
+
+void TextRenderer::RemoveTextStream(DemuxerStream* text_stream) {
+ DCHECK(message_loop_->BelongsToCurrentThread());
+
+ TextTrackStateMap::iterator itr = text_track_state_map_.find(text_stream);
+ DCHECK(itr != text_track_state_map_.end());
+
+ TextTrackState* state = itr->second;
+ DCHECK_EQ(state->read_state, TextTrackState::kReadIdle);
+ delete state;
+ text_track_state_map_.erase(itr);
+
+ pending_eos_set_.erase(text_stream);
+}
+
+bool TextRenderer::HasTracks() const {
+ DCHECK(message_loop_->BelongsToCurrentThread());
+ return !text_track_state_map_.empty();
+}
+
+void TextRenderer::BufferReady(
+ DemuxerStream* stream,
+ DemuxerStream::Status status,
+ const scoped_refptr<DecoderBuffer>& input) {
+ DCHECK(message_loop_->BelongsToCurrentThread());
+ DCHECK_NE(status, DemuxerStream::kConfigChanged);
+
+ if (status == DemuxerStream::kAborted) {
+ DCHECK(!input);
+ DCHECK_GT(pending_read_count_, 0);
+ DCHECK(pending_eos_set_.find(stream) != pending_eos_set_.end());
+
+ TextTrackStateMap::iterator itr = text_track_state_map_.find(stream);
+ DCHECK(itr != text_track_state_map_.end());
+
+ TextTrackState* state = itr->second;
+ DCHECK_EQ(state->read_state, TextTrackState::kReadPending);
+
+ --pending_read_count_;
+ state->read_state = TextTrackState::kReadIdle;
+
+ switch (state_) {
+ case kPlaying:
+ return;
+
+ case kPausePending:
+ if (pending_read_count_ == 0) {
+ state_ = kPaused;
+ base::ResetAndReturn(&pause_cb_).Run();
+ }
+
+ return;
+
+ case kStopPending:
+ if (pending_read_count_ == 0) {
+ state_ = kStopped;
+ base::ResetAndReturn(&stop_cb_).Run();
+ }
+
+ return;
+
+ case kPaused:
+ case kStopped:
+ case kUninitialized:
+ case kEnded:
+ NOTREACHED();
+ return;
+ }
+
+ NOTREACHED();
+ return;
+ }
+
+ if (input->end_of_stream()) {
+ CueReady(stream, NULL);
+ return;
+ }
+
+ DCHECK_EQ(status, DemuxerStream::kOk);
+ DCHECK_GE(input->side_data_size(), 2);
+
+ // The side data contains both the cue id and cue settings,
+ // each terminated with a NUL.
+ const char* id_ptr = reinterpret_cast<const char*>(input->side_data());
+ size_t id_len = strlen(id_ptr);
+ std::string id(id_ptr, id_len);
+
+ const char* settings_ptr = id_ptr + id_len + 1;
+ size_t settings_len = strlen(settings_ptr);
+ std::string settings(settings_ptr, settings_len);
+
+ // The cue payload is stored in the data-part of the input buffer.
+ std::string text(input->data(), input->data() + input->data_size());
+
+ scoped_refptr<TextCue> text_cue(
+ new TextCue(input->timestamp(),
+ input->duration(),
+ id,
+ settings,
+ text));
+
+ CueReady(stream, text_cue);
+}
+
+void TextRenderer::CueReady(
+ DemuxerStream* text_stream,
+ const scoped_refptr<TextCue>& text_cue) {
+ DCHECK(message_loop_->BelongsToCurrentThread());
+ DCHECK(state_ != kUninitialized &&
+ state_ != kStopped) << "state_ " << state_;
+ DCHECK_GT(pending_read_count_, 0);
+ DCHECK(pending_eos_set_.find(text_stream) != pending_eos_set_.end());
+
+ TextTrackStateMap::iterator itr = text_track_state_map_.find(text_stream);
+ DCHECK(itr != text_track_state_map_.end());
+
+ TextTrackState* state = itr->second;
+ DCHECK_EQ(state->read_state, TextTrackState::kReadPending);
+ DCHECK(state->text_track);
+
+ --pending_read_count_;
+ state->read_state = TextTrackState::kReadIdle;
+
+ switch (state_) {
+ case kPlaying: {
+ if (text_cue)
+ break;
+
+ const size_t count = pending_eos_set_.erase(text_stream);
+ DCHECK_EQ(count, 1U);
+
+ if (pending_eos_set_.empty()) {
+ DCHECK_EQ(pending_read_count_, 0);
+ state_ = kEnded;
+ ended_cb_.Run();
+ return;
+ }
+
+ DCHECK_GT(pending_read_count_, 0);
+ return;
+ }
+ case kPausePending: {
+ if (text_cue)
+ break;
+
+ const size_t count = pending_eos_set_.erase(text_stream);
+ DCHECK_EQ(count, 1U);
+
+ if (pending_read_count_ > 0) {
+ DCHECK(!pending_eos_set_.empty());
+ return;
+ }
+
+ state_ = kPaused;
+ base::ResetAndReturn(&pause_cb_).Run();
+
+ return;
+ }
+ case kStopPending:
+ if (pending_read_count_ == 0) {
+ state_ = kStopped;
+ base::ResetAndReturn(&stop_cb_).Run();
+ }
+
+ return;
+
+ case kPaused:
+ case kStopped:
+ case kUninitialized:
+ case kEnded:
+ NOTREACHED();
+ return;
+ }
+
+ base::TimeDelta start = text_cue->timestamp();
+ base::TimeDelta end = start + text_cue->duration();
+
+ state->text_track->addWebVTTCue(start, end,
+ text_cue->id(),
+ text_cue->text(),
+ text_cue->settings());
+
+ if (state_ == kPlaying) {
+ Read(state, text_stream);
+ return;
+ }
+
+ if (pending_read_count_ == 0) {
+ DCHECK_EQ(state_, kPausePending) << "state_ " << state_;
+ state_ = kPaused;
+ base::ResetAndReturn(&pause_cb_).Run();
+ }
+}
+
+void TextRenderer::OnAddTextTrackDone(DemuxerStream* text_stream,
+ scoped_ptr<TextTrack> text_track) {
+ DCHECK(message_loop_->BelongsToCurrentThread());
+ DCHECK(state_ != kUninitialized &&
+ state_ != kStopped &&
+ state_ != kStopPending) << "state_ " << state_;
+ DCHECK(text_stream);
+ DCHECK(text_track);
+
+ scoped_ptr<TextTrackState> state(new TextTrackState(text_track.Pass()));
+ text_track_state_map_[text_stream] = state.release();
+ pending_eos_set_.insert(text_stream);
+
+ if (state_ == kPlaying)
+ Read(text_track_state_map_[text_stream], text_stream);
+}
+
+void TextRenderer::Read(
+ TextTrackState* state,
+ DemuxerStream* text_stream) {
+ DCHECK_NE(state->read_state, TextTrackState::kReadPending);
+
+ state->read_state = TextTrackState::kReadPending;
+ ++pending_read_count_;
+
+ text_stream->Read(base::Bind(&TextRenderer::BufferReady,
+ weak_this_,
+ text_stream));
+}
+
+TextRenderer::TextTrackState::TextTrackState(scoped_ptr<TextTrack> tt)
+ : read_state(kReadIdle),
+ text_track(tt.Pass()) {
+}
+
+TextRenderer::TextTrackState::~TextTrackState() {
+}
+
+} // namespace media
diff --git a/media/base/text_renderer.h b/media/base/text_renderer.h
new file mode 100644
index 0000000..532a1fa
--- /dev/null
+++ b/media/base/text_renderer.h
@@ -0,0 +1,145 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef MEDIA_BASE_TEXT_RENDERER_H_
+#define MEDIA_BASE_TEXT_RENDERER_H_
+
+#include <map>
+#include <set>
+
+#include "base/callback.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/memory/weak_ptr.h"
+#include "media/base/demuxer_stream.h"
+#include "media/base/media_export.h"
+#include "media/base/pipeline_status.h"
+#include "media/base/text_track.h"
+
+namespace base {
+class MessageLoopProxy;
+}
+
+namespace media {
+
+class TextCue;
+class TextTrackConfig;
+
+// Receives decoder buffers from the upstream demuxer, decodes them to text
+// cues, and then passes them onto the TextTrack object associated with each
+// demuxer text stream.
+class MEDIA_EXPORT TextRenderer {
+ public:
+ // |message_loop| is the thread on which TextRenderer will execute.
+ //
+ // |add_text_track_cb] is called when the demuxer requests (via its host)
+ // that a new text track be created.
+ TextRenderer(const scoped_refptr<base::MessageLoopProxy>& message_loop,
+ const AddTextTrackCB& add_text_track_cb);
+ ~TextRenderer();
+
+ // |ended_cb| is executed when all of the text tracks have reached
+ // end of stream, following a play request.
+ void Initialize(const base::Closure& ended_cb);
+
+ // Start text track cue decoding and rendering, executing |callback| when
+ // playback is underway.
+ void Play(const base::Closure& callback);
+
+ // Temporarily suspend decoding and rendering, executing |callback| when
+ // playback has been suspended.
+ void Pause(const base::Closure& callback);
+
+ // Discard any text data, executing |callback| when completed.
+ void Flush(const base::Closure& callback);
+
+ // Stop all operations in preparation for being deleted, executing |callback|
+ // when complete.
+ void Stop(const base::Closure& callback);
+
+ // Add new |text_stream|, having the indicated |config|, to the text stream
+ // collection managed by this text renderer.
+ void AddTextStream(DemuxerStream* text_stream,
+ const TextTrackConfig& config);
+
+ // Remove |text_stream| from the text stream collection.
+ void RemoveTextStream(DemuxerStream* text_stream);
+
+ // Returns true if there are extant text tracks.
+ bool HasTracks() const;
+
+ private:
+ struct TextTrackState {
+ // To determine read progress.
+ enum ReadState {
+ kReadIdle,
+ kReadPending
+ };
+
+ explicit TextTrackState(scoped_ptr<TextTrack> text_track);
+ ~TextTrackState();
+
+ ReadState read_state;
+ scoped_ptr<TextTrack> text_track;
+ };
+
+ // Callback delivered by the demuxer |text_stream| when
+ // a read from the stream completes.
+ void BufferReady(DemuxerStream* text_stream,
+ DemuxerStream::Status status,
+ const scoped_refptr<DecoderBuffer>& input);
+
+ // Dispatches the decoded cue delivered on the demuxer's |text_stream|.
+ void CueReady(DemuxerStream* text_stream,
+ const scoped_refptr<TextCue>& text_cue);
+
+ // Dispatched when the AddTextTrackCB completes, after having created
+ // the TextTrack object associated with |text_stream|.
+ void OnAddTextTrackDone(DemuxerStream* text_stream,
+ scoped_ptr<TextTrack> text_track);
+
+ // Utility function to post a read request on |text_stream|.
+ void Read(TextTrackState* state, DemuxerStream* text_stream);
+
+ scoped_refptr<base::MessageLoopProxy> message_loop_;
+ base::WeakPtrFactory<TextRenderer> weak_factory_;
+ base::WeakPtr<TextRenderer> weak_this_;
+ const AddTextTrackCB add_text_track_cb_;
+
+ // Callbacks provided during Initialize().
+ base::Closure ended_cb_;
+
+ // Callback provided to Pause().
+ base::Closure pause_cb_;
+
+ // Callback provided to Stop().
+ base::Closure stop_cb_;
+
+ // Simple state tracking variable.
+ enum State {
+ kUninitialized,
+ kPausePending,
+ kPaused,
+ kPlaying,
+ kEnded,
+ kStopPending,
+ kStopped
+ };
+ State state_;
+
+ typedef std::map<DemuxerStream*, TextTrackState*> TextTrackStateMap;
+ TextTrackStateMap text_track_state_map_;
+
+ // Indicates how many read requests are in flight.
+ int pending_read_count_;
+
+ // Indicates which text streams have not delivered end-of-stream yet.
+ typedef std::set<DemuxerStream*> PendingEosSet;
+ PendingEosSet pending_eos_set_;
+
+ DISALLOW_IMPLICIT_CONSTRUCTORS(TextRenderer);
+};
+
+} // namespace media
+
+#endif // MEDIA_BASE_TEXT_RENDERER_H_
diff --git a/media/base/text_renderer_unittest.cc b/media/base/text_renderer_unittest.cc
new file mode 100644
index 0000000..0188763
--- /dev/null
+++ b/media/base/text_renderer_unittest.cc
@@ -0,0 +1,1382 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <vector>
+
+#include "base/bind.h"
+#include "base/callback_helpers.h"
+#include "base/memory/scoped_vector.h"
+#include "base/message_loop/message_loop.h"
+#include "media/base/audio_decoder_config.h"
+#include "media/base/decoder_buffer.h"
+#include "media/base/demuxer_stream.h"
+#include "media/base/fake_text_track_stream.h"
+#include "media/base/text_renderer.h"
+#include "media/base/text_track_config.h"
+#include "media/base/video_decoder_config.h"
+#include "media/filters/webvtt_util.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using ::testing::Eq;
+using ::testing::Exactly;
+using ::testing::Invoke;
+using ::testing::_;
+
+namespace media {
+
+// Local implementation of the TextTrack interface.
+class FakeTextTrack : public TextTrack {
+ public:
+ FakeTextTrack(const base::Closure& destroy_cb,
+ const TextTrackConfig& config)
+ : destroy_cb_(destroy_cb),
+ config_(config) {
+ }
+ virtual ~FakeTextTrack() {
+ destroy_cb_.Run();
+ }
+
+ MOCK_METHOD5(addWebVTTCue, void(const base::TimeDelta& start,
+ const base::TimeDelta& end,
+ const std::string& id,
+ const std::string& content,
+ const std::string& settings));
+
+ const base::Closure destroy_cb_;
+ const TextTrackConfig config_;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(FakeTextTrack);
+};
+
+class TextRendererTest : public testing::Test {
+ public:
+ TextRendererTest() {}
+
+ void CreateTextRenderer() {
+ DCHECK(!text_renderer_);
+
+ text_renderer_.reset(
+ new TextRenderer(message_loop_.message_loop_proxy(),
+ base::Bind(&TextRendererTest::OnAddTextTrack,
+ base::Unretained(this))));
+ text_renderer_->Initialize(base::Bind(&TextRendererTest::OnEnd,
+ base::Unretained(this)));
+ }
+
+ void DestroyTextRenderer() {
+ EXPECT_CALL(*this, OnStop());
+ text_renderer_->Stop(base::Bind(&TextRendererTest::OnStop,
+ base::Unretained(this)));
+ message_loop_.RunUntilIdle();
+
+ text_renderer_.reset();
+ text_track_streams_.clear();
+ }
+
+ void AddTextTrack(TextKind kind,
+ const std::string& name,
+ const std::string& language,
+ bool expect_read) {
+ const size_t idx = text_track_streams_.size();
+ text_track_streams_.push_back(new FakeTextTrackStream);
+
+ if (expect_read)
+ ExpectRead(idx);
+
+ const TextTrackConfig config(kind, name, language);
+ text_renderer_->AddTextStream(text_track_streams_.back(), config);
+ message_loop_.RunUntilIdle();
+
+ EXPECT_EQ(text_tracks_.size(), text_track_streams_.size());
+ FakeTextTrack* const text_track = text_tracks_.back();
+ EXPECT_TRUE(text_track);
+ EXPECT_TRUE(text_track->config_.Matches(config));
+ }
+
+ void OnAddTextTrack(const TextTrackConfig& config,
+ const AddTextTrackDoneCB& done_cb) {
+ base::Closure destroy_cb =
+ base::Bind(&TextRendererTest::OnDestroyTextTrack,
+ base::Unretained(this),
+ text_tracks_.size());
+ // Text track objects are owned by the text renderer, but we cache them
+ // here so we can inspect them. They get removed from our cache when the
+ // text renderer deallocates them.
+ text_tracks_.push_back(new FakeTextTrack(destroy_cb, config));
+ scoped_ptr<TextTrack> text_track(text_tracks_.back());
+ done_cb.Run(text_track.Pass());
+ }
+
+ void RemoveTextTrack(unsigned idx) {
+ FakeTextTrackStream* const stream = text_track_streams_[idx];
+ text_renderer_->RemoveTextStream(stream);
+ EXPECT_FALSE(text_tracks_[idx]);
+ }
+
+ void SatisfyPendingReads(const base::TimeDelta& start,
+ const base::TimeDelta& duration,
+ const std::string& id,
+ const std::string& content,
+ const std::string& settings) {
+ for (TextTrackStreams::iterator itr = text_track_streams_.begin();
+ itr != text_track_streams_.end(); ++itr) {
+ (*itr)->SatisfyPendingRead(start, duration, id, content, settings);
+ }
+ }
+
+ void AbortPendingRead(unsigned idx) {
+ FakeTextTrackStream* const stream = text_track_streams_[idx];
+ stream->AbortPendingRead();
+ message_loop_.RunUntilIdle();
+ }
+
+ void AbortPendingReads() {
+ for (size_t idx = 0; idx < text_track_streams_.size(); ++idx) {
+ AbortPendingRead(idx);
+ }
+ }
+
+ void SendEosNotification(unsigned idx) {
+ FakeTextTrackStream* const stream = text_track_streams_[idx];
+ stream->SendEosNotification();
+ message_loop_.RunUntilIdle();
+ }
+
+ void SendEosNotifications() {
+ for (size_t idx = 0; idx < text_track_streams_.size(); ++idx) {
+ SendEosNotification(idx);
+ }
+ }
+
+ void SendCue(unsigned idx, bool expect_cue) {
+ FakeTextTrackStream* const text_stream = text_track_streams_[idx];
+
+ const base::TimeDelta start;
+ const base::TimeDelta duration = base::TimeDelta::FromSeconds(42);
+ const std::string id = "id";
+ const std::string content = "subtitle";
+ const std::string settings;
+
+ if (expect_cue) {
+ FakeTextTrack* const text_track = text_tracks_[idx];
+ EXPECT_CALL(*text_track, addWebVTTCue(start,
+ start + duration,
+ id,
+ content,
+ settings));
+ }
+
+ text_stream->SatisfyPendingRead(start, duration, id, content, settings);
+ message_loop_.RunUntilIdle();
+ }
+
+ void SendCues(bool expect_cue) {
+ for (size_t idx = 0; idx < text_track_streams_.size(); ++idx) {
+ SendCue(idx, expect_cue);
+ }
+ }
+
+ void OnDestroyTextTrack(unsigned idx) {
+ text_tracks_[idx] = NULL;
+ }
+
+ void Play() {
+ EXPECT_CALL(*this, OnPlay());
+ text_renderer_->Play(base::Bind(&TextRendererTest::OnPlay,
+ base::Unretained(this)));
+ message_loop_.RunUntilIdle();
+ }
+
+ void Pause() {
+ text_renderer_->Pause(base::Bind(&TextRendererTest::OnPause,
+ base::Unretained(this)));
+ message_loop_.RunUntilIdle();
+ }
+
+ void Flush() {
+ EXPECT_CALL(*this, OnFlush());
+ text_renderer_->Flush(base::Bind(&TextRendererTest::OnFlush,
+ base::Unretained(this)));
+ }
+
+ void Stop() {
+ text_renderer_->Stop(base::Bind(&TextRendererTest::OnStop,
+ base::Unretained(this)));
+ message_loop_.RunUntilIdle();
+ }
+
+ void ExpectRead(size_t idx) {
+ FakeTextTrackStream* const stream = text_track_streams_[idx];
+ EXPECT_CALL(*stream, OnRead());
+ }
+
+ MOCK_METHOD0(OnEnd, void());
+ MOCK_METHOD0(OnStop, void());
+ MOCK_METHOD0(OnPlay, void());
+ MOCK_METHOD0(OnPause, void());
+ MOCK_METHOD0(OnFlush, void());
+
+ scoped_ptr<TextRenderer> text_renderer_;
+ base::MessageLoop message_loop_;
+
+ typedef ScopedVector<FakeTextTrackStream> TextTrackStreams;
+ TextTrackStreams text_track_streams_;
+
+ typedef std::vector<FakeTextTrack*> TextTracks;
+ TextTracks text_tracks_;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(TextRendererTest);
+};
+
+TEST_F(TextRendererTest, CreateTextRendererNoInit) {
+ text_renderer_.reset(
+ new TextRenderer(message_loop_.message_loop_proxy(),
+ base::Bind(&TextRendererTest::OnAddTextTrack,
+ base::Unretained(this))));
+ text_renderer_.reset();
+}
+
+TEST_F(TextRendererTest, TestStop) {
+ CreateTextRenderer();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, AddTextTrackOnly_OneTrack) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", false);
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, AddTextTrackOnly_TwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "track 1", "", false);
+ AddTextTrack(kTextSubtitles, "track 2", "", false);
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayOnly) {
+ CreateTextRenderer();
+ Play();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, AddTrackBeforePlay_OneTrack) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ AbortPendingReads();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, AddTrackBeforePlay_TwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ AbortPendingReads();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, AddTrackAfterPlay_OneTrackAfter) {
+ CreateTextRenderer();
+ Play();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ AbortPendingReads();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, AddTrackAfterPlay_TwoTracksAfter) {
+ CreateTextRenderer();
+ Play();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ AbortPendingReads();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, AddTrackAfterPlay_OneTrackBeforeOneTrackAfter) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ Play();
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ AbortPendingReads();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayAddCue_OneTrack) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ ExpectRead(0);
+ SendCues(true);
+ AbortPendingReads();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayAddCue_TwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ ExpectRead(0);
+ ExpectRead(1);
+ SendCues(true);
+ AbortPendingReads();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosOnly_OneTrack) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotifications();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosOnly_TwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotifications();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayCueEos_OneTrack) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ ExpectRead(0);
+ SendCues(true);
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotifications();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayCueEos_TwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ ExpectRead(0);
+ ExpectRead(1);
+ SendCues(true);
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotifications();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, StopPending_OneTrack) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ Stop();
+ EXPECT_CALL(*this, OnStop());
+ SendEosNotifications();
+ text_renderer_.reset();
+ text_track_streams_.clear();
+}
+
+TEST_F(TextRendererTest, StopPending_TwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ Stop();
+ EXPECT_CALL(*this, OnStop());
+ SendEosNotifications();
+ text_renderer_.reset();
+ text_track_streams_.clear();
+}
+
+TEST_F(TextRendererTest, PlayPause_OneTrack) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ AbortPendingReads();
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayPause_TwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ AbortPendingReads();
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosPausePending_OneTrack) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ Pause();
+ EXPECT_CALL(*this, OnPause());
+ SendEosNotifications();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosPausePending_TwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ Pause();
+ EXPECT_CALL(*this, OnPause());
+ SendEosNotifications();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayCuePausePending_OneTrack) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ Pause();
+ EXPECT_CALL(*this, OnPause());
+ SendCues(true);
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayCuePausePending_TwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ Pause();
+ EXPECT_CALL(*this, OnPause());
+ SendCues(true);
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosPause_OneTrack) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotifications();
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosPause_TwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotifications();
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosPause_SplitEos) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ SendEosNotification(0);
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotification(1);
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosFlush_OneTrack) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotifications();
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ Flush();
+ ExpectRead(0);
+ Play();
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotifications();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosFlush_TwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotifications();
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ Flush();
+ ExpectRead(0);
+ ExpectRead(1);
+ Play();
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotifications();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, AddTextTrackOnlyRemove_OneTrack) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", false);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, AddTextTrackOnlyRemove_TwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "track 1", "", false);
+ AddTextTrack(kTextSubtitles, "track 2", "", false);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, AddTrackBeforePlayRemove_OneTrack) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ AbortPendingReads();
+ RemoveTextTrack(0);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, AddTrackBeforePlayRemove_TwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ AbortPendingReads();
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, AddTrackBeforePlayRemove_SeparateCancel) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ AbortPendingRead(0);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ AbortPendingRead(1);
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, AddTrackBeforePlayRemove_RemoveOneThenPlay) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", false);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ Play();
+ AbortPendingRead(1);
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, AddTrackBeforePlayRemove_RemoveTwoThenPlay) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", false);
+ AddTextTrack(kTextSubtitles, "2", "", false);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ Play();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, AddTrackAfterPlayRemove_OneTrack) {
+ CreateTextRenderer();
+ Play();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ AbortPendingReads();
+ RemoveTextTrack(0);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, AddTrackAfterPlayRemove_TwoTracks) {
+ CreateTextRenderer();
+ Play();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ AbortPendingReads();
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, AddTrackAfterPlayRemove_SplitCancel) {
+ CreateTextRenderer();
+ Play();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ AbortPendingRead(0);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ AbortPendingRead(1);
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, AddTrackAfterPlayRemove_SplitAdd) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ Play();
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ AbortPendingRead(0);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ AbortPendingRead(1);
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayAddCueRemove_OneTrack) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ ExpectRead(0);
+ SendCues(true);
+ AbortPendingReads();
+ RemoveTextTrack(0);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayAddCueRemove_TwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ ExpectRead(0);
+ ExpectRead(1);
+ SendCues(true);
+ AbortPendingRead(0);
+ AbortPendingRead(1);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosOnlyRemove_OneTrack) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotifications();
+ RemoveTextTrack(0);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosOnlyRemove_TwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotifications();
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayCueEosRemove_OneTrack) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ ExpectRead(0);
+ SendCues(true);
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotifications();
+ RemoveTextTrack(0);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayCueEosRemove_TwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ ExpectRead(0);
+ ExpectRead(1);
+ SendCues(true);
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotifications();
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, TestStopPendingRemove_OneTrack) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ Stop();
+ EXPECT_CALL(*this, OnStop());
+ SendEosNotifications();
+ RemoveTextTrack(0);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ text_renderer_.reset();
+ text_track_streams_.clear();
+}
+
+TEST_F(TextRendererTest, TestStopPendingRemove_TwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ Stop();
+ SendEosNotification(0);
+ EXPECT_CALL(*this, OnStop());
+ SendEosNotification(1);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ text_renderer_.reset();
+ text_track_streams_.clear();
+}
+
+TEST_F(TextRendererTest, TestStopPendingRemove_RemoveThenSendEos) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ Stop();
+ SendEosNotification(0);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ EXPECT_CALL(*this, OnStop());
+ SendEosNotification(1);
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ text_renderer_.reset();
+ text_track_streams_.clear();
+}
+
+TEST_F(TextRendererTest, PlayPauseRemove_PauseThenRemove) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ AbortPendingReads();
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ RemoveTextTrack(0);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayPauseRemove_RemoveThanPause) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ AbortPendingReads();
+ RemoveTextTrack(0);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayPause_PauseThenRemoveTwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ AbortPendingReads();
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayPauseRemove_RemoveThenPauseTwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ AbortPendingReads();
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayPauseRemove_SplitCancel) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ AbortPendingRead(0);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ AbortPendingRead(1);
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+
+TEST_F(TextRendererTest, PlayPauseRemove_PauseLast) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ AbortPendingRead(0);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ AbortPendingRead(1);
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosPausePendingRemove_OneTrack) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ Pause();
+ EXPECT_CALL(*this, OnPause());
+ SendEosNotifications();
+ RemoveTextTrack(0);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosPausePendingRemove_TwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ Pause();
+ SendEosNotification(0);
+ EXPECT_CALL(*this, OnPause());
+ SendEosNotification(1);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosPausePendingRemove_SplitEos) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ Pause();
+ SendEosNotification(0);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ EXPECT_CALL(*this, OnPause());
+ SendEosNotification(1);
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayCuePausePendingRemove_OneTrack) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ Pause();
+ EXPECT_CALL(*this, OnPause());
+ SendCues(true);
+ RemoveTextTrack(0);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayCuePausePendingRemove_TwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ Pause();
+ SendCue(0, true);
+ EXPECT_CALL(*this, OnPause());
+ SendCue(1, true);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayCuePausePendingRemove_SplitSendCue) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ Pause();
+ SendCue(0, true);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ EXPECT_CALL(*this, OnPause());
+ SendCue(1, true);
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosPauseRemove_PauseThenRemove) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotifications();
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ RemoveTextTrack(0);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosPauseRemove_RemoveThenPause) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotifications();
+ RemoveTextTrack(0);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosPause_PauseThenRemoveTwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ SendEosNotification(0);
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotification(1);
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosPause_RemovePauseRemove) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ SendEosNotification(0);
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotification(1);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosPause_EosThenPause) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ SendEosNotification(0);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotification(1);
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosPause_PauseLast) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ SendEosNotification(0);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotification(1);
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosPause_EosPauseRemove) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ SendEosNotification(0);
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotification(1);
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosPause_EosRemovePause) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ SendEosNotification(0);
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotification(1);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosPause_EosRemoveEosPause) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ SendEosNotification(0);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotification(1);
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosPause_EosRemoveEosRemovePause) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ SendEosNotification(0);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotification(1);
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosFlushRemove_OneTrack) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotifications();
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ Flush();
+ ExpectRead(0);
+ Play();
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotifications();
+ RemoveTextTrack(0);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosFlushRemove_TwoTracks) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotifications();
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ Flush();
+ ExpectRead(0);
+ ExpectRead(1);
+ Play();
+ SendEosNotification(0);
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotification(1);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayEosFlushRemove_EosRemove) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotifications();
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ Flush();
+ ExpectRead(0);
+ ExpectRead(1);
+ Play();
+ SendEosNotification(0);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotification(1);
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayShort_SendCueThenEos) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ Pause();
+ SendCue(0, true);
+ EXPECT_CALL(*this, OnPause());
+ SendEosNotification(1);
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayShort_EosThenSendCue) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ Pause();
+ SendEosNotification(0);
+ EXPECT_CALL(*this, OnPause());
+ SendCue(1, true);
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayShortRemove_SendEosRemove) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ Pause();
+ SendCue(0, true);
+ EXPECT_CALL(*this, OnPause());
+ SendEosNotification(1);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayShortRemove_SendRemoveEos) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ Pause();
+ SendCue(0, true);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ EXPECT_CALL(*this, OnPause());
+ SendEosNotification(1);
+ RemoveTextTrack(1);
+ EXPECT_FALSE(text_renderer_->HasTracks());
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayCuePausePendingCancel_OneTrack) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ Pause();
+ EXPECT_CALL(*this, OnPause());
+ AbortPendingRead(0);
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayCuePausePendingCancel_SendThenCancel) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ Pause();
+ SendCue(0, true);
+ EXPECT_CALL(*this, OnPause());
+ AbortPendingRead(1);
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayCuePausePendingCancel_CancelThenSend) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ Pause();
+ AbortPendingRead(0);
+ EXPECT_CALL(*this, OnPause());
+ SendCue(1, true);
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, PlayCueStopPendingCancel_OneTrack) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ Pause();
+ Stop();
+ EXPECT_CALL(*this, OnStop());
+ AbortPendingRead(0);
+ text_renderer_.reset();
+ text_track_streams_.clear();
+}
+
+TEST_F(TextRendererTest, PlayCueStopPendingCancel_SendThenCancel) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ Pause();
+ Stop();
+ SendCue(0, false);
+ EXPECT_CALL(*this, OnStop());
+ AbortPendingRead(1);
+ text_renderer_.reset();
+ text_track_streams_.clear();
+}
+
+TEST_F(TextRendererTest, PlayCueStopPendingCancel_CancelThenSend) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ Pause();
+ Stop();
+ AbortPendingRead(0);
+ EXPECT_CALL(*this, OnStop());
+ SendCue(1, false);
+ text_renderer_.reset();
+ text_track_streams_.clear();
+}
+
+TEST_F(TextRendererTest, AddRemoveAdd) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ AbortPendingRead(0);
+ RemoveTextTrack(0);
+ EXPECT_CALL(*this, OnPause());
+ Pause();
+ AddTextTrack(kTextSubtitles, "", "", true);
+ Play();
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotification(1);
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, AddRemoveEos) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ AbortPendingRead(0);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ EXPECT_CALL(*this, OnEnd());
+ SendEosNotification(1);
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, AddRemovePause) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ AbortPendingRead(0);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ Pause();
+ EXPECT_CALL(*this, OnPause());
+ SendEosNotification(1);
+ DestroyTextRenderer();
+}
+
+TEST_F(TextRendererTest, AddRemovePauseStop) {
+ CreateTextRenderer();
+ AddTextTrack(kTextSubtitles, "1", "", true);
+ AddTextTrack(kTextSubtitles, "2", "", true);
+ Play();
+ AbortPendingRead(0);
+ RemoveTextTrack(0);
+ EXPECT_TRUE(text_renderer_->HasTracks());
+ Pause();
+ Stop();
+ EXPECT_CALL(*this, OnStop());
+ SendEosNotification(1);
+ text_renderer_.reset();
+ text_track_streams_.clear();
+}
+
+} // namespace media
diff --git a/media/base/text_track.h b/media/base/text_track.h
index 01a2ed7..0e04a0e 100644
--- a/media/base/text_track.h
+++ b/media/base/text_track.h
@@ -13,14 +13,7 @@
namespace media {
-// Specifies the varieties of text tracks.
-enum TextKind {
- kTextSubtitles,
- kTextCaptions,
- kTextDescriptions,
- kTextMetadata,
- kTextNone
-};
+class TextTrackConfig;
class TextTrack {
public:
@@ -32,10 +25,12 @@ class TextTrack {
const std::string& settings) = 0;
};
-typedef base::Callback<scoped_ptr<TextTrack>
- (TextKind kind,
- const std::string& label,
- const std::string& language)> AddTextTrackCB;
+typedef base::Callback<void
+ (scoped_ptr<TextTrack>)> AddTextTrackDoneCB;
+
+typedef base::Callback<void
+ (const TextTrackConfig& config,
+ const AddTextTrackDoneCB& done_cb)> AddTextTrackCB;
} // namespace media
diff --git a/media/base/text_track_config.cc b/media/base/text_track_config.cc
new file mode 100644
index 0000000..2d30fc6
--- /dev/null
+++ b/media/base/text_track_config.cc
@@ -0,0 +1,27 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "media/base/text_track_config.h"
+
+namespace media {
+
+TextTrackConfig::TextTrackConfig()
+ : kind_(kTextNone) {
+}
+
+TextTrackConfig::TextTrackConfig(TextKind kind,
+ const std::string& label,
+ const std::string& language)
+ : kind_(kind),
+ label_(label),
+ language_(language) {
+}
+
+bool TextTrackConfig::Matches(const TextTrackConfig& config) const {
+ return config.kind() == kind_ &&
+ config.label() == label_ &&
+ config.language() == language_;
+}
+
+} // namespace media
diff --git a/media/base/text_track_config.h b/media/base/text_track_config.h
new file mode 100644
index 0000000..5619aec
--- /dev/null
+++ b/media/base/text_track_config.h
@@ -0,0 +1,45 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef MEDIA_BASE_TEXT_TRACK_CONFIG_H_
+#define MEDIA_BASE_TEXT_TRACK_CONFIG_H_
+
+#include <string>
+
+#include "media/base/media_export.h"
+
+namespace media {
+
+// Specifies the varieties of text tracks.
+enum TextKind {
+ kTextSubtitles,
+ kTextCaptions,
+ kTextDescriptions,
+ kTextMetadata,
+ kTextNone
+};
+
+class MEDIA_EXPORT TextTrackConfig {
+ public:
+ TextTrackConfig();
+ TextTrackConfig(TextKind kind,
+ const std::string& label,
+ const std::string& language);
+
+ // Returns true if all fields in |config| match this config.
+ bool Matches(const TextTrackConfig& config) const;
+
+ TextKind kind() const { return kind_; }
+ const std::string& label() const { return label_; }
+ const std::string& language() const { return language_; }
+
+ private:
+ TextKind kind_;
+ std::string label_;
+ std::string language_;
+};
+
+} // namespace media
+
+#endif // MEDIA_BASE_TEXT_TRACK_H_
diff --git a/media/filters/chunk_demuxer.cc b/media/filters/chunk_demuxer.cc
index d4eff22..9dfaa04 100644
--- a/media/filters/chunk_demuxer.cc
+++ b/media/filters/chunk_demuxer.cc
@@ -12,12 +12,12 @@
#include "base/callback_helpers.h"
#include "base/location.h"
#include "base/message_loop/message_loop_proxy.h"
+#include "base/stl_util.h"
#include "media/base/audio_decoder_config.h"
#include "media/base/bind_to_loop.h"
#include "media/base/stream_parser_buffer.h"
#include "media/base/video_decoder_config.h"
#include "media/filters/stream_parser_factory.h"
-#include "media/webm/webm_webvtt_parser.h"
using base::TimeDelta;
@@ -35,16 +35,20 @@ class SourceState {
typedef base::Callback<void(
TimeDelta, ChunkDemuxerStream*)> IncreaseDurationCB;
+ typedef base::Callback<void(
+ ChunkDemuxerStream*, const TextTrackConfig&)> NewTextTrackCB;
+
SourceState(scoped_ptr<StreamParser> stream_parser, const LogCB& log_cb,
const CreateDemuxerStreamCB& create_demuxer_stream_cb,
const IncreaseDurationCB& increase_duration_cb);
+ ~SourceState();
+
void Init(const StreamParser::InitCB& init_cb,
bool allow_audio,
bool allow_video,
- const StreamParser::NewTextBuffersCB& text_cb,
const StreamParser::NeedKeyCB& need_key_cb,
- const AddTextTrackCB& add_text_track_cb);
+ const NewTextTrackCB& new_text_track_cb);
// Appends new data to the StreamParser.
// Returns true if the data was successfully appended. Returns false if an
@@ -66,6 +70,11 @@ class SourceState {
}
void set_append_window_end(TimeDelta end) { append_window_end_ = end; }
+ void TextStartReturningData();
+ void TextAbortReads();
+ void TextSeek(TimeDelta seek_time);
+ void TextCompletePendingReadIfPossible();
+
private:
// Called by the |stream_parser_| when a new initialization segment is
// encountered.
@@ -73,7 +82,8 @@ class SourceState {
// processing decoder configurations.
bool OnNewConfigs(bool allow_audio, bool allow_video,
const AudioDecoderConfig& audio_config,
- const VideoDecoderConfig& video_config);
+ const VideoDecoderConfig& video_config,
+ const StreamParser::TextTrackConfigMap& text_configs);
// Called by the |stream_parser_| at the beginning of a new media segment.
void OnNewMediaSegment();
@@ -91,12 +101,12 @@ class SourceState {
const StreamParser::BufferQueue& video_buffers);
// Called by the |stream_parser_| when new text buffers have been parsed. It
- // applies |timestamp_offset_| to all buffers in |buffers| and then calls
- // |new_buffers_cb| with the modified buffers.
+ // applies |timestamp_offset_| to all buffers in |buffers| and then appends
+ // the (modified) buffers to the demuxer stream associated with
+ // the track having |text_track_number|.
// Returns true on a successful call. Returns false if an error occured while
// processing the buffers.
- bool OnTextBuffers(const StreamParser::NewTextBuffersCB& new_buffers_cb,
- TextTrack* text_track,
+ bool OnTextBuffers(int text_track_number,
const StreamParser::BufferQueue& buffers);
// Helper function that adds |timestamp_offset_| to each buffer in |buffers|.
@@ -115,6 +125,7 @@ class SourceState {
CreateDemuxerStreamCB create_demuxer_stream_cb_;
IncreaseDurationCB increase_duration_cb_;
+ NewTextTrackCB new_text_track_cb_;
// The offset to apply to media segment timestamps.
TimeDelta timestamp_offset_;
@@ -142,6 +153,9 @@ class SourceState {
ChunkDemuxerStream* video_;
bool video_needs_keyframe_;
+ typedef std::map<int, ChunkDemuxerStream*> TextStreamMap;
+ TextStreamMap text_stream_map_;
+
LogCB log_cb_;
DISALLOW_COPY_AND_ASSIGN(SourceState);
@@ -193,6 +207,7 @@ class ChunkDemuxerStream : public DemuxerStream {
// Returns false if the new config should trigger an error.
bool UpdateAudioConfig(const AudioDecoderConfig& config, const LogCB& log_cb);
bool UpdateVideoConfig(const VideoDecoderConfig& config, const LogCB& log_cb);
+ void UpdateTextConfig(const TextTrackConfig& config, const LogCB& log_cb);
void MarkEndOfStream();
void UnmarkEndOfStream();
@@ -204,6 +219,10 @@ class ChunkDemuxerStream : public DemuxerStream {
virtual AudioDecoderConfig audio_decoder_config() OVERRIDE;
virtual VideoDecoderConfig video_decoder_config() OVERRIDE;
+ // Returns the text track configuration. It is an error to call this method
+ // if type() != TEXT.
+ TextTrackConfig text_track_config();
+
void set_memory_limit_for_testing(int memory_limit) {
stream_->set_memory_limit_for_testing(memory_limit);
}
@@ -227,7 +246,7 @@ class ChunkDemuxerStream : public DemuxerStream {
bool GetNextBuffer_Locked(DemuxerStream::Status* status,
scoped_refptr<StreamParserBuffer>* buffer);
- // Specifies the type of the stream (must be AUDIO or VIDEO for now).
+ // Specifies the type of the stream.
Type type_;
scoped_ptr<SourceBufferStream> stream_;
@@ -258,13 +277,27 @@ SourceState::SourceState(scoped_ptr<StreamParser> stream_parser,
DCHECK(!increase_duration_cb_.is_null());
}
+SourceState::~SourceState() {
+ for (TextStreamMap::iterator itr = text_stream_map_.begin();
+ itr != text_stream_map_.end(); ++itr) {
+ itr->second->Shutdown();
+ delete itr->second;
+ }
+}
+
void SourceState::Init(const StreamParser::InitCB& init_cb,
bool allow_audio,
bool allow_video,
- const StreamParser::NewTextBuffersCB& text_cb,
const StreamParser::NeedKeyCB& need_key_cb,
- const AddTextTrackCB& add_text_track_cb) {
- StreamParser::NewBuffersCB audio_cb;
+ const NewTextTrackCB& new_text_track_cb) {
+ new_text_track_cb_ = new_text_track_cb;
+
+ StreamParser::NewTextBuffersCB new_text_buffers_cb;
+
+ if (!new_text_track_cb_.is_null()) {
+ new_text_buffers_cb = base::Bind(&SourceState::OnTextBuffers,
+ base::Unretained(this));
+ }
stream_parser_->Init(init_cb,
base::Bind(&SourceState::OnNewConfigs,
@@ -273,10 +306,8 @@ void SourceState::Init(const StreamParser::InitCB& init_cb,
allow_video),
base::Bind(&SourceState::OnNewBuffers,
base::Unretained(this)),
- base::Bind(&SourceState::OnTextBuffers,
- base::Unretained(this), text_cb),
+ new_text_buffers_cb,
need_key_cb,
- add_text_track_cb,
base::Bind(&SourceState::OnNewMediaSegment,
base::Unretained(this)),
base::Bind(&SourceState::OnEndOfMediaSegment,
@@ -303,6 +334,35 @@ void SourceState::Abort() {
can_update_offset_ = true;
}
+
+void SourceState::TextStartReturningData() {
+ for (TextStreamMap::iterator itr = text_stream_map_.begin();
+ itr != text_stream_map_.end(); ++itr) {
+ itr->second->StartReturningData();
+ }
+}
+
+void SourceState::TextAbortReads() {
+ for (TextStreamMap::iterator itr = text_stream_map_.begin();
+ itr != text_stream_map_.end(); ++itr) {
+ itr->second->AbortReads();
+ }
+}
+
+void SourceState::TextSeek(TimeDelta seek_time) {
+ for (TextStreamMap::iterator itr = text_stream_map_.begin();
+ itr != text_stream_map_.end(); ++itr) {
+ itr->second->Seek(seek_time);
+ }
+}
+
+void SourceState::TextCompletePendingReadIfPossible() {
+ for (TextStreamMap::iterator itr = text_stream_map_.begin();
+ itr != text_stream_map_.end(); ++itr) {
+ itr->second->CompletePendingReadIfPossible();
+ }
+}
+
void SourceState::AdjustBufferTimestamps(
const StreamParser::BufferQueue& buffers) {
if (timestamp_offset_ == TimeDelta())
@@ -316,9 +376,11 @@ void SourceState::AdjustBufferTimestamps(
}
}
-bool SourceState::OnNewConfigs(bool allow_audio, bool allow_video,
- const AudioDecoderConfig& audio_config,
- const VideoDecoderConfig& video_config) {
+bool SourceState::OnNewConfigs(
+ bool allow_audio, bool allow_video,
+ const AudioDecoderConfig& audio_config,
+ const VideoDecoderConfig& video_config,
+ const StreamParser::TextTrackConfigMap& text_configs) {
DVLOG(1) << "OnNewConfigs(" << allow_audio << ", " << allow_video
<< ", " << audio_config.IsValidConfig()
<< ", " << video_config.IsValidConfig() << ")";
@@ -377,6 +439,61 @@ bool SourceState::OnNewConfigs(bool allow_audio, bool allow_video,
success &= video_->UpdateVideoConfig(video_config, log_cb_);
}
+ typedef StreamParser::TextTrackConfigMap::const_iterator TextConfigItr;
+ if (text_stream_map_.empty()) {
+ for (TextConfigItr itr = text_configs.begin();
+ itr != text_configs.end(); ++itr) {
+ ChunkDemuxerStream* const text_stream =
+ create_demuxer_stream_cb_.Run(DemuxerStream::TEXT);
+ text_stream->UpdateTextConfig(itr->second, log_cb_);
+ text_stream_map_[itr->first] = text_stream;
+ new_text_track_cb_.Run(text_stream, itr->second);
+ }
+ } else {
+ const size_t text_count = text_stream_map_.size();
+ if (text_configs.size() != text_count) {
+ success &= false;
+ MEDIA_LOG(log_cb_) << "The number of text track configs changed.";
+ } else if (text_count == 1) {
+ TextConfigItr config_itr = text_configs.begin();
+ const TextTrackConfig& new_config = config_itr->second;
+ TextStreamMap::iterator stream_itr = text_stream_map_.begin();
+ ChunkDemuxerStream* text_stream = stream_itr->second;
+ TextTrackConfig old_config = text_stream->text_track_config();
+ if (!new_config.Matches(old_config)) {
+ success &= false;
+ MEDIA_LOG(log_cb_) << "New text track config does not match old one.";
+ } else {
+ text_stream_map_.clear();
+ text_stream_map_[config_itr->first] = text_stream;
+ }
+ } else {
+ for (TextConfigItr config_itr = text_configs.begin();
+ config_itr != text_configs.end(); ++config_itr) {
+ TextStreamMap::iterator stream_itr =
+ text_stream_map_.find(config_itr->first);
+ if (stream_itr == text_stream_map_.end()) {
+ success &= false;
+ MEDIA_LOG(log_cb_) << "Unexpected text track configuration "
+ "for track ID "
+ << config_itr->first;
+ break;
+ }
+
+ const TextTrackConfig& new_config = config_itr->second;
+ ChunkDemuxerStream* stream = stream_itr->second;
+ TextTrackConfig old_config = stream->text_track_config();
+ if (!new_config.Matches(old_config)) {
+ success &= false;
+ MEDIA_LOG(log_cb_) << "New text track config for track ID "
+ << config_itr->first
+ << " does not match old one.";
+ break;
+ }
+ }
+ }
+ }
+
DVLOG(1) << "OnNewConfigs() : " << (success ? "success" : "failed");
return success;
}
@@ -432,6 +549,11 @@ bool SourceState::OnNewBuffers(const StreamParser::BufferQueue& audio_buffers,
if (video_)
video_->OnNewMediaSegment(segment_timestamp);
+
+ for (TextStreamMap::iterator itr = text_stream_map_.begin();
+ itr != text_stream_map_.end(); ++itr) {
+ itr->second->OnNewMediaSegment(segment_timestamp);
+ }
}
if (!filtered_audio.empty()) {
@@ -450,15 +572,17 @@ bool SourceState::OnNewBuffers(const StreamParser::BufferQueue& audio_buffers,
}
bool SourceState::OnTextBuffers(
- const StreamParser::NewTextBuffersCB& new_buffers_cb,
- TextTrack* text_track,
+ int text_track_number,
const StreamParser::BufferQueue& buffers) {
- if (new_buffers_cb.is_null())
+ DCHECK(!buffers.empty());
+
+ TextStreamMap::iterator itr = text_stream_map_.find(text_track_number);
+ if (itr == text_stream_map_.end())
return false;
AdjustBufferTimestamps(buffers);
- return new_buffers_cb.Run(text_track, buffers);
+ return itr->second->Append(buffers);
}
void SourceState::FilterWithAppendWindow(
@@ -649,6 +773,15 @@ bool ChunkDemuxerStream::UpdateVideoConfig(const VideoDecoderConfig& config,
return stream_->UpdateVideoConfig(config);
}
+void ChunkDemuxerStream::UpdateTextConfig(const TextTrackConfig& config,
+ const LogCB& log_cb) {
+ DCHECK_EQ(type_, TEXT);
+ base::AutoLock auto_lock(lock_);
+ DCHECK(!stream_);
+ DCHECK_EQ(state_, UNINITIALIZED);
+ stream_.reset(new SourceBufferStream(config, log_cb));
+}
+
void ChunkDemuxerStream::MarkEndOfStream() {
base::AutoLock auto_lock(lock_);
stream_->MarkEndOfStream();
@@ -685,6 +818,12 @@ VideoDecoderConfig ChunkDemuxerStream::video_decoder_config() {
return stream_->GetCurrentVideoDecoderConfig();
}
+TextTrackConfig ChunkDemuxerStream::text_track_config() {
+ CHECK_EQ(type_, TEXT);
+ base::AutoLock auto_lock(lock_);
+ return stream_->GetCurrentTextTrackConfig();
+}
+
void ChunkDemuxerStream::ChangeState_Locked(State state) {
lock_.AssertAcquired();
DVLOG(1) << "ChunkDemuxerStream::ChangeState_Locked() : "
@@ -744,14 +883,13 @@ void ChunkDemuxerStream::CompletePendingReadIfPossible_Locked() {
ChunkDemuxer::ChunkDemuxer(const base::Closure& open_cb,
const NeedKeyCB& need_key_cb,
- const AddTextTrackCB& add_text_track_cb,
const LogCB& log_cb)
: state_(WAITING_FOR_INIT),
cancel_next_seek_(false),
host_(NULL),
open_cb_(open_cb),
need_key_cb_(need_key_cb),
- add_text_track_cb_(add_text_track_cb),
+ enable_text_(false),
log_cb_(log_cb),
duration_(kNoTimestamp()),
user_specified_duration_(-1) {
@@ -759,7 +897,10 @@ ChunkDemuxer::ChunkDemuxer(const base::Closure& open_cb,
DCHECK(!need_key_cb_.is_null());
}
-void ChunkDemuxer::Initialize(DemuxerHost* host, const PipelineStatusCB& cb) {
+void ChunkDemuxer::Initialize(
+ DemuxerHost* host,
+ const PipelineStatusCB& cb,
+ bool enable_text_tracks) {
DVLOG(1) << "Init()";
base::AutoLock auto_lock(lock_);
@@ -771,6 +912,7 @@ void ChunkDemuxer::Initialize(DemuxerHost* host, const PipelineStatusCB& cb) {
}
DCHECK_EQ(state_, WAITING_FOR_INIT);
host_ = host;
+ enable_text_ = enable_text_tracks;
ChangeState_Locked(INITIALIZING);
@@ -821,6 +963,7 @@ void ChunkDemuxer::OnAudioRendererDisabled() {
// Demuxer implementation.
DemuxerStream* ChunkDemuxer::GetStream(DemuxerStream::Type type) {
+ DCHECK_NE(type, DemuxerStream::TEXT);
base::AutoLock auto_lock(lock_);
if (type == DemuxerStream::VIDEO)
return video_.get();
@@ -906,13 +1049,19 @@ ChunkDemuxer::Status ChunkDemuxer::AddId(const std::string& id,
base::Bind(&ChunkDemuxer::IncreaseDurationIfNecessary,
base::Unretained(this))));
+ SourceState::NewTextTrackCB new_text_track_cb;
+
+ if (enable_text_) {
+ new_text_track_cb = base::Bind(&ChunkDemuxer::OnNewTextTrack,
+ base::Unretained(this));
+ }
+
source_state->Init(
base::Bind(&ChunkDemuxer::OnSourceInitDone, base::Unretained(this)),
has_audio,
has_video,
- base::Bind(&ChunkDemuxer::OnTextBuffers, base::Unretained(this)),
need_key_cb_,
- add_text_track_cb_);
+ new_text_track_cb);
source_state_map_[id] = source_state.release();
return kOk;
@@ -1342,6 +1491,10 @@ ChunkDemuxer::CreateDemuxerStream(DemuxerStream::Type type) {
video_.reset(new ChunkDemuxerStream(DemuxerStream::VIDEO));
return video_.get();
break;
+ case DemuxerStream::TEXT: {
+ return new ChunkDemuxerStream(DemuxerStream::TEXT);
+ break;
+ }
case DemuxerStream::UNKNOWN:
case DemuxerStream::NUM_TYPES:
NOTREACHED();
@@ -1351,30 +1504,11 @@ ChunkDemuxer::CreateDemuxerStream(DemuxerStream::Type type) {
return NULL;
}
-bool ChunkDemuxer::OnTextBuffers(
- TextTrack* text_track,
- const StreamParser::BufferQueue& buffers) {
+void ChunkDemuxer::OnNewTextTrack(ChunkDemuxerStream* text_stream,
+ const TextTrackConfig& config) {
lock_.AssertAcquired();
DCHECK_NE(state_, SHUTDOWN);
-
- // TODO(matthewjheaney): IncreaseDurationIfNecessary
-
- for (StreamParser::BufferQueue::const_iterator itr = buffers.begin();
- itr != buffers.end(); ++itr) {
- const StreamParserBuffer* const buffer = itr->get();
- const TimeDelta start = buffer->timestamp();
- const TimeDelta end = start + buffer->duration();
-
- std::string id, settings, content;
-
- WebMWebVTTParser::Parse(buffer->data(),
- buffer->data_size(),
- &id, &settings, &content);
-
- text_track->addWebVTTCue(start, end, id, content, settings);
- }
-
- return true;
+ host_->AddTextStream(text_stream, config);
}
bool ChunkDemuxer::IsValidId(const std::string& source_id) const {
@@ -1435,6 +1569,11 @@ void ChunkDemuxer::StartReturningData() {
if (video_)
video_->StartReturningData();
+
+ for (SourceStateMap::iterator itr = source_state_map_.begin();
+ itr != source_state_map_.end(); ++itr) {
+ itr->second->TextStartReturningData();
+ }
}
void ChunkDemuxer::AbortPendingReads() {
@@ -1443,6 +1582,11 @@ void ChunkDemuxer::AbortPendingReads() {
if (video_)
video_->AbortReads();
+
+ for (SourceStateMap::iterator itr = source_state_map_.begin();
+ itr != source_state_map_.end(); ++itr) {
+ itr->second->TextAbortReads();
+ }
}
void ChunkDemuxer::SeekAllSources(TimeDelta seek_time) {
@@ -1451,6 +1595,11 @@ void ChunkDemuxer::SeekAllSources(TimeDelta seek_time) {
if (video_)
video_->Seek(seek_time);
+
+ for (SourceStateMap::iterator itr = source_state_map_.begin();
+ itr != source_state_map_.end(); ++itr) {
+ itr->second->TextSeek(seek_time);
+ }
}
void ChunkDemuxer::CompletePendingReadsIfPossible() {
@@ -1459,6 +1608,11 @@ void ChunkDemuxer::CompletePendingReadsIfPossible() {
if (video_)
video_->CompletePendingReadIfPossible();
+
+ for (SourceStateMap::iterator itr = source_state_map_.begin();
+ itr != source_state_map_.end(); ++itr) {
+ itr->second->TextCompletePendingReadIfPossible();
+ }
}
} // namespace media
diff --git a/media/filters/chunk_demuxer.h b/media/filters/chunk_demuxer.h
index cb2b72c..51739db 100644
--- a/media/filters/chunk_demuxer.h
+++ b/media/filters/chunk_demuxer.h
@@ -15,7 +15,6 @@
#include "media/base/demuxer.h"
#include "media/base/ranges.h"
#include "media/base/stream_parser.h"
-#include "media/base/text_track.h"
#include "media/filters/source_buffer_stream.h"
namespace media {
@@ -38,19 +37,19 @@ class MEDIA_EXPORT ChunkDemuxer : public Demuxer {
// is ready to receive media data via AppenData().
// |need_key_cb| Run when the demuxer determines that an encryption key is
// needed to decrypt the content.
- // |add_text_track_cb| Run when demuxer detects the presence of an inband
- // text track.
+ // |enable_text| Process inband text tracks in the normal way when true,
+ // otherwise ignore them.
// |log_cb| Run when parsing error messages need to be logged to the error
// console.
ChunkDemuxer(const base::Closure& open_cb,
const NeedKeyCB& need_key_cb,
- const AddTextTrackCB& add_text_track_cb,
const LogCB& log_cb);
virtual ~ChunkDemuxer();
// Demuxer implementation.
virtual void Initialize(DemuxerHost* host,
- const PipelineStatusCB& cb) OVERRIDE;
+ const PipelineStatusCB& cb,
+ bool enable_text_tracks) OVERRIDE;
virtual void Stop(const base::Closure& callback) OVERRIDE;
virtual void Seek(base::TimeDelta time, const PipelineStatusCB& cb) OVERRIDE;
virtual void OnAudioRendererDisabled() OVERRIDE;
@@ -177,8 +176,8 @@ class MEDIA_EXPORT ChunkDemuxer : public Demuxer {
// has not been created before. Returns NULL otherwise.
ChunkDemuxerStream* CreateDemuxerStream(DemuxerStream::Type type);
- bool OnTextBuffers(TextTrack* text_track,
- const StreamParser::BufferQueue& buffers);
+ void OnNewTextTrack(ChunkDemuxerStream* text_stream,
+ const TextTrackConfig& config);
void OnNewMediaSegment(const std::string& source_id,
base::TimeDelta start_timestamp);
@@ -230,7 +229,7 @@ class MEDIA_EXPORT ChunkDemuxer : public Demuxer {
DemuxerHost* host_;
base::Closure open_cb_;
NeedKeyCB need_key_cb_;
- AddTextTrackCB add_text_track_cb_;
+ bool enable_text_;
// Callback used to report error strings that can help the web developer
// figure out what is wrong with the content.
LogCB log_cb_;
diff --git a/media/filters/chunk_demuxer_unittest.cc b/media/filters/chunk_demuxer_unittest.cc
index 3d9b26f..4b279b6 100644
--- a/media/filters/chunk_demuxer_unittest.cc
+++ b/media/filters/chunk_demuxer_unittest.cc
@@ -159,17 +159,14 @@ class ChunkDemuxerTest : public testing::Test {
base::Bind(&ChunkDemuxerTest::DemuxerOpened, base::Unretained(this));
Demuxer::NeedKeyCB need_key_cb =
base::Bind(&ChunkDemuxerTest::DemuxerNeedKey, base::Unretained(this));
- AddTextTrackCB add_text_track_cb =
- base::Bind(&ChunkDemuxerTest::OnTextTrack, base::Unretained(this));
- demuxer_.reset(new ChunkDemuxer(open_cb, need_key_cb,
- add_text_track_cb, LogCB()));
+ demuxer_.reset(new ChunkDemuxer(open_cb, need_key_cb, LogCB()));
}
virtual ~ChunkDemuxerTest() {
ShutdownDemuxer();
}
- void CreateInitSegment(bool has_audio, bool has_video,
+ void CreateInitSegment(bool has_audio, bool has_video, bool has_text,
bool is_audio_encrypted, bool is_video_encrypted,
scoped_ptr<uint8[]>* buffer,
int* size) {
@@ -179,6 +176,7 @@ class ChunkDemuxerTest : public testing::Test {
scoped_refptr<DecoderBuffer> video_track_entry;
scoped_refptr<DecoderBuffer> audio_content_encodings;
scoped_refptr<DecoderBuffer> video_content_encodings;
+ scoped_refptr<DecoderBuffer> text_track_entry;
ebml_header = ReadTestDataFile("webm_ebml_element");
@@ -204,6 +202,25 @@ class ChunkDemuxerTest : public testing::Test {
}
}
+ if (has_text) {
+ // TODO(matthewjheaney): create an abstraction to do
+ // this (http://crbug/321454).
+ // We need it to also handle the creation of multiple text tracks.
+ //
+ // This is the track entry for a text track,
+ // TrackEntry [AE], size=26
+ // TrackNum [D7], size=1, val=3
+ // TrackType [83], size=1, val=0x11
+ // CodecId [86], size=18, val="D_WEBVTT/SUBTITLES"
+ const char str[] = "\xAE\x9A\xD7\x81\x03\x83\x81\x11\x86\x92"
+ "D_WEBVTT/SUBTITLES";
+ const int len = strlen(str);
+ DCHECK_EQ(len, 28);
+ const uint8* const buf = reinterpret_cast<const uint8*>(str);
+ text_track_entry = DecoderBuffer::CopyFrom(buf, len);
+ tracks_element_size += text_track_entry->data_size();
+ }
+
*size = ebml_header->data_size() + info->data_size() +
kTracksHeaderSize + tracks_element_size;
@@ -253,6 +270,12 @@ class ChunkDemuxerTest : public testing::Test {
}
buf += video_track_entry->data_size();
}
+
+ if (has_text) {
+ memcpy(buf, text_track_entry->data(),
+ text_track_entry->data_size());
+ buf += text_track_entry->data_size();
+ }
}
ChunkDemuxer::Status AddId() {
@@ -365,22 +388,28 @@ class ChunkDemuxerTest : public testing::Test {
}
void AppendInitSegment(bool has_audio, bool has_video) {
- AppendInitSegmentWithSourceId(kSourceId, has_audio, has_video);
+ AppendInitSegmentWithSourceId(kSourceId, has_audio, has_video, false);
+ }
+
+ void AppendInitSegmentText(bool has_audio, bool has_video) {
+ AppendInitSegmentWithSourceId(kSourceId, has_audio, has_video, true);
}
void AppendInitSegmentWithSourceId(const std::string& source_id,
- bool has_audio, bool has_video) {
+ bool has_audio, bool has_video,
+ bool has_text) {
AppendInitSegmentWithEncryptedInfo(
- source_id, has_audio, has_video, false, false);
+ source_id, has_audio, has_video, has_text, false, false);
}
void AppendInitSegmentWithEncryptedInfo(const std::string& source_id,
bool has_audio, bool has_video,
+ bool has_text,
bool is_audio_encrypted,
bool is_video_encrypted) {
scoped_ptr<uint8[]> info_tracks;
int info_tracks_size = 0;
- CreateInitSegment(has_audio, has_video,
+ CreateInitSegment(has_audio, has_video, has_text,
is_audio_encrypted, is_video_encrypted,
&info_tracks, &info_tracks_size);
AppendData(source_id, info_tracks.get(), info_tracks_size);
@@ -418,11 +447,17 @@ class ChunkDemuxerTest : public testing::Test {
}
bool InitDemuxer(bool has_audio, bool has_video) {
- return InitDemuxerWithEncryptionInfo(has_audio, has_video, false, false);
+ return InitDemuxerWithEncryptionInfo(has_audio, has_video, false,
+ false, false);
+ }
+
+ bool InitDemuxerText(bool has_audio, bool has_video) {
+ return InitDemuxerWithEncryptionInfo(has_audio, has_video, true,
+ false, false);
}
bool InitDemuxerWithEncryptionInfo(
- bool has_audio, bool has_video,
+ bool has_audio, bool has_video, bool has_text,
bool is_audio_encrypted, bool is_video_encrypted) {
PipelineStatus expected_status =
(has_audio || has_video) ? PIPELINE_OK : DEMUXER_ERROR_COULD_NOT_OPEN;
@@ -433,33 +468,39 @@ class ChunkDemuxerTest : public testing::Test {
EXPECT_CALL(*this, DemuxerOpened());
demuxer_->Initialize(
- &host_, CreateInitDoneCB(expected_duration, expected_status));
+ &host_, CreateInitDoneCB(expected_duration, expected_status), true);
if (AddId(kSourceId, has_audio, has_video) != ChunkDemuxer::kOk)
return false;
AppendInitSegmentWithEncryptedInfo(
- kSourceId, has_audio, has_video,
+ kSourceId, has_audio, has_video, has_text,
is_audio_encrypted, is_video_encrypted);
return true;
}
- bool InitDemuxerAudioAndVideoSources(const std::string& audio_id,
- const std::string& video_id) {
+ bool InitDemuxerAudioAndVideoSourcesText(const std::string& audio_id,
+ const std::string& video_id,
+ bool has_text) {
EXPECT_CALL(*this, DemuxerOpened());
demuxer_->Initialize(
- &host_, CreateInitDoneCB(kDefaultDuration(), PIPELINE_OK));
+ &host_, CreateInitDoneCB(kDefaultDuration(), PIPELINE_OK), true);
if (AddId(audio_id, true, false) != ChunkDemuxer::kOk)
return false;
if (AddId(video_id, false, true) != ChunkDemuxer::kOk)
return false;
- AppendInitSegmentWithSourceId(audio_id, true, false);
- AppendInitSegmentWithSourceId(video_id, false, true);
+ AppendInitSegmentWithSourceId(audio_id, true, false, has_text);
+ AppendInitSegmentWithSourceId(video_id, false, true, has_text);
return true;
}
+ bool InitDemuxerAudioAndVideoSources(const std::string& audio_id,
+ const std::string& video_id) {
+ return InitDemuxerAudioAndVideoSourcesText(audio_id, video_id, false);
+ }
+
// Initializes the demuxer with data from 2 files with different
// decoder configurations. This is used to test the decoder config change
// logic.
@@ -484,7 +525,7 @@ class ChunkDemuxerTest : public testing::Test {
EXPECT_CALL(*this, DemuxerOpened());
demuxer_->Initialize(
&host_, CreateInitDoneCB(base::TimeDelta::FromMilliseconds(2744),
- PIPELINE_OK));
+ PIPELINE_OK), true);
if (AddId(kSourceId, true, true) != ChunkDemuxer::kOk)
return false;
@@ -810,7 +851,7 @@ class ChunkDemuxerTest : public testing::Test {
bool has_audio, bool has_video) {
EXPECT_CALL(*this, DemuxerOpened());
demuxer_->Initialize(
- &host_, CreateInitDoneCB(duration, PIPELINE_OK));
+ &host_, CreateInitDoneCB(duration, PIPELINE_OK), true);
if (AddId(kSourceId, has_audio, has_video) != ChunkDemuxer::kOk)
return false;
@@ -861,12 +902,6 @@ class ChunkDemuxerTest : public testing::Test {
NeedKeyMock(type, init_data_ptr, init_data.size());
}
- scoped_ptr<TextTrack> OnTextTrack(TextKind kind,
- const std::string& label,
- const std::string& language) {
- return scoped_ptr<TextTrack>();
- }
-
void Seek(base::TimeDelta seek_time) {
demuxer_->StartWaitingForSeek(seek_time);
demuxer_->Seek(seek_time, NewExpectedStatusCB(PIPELINE_OK));
@@ -913,7 +948,62 @@ TEST_F(ChunkDemuxerTest, Init) {
}
ASSERT_TRUE(InitDemuxerWithEncryptionInfo(
- has_audio, has_video, is_audio_encrypted, is_video_encrypted));
+ has_audio, has_video, false, is_audio_encrypted, is_video_encrypted));
+
+ DemuxerStream* audio_stream = demuxer_->GetStream(DemuxerStream::AUDIO);
+ if (has_audio) {
+ ASSERT_TRUE(audio_stream);
+
+ const AudioDecoderConfig& config = audio_stream->audio_decoder_config();
+ EXPECT_EQ(kCodecVorbis, config.codec());
+ EXPECT_EQ(32, config.bits_per_channel());
+ EXPECT_EQ(CHANNEL_LAYOUT_STEREO, config.channel_layout());
+ EXPECT_EQ(44100, config.samples_per_second());
+ EXPECT_TRUE(config.extra_data());
+ EXPECT_GT(config.extra_data_size(), 0u);
+ EXPECT_EQ(kSampleFormatPlanarF32, config.sample_format());
+ EXPECT_EQ(is_audio_encrypted,
+ audio_stream->audio_decoder_config().is_encrypted());
+ } else {
+ EXPECT_FALSE(audio_stream);
+ }
+
+ DemuxerStream* video_stream = demuxer_->GetStream(DemuxerStream::VIDEO);
+ if (has_video) {
+ EXPECT_TRUE(video_stream);
+ EXPECT_EQ(is_video_encrypted,
+ video_stream->video_decoder_config().is_encrypted());
+ } else {
+ EXPECT_FALSE(video_stream);
+ }
+
+ ShutdownDemuxer();
+ demuxer_.reset();
+ }
+}
+
+TEST_F(ChunkDemuxerTest, InitText) {
+ // Test with 1 video stream and 1 text streams, and 0 or 1 audio streams.
+ // No encryption cases handled here.
+ bool has_video = true;
+ bool is_audio_encrypted = false;
+ bool is_video_encrypted = false;
+ for (int i = 0; i < 2; i++) {
+ bool has_audio = (i & 0x1) != 0;
+
+ CreateNewDemuxer();
+
+ DemuxerStream* text_stream = NULL;
+ TextTrackConfig text_config;
+ EXPECT_CALL(host_, AddTextStream(_,_))
+ .WillOnce(DoAll(SaveArg<0>(&text_stream),
+ SaveArg<1>(&text_config)));
+
+ ASSERT_TRUE(InitDemuxerWithEncryptionInfo(
+ has_audio, has_video, true, is_audio_encrypted, is_video_encrypted));
+ ASSERT_TRUE(text_stream);
+ EXPECT_EQ(DemuxerStream::TEXT, text_stream->type());
+ EXPECT_EQ(kTextSubtitles, text_config.kind());
DemuxerStream* audio_stream = demuxer_->GetStream(DemuxerStream::AUDIO);
if (has_audio) {
@@ -953,12 +1043,27 @@ TEST_F(ChunkDemuxerTest, ShutdownBeforeAllInitSegmentsAppended) {
EXPECT_CALL(*this, DemuxerOpened());
demuxer_->Initialize(
&host_, CreateInitDoneCB(
- kDefaultDuration(), DEMUXER_ERROR_COULD_NOT_OPEN));
+ kDefaultDuration(), DEMUXER_ERROR_COULD_NOT_OPEN), true);
EXPECT_EQ(AddId("audio", true, false), ChunkDemuxer::kOk);
EXPECT_EQ(AddId("video", false, true), ChunkDemuxer::kOk);
- AppendInitSegmentWithSourceId("audio", true, false);
+ AppendInitSegmentWithSourceId("audio", true, false, false);
+}
+
+TEST_F(ChunkDemuxerTest, ShutdownBeforeAllInitSegmentsAppendedText) {
+ EXPECT_CALL(*this, DemuxerOpened());
+ demuxer_->Initialize(
+ &host_, CreateInitDoneCB(
+ kDefaultDuration(), DEMUXER_ERROR_COULD_NOT_OPEN), true);
+
+ EXPECT_EQ(AddId("audio", true, false), ChunkDemuxer::kOk);
+ EXPECT_EQ(AddId("video", false, true), ChunkDemuxer::kOk);
+
+ EXPECT_CALL(host_, AddTextStream(_,_))
+ .Times(Exactly(1));
+
+ AppendInitSegmentWithSourceId("video", false, true, true);
}
// Test that Seek() completes successfully when the first cluster
@@ -1033,7 +1138,8 @@ TEST_F(ChunkDemuxerTest, SeekWhileParsingCluster) {
TEST_F(ChunkDemuxerTest, AppendDataBeforeInit) {
scoped_ptr<uint8[]> info_tracks;
int info_tracks_size = 0;
- CreateInitSegment(true, true, false, false, &info_tracks, &info_tracks_size);
+ CreateInitSegment(true, true, false,
+ false, false, &info_tracks, &info_tracks_size);
demuxer_->AppendData(kSourceId, info_tracks.get(), info_tracks_size);
}
@@ -1139,7 +1245,7 @@ TEST_F(ChunkDemuxerTest, PerStreamMonotonicallyIncreasingTimestamps) {
TEST_F(ChunkDemuxerTest, ClusterBeforeInitSegment) {
EXPECT_CALL(*this, DemuxerOpened());
demuxer_->Initialize(
- &host_, NewExpectedStatusCB(DEMUXER_ERROR_COULD_NOT_OPEN));
+ &host_, NewExpectedStatusCB(DEMUXER_ERROR_COULD_NOT_OPEN), true);
ASSERT_EQ(AddId(), ChunkDemuxer::kOk);
@@ -1150,14 +1256,14 @@ TEST_F(ChunkDemuxerTest, ClusterBeforeInitSegment) {
TEST_F(ChunkDemuxerTest, EOSDuringInit) {
EXPECT_CALL(*this, DemuxerOpened());
demuxer_->Initialize(
- &host_, NewExpectedStatusCB(DEMUXER_ERROR_COULD_NOT_OPEN));
+ &host_, NewExpectedStatusCB(DEMUXER_ERROR_COULD_NOT_OPEN), true);
MarkEndOfStream(PIPELINE_OK);
}
TEST_F(ChunkDemuxerTest, EndOfStreamWithNoAppend) {
EXPECT_CALL(*this, DemuxerOpened());
demuxer_->Initialize(
- &host_, NewExpectedStatusCB(DEMUXER_ERROR_COULD_NOT_OPEN));
+ &host_, NewExpectedStatusCB(DEMUXER_ERROR_COULD_NOT_OPEN), true);
ASSERT_EQ(AddId(), ChunkDemuxer::kOk);
@@ -1356,13 +1462,14 @@ TEST_F(ChunkDemuxerTest, EndOfStreamDuringCanceledSeek) {
TEST_F(ChunkDemuxerTest, AppendingInPieces) {
EXPECT_CALL(*this, DemuxerOpened());
demuxer_->Initialize(
- &host_, CreateInitDoneCB(kDefaultDuration(), PIPELINE_OK));
+ &host_, CreateInitDoneCB(kDefaultDuration(), PIPELINE_OK), true);
ASSERT_EQ(AddId(), ChunkDemuxer::kOk);
scoped_ptr<uint8[]> info_tracks;
int info_tracks_size = 0;
- CreateInitSegment(true, true, false, false, &info_tracks, &info_tracks_size);
+ CreateInitSegment(true, true, false,
+ false, false, &info_tracks, &info_tracks_size);
scoped_ptr<Cluster> cluster_a(kDefaultFirstCluster());
scoped_ptr<Cluster> cluster_b(kDefaultSecondCluster());
@@ -1524,7 +1631,7 @@ TEST_F(ChunkDemuxerTest, ParseErrorDuringInit) {
EXPECT_CALL(*this, DemuxerOpened());
demuxer_->Initialize(
&host_, CreateInitDoneCB(
- kNoTimestamp(), DEMUXER_ERROR_COULD_NOT_OPEN));
+ kNoTimestamp(), DEMUXER_ERROR_COULD_NOT_OPEN), true);
ASSERT_EQ(AddId(), ChunkDemuxer::kOk);
@@ -1536,7 +1643,7 @@ TEST_F(ChunkDemuxerTest, AVHeadersWithAudioOnlyType) {
EXPECT_CALL(*this, DemuxerOpened());
demuxer_->Initialize(
&host_, CreateInitDoneCB(kNoTimestamp(),
- DEMUXER_ERROR_COULD_NOT_OPEN));
+ DEMUXER_ERROR_COULD_NOT_OPEN), true);
std::vector<std::string> codecs(1);
codecs[0] = "vorbis";
@@ -1550,7 +1657,7 @@ TEST_F(ChunkDemuxerTest, AVHeadersWithVideoOnlyType) {
EXPECT_CALL(*this, DemuxerOpened());
demuxer_->Initialize(
&host_, CreateInitDoneCB(kNoTimestamp(),
- DEMUXER_ERROR_COULD_NOT_OPEN));
+ DEMUXER_ERROR_COULD_NOT_OPEN), true);
std::vector<std::string> codecs(1);
codecs[0] = "vp8";
@@ -1587,10 +1694,30 @@ TEST_F(ChunkDemuxerTest, AddSeparateSourcesForAudioAndVideo) {
GenerateVideoStreamExpectedReads(0, 4);
}
+TEST_F(ChunkDemuxerTest, AddSeparateSourcesForAudioAndVideoText) {
+ // TODO(matthewjheaney): Here and elsewhere, we need more tests
+ // for inband text tracks (http://crbug/321455).
+
+ std::string audio_id = "audio1";
+ std::string video_id = "video1";
+
+ EXPECT_CALL(host_, AddTextStream(_,_))
+ .Times(Exactly(2));
+ ASSERT_TRUE(InitDemuxerAudioAndVideoSourcesText(audio_id, video_id, true));
+
+ // Append audio and video data into separate source ids.
+ AppendCluster(audio_id,
+ GenerateSingleStreamCluster(0, 92, kAudioTrackNum, kAudioBlockDuration));
+ GenerateAudioStreamExpectedReads(0, 4);
+ AppendCluster(video_id,
+ GenerateSingleStreamCluster(0, 132, kVideoTrackNum, kVideoBlockDuration));
+ GenerateVideoStreamExpectedReads(0, 4);
+}
+
TEST_F(ChunkDemuxerTest, AddIdFailures) {
EXPECT_CALL(*this, DemuxerOpened());
demuxer_->Initialize(
- &host_, CreateInitDoneCB(kDefaultDuration(), PIPELINE_OK));
+ &host_, CreateInitDoneCB(kDefaultDuration(), PIPELINE_OK), true);
std::string audio_id = "audio1";
std::string video_id = "video1";
@@ -1600,7 +1727,7 @@ TEST_F(ChunkDemuxerTest, AddIdFailures) {
// Adding an id with audio/video should fail because we already added audio.
ASSERT_EQ(AddId(), ChunkDemuxer::kReachedIdLimit);
- AppendInitSegmentWithSourceId(audio_id, true, false);
+ AppendInitSegmentWithSourceId(audio_id, true, false, false);
// Adding an id after append should fail.
ASSERT_EQ(AddId(video_id, false, true), ChunkDemuxer::kReachedIdLimit);
@@ -1829,7 +1956,7 @@ TEST_F(ChunkDemuxerTest, EndOfStreamDuringPendingSeek) {
TEST_F(ChunkDemuxerTest, GetBufferedRanges_AudioIdOnly) {
EXPECT_CALL(*this, DemuxerOpened());
demuxer_->Initialize(
- &host_, CreateInitDoneCB(kDefaultDuration(), PIPELINE_OK));
+ &host_, CreateInitDoneCB(kDefaultDuration(), PIPELINE_OK), true);
ASSERT_EQ(AddId(kSourceId, true, false), ChunkDemuxer::kOk);
AppendInitSegment(true, false);
@@ -1851,7 +1978,7 @@ TEST_F(ChunkDemuxerTest, GetBufferedRanges_AudioIdOnly) {
TEST_F(ChunkDemuxerTest, GetBufferedRanges_VideoIdOnly) {
EXPECT_CALL(*this, DemuxerOpened());
demuxer_->Initialize(
- &host_, CreateInitDoneCB(kDefaultDuration(), PIPELINE_OK));
+ &host_, CreateInitDoneCB(kDefaultDuration(), PIPELINE_OK), true);
ASSERT_EQ(AddId(kSourceId, false, true), ChunkDemuxer::kOk);
AppendInitSegment(false, true);
@@ -2093,7 +2220,7 @@ TEST_F(ChunkDemuxerTest, EndOfStreamStillSetAfterSeek) {
TEST_F(ChunkDemuxerTest, GetBufferedRangesBeforeInitSegment) {
EXPECT_CALL(*this, DemuxerOpened());
- demuxer_->Initialize(&host_, CreateInitDoneCB(PIPELINE_OK));
+ demuxer_->Initialize(&host_, CreateInitDoneCB(PIPELINE_OK), true);
ASSERT_EQ(AddId("audio", true, false), ChunkDemuxer::kOk);
ASSERT_EQ(AddId("video", false, true), ChunkDemuxer::kOk);
@@ -2417,7 +2544,7 @@ TEST_F(ChunkDemuxerTest, AppendAfterEndOfStream) {
TEST_F(ChunkDemuxerTest, ShutdownBeforeInitialize) {
demuxer_->Shutdown();
demuxer_->Initialize(
- &host_, CreateInitDoneCB(DEMUXER_ERROR_COULD_NOT_OPEN));
+ &host_, CreateInitDoneCB(DEMUXER_ERROR_COULD_NOT_OPEN), true);
message_loop_.RunUntilIdle();
}
@@ -2566,7 +2693,7 @@ TEST_F(ChunkDemuxerTest, GCDuringSeek) {
TEST_F(ChunkDemuxerTest, RemoveBeforeInitSegment) {
EXPECT_CALL(*this, DemuxerOpened());
demuxer_->Initialize(
- &host_, CreateInitDoneCB(kNoTimestamp(), PIPELINE_OK));
+ &host_, CreateInitDoneCB(kNoTimestamp(), PIPELINE_OK), true);
EXPECT_EQ(ChunkDemuxer::kOk, AddId(kSourceId, true, true));
diff --git a/media/filters/ffmpeg_demuxer.cc b/media/filters/ffmpeg_demuxer.cc
index 2ba0c54..c3d4490 100644
--- a/media/filters/ffmpeg_demuxer.cc
+++ b/media/filters/ffmpeg_demuxer.cc
@@ -11,11 +11,9 @@
#include "base/bind.h"
#include "base/callback.h"
#include "base/callback_helpers.h"
-#include "base/command_line.h"
#include "base/memory/scoped_ptr.h"
#include "base/message_loop/message_loop.h"
#include "base/metrics/sparse_histogram.h"
-#include "base/stl_util.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/sys_byteorder.h"
@@ -27,11 +25,11 @@
#include "media/base/decrypt_config.h"
#include "media/base/limits.h"
#include "media/base/media_log.h"
-#include "media/base/media_switches.h"
#include "media/base/video_decoder_config.h"
#include "media/ffmpeg/ffmpeg_common.h"
#include "media/filters/ffmpeg_glue.h"
#include "media/filters/ffmpeg_h264_to_annex_b_bitstream_converter.h"
+#include "media/filters/webvtt_util.h"
#include "media/webm/webm_crypto_helpers.h"
namespace media {
@@ -65,6 +63,9 @@ FFmpegDemuxerStream::FFmpegDemuxerStream(
AVStreamToVideoDecoderConfig(stream, &video_config_, true);
is_encrypted = video_config_.is_encrypted();
break;
+ case AVMEDIA_TYPE_SUBTITLE:
+ type_ = TEXT;
+ break;
default:
NOTREACHED();
break;
@@ -115,44 +116,67 @@ void FFmpegDemuxerStream::EnqueuePacket(ScopedAVPacket packet) {
// keep this generic so that other side_data types in the future can be
// handled the same way as well.
av_packet_split_side_data(packet.get());
- int side_data_size = 0;
- uint8* side_data = av_packet_get_side_data(
- packet.get(),
- AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL,
- &side_data_size);
-
- // If a packet is returned by FFmpeg's av_parser_parse2() the packet will
- // reference inner memory of FFmpeg. As such we should transfer the packet
- // into memory we control.
scoped_refptr<DecoderBuffer> buffer;
- if (side_data_size > 0) {
+
+ if (type() == DemuxerStream::TEXT) {
+ int id_size = 0;
+ uint8* id_data = av_packet_get_side_data(
+ packet.get(),
+ AV_PKT_DATA_WEBVTT_IDENTIFIER,
+ &id_size);
+
+ int settings_size = 0;
+ uint8* settings_data = av_packet_get_side_data(
+ packet.get(),
+ AV_PKT_DATA_WEBVTT_SETTINGS,
+ &settings_size);
+
+ std::vector<uint8> side_data;
+ MakeSideData(id_data, id_data + id_size,
+ settings_data, settings_data + settings_size,
+ &side_data);
+
buffer = DecoderBuffer::CopyFrom(packet.get()->data, packet.get()->size,
- side_data, side_data_size);
+ side_data.data(), side_data.size());
} else {
- buffer = DecoderBuffer::CopyFrom(packet.get()->data, packet.get()->size);
- }
-
- int skip_samples_size = 0;
- uint8* skip_samples = av_packet_get_side_data(packet.get(),
- AV_PKT_DATA_SKIP_SAMPLES,
- &skip_samples_size);
- const int kSkipSamplesValidSize = 10;
- const int kSkipSamplesOffset = 4;
- if (skip_samples_size >= kSkipSamplesValidSize) {
- int discard_padding_samples = base::ByteSwapToLE32(
- *(reinterpret_cast<const uint32*>(skip_samples +
- kSkipSamplesOffset)));
- // TODO(vigneshv): Change decoder buffer to use number of samples so that
- // this conversion can be avoided.
- buffer->set_discard_padding(base::TimeDelta::FromMicroseconds(
- discard_padding_samples * 1000000.0 /
- audio_decoder_config().samples_per_second()));
+ int side_data_size = 0;
+ uint8* side_data = av_packet_get_side_data(
+ packet.get(),
+ AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL,
+ &side_data_size);
+
+ // If a packet is returned by FFmpeg's av_parser_parse2() the packet will
+ // reference inner memory of FFmpeg. As such we should transfer the packet
+ // into memory we control.
+ if (side_data_size > 0) {
+ buffer = DecoderBuffer::CopyFrom(packet.get()->data, packet.get()->size,
+ side_data, side_data_size);
+ } else {
+ buffer = DecoderBuffer::CopyFrom(packet.get()->data, packet.get()->size);
+ }
+
+ int skip_samples_size = 0;
+ uint8* skip_samples = av_packet_get_side_data(packet.get(),
+ AV_PKT_DATA_SKIP_SAMPLES,
+ &skip_samples_size);
+ const int kSkipSamplesValidSize = 10;
+ const int kSkipSamplesOffset = 4;
+ if (skip_samples_size >= kSkipSamplesValidSize) {
+ int discard_padding_samples = base::ByteSwapToLE32(
+ *(reinterpret_cast<const uint32*>(skip_samples +
+ kSkipSamplesOffset)));
+ // TODO(vigneshv): Change decoder buffer to use number of samples so that
+ // this conversion can be avoided.
+ buffer->set_discard_padding(base::TimeDelta::FromMicroseconds(
+ discard_padding_samples * 1000000.0 /
+ audio_decoder_config().samples_per_second()));
+ }
}
if ((type() == DemuxerStream::AUDIO && audio_config_.is_encrypted()) ||
(type() == DemuxerStream::VIDEO && video_config_.is_encrypted())) {
scoped_ptr<DecryptConfig> config(WebMCreateDecryptConfig(
- packet->data, packet->size,
+ packet->data, packet->size,
reinterpret_cast<const uint8*>(encryption_key_id_.data()),
encryption_key_id_.size()));
if (!config)
@@ -290,6 +314,27 @@ bool FFmpegDemuxerStream::HasAvailableCapacity() {
return buffer_queue_.IsEmpty() || buffer_queue_.Duration() < kCapacity;
}
+TextKind FFmpegDemuxerStream::GetTextKind() const {
+ DCHECK_EQ(type_, DemuxerStream::TEXT);
+
+ if (stream_->disposition & AV_DISPOSITION_CAPTIONS)
+ return kTextCaptions;
+
+ if (stream_->disposition & AV_DISPOSITION_DESCRIPTIONS)
+ return kTextDescriptions;
+
+ if (stream_->disposition & AV_DISPOSITION_METADATA)
+ return kTextMetadata;
+
+ return kTextSubtitles;
+}
+
+std::string FFmpegDemuxerStream::GetMetadata(const char* key) const {
+ const AVDictionaryEntry* entry =
+ av_dict_get(stream_->metadata, key, NULL, 0);
+ return (entry == NULL || entry->value == NULL) ? "" : entry->value;
+}
+
// static
base::TimeDelta FFmpegDemuxerStream::ConvertStreamTimestamp(
const AVRational& time_base, int64 timestamp) {
@@ -318,6 +363,7 @@ FFmpegDemuxer::FFmpegDemuxer(
bitrate_(0),
start_time_(kNoTimestamp()),
audio_disabled_(false),
+ text_enabled_(false),
duration_known_(false),
url_protocol_(data_source, BindToLoop(message_loop_, base::Bind(
&FFmpegDemuxer::OnDataSourceError, base::Unretained(this)))),
@@ -375,10 +421,12 @@ void FFmpegDemuxer::OnAudioRendererDisabled() {
}
void FFmpegDemuxer::Initialize(DemuxerHost* host,
- const PipelineStatusCB& status_cb) {
+ const PipelineStatusCB& status_cb,
+ bool enable_text_tracks) {
DCHECK(message_loop_->BelongsToCurrentThread());
host_ = host;
weak_this_ = weak_factory_.GetWeakPtr();
+ text_enabled_ = enable_text_tracks;
// TODO(scherkus): DataSource should have a host by this point,
// see http://crbug.com/122071
@@ -422,6 +470,22 @@ base::TimeDelta FFmpegDemuxer::GetStartTime() const {
return start_time_;
}
+void FFmpegDemuxer::AddTextStreams() {
+ DCHECK(message_loop_->BelongsToCurrentThread());
+
+ for (StreamVector::size_type idx = 0; idx < streams_.size(); ++idx) {
+ FFmpegDemuxerStream* stream = streams_[idx];
+ if (stream == NULL || stream->type() != DemuxerStream::TEXT)
+ continue;
+
+ TextKind kind = stream->GetTextKind();
+ std::string title = stream->GetMetadata("title");
+ std::string language = stream->GetMetadata("language");
+
+ host_->AddTextStream(stream, TextTrackConfig(kind, title, language));
+ }
+}
+
// Helper for calculating the bitrate of the media based on information stored
// in |format_context| or failing that the size and duration of the media.
//
@@ -540,6 +604,10 @@ void FFmpegDemuxer::OnFindStreamInfoDone(const PipelineStatusCB& status_cb,
if (!video_config.IsValidConfig())
continue;
video_stream = stream;
+ } else if (codec_type == AVMEDIA_TYPE_SUBTITLE) {
+ if (codec_context->codec_id != AV_CODEC_ID_WEBVTT || !text_enabled_) {
+ continue;
+ }
} else {
continue;
}
@@ -560,6 +628,9 @@ void FFmpegDemuxer::OnFindStreamInfoDone(const PipelineStatusCB& status_cb,
return;
}
+ if (text_enabled_)
+ AddTextStreams();
+
if (format_context->duration != static_cast<int64_t>(AV_NOPTS_VALUE)) {
// If there is a duration value in the container use that to find the
// maximum between it and the duration from A/V streams.
diff --git a/media/filters/ffmpeg_demuxer.h b/media/filters/ffmpeg_demuxer.h
index 04a39e5..69f6c96 100644
--- a/media/filters/ffmpeg_demuxer.h
+++ b/media/filters/ffmpeg_demuxer.h
@@ -34,6 +34,7 @@
#include "media/base/decoder_buffer_queue.h"
#include "media/base/demuxer.h"
#include "media/base/pipeline.h"
+#include "media/base/text_track_config.h"
#include "media/base/video_decoder_config.h"
#include "media/filters/blocking_url_protocol.h"
@@ -93,6 +94,12 @@ class FFmpegDemuxerStream : public DemuxerStream {
// Returns true if this stream has capacity for additional data.
bool HasAvailableCapacity();
+ TextKind GetTextKind() const;
+
+ // Returns the value associated with |key| in the metadata for the avstream.
+ // Returns an empty string if the key is not present.
+ std::string GetMetadata(const char* key) const;
+
private:
friend class FFmpegDemuxerTest;
@@ -136,7 +143,8 @@ class MEDIA_EXPORT FFmpegDemuxer : public Demuxer {
// Demuxer implementation.
virtual void Initialize(DemuxerHost* host,
- const PipelineStatusCB& status_cb) OVERRIDE;
+ const PipelineStatusCB& status_cb,
+ bool enable_text_tracks) OVERRIDE;
virtual void Stop(const base::Closure& callback) OVERRIDE;
virtual void Seek(base::TimeDelta time, const PipelineStatusCB& cb) OVERRIDE;
virtual void OnAudioRendererDisabled() OVERRIDE;
@@ -184,6 +192,10 @@ class MEDIA_EXPORT FFmpegDemuxer : public Demuxer {
// FFmpegDemuxerStream.
FFmpegDemuxerStream* GetFFmpegStream(DemuxerStream::Type type) const;
+ // Called after the streams have been collected from the media, to allow
+ // the text renderer to bind each text stream to the cue rendering engine.
+ void AddTextStreams();
+
DemuxerHost* host_;
scoped_refptr<base::MessageLoopProxy> message_loop_;
@@ -233,6 +245,9 @@ class MEDIA_EXPORT FFmpegDemuxer : public Demuxer {
// drops packets destined for AUDIO demuxer streams on the floor).
bool audio_disabled_;
+ // Whether text streams have been enabled for this demuxer.
+ bool text_enabled_;
+
// Set if we know duration of the audio stream. Used when processing end of
// stream -- at this moment we definitely know duration.
bool duration_known_;
diff --git a/media/filters/ffmpeg_demuxer_unittest.cc b/media/filters/ffmpeg_demuxer_unittest.cc
index f5b0e97..7c6fcb5 100644
--- a/media/filters/ffmpeg_demuxer_unittest.cc
+++ b/media/filters/ffmpeg_demuxer_unittest.cc
@@ -83,6 +83,7 @@ class FFmpegDemuxerTest : public testing::Test {
Demuxer::NeedKeyCB need_key_cb =
base::Bind(&FFmpegDemuxerTest::NeedKeyCB, base::Unretained(this));
+
demuxer_.reset(new FFmpegDemuxer(message_loop_.message_loop_proxy(),
data_source_.get(),
need_key_cb,
@@ -91,13 +92,17 @@ class FFmpegDemuxerTest : public testing::Test {
MOCK_METHOD1(CheckPoint, void(int v));
- void InitializeDemuxer() {
+ void InitializeDemuxerText(bool enable_text) {
EXPECT_CALL(host_, SetDuration(_));
WaitableMessageLoopEvent event;
- demuxer_->Initialize(&host_, event.GetPipelineStatusCB());
+ demuxer_->Initialize(&host_, event.GetPipelineStatusCB(), enable_text);
event.RunAndWaitForStatus(PIPELINE_OK);
}
+ void InitializeDemuxer() {
+ InitializeDemuxerText(false);
+ }
+
MOCK_METHOD2(OnReadDoneCalled, void(int, int64));
// Verifies that |buffer| has a specific |size| and |timestamp|.
@@ -199,7 +204,7 @@ TEST_F(FFmpegDemuxerTest, Initialize_OpenFails) {
// Simulate avformat_open_input() failing.
CreateDemuxer("ten_byte_file");
WaitableMessageLoopEvent event;
- demuxer_->Initialize(&host_, event.GetPipelineStatusCB());
+ demuxer_->Initialize(&host_, event.GetPipelineStatusCB(), true);
event.RunAndWaitForStatus(DEMUXER_ERROR_COULD_NOT_OPEN);
}
@@ -217,7 +222,7 @@ TEST_F(FFmpegDemuxerTest, Initialize_NoStreams) {
// Open a file with no streams whatsoever.
CreateDemuxer("no_streams.webm");
WaitableMessageLoopEvent event;
- demuxer_->Initialize(&host_, event.GetPipelineStatusCB());
+ demuxer_->Initialize(&host_, event.GetPipelineStatusCB(), true);
event.RunAndWaitForStatus(DEMUXER_ERROR_NO_SUPPORTED_STREAMS);
}
@@ -225,7 +230,7 @@ TEST_F(FFmpegDemuxerTest, Initialize_NoAudioVideo) {
// Open a file containing streams but none of which are audio/video streams.
CreateDemuxer("no_audio_video.webm");
WaitableMessageLoopEvent event;
- demuxer_->Initialize(&host_, event.GetPipelineStatusCB());
+ demuxer_->Initialize(&host_, event.GetPipelineStatusCB(), true);
event.RunAndWaitForStatus(DEMUXER_ERROR_NO_SUPPORTED_STREAMS);
}
@@ -298,6 +303,36 @@ TEST_F(FFmpegDemuxerTest, Initialize_Multitrack) {
EXPECT_FALSE(demuxer_->GetStream(DemuxerStream::UNKNOWN));
}
+TEST_F(FFmpegDemuxerTest, Initialize_MultitrackText) {
+ // Open a file containing the following streams:
+ // Stream #0: Video (VP8)
+ // Stream #1: Audio (Vorbis)
+ // Stream #2: Text (WebVTT)
+
+ CreateDemuxer("bear-vp8-webvtt.webm");
+ DemuxerStream* text_stream = NULL;
+ EXPECT_CALL(host_, AddTextStream(_, _))
+ .WillOnce(SaveArg<0>(&text_stream));
+ InitializeDemuxerText(true);
+ ASSERT_TRUE(text_stream);
+ EXPECT_EQ(DemuxerStream::TEXT, text_stream->type());
+
+ // Video stream should be VP8.
+ DemuxerStream* stream = demuxer_->GetStream(DemuxerStream::VIDEO);
+ ASSERT_TRUE(stream);
+ EXPECT_EQ(DemuxerStream::VIDEO, stream->type());
+ EXPECT_EQ(kCodecVP8, stream->video_decoder_config().codec());
+
+ // Audio stream should be Vorbis.
+ stream = demuxer_->GetStream(DemuxerStream::AUDIO);
+ ASSERT_TRUE(stream);
+ EXPECT_EQ(DemuxerStream::AUDIO, stream->type());
+ EXPECT_EQ(kCodecVorbis, stream->audio_decoder_config().codec());
+
+ // Unknown stream should never be present.
+ EXPECT_FALSE(demuxer_->GetStream(DemuxerStream::UNKNOWN));
+}
+
TEST_F(FFmpegDemuxerTest, Initialize_Encrypted) {
EXPECT_CALL(*this, NeedKeyCBMock(kWebMEncryptInitDataType, NotNull(),
DecryptConfig::kDecryptionKeySize))
@@ -337,6 +372,23 @@ TEST_F(FFmpegDemuxerTest, Read_Video) {
message_loop_.Run();
}
+TEST_F(FFmpegDemuxerTest, Read_Text) {
+ // We test that on a successful text packet read.
+ CreateDemuxer("bear-vp8-webvtt.webm");
+ DemuxerStream* text_stream = NULL;
+ EXPECT_CALL(host_, AddTextStream(_, _))
+ .WillOnce(SaveArg<0>(&text_stream));
+ InitializeDemuxerText(true);
+ ASSERT_TRUE(text_stream);
+ EXPECT_EQ(DemuxerStream::TEXT, text_stream->type());
+
+ text_stream->Read(NewReadCB(FROM_HERE, 31, 0));
+ message_loop_.Run();
+
+ text_stream->Read(NewReadCB(FROM_HERE, 19, 500000));
+ message_loop_.Run();
+}
+
TEST_F(FFmpegDemuxerTest, Read_VideoNonZeroStart) {
// Test the start time is the first timestamp of the video and audio stream.
CreateDemuxer("nonzero-start-time.webm");
@@ -365,6 +417,26 @@ TEST_F(FFmpegDemuxerTest, Read_EndOfStream) {
ReadUntilEndOfStream();
}
+TEST_F(FFmpegDemuxerTest, Read_EndOfStreamText) {
+ // Verify that end of stream buffers are created.
+ CreateDemuxer("bear-vp8-webvtt.webm");
+ DemuxerStream* text_stream = NULL;
+ EXPECT_CALL(host_, AddTextStream(_, _))
+ .WillOnce(SaveArg<0>(&text_stream));
+ InitializeDemuxerText(true);
+ ASSERT_TRUE(text_stream);
+ EXPECT_EQ(DemuxerStream::TEXT, text_stream->type());
+
+ bool got_eos_buffer = false;
+ const int kMaxBuffers = 10;
+ for (int i = 0; !got_eos_buffer && i < kMaxBuffers; i++) {
+ text_stream->Read(base::Bind(&EosOnReadDone, &got_eos_buffer));
+ message_loop_.Run();
+ }
+
+ EXPECT_TRUE(got_eos_buffer);
+}
+
TEST_F(FFmpegDemuxerTest, Read_EndOfStream_NoDuration) {
// Verify that end of stream buffers are created.
CreateDemuxer("bear-320x240.webm");
@@ -413,6 +485,58 @@ TEST_F(FFmpegDemuxerTest, Seek) {
message_loop_.Run();
}
+TEST_F(FFmpegDemuxerTest, SeekText) {
+ // We're testing that the demuxer frees all queued packets when it receives
+ // a Seek().
+ CreateDemuxer("bear-vp8-webvtt.webm");
+ DemuxerStream* text_stream = NULL;
+ EXPECT_CALL(host_, AddTextStream(_, _))
+ .WillOnce(SaveArg<0>(&text_stream));
+ InitializeDemuxerText(true);
+ ASSERT_TRUE(text_stream);
+ EXPECT_EQ(DemuxerStream::TEXT, text_stream->type());
+
+ // Get our streams.
+ DemuxerStream* video = demuxer_->GetStream(DemuxerStream::VIDEO);
+ DemuxerStream* audio = demuxer_->GetStream(DemuxerStream::AUDIO);
+ ASSERT_TRUE(video);
+ ASSERT_TRUE(audio);
+
+ // Read a text packet and release it.
+ text_stream->Read(NewReadCB(FROM_HERE, 31, 0));
+ message_loop_.Run();
+
+ // Issue a simple forward seek, which should discard queued packets.
+ WaitableMessageLoopEvent event;
+ demuxer_->Seek(base::TimeDelta::FromMicroseconds(1000000),
+ event.GetPipelineStatusCB());
+ event.RunAndWaitForStatus(PIPELINE_OK);
+
+ // Audio read #1.
+ audio->Read(NewReadCB(FROM_HERE, 145, 803000));
+ message_loop_.Run();
+
+ // Audio read #2.
+ audio->Read(NewReadCB(FROM_HERE, 148, 826000));
+ message_loop_.Run();
+
+ // Video read #1.
+ video->Read(NewReadCB(FROM_HERE, 5425, 801000));
+ message_loop_.Run();
+
+ // Video read #2.
+ video->Read(NewReadCB(FROM_HERE, 1906, 834000));
+ message_loop_.Run();
+
+ // Text read #1.
+ text_stream->Read(NewReadCB(FROM_HERE, 19, 500000));
+ message_loop_.Run();
+
+ // Text read #2.
+ text_stream->Read(NewReadCB(FROM_HERE, 19, 1000000));
+ message_loop_.Run();
+}
+
class MockReadCB {
public:
MockReadCB() {}
diff --git a/media/filters/pipeline_integration_test.cc b/media/filters/pipeline_integration_test.cc
index 221e641..8669297 100644
--- a/media/filters/pipeline_integration_test.cc
+++ b/media/filters/pipeline_integration_test.cc
@@ -240,8 +240,6 @@ class MockMediaSource {
base::Unretained(this)),
base::Bind(&MockMediaSource::DemuxerNeedKey,
base::Unretained(this)),
- base::Bind(&MockMediaSource::OnTextTrack,
- base::Unretained(this)),
LogCB())),
owned_chunk_demuxer_(chunk_demuxer_) {
@@ -343,12 +341,6 @@ class MockMediaSource {
need_key_cb_.Run(type, init_data);
}
- scoped_ptr<TextTrack> OnTextTrack(TextKind kind,
- const std::string& label,
- const std::string& language) {
- return scoped_ptr<TextTrack>();
- }
-
private:
base::FilePath file_path_;
scoped_refptr<DecoderBuffer> file_data_;
@@ -1087,7 +1079,8 @@ TEST_F(PipelineIntegrationTest, BasicPlayback_VP8A_WebM) {
}
// Verify that VP8 video with inband text track can be played back.
-TEST_F(PipelineIntegrationTest, BasicPlayback_VP8_WebVTT_WebM) {
+TEST_F(PipelineIntegrationTest,
+ BasicPlayback_VP8_WebVTT_WebM) {
ASSERT_TRUE(Start(GetTestDataFilePath("bear-vp8-webvtt.webm"),
PIPELINE_OK));
Play();
diff --git a/media/filters/source_buffer_stream.cc b/media/filters/source_buffer_stream.cc
index 12a3bff..77fb279 100644
--- a/media/filters/source_buffer_stream.cc
+++ b/media/filters/source_buffer_stream.cc
@@ -368,6 +368,27 @@ SourceBufferStream::SourceBufferStream(const VideoDecoderConfig& video_config,
video_configs_.push_back(video_config);
}
+SourceBufferStream::SourceBufferStream(const TextTrackConfig& text_config,
+ const LogCB& log_cb)
+ : log_cb_(log_cb),
+ current_config_index_(0),
+ append_config_index_(0),
+ text_track_config_(text_config),
+ seek_pending_(false),
+ end_of_stream_(false),
+ seek_buffer_timestamp_(kNoTimestamp()),
+ selected_range_(NULL),
+ media_segment_start_time_(kNoTimestamp()),
+ range_for_next_append_(ranges_.end()),
+ new_media_segment_(false),
+ last_appended_buffer_timestamp_(kNoTimestamp()),
+ last_appended_buffer_is_keyframe_(false),
+ last_output_buffer_timestamp_(kNoTimestamp()),
+ max_interbuffer_distance_(kNoTimestamp()),
+ memory_limit_(kDefaultAudioMemoryLimit),
+ config_change_pending_(false) {
+}
+
SourceBufferStream::~SourceBufferStream() {
while (!ranges_.empty()) {
delete ranges_.front();
@@ -1215,6 +1236,10 @@ const VideoDecoderConfig& SourceBufferStream::GetCurrentVideoDecoderConfig() {
return video_configs_[current_config_index_];
}
+const TextTrackConfig& SourceBufferStream::GetCurrentTextTrackConfig() {
+ return text_track_config_;
+}
+
base::TimeDelta SourceBufferStream::GetMaxInterbufferDistance() const {
if (max_interbuffer_distance_ == kNoTimestamp())
return base::TimeDelta::FromMilliseconds(kDefaultBufferDurationInMs);
diff --git a/media/filters/source_buffer_stream.h b/media/filters/source_buffer_stream.h
index e0848af..4b00504 100644
--- a/media/filters/source_buffer_stream.h
+++ b/media/filters/source_buffer_stream.h
@@ -22,6 +22,7 @@
#include "media/base/media_log.h"
#include "media/base/ranges.h"
#include "media/base/stream_parser_buffer.h"
+#include "media/base/text_track_config.h"
#include "media/base/video_decoder_config.h"
namespace media {
@@ -49,6 +50,8 @@ class MEDIA_EXPORT SourceBufferStream {
const LogCB& log_cb);
SourceBufferStream(const VideoDecoderConfig& video_config,
const LogCB& log_cb);
+ SourceBufferStream(const TextTrackConfig& text_config,
+ const LogCB& log_cb);
~SourceBufferStream();
@@ -107,6 +110,7 @@ class MEDIA_EXPORT SourceBufferStream {
const AudioDecoderConfig& GetCurrentAudioDecoderConfig();
const VideoDecoderConfig& GetCurrentVideoDecoderConfig();
+ const TextTrackConfig& GetCurrentTextTrackConfig();
// Notifies this object that the audio config has changed and buffers in
// future Append() calls should be associated with this new config.
@@ -307,6 +311,9 @@ class MEDIA_EXPORT SourceBufferStream {
std::vector<AudioDecoderConfig> audio_configs_;
std::vector<VideoDecoderConfig> video_configs_;
+ // Holds the text config for this stream.
+ TextTrackConfig text_track_config_;
+
// True if more data needs to be appended before the Seek() can complete,
// false if no Seek() has been requested or the Seek() is completed.
bool seek_pending_;
diff --git a/media/filters/webvtt_util.h b/media/filters/webvtt_util.h
new file mode 100644
index 0000000..b71b66f
--- /dev/null
+++ b/media/filters/webvtt_util.h
@@ -0,0 +1,30 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef MEDIA_FILTERS_WEBVTT_UTIL_H_
+#define MEDIA_FILTERS_WEBVTT_UTIL_H_
+
+#include <vector>
+
+namespace media {
+
+// Utility function to create side data item for decoder buffer.
+template<typename T>
+void MakeSideData(T id_begin, T id_end,
+ T settings_begin, T settings_end,
+ std::vector<uint8>* side_data) {
+ // The DecoderBuffer only supports a single side data item. In the case of
+ // a WebVTT cue, we can have potentially two side data items. In order to
+ // avoid disrupting DecoderBuffer any more than we need to, we copy both
+ // side data items onto a single one, and terminate each with a NUL marker.
+ side_data->clear();
+ side_data->insert(side_data->end(), id_begin, id_end);
+ side_data->push_back(0);
+ side_data->insert(side_data->end(), settings_begin, settings_end);
+ side_data->push_back(0);
+}
+
+} // namespace media
+
+#endif // MEDIA_FILTERS_WEBVTT_UTIL_H_
diff --git a/media/media.gyp b/media/media.gyp
index d19ca4b..9bc4985 100644
--- a/media/media.gyp
+++ b/media/media.gyp
@@ -308,7 +308,13 @@
'base/stream_parser.h',
'base/stream_parser_buffer.cc',
'base/stream_parser_buffer.h',
+ 'base/text_cue.cc',
+ 'base/text_cue.h',
+ 'base/text_renderer.cc',
+ 'base/text_renderer.h',
'base/text_track.h',
+ 'base/text_track_config.cc',
+ 'base/text_track_config.h',
'base/user_input_monitor.cc',
'base/user_input_monitor.h',
'base/user_input_monitor_linux.cc',
@@ -388,6 +394,7 @@
'filters/video_renderer_base.h',
'filters/vpx_video_decoder.cc',
'filters/vpx_video_decoder.h',
+ 'filters/webvtt_util.h',
'filters/wsola_internals.cc',
'filters/wsola_internals.h',
'midi/midi_manager.cc',
@@ -933,6 +940,7 @@
'base/sinc_resampler_unittest.cc',
'base/test_data_util.cc',
'base/test_data_util.h',
+ 'base/text_renderer_unittest.cc',
'base/user_input_monitor_unittest.cc',
'base/vector_math_testing.h',
'base/vector_math_unittest.cc',
@@ -1157,6 +1165,8 @@
'base/fake_audio_render_callback.h',
'base/fake_audio_renderer_sink.cc',
'base/fake_audio_renderer_sink.h',
+ 'base/fake_text_track_stream.cc',
+ 'base/fake_text_track_stream.h',
'base/gmock_callback_support.h',
'base/mock_audio_renderer_sink.cc',
'base/mock_audio_renderer_sink.h',
diff --git a/media/mp2t/mp2t_stream_parser.cc b/media/mp2t/mp2t_stream_parser.cc
index 68fca5c..9666747 100644
--- a/media/mp2t/mp2t_stream_parser.cc
+++ b/media/mp2t/mp2t_stream_parser.cc
@@ -10,6 +10,7 @@
#include "media/base/audio_decoder_config.h"
#include "media/base/buffers.h"
#include "media/base/stream_parser_buffer.h"
+#include "media/base/text_track_config.h"
#include "media/base/video_decoder_config.h"
#include "media/mp2t/es_parser.h"
#include "media/mp2t/es_parser_adts.h"
@@ -166,9 +167,8 @@ void Mp2tStreamParser::Init(
const InitCB& init_cb,
const NewConfigCB& config_cb,
const NewBuffersCB& new_buffers_cb,
- const NewTextBuffersCB& text_cb,
+ const NewTextBuffersCB& /* text_cb */ ,
const NeedKeyCB& need_key_cb,
- const AddTextTrackCB& add_text_track_cb,
const NewMediaSegmentCB& new_segment_cb,
const base::Closure& end_of_segment_cb,
const LogCB& log_cb) {
@@ -476,7 +476,8 @@ bool Mp2tStreamParser::FinishInitializationIfNeeded() {
// Pass the config before invoking the initialization callback.
RCHECK(config_cb_.Run(queue_with_config.audio_config,
- queue_with_config.video_config));
+ queue_with_config.video_config,
+ TextTrackConfigMap()));
queue_with_config.is_config_sent = true;
// For Mpeg2 TS, the duration is not known.
@@ -585,7 +586,8 @@ bool Mp2tStreamParser::EmitRemainingBuffers() {
BufferQueueWithConfig& queue_with_config = buffer_queue_chain_.front();
if (!queue_with_config.is_config_sent) {
if (!config_cb_.Run(queue_with_config.audio_config,
- queue_with_config.video_config))
+ queue_with_config.video_config,
+ TextTrackConfigMap()))
return false;
queue_with_config.is_config_sent = true;
}
diff --git a/media/mp2t/mp2t_stream_parser.h b/media/mp2t/mp2t_stream_parser.h
index dcab559..13d0c99 100644
--- a/media/mp2t/mp2t_stream_parser.h
+++ b/media/mp2t/mp2t_stream_parser.h
@@ -35,7 +35,6 @@ class MEDIA_EXPORT Mp2tStreamParser : public StreamParser {
const NewBuffersCB& new_buffers_cb,
const NewTextBuffersCB& text_cb,
const NeedKeyCB& need_key_cb,
- const AddTextTrackCB& add_text_track_cb,
const NewMediaSegmentCB& new_segment_cb,
const base::Closure& end_of_segment_cb,
const LogCB& log_cb) OVERRIDE;
diff --git a/media/mp2t/mp2t_stream_parser_unittest.cc b/media/mp2t/mp2t_stream_parser_unittest.cc
index 12a3b95..418ce8d 100644
--- a/media/mp2t/mp2t_stream_parser_unittest.cc
+++ b/media/mp2t/mp2t_stream_parser_unittest.cc
@@ -14,6 +14,7 @@
#include "media/base/decoder_buffer.h"
#include "media/base/stream_parser_buffer.h"
#include "media/base/test_data_util.h"
+#include "media/base/text_track_config.h"
#include "media/base/video_decoder_config.h"
#include "media/mp2t/mp2t_stream_parser.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -60,7 +61,9 @@ class Mp2tStreamParserTest : public testing::Test {
<< ", dur=" << duration.InMilliseconds();
}
- bool OnNewConfig(const AudioDecoderConfig& ac, const VideoDecoderConfig& vc) {
+ bool OnNewConfig(const AudioDecoderConfig& ac,
+ const VideoDecoderConfig& vc,
+ const StreamParser::TextTrackConfigMap& tc) {
DVLOG(1) << "OnNewConfig: audio=" << ac.IsValidConfig()
<< ", video=" << vc.IsValidConfig();
return true;
@@ -101,23 +104,11 @@ class Mp2tStreamParserTest : public testing::Test {
return true;
}
- bool OnNewTextBuffers(TextTrack* text_track,
- const StreamParser::BufferQueue& buffers) {
- return true;
- }
-
void OnKeyNeeded(const std::string& type,
const std::vector<uint8>& init_data) {
DVLOG(1) << "OnKeyNeeded: " << init_data.size();
}
- scoped_ptr<TextTrack> OnAddTextTrack(
- TextKind kind,
- const std::string& label,
- const std::string& language) {
- return scoped_ptr<TextTrack>();
- }
-
void OnNewSegment() {
DVLOG(1) << "OnNewSegment";
}
@@ -134,12 +125,9 @@ class Mp2tStreamParserTest : public testing::Test {
base::Unretained(this)),
base::Bind(&Mp2tStreamParserTest::OnNewBuffers,
base::Unretained(this)),
- base::Bind(&Mp2tStreamParserTest::OnNewTextBuffers,
- base::Unretained(this)),
+ StreamParser::NewTextBuffersCB(),
base::Bind(&Mp2tStreamParserTest::OnKeyNeeded,
base::Unretained(this)),
- base::Bind(&Mp2tStreamParserTest::OnAddTextTrack,
- base::Unretained(this)),
base::Bind(&Mp2tStreamParserTest::OnNewSegment,
base::Unretained(this)),
base::Bind(&Mp2tStreamParserTest::OnEndOfSegment,
diff --git a/media/mp3/mp3_stream_parser.cc b/media/mp3/mp3_stream_parser.cc
index 5d2f8c1..b20756c 100644
--- a/media/mp3/mp3_stream_parser.cc
+++ b/media/mp3/mp3_stream_parser.cc
@@ -10,6 +10,7 @@
#include "media/base/bit_reader.h"
#include "media/base/buffers.h"
#include "media/base/stream_parser_buffer.h"
+#include "media/base/text_track_config.h"
#include "media/base/video_decoder_config.h"
#include "net/http/http_util.h"
@@ -119,7 +120,6 @@ void MP3StreamParser::Init(const InitCB& init_cb,
const NewBuffersCB& new_buffers_cb,
const NewTextBuffersCB& text_cb,
const NeedKeyCB& need_key_cb,
- const AddTextTrackCB& add_text_track_cb,
const NewMediaSegmentCB& new_segment_cb,
const base::Closure& end_of_segment_cb,
const LogCB& log_cb) {
@@ -410,7 +410,7 @@ int MP3StreamParser::ParseMP3Frame(const uint8* data,
timestamp_helper_->SetBaseTimestamp(base_timestamp);
VideoDecoderConfig video_config;
- bool success = config_cb_.Run(config_, video_config);
+ bool success = config_cb_.Run(config_, video_config, TextTrackConfigMap());
if (!init_cb_.is_null())
base::ResetAndReturn(&init_cb_).Run(success, kInfiniteDuration());
diff --git a/media/mp3/mp3_stream_parser.h b/media/mp3/mp3_stream_parser.h
index 97730ae..1e2e8c6 100644
--- a/media/mp3/mp3_stream_parser.h
+++ b/media/mp3/mp3_stream_parser.h
@@ -30,7 +30,6 @@ class MEDIA_EXPORT MP3StreamParser : public StreamParser {
const NewBuffersCB& new_buffers_cb,
const NewTextBuffersCB& text_cb,
const NeedKeyCB& need_key_cb,
- const AddTextTrackCB& add_text_track_cb,
const NewMediaSegmentCB& new_segment_cb,
const base::Closure& end_of_segment_cb,
const LogCB& log_cb) OVERRIDE;
diff --git a/media/mp3/mp3_stream_parser_unittest.cc b/media/mp3/mp3_stream_parser_unittest.cc
index 2e2b12e..f565093 100644
--- a/media/mp3/mp3_stream_parser_unittest.cc
+++ b/media/mp3/mp3_stream_parser_unittest.cc
@@ -7,6 +7,7 @@
#include "media/base/decoder_buffer.h"
#include "media/base/stream_parser_buffer.h"
#include "media/base/test_data_util.h"
+#include "media/base/text_track_config.h"
#include "media/base/video_decoder_config.h"
#include "media/mp3/mp3_stream_parser.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -44,7 +45,8 @@ class MP3StreamParserTest : public testing::Test {
}
bool OnNewConfig(const AudioDecoderConfig& audio_config,
- const VideoDecoderConfig& video_config) {
+ const VideoDecoderConfig& video_config,
+ const StreamParser::TextTrackConfigMap& text_config) {
DVLOG(1) << __FUNCTION__ << "(" << audio_config.IsValidConfig() << ", "
<< video_config.IsValidConfig() << ")";
EXPECT_TRUE(audio_config.IsValidConfig());
@@ -79,22 +81,11 @@ class MP3StreamParserTest : public testing::Test {
return true;
}
- bool OnNewTextBuffers(TextTrack* text_track,
- const StreamParser::BufferQueue& buffers) {
- return true;
- }
-
void OnKeyNeeded(const std::string& type,
const std::vector<uint8>& init_data) {
DVLOG(1) << __FUNCTION__ << "(" << type << ", " << init_data.size() << ")";
}
- scoped_ptr<TextTrack> OnAddTextTrack(TextKind kind,
- const std::string& label,
- const std::string& language) {
- return scoped_ptr<TextTrack>();
- }
-
void OnNewSegment() {
DVLOG(1) << __FUNCTION__;
results_stream_ << "NewSegment";
@@ -110,11 +101,8 @@ class MP3StreamParserTest : public testing::Test {
base::Bind(&MP3StreamParserTest::OnInitDone, base::Unretained(this)),
base::Bind(&MP3StreamParserTest::OnNewConfig, base::Unretained(this)),
base::Bind(&MP3StreamParserTest::OnNewBuffers, base::Unretained(this)),
- base::Bind(&MP3StreamParserTest::OnNewTextBuffers,
- base::Unretained(this)),
+ StreamParser::NewTextBuffersCB(),
base::Bind(&MP3StreamParserTest::OnKeyNeeded, base::Unretained(this)),
- base::Bind(&MP3StreamParserTest::OnAddTextTrack,
- base::Unretained(this)),
base::Bind(&MP3StreamParserTest::OnNewSegment, base::Unretained(this)),
base::Bind(&MP3StreamParserTest::OnEndOfSegment,
base::Unretained(this)),
diff --git a/media/mp4/mp4_stream_parser.cc b/media/mp4/mp4_stream_parser.cc
index 19855ab..db1b59b 100644
--- a/media/mp4/mp4_stream_parser.cc
+++ b/media/mp4/mp4_stream_parser.cc
@@ -10,6 +10,7 @@
#include "base/time/time.h"
#include "media/base/audio_decoder_config.h"
#include "media/base/stream_parser_buffer.h"
+#include "media/base/text_track_config.h"
#include "media/base/video_decoder_config.h"
#include "media/base/video_util.h"
#include "media/mp4/box_definitions.h"
@@ -45,7 +46,6 @@ void MP4StreamParser::Init(const InitCB& init_cb,
const NewBuffersCB& new_buffers_cb,
const NewTextBuffersCB& /* text_cb */ ,
const NeedKeyCB& need_key_cb,
- const AddTextTrackCB& /* add_text_track_cb */ ,
const NewMediaSegmentCB& new_segment_cb,
const base::Closure& end_of_segment_cb,
const LogCB& log_cb) {
@@ -292,7 +292,7 @@ bool MP4StreamParser::ParseMoov(BoxReader* reader) {
}
}
- RCHECK(config_cb_.Run(audio_config, video_config));
+ RCHECK(config_cb_.Run(audio_config, video_config, TextTrackConfigMap()));
base::TimeDelta duration;
if (moov_->extends.header.fragment_duration > 0) {
diff --git a/media/mp4/mp4_stream_parser.h b/media/mp4/mp4_stream_parser.h
index 81139d5..946513f 100644
--- a/media/mp4/mp4_stream_parser.h
+++ b/media/mp4/mp4_stream_parser.h
@@ -32,7 +32,6 @@ class MEDIA_EXPORT MP4StreamParser : public StreamParser {
const NewBuffersCB& new_buffers_cb,
const NewTextBuffersCB& text_cb,
const NeedKeyCB& need_key_cb,
- const AddTextTrackCB& add_text_track_cb,
const NewMediaSegmentCB& new_segment_cb,
const base::Closure& end_of_segment_cb,
const LogCB& log_cb) OVERRIDE;
diff --git a/media/mp4/mp4_stream_parser_unittest.cc b/media/mp4/mp4_stream_parser_unittest.cc
index 816a210..dd394c4 100644
--- a/media/mp4/mp4_stream_parser_unittest.cc
+++ b/media/mp4/mp4_stream_parser_unittest.cc
@@ -14,6 +14,7 @@
#include "media/base/decoder_buffer.h"
#include "media/base/stream_parser_buffer.h"
#include "media/base/test_data_util.h"
+#include "media/base/text_track_config.h"
#include "media/base/video_decoder_config.h"
#include "media/mp4/es_descriptor.h"
#include "media/mp4/mp4_stream_parser.h"
@@ -62,7 +63,9 @@ class MP4StreamParserTest : public testing::Test {
<< ", dur=" << duration.InMilliseconds();
}
- bool NewConfigF(const AudioDecoderConfig& ac, const VideoDecoderConfig& vc) {
+ bool NewConfigF(const AudioDecoderConfig& ac,
+ const VideoDecoderConfig& vc,
+ const StreamParser::TextTrackConfigMap& tc) {
DVLOG(1) << "NewConfigF: audio=" << ac.IsValidConfig()
<< ", video=" << vc.IsValidConfig();
configs_received_ = true;
@@ -88,11 +91,6 @@ class MP4StreamParserTest : public testing::Test {
return true;
}
- bool NewTextBuffersF(TextTrack* text_track,
- const StreamParser::BufferQueue& buffers) {
- return true;
- }
-
void KeyNeededF(const std::string& type,
const std::vector<uint8>& init_data) {
DVLOG(1) << "KeyNeededF: " << init_data.size();
@@ -100,13 +98,6 @@ class MP4StreamParserTest : public testing::Test {
EXPECT_FALSE(init_data.empty());
}
- scoped_ptr<TextTrack> AddTextTrackF(
- TextKind kind,
- const std::string& label,
- const std::string& language) {
- return scoped_ptr<TextTrack>();
- }
-
void NewSegmentF() {
DVLOG(1) << "NewSegmentF";
}
@@ -120,10 +111,8 @@ class MP4StreamParserTest : public testing::Test {
base::Bind(&MP4StreamParserTest::InitF, base::Unretained(this)),
base::Bind(&MP4StreamParserTest::NewConfigF, base::Unretained(this)),
base::Bind(&MP4StreamParserTest::NewBuffersF, base::Unretained(this)),
- base::Bind(&MP4StreamParserTest::NewTextBuffersF,
- base::Unretained(this)),
+ StreamParser::NewTextBuffersCB(),
base::Bind(&MP4StreamParserTest::KeyNeededF, base::Unretained(this)),
- base::Bind(&MP4StreamParserTest::AddTextTrackF, base::Unretained(this)),
base::Bind(&MP4StreamParserTest::NewSegmentF, base::Unretained(this)),
base::Bind(&MP4StreamParserTest::EndOfSegmentF,
base::Unretained(this)),
diff --git a/media/webm/webm_cluster_parser.cc b/media/webm/webm_cluster_parser.cc
index 87cccae..df9e4ce2 100644
--- a/media/webm/webm_cluster_parser.cc
+++ b/media/webm/webm_cluster_parser.cc
@@ -10,8 +10,10 @@
#include "base/sys_byteorder.h"
#include "media/base/buffers.h"
#include "media/base/decrypt_config.h"
+#include "media/filters/webvtt_util.h"
#include "media/webm/webm_constants.h"
#include "media/webm/webm_crypto_helpers.h"
+#include "media/webm/webm_webvtt_parser.h"
namespace media {
@@ -307,6 +309,7 @@ bool WebMClusterParser::OnBlock(bool is_simple_block, int track_num,
}
Track* track = NULL;
+ bool is_text = false;
std::string encryption_key_id;
if (track_num == audio_.track_num()) {
track = &audio_;
@@ -322,6 +325,7 @@ bool WebMClusterParser::OnBlock(bool is_simple_block, int track_num,
if (block_duration < 0) // not specified
return false;
track = text_track;
+ is_text = true;
} else {
MEDIA_LOG(log_cb_) << "Unexpected track number " << track_num;
return false;
@@ -339,9 +343,28 @@ bool WebMClusterParser::OnBlock(bool is_simple_block, int track_num,
bool is_keyframe =
is_simple_block ? (flags & 0x80) != 0 : track->IsKeyframe(data, size);
- scoped_refptr<StreamParserBuffer> buffer =
- StreamParserBuffer::CopyFrom(data, size, additional, additional_size,
- is_keyframe);
+ scoped_refptr<StreamParserBuffer> buffer;
+ if (!is_text) {
+ buffer = StreamParserBuffer::CopyFrom(data, size,
+ additional, additional_size,
+ is_keyframe);
+ } else {
+ std::string id, settings, content;
+ WebMWebVTTParser::Parse(data, size,
+ &id, &settings, &content);
+
+ std::vector<uint8> side_data;
+ MakeSideData(id.begin(), id.end(),
+ settings.begin(), settings.end(),
+ &side_data);
+
+ buffer = StreamParserBuffer::CopyFrom(
+ reinterpret_cast<const uint8*>(content.data()),
+ content.length(),
+ &side_data[0],
+ side_data.size(),
+ is_keyframe);
+ }
// Every encrypted Block has a signal byte and IV prepended to it. Current
// encrypted WebM request for comments specification is here
diff --git a/media/webm/webm_cluster_parser_unittest.cc b/media/webm/webm_cluster_parser_unittest.cc
index 5c5837f..fe5e83f 100644
--- a/media/webm/webm_cluster_parser_unittest.cc
+++ b/media/webm/webm_cluster_parser_unittest.cc
@@ -374,11 +374,9 @@ TEST_F(WebMClusterParserTest, IgnoredTracks) {
TEST_F(WebMClusterParserTest, ParseTextTracks) {
typedef WebMTracksParser::TextTracks TextTracks;
TextTracks text_tracks;
- WebMTracksParser::TextTrackInfo text_track_info;
- text_track_info.kind = kTextSubtitles;
text_tracks.insert(std::make_pair(TextTracks::key_type(kTextTrackNum),
- text_track_info));
+ TextTrackConfig(kTextSubtitles, "", "")));
parser_.reset(new WebMClusterParser(kTimecodeScale,
kAudioTrackNum,
@@ -410,12 +408,10 @@ TEST_F(WebMClusterParserTest, ParseTextTracks) {
TEST_F(WebMClusterParserTest, TextTracksSimpleBlock) {
typedef WebMTracksParser::TextTracks TextTracks;
- TextTracks text_tracks;
- WebMTracksParser::TextTrackInfo text_track_info;
+ WebMTracksParser::TextTracks text_tracks;
- text_track_info.kind = kTextSubtitles;
text_tracks.insert(std::make_pair(TextTracks::key_type(kTextTrackNum),
- text_track_info));
+ TextTrackConfig(kTextSubtitles, "", "")));
parser_.reset(new WebMClusterParser(kTimecodeScale,
kAudioTrackNum,
@@ -441,18 +437,15 @@ TEST_F(WebMClusterParserTest, TextTracksSimpleBlock) {
TEST_F(WebMClusterParserTest, ParseMultipleTextTracks) {
typedef WebMTracksParser::TextTracks TextTracks;
TextTracks text_tracks;
- WebMTracksParser::TextTrackInfo text_track_info;
const int kSubtitleTextTrackNum = kTextTrackNum;
const int kCaptionTextTrackNum = kTextTrackNum + 1;
- text_track_info.kind = kTextSubtitles;
text_tracks.insert(std::make_pair(TextTracks::key_type(kSubtitleTextTrackNum),
- text_track_info));
+ TextTrackConfig(kTextSubtitles, "", "")));
- text_track_info.kind = kTextCaptions;
text_tracks.insert(std::make_pair(TextTracks::key_type(kCaptionTextTrackNum),
- text_track_info));
+ TextTrackConfig(kTextCaptions, "", "")));
parser_.reset(new WebMClusterParser(kTimecodeScale,
kAudioTrackNum,
diff --git a/media/webm/webm_stream_parser.cc b/media/webm/webm_stream_parser.cc
index 12be449..8e7d055 100644
--- a/media/webm/webm_stream_parser.cc
+++ b/media/webm/webm_stream_parser.cc
@@ -8,7 +8,6 @@
#include "base/callback.h"
#include "base/logging.h"
-#include "base/stl_util.h"
#include "media/webm/webm_cluster_parser.h"
#include "media/webm/webm_constants.h"
#include "media/webm/webm_content_encodings.h"
@@ -24,7 +23,6 @@ WebMStreamParser::WebMStreamParser()
}
WebMStreamParser::~WebMStreamParser() {
- STLDeleteValues(&text_track_map_);
}
void WebMStreamParser::Init(const InitCB& init_cb,
@@ -32,7 +30,6 @@ void WebMStreamParser::Init(const InitCB& init_cb,
const NewBuffersCB& new_buffers_cb,
const NewTextBuffersCB& text_cb,
const NeedKeyCB& need_key_cb,
- const AddTextTrackCB& add_text_track_cb,
const NewMediaSegmentCB& new_segment_cb,
const base::Closure& end_of_segment_cb,
const LogCB& log_cb) {
@@ -41,7 +38,6 @@ void WebMStreamParser::Init(const InitCB& init_cb,
DCHECK(!init_cb.is_null());
DCHECK(!config_cb.is_null());
DCHECK(!new_buffers_cb.is_null());
- DCHECK(!text_cb.is_null());
DCHECK(!need_key_cb.is_null());
DCHECK(!new_segment_cb.is_null());
DCHECK(!end_of_segment_cb.is_null());
@@ -52,7 +48,6 @@ void WebMStreamParser::Init(const InitCB& init_cb,
new_buffers_cb_ = new_buffers_cb;
text_cb_ = text_cb;
need_key_cb_ = need_key_cb;
- add_text_track_cb_ = add_text_track_cb;
new_segment_cb_ = new_segment_cb;
end_of_segment_cb_ = end_of_segment_cb;
log_cb_ = log_cb;
@@ -175,7 +170,7 @@ int WebMStreamParser::ParseInfoAndTracks(const uint8* data, int size) {
cur_size -= result;
bytes_parsed += result;
- WebMTracksParser tracks_parser(log_cb_, add_text_track_cb_.is_null());
+ WebMTracksParser tracks_parser(log_cb_, text_cb_.is_null());
result = tracks_parser.Parse(cur, cur_size);
if (result <= 0)
@@ -199,37 +194,18 @@ int WebMStreamParser::ParseInfoAndTracks(const uint8* data, int size) {
if (video_config.is_encrypted())
FireNeedKey(tracks_parser.video_encryption_key_id());
- if (!config_cb_.Run(audio_config, video_config)) {
+ if (!config_cb_.Run(audio_config,
+ video_config,
+ tracks_parser.text_tracks())) {
DVLOG(1) << "New config data isn't allowed.";
return -1;
}
- typedef WebMTracksParser::TextTracks TextTracks;
- const TextTracks& text_tracks = tracks_parser.text_tracks();
-
- for (TextTracks::const_iterator itr = text_tracks.begin();
- itr != text_tracks.end(); ++itr) {
- const WebMTracksParser::TextTrackInfo& text_track_info = itr->second;
-
- // TODO(matthewjheaney): verify that WebVTT uses ISO 639-2 for lang
- scoped_ptr<TextTrack> text_track =
- add_text_track_cb_.Run(text_track_info.kind,
- text_track_info.name,
- text_track_info.language);
-
- // Assume ownership of pointer, and cache the text track object, for use
- // later when we have text track buffers. (The text track objects are
- // deallocated in the dtor for this class.)
-
- if (text_track)
- text_track_map_.insert(std::make_pair(itr->first, text_track.release()));
- }
-
cluster_parser_.reset(new WebMClusterParser(
info_parser.timecode_scale(),
tracks_parser.audio_track_num(),
tracks_parser.video_track_num(),
- text_tracks,
+ tracks_parser.text_tracks(),
tracks_parser.ignored_tracks(),
tracks_parser.audio_encryption_key_id(),
tracks_parser.video_encryption_key_id(),
@@ -301,14 +277,7 @@ int WebMStreamParser::ParseCluster(const uint8* data, int size) {
const BufferQueue* text_buffers;
while (text_track_iter(&text_track_num, &text_buffers)) {
- TextTrackMap::iterator find_result = text_track_map_.find(text_track_num);
-
- if (find_result == text_track_map_.end())
- continue;
-
- TextTrack* const text_track = find_result->second;
-
- if (!text_buffers->empty() && !text_cb_.Run(text_track, *text_buffers))
+ if (!text_buffers->empty() && !text_cb_.Run(text_track_num, *text_buffers))
return -1;
}
diff --git a/media/webm/webm_stream_parser.h b/media/webm/webm_stream_parser.h
index 9c3a6d5..aec484b 100644
--- a/media/webm/webm_stream_parser.h
+++ b/media/webm/webm_stream_parser.h
@@ -5,8 +5,6 @@
#ifndef MEDIA_WEBM_WEBM_STREAM_PARSER_H_
#define MEDIA_WEBM_WEBM_STREAM_PARSER_H_
-#include <map>
-
#include "base/callback_forward.h"
#include "base/memory/ref_counted.h"
#include "media/base/audio_decoder_config.h"
@@ -29,7 +27,6 @@ class WebMStreamParser : public StreamParser {
const NewBuffersCB& new_buffers_cb,
const NewTextBuffersCB& text_cb,
const NeedKeyCB& need_key_cb,
- const AddTextTrackCB& add_text_track_cb,
const NewMediaSegmentCB& new_segment_cb,
const base::Closure& end_of_segment_cb,
const LogCB& log_cb) OVERRIDE;
@@ -74,10 +71,6 @@ class WebMStreamParser : public StreamParser {
NewBuffersCB new_buffers_cb_;
NewTextBuffersCB text_cb_;
NeedKeyCB need_key_cb_;
- AddTextTrackCB add_text_track_cb_;
-
- typedef std::map<int, TextTrack* > TextTrackMap;
- TextTrackMap text_track_map_;
NewMediaSegmentCB new_segment_cb_;
base::Closure end_of_segment_cb_;
diff --git a/media/webm/webm_tracks_parser.cc b/media/webm/webm_tracks_parser.cc
index aa28d6f..47fade3 100644
--- a/media/webm/webm_tracks_parser.cc
+++ b/media/webm/webm_tracks_parser.cc
@@ -192,10 +192,9 @@ bool WebMTracksParser::OnListEnd(int id) {
MEDIA_LOG(log_cb_) << "Ignoring text track " << track_num_;
ignored_tracks_.insert(track_num_);
} else {
- TextTrackInfo& text_track_info = text_tracks_[track_num_];
- text_track_info.kind = text_track_kind;
- text_track_info.name = track_name_;
- text_track_info.language = track_language_;
+ text_tracks_[track_num_] = TextTrackConfig(text_track_kind,
+ track_name_,
+ track_language_);
}
} else {
MEDIA_LOG(log_cb_) << "Unexpected TrackType " << track_type_;
diff --git a/media/webm/webm_tracks_parser.h b/media/webm/webm_tracks_parser.h
index d399320..cd6f942 100644
--- a/media/webm/webm_tracks_parser.h
+++ b/media/webm/webm_tracks_parser.h
@@ -14,7 +14,7 @@
#include "base/memory/scoped_ptr.h"
#include "media/base/audio_decoder_config.h"
#include "media/base/media_log.h"
-#include "media/base/text_track.h"
+#include "media/base/text_track_config.h"
#include "media/base/video_decoder_config.h"
#include "media/webm/webm_audio_client.h"
#include "media/webm/webm_content_encodings_client.h"
@@ -56,13 +56,7 @@ class MEDIA_EXPORT WebMTracksParser : public WebMParserClient {
return video_decoder_config_;
}
- struct TextTrackInfo {
- TextKind kind;
- std::string name;
- std::string language;
- };
-
- typedef std::map<int64, TextTrackInfo> TextTracks;
+ typedef std::map<int, TextTrackConfig> TextTracks;
const TextTracks& text_tracks() const {
return text_tracks_;
diff --git a/media/webm/webm_tracks_parser_unittest.cc b/media/webm/webm_tracks_parser_unittest.cc
index ad001ab..8e10c31 100644
--- a/media/webm/webm_tracks_parser_unittest.cc
+++ b/media/webm/webm_tracks_parser_unittest.cc
@@ -37,10 +37,10 @@ static void VerifyTextTrackInfo(const uint8* buffer,
const WebMTracksParser::TextTracks::const_iterator itr = text_tracks.begin();
EXPECT_EQ(itr->first, 1); // track num
- const WebMTracksParser::TextTrackInfo& info = itr->second;
- EXPECT_EQ(info.kind, text_kind);
- EXPECT_TRUE(info.name == name);
- EXPECT_TRUE(info.language == language);
+ const TextTrackConfig& config = itr->second;
+ EXPECT_EQ(config.kind(), text_kind);
+ EXPECT_TRUE(config.label() == name);
+ EXPECT_TRUE(config.language() == language);
}
TEST_F(WebMTracksParserTest, SubtitleNoNameNoLang) {