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