Plumbing for OPUS encoding.
Webm, OGG container extensions to hold opus.
hooks so we can say that we're recording in opus format.
OMX-specific changes are omitted since our target is Codec2
Bug: 111850384
Test: with separate omx routines
Change-Id: Iecb8b53df3fbd8506d2e6f007602284eb2d0decc
diff --git a/media/libmedia/MediaProfiles.cpp b/media/libmedia/MediaProfiles.cpp
index e0f5a40..08c6a50 100644
--- a/media/libmedia/MediaProfiles.cpp
+++ b/media/libmedia/MediaProfiles.cpp
@@ -48,7 +48,8 @@
{"amrwb", AUDIO_ENCODER_AMR_WB},
{"aac", AUDIO_ENCODER_AAC},
{"heaac", AUDIO_ENCODER_HE_AAC},
- {"aaceld", AUDIO_ENCODER_AAC_ELD}
+ {"aaceld", AUDIO_ENCODER_AAC_ELD},
+ {"opus", AUDIO_ENCODER_OPUS}
};
const MediaProfiles::NameToTagMap MediaProfiles::sFileFormatMap[] = {
diff --git a/media/libmedia/include/media/mediarecorder.h b/media/libmedia/include/media/mediarecorder.h
index d8b0fe7..bdf1aae 100644
--- a/media/libmedia/include/media/mediarecorder.h
+++ b/media/libmedia/include/media/mediarecorder.h
@@ -67,7 +67,7 @@
OUTPUT_FORMAT_AAC_ADTS = 6,
OUTPUT_FORMAT_AUDIO_ONLY_END = 7, // Used in validating the output format. Should be the
- // at the end of the audio only output formats.
+ // at the end of the audio only output formats.
/* Stream over a socket, limited to a single stream */
OUTPUT_FORMAT_RTP_AVP = 7,
@@ -81,6 +81,9 @@
/* HEIC data in a HEIF container */
OUTPUT_FORMAT_HEIF = 10,
+ /* Opus data in a OGG container */
+ OUTPUT_FORMAT_OGG = 11,
+
OUTPUT_FORMAT_LIST_END // must be last - used to validate format type
};
@@ -92,6 +95,7 @@
AUDIO_ENCODER_HE_AAC = 4,
AUDIO_ENCODER_AAC_ELD = 5,
AUDIO_ENCODER_VORBIS = 6,
+ AUDIO_ENCODER_OPUS = 7,
AUDIO_ENCODER_LIST_END // must be the last - used to validate the audio encoder type
};
diff --git a/media/libmediaplayerservice/StagefrightRecorder.cpp b/media/libmediaplayerservice/StagefrightRecorder.cpp
index e3ae02e..eae52c2 100644
--- a/media/libmediaplayerservice/StagefrightRecorder.cpp
+++ b/media/libmediaplayerservice/StagefrightRecorder.cpp
@@ -46,6 +46,7 @@
#include <media/stagefright/MediaDefs.h>
#include <media/stagefright/MetaData.h>
#include <media/stagefright/MediaCodecSource.h>
+#include <media/stagefright/OggWriter.h>
#include <media/stagefright/PersistentSurface.h>
#include <media/MediaProfiles.h>
#include <camera/CameraParameters.h>
@@ -948,6 +949,10 @@
status = setupMPEG2TSRecording();
break;
+ case OUTPUT_FORMAT_OGG:
+ status = setupOggRecording();
+ break;
+
default:
ALOGE("Unsupported output file format: %d", mOutputFormat);
status = UNKNOWN_ERROR;
@@ -1013,6 +1018,7 @@
case OUTPUT_FORMAT_AAC_ADTS:
case OUTPUT_FORMAT_RTP_AVP:
case OUTPUT_FORMAT_MPEG2TS:
+ case OUTPUT_FORMAT_OGG:
{
sp<MetaData> meta = new MetaData;
int64_t startTimeUs = systemTime() / 1000;
@@ -1113,6 +1119,9 @@
format->setString("mime", MEDIA_MIMETYPE_AUDIO_AAC);
format->setInt32("aac-profile", OMX_AUDIO_AACObjectELD);
break;
+ case AUDIO_ENCODER_OPUS:
+ format->setString("mime", MEDIA_MIMETYPE_AUDIO_OPUS);
+ break;
default:
ALOGE("Unknown audio encoder: %d", mAudioEncoder);
@@ -1169,6 +1178,13 @@
return setupRawAudioRecording();
}
+status_t StagefrightRecorder::setupOggRecording() {
+ CHECK_EQ(mOutputFormat, OUTPUT_FORMAT_OGG);
+
+ mWriter = new OggWriter(mOutputFd);
+ return setupRawAudioRecording();
+}
+
status_t StagefrightRecorder::setupAMRRecording() {
CHECK(mOutputFormat == OUTPUT_FORMAT_AMR_NB ||
mOutputFormat == OUTPUT_FORMAT_AMR_WB);
@@ -1813,6 +1829,7 @@
case AUDIO_ENCODER_AAC:
case AUDIO_ENCODER_HE_AAC:
case AUDIO_ENCODER_AAC_ELD:
+ case AUDIO_ENCODER_OPUS:
break;
default:
@@ -1863,19 +1880,18 @@
mTotalBitRate += mVideoBitRate;
}
- if (mOutputFormat != OUTPUT_FORMAT_WEBM) {
- // Audio source is added at the end if it exists.
- // This help make sure that the "recoding" sound is suppressed for
- // camcorder applications in the recorded files.
- // TODO Audio source is currently unsupported for webm output; vorbis encoder needed.
- // disable audio for time lapse recording
- bool disableAudio = mCaptureFpsEnable && mCaptureFps < mFrameRate;
- if (!disableAudio && mAudioSource != AUDIO_SOURCE_CNT) {
- err = setupAudioEncoder(writer);
- if (err != OK) return err;
- mTotalBitRate += mAudioBitRate;
- }
+ // Audio source is added at the end if it exists.
+ // This help make sure that the "recoding" sound is suppressed for
+ // camcorder applications in the recorded files.
+ // disable audio for time lapse recording
+ const bool disableAudio = mCaptureFpsEnable && mCaptureFps < mFrameRate;
+ if (!disableAudio && mAudioSource != AUDIO_SOURCE_CNT) {
+ err = setupAudioEncoder(writer);
+ if (err != OK) return err;
+ mTotalBitRate += mAudioBitRate;
+ }
+ if (mOutputFormat != OUTPUT_FORMAT_WEBM) {
if (mCaptureFpsEnable) {
mp4writer->setCaptureRate(mCaptureFps);
}
diff --git a/media/libmediaplayerservice/StagefrightRecorder.h b/media/libmediaplayerservice/StagefrightRecorder.h
index faa2e59..2ada301 100644
--- a/media/libmediaplayerservice/StagefrightRecorder.h
+++ b/media/libmediaplayerservice/StagefrightRecorder.h
@@ -166,6 +166,7 @@
void setupMPEG4orWEBMMetaData(sp<MetaData> *meta);
status_t setupAMRRecording();
status_t setupAACRecording();
+ status_t setupOggRecording();
status_t setupRawAudioRecording();
status_t setupRTPRecording();
status_t setupMPEG2TSRecording();
diff --git a/media/libstagefright/Android.bp b/media/libstagefright/Android.bp
index 02bb4e0..9aea88a 100644
--- a/media/libstagefright/Android.bp
+++ b/media/libstagefright/Android.bp
@@ -120,6 +120,7 @@
"MediaMuxer.cpp",
"NuCachedSource2.cpp",
"NuMediaExtractor.cpp",
+ "OggWriter.cpp",
"OMXClient.cpp",
"OmxInfoBuilder.cpp",
"RemoteMediaExtractor.cpp",
@@ -159,6 +160,7 @@
"libstagefright_codecbase",
"libstagefright_foundation",
"libstagefright_omx_utils",
+ "libstagefright_opus_common",
"libstagefright_xmlparser",
"libRScpp",
"libhidlallocatorutils",
@@ -179,6 +181,7 @@
"libstagefright_webm",
"libstagefright_timedtext",
"libvpx",
+ "libogg",
"libwebm",
"libstagefright_esds",
"libstagefright_id3",
diff --git a/media/libstagefright/MediaMuxer.cpp b/media/libstagefright/MediaMuxer.cpp
index 98f59b5..9ba2add 100644
--- a/media/libstagefright/MediaMuxer.cpp
+++ b/media/libstagefright/MediaMuxer.cpp
@@ -35,6 +35,7 @@
#include <media/stagefright/MediaErrors.h>
#include <media/stagefright/MetaData.h>
#include <media/stagefright/MPEG4Writer.h>
+#include <media/stagefright/OggWriter.h>
#include <media/stagefright/Utils.h>
namespace android {
@@ -52,6 +53,8 @@
mWriter = new MPEG4Writer(fd);
} else if (format == OUTPUT_FORMAT_WEBM) {
mWriter = new WebmWriter(fd);
+ } else if (format == OUTPUT_FORMAT_OGG) {
+ mWriter = new OggWriter(fd);
}
if (mWriter != NULL) {
@@ -59,6 +62,8 @@
if (format == OUTPUT_FORMAT_HEIF) {
// Note that the key uses recorder file types.
mFileMeta->setInt32(kKeyFileType, output_format::OUTPUT_FORMAT_HEIF);
+ } else if (format == OUTPUT_FORMAT_OGG) {
+ mFileMeta->setInt32(kKeyFileType, output_format::OUTPUT_FORMAT_OGG);
}
mState = INITIALIZED;
}
diff --git a/media/libstagefright/OggWriter.cpp b/media/libstagefright/OggWriter.cpp
new file mode 100644
index 0000000..ad55c56
--- /dev/null
+++ b/media/libstagefright/OggWriter.cpp
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2018 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 "OggWriter"
+
+#include <fcntl.h>
+#include <inttypes.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <media/MediaSource.h>
+#include <media/mediarecorder.h>
+#include <media/stagefright/MediaBuffer.h>
+#include <media/stagefright/MediaDefs.h>
+#include <media/stagefright/MediaErrors.h>
+#include <media/stagefright/MetaData.h>
+#include <media/stagefright/OggWriter.h>
+#include <media/stagefright/foundation/ADebug.h>
+#include "OpusHeader.h"
+
+extern "C" {
+#include <ogg/ogg.h>
+}
+
+// store the int32 value in little-endian order.
+static inline void writeint(char *buf, int base, int32_t val) {
+ buf[base + 3] = ((val) >> 24) & 0xff;
+ buf[base + 2] = ((val) >> 16) & 0xff;
+ buf[base + 1] = ((val) >> 8) & 0xff;
+ buf[base] = (val)&0xff;
+}
+
+// linkage between our header OggStreamState and the underlying ogg_stream_state
+// so that consumers of our interface do not require the ogg headers themselves.
+struct OggStreamState : public ogg_stream_state {};
+
+namespace android {
+
+OggWriter::OggWriter(int fd)
+ : mFd(dup(fd)),
+ mInitCheck(mFd < 0 ? NO_INIT : OK) {
+ // empty
+}
+
+OggWriter::~OggWriter() {
+ if (mStarted) {
+ reset();
+ }
+
+ if (mFd != -1) {
+ close(mFd);
+ mFd = -1;
+ }
+
+ free(mOs);
+}
+
+status_t OggWriter::initCheck() const {
+ return mInitCheck;
+}
+
+status_t OggWriter::addSource(const sp<MediaSource>& source) {
+ if (mInitCheck != OK) {
+ return mInitCheck;
+ }
+
+ if (mSource != NULL) {
+ return UNKNOWN_ERROR;
+ }
+
+ // Support is limited to single track of Opus audio.
+ const char* mime;
+ source->getFormat()->findCString(kKeyMIMEType, &mime);
+ const char* opus = MEDIA_MIMETYPE_AUDIO_OPUS;
+ if (strncasecmp(mime, opus, strlen(opus))) {
+ ALOGE("Track (%s) other than %s is not supported", mime, opus);
+ return ERROR_UNSUPPORTED;
+ }
+
+ mOs = (OggStreamState*) malloc(sizeof(ogg_stream_state));
+ if (ogg_stream_init((ogg_stream_state*)mOs, rand()) == -1) {
+ ALOGE("ogg stream init failed");
+ return UNKNOWN_ERROR;
+ }
+
+ // Write Ogg headers.
+ int32_t nChannels = 0;
+ if (!source->getFormat()->findInt32(kKeyChannelCount, &nChannels)) {
+ ALOGE("Missing format keys for audio track");
+ source->getFormat()->dumpToLog();
+ return BAD_VALUE;
+ }
+ source->getFormat()->dumpToLog();
+
+ int32_t sampleRate = 0;
+ if (!source->getFormat()->findInt32(kKeySampleRate, &sampleRate)) {
+ ALOGE("Missing format key for sample rate");
+ source->getFormat()->dumpToLog();
+ return UNKNOWN_ERROR;
+ }
+
+ mSampleRate = sampleRate;
+
+ OpusHeader header;
+ header.channels = nChannels;
+ header.num_streams = nChannels;
+ header.num_coupled = 0;
+ header.channel_mapping = ((nChannels > 8) ? 255 : (nChannels > 2));
+ header.gain_db = 0;
+ header.skip_samples = 0;
+
+ // headers are 21-bytes + something driven by channel count
+ // expect numbers in the low 30's here. WriteOpusHeader() will tell us
+ // if things are bad.
+ unsigned char header_data[100];
+ ogg_packet op;
+ ogg_page og;
+
+ const int packet_size = WriteOpusHeader(header, mSampleRate, (uint8_t*)header_data,
+ sizeof(header_data));
+
+ if (packet_size < 0) {
+ ALOGE("opus header writing failed");
+ return UNKNOWN_ERROR;
+ }
+ op.packet = header_data;
+ op.bytes = packet_size;
+ op.b_o_s = 1;
+ op.e_o_s = 0;
+ op.granulepos = 0;
+ op.packetno = 0;
+ ogg_stream_packetin((ogg_stream_state*)mOs, &op);
+
+ int ret;
+ while ((ret = ogg_stream_flush((ogg_stream_state*)mOs, &og))) {
+ if (!ret) break;
+ write(mFd, og.header, og.header_len);
+ write(mFd, og.body, og.body_len);
+ }
+
+
+ const char* vendor_string = "libopus";
+ const int vendor_length = strlen(vendor_string);
+ int user_comment_list_length = 0;
+
+ const int comments_length = 8 + 4 + vendor_length + 4 + user_comment_list_length;
+ char* comments = (char*)malloc(comments_length);
+ if (comments == NULL) {
+ ALOGE("failed to allocate ogg comment buffer");
+ return UNKNOWN_ERROR;
+ }
+ memcpy(comments, "OpusTags", 8);
+ writeint(comments, 8, vendor_length);
+ memcpy(comments + 12, vendor_string, vendor_length);
+ writeint(comments, 12 + vendor_length, user_comment_list_length);
+
+ op.packet = (unsigned char*)comments;
+ op.bytes = comments_length;
+ op.b_o_s = 0;
+ op.e_o_s = 0;
+ op.granulepos = 0;
+ op.packetno = 1;
+ ogg_stream_packetin((ogg_stream_state*)mOs, &op);
+
+ while ((ret = ogg_stream_flush((ogg_stream_state*)mOs, &og))) {
+ if (!ret) break;
+ write(mFd, og.header, og.header_len);
+ write(mFd, og.body, og.body_len);
+ }
+
+ mSource = source;
+ free(comments);
+ return OK;
+}
+
+status_t OggWriter::start(MetaData* /* params */) {
+ if (mInitCheck != OK) {
+ return mInitCheck;
+ }
+
+ if (mSource == NULL) {
+ return UNKNOWN_ERROR;
+ }
+
+ if (mStarted && mPaused) {
+ mPaused = false;
+ mResumed = true;
+ return OK;
+ } else if (mStarted) {
+ // Already started, does nothing
+ return OK;
+ }
+
+ status_t err = mSource->start();
+
+ if (err != OK) {
+ return err;
+ }
+
+ pthread_attr_t attr;
+ pthread_attr_init(&attr);
+ pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
+
+ mReachedEOS = false;
+ mDone = false;
+
+ pthread_create(&mThread, &attr, ThreadWrapper, this);
+ pthread_attr_destroy(&attr);
+
+ mStarted = true;
+
+ return OK;
+}
+
+status_t OggWriter::pause() {
+ if (!mStarted) {
+ return OK;
+ }
+ mPaused = true;
+ return OK;
+}
+
+status_t OggWriter::reset() {
+ if (!mStarted) {
+ return OK;
+ }
+
+ mDone = true;
+
+ void* dummy;
+ pthread_join(mThread, &dummy);
+
+ status_t err = static_cast<status_t>(reinterpret_cast<uintptr_t>(dummy));
+ {
+ status_t status = mSource->stop();
+ if (err == OK && (status != OK && status != ERROR_END_OF_STREAM)) {
+ err = status;
+ }
+ }
+
+ mStarted = false;
+ return err;
+}
+
+bool OggWriter::exceedsFileSizeLimit() {
+ if (mMaxFileSizeLimitBytes == 0) {
+ return false;
+ }
+ return mEstimatedSizeBytes > mMaxFileSizeLimitBytes;
+}
+
+bool OggWriter::exceedsFileDurationLimit() {
+ if (mMaxFileDurationLimitUs == 0) {
+ return false;
+ }
+ return mEstimatedDurationUs > mMaxFileDurationLimitUs;
+}
+
+// static
+void* OggWriter::ThreadWrapper(void* me) {
+ return (void*)(uintptr_t) static_cast<OggWriter*>(me)->threadFunc();
+}
+
+status_t OggWriter::threadFunc() {
+ mEstimatedDurationUs = 0;
+ mEstimatedSizeBytes = 0;
+ bool stoppedPrematurely = true;
+ int64_t previousPausedDurationUs = 0;
+ int64_t maxTimestampUs = 0;
+ status_t err = OK;
+
+ prctl(PR_SET_NAME, (unsigned long)"OggWriter", 0, 0, 0);
+
+ while (!mDone) {
+ MediaBufferBase* buffer = nullptr;
+ err = mSource->read(&buffer);
+
+ if (err != OK) {
+ ALOGW("failed to read next buffer");
+ break;
+ }
+
+ if (mPaused) {
+ buffer->release();
+ buffer = nullptr;
+ continue;
+ }
+ mEstimatedSizeBytes += buffer->range_length();
+ if (exceedsFileSizeLimit()) {
+ buffer->release();
+ buffer = nullptr;
+ notify(MEDIA_RECORDER_EVENT_INFO, MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED, 0);
+ ALOGW("estimated size(%" PRId64 ") exceeds limit (%" PRId64 ")",
+ mEstimatedSizeBytes, mMaxFileSizeLimitBytes);
+ break;
+ }
+ int64_t timestampUs;
+ CHECK(buffer->meta_data().findInt64(kKeyTime, ×tampUs));
+ if (timestampUs > mEstimatedDurationUs) {
+ mEstimatedDurationUs = timestampUs;
+ }
+ if (mResumed) {
+ previousPausedDurationUs += (timestampUs - maxTimestampUs - 20000);
+ mResumed = false;
+ }
+
+ timestampUs -= previousPausedDurationUs;
+
+ ALOGV("time stamp: %" PRId64 ", previous paused duration: %" PRId64, timestampUs,
+ previousPausedDurationUs);
+ if (timestampUs > maxTimestampUs) {
+ maxTimestampUs = timestampUs;
+ }
+
+ if (exceedsFileDurationLimit()) {
+ buffer->release();
+ buffer = nullptr;
+ notify(MEDIA_RECORDER_EVENT_INFO, MEDIA_RECORDER_INFO_MAX_DURATION_REACHED, 0);
+ ALOGW("estimated duration(%" PRId64 " us) exceeds limit(%" PRId64 " us)",
+ mEstimatedDurationUs, mMaxFileDurationLimitUs);
+ break;
+ }
+
+ ogg_packet op;
+ ogg_page og;
+ op.packet = (uint8_t*)buffer->data() + buffer->range_offset();
+ op.bytes = (long)buffer->range_length();
+ op.b_o_s = 0;
+ op.e_o_s = mReachedEOS ? 1 : 0;
+ // granulepos is the total number of PCM audio samples @ 48 kHz, up to and
+ // including the current packet.
+ ogg_int64_t granulepos = (48000 * mEstimatedDurationUs) / 1000000;
+ op.granulepos = granulepos;
+
+ // Headers are at packets 0 and 1.
+ op.packetno = 2 + (ogg_int32_t)mCurrentPacketId++;
+ ogg_stream_packetin((ogg_stream_state*)mOs, &op);
+ size_t n = 0;
+
+ while (ogg_stream_flush((ogg_stream_state*)mOs, &og) > 0) {
+ write(mFd, og.header, og.header_len);
+ write(mFd, og.body, og.body_len);
+ n = n + og.header_len + og.body_len;
+ }
+
+ if (n < buffer->range_length()) {
+ buffer->release();
+ buffer = nullptr;
+ err = ERROR_IO;
+ break;
+ }
+
+ if (err != OK) {
+ break;
+ }
+
+ stoppedPrematurely = false;
+
+ buffer->release();
+ buffer = nullptr;
+ }
+
+ // end of stream is an ok thing
+ if (err == ERROR_END_OF_STREAM) {
+ err = OK;
+ }
+
+ if (err == OK && stoppedPrematurely) {
+ err = ERROR_MALFORMED;
+ }
+
+ close(mFd);
+ mFd = -1;
+ mReachedEOS = true;
+
+ return err;
+}
+
+bool OggWriter::reachedEOS() {
+ return mReachedEOS;
+}
+
+} // namespace android
diff --git a/media/libstagefright/include/media/stagefright/MediaMuxer.h b/media/libstagefright/include/media/stagefright/MediaMuxer.h
index 66f4d72..69d6cde 100644
--- a/media/libstagefright/include/media/stagefright/MediaMuxer.h
+++ b/media/libstagefright/include/media/stagefright/MediaMuxer.h
@@ -49,6 +49,7 @@
OUTPUT_FORMAT_WEBM = 1,
OUTPUT_FORMAT_THREE_GPP = 2,
OUTPUT_FORMAT_HEIF = 3,
+ OUTPUT_FORMAT_OGG = 4,
OUTPUT_FORMAT_LIST_END // must be last - used to validate format type
};
diff --git a/media/libstagefright/include/media/stagefright/OggWriter.h b/media/libstagefright/include/media/stagefright/OggWriter.h
new file mode 100644
index 0000000..e3837cd
--- /dev/null
+++ b/media/libstagefright/include/media/stagefright/OggWriter.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2018 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 OGG_WRITER_H_
+
+#define OGG_WRITER_H_
+
+#include <stdio.h>
+
+#include <media/stagefright/MediaWriter.h>
+#include <utils/threads.h>
+
+struct OggStreamState;
+
+namespace android {
+
+struct OggWriter : public MediaWriter {
+ OggWriter(int fd);
+
+ status_t initCheck() const;
+
+ virtual status_t addSource(const sp<MediaSource>& source);
+ virtual bool reachedEOS();
+ virtual status_t start(MetaData* params = NULL);
+ virtual status_t stop() { return reset(); }
+ virtual status_t pause();
+
+protected:
+ ~OggWriter();
+
+private:
+ int mFd;
+ status_t mInitCheck;
+ sp<MediaSource> mSource;
+ bool mStarted = false;
+ volatile bool mPaused = false;
+ volatile bool mResumed = false;
+ volatile bool mDone;
+ volatile bool mReachedEOS;
+ pthread_t mThread;
+ int64_t mSampleRate;
+ int64_t mEstimatedSizeBytes;
+ int64_t mEstimatedDurationUs;
+
+ static void* ThreadWrapper(void*);
+ status_t threadFunc();
+ bool exceedsFileSizeLimit();
+ bool exceedsFileDurationLimit();
+ status_t reset();
+
+ int32_t mCurrentPacketId;
+ OggStreamState* mOs = nullptr;
+
+ OggWriter(const OggWriter&);
+ OggWriter& operator=(const OggWriter&);
+};
+
+} // namespace android
+
+#endif // OGG_WRITER_H_
diff --git a/media/libstagefright/opus/Android.bp b/media/libstagefright/opus/Android.bp
new file mode 100644
index 0000000..c5086ec
--- /dev/null
+++ b/media/libstagefright/opus/Android.bp
@@ -0,0 +1,21 @@
+cc_library_shared {
+ name: "libstagefright_opus_common",
+ vendor_available: true,
+
+ export_include_dirs: ["include"],
+
+ srcs: ["OpusHeader.cpp"],
+
+ shared_libs: ["liblog"],
+
+ cflags: ["-Werror"],
+
+ sanitize: {
+ integer_overflow: true,
+ cfi: true,
+ diag: {
+ integer_overflow: true,
+ cfi: true,
+ },
+ },
+}
\ No newline at end of file
diff --git a/media/libstagefright/opus/OpusHeader.cpp b/media/libstagefright/opus/OpusHeader.cpp
new file mode 100644
index 0000000..e4a460c
--- /dev/null
+++ b/media/libstagefright/opus/OpusHeader.cpp
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2018 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_NDEBUG 0
+#define LOG_TAG "SoftOpus"
+
+#include <cstring>
+#include <stdint.h>
+
+#include <log/log.h>
+
+#include "OpusHeader.h"
+
+namespace android {
+
+// Opus uses Vorbis channel mapping, and Vorbis channel mapping specifies
+// mappings for up to 8 channels. This information is part of the Vorbis I
+// Specification:
+// http://www.xiph.org/vorbis/doc/Vorbis_I_spec.html
+constexpr int kMaxChannels = 8;
+
+constexpr uint8_t kOpusChannelMap[kMaxChannels][kMaxChannels] = {
+ {0},
+ {0, 1},
+ {0, 2, 1},
+ {0, 1, 2, 3},
+ {0, 4, 1, 2, 3},
+ {0, 4, 1, 2, 3, 5},
+ {0, 4, 1, 2, 3, 5, 6},
+ {0, 6, 1, 2, 3, 4, 5, 7},
+};
+
+// Opus always has a 48kHz output rate. This is true for all Opus, not just this
+// implementation.
+constexpr int kRate = 48000;
+// Size of the Opus header excluding optional mapping information.
+constexpr size_t kOpusHeaderSize = 19;
+// Offset to magic string that starts Opus header.
+constexpr size_t kOpusHeaderLabelOffset = 0;
+// Offset to Opus version in the Opus header.
+constexpr size_t kOpusHeaderVersionOffset = 8;
+// Offset to the channel count byte in the Opus header.
+constexpr size_t kOpusHeaderChannelsOffset = 9;
+// Offset to the pre-skip value in the Opus header.
+constexpr size_t kOpusHeaderSkipSamplesOffset = 10;
+// Offset to sample rate in the Opus header.
+constexpr size_t kOpusHeaderSampleRateOffset = 12;
+// Offset to the gain value in the Opus header.
+constexpr size_t kOpusHeaderGainOffset = 16;
+// Offset to the channel mapping byte in the Opus header.
+constexpr size_t kOpusHeaderChannelMappingOffset = 18;
+// Opus Header contains a stream map. The mapping values are in the header
+// beyond the always present |kOpusHeaderSize| bytes of data. The mapping
+// data contains stream count, coupling information, and per channel mapping
+// values:
+// - Byte 0: Number of streams.
+// - Byte 1: Number coupled.
+// - Byte 2: Starting at byte 2 are |header->channels| uint8 mapping
+// values.
+// Offset to the number of streams in the Opus header.
+constexpr size_t kOpusHeaderNumStreamsOffset = 19;
+// Offset to the number of streams that are coupled in the Opus header.
+constexpr size_t kOpusHeaderNumCoupledStreamsOffset = 20;
+// Offset to the stream to channel mapping in the Opus header.
+constexpr size_t kOpusHeaderStreamMapOffset = 21;
+// Maximum packet size used in Xiph's opusdec.
+constexpr int kMaxOpusOutputPacketSizeSamples = 960 * 6;
+
+// Default audio output channel layout. Used to initialize |stream_map| in
+// OpusHeader, and passed to opus_multistream_decoder_create() when the header
+// does not contain mapping information. The values are valid only for mono and
+// stereo output: Opus streams with more than 2 channels require a stream map.
+constexpr int kMaxChannelsWithDefaultLayout = 2;
+constexpr uint8_t kDefaultOpusChannelLayout[kMaxChannelsWithDefaultLayout] = {0, 1};
+
+static uint16_t ReadLE16(const uint8_t* data, size_t data_size, uint32_t read_offset) {
+ // check whether the 2nd byte is within the buffer
+ if (read_offset + 1 >= data_size) return 0;
+ uint16_t val;
+ val = data[read_offset];
+ val |= data[read_offset + 1] << 8;
+ return val;
+}
+
+// Parses Opus Header. Header spec: http://wiki.xiph.org/OggOpus#ID_Header
+bool ParseOpusHeader(const uint8_t* data, size_t data_size, OpusHeader* header) {
+ if (data_size < kOpusHeaderSize) {
+ ALOGV("Header size is too small.");
+ return false;
+ }
+ header->channels = data[kOpusHeaderChannelsOffset];
+
+ if (header->channels < 1 || header->channels > kMaxChannels) {
+ ALOGV("Invalid Header, bad channel count: %d", header->channels);
+ return false;
+ }
+ header->skip_samples = ReadLE16(data, data_size, kOpusHeaderSkipSamplesOffset);
+ header->gain_db = static_cast<int16_t>(ReadLE16(data, data_size, kOpusHeaderGainOffset));
+ header->channel_mapping = data[kOpusHeaderChannelMappingOffset];
+ if (!header->channel_mapping) {
+ if (header->channels > kMaxChannelsWithDefaultLayout) {
+ ALOGV("Invalid Header, missing stream map.");
+ return false;
+ }
+ header->num_streams = 1;
+ header->num_coupled = header->channels > 1;
+ header->stream_map[0] = 0;
+ header->stream_map[1] = 1;
+ return true;
+ }
+ if (data_size < kOpusHeaderStreamMapOffset + header->channels) {
+ ALOGV("Invalid stream map; insufficient data for current channel "
+ "count: %d",
+ header->channels);
+ return false;
+ }
+ header->num_streams = data[kOpusHeaderNumStreamsOffset];
+ header->num_coupled = data[kOpusHeaderNumCoupledStreamsOffset];
+ if (header->num_streams + header->num_coupled != header->channels) {
+ ALOGV("Inconsistent channel mapping.");
+ return false;
+ }
+ for (int i = 0; i < header->channels; ++i)
+ header->stream_map[i] = data[kOpusHeaderStreamMapOffset + i];
+ return true;
+}
+
+int WriteOpusHeader(const OpusHeader &header, int input_sample_rate,
+ uint8_t* output, size_t output_size) {
+ // See https://wiki.xiph.org/OggOpus#ID_Header.
+ const size_t total_size = kOpusHeaderStreamMapOffset + header.channels;
+ if (output_size < total_size) {
+ ALOGE("Output buffer too small for header.");
+ return -1;
+ }
+
+ // ensure entire header is cleared, even though we overwrite much of it below
+ memset(output, 0, output_size);
+
+ // Set magic signature.
+ memcpy(output + kOpusHeaderLabelOffset, "OpusHead", 8);
+ // Set Opus version.
+ output[kOpusHeaderVersionOffset] = 1;
+ // Set channel count.
+ output[kOpusHeaderChannelsOffset] = (uint8_t)header.channels;
+ // Set pre-skip
+ memcpy(output + kOpusHeaderSkipSamplesOffset, &header.skip_samples, sizeof(uint16_t));
+ // Set original input sample rate in Hz.
+ memcpy(output + kOpusHeaderSampleRateOffset, &input_sample_rate, sizeof(uint32_t));
+ // Set output gain in dB.
+ memcpy(output + kOpusHeaderGainOffset, &header.gain_db, sizeof(uint16_t));
+
+ if (header.channels > 2) {
+ // Set channel mapping
+ output[kOpusHeaderChannelMappingOffset] = 1;
+ // Assuming no coupled streams. This should actually be
+ // channels() - |coupled_streams|.
+ output[kOpusHeaderNumStreamsOffset] = header.channels;
+ output[kOpusHeaderNumCoupledStreamsOffset] = 0;
+
+ // Set the actual stream map.
+ for (int i = 0; i < header.channels; ++i) {
+ output[kOpusHeaderStreamMapOffset + i] = kOpusChannelMap[header.channels - 1][i];
+ }
+ return kOpusHeaderStreamMapOffset + header.channels + 1;
+ } else {
+ output[kOpusHeaderChannelMappingOffset] = 0;
+ return kOpusHeaderChannelMappingOffset + 1;
+ }
+}
+
+} // namespace android
diff --git a/media/libstagefright/opus/include/OpusHeader.h b/media/libstagefright/opus/include/OpusHeader.h
new file mode 100644
index 0000000..f9f79cd
--- /dev/null
+++ b/media/libstagefright/opus/include/OpusHeader.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+/*
+ * The Opus specification is part of IETF RFC 6716:
+ * http://tools.ietf.org/html/rfc6716
+ */
+
+#ifndef OPUS_HEADER_H_
+#define OPUS_HEADER_H_
+
+namespace android {
+
+struct OpusHeader {
+ int channels;
+ int channel_mapping;
+ int num_streams;
+ int num_coupled;
+ int16_t gain_db;
+ int skip_samples;
+ uint8_t stream_map[8];
+};
+
+bool ParseOpusHeader(const uint8_t* data, size_t data_size, OpusHeader* header);
+int WriteOpusHeader(const OpusHeader &header, int input_sample_rate, uint8_t* output, size_t output_size);
+} // namespace android
+
+#endif // OPUS_HEADER_H_
diff --git a/media/libstagefright/webm/Android.bp b/media/libstagefright/webm/Android.bp
index 64ecc2d..1f840b7 100644
--- a/media/libstagefright/webm/Android.bp
+++ b/media/libstagefright/webm/Android.bp
@@ -28,6 +28,7 @@
shared_libs: [
"libstagefright_foundation",
+ "libstagefright_opus_common",
"libutils",
"liblog",
],
diff --git a/media/libstagefright/webm/WebmElement.cpp b/media/libstagefright/webm/WebmElement.cpp
index a5120b9..4d504e0 100644
--- a/media/libstagefright/webm/WebmElement.cpp
+++ b/media/libstagefright/webm/WebmElement.cpp
@@ -305,6 +305,7 @@
}
sp<WebmElement> WebmElement::AudioTrackEntry(
+ const char *codec,
int chans,
double rate,
const sp<ABuffer> &buf,
@@ -322,7 +323,7 @@
uid,
lacing,
lang,
- "A_VORBIS",
+ codec,
kAudioType,
trackEntryFields);
diff --git a/media/libstagefright/webm/WebmElement.h b/media/libstagefright/webm/WebmElement.h
index ffbba1b..a94c23f 100644
--- a/media/libstagefright/webm/WebmElement.h
+++ b/media/libstagefright/webm/WebmElement.h
@@ -50,6 +50,7 @@
static sp<WebmElement> SegmentInfo(uint64_t scale = 1000000, double dur = 0);
static sp<WebmElement> AudioTrackEntry(
+ const char *codec,
int chans,
double rate,
const sp<ABuffer> &buf,
diff --git a/media/libstagefright/webm/WebmWriter.cpp b/media/libstagefright/webm/WebmWriter.cpp
index 4d73eb8..7b4b23a 100644
--- a/media/libstagefright/webm/WebmWriter.cpp
+++ b/media/libstagefright/webm/WebmWriter.cpp
@@ -23,6 +23,8 @@
#include <media/stagefright/MetaData.h>
#include <media/stagefright/MediaDefs.h>
#include <media/stagefright/foundation/ADebug.h>
+#include <media/stagefright/foundation/hexdump.h>
+#include <OpusHeader.h>
#include <utils/Errors.h>
@@ -112,46 +114,102 @@
// static
sp<WebmElement> WebmWriter::audioTrack(const sp<MetaData>& md) {
int32_t nChannels, samplerate;
- uint32_t type;
- const void *headerData1;
- const char headerData2[] = { 3, 'v', 'o', 'r', 'b', 'i', 's', 7, 0, 0, 0,
- 'a', 'n', 'd', 'r', 'o', 'i', 'd', 0, 0, 0, 0, 1 };
- const void *headerData3;
- size_t headerSize1, headerSize2 = sizeof(headerData2), headerSize3;
+ const char* mimeType;
if (!md->findInt32(kKeyChannelCount, &nChannels)
- || !md->findInt32(kKeySampleRate, &samplerate)
- || !md->findData(kKeyVorbisInfo, &type, &headerData1, &headerSize1)
- || !md->findData(kKeyVorbisBooks, &type, &headerData3, &headerSize3)) {
+ || !md->findInt32(kKeySampleRate, &samplerate)
+ || !md->findCString(kKeyMIMEType, &mimeType)) {
ALOGE("Missing format keys for audio track");
md->dumpToLog();
return NULL;
}
- size_t codecPrivateSize = 1;
- codecPrivateSize += XiphLaceCodeLen(headerSize1);
- codecPrivateSize += XiphLaceCodeLen(headerSize2);
- codecPrivateSize += headerSize1 + headerSize2 + headerSize3;
+ if (!strncasecmp(mimeType, MEDIA_MIMETYPE_AUDIO_OPUS, strlen(MEDIA_MIMETYPE_AUDIO_OPUS))) {
+ // Opus in WebM is a well-known, yet under-documented, format. The codec private data
+ // of the track is an Opus Ogg header (https://tools.ietf.org/html/rfc7845#section-5.1)
+ // The name of the track isn't standardized, its value should be "A_OPUS".
+ OpusHeader header;
+ header.channels = nChannels;
+ header.num_streams = nChannels;
+ header.num_coupled = 0;
+ // - Channel mapping family (8 bits unsigned)
+ // -- 0 = one stream: mono or L,R stereo
+ // -- 1 = channels in vorbis spec order: mono or L,R stereo or ... or FL,C,FR,RL,RR,LFE, ...
+ // -- 2..254 = reserved (treat as 255)
+ // -- 255 = no defined channel meaning
+ //
+ // our implementation encodes: 0, 1, or 255
+ header.channel_mapping = ((nChannels > 8) ? 255 : (nChannels > 2));
+ header.gain_db = 0;
+ header.skip_samples = 0;
- off_t off = 0;
- sp<ABuffer> codecPrivateBuf = new ABuffer(codecPrivateSize);
- uint8_t *codecPrivateData = codecPrivateBuf->data();
- codecPrivateData[off++] = 2;
+ // headers are 21-bytes + something driven by channel count
+ // expect numbers in the low 30's here. WriteOpusHeader() will tell us
+ // if things are bad.
+ unsigned char header_data[100];
+ int headerSize = WriteOpusHeader(header, samplerate, (uint8_t*)header_data,
+ sizeof(header_data));
- off += XiphLaceEnc(codecPrivateData + off, headerSize1);
- off += XiphLaceEnc(codecPrivateData + off, headerSize2);
+ if (headerSize < 0) {
+ // didn't fill out that header for some reason
+ ALOGE("failed to generate OPUS header");
+ return NULL;
+ }
- memcpy(codecPrivateData + off, headerData1, headerSize1);
- off += headerSize1;
- memcpy(codecPrivateData + off, headerData2, headerSize2);
- off += headerSize2;
- memcpy(codecPrivateData + off, headerData3, headerSize3);
+ size_t codecPrivateSize = 0;
+ codecPrivateSize += headerSize;
- sp<WebmElement> entry = WebmElement::AudioTrackEntry(
- nChannels,
- samplerate,
- codecPrivateBuf);
- return entry;
+ off_t off = 0;
+ sp<ABuffer> codecPrivateBuf = new ABuffer(codecPrivateSize);
+ uint8_t* codecPrivateData = codecPrivateBuf->data();
+
+ memcpy(codecPrivateData + off, (uint8_t*)header_data, headerSize);
+ sp<WebmElement> entry =
+ WebmElement::AudioTrackEntry("A_OPUS", nChannels, samplerate, codecPrivateBuf);
+ return entry;
+ } else if (!strncasecmp(mimeType,
+ MEDIA_MIMETYPE_AUDIO_VORBIS,
+ strlen(MEDIA_MIMETYPE_AUDIO_VORBIS))) {
+ uint32_t type;
+ const void *headerData1;
+ const char headerData2[] = { 3, 'v', 'o', 'r', 'b', 'i', 's', 7, 0, 0, 0,
+ 'a', 'n', 'd', 'r', 'o', 'i', 'd', 0, 0, 0, 0, 1 };
+ const void *headerData3;
+ size_t headerSize1, headerSize2 = sizeof(headerData2), headerSize3;
+
+ if (!md->findData(kKeyVorbisInfo, &type, &headerData1, &headerSize1)
+ || !md->findData(kKeyVorbisBooks, &type, &headerData3, &headerSize3)) {
+ ALOGE("Missing header format keys for vorbis track");
+ md->dumpToLog();
+ return NULL;
+ }
+
+ size_t codecPrivateSize = 1;
+ codecPrivateSize += XiphLaceCodeLen(headerSize1);
+ codecPrivateSize += XiphLaceCodeLen(headerSize2);
+ codecPrivateSize += headerSize1 + headerSize2 + headerSize3;
+
+ off_t off = 0;
+ sp<ABuffer> codecPrivateBuf = new ABuffer(codecPrivateSize);
+ uint8_t *codecPrivateData = codecPrivateBuf->data();
+ codecPrivateData[off++] = 2;
+
+ off += XiphLaceEnc(codecPrivateData + off, headerSize1);
+ off += XiphLaceEnc(codecPrivateData + off, headerSize2);
+
+ memcpy(codecPrivateData + off, headerData1, headerSize1);
+ off += headerSize1;
+ memcpy(codecPrivateData + off, headerData2, headerSize2);
+ off += headerSize2;
+ memcpy(codecPrivateData + off, headerData3, headerSize3);
+
+ sp<WebmElement> entry =
+ WebmElement::AudioTrackEntry("A_VORBIS", nChannels, samplerate, codecPrivateBuf);
+ return entry;
+ } else {
+ ALOGE("Track (%s) is not a supported audio format", mimeType);
+ return NULL;
+ }
}
size_t WebmWriter::numTracks() {
@@ -382,16 +440,18 @@
const char *vp8 = MEDIA_MIMETYPE_VIDEO_VP8;
const char *vp9 = MEDIA_MIMETYPE_VIDEO_VP9;
const char *vorbis = MEDIA_MIMETYPE_AUDIO_VORBIS;
+ const char* opus = MEDIA_MIMETYPE_AUDIO_OPUS;
size_t streamIndex;
if (!strncasecmp(mime, vp8, strlen(vp8)) ||
!strncasecmp(mime, vp9, strlen(vp9))) {
streamIndex = kVideoIndex;
- } else if (!strncasecmp(mime, vorbis, strlen(vorbis))) {
+ } else if (!strncasecmp(mime, vorbis, strlen(vorbis)) ||
+ !strncasecmp(mime, opus, strlen(opus))) {
streamIndex = kAudioIndex;
} else {
- ALOGE("Track (%s) other than %s, %s or %s is not supported",
- mime, vp8, vp9, vorbis);
+ ALOGE("Track (%s) other than %s, %s, %s, or %s is not supported",
+ mime, vp8, vp9, vorbis, opus);
return ERROR_UNSUPPORTED;
}