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