blob: e58964da9ac469932142ab53fe9e977401ae41ea [file] [log] [blame]
/*
* Copyright (C) 2014 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 "WebmWriter"
#include "EbmlUtil.h"
#include "WebmWriter.h"
#include <media/stagefright/MetaData.h>
#include <media/stagefright/MediaDefs.h>
#include <media/stagefright/foundation/ADebug.h>
#include <utils/Errors.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <inttypes.h>
using namespace webm;
namespace {
size_t XiphLaceCodeLen(size_t size) {
return size / 0xff + 1;
}
size_t XiphLaceEnc(uint8_t *buf, size_t size) {
size_t i;
for (i = 0; size >= 0xff; ++i, size -= 0xff) {
buf[i] = 0xff;
}
buf[i++] = size;
return i;
}
}
namespace android {
static const int64_t kMinStreamableFileSizeInBytes = 5 * 1024 * 1024;
WebmWriter::WebmWriter(int fd)
: mFd(dup(fd)),
mInitCheck(mFd < 0 ? NO_INIT : OK),
mTimeCodeScale(1000000),
mStartTimestampUs(0),
mStartTimeOffsetMs(0),
mSegmentOffset(0),
mSegmentDataStart(0),
mInfoOffset(0),
mInfoSize(0),
mTracksOffset(0),
mCuesOffset(0),
mPaused(false),
mStarted(false),
mIsFileSizeLimitExplicitlyRequested(false),
mIsRealTimeRecording(false),
mStreamableFile(true),
mEstimatedCuesSize(0) {
mStreams[kAudioIndex] = WebmStream(kAudioType, "Audio", &WebmWriter::audioTrack);
mStreams[kVideoIndex] = WebmStream(kVideoType, "Video", &WebmWriter::videoTrack);
mSinkThread = new WebmFrameSinkThread(
mFd,
mSegmentDataStart,
mStreams[kVideoIndex].mSink,
mStreams[kAudioIndex].mSink,
mCuePoints);
}
// static
sp<WebmElement> WebmWriter::videoTrack(const sp<MetaData>& md) {
int32_t width, height;
const char *mimeType;
if (!md->findInt32(kKeyWidth, &width)
|| !md->findInt32(kKeyHeight, &height)
|| !md->findCString(kKeyMIMEType, &mimeType)) {
ALOGE("Missing format keys for video track");
md->dumpToLog();
return NULL;
}
const char *codec;
if (!strncasecmp(
mimeType,
MEDIA_MIMETYPE_VIDEO_VP8,
strlen(MEDIA_MIMETYPE_VIDEO_VP8))) {
codec = "V_VP8";
} else if (!strncasecmp(
mimeType,
MEDIA_MIMETYPE_VIDEO_VP9,
strlen(MEDIA_MIMETYPE_VIDEO_VP9))) {
codec = "V_VP9";
} else {
ALOGE("Unsupported codec: %s", mimeType);
return NULL;
}
return WebmElement::VideoTrackEntry(codec, width, height, md);
}
// 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;
if (!md->findInt32(kKeyChannelCount, &nChannels)
|| !md->findInt32(kKeySampleRate, &samplerate)
|| !md->findData(kKeyVorbisInfo, &type, &headerData1, &headerSize1)
|| !md->findData(kKeyVorbisBooks, &type, &headerData3, &headerSize3)) {
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;
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(
nChannels,
samplerate,
codecPrivateBuf);
return entry;
}
size_t WebmWriter::numTracks() {
Mutex::Autolock autolock(mLock);
size_t numTracks = 0;
for (size_t i = 0; i < kMaxStreams; ++i) {
if (mStreams[i].mTrackEntry != NULL) {
numTracks++;
}
}
return numTracks;
}
uint64_t WebmWriter::estimateCuesSize(int32_t bitRate) {
// This implementation is based on estimateMoovBoxSize in MPEG4Writer.
//
// Statistical analysis shows that metadata usually accounts
// for a small portion of the total file size, usually < 0.6%.
// The default MIN_MOOV_BOX_SIZE is set to 0.6% x 1MB / 2,
// where 1MB is the common file size limit for MMS application.
// The default MAX _MOOV_BOX_SIZE value is based on about 3
// minute video recording with a bit rate about 3 Mbps, because
// statistics also show that most of the video captured are going
// to be less than 3 minutes.
// If the estimation is wrong, we will pay the price of wasting
// some reserved space. This should not happen so often statistically.
static const int32_t factor = 2;
static const int64_t MIN_CUES_SIZE = 3 * 1024; // 3 KB
static const int64_t MAX_CUES_SIZE = (180 * 3000000 * 6LL / 8000);
int64_t size = MIN_CUES_SIZE;
// Max file size limit is set
if (mMaxFileSizeLimitBytes != 0 && mIsFileSizeLimitExplicitlyRequested) {
size = mMaxFileSizeLimitBytes * 6 / 1000;
}
// Max file duration limit is set
if (mMaxFileDurationLimitUs != 0) {
if (bitRate > 0) {
int64_t size2 = ((mMaxFileDurationLimitUs * bitRate * 6) / 1000 / 8000000);
if (mMaxFileSizeLimitBytes != 0 && mIsFileSizeLimitExplicitlyRequested) {
// When both file size and duration limits are set,
// we use the smaller limit of the two.
if (size > size2) {
size = size2;
}
} else {
// Only max file duration limit is set
size = size2;
}
}
}
if (size < MIN_CUES_SIZE) {
size = MIN_CUES_SIZE;
}
// Any long duration recording will be probably end up with
// non-streamable webm file.
if (size > MAX_CUES_SIZE) {
size = MAX_CUES_SIZE;
}
ALOGV("limits: %" PRId64 "/%" PRId64 " bytes/us,"
" bit rate: %d bps and the estimated cues size %" PRId64 " bytes",
mMaxFileSizeLimitBytes, mMaxFileDurationLimitUs, bitRate, size);
return factor * size;
}
void WebmWriter::initStream(size_t idx) {
if (mStreams[idx].mThread != NULL) {
return;
}
if (mStreams[idx].mSource == NULL) {
ALOGV("adding dummy source ... ");
mStreams[idx].mThread = new WebmFrameEmptySourceThread(
mStreams[idx].mType, mStreams[idx].mSink);
} else {
ALOGV("adding source %p", mStreams[idx].mSource.get());
mStreams[idx].mThread = new WebmFrameMediaSourceThread(
mStreams[idx].mSource,
mStreams[idx].mType,
mStreams[idx].mSink,
mTimeCodeScale,
mStartTimestampUs,
mStartTimeOffsetMs,
numTracks(),
mIsRealTimeRecording);
}
}
void WebmWriter::release() {
close(mFd);
mFd = -1;
mInitCheck = NO_INIT;
mStarted = false;
for (size_t ix = 0; ix < kMaxStreams; ++ix) {
mStreams[ix].mTrackEntry.clear();
mStreams[ix].mSource.clear();
}
mStreamsInOrder.clear();
}
status_t WebmWriter::reset() {
if (mInitCheck != OK) {
return OK;
} else {
if (!mStarted) {
release();
return OK;
}
}
status_t err = OK;
int64_t maxDurationUs = 0;
int64_t minDurationUs = 0x7fffffffffffffffLL;
for (int i = 0; i < kMaxStreams; ++i) {
if (mStreams[i].mThread == NULL) {
continue;
}
status_t status = mStreams[i].mThread->stop();
if (err == OK && status != OK) {
err = status;
}
int64_t durationUs = mStreams[i].mThread->getDurationUs();
if (durationUs > maxDurationUs) {
maxDurationUs = durationUs;
}
if (durationUs < minDurationUs) {
minDurationUs = durationUs;
}
mStreams[i].mThread.clear();
}
if (numTracks() > 1) {
ALOGD("Duration from tracks range is [%" PRId64 ", %" PRId64 "] us", minDurationUs, maxDurationUs);
}
mSinkThread->stop();
// Do not write out movie header on error.
if (err != OK) {
release();
return err;
}
sp<WebmElement> cues = new WebmMaster(kMkvCues, mCuePoints);
uint64_t cuesSize = cues->totalSize();
// TRICKY Even when the cues do fit in the space we reserved, if they do not fit
// perfectly, we still need to check if there is enough "extra space" to write an
// EBML void element.
if (cuesSize != mEstimatedCuesSize && cuesSize > mEstimatedCuesSize - kMinEbmlVoidSize) {
mCuesOffset = ::lseek(mFd, 0, SEEK_CUR);
cues->write(mFd, cuesSize);
} else {
uint64_t spaceSize;
::lseek(mFd, mCuesOffset, SEEK_SET);
cues->write(mFd, cuesSize);
sp<WebmElement> space = new EbmlVoid(mEstimatedCuesSize - cuesSize);
space->write(mFd, spaceSize);
}
mCuePoints.clear();
mStreams[kVideoIndex].mSink.clear();
mStreams[kAudioIndex].mSink.clear();
uint8_t bary[sizeof(uint64_t)];
uint64_t totalSize = ::lseek(mFd, 0, SEEK_END);
uint64_t segmentSize = totalSize - mSegmentDataStart;
::lseek(mFd, mSegmentOffset + sizeOf(kMkvSegment), SEEK_SET);
uint64_t segmentSizeCoded = encodeUnsigned(segmentSize, sizeOf(kMkvUnknownLength));
serializeCodedUnsigned(segmentSizeCoded, bary);
::write(mFd, bary, sizeOf(kMkvUnknownLength));
uint64_t durationOffset = mInfoOffset + sizeOf(kMkvInfo) + sizeOf(mInfoSize)
+ sizeOf(kMkvSegmentDuration) + sizeOf(sizeof(double));
sp<WebmElement> duration = new WebmFloat(
kMkvSegmentDuration,
(double) (maxDurationUs * 1000 / mTimeCodeScale));
duration->serializePayload(bary);
::lseek(mFd, durationOffset, SEEK_SET);
::write(mFd, bary, sizeof(double));
List<sp<WebmElement> > seekEntries;
seekEntries.push_back(WebmElement::SeekEntry(kMkvInfo, mInfoOffset - mSegmentDataStart));
seekEntries.push_back(WebmElement::SeekEntry(kMkvTracks, mTracksOffset - mSegmentDataStart));
seekEntries.push_back(WebmElement::SeekEntry(kMkvCues, mCuesOffset - mSegmentDataStart));
sp<WebmElement> seekHead = new WebmMaster(kMkvSeekHead, seekEntries);
uint64_t metaSeekSize;
::lseek(mFd, mSegmentDataStart, SEEK_SET);
seekHead->write(mFd, metaSeekSize);
uint64_t spaceSize;
sp<WebmElement> space = new EbmlVoid(kMaxMetaSeekSize - metaSeekSize);
space->write(mFd, spaceSize);
release();
return err;
}
status_t WebmWriter::addSource(const sp<IMediaSource> &source) {
Mutex::Autolock l(mLock);
if (mStarted) {
ALOGE("Attempt to add source AFTER recording is started");
return UNKNOWN_ERROR;
}
// At most 2 tracks can be supported.
if (mStreams[kVideoIndex].mTrackEntry != NULL
&& mStreams[kAudioIndex].mTrackEntry != NULL) {
ALOGE("Too many tracks (2) to add");
return ERROR_UNSUPPORTED;
}
CHECK(source != NULL);
// A track of type other than video or audio is not supported.
const char *mime;
source->getFormat()->findCString(kKeyMIMEType, &mime);
const char *vp8 = MEDIA_MIMETYPE_VIDEO_VP8;
const char *vp9 = MEDIA_MIMETYPE_VIDEO_VP9;
const char *vorbis = MEDIA_MIMETYPE_AUDIO_VORBIS;
size_t streamIndex;
if (!strncasecmp(mime, vp8, strlen(vp8)) ||
!strncasecmp(mime, vp9, strlen(vp9))) {
streamIndex = kVideoIndex;
} else if (!strncasecmp(mime, vorbis, strlen(vorbis))) {
streamIndex = kAudioIndex;
} else {
ALOGE("Track (%s) other than %s, %s or %s is not supported",
mime, vp8, vp9, vorbis);
return ERROR_UNSUPPORTED;
}
// No more than one video or one audio track is supported.
if (mStreams[streamIndex].mTrackEntry != NULL) {
ALOGE("%s track already exists", mStreams[streamIndex].mName);
return ERROR_UNSUPPORTED;
}
// This is the first track of either audio or video.
// Go ahead to add the track.
mStreams[streamIndex].mSource = source;
mStreams[streamIndex].mTrackEntry = mStreams[streamIndex].mMakeTrack(source->getFormat());
if (mStreams[streamIndex].mTrackEntry == NULL) {
mStreams[streamIndex].mSource.clear();
return BAD_VALUE;
}
mStreamsInOrder.push_back(mStreams[streamIndex].mTrackEntry);
return OK;
}
status_t WebmWriter::start(MetaData *params) {
if (mInitCheck != OK) {
return UNKNOWN_ERROR;
}
if (mStreams[kVideoIndex].mTrackEntry == NULL
&& mStreams[kAudioIndex].mTrackEntry == NULL) {
ALOGE("No source added");
return INVALID_OPERATION;
}
if (mMaxFileSizeLimitBytes != 0) {
mIsFileSizeLimitExplicitlyRequested = true;
}
if (params) {
int32_t isRealTimeRecording;
params->findInt32(kKeyRealTimeRecording, &isRealTimeRecording);
mIsRealTimeRecording = isRealTimeRecording;
}
if (mStarted) {
if (mPaused) {
mPaused = false;
mStreams[kAudioIndex].mThread->resume();
mStreams[kVideoIndex].mThread->resume();
}
return OK;
}
if (params) {
int32_t tcsl;
if (params->findInt32(kKeyTimeScale, &tcsl)) {
mTimeCodeScale = tcsl;
}
}
if (mTimeCodeScale == 0) {
ALOGE("movie time scale is 0");
return BAD_VALUE;
}
ALOGV("movie time scale: %" PRIu64, mTimeCodeScale);
/*
* When the requested file size limit is small, the priority
* is to meet the file size limit requirement, rather than
* to make the file streamable. mStreamableFile does not tell
* whether the actual recorded file is streamable or not.
*/
mStreamableFile = (!mMaxFileSizeLimitBytes)
|| (mMaxFileSizeLimitBytes >= kMinStreamableFileSizeInBytes);
/*
* Write various metadata.
*/
sp<WebmElement> ebml, segment, info, seekHead, tracks, cues;
ebml = WebmElement::EbmlHeader();
segment = new WebmMaster(kMkvSegment);
seekHead = new EbmlVoid(kMaxMetaSeekSize);
info = WebmElement::SegmentInfo(mTimeCodeScale, 0);
List<sp<WebmElement> > children;
for (size_t i = 0; i < mStreamsInOrder.size(); ++i) {
children.push_back(mStreamsInOrder[i]);
}
tracks = new WebmMaster(kMkvTracks, children);
if (!mStreamableFile) {
cues = NULL;
} else {
int32_t bitRate = -1;
if (params) {
params->findInt32(kKeyBitRate, &bitRate);
}
mEstimatedCuesSize = estimateCuesSize(bitRate);
CHECK_GE(mEstimatedCuesSize, 8);
cues = new EbmlVoid(mEstimatedCuesSize);
}
sp<WebmElement> elems[] = { ebml, segment, seekHead, info, tracks, cues };
size_t nElems = sizeof(elems) / sizeof(elems[0]);
uint64_t offsets[nElems];
uint64_t sizes[nElems];
for (uint32_t i = 0; i < nElems; i++) {
WebmElement *e = elems[i].get();
if (!e) {
continue;
}
uint64_t size;
offsets[i] = ::lseek(mFd, 0, SEEK_CUR);
sizes[i] = e->mSize;
e->write(mFd, size);
}
mSegmentOffset = offsets[1];
mSegmentDataStart = offsets[2];
mInfoOffset = offsets[3];
mInfoSize = sizes[3];
mTracksOffset = offsets[4];
mCuesOffset = offsets[5];
// start threads
if (params) {
params->findInt64(kKeyTime, &mStartTimestampUs);
}
initStream(kAudioIndex);
initStream(kVideoIndex);
mStreams[kAudioIndex].mThread->start();
mStreams[kVideoIndex].mThread->start();
mSinkThread->start();
mStarted = true;
return OK;
}
status_t WebmWriter::pause() {
if (mInitCheck != OK) {
return OK;
}
mPaused = true;
status_t err = OK;
for (int i = 0; i < kMaxStreams; ++i) {
if (mStreams[i].mThread == NULL) {
continue;
}
status_t status = mStreams[i].mThread->pause();
if (status != OK) {
err = status;
}
}
return err;
}
status_t WebmWriter::stop() {
return reset();
}
bool WebmWriter::reachedEOS() {
return !mSinkThread->running();
}
} /* namespace android */