Home | History | Annotate | Download | only in webm
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 // #define LOG_NDEBUG 0
     18 #define LOG_TAG "WebmWriter"
     19 
     20 #include "EbmlUtil.h"
     21 #include "WebmWriter.h"
     22 
     23 #include <media/stagefright/MetaData.h>
     24 #include <media/stagefright/MediaDefs.h>
     25 #include <media/stagefright/foundation/ADebug.h>
     26 
     27 #include <utils/Errors.h>
     28 
     29 #include <unistd.h>
     30 #include <fcntl.h>
     31 #include <sys/stat.h>
     32 #include <inttypes.h>
     33 
     34 using namespace webm;
     35 
     36 namespace {
     37 size_t XiphLaceCodeLen(size_t size) {
     38     return size / 0xff + 1;
     39 }
     40 
     41 size_t XiphLaceEnc(uint8_t *buf, size_t size) {
     42     size_t i;
     43     for (i = 0; size >= 0xff; ++i, size -= 0xff) {
     44         buf[i] = 0xff;
     45     }
     46     buf[i++] = size;
     47     return i;
     48 }
     49 }
     50 
     51 namespace android {
     52 
     53 static const int64_t kMinStreamableFileSizeInBytes = 5 * 1024 * 1024;
     54 
     55 WebmWriter::WebmWriter(int fd)
     56     : mFd(dup(fd)),
     57       mInitCheck(mFd < 0 ? NO_INIT : OK),
     58       mTimeCodeScale(1000000),
     59       mStartTimestampUs(0),
     60       mStartTimeOffsetMs(0),
     61       mSegmentOffset(0),
     62       mSegmentDataStart(0),
     63       mInfoOffset(0),
     64       mInfoSize(0),
     65       mTracksOffset(0),
     66       mCuesOffset(0),
     67       mPaused(false),
     68       mStarted(false),
     69       mIsFileSizeLimitExplicitlyRequested(false),
     70       mIsRealTimeRecording(false),
     71       mStreamableFile(true),
     72       mEstimatedCuesSize(0) {
     73     mStreams[kAudioIndex] = WebmStream(kAudioType, "Audio", &WebmWriter::audioTrack);
     74     mStreams[kVideoIndex] = WebmStream(kVideoType, "Video", &WebmWriter::videoTrack);
     75     mSinkThread = new WebmFrameSinkThread(
     76             mFd,
     77             mSegmentDataStart,
     78             mStreams[kVideoIndex].mSink,
     79             mStreams[kAudioIndex].mSink,
     80             mCuePoints);
     81 }
     82 
     83 // static
     84 sp<WebmElement> WebmWriter::videoTrack(const sp<MetaData>& md) {
     85     int32_t width, height;
     86     const char *mimeType;
     87     if (!md->findInt32(kKeyWidth, &width)
     88             || !md->findInt32(kKeyHeight, &height)
     89             || !md->findCString(kKeyMIMEType, &mimeType)) {
     90         ALOGE("Missing format keys for video track");
     91         md->dumpToLog();
     92         return NULL;
     93     }
     94     const char *codec;
     95     if (!strncasecmp(
     96             mimeType,
     97             MEDIA_MIMETYPE_VIDEO_VP8,
     98             strlen(MEDIA_MIMETYPE_VIDEO_VP8))) {
     99         codec = "V_VP8";
    100     } else if (!strncasecmp(
    101             mimeType,
    102             MEDIA_MIMETYPE_VIDEO_VP9,
    103             strlen(MEDIA_MIMETYPE_VIDEO_VP9))) {
    104         codec = "V_VP9";
    105     } else {
    106         ALOGE("Unsupported codec: %s", mimeType);
    107         return NULL;
    108     }
    109     return WebmElement::VideoTrackEntry(codec, width, height, md);
    110 }
    111 
    112 // static
    113 sp<WebmElement> WebmWriter::audioTrack(const sp<MetaData>& md) {
    114     int32_t nChannels, samplerate;
    115     uint32_t type;
    116     const void *headerData1;
    117     const char headerData2[] = { 3, 'v', 'o', 'r', 'b', 'i', 's', 7, 0, 0, 0,
    118             'a', 'n', 'd', 'r', 'o', 'i', 'd', 0, 0, 0, 0, 1 };
    119     const void *headerData3;
    120     size_t headerSize1, headerSize2 = sizeof(headerData2), headerSize3;
    121 
    122     if (!md->findInt32(kKeyChannelCount, &nChannels)
    123             || !md->findInt32(kKeySampleRate, &samplerate)
    124             || !md->findData(kKeyVorbisInfo, &type, &headerData1, &headerSize1)
    125             || !md->findData(kKeyVorbisBooks, &type, &headerData3, &headerSize3)) {
    126         ALOGE("Missing format keys for audio track");
    127         md->dumpToLog();
    128         return NULL;
    129     }
    130 
    131     size_t codecPrivateSize = 1;
    132     codecPrivateSize += XiphLaceCodeLen(headerSize1);
    133     codecPrivateSize += XiphLaceCodeLen(headerSize2);
    134     codecPrivateSize += headerSize1 + headerSize2 + headerSize3;
    135 
    136     off_t off = 0;
    137     sp<ABuffer> codecPrivateBuf = new ABuffer(codecPrivateSize);
    138     uint8_t *codecPrivateData = codecPrivateBuf->data();
    139     codecPrivateData[off++] = 2;
    140 
    141     off += XiphLaceEnc(codecPrivateData + off, headerSize1);
    142     off += XiphLaceEnc(codecPrivateData + off, headerSize2);
    143 
    144     memcpy(codecPrivateData + off, headerData1, headerSize1);
    145     off += headerSize1;
    146     memcpy(codecPrivateData + off, headerData2, headerSize2);
    147     off += headerSize2;
    148     memcpy(codecPrivateData + off, headerData3, headerSize3);
    149 
    150     sp<WebmElement> entry = WebmElement::AudioTrackEntry(
    151             nChannels,
    152             samplerate,
    153             codecPrivateBuf);
    154     return entry;
    155 }
    156 
    157 size_t WebmWriter::numTracks() {
    158     Mutex::Autolock autolock(mLock);
    159 
    160     size_t numTracks = 0;
    161     for (size_t i = 0; i < kMaxStreams; ++i) {
    162         if (mStreams[i].mTrackEntry != NULL) {
    163             numTracks++;
    164         }
    165     }
    166 
    167     return numTracks;
    168 }
    169 
    170 uint64_t WebmWriter::estimateCuesSize(int32_t bitRate) {
    171     // This implementation is based on estimateMoovBoxSize in MPEG4Writer.
    172     //
    173     // Statistical analysis shows that metadata usually accounts
    174     // for a small portion of the total file size, usually < 0.6%.
    175 
    176     // The default MIN_MOOV_BOX_SIZE is set to 0.6% x 1MB / 2,
    177     // where 1MB is the common file size limit for MMS application.
    178     // The default MAX _MOOV_BOX_SIZE value is based on about 3
    179     // minute video recording with a bit rate about 3 Mbps, because
    180     // statistics also show that most of the video captured are going
    181     // to be less than 3 minutes.
    182 
    183     // If the estimation is wrong, we will pay the price of wasting
    184     // some reserved space. This should not happen so often statistically.
    185     static const int32_t factor = 2;
    186     static const int64_t MIN_CUES_SIZE = 3 * 1024;  // 3 KB
    187     static const int64_t MAX_CUES_SIZE = (180 * 3000000 * 6LL / 8000);
    188     int64_t size = MIN_CUES_SIZE;
    189 
    190     // Max file size limit is set
    191     if (mMaxFileSizeLimitBytes != 0 && mIsFileSizeLimitExplicitlyRequested) {
    192         size = mMaxFileSizeLimitBytes * 6 / 1000;
    193     }
    194 
    195     // Max file duration limit is set
    196     if (mMaxFileDurationLimitUs != 0) {
    197         if (bitRate > 0) {
    198             int64_t size2 = ((mMaxFileDurationLimitUs * bitRate * 6) / 1000 / 8000000);
    199             if (mMaxFileSizeLimitBytes != 0 && mIsFileSizeLimitExplicitlyRequested) {
    200                 // When both file size and duration limits are set,
    201                 // we use the smaller limit of the two.
    202                 if (size > size2) {
    203                     size = size2;
    204                 }
    205             } else {
    206                 // Only max file duration limit is set
    207                 size = size2;
    208             }
    209         }
    210     }
    211 
    212     if (size < MIN_CUES_SIZE) {
    213         size = MIN_CUES_SIZE;
    214     }
    215 
    216     // Any long duration recording will be probably end up with
    217     // non-streamable webm file.
    218     if (size > MAX_CUES_SIZE) {
    219         size = MAX_CUES_SIZE;
    220     }
    221 
    222     ALOGV("limits: %" PRId64 "/%" PRId64 " bytes/us,"
    223             " bit rate: %d bps and the estimated cues size %" PRId64 " bytes",
    224             mMaxFileSizeLimitBytes, mMaxFileDurationLimitUs, bitRate, size);
    225     return factor * size;
    226 }
    227 
    228 void WebmWriter::initStream(size_t idx) {
    229     if (mStreams[idx].mThread != NULL) {
    230         return;
    231     }
    232     if (mStreams[idx].mSource == NULL) {
    233         ALOGV("adding dummy source ... ");
    234         mStreams[idx].mThread = new WebmFrameEmptySourceThread(
    235                 mStreams[idx].mType, mStreams[idx].mSink);
    236     } else {
    237         ALOGV("adding source %p", mStreams[idx].mSource.get());
    238         mStreams[idx].mThread = new WebmFrameMediaSourceThread(
    239                 mStreams[idx].mSource,
    240                 mStreams[idx].mType,
    241                 mStreams[idx].mSink,
    242                 mTimeCodeScale,
    243                 mStartTimestampUs,
    244                 mStartTimeOffsetMs,
    245                 numTracks(),
    246                 mIsRealTimeRecording);
    247     }
    248 }
    249 
    250 void WebmWriter::release() {
    251     close(mFd);
    252     mFd = -1;
    253     mInitCheck = NO_INIT;
    254     mStarted = false;
    255     for (size_t ix = 0; ix < kMaxStreams; ++ix) {
    256         mStreams[ix].mTrackEntry.clear();
    257         mStreams[ix].mSource.clear();
    258     }
    259     mStreamsInOrder.clear();
    260 }
    261 
    262 status_t WebmWriter::reset() {
    263     if (mInitCheck != OK) {
    264         return OK;
    265     } else {
    266         if (!mStarted) {
    267             release();
    268             return OK;
    269         }
    270     }
    271 
    272     status_t err = OK;
    273     int64_t maxDurationUs = 0;
    274     int64_t minDurationUs = 0x7fffffffffffffffLL;
    275     for (int i = 0; i < kMaxStreams; ++i) {
    276         if (mStreams[i].mThread == NULL) {
    277             continue;
    278         }
    279 
    280         status_t status = mStreams[i].mThread->stop();
    281         if (err == OK && status != OK) {
    282             err = status;
    283         }
    284 
    285         int64_t durationUs = mStreams[i].mThread->getDurationUs();
    286         if (durationUs > maxDurationUs) {
    287             maxDurationUs = durationUs;
    288         }
    289         if (durationUs < minDurationUs) {
    290             minDurationUs = durationUs;
    291         }
    292 
    293         mStreams[i].mThread.clear();
    294     }
    295 
    296     if (numTracks() > 1) {
    297         ALOGD("Duration from tracks range is [%" PRId64 ", %" PRId64 "] us", minDurationUs, maxDurationUs);
    298     }
    299 
    300     mSinkThread->stop();
    301 
    302     // Do not write out movie header on error.
    303     if (err != OK) {
    304         release();
    305         return err;
    306     }
    307 
    308     sp<WebmElement> cues = new WebmMaster(kMkvCues, mCuePoints);
    309     uint64_t cuesSize = cues->totalSize();
    310     // TRICKY Even when the cues do fit in the space we reserved, if they do not fit
    311     // perfectly, we still need to check if there is enough "extra space" to write an
    312     // EBML void element.
    313     if (cuesSize != mEstimatedCuesSize && cuesSize > mEstimatedCuesSize - kMinEbmlVoidSize) {
    314         mCuesOffset = ::lseek(mFd, 0, SEEK_CUR);
    315         cues->write(mFd, cuesSize);
    316     } else {
    317         uint64_t spaceSize;
    318         ::lseek(mFd, mCuesOffset, SEEK_SET);
    319         cues->write(mFd, cuesSize);
    320         sp<WebmElement> space = new EbmlVoid(mEstimatedCuesSize - cuesSize);
    321         space->write(mFd, spaceSize);
    322     }
    323 
    324     mCuePoints.clear();
    325     mStreams[kVideoIndex].mSink.clear();
    326     mStreams[kAudioIndex].mSink.clear();
    327 
    328     uint8_t bary[sizeof(uint64_t)];
    329     uint64_t totalSize = ::lseek(mFd, 0, SEEK_END);
    330     uint64_t segmentSize = totalSize - mSegmentDataStart;
    331     ::lseek(mFd, mSegmentOffset + sizeOf(kMkvSegment), SEEK_SET);
    332     uint64_t segmentSizeCoded = encodeUnsigned(segmentSize, sizeOf(kMkvUnknownLength));
    333     serializeCodedUnsigned(segmentSizeCoded, bary);
    334     ::write(mFd, bary, sizeOf(kMkvUnknownLength));
    335 
    336     uint64_t durationOffset = mInfoOffset + sizeOf(kMkvInfo) + sizeOf(mInfoSize)
    337         + sizeOf(kMkvSegmentDuration) + sizeOf(sizeof(double));
    338     sp<WebmElement> duration = new WebmFloat(
    339             kMkvSegmentDuration,
    340             (double) (maxDurationUs * 1000 / mTimeCodeScale));
    341     duration->serializePayload(bary);
    342     ::lseek(mFd, durationOffset, SEEK_SET);
    343     ::write(mFd, bary, sizeof(double));
    344 
    345     List<sp<WebmElement> > seekEntries;
    346     seekEntries.push_back(WebmElement::SeekEntry(kMkvInfo, mInfoOffset - mSegmentDataStart));
    347     seekEntries.push_back(WebmElement::SeekEntry(kMkvTracks, mTracksOffset - mSegmentDataStart));
    348     seekEntries.push_back(WebmElement::SeekEntry(kMkvCues, mCuesOffset - mSegmentDataStart));
    349     sp<WebmElement> seekHead = new WebmMaster(kMkvSeekHead, seekEntries);
    350 
    351     uint64_t metaSeekSize;
    352     ::lseek(mFd, mSegmentDataStart, SEEK_SET);
    353     seekHead->write(mFd, metaSeekSize);
    354 
    355     uint64_t spaceSize;
    356     sp<WebmElement> space = new EbmlVoid(kMaxMetaSeekSize - metaSeekSize);
    357     space->write(mFd, spaceSize);
    358 
    359     release();
    360     return err;
    361 }
    362 
    363 status_t WebmWriter::addSource(const sp<IMediaSource> &source) {
    364     Mutex::Autolock l(mLock);
    365     if (mStarted) {
    366         ALOGE("Attempt to add source AFTER recording is started");
    367         return UNKNOWN_ERROR;
    368     }
    369 
    370     // At most 2 tracks can be supported.
    371     if (mStreams[kVideoIndex].mTrackEntry != NULL
    372             && mStreams[kAudioIndex].mTrackEntry != NULL) {
    373         ALOGE("Too many tracks (2) to add");
    374         return ERROR_UNSUPPORTED;
    375     }
    376 
    377     CHECK(source != NULL);
    378 
    379     // A track of type other than video or audio is not supported.
    380     const char *mime;
    381     source->getFormat()->findCString(kKeyMIMEType, &mime);
    382     const char *vp8 = MEDIA_MIMETYPE_VIDEO_VP8;
    383     const char *vp9 = MEDIA_MIMETYPE_VIDEO_VP9;
    384     const char *vorbis = MEDIA_MIMETYPE_AUDIO_VORBIS;
    385 
    386     size_t streamIndex;
    387     if (!strncasecmp(mime, vp8, strlen(vp8)) ||
    388         !strncasecmp(mime, vp9, strlen(vp9))) {
    389         streamIndex = kVideoIndex;
    390     } else if (!strncasecmp(mime, vorbis, strlen(vorbis))) {
    391         streamIndex = kAudioIndex;
    392     } else {
    393         ALOGE("Track (%s) other than %s, %s or %s is not supported",
    394               mime, vp8, vp9, vorbis);
    395         return ERROR_UNSUPPORTED;
    396     }
    397 
    398     // No more than one video or one audio track is supported.
    399     if (mStreams[streamIndex].mTrackEntry != NULL) {
    400         ALOGE("%s track already exists", mStreams[streamIndex].mName);
    401         return ERROR_UNSUPPORTED;
    402     }
    403 
    404     // This is the first track of either audio or video.
    405     // Go ahead to add the track.
    406     mStreams[streamIndex].mSource = source;
    407     mStreams[streamIndex].mTrackEntry = mStreams[streamIndex].mMakeTrack(source->getFormat());
    408     if (mStreams[streamIndex].mTrackEntry == NULL) {
    409         mStreams[streamIndex].mSource.clear();
    410         return BAD_VALUE;
    411     }
    412     mStreamsInOrder.push_back(mStreams[streamIndex].mTrackEntry);
    413 
    414     return OK;
    415 }
    416 
    417 status_t WebmWriter::start(MetaData *params) {
    418     if (mInitCheck != OK) {
    419         return UNKNOWN_ERROR;
    420     }
    421 
    422     if (mStreams[kVideoIndex].mTrackEntry == NULL
    423             && mStreams[kAudioIndex].mTrackEntry == NULL) {
    424         ALOGE("No source added");
    425         return INVALID_OPERATION;
    426     }
    427 
    428     if (mMaxFileSizeLimitBytes != 0) {
    429         mIsFileSizeLimitExplicitlyRequested = true;
    430     }
    431 
    432     if (params) {
    433         int32_t isRealTimeRecording;
    434         params->findInt32(kKeyRealTimeRecording, &isRealTimeRecording);
    435         mIsRealTimeRecording = isRealTimeRecording;
    436     }
    437 
    438     if (mStarted) {
    439         if (mPaused) {
    440             mPaused = false;
    441             mStreams[kAudioIndex].mThread->resume();
    442             mStreams[kVideoIndex].mThread->resume();
    443         }
    444         return OK;
    445     }
    446 
    447     if (params) {
    448         int32_t tcsl;
    449         if (params->findInt32(kKeyTimeScale, &tcsl)) {
    450             mTimeCodeScale = tcsl;
    451         }
    452     }
    453     if (mTimeCodeScale == 0) {
    454         ALOGE("movie time scale is 0");
    455         return BAD_VALUE;
    456     }
    457     ALOGV("movie time scale: %" PRIu64, mTimeCodeScale);
    458 
    459     /*
    460      * When the requested file size limit is small, the priority
    461      * is to meet the file size limit requirement, rather than
    462      * to make the file streamable. mStreamableFile does not tell
    463      * whether the actual recorded file is streamable or not.
    464      */
    465     mStreamableFile = (!mMaxFileSizeLimitBytes)
    466         || (mMaxFileSizeLimitBytes >= kMinStreamableFileSizeInBytes);
    467 
    468     /*
    469      * Write various metadata.
    470      */
    471     sp<WebmElement> ebml, segment, info, seekHead, tracks, cues;
    472     ebml = WebmElement::EbmlHeader();
    473     segment = new WebmMaster(kMkvSegment);
    474     seekHead = new EbmlVoid(kMaxMetaSeekSize);
    475     info = WebmElement::SegmentInfo(mTimeCodeScale, 0);
    476 
    477     List<sp<WebmElement> > children;
    478     for (size_t i = 0; i < mStreamsInOrder.size(); ++i) {
    479         children.push_back(mStreamsInOrder[i]);
    480     }
    481     tracks = new WebmMaster(kMkvTracks, children);
    482 
    483     if (!mStreamableFile) {
    484         cues = NULL;
    485     } else {
    486         int32_t bitRate = -1;
    487         if (params) {
    488             params->findInt32(kKeyBitRate, &bitRate);
    489         }
    490         mEstimatedCuesSize = estimateCuesSize(bitRate);
    491         CHECK_GE(mEstimatedCuesSize, 8u);
    492         cues = new EbmlVoid(mEstimatedCuesSize);
    493     }
    494 
    495     sp<WebmElement> elems[] = { ebml, segment, seekHead, info, tracks, cues };
    496     static const size_t nElems = sizeof(elems) / sizeof(elems[0]);
    497     uint64_t offsets[nElems];
    498     uint64_t sizes[nElems];
    499     for (uint32_t i = 0; i < nElems; i++) {
    500         WebmElement *e = elems[i].get();
    501         if (!e) {
    502             continue;
    503         }
    504 
    505         uint64_t size;
    506         offsets[i] = ::lseek(mFd, 0, SEEK_CUR);
    507         sizes[i] = e->mSize;
    508         e->write(mFd, size);
    509     }
    510 
    511     mSegmentOffset = offsets[1];
    512     mSegmentDataStart = offsets[2];
    513     mInfoOffset = offsets[3];
    514     mInfoSize = sizes[3];
    515     mTracksOffset = offsets[4];
    516     mCuesOffset = offsets[5];
    517 
    518     // start threads
    519     if (params) {
    520         params->findInt64(kKeyTime, &mStartTimestampUs);
    521     }
    522 
    523     initStream(kAudioIndex);
    524     initStream(kVideoIndex);
    525 
    526     mStreams[kAudioIndex].mThread->start();
    527     mStreams[kVideoIndex].mThread->start();
    528     mSinkThread->start();
    529 
    530     mStarted = true;
    531     return OK;
    532 }
    533 
    534 status_t WebmWriter::pause() {
    535     if (mInitCheck != OK) {
    536         return OK;
    537     }
    538     mPaused = true;
    539     status_t err = OK;
    540     for (int i = 0; i < kMaxStreams; ++i) {
    541         if (mStreams[i].mThread == NULL) {
    542             continue;
    543         }
    544         status_t status = mStreams[i].mThread->pause();
    545         if (status != OK) {
    546             err = status;
    547         }
    548     }
    549     return err;
    550 }
    551 
    552 status_t WebmWriter::stop() {
    553     return reset();
    554 }
    555 
    556 bool WebmWriter::reachedEOS() {
    557     return !mSinkThread->running();
    558 }
    559 } /* namespace android */
    560