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