| /* |
| * 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 }; |
| static const 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 */ |