WebmWriter.cpp revision 2bae6deec84016b9caaffcb536534820c47b12d2
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
34using namespace webm;
35
36namespace {
37size_t XiphLaceCodeLen(size_t size) {
38    return size / 0xff + 1;
39}
40
41size_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
51namespace android {
52
53static const int64_t kMinStreamableFileSizeInBytes = 5 * 1024 * 1024;
54
55WebmWriter::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
84sp<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
113sp<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
157size_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
170uint64_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
228void 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
250void 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
262status_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
363status_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
417status_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, 8);
492        cues = new EbmlVoid(mEstimatedCuesSize);
493    }
494
495    sp<WebmElement> elems[] = { ebml, segment, seekHead, info, tracks, cues };
496    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
534status_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
552status_t WebmWriter::stop() {
553    return reset();
554}
555
556bool WebmWriter::reachedEOS() {
557    return !mSinkThread->running();
558}
559} /* namespace android */
560