WebmWriter.cpp revision c6ac859f5a82ea8642bc6351a45508a15f224f32
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    CHECK(md->findInt32(kKeyWidth, &width));
87    CHECK(md->findInt32(kKeyHeight, &height));
88    return WebmElement::VideoTrackEntry(width, height);
89}
90
91// static
92sp<WebmElement> WebmWriter::audioTrack(const sp<MetaData>& md) {
93    int32_t nChannels, samplerate;
94    uint32_t type;
95    const void *headerData1;
96    const char headerData2[] = { 3, 'v', 'o', 'r', 'b', 'i', 's', 7, 0, 0, 0,
97            'a', 'n', 'd', 'r', 'o', 'i', 'd', 0, 0, 0, 0, 1 };
98    const void *headerData3;
99    size_t headerSize1, headerSize2 = sizeof(headerData2), headerSize3;
100
101    CHECK(md->findInt32(kKeyChannelCount, &nChannels));
102    CHECK(md->findInt32(kKeySampleRate, &samplerate));
103    CHECK(md->findData(kKeyVorbisInfo, &type, &headerData1, &headerSize1));
104    CHECK(md->findData(kKeyVorbisBooks, &type, &headerData3, &headerSize3));
105
106    size_t codecPrivateSize = 1;
107    codecPrivateSize += XiphLaceCodeLen(headerSize1);
108    codecPrivateSize += XiphLaceCodeLen(headerSize2);
109    codecPrivateSize += headerSize1 + headerSize2 + headerSize3;
110
111    off_t off = 0;
112    sp<ABuffer> codecPrivateBuf = new ABuffer(codecPrivateSize);
113    uint8_t *codecPrivateData = codecPrivateBuf->data();
114    codecPrivateData[off++] = 2;
115
116    off += XiphLaceEnc(codecPrivateData + off, headerSize1);
117    off += XiphLaceEnc(codecPrivateData + off, headerSize2);
118
119    memcpy(codecPrivateData + off, headerData1, headerSize1);
120    off += headerSize1;
121    memcpy(codecPrivateData + off, headerData2, headerSize2);
122    off += headerSize2;
123    memcpy(codecPrivateData + off, headerData3, headerSize3);
124
125    sp<WebmElement> entry = WebmElement::AudioTrackEntry(
126            nChannels,
127            samplerate,
128            codecPrivateBuf);
129    return entry;
130}
131
132size_t WebmWriter::numTracks() {
133    Mutex::Autolock autolock(mLock);
134
135    size_t numTracks = 0;
136    for (size_t i = 0; i < kMaxStreams; ++i) {
137        if (mStreams[i].mTrackEntry != NULL) {
138            numTracks++;
139        }
140    }
141
142    return numTracks;
143}
144
145uint64_t WebmWriter::estimateCuesSize(int32_t bitRate) {
146    // This implementation is based on estimateMoovBoxSize in MPEG4Writer.
147    //
148    // Statistical analysis shows that metadata usually accounts
149    // for a small portion of the total file size, usually < 0.6%.
150
151    // The default MIN_MOOV_BOX_SIZE is set to 0.6% x 1MB / 2,
152    // where 1MB is the common file size limit for MMS application.
153    // The default MAX _MOOV_BOX_SIZE value is based on about 3
154    // minute video recording with a bit rate about 3 Mbps, because
155    // statistics also show that most of the video captured are going
156    // to be less than 3 minutes.
157
158    // If the estimation is wrong, we will pay the price of wasting
159    // some reserved space. This should not happen so often statistically.
160    static const int32_t factor = 2;
161    static const int64_t MIN_CUES_SIZE = 3 * 1024;  // 3 KB
162    static const int64_t MAX_CUES_SIZE = (180 * 3000000 * 6LL / 8000);
163    int64_t size = MIN_CUES_SIZE;
164
165    // Max file size limit is set
166    if (mMaxFileSizeLimitBytes != 0 && mIsFileSizeLimitExplicitlyRequested) {
167        size = mMaxFileSizeLimitBytes * 6 / 1000;
168    }
169
170    // Max file duration limit is set
171    if (mMaxFileDurationLimitUs != 0) {
172        if (bitRate > 0) {
173            int64_t size2 = ((mMaxFileDurationLimitUs * bitRate * 6) / 1000 / 8000000);
174            if (mMaxFileSizeLimitBytes != 0 && mIsFileSizeLimitExplicitlyRequested) {
175                // When both file size and duration limits are set,
176                // we use the smaller limit of the two.
177                if (size > size2) {
178                    size = size2;
179                }
180            } else {
181                // Only max file duration limit is set
182                size = size2;
183            }
184        }
185    }
186
187    if (size < MIN_CUES_SIZE) {
188        size = MIN_CUES_SIZE;
189    }
190
191    // Any long duration recording will be probably end up with
192    // non-streamable webm file.
193    if (size > MAX_CUES_SIZE) {
194        size = MAX_CUES_SIZE;
195    }
196
197    ALOGV("limits: %" PRId64 "/%" PRId64 " bytes/us,"
198            " bit rate: %d bps and the estimated cues size %" PRId64 " bytes",
199            mMaxFileSizeLimitBytes, mMaxFileDurationLimitUs, bitRate, size);
200    return factor * size;
201}
202
203void WebmWriter::initStream(size_t idx) {
204    if (mStreams[idx].mThread != NULL) {
205        return;
206    }
207    if (mStreams[idx].mSource == NULL) {
208        ALOGV("adding dummy source ... ");
209        mStreams[idx].mThread = new WebmFrameEmptySourceThread(
210                mStreams[idx].mType, mStreams[idx].mSink);
211    } else {
212        ALOGV("adding source %p", mStreams[idx].mSource.get());
213        mStreams[idx].mThread = new WebmFrameMediaSourceThread(
214                mStreams[idx].mSource,
215                mStreams[idx].mType,
216                mStreams[idx].mSink,
217                mTimeCodeScale,
218                mStartTimestampUs,
219                mStartTimeOffsetMs,
220                numTracks(),
221                mIsRealTimeRecording);
222    }
223}
224
225void WebmWriter::release() {
226    close(mFd);
227    mFd = -1;
228    mInitCheck = NO_INIT;
229    mStarted = false;
230}
231
232status_t WebmWriter::reset() {
233    if (mInitCheck != OK) {
234        return OK;
235    } else {
236        if (!mStarted) {
237            release();
238            return OK;
239        }
240    }
241
242    status_t err = OK;
243    int64_t maxDurationUs = 0;
244    int64_t minDurationUs = 0x7fffffffffffffffLL;
245    for (int i = 0; i < kMaxStreams; ++i) {
246        if (mStreams[i].mThread == NULL) {
247            continue;
248        }
249
250        status_t status = mStreams[i].mThread->stop();
251        if (err == OK && status != OK) {
252            err = status;
253        }
254
255        int64_t durationUs = mStreams[i].mThread->getDurationUs();
256        if (durationUs > maxDurationUs) {
257            maxDurationUs = durationUs;
258        }
259        if (durationUs < minDurationUs) {
260            minDurationUs = durationUs;
261        }
262    }
263
264    if (numTracks() > 1) {
265        ALOGD("Duration from tracks range is [%" PRId64 ", %" PRId64 "] us", minDurationUs, maxDurationUs);
266    }
267
268    mSinkThread->stop();
269
270    // Do not write out movie header on error.
271    if (err != OK) {
272        release();
273        return err;
274    }
275
276    sp<WebmElement> cues = new WebmMaster(kMkvCues, mCuePoints);
277    uint64_t cuesSize = cues->totalSize();
278    // TRICKY Even when the cues do fit in the space we reserved, if they do not fit
279    // perfectly, we still need to check if there is enough "extra space" to write an
280    // EBML void element.
281    if (cuesSize != mEstimatedCuesSize && cuesSize > mEstimatedCuesSize - kMinEbmlVoidSize) {
282        mCuesOffset = ::lseek(mFd, 0, SEEK_CUR);
283        cues->write(mFd, cuesSize);
284    } else {
285        uint64_t spaceSize;
286        ::lseek(mFd, mCuesOffset, SEEK_SET);
287        cues->write(mFd, cuesSize);
288        sp<WebmElement> space = new EbmlVoid(mEstimatedCuesSize - cuesSize);
289        space->write(mFd, spaceSize);
290    }
291
292    mCuePoints.clear();
293    mStreams[kVideoIndex].mSink.clear();
294    mStreams[kAudioIndex].mSink.clear();
295
296    uint8_t bary[sizeof(uint64_t)];
297    uint64_t totalSize = ::lseek(mFd, 0, SEEK_END);
298    uint64_t segmentSize = totalSize - mSegmentDataStart;
299    ::lseek(mFd, mSegmentOffset + sizeOf(kMkvSegment), SEEK_SET);
300    uint64_t segmentSizeCoded = encodeUnsigned(segmentSize, sizeOf(kMkvUnknownLength));
301    serializeCodedUnsigned(segmentSizeCoded, bary);
302    ::write(mFd, bary, sizeOf(kMkvUnknownLength));
303
304    uint64_t durationOffset = mInfoOffset + sizeOf(kMkvInfo) + sizeOf(mInfoSize)
305        + sizeOf(kMkvSegmentDuration) + sizeOf(sizeof(double));
306    sp<WebmElement> duration = new WebmFloat(
307            kMkvSegmentDuration,
308            (double) (maxDurationUs * 1000 / mTimeCodeScale));
309    duration->serializePayload(bary);
310    ::lseek(mFd, durationOffset, SEEK_SET);
311    ::write(mFd, bary, sizeof(double));
312
313    List<sp<WebmElement> > seekEntries;
314    seekEntries.push_back(WebmElement::SeekEntry(kMkvInfo, mInfoOffset - mSegmentDataStart));
315    seekEntries.push_back(WebmElement::SeekEntry(kMkvTracks, mTracksOffset - mSegmentDataStart));
316    seekEntries.push_back(WebmElement::SeekEntry(kMkvCues, mCuesOffset - mSegmentDataStart));
317    sp<WebmElement> seekHead = new WebmMaster(kMkvSeekHead, seekEntries);
318
319    uint64_t metaSeekSize;
320    ::lseek(mFd, mSegmentDataStart, SEEK_SET);
321    seekHead->write(mFd, metaSeekSize);
322
323    uint64_t spaceSize;
324    sp<WebmElement> space = new EbmlVoid(kMaxMetaSeekSize - metaSeekSize);
325    space->write(mFd, spaceSize);
326
327    release();
328    return err;
329}
330
331status_t WebmWriter::addSource(const sp<MediaSource> &source) {
332    Mutex::Autolock l(mLock);
333    if (mStarted) {
334        ALOGE("Attempt to add source AFTER recording is started");
335        return UNKNOWN_ERROR;
336    }
337
338    // At most 2 tracks can be supported.
339    if (mStreams[kVideoIndex].mTrackEntry != NULL
340            && mStreams[kAudioIndex].mTrackEntry != NULL) {
341        ALOGE("Too many tracks (2) to add");
342        return ERROR_UNSUPPORTED;
343    }
344
345    CHECK(source != NULL);
346
347    // A track of type other than video or audio is not supported.
348    const char *mime;
349    source->getFormat()->findCString(kKeyMIMEType, &mime);
350    const char *vp8 = MEDIA_MIMETYPE_VIDEO_VP8;
351    const char *vorbis = MEDIA_MIMETYPE_AUDIO_VORBIS;
352
353    size_t streamIndex;
354    if (!strncasecmp(mime, vp8, strlen(vp8))) {
355        streamIndex = kVideoIndex;
356    } else if (!strncasecmp(mime, vorbis, strlen(vorbis))) {
357        streamIndex = kAudioIndex;
358    } else {
359        ALOGE("Track (%s) other than %s or %s is not supported", mime, vp8, vorbis);
360        return ERROR_UNSUPPORTED;
361    }
362
363    // No more than one video or one audio track is supported.
364    if (mStreams[streamIndex].mTrackEntry != NULL) {
365        ALOGE("%s track already exists", mStreams[streamIndex].mName);
366        return ERROR_UNSUPPORTED;
367    }
368
369    // This is the first track of either audio or video.
370    // Go ahead to add the track.
371    mStreams[streamIndex].mSource = source;
372    mStreams[streamIndex].mTrackEntry = mStreams[streamIndex].mMakeTrack(source->getFormat());
373
374    return OK;
375}
376
377status_t WebmWriter::start(MetaData *params) {
378    if (mInitCheck != OK) {
379        return UNKNOWN_ERROR;
380    }
381
382    if (mStreams[kVideoIndex].mTrackEntry == NULL
383            && mStreams[kAudioIndex].mTrackEntry == NULL) {
384        ALOGE("No source added");
385        return INVALID_OPERATION;
386    }
387
388    if (mMaxFileSizeLimitBytes != 0) {
389        mIsFileSizeLimitExplicitlyRequested = true;
390    }
391
392    if (params) {
393        int32_t isRealTimeRecording;
394        params->findInt32(kKeyRealTimeRecording, &isRealTimeRecording);
395        mIsRealTimeRecording = isRealTimeRecording;
396    }
397
398    if (mStarted) {
399        if (mPaused) {
400            mPaused = false;
401            mStreams[kAudioIndex].mThread->resume();
402            mStreams[kVideoIndex].mThread->resume();
403        }
404        return OK;
405    }
406
407    if (params) {
408        int32_t tcsl;
409        if (params->findInt32(kKeyTimeScale, &tcsl)) {
410            mTimeCodeScale = tcsl;
411        }
412    }
413    CHECK_GT(mTimeCodeScale, 0);
414    ALOGV("movie time scale: %" PRIu64, mTimeCodeScale);
415
416    /*
417     * When the requested file size limit is small, the priority
418     * is to meet the file size limit requirement, rather than
419     * to make the file streamable. mStreamableFile does not tell
420     * whether the actual recorded file is streamable or not.
421     */
422    mStreamableFile = (!mMaxFileSizeLimitBytes)
423        || (mMaxFileSizeLimitBytes >= kMinStreamableFileSizeInBytes);
424
425    /*
426     * Write various metadata.
427     */
428    sp<WebmElement> ebml, segment, info, seekHead, tracks, cues;
429    ebml = WebmElement::EbmlHeader();
430    segment = new WebmMaster(kMkvSegment);
431    seekHead = new EbmlVoid(kMaxMetaSeekSize);
432    info = WebmElement::SegmentInfo(mTimeCodeScale, 0);
433
434    List<sp<WebmElement> > children;
435    for (size_t i = 0; i < kMaxStreams; ++i) {
436        if (mStreams[i].mTrackEntry != NULL) {
437            children.push_back(mStreams[i].mTrackEntry);
438        }
439    }
440    tracks = new WebmMaster(kMkvTracks, children);
441
442    if (!mStreamableFile) {
443        cues = NULL;
444    } else {
445        int32_t bitRate = -1;
446        if (params) {
447            params->findInt32(kKeyBitRate, &bitRate);
448        }
449        mEstimatedCuesSize = estimateCuesSize(bitRate);
450        CHECK_GE(mEstimatedCuesSize, 8);
451        cues = new EbmlVoid(mEstimatedCuesSize);
452    }
453
454    sp<WebmElement> elems[] = { ebml, segment, seekHead, info, tracks, cues };
455    size_t nElems = sizeof(elems) / sizeof(elems[0]);
456    uint64_t offsets[nElems];
457    uint64_t sizes[nElems];
458    for (uint32_t i = 0; i < nElems; i++) {
459        WebmElement *e = elems[i].get();
460        if (!e) {
461            continue;
462        }
463
464        uint64_t size;
465        offsets[i] = ::lseek(mFd, 0, SEEK_CUR);
466        sizes[i] = e->mSize;
467        e->write(mFd, size);
468    }
469
470    mSegmentOffset = offsets[1];
471    mSegmentDataStart = offsets[2];
472    mInfoOffset = offsets[3];
473    mInfoSize = sizes[3];
474    mTracksOffset = offsets[4];
475    mCuesOffset = offsets[5];
476
477    // start threads
478    if (params) {
479        params->findInt64(kKeyTime, &mStartTimestampUs);
480    }
481
482    initStream(kAudioIndex);
483    initStream(kVideoIndex);
484
485    mStreams[kAudioIndex].mThread->start();
486    mStreams[kVideoIndex].mThread->start();
487    mSinkThread->start();
488
489    mStarted = true;
490    return OK;
491}
492
493status_t WebmWriter::pause() {
494    if (mInitCheck != OK) {
495        return OK;
496    }
497    mPaused = true;
498    status_t err = OK;
499    for (int i = 0; i < kMaxStreams; ++i) {
500        if (mStreams[i].mThread == NULL) {
501            continue;
502        }
503        status_t status = mStreams[i].mThread->pause();
504        if (status != OK) {
505            err = status;
506        }
507    }
508    return err;
509}
510
511status_t WebmWriter::stop() {
512    return reset();
513}
514
515bool WebmWriter::reachedEOS() {
516    return !mSinkThread->running();
517}
518} /* namespace android */
519