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