| Index: chromecast/media/cma/backend/media_pipeline_device_fake.cc
|
| diff --git a/chromecast/media/cma/backend/media_pipeline_device_fake.cc b/chromecast/media/cma/backend/media_pipeline_device_fake.cc
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..98c05f6d8b13baffe4fda7fc10c423471ad94897
|
| --- /dev/null
|
| +++ b/chromecast/media/cma/backend/media_pipeline_device_fake.cc
|
| @@ -0,0 +1,574 @@
|
| +// Copyright 2014 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 "chromecast/media/cma/backend/media_pipeline_device_fake.h"
|
| +
|
| +#include <list>
|
| +
|
| +#include "base/bind.h"
|
| +#include "base/callback_helpers.h"
|
| +#include "base/location.h"
|
| +#include "base/logging.h"
|
| +#include "base/macros.h"
|
| +#include "base/message_loop/message_loop_proxy.h"
|
| +#include "chromecast/media/cma/backend/audio_pipeline_device.h"
|
| +#include "chromecast/media/cma/backend/media_clock_device.h"
|
| +#include "chromecast/media/cma/backend/media_component_device.h"
|
| +#include "chromecast/media/cma/backend/video_pipeline_device.h"
|
| +#include "chromecast/media/cma/base/decoder_buffer_base.h"
|
| +#include "media/base/audio_decoder_config.h"
|
| +#include "media/base/buffers.h"
|
| +#include "media/base/video_decoder_config.h"
|
| +
|
| +namespace chromecast {
|
| +namespace media {
|
| +
|
| +class MediaClockDeviceFake : public MediaClockDevice {
|
| + public:
|
| + MediaClockDeviceFake();
|
| + virtual ~MediaClockDeviceFake();
|
| +
|
| + // MediaClockDevice implementation.
|
| + virtual State GetState() const OVERRIDE;
|
| + virtual bool SetState(State new_state) OVERRIDE;
|
| + virtual bool ResetTimeline(base::TimeDelta time) OVERRIDE;
|
| + virtual bool SetRate(float rate) OVERRIDE;
|
| + virtual base::TimeDelta GetTime() OVERRIDE;
|
| +
|
| + private:
|
| + State state_;
|
| +
|
| + // Media time sampled at STC time |stc_|.
|
| + base::TimeDelta media_time_;
|
| + base::TimeTicks stc_;
|
| +
|
| + float rate_;
|
| +
|
| + DISALLOW_COPY_AND_ASSIGN(MediaClockDeviceFake);
|
| +};
|
| +
|
| +MediaClockDeviceFake::MediaClockDeviceFake()
|
| + : state_(kStateUninitialized),
|
| + media_time_(::media::kNoTimestamp()) {
|
| + DetachFromThread();
|
| +}
|
| +
|
| +MediaClockDeviceFake::~MediaClockDeviceFake() {
|
| +}
|
| +
|
| +MediaClockDevice::State MediaClockDeviceFake::GetState() const {
|
| + DCHECK(CalledOnValidThread());
|
| + return state_;
|
| +}
|
| +
|
| +bool MediaClockDeviceFake::SetState(State new_state) {
|
| + DCHECK(CalledOnValidThread());
|
| + if (!MediaClockDevice::IsValidStateTransition(state_, new_state))
|
| + return false;
|
| +
|
| + if (new_state == state_)
|
| + return true;
|
| +
|
| + state_ = new_state;
|
| +
|
| + if (state_ == kStateRunning) {
|
| + stc_ = base::TimeTicks::Now();
|
| + DCHECK(media_time_ != ::media::kNoTimestamp());
|
| + return true;
|
| + }
|
| +
|
| + if (state_ == kStateIdle) {
|
| + media_time_ = ::media::kNoTimestamp();
|
| + return true;
|
| + }
|
| +
|
| + return true;
|
| +}
|
| +
|
| +bool MediaClockDeviceFake::ResetTimeline(base::TimeDelta time) {
|
| + DCHECK(CalledOnValidThread());
|
| + DCHECK_EQ(state_, kStateIdle);
|
| + media_time_ = time;
|
| + return true;
|
| +}
|
| +
|
| +bool MediaClockDeviceFake::SetRate(float rate) {
|
| + DCHECK(CalledOnValidThread());
|
| + if (state_ == kStateRunning) {
|
| + base::TimeTicks now = base::TimeTicks::Now();
|
| + media_time_ = media_time_ + (now - stc_) * rate_;
|
| + stc_ = now;
|
| + }
|
| +
|
| + rate_ = rate;
|
| + return true;
|
| +}
|
| +
|
| +base::TimeDelta MediaClockDeviceFake::GetTime() {
|
| + DCHECK(CalledOnValidThread());
|
| + if (state_ != kStateRunning)
|
| + return media_time_;
|
| +
|
| + if (media_time_ == ::media::kNoTimestamp())
|
| + return ::media::kNoTimestamp();
|
| +
|
| + base::TimeTicks now = base::TimeTicks::Now();
|
| + base::TimeDelta interpolated_media_time =
|
| + media_time_ + (now - stc_) * rate_;
|
| + return interpolated_media_time;
|
| +}
|
| +
|
| +
|
| +namespace {
|
| +
|
| +// Maximum number of frames that can be buffered.
|
| +const size_t kMaxFrameCount = 20;
|
| +
|
| +} // namespace
|
| +
|
| +class MediaComponentDeviceFake : public MediaComponentDevice {
|
| + public:
|
| + explicit MediaComponentDeviceFake(MediaClockDeviceFake* media_clock_device);
|
| + virtual ~MediaComponentDeviceFake();
|
| +
|
| + // MediaComponentDevice implementation.
|
| + virtual void SetClient(const Client& client) OVERRIDE;
|
| + virtual State GetState() const OVERRIDE;
|
| + virtual bool SetState(State new_state) OVERRIDE;
|
| + virtual bool SetStartPts(base::TimeDelta time) OVERRIDE;
|
| + virtual FrameStatus PushFrame(
|
| + const scoped_refptr<DecryptContext>& decrypt_context,
|
| + const scoped_refptr<DecoderBufferBase>& buffer,
|
| + const FrameStatusCB& completion_cb) OVERRIDE;
|
| + virtual base::TimeDelta GetRenderingTime() const OVERRIDE;
|
| + virtual base::TimeDelta GetRenderingDelay() const OVERRIDE;
|
| + virtual bool GetStatistics(Statistics* stats) const OVERRIDE;
|
| +
|
| + private:
|
| + struct FakeDecoderBuffer {
|
| + FakeDecoderBuffer();
|
| + ~FakeDecoderBuffer();
|
| +
|
| + // Buffer size.
|
| + size_t size;
|
| +
|
| + // Presentation timestamp.
|
| + base::TimeDelta pts;
|
| + };
|
| +
|
| + void RenderTask();
|
| +
|
| + MediaClockDeviceFake* const media_clock_device_;
|
| + Client client_;
|
| +
|
| + State state_;
|
| +
|
| + // Indicate whether the end of stream has been received.
|
| + bool is_eos_;
|
| +
|
| + // Media time of the last rendered audio sample.
|
| + base::TimeDelta rendering_time_;
|
| +
|
| + // Frame decoded/rendered since the pipeline left the idle state.
|
| + uint64 decoded_frame_count_;
|
| + uint64 decoded_byte_count_;
|
| +
|
| + // List of frames not rendered yet.
|
| + std::list<FakeDecoderBuffer> frames_;
|
| +
|
| + // Indicate whether there is a scheduled rendering task.
|
| + bool scheduled_rendering_task_;
|
| +
|
| + // Pending frame.
|
| + scoped_refptr<DecoderBufferBase> pending_buffer_;
|
| + FrameStatusCB frame_pushed_cb_;
|
| +
|
| + base::WeakPtr<MediaComponentDeviceFake> weak_this_;
|
| + base::WeakPtrFactory<MediaComponentDeviceFake> weak_factory_;
|
| +
|
| + DISALLOW_COPY_AND_ASSIGN(MediaComponentDeviceFake);
|
| +};
|
| +
|
| +MediaComponentDeviceFake::FakeDecoderBuffer::FakeDecoderBuffer()
|
| + : size(0) {
|
| +}
|
| +
|
| +MediaComponentDeviceFake::FakeDecoderBuffer::~FakeDecoderBuffer() {
|
| +}
|
| +
|
| +MediaComponentDeviceFake::MediaComponentDeviceFake(
|
| + MediaClockDeviceFake* media_clock_device)
|
| + : media_clock_device_(media_clock_device),
|
| + state_(kStateUninitialized),
|
| + rendering_time_(::media::kNoTimestamp()),
|
| + decoded_frame_count_(0),
|
| + decoded_byte_count_(0),
|
| + scheduled_rendering_task_(false),
|
| + weak_factory_(this) {
|
| + weak_this_ = weak_factory_.GetWeakPtr();
|
| + DetachFromThread();
|
| +}
|
| +
|
| +MediaComponentDeviceFake::~MediaComponentDeviceFake() {
|
| +}
|
| +
|
| +void MediaComponentDeviceFake::SetClient(const Client& client) {
|
| + DCHECK(CalledOnValidThread());
|
| + client_ = client;
|
| +}
|
| +
|
| +MediaComponentDevice::State MediaComponentDeviceFake::GetState() const {
|
| + DCHECK(CalledOnValidThread());
|
| + return state_;
|
| +}
|
| +
|
| +bool MediaComponentDeviceFake::SetState(State new_state) {
|
| + DCHECK(CalledOnValidThread());
|
| + if (!MediaComponentDevice::IsValidStateTransition(state_, new_state))
|
| + return false;
|
| + state_ = new_state;
|
| +
|
| + if (state_ == kStateIdle) {
|
| + // Back to the idle state: reset a bunch of parameters.
|
| + is_eos_ = false;
|
| + rendering_time_ = ::media::kNoTimestamp();
|
| + decoded_frame_count_ = 0;
|
| + decoded_byte_count_ = 0;
|
| + frames_.clear();
|
| + pending_buffer_ = scoped_refptr<DecoderBufferBase>();
|
| + frame_pushed_cb_.Reset();
|
| + return true;
|
| + }
|
| +
|
| + if (state_ == kStateRunning) {
|
| + if (!scheduled_rendering_task_) {
|
| + scheduled_rendering_task_ = true;
|
| + base::MessageLoopProxy::current()->PostTask(
|
| + FROM_HERE,
|
| + base::Bind(&MediaComponentDeviceFake::RenderTask, weak_this_));
|
| + }
|
| + return true;
|
| + }
|
| +
|
| + return true;
|
| +}
|
| +
|
| +bool MediaComponentDeviceFake::SetStartPts(base::TimeDelta time) {
|
| + DCHECK(CalledOnValidThread());
|
| + DCHECK_EQ(state_, kStateIdle);
|
| + rendering_time_ = time;
|
| + return true;
|
| +}
|
| +
|
| +MediaComponentDevice::FrameStatus MediaComponentDeviceFake::PushFrame(
|
| + const scoped_refptr<DecryptContext>& decrypt_context,
|
| + const scoped_refptr<DecoderBufferBase>& buffer,
|
| + const FrameStatusCB& completion_cb) {
|
| + DCHECK(CalledOnValidThread());
|
| + DCHECK(state_ == kStatePaused || state_ == kStateRunning);
|
| + DCHECK(!is_eos_);
|
| + DCHECK(!pending_buffer_.get());
|
| + DCHECK(buffer.get());
|
| +
|
| + if (buffer->end_of_stream()) {
|
| + is_eos_ = true;
|
| + return kFrameSuccess;
|
| + }
|
| +
|
| + if (frames_.size() > kMaxFrameCount) {
|
| + pending_buffer_ = buffer;
|
| + frame_pushed_cb_ = completion_cb;
|
| + return kFramePending;
|
| + }
|
| +
|
| + FakeDecoderBuffer fake_buffer;
|
| + fake_buffer.size = buffer->data_size();
|
| + fake_buffer.pts = buffer->timestamp();
|
| + frames_.push_back(fake_buffer);
|
| + return kFrameSuccess;
|
| +}
|
| +
|
| +base::TimeDelta MediaComponentDeviceFake::GetRenderingTime() const {
|
| + return rendering_time_;
|
| +}
|
| +
|
| +base::TimeDelta MediaComponentDeviceFake::GetRenderingDelay() const {
|
| + NOTIMPLEMENTED();
|
| + return ::media::kNoTimestamp();
|
| +}
|
| +
|
| +void MediaComponentDeviceFake::RenderTask() {
|
| + scheduled_rendering_task_ = false;
|
| +
|
| + if (state_ != kStateRunning)
|
| + return;
|
| +
|
| + base::TimeDelta media_time = media_clock_device_->GetTime();
|
| + if (media_time == ::media::kNoTimestamp()) {
|
| + scheduled_rendering_task_ = true;
|
| + base::MessageLoopProxy::current()->PostDelayedTask(
|
| + FROM_HERE,
|
| + base::Bind(&MediaComponentDeviceFake::RenderTask, weak_this_),
|
| + base::TimeDelta::FromMilliseconds(50));
|
| + return;
|
| + }
|
| +
|
| + while (!frames_.empty() && frames_.front().pts <= media_time) {
|
| + rendering_time_ = frames_.front().pts;
|
| + decoded_frame_count_++;
|
| + decoded_byte_count_ += frames_.front().size;
|
| + frames_.pop_front();
|
| + if (pending_buffer_.get()) {
|
| + FakeDecoderBuffer fake_buffer;
|
| + fake_buffer.size = pending_buffer_->data_size();
|
| + fake_buffer.pts = pending_buffer_->timestamp();
|
| + frames_.push_back(fake_buffer);
|
| + pending_buffer_ = scoped_refptr<DecoderBufferBase>();
|
| + base::ResetAndReturn(&frame_pushed_cb_).Run(kFrameSuccess);
|
| + }
|
| + }
|
| +
|
| + if (frames_.empty() && is_eos_) {
|
| + if (!client_.eos_cb.is_null())
|
| + client_.eos_cb.Run();
|
| + return;
|
| + }
|
| +
|
| + scheduled_rendering_task_ = true;
|
| + base::MessageLoopProxy::current()->PostDelayedTask(
|
| + FROM_HERE,
|
| + base::Bind(&MediaComponentDeviceFake::RenderTask, weak_this_),
|
| + base::TimeDelta::FromMilliseconds(50));
|
| +}
|
| +
|
| +bool MediaComponentDeviceFake::GetStatistics(Statistics* stats) const {
|
| + if (state_ != kStateRunning)
|
| + return false;
|
| +
|
| + // Note: what is returned here is not the number of samples but the number of
|
| + // frames. The value is different for audio.
|
| + stats->decoded_bytes = decoded_byte_count_;
|
| + stats->decoded_samples = decoded_frame_count_;
|
| + stats->dropped_samples = 0;
|
| + return true;
|
| +}
|
| +
|
| +
|
| +class AudioPipelineDeviceFake : public AudioPipelineDevice {
|
| + public:
|
| + explicit AudioPipelineDeviceFake(MediaClockDeviceFake* media_clock_device);
|
| + virtual ~AudioPipelineDeviceFake();
|
| +
|
| + // AudioPipelineDevice implementation.
|
| + virtual void SetClient(const Client& client) OVERRIDE;
|
| + virtual State GetState() const OVERRIDE;
|
| + virtual bool SetState(State new_state) OVERRIDE;
|
| + virtual bool SetStartPts(base::TimeDelta time) OVERRIDE;
|
| + virtual FrameStatus PushFrame(
|
| + const scoped_refptr<DecryptContext>& decrypt_context,
|
| + const scoped_refptr<DecoderBufferBase>& buffer,
|
| + const FrameStatusCB& completion_cb) OVERRIDE;
|
| + virtual base::TimeDelta GetRenderingTime() const OVERRIDE;
|
| + virtual base::TimeDelta GetRenderingDelay() const OVERRIDE;
|
| + virtual bool SetConfig(const ::media::AudioDecoderConfig& config) OVERRIDE;
|
| + virtual void SetStreamVolumeMultiplier(float multiplier) OVERRIDE;
|
| + virtual bool GetStatistics(Statistics* stats) const OVERRIDE;
|
| +
|
| + private:
|
| + scoped_ptr<MediaComponentDeviceFake> fake_pipeline_;
|
| +
|
| + ::media::AudioDecoderConfig config_;
|
| +
|
| + DISALLOW_COPY_AND_ASSIGN(AudioPipelineDeviceFake);
|
| +};
|
| +
|
| +AudioPipelineDeviceFake::AudioPipelineDeviceFake(
|
| + MediaClockDeviceFake* media_clock_device)
|
| + : fake_pipeline_(new MediaComponentDeviceFake(media_clock_device)) {
|
| + DetachFromThread();
|
| +}
|
| +
|
| +AudioPipelineDeviceFake::~AudioPipelineDeviceFake() {
|
| +}
|
| +
|
| +void AudioPipelineDeviceFake::SetClient(const Client& client) {
|
| + fake_pipeline_->SetClient(client);
|
| +}
|
| +
|
| +MediaComponentDevice::State AudioPipelineDeviceFake::GetState() const {
|
| + return fake_pipeline_->GetState();
|
| +}
|
| +
|
| +bool AudioPipelineDeviceFake::SetState(State new_state) {
|
| + bool success = fake_pipeline_->SetState(new_state);
|
| + if (!success)
|
| + return false;
|
| +
|
| + if (new_state == kStateIdle) {
|
| + DCHECK(config_.IsValidConfig());
|
| + }
|
| + if (new_state == kStateUninitialized) {
|
| + config_ = ::media::AudioDecoderConfig();
|
| + }
|
| + return true;
|
| +}
|
| +
|
| +bool AudioPipelineDeviceFake::SetStartPts(base::TimeDelta time) {
|
| + return fake_pipeline_->SetStartPts(time);
|
| +}
|
| +
|
| +MediaComponentDevice::FrameStatus AudioPipelineDeviceFake::PushFrame(
|
| + const scoped_refptr<DecryptContext>& decrypt_context,
|
| + const scoped_refptr<DecoderBufferBase>& buffer,
|
| + const FrameStatusCB& completion_cb) {
|
| + return fake_pipeline_->PushFrame(decrypt_context, buffer, completion_cb);
|
| +}
|
| +
|
| +base::TimeDelta AudioPipelineDeviceFake::GetRenderingTime() const {
|
| + return fake_pipeline_->GetRenderingTime();
|
| +}
|
| +
|
| +base::TimeDelta AudioPipelineDeviceFake::GetRenderingDelay() const {
|
| + return fake_pipeline_->GetRenderingDelay();
|
| +}
|
| +
|
| +bool AudioPipelineDeviceFake::SetConfig(
|
| + const ::media::AudioDecoderConfig& config) {
|
| + DCHECK(CalledOnValidThread());
|
| + if (!config.IsValidConfig())
|
| + return false;
|
| + config_ = config;
|
| + return true;
|
| +}
|
| +
|
| +void AudioPipelineDeviceFake::SetStreamVolumeMultiplier(float multiplier) {
|
| + DCHECK(CalledOnValidThread());
|
| +}
|
| +
|
| +bool AudioPipelineDeviceFake::GetStatistics(Statistics* stats) const {
|
| + return fake_pipeline_->GetStatistics(stats);
|
| +}
|
| +
|
| +
|
| +class VideoPipelineDeviceFake : public VideoPipelineDevice {
|
| + public:
|
| + explicit VideoPipelineDeviceFake(MediaClockDeviceFake* media_clock_device);
|
| + virtual ~VideoPipelineDeviceFake();
|
| +
|
| + // VideoPipelineDevice implementation.
|
| + virtual void SetClient(const Client& client) OVERRIDE;
|
| + virtual State GetState() const OVERRIDE;
|
| + virtual bool SetState(State new_state) OVERRIDE;
|
| + virtual bool SetStartPts(base::TimeDelta time) OVERRIDE;
|
| + virtual FrameStatus PushFrame(
|
| + const scoped_refptr<DecryptContext>& decrypt_context,
|
| + const scoped_refptr<DecoderBufferBase>& buffer,
|
| + const FrameStatusCB& completion_cb) OVERRIDE;
|
| + virtual base::TimeDelta GetRenderingTime() const OVERRIDE;
|
| + virtual base::TimeDelta GetRenderingDelay() const OVERRIDE;
|
| + virtual void SetVideoClient(const VideoClient& client) OVERRIDE;
|
| + virtual bool SetConfig(const ::media::VideoDecoderConfig& config) OVERRIDE;
|
| + virtual bool GetStatistics(Statistics* stats) const OVERRIDE;
|
| +
|
| + private:
|
| + scoped_ptr<MediaComponentDeviceFake> fake_pipeline_;
|
| +
|
| + ::media::VideoDecoderConfig config_;
|
| +
|
| + DISALLOW_COPY_AND_ASSIGN(VideoPipelineDeviceFake);
|
| +};
|
| +
|
| +VideoPipelineDeviceFake::VideoPipelineDeviceFake(
|
| + MediaClockDeviceFake* media_clock_device)
|
| + : fake_pipeline_(new MediaComponentDeviceFake(media_clock_device)) {
|
| + DetachFromThread();
|
| +}
|
| +
|
| +VideoPipelineDeviceFake::~VideoPipelineDeviceFake() {
|
| +}
|
| +
|
| +void VideoPipelineDeviceFake::SetClient(const Client& client) {
|
| + fake_pipeline_->SetClient(client);
|
| +}
|
| +
|
| +MediaComponentDevice::State VideoPipelineDeviceFake::GetState() const {
|
| + return fake_pipeline_->GetState();
|
| +}
|
| +
|
| +bool VideoPipelineDeviceFake::SetState(State new_state) {
|
| + bool success = fake_pipeline_->SetState(new_state);
|
| + if (!success)
|
| + return false;
|
| +
|
| + if (new_state == kStateIdle) {
|
| + DCHECK(config_.IsValidConfig());
|
| + }
|
| + if (new_state == kStateUninitialized) {
|
| + config_ = ::media::VideoDecoderConfig();
|
| + }
|
| + return true;
|
| +}
|
| +
|
| +bool VideoPipelineDeviceFake::SetStartPts(base::TimeDelta time) {
|
| + return fake_pipeline_->SetStartPts(time);
|
| +}
|
| +
|
| +MediaComponentDevice::FrameStatus VideoPipelineDeviceFake::PushFrame(
|
| + const scoped_refptr<DecryptContext>& decrypt_context,
|
| + const scoped_refptr<DecoderBufferBase>& buffer,
|
| + const FrameStatusCB& completion_cb) {
|
| + return fake_pipeline_->PushFrame(decrypt_context, buffer, completion_cb);
|
| +}
|
| +
|
| +base::TimeDelta VideoPipelineDeviceFake::GetRenderingTime() const {
|
| + return fake_pipeline_->GetRenderingTime();
|
| +}
|
| +
|
| +base::TimeDelta VideoPipelineDeviceFake::GetRenderingDelay() const {
|
| + return fake_pipeline_->GetRenderingDelay();
|
| +}
|
| +
|
| +void VideoPipelineDeviceFake::SetVideoClient(const VideoClient& client) {
|
| +}
|
| +
|
| +bool VideoPipelineDeviceFake::SetConfig(
|
| + const ::media::VideoDecoderConfig& config) {
|
| + DCHECK(CalledOnValidThread());
|
| + if (!config.IsValidConfig())
|
| + return false;
|
| + config_ = config;
|
| + return true;
|
| +}
|
| +
|
| +bool VideoPipelineDeviceFake::GetStatistics(Statistics* stats) const {
|
| + return fake_pipeline_->GetStatistics(stats);
|
| +}
|
| +
|
| +
|
| +MediaPipelineDeviceFake::MediaPipelineDeviceFake()
|
| + : media_clock_device_(new MediaClockDeviceFake()),
|
| + audio_pipeline_device_(
|
| + new AudioPipelineDeviceFake(media_clock_device_.get())),
|
| + video_pipeline_device_(
|
| + new VideoPipelineDeviceFake(media_clock_device_.get())) {
|
| +}
|
| +
|
| +MediaPipelineDeviceFake::~MediaPipelineDeviceFake() {
|
| +}
|
| +
|
| +AudioPipelineDevice* MediaPipelineDeviceFake::GetAudioPipelineDevice() const {
|
| + return audio_pipeline_device_.get();
|
| +}
|
| +
|
| +VideoPipelineDevice* MediaPipelineDeviceFake::GetVideoPipelineDevice() const {
|
| + return video_pipeline_device_.get();
|
| +}
|
| +
|
| +MediaClockDevice* MediaPipelineDeviceFake::GetMediaClockDevice() const {
|
| + return media_clock_device_.get();
|
| +}
|
| +
|
| +} // namespace media
|
| +} // namespace chromecast
|
| +
|
|
|