From b9e63830c69231c53dc23a5e29f5b58a1d9d3668 Mon Sep 17 00:00:00 2001 From: Andreas Huber Date: Tue, 26 Jan 2010 16:20:10 -0800 Subject: [PATCH] Better support for HTTP streaming media content, fixes to the way HTTPDataSource streams the data, prefetcher implementation. related-to-bug: 2295438 --- include/media/stagefright/CachingDataSource.h | 2 + include/media/stagefright/DataSource.h | 8 + include/media/stagefright/HTTPDataSource.h | 6 + media/libstagefright/Android.mk | 1 + media/libstagefright/AwesomePlayer.cpp | 84 +++++- media/libstagefright/CachingDataSource.cpp | 4 + media/libstagefright/HTTPDataSource.cpp | 60 ++-- media/libstagefright/Prefetcher.cpp | 381 ++++++++++++++++++++++++++ media/libstagefright/include/AwesomePlayer.h | 18 +- media/libstagefright/include/Prefetcher.h | 63 +++++ 10 files changed, 590 insertions(+), 37 deletions(-) create mode 100644 media/libstagefright/Prefetcher.cpp create mode 100644 media/libstagefright/include/Prefetcher.h diff --git a/include/media/stagefright/CachingDataSource.h b/include/media/stagefright/CachingDataSource.h index b0fc4b2c04fd..30b7ad92ac80 100644 --- a/include/media/stagefright/CachingDataSource.h +++ b/include/media/stagefright/CachingDataSource.h @@ -33,6 +33,8 @@ public: virtual ssize_t readAt(off_t offset, void *data, size_t size); + virtual uint32_t flags(); + protected: virtual ~CachingDataSource(); diff --git a/include/media/stagefright/DataSource.h b/include/media/stagefright/DataSource.h index f88666a7432d..0c0ace023171 100644 --- a/include/media/stagefright/DataSource.h +++ b/include/media/stagefright/DataSource.h @@ -31,6 +31,10 @@ class String8; class DataSource : public RefBase { public: + enum Flags { + kWantsPrefetching = 1, + }; + static sp CreateFromURI(const char *uri); DataSource() {} @@ -45,6 +49,10 @@ public: // May return ERROR_UNSUPPORTED. virtual status_t getSize(off_t *size); + virtual uint32_t flags() { + return 0; + } + //////////////////////////////////////////////////////////////////////////// bool sniff(String8 *mimeType, float *confidence); diff --git a/include/media/stagefright/HTTPDataSource.h b/include/media/stagefright/HTTPDataSource.h index d5dc9e6c8886..3075f1c82f5b 100644 --- a/include/media/stagefright/HTTPDataSource.h +++ b/include/media/stagefright/HTTPDataSource.h @@ -33,6 +33,10 @@ public: virtual ssize_t readAt(off_t offset, void *data, size_t size); + virtual uint32_t flags() { + return kWantsPrefetching; + } + protected: virtual ~HTTPDataSource(); @@ -52,6 +56,8 @@ private: status_t mInitCheck; + ssize_t sendRangeRequest(size_t offset); + HTTPDataSource(const HTTPDataSource &); HTTPDataSource &operator=(const HTTPDataSource &); }; diff --git a/media/libstagefright/Android.mk b/media/libstagefright/Android.mk index 3813907e8710..dbb52c63c63f 100644 --- a/media/libstagefright/Android.mk +++ b/media/libstagefright/Android.mk @@ -31,6 +31,7 @@ LOCAL_SRC_FILES += \ MPEG4Extractor.cpp \ MPEG4Writer.cpp \ MediaExtractor.cpp \ + Prefetcher.cpp \ SampleIterator.cpp \ SampleTable.cpp \ ShoutcastSource.cpp \ diff --git a/media/libstagefright/AwesomePlayer.cpp b/media/libstagefright/AwesomePlayer.cpp index f6cd46ac87b5..42b9acce1cfb 100644 --- a/media/libstagefright/AwesomePlayer.cpp +++ b/media/libstagefright/AwesomePlayer.cpp @@ -19,6 +19,7 @@ #include #include "include/AwesomePlayer.h" +#include "include/Prefetcher.h" #include "include/SoftwareRenderer.h" #include @@ -118,6 +119,8 @@ AwesomePlayer::AwesomePlayer() mVideoEventPending = false; mStreamDoneEvent = new AwesomeEvent(this, 1); mStreamDoneEventPending = false; + mBufferingEvent = new AwesomeEvent(this, 2); + mBufferingEventPending = false; mQueue.start(); @@ -132,11 +135,16 @@ AwesomePlayer::~AwesomePlayer() { mClient.disconnect(); } -void AwesomePlayer::cancelPlayerEvents() { +void AwesomePlayer::cancelPlayerEvents(bool keepBufferingGoing) { mQueue.cancelEvent(mVideoEvent->eventID()); mVideoEventPending = false; mQueue.cancelEvent(mStreamDoneEvent->eventID()); mStreamDoneEventPending = false; + + if (!keepBufferingGoing) { + mQueue.cancelEvent(mBufferingEvent->eventID()); + mBufferingEventPending = false; + } } void AwesomePlayer::setListener(const wp &listener) { @@ -149,12 +157,22 @@ status_t AwesomePlayer::setDataSource(const char *uri) { reset_l(); - sp extractor = MediaExtractor::CreateFromURI(uri); + sp dataSource = DataSource::CreateFromURI(uri); + + if (dataSource == NULL) { + return UNKNOWN_ERROR; + } + + sp extractor = MediaExtractor::Create(dataSource); if (extractor == NULL) { return UNKNOWN_ERROR; } + if (dataSource->flags() & DataSource::kWantsPrefetching) { + mPrefetcher = new Prefetcher; + } + return setDataSource_l(extractor); } @@ -182,8 +200,6 @@ status_t AwesomePlayer::setDataSource( } status_t AwesomePlayer::setDataSource_l(const sp &extractor) { - reset_l(); - bool haveAudio = false; bool haveVideo = false; for (size_t i = 0; i < extractor->countTracks(); ++i) { @@ -253,6 +269,8 @@ void AwesomePlayer::reset_l() { mSeeking = false; mSeekTimeUs = 0; + + mPrefetcher.clear(); } // static @@ -278,13 +296,35 @@ void AwesomePlayer::AudioNotify(void *_me, int what) { } } -void AwesomePlayer::notifyListener_l(int msg) { +void AwesomePlayer::notifyListener_l(int msg, int ext1) { if (mListener != NULL) { sp listener = mListener.promote(); if (listener != NULL) { - listener->sendEvent(msg); + listener->sendEvent(msg, ext1); + } + } +} + +void AwesomePlayer::onBufferingUpdate() { + Mutex::Autolock autoLock(mLock); + mBufferingEventPending = false; + + if (mDurationUs >= 0) { + int64_t cachedDurationUs = mPrefetcher->getCachedDurationUs(); + int64_t positionUs = 0; + if (mVideoRenderer != NULL) { + positionUs = mVideoTimeUs; + } else if (mAudioPlayer != NULL) { + positionUs = mAudioPlayer->getMediaTimeUs(); } + + cachedDurationUs += positionUs; + + double percentage = (double)cachedDurationUs / mDurationUs; + notifyListener_l(MEDIA_BUFFERING_UPDATE, percentage * 100.0); + + postBufferingEvent_l(); } } @@ -361,6 +401,8 @@ status_t AwesomePlayer::play() { seekAudioIfNecessary_l(); } + postBufferingEvent_l(); + return OK; } @@ -414,7 +456,7 @@ status_t AwesomePlayer::pause_l() { return OK; } - cancelPlayerEvents(); + cancelPlayerEvents(true /* keepBufferingGoing */); if (mAudioPlayer != NULL) { mAudioPlayer->pause(); @@ -518,11 +560,15 @@ status_t AwesomePlayer::getVideoDimensions( return OK; } -status_t AwesomePlayer::setAudioSource(const sp &source) { +status_t AwesomePlayer::setAudioSource(sp source) { if (source == NULL) { return UNKNOWN_ERROR; } + if (mPrefetcher != NULL) { + source = mPrefetcher->addSource(source); + } + sp meta = source->getFormat(); const char *mime; @@ -549,11 +595,15 @@ status_t AwesomePlayer::setAudioSource(const sp &source) { return mAudioSource != NULL ? OK : UNKNOWN_ERROR; } -status_t AwesomePlayer::setVideoSource(const sp &source) { +status_t AwesomePlayer::setVideoSource(sp source) { if (source == NULL) { return UNKNOWN_ERROR; } + if (mPrefetcher != NULL) { + source = mPrefetcher->addSource(source); + } + mVideoSource = OMXCodec::Create( mClient.interface(), source->getFormat(), false, // createEncoder @@ -580,9 +630,13 @@ void AwesomePlayer::onEvent(int32_t code) { if (code == 1) { onStreamDone(); return; + } else if (code == 2) { + onBufferingUpdate(); + return; } Mutex::Autolock autoLock(mLock); + mVideoEventPending = false; if (mSeeking) { @@ -718,5 +772,17 @@ void AwesomePlayer::postStreamDoneEvent_l() { mQueue.postEvent(mStreamDoneEvent); } +void AwesomePlayer::postBufferingEvent_l() { + if (mPrefetcher == NULL) { + return; + } + + if (mBufferingEventPending) { + return; + } + mBufferingEventPending = true; + mQueue.postEventWithDelay(mBufferingEvent, 1000000ll); +} + } // namespace android diff --git a/media/libstagefright/CachingDataSource.cpp b/media/libstagefright/CachingDataSource.cpp index 23f48978b5e6..8d04ead950d6 100644 --- a/media/libstagefright/CachingDataSource.cpp +++ b/media/libstagefright/CachingDataSource.cpp @@ -65,6 +65,10 @@ status_t CachingDataSource::initCheck() const { return mSource->initCheck(); } +uint32_t CachingDataSource::flags() { + return mSource->flags(); +} + ssize_t CachingDataSource::readAt(off_t offset, void *data, size_t size) { Mutex::Autolock autoLock(mLock); diff --git a/media/libstagefright/HTTPDataSource.cpp b/media/libstagefright/HTTPDataSource.cpp index 7e8bbc692237..135a0448c08a 100644 --- a/media/libstagefright/HTTPDataSource.cpp +++ b/media/libstagefright/HTTPDataSource.cpp @@ -190,30 +190,12 @@ HTTPDataSource::~HTTPDataSource() { mHttp = NULL; } -ssize_t HTTPDataSource::readAt(off_t offset, void *data, size_t size) { - if (offset >= mBufferOffset - && offset < (off_t)(mBufferOffset + mBufferLength)) { - size_t num_bytes_available = mBufferLength - (offset - mBufferOffset); - - size_t copy = num_bytes_available; - if (copy > size) { - copy = size; - } - - memcpy(data, (const char *)mBuffer + (offset - mBufferOffset), copy); - - return copy; - } - - mBufferOffset = offset; - mBufferLength = 0; - +ssize_t HTTPDataSource::sendRangeRequest(size_t offset) { char host[128]; sprintf(host, "Host: %s\r\n", mHost); char range[128]; - sprintf(range, "Range: bytes=%ld-%ld\r\n\r\n", - mBufferOffset, mBufferOffset + kBufferSize - 1); + sprintf(range, "Range: bytes=%d-\r\n\r\n", offset); int http_status; @@ -251,12 +233,44 @@ ssize_t HTTPDataSource::readAt(off_t offset, void *data, size_t size) { char *end; unsigned long contentLength = strtoul(value.c_str(), &end, 10); - ssize_t num_bytes_received = mHttp->receive(mBuffer, contentLength); + return contentLength; +} - if (num_bytes_received <= 0) { - return num_bytes_received; +ssize_t HTTPDataSource::readAt(off_t offset, void *data, size_t size) { + if (offset >= mBufferOffset + && offset < (off_t)(mBufferOffset + mBufferLength)) { + size_t num_bytes_available = mBufferLength - (offset - mBufferOffset); + + size_t copy = num_bytes_available; + if (copy > size) { + copy = size; + } + + memcpy(data, (const char *)mBuffer + (offset - mBufferOffset), copy); + + return copy; } + ssize_t contentLength = 0; + if (mBufferLength <= 0 || offset != mBufferOffset + mBufferLength) { + mHttp->disconnect(); + contentLength = sendRangeRequest(offset); + + if (contentLength > kBufferSize) { + contentLength = kBufferSize; + } + } else { + contentLength = kBufferSize; + } + + mBufferOffset = offset; + + if (contentLength <= 0) { + return contentLength; + } + + ssize_t num_bytes_received = mHttp->receive(mBuffer, contentLength); + mBufferLength = (size_t)num_bytes_received; size_t copy = mBufferLength; diff --git a/media/libstagefright/Prefetcher.cpp b/media/libstagefright/Prefetcher.cpp new file mode 100644 index 000000000000..93e3fdc83832 --- /dev/null +++ b/media/libstagefright/Prefetcher.cpp @@ -0,0 +1,381 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#define LOG_TAG "Prefetcher" +//#define LOG_NDEBUG 0 +#include + +#include "include/Prefetcher.h" + +#include +#include +#include +#include +#include +#include + +namespace android { + +struct PrefetchedSource : public MediaSource { + PrefetchedSource( + const sp &prefetcher, + size_t index, + const sp &source); + + virtual status_t start(MetaData *params); + virtual status_t stop(); + + virtual status_t read( + MediaBuffer **buffer, const ReadOptions *options); + + virtual sp getFormat(); + +protected: + virtual ~PrefetchedSource(); + +private: + friend struct Prefetcher; + + Mutex mLock; + Condition mCondition; + + sp mPrefetcher; + sp mSource; + size_t mIndex; + bool mStarted; + bool mReachedEOS; + int64_t mSeekTimeUs; + int64_t mCacheDurationUs; + + List mCachedBuffers; + + // Returns true iff source is currently caching. + bool getCacheDurationUs(int64_t *durationUs); + + void updateCacheDuration_l(); + void clearCache_l(); + + void cacheMore(); + + PrefetchedSource(const PrefetchedSource &); + PrefetchedSource &operator=(const PrefetchedSource &); +}; + +Prefetcher::Prefetcher() + : mDone(false), + mThreadExited(false) { + startThread(); +} + +Prefetcher::~Prefetcher() { + stopThread(); +} + +sp Prefetcher::addSource(const sp &source) { + Mutex::Autolock autoLock(mLock); + + sp psource = + new PrefetchedSource(this, mSources.size(), source); + + mSources.add(psource); + + return psource; +} + +void Prefetcher::startThread() { + mThreadExited = false; + mDone = false; + + int res = androidCreateThreadEtc( + ThreadWrapper, this, "Prefetcher", + ANDROID_PRIORITY_DEFAULT, 0, &mThread); + + CHECK_EQ(res, 1); +} + +void Prefetcher::stopThread() { + Mutex::Autolock autoLock(mLock); + + while (!mThreadExited) { + mDone = true; + mCondition.signal(); + mCondition.wait(mLock); + } +} + +// static +int Prefetcher::ThreadWrapper(void *me) { + static_cast(me)->threadFunc(); + + return 0; +} + +// Cache about 10secs for each source. +static int64_t kMaxCacheDurationUs = 10000000ll; + +void Prefetcher::threadFunc() { + for (;;) { + Mutex::Autolock autoLock(mLock); + if (mDone) { + mThreadExited = true; + mCondition.signal(); + break; + } + mCondition.waitRelative(mLock, 10000000ll); + + int64_t minCacheDurationUs = -1; + ssize_t minIndex = -1; + for (size_t i = 0; i < mSources.size(); ++i) { + sp source = mSources[i].promote(); + + if (source == NULL) { + continue; + } + + int64_t cacheDurationUs; + if (!source->getCacheDurationUs(&cacheDurationUs)) { + continue; + } + + if (cacheDurationUs >= kMaxCacheDurationUs) { + continue; + } + + if (minIndex < 0 || cacheDurationUs < minCacheDurationUs) { + minCacheDurationUs = cacheDurationUs; + minIndex = i; + } + } + + if (minIndex < 0) { + continue; + } + + sp source = mSources[minIndex].promote(); + if (source != NULL) { + source->cacheMore(); + } + } +} + +int64_t Prefetcher::getCachedDurationUs() { + Mutex::Autolock autoLock(mLock); + + int64_t minCacheDurationUs = -1; + ssize_t minIndex = -1; + for (size_t i = 0; i < mSources.size(); ++i) { + int64_t cacheDurationUs; + sp source = mSources[i].promote(); + if (source == NULL) { + continue; + } + + if (!source->getCacheDurationUs(&cacheDurationUs)) { + continue; + } + + if (cacheDurationUs >= kMaxCacheDurationUs) { + continue; + } + + if (minIndex < 0 || cacheDurationUs < minCacheDurationUs) { + minCacheDurationUs = cacheDurationUs; + minIndex = i; + } + } + + return minCacheDurationUs < 0 ? 0 : minCacheDurationUs; +} + +//////////////////////////////////////////////////////////////////////////////// + +PrefetchedSource::PrefetchedSource( + const sp &prefetcher, + size_t index, + const sp &source) + : mPrefetcher(prefetcher), + mSource(source), + mIndex(index), + mStarted(false), + mReachedEOS(false), + mSeekTimeUs(0), + mCacheDurationUs(0) { +} + +PrefetchedSource::~PrefetchedSource() { + if (mStarted) { + stop(); + } +} + +status_t PrefetchedSource::start(MetaData *params) { + Mutex::Autolock autoLock(mLock); + + status_t err = mSource->start(params); + + if (err != OK) { + return err; + } + + mStarted = true; + + for (;;) { + // Buffer 2 secs on startup. + if (mReachedEOS || mCacheDurationUs > 2000000) { + break; + } + + mCondition.wait(mLock); + } + + return OK; +} + +status_t PrefetchedSource::stop() { + Mutex::Autolock autoLock(mLock); + + clearCache_l(); + + status_t err = mSource->stop(); + + mStarted = false; + + return err; +} + +status_t PrefetchedSource::read( + MediaBuffer **out, const ReadOptions *options) { + *out = NULL; + + Mutex::Autolock autoLock(mLock); + + CHECK(mStarted); + + int64_t seekTimeUs; + if (options && options->getSeekTo(&seekTimeUs)) { + CHECK(seekTimeUs >= 0); + + clearCache_l(); + + mReachedEOS = false; + mSeekTimeUs = seekTimeUs; + } + + while (!mReachedEOS && mCachedBuffers.empty()) { + mCondition.wait(mLock); + } + + if (mCachedBuffers.empty()) { + return ERROR_END_OF_STREAM; + } + + *out = *mCachedBuffers.begin(); + mCachedBuffers.erase(mCachedBuffers.begin()); + updateCacheDuration_l(); + + return OK; +} + +sp PrefetchedSource::getFormat() { + return mSource->getFormat(); +} + +bool PrefetchedSource::getCacheDurationUs(int64_t *durationUs) { + Mutex::Autolock autoLock(mLock); + + if (!mStarted || mReachedEOS) { + *durationUs = 0; + + return false; + } + + *durationUs = mCacheDurationUs; + + return true; +} + +void PrefetchedSource::cacheMore() { + Mutex::Autolock autoLock(mLock); + + if (!mStarted) { + return; + } + + MediaBuffer *buffer; + MediaSource::ReadOptions options; + if (mSeekTimeUs >= 0) { + options.setSeekTo(mSeekTimeUs); + mSeekTimeUs = -1; + } + + status_t err = mSource->read(&buffer, &options); + + if (err != OK) { + mReachedEOS = true; + mCondition.signal(); + + return; + } + + CHECK(buffer != NULL); + + MediaBuffer *copy = new MediaBuffer(buffer->range_length()); + memcpy(copy->data(), + (const uint8_t *)buffer->data() + buffer->range_offset(), + buffer->range_length()); + + sp from = buffer->meta_data(); + sp to = copy->meta_data(); + + int64_t timeUs; + if (from->findInt64(kKeyTime, &timeUs)) { + to->setInt64(kKeyTime, timeUs); + } + + buffer->release(); + buffer = NULL; + + mCachedBuffers.push_back(copy); + updateCacheDuration_l(); + mCondition.signal(); +} + +void PrefetchedSource::updateCacheDuration_l() { + if (mCachedBuffers.size() < 2) { + mCacheDurationUs = 0; + } else { + int64_t firstTimeUs, lastTimeUs; + CHECK((*mCachedBuffers.begin())->meta_data()->findInt64( + kKeyTime, &firstTimeUs)); + CHECK((*--mCachedBuffers.end())->meta_data()->findInt64( + kKeyTime, &lastTimeUs)); + + mCacheDurationUs = lastTimeUs - firstTimeUs; + } +} + +void PrefetchedSource::clearCache_l() { + List::iterator it = mCachedBuffers.begin(); + while (it != mCachedBuffers.end()) { + (*it)->release(); + + it = mCachedBuffers.erase(it); + } + + updateCacheDuration_l(); +} + +} // namespace android diff --git a/media/libstagefright/include/AwesomePlayer.h b/media/libstagefright/include/AwesomePlayer.h index b28a12c9ce1b..c2e46c0cb75d 100644 --- a/media/libstagefright/include/AwesomePlayer.h +++ b/media/libstagefright/include/AwesomePlayer.h @@ -26,10 +26,11 @@ namespace android { +struct AudioPlayer; struct MediaBuffer; struct MediaExtractor; struct MediaSource; -struct AudioPlayer; +struct Prefetcher; struct TimeSource; struct AwesomeRenderer : public RefBase { @@ -109,13 +110,18 @@ private: bool mVideoEventPending; sp mStreamDoneEvent; bool mStreamDoneEventPending; + sp mBufferingEvent; + bool mBufferingEventPending; void postVideoEvent_l(int64_t delayUs = -1); + void postBufferingEvent_l(); void postStreamDoneEvent_l(); MediaBuffer *mLastVideoBuffer; MediaBuffer *mVideoBuffer; + sp mPrefetcher; + status_t setDataSource_l(const sp &extractor); void reset_l(); status_t seekTo_l(int64_t timeUs); @@ -123,17 +129,19 @@ private: void initRenderer_l(); void seekAudioIfNecessary_l(); - void cancelPlayerEvents(); + void cancelPlayerEvents(bool keepBufferingGoing = false); - status_t setAudioSource(const sp &source); - status_t setVideoSource(const sp &source); + status_t setAudioSource(sp source); + status_t setVideoSource(sp source); void onEvent(int32_t code); static void AudioNotify(void *me, int what); void onStreamDone(); - void notifyListener_l(int msg); + void notifyListener_l(int msg, int ext1 = 0); + + void onBufferingUpdate(); AwesomePlayer(const AwesomePlayer &); AwesomePlayer &operator=(const AwesomePlayer &); diff --git a/media/libstagefright/include/Prefetcher.h b/media/libstagefright/include/Prefetcher.h new file mode 100644 index 000000000000..7a977855d416 --- /dev/null +++ b/media/libstagefright/include/Prefetcher.h @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef PREFETCHER_H_ + +#define PREFETCHER_H_ + +#include +#include +#include + +namespace android { + +struct MediaSource; +struct PrefetchedSource; + +struct Prefetcher : public RefBase { + Prefetcher(); + + // Given an existing MediaSource returns a new MediaSource + // that will benefit from prefetching/caching the original one. + sp addSource(const sp &source); + + int64_t getCachedDurationUs(); + +protected: + virtual ~Prefetcher(); + +private: + Mutex mLock; + Condition mCondition; + + Vector > mSources; + android_thread_id_t mThread; + bool mDone; + bool mThreadExited; + + void startThread(); + void stopThread(); + + static int ThreadWrapper(void *me); + void threadFunc(); + + Prefetcher(const Prefetcher &); + Prefetcher &operator=(const Prefetcher &); +}; + +} // namespace android + +#endif // PREFETCHER_H_ -- 2.11.0