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 #include <media/stagefright/foundation/hexdump.h>
27 #include <media/stagefright/foundation/OpusHeader.h>
28
29 #include <utils/Errors.h>
30
31 #include <unistd.h>
32 #include <fcntl.h>
33 #include <sys/stat.h>
34 #include <inttypes.h>
35
36 using namespace webm;
37
38 namespace {
XiphLaceCodeLen(size_t size)39 size_t XiphLaceCodeLen(size_t size) {
40 return size / 0xff + 1;
41 }
42
XiphLaceEnc(uint8_t * buf,size_t size)43 size_t XiphLaceEnc(uint8_t *buf, size_t size) {
44 size_t i;
45 for (i = 0; size >= 0xff; ++i, size -= 0xff) {
46 buf[i] = 0xff;
47 }
48 buf[i++] = size;
49 return i;
50 }
51 }
52
53 namespace android {
54
55 static const int64_t kMinStreamableFileSizeInBytes = 5 * 1024 * 1024;
56
WebmWriter(int fd)57 WebmWriter::WebmWriter(int fd)
58 : mFd(dup(fd)),
59 mInitCheck(mFd < 0 ? NO_INIT : OK),
60 mTimeCodeScale(1000000),
61 mStartTimestampUs(0),
62 mStartTimeOffsetMs(0),
63 mSegmentOffset(0),
64 mSegmentDataStart(0),
65 mInfoOffset(0),
66 mInfoSize(0),
67 mTracksOffset(0),
68 mCuesOffset(0),
69 mPaused(false),
70 mStarted(false),
71 mIsFileSizeLimitExplicitlyRequested(false),
72 mIsRealTimeRecording(false),
73 mStreamableFile(true),
74 mEstimatedCuesSize(0) {
75 mStreams[kAudioIndex] = WebmStream(kAudioType, "Audio", &WebmWriter::audioTrack);
76 mStreams[kVideoIndex] = WebmStream(kVideoType, "Video", &WebmWriter::videoTrack);
77 mSinkThread = new WebmFrameSinkThread(
78 mFd,
79 mSegmentDataStart,
80 mStreams[kVideoIndex].mSink,
81 mStreams[kAudioIndex].mSink,
82 mCuePoints);
83 }
84
85 // static
videoTrack(const sp<MetaData> & md)86 sp<WebmElement> WebmWriter::videoTrack(const sp<MetaData>& md) {
87 int32_t width, height;
88 const char *mimeType;
89 if (!md->findInt32(kKeyWidth, &width)
90 || !md->findInt32(kKeyHeight, &height)
91 || !md->findCString(kKeyMIMEType, &mimeType)) {
92 ALOGE("Missing format keys for video track");
93 md->dumpToLog();
94 return NULL;
95 }
96 const char *codec;
97 if (!strncasecmp(
98 mimeType,
99 MEDIA_MIMETYPE_VIDEO_VP8,
100 strlen(MEDIA_MIMETYPE_VIDEO_VP8))) {
101 codec = "V_VP8";
102 } else if (!strncasecmp(
103 mimeType,
104 MEDIA_MIMETYPE_VIDEO_VP9,
105 strlen(MEDIA_MIMETYPE_VIDEO_VP9))) {
106 codec = "V_VP9";
107 } else {
108 ALOGE("Unsupported codec: %s", mimeType);
109 return NULL;
110 }
111 return WebmElement::VideoTrackEntry(codec, width, height, md);
112 }
113
114 // static
audioTrack(const sp<MetaData> & md)115 sp<WebmElement> WebmWriter::audioTrack(const sp<MetaData>& md) {
116 int32_t nChannels, samplerate;
117 const char* mimeType;
118
119 if (!md->findInt32(kKeyChannelCount, &nChannels)
120 || !md->findInt32(kKeySampleRate, &samplerate)
121 || !md->findCString(kKeyMIMEType, &mimeType)) {
122 ALOGE("Missing format keys for audio track");
123 md->dumpToLog();
124 return NULL;
125 }
126
127 int32_t bitsPerSample = 0;
128 if (!md->findInt32(kKeyBitsPerSample, &bitsPerSample)) {
129 ALOGV("kKeyBitsPerSample not available");
130 }
131
132 if (!strncasecmp(mimeType, MEDIA_MIMETYPE_AUDIO_OPUS, strlen(MEDIA_MIMETYPE_AUDIO_OPUS))) {
133 // Opus in WebM is a well-known, yet under-documented, format. The codec private data
134 // of the track is an Opus Ogg header (https://tools.ietf.org/html/rfc7845#section-5.1)
135 // The name of the track isn't standardized, its value should be "A_OPUS".
136 OpusHeader header;
137 header.channels = nChannels;
138 header.num_streams = nChannels;
139 header.num_coupled = 0;
140 // - Channel mapping family (8 bits unsigned)
141 // -- 0 = one stream: mono or L,R stereo
142 // -- 1 = channels in vorbis spec order: mono or L,R stereo or ... or FL,C,FR,RL,RR,LFE, ...
143 // -- 2..254 = reserved (treat as 255)
144 // -- 255 = no defined channel meaning
145 //
146 // our implementation encodes: 0, 1, or 255
147 header.channel_mapping = ((nChannels > 8) ? 255 : (nChannels > 2));
148 header.gain_db = 0;
149 header.skip_samples = 0;
150
151 // headers are 21-bytes + something driven by channel count
152 // expect numbers in the low 30's here. WriteOpusHeader() will tell us
153 // if things are bad.
154 unsigned char header_data[100];
155 int headerSize = WriteOpusHeader(header, samplerate, (uint8_t*)header_data,
156 sizeof(header_data));
157
158 if (headerSize < 0) {
159 // didn't fill out that header for some reason
160 ALOGE("failed to generate OPUS header");
161 return NULL;
162 }
163
164 size_t codecPrivateSize = 0;
165 codecPrivateSize += headerSize;
166
167 off_t off = 0;
168 sp<ABuffer> codecPrivateBuf = new ABuffer(codecPrivateSize);
169 uint8_t* codecPrivateData = codecPrivateBuf->data();
170
171 memcpy(codecPrivateData + off, (uint8_t*)header_data, headerSize);
172 sp<WebmElement> entry = WebmElement::AudioTrackEntry("A_OPUS", nChannels, samplerate,
173 codecPrivateBuf, bitsPerSample);
174 return entry;
175 } else if (!strncasecmp(mimeType,
176 MEDIA_MIMETYPE_AUDIO_VORBIS,
177 strlen(MEDIA_MIMETYPE_AUDIO_VORBIS))) {
178 uint32_t type;
179 const void *headerData1;
180 const char headerData2[] = { 3, 'v', 'o', 'r', 'b', 'i', 's', 7, 0, 0, 0,
181 'a', 'n', 'd', 'r', 'o', 'i', 'd', 0, 0, 0, 0, 1 };
182 const void *headerData3;
183 size_t headerSize1, headerSize2 = sizeof(headerData2), headerSize3;
184
185 if (!md->findData(kKeyOpaqueCSD0, &type, &headerData1, &headerSize1)
186 || !md->findData(kKeyOpaqueCSD1, &type, &headerData3, &headerSize3)) {
187 ALOGE("Missing header format keys for vorbis track");
188 md->dumpToLog();
189 return NULL;
190 }
191
192 size_t codecPrivateSize = 1;
193 codecPrivateSize += XiphLaceCodeLen(headerSize1);
194 codecPrivateSize += XiphLaceCodeLen(headerSize2);
195 codecPrivateSize += headerSize1 + headerSize2 + headerSize3;
196
197 off_t off = 0;
198 sp<ABuffer> codecPrivateBuf = new ABuffer(codecPrivateSize);
199 uint8_t *codecPrivateData = codecPrivateBuf->data();
200 codecPrivateData[off++] = 2;
201
202 off += XiphLaceEnc(codecPrivateData + off, headerSize1);
203 off += XiphLaceEnc(codecPrivateData + off, headerSize2);
204
205 memcpy(codecPrivateData + off, headerData1, headerSize1);
206 off += headerSize1;
207 memcpy(codecPrivateData + off, headerData2, headerSize2);
208 off += headerSize2;
209 memcpy(codecPrivateData + off, headerData3, headerSize3);
210
211 sp<WebmElement> entry = WebmElement::AudioTrackEntry("A_VORBIS", nChannels, samplerate,
212 codecPrivateBuf, bitsPerSample);
213 return entry;
214 } else {
215 ALOGE("Track (%s) is not a supported audio format", mimeType);
216 return NULL;
217 }
218 }
219
numTracks()220 size_t WebmWriter::numTracks() {
221 Mutex::Autolock autolock(mLock);
222
223 size_t numTracks = 0;
224 for (size_t i = 0; i < kMaxStreams; ++i) {
225 if (mStreams[i].mTrackEntry != NULL) {
226 numTracks++;
227 }
228 }
229
230 return numTracks;
231 }
232
estimateCuesSize(int32_t bitRate)233 uint64_t WebmWriter::estimateCuesSize(int32_t bitRate) {
234 // This implementation is based on estimateMoovBoxSize in MPEG4Writer.
235 //
236 // Statistical analysis shows that metadata usually accounts
237 // for a small portion of the total file size, usually < 0.6%.
238
239 // The default MIN_MOOV_BOX_SIZE is set to 0.6% x 1MB / 2,
240 // where 1MB is the common file size limit for MMS application.
241 // The default MAX _MOOV_BOX_SIZE value is based on about 3
242 // minute video recording with a bit rate about 3 Mbps, because
243 // statistics also show that most of the video captured are going
244 // to be less than 3 minutes.
245
246 // If the estimation is wrong, we will pay the price of wasting
247 // some reserved space. This should not happen so often statistically.
248 static const int32_t factor = 2;
249 static const int64_t MIN_CUES_SIZE = 3 * 1024; // 3 KB
250 static const int64_t MAX_CUES_SIZE = (180 * 3000000 * 6LL / 8000);
251 int64_t size = MIN_CUES_SIZE;
252
253 // Max file size limit is set
254 if (mMaxFileSizeLimitBytes != 0 && mIsFileSizeLimitExplicitlyRequested) {
255 size = mMaxFileSizeLimitBytes * 6 / 1000;
256 }
257
258 // Max file duration limit is set
259 if (mMaxFileDurationLimitUs != 0) {
260 if (bitRate > 0) {
261 int64_t size2 = ((mMaxFileDurationLimitUs * bitRate * 6) / 1000 / 8000000);
262 if (mMaxFileSizeLimitBytes != 0 && mIsFileSizeLimitExplicitlyRequested) {
263 // When both file size and duration limits are set,
264 // we use the smaller limit of the two.
265 if (size > size2) {
266 size = size2;
267 }
268 } else {
269 // Only max file duration limit is set
270 size = size2;
271 }
272 }
273 }
274
275 if (size < MIN_CUES_SIZE) {
276 size = MIN_CUES_SIZE;
277 }
278
279 // Any long duration recording will be probably end up with
280 // non-streamable webm file.
281 if (size > MAX_CUES_SIZE) {
282 size = MAX_CUES_SIZE;
283 }
284
285 ALOGV("limits: %" PRId64 "/%" PRId64 " bytes/us,"
286 " bit rate: %d bps and the estimated cues size %" PRId64 " bytes",
287 mMaxFileSizeLimitBytes, mMaxFileDurationLimitUs, bitRate, size);
288 return factor * size;
289 }
290
initStream(size_t idx)291 void WebmWriter::initStream(size_t idx) {
292 if (mStreams[idx].mThread != NULL) {
293 return;
294 }
295 if (mStreams[idx].mSource == NULL) {
296 ALOGV("adding dummy source ... ");
297 mStreams[idx].mThread = new WebmFrameEmptySourceThread(
298 mStreams[idx].mType, mStreams[idx].mSink);
299 } else {
300 ALOGV("adding source %p", mStreams[idx].mSource.get());
301 mStreams[idx].mThread = new WebmFrameMediaSourceThread(
302 mStreams[idx].mSource,
303 mStreams[idx].mType,
304 mStreams[idx].mSink,
305 mTimeCodeScale,
306 mStartTimestampUs,
307 mStartTimeOffsetMs,
308 numTracks(),
309 mIsRealTimeRecording);
310 }
311 }
312
release()313 void WebmWriter::release() {
314 close(mFd);
315 mFd = -1;
316 mInitCheck = NO_INIT;
317 mStarted = false;
318 for (size_t ix = 0; ix < kMaxStreams; ++ix) {
319 mStreams[ix].mTrackEntry.clear();
320 mStreams[ix].mSource.clear();
321 }
322 mStreamsInOrder.clear();
323 }
324
reset()325 status_t WebmWriter::reset() {
326 if (mInitCheck != OK) {
327 return OK;
328 } else {
329 if (!mStarted) {
330 release();
331 return OK;
332 }
333 }
334
335 status_t err = OK;
336 int64_t maxDurationUs = 0;
337 int64_t minDurationUs = 0x7fffffffffffffffLL;
338 for (int i = 0; i < kMaxStreams; ++i) {
339 if (mStreams[i].mThread == NULL) {
340 continue;
341 }
342
343 status_t status = mStreams[i].mThread->stop();
344 if (err == OK && status != OK) {
345 err = status;
346 }
347
348 int64_t durationUs = mStreams[i].mThread->getDurationUs();
349 if (durationUs > maxDurationUs) {
350 maxDurationUs = durationUs;
351 }
352 if (durationUs < minDurationUs) {
353 minDurationUs = durationUs;
354 }
355
356 mStreams[i].mThread.clear();
357 }
358
359 if (numTracks() > 1) {
360 ALOGD("Duration from tracks range is [%" PRId64 ", %" PRId64 "] us", minDurationUs, maxDurationUs);
361 }
362
363 mSinkThread->stop();
364
365 // Do not write out movie header on error.
366 if (err != OK) {
367 release();
368 return err;
369 }
370
371 sp<WebmElement> cues = new WebmMaster(kMkvCues, mCuePoints);
372 uint64_t cuesSize = cues->totalSize();
373 // TRICKY Even when the cues do fit in the space we reserved, if they do not fit
374 // perfectly, we still need to check if there is enough "extra space" to write an
375 // EBML void element.
376 if (cuesSize != mEstimatedCuesSize && cuesSize > mEstimatedCuesSize - kMinEbmlVoidSize) {
377 mCuesOffset = ::lseek(mFd, 0, SEEK_CUR);
378 cues->write(mFd, cuesSize);
379 } else {
380 uint64_t spaceSize;
381 ::lseek(mFd, mCuesOffset, SEEK_SET);
382 cues->write(mFd, cuesSize);
383 sp<WebmElement> space = new EbmlVoid(mEstimatedCuesSize - cuesSize);
384 space->write(mFd, spaceSize);
385 }
386
387 mCuePoints.clear();
388 mStreams[kVideoIndex].mSink.clear();
389 mStreams[kAudioIndex].mSink.clear();
390
391 uint8_t bary[sizeof(uint64_t)];
392 uint64_t totalSize = ::lseek(mFd, 0, SEEK_END);
393 uint64_t segmentSize = totalSize - mSegmentDataStart;
394 ::lseek(mFd, mSegmentOffset + sizeOf(kMkvSegment), SEEK_SET);
395 uint64_t segmentSizeCoded = encodeUnsigned(segmentSize, sizeOf(kMkvUnknownLength));
396 serializeCodedUnsigned(segmentSizeCoded, bary);
397 ::write(mFd, bary, sizeOf(kMkvUnknownLength));
398
399 uint64_t durationOffset = mInfoOffset + sizeOf(kMkvInfo) + sizeOf(mInfoSize)
400 + sizeOf(kMkvSegmentDuration) + sizeOf(sizeof(double));
401 sp<WebmElement> duration = new WebmFloat(
402 kMkvSegmentDuration,
403 (double) (maxDurationUs * 1000 / mTimeCodeScale));
404 duration->serializePayload(bary);
405 ::lseek(mFd, durationOffset, SEEK_SET);
406 ::write(mFd, bary, sizeof(double));
407
408 List<sp<WebmElement> > seekEntries;
409 seekEntries.push_back(WebmElement::SeekEntry(kMkvInfo, mInfoOffset - mSegmentDataStart));
410 seekEntries.push_back(WebmElement::SeekEntry(kMkvTracks, mTracksOffset - mSegmentDataStart));
411 seekEntries.push_back(WebmElement::SeekEntry(kMkvCues, mCuesOffset - mSegmentDataStart));
412 sp<WebmElement> seekHead = new WebmMaster(kMkvSeekHead, seekEntries);
413
414 uint64_t metaSeekSize;
415 ::lseek(mFd, mSegmentDataStart, SEEK_SET);
416 seekHead->write(mFd, metaSeekSize);
417
418 uint64_t spaceSize;
419 sp<WebmElement> space = new EbmlVoid(kMaxMetaSeekSize - metaSeekSize);
420 space->write(mFd, spaceSize);
421
422 release();
423 return err;
424 }
425
addSource(const sp<MediaSource> & source)426 status_t WebmWriter::addSource(const sp<MediaSource> &source) {
427 Mutex::Autolock l(mLock);
428 if (mStarted) {
429 ALOGE("Attempt to add source AFTER recording is started");
430 return UNKNOWN_ERROR;
431 }
432
433 // At most 2 tracks can be supported.
434 if (mStreams[kVideoIndex].mTrackEntry != NULL
435 && mStreams[kAudioIndex].mTrackEntry != NULL) {
436 ALOGE("Too many tracks (2) to add");
437 return ERROR_UNSUPPORTED;
438 }
439
440 CHECK(source != NULL);
441
442 // A track of type other than video or audio is not supported.
443 const char *mime;
444 source->getFormat()->findCString(kKeyMIMEType, &mime);
445 const char *vp8 = MEDIA_MIMETYPE_VIDEO_VP8;
446 const char *vp9 = MEDIA_MIMETYPE_VIDEO_VP9;
447 const char *vorbis = MEDIA_MIMETYPE_AUDIO_VORBIS;
448 const char* opus = MEDIA_MIMETYPE_AUDIO_OPUS;
449
450 size_t streamIndex;
451 if (!strncasecmp(mime, vp8, strlen(vp8)) ||
452 !strncasecmp(mime, vp9, strlen(vp9))) {
453 streamIndex = kVideoIndex;
454 } else if (!strncasecmp(mime, vorbis, strlen(vorbis)) ||
455 !strncasecmp(mime, opus, strlen(opus))) {
456 streamIndex = kAudioIndex;
457 } else {
458 ALOGE("Track (%s) other than %s, %s, %s, or %s is not supported",
459 mime, vp8, vp9, vorbis, opus);
460 return ERROR_UNSUPPORTED;
461 }
462
463 // No more than one video or one audio track is supported.
464 if (mStreams[streamIndex].mTrackEntry != NULL) {
465 ALOGE("%s track already exists", mStreams[streamIndex].mName);
466 return ERROR_UNSUPPORTED;
467 }
468
469 // This is the first track of either audio or video.
470 // Go ahead to add the track.
471 mStreams[streamIndex].mSource = source;
472 mStreams[streamIndex].mTrackEntry = mStreams[streamIndex].mMakeTrack(source->getFormat());
473 if (mStreams[streamIndex].mTrackEntry == NULL) {
474 mStreams[streamIndex].mSource.clear();
475 return BAD_VALUE;
476 }
477 mStreamsInOrder.push_back(mStreams[streamIndex].mTrackEntry);
478
479 return OK;
480 }
481
start(MetaData * params)482 status_t WebmWriter::start(MetaData *params) {
483 if (mInitCheck != OK) {
484 return UNKNOWN_ERROR;
485 }
486
487 if (mStreams[kVideoIndex].mTrackEntry == NULL
488 && mStreams[kAudioIndex].mTrackEntry == NULL) {
489 ALOGE("No source added");
490 return INVALID_OPERATION;
491 }
492
493 if (mMaxFileSizeLimitBytes != 0) {
494 mIsFileSizeLimitExplicitlyRequested = true;
495 }
496
497 if (params) {
498 int32_t isRealTimeRecording;
499 params->findInt32(kKeyRealTimeRecording, &isRealTimeRecording);
500 mIsRealTimeRecording = isRealTimeRecording;
501 }
502
503 if (mStarted) {
504 if (mPaused) {
505 mPaused = false;
506 mStreams[kAudioIndex].mThread->resume();
507 mStreams[kVideoIndex].mThread->resume();
508 }
509 return OK;
510 }
511
512 if (params) {
513 int32_t tcsl;
514 if (params->findInt32(kKeyTimeScale, &tcsl)) {
515 mTimeCodeScale = tcsl;
516 }
517 }
518 if (mTimeCodeScale == 0) {
519 ALOGE("movie time scale is 0");
520 return BAD_VALUE;
521 }
522 ALOGV("movie time scale: %" PRIu64, mTimeCodeScale);
523
524 /*
525 * When the requested file size limit is small, the priority
526 * is to meet the file size limit requirement, rather than
527 * to make the file streamable. mStreamableFile does not tell
528 * whether the actual recorded file is streamable or not.
529 */
530 mStreamableFile = (!mMaxFileSizeLimitBytes)
531 || (mMaxFileSizeLimitBytes >= kMinStreamableFileSizeInBytes);
532
533 /*
534 * Write various metadata.
535 */
536 sp<WebmElement> ebml, segment, info, seekHead, tracks, cues;
537 ebml = WebmElement::EbmlHeader();
538 segment = new WebmMaster(kMkvSegment);
539 seekHead = new EbmlVoid(kMaxMetaSeekSize);
540 info = WebmElement::SegmentInfo(mTimeCodeScale, 0);
541
542 List<sp<WebmElement> > children;
543 for (size_t i = 0; i < mStreamsInOrder.size(); ++i) {
544 children.push_back(mStreamsInOrder[i]);
545 }
546 tracks = new WebmMaster(kMkvTracks, children);
547
548 if (!mStreamableFile) {
549 cues = NULL;
550 } else {
551 int32_t bitRate = -1;
552 if (params) {
553 params->findInt32(kKeyBitRate, &bitRate);
554 }
555 mEstimatedCuesSize = estimateCuesSize(bitRate);
556 CHECK_GE(mEstimatedCuesSize, 8u);
557 cues = new EbmlVoid(mEstimatedCuesSize);
558 }
559
560 sp<WebmElement> elems[] = { ebml, segment, seekHead, info, tracks, cues };
561 static const size_t nElems = sizeof(elems) / sizeof(elems[0]);
562 uint64_t offsets[nElems];
563 uint64_t sizes[nElems];
564 for (uint32_t i = 0; i < nElems; i++) {
565 WebmElement *e = elems[i].get();
566 if (!e) {
567 continue;
568 }
569
570 uint64_t size;
571 offsets[i] = ::lseek(mFd, 0, SEEK_CUR);
572 sizes[i] = e->mSize;
573 e->write(mFd, size);
574 }
575
576 mSegmentOffset = offsets[1];
577 mSegmentDataStart = offsets[2];
578 mInfoOffset = offsets[3];
579 mInfoSize = sizes[3];
580 mTracksOffset = offsets[4];
581 mCuesOffset = offsets[5];
582
583 // start threads
584 if (params) {
585 params->findInt64(kKeyTime, &mStartTimestampUs);
586 }
587
588 initStream(kAudioIndex);
589 initStream(kVideoIndex);
590
591 mStreams[kAudioIndex].mThread->start();
592 mStreams[kVideoIndex].mThread->start();
593 mSinkThread->start();
594
595 mStarted = true;
596 return OK;
597 }
598
pause()599 status_t WebmWriter::pause() {
600 if (mInitCheck != OK) {
601 return OK;
602 }
603 mPaused = true;
604 status_t err = OK;
605 for (int i = 0; i < kMaxStreams; ++i) {
606 if (mStreams[i].mThread == NULL) {
607 continue;
608 }
609 status_t status = mStreams[i].mThread->pause();
610 if (status != OK) {
611 err = status;
612 }
613 }
614 return err;
615 }
616
stop()617 status_t WebmWriter::stop() {
618 return reset();
619 }
620
reachedEOS()621 bool WebmWriter::reachedEOS() {
622 return !mSinkThread->running();
623 }
624 } /* namespace android */
625