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 {
XiphLaceCodeLen(size_t size)37 size_t XiphLaceCodeLen(size_t size) {
38 return size / 0xff + 1;
39 }
40
XiphLaceEnc(uint8_t * buf,size_t size)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
WebmWriter(int fd)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
videoTrack(const sp<MetaData> & md)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
audioTrack(const sp<MetaData> & md)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
numTracks()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
estimateCuesSize(int32_t bitRate)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
initStream(size_t idx)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
release()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
reset()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
addSource(const sp<IMediaSource> & source)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
start(MetaData * params)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, 8);
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
pause()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
stop()552 status_t WebmWriter::stop() {
553 return reset();
554 }
555
reachedEOS()556 bool WebmWriter::reachedEOS() {
557 return !mSinkThread->running();
558 }
559 } /* namespace android */
560