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