LiveSession.cpp revision 9b80c2bdb205bc143104f54d0743b6eedd67b14e
1/*
2 * Copyright (C) 2010 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 "LiveSession"
19#include <utils/Log.h>
20
21#include "include/LiveSession.h"
22
23#include "LiveDataSource.h"
24
25#include "include/M3UParser.h"
26#include "include/HTTPBase.h"
27
28#include <cutils/properties.h>
29#include <media/stagefright/foundation/hexdump.h>
30#include <media/stagefright/foundation/ABuffer.h>
31#include <media/stagefright/foundation/ADebug.h>
32#include <media/stagefright/foundation/AMessage.h>
33#include <media/stagefright/DataSource.h>
34#include <media/stagefright/FileSource.h>
35#include <media/stagefright/MediaErrors.h>
36
37#include <ctype.h>
38#include <openssl/aes.h>
39
40namespace android {
41
42const int64_t LiveSession::kMaxPlaylistAgeUs = 15000000ll;
43
44LiveSession::LiveSession(uint32_t flags, bool uidValid, uid_t uid)
45    : mFlags(flags),
46      mUIDValid(uidValid),
47      mUID(uid),
48      mDataSource(new LiveDataSource),
49      mHTTPDataSource(
50              HTTPBase::Create(
51                  (mFlags & kFlagIncognito)
52                    ? HTTPBase::kFlagIncognito
53                    : 0)),
54      mPrevBandwidthIndex(-1),
55      mLastPlaylistFetchTimeUs(-1),
56      mSeqNumber(-1),
57      mSeekTimeUs(-1),
58      mNumRetries(0),
59      mDurationUs(-1),
60      mSeekDone(false),
61      mDisconnectPending(false),
62      mMonitorQueueGeneration(0) {
63    if (mUIDValid) {
64        mHTTPDataSource->setUID(mUID);
65    }
66}
67
68LiveSession::~LiveSession() {
69}
70
71sp<DataSource> LiveSession::getDataSource() {
72    return mDataSource;
73}
74
75void LiveSession::connect(
76        const char *url, const KeyedVector<String8, String8> *headers) {
77    sp<AMessage> msg = new AMessage(kWhatConnect, id());
78    msg->setString("url", url);
79
80    if (headers != NULL) {
81        msg->setPointer(
82                "headers",
83                new KeyedVector<String8, String8>(*headers));
84    }
85
86    msg->post();
87}
88
89void LiveSession::disconnect() {
90    Mutex::Autolock autoLock(mLock);
91    mDisconnectPending = true;
92
93    mHTTPDataSource->disconnect();
94
95    (new AMessage(kWhatDisconnect, id()))->post();
96}
97
98void LiveSession::seekTo(int64_t timeUs) {
99    Mutex::Autolock autoLock(mLock);
100    mSeekDone = false;
101
102    sp<AMessage> msg = new AMessage(kWhatSeek, id());
103    msg->setInt64("timeUs", timeUs);
104    msg->post();
105
106    while (!mSeekDone) {
107        mCondition.wait(mLock);
108    }
109}
110
111void LiveSession::onMessageReceived(const sp<AMessage> &msg) {
112    switch (msg->what()) {
113        case kWhatConnect:
114            onConnect(msg);
115            break;
116
117        case kWhatDisconnect:
118            onDisconnect();
119            break;
120
121        case kWhatMonitorQueue:
122        {
123            int32_t generation;
124            CHECK(msg->findInt32("generation", &generation));
125
126            if (generation != mMonitorQueueGeneration) {
127                // Stale event
128                break;
129            }
130
131            onMonitorQueue();
132            break;
133        }
134
135        case kWhatSeek:
136            onSeek(msg);
137            break;
138
139        default:
140            TRESPASS();
141            break;
142    }
143}
144
145// static
146int LiveSession::SortByBandwidth(const BandwidthItem *a, const BandwidthItem *b) {
147    if (a->mBandwidth < b->mBandwidth) {
148        return -1;
149    } else if (a->mBandwidth == b->mBandwidth) {
150        return 0;
151    }
152
153    return 1;
154}
155
156void LiveSession::onConnect(const sp<AMessage> &msg) {
157    AString url;
158    CHECK(msg->findString("url", &url));
159
160    KeyedVector<String8, String8> *headers = NULL;
161    if (!msg->findPointer("headers", (void **)&headers)) {
162        mExtraHeaders.clear();
163    } else {
164        mExtraHeaders = *headers;
165
166        delete headers;
167        headers = NULL;
168    }
169
170    if (!(mFlags & kFlagIncognito)) {
171        LOGI("onConnect '%s'", url.c_str());
172    } else {
173        LOGI("onConnect <URL suppressed>");
174    }
175
176    mMasterURL = url;
177
178    sp<M3UParser> playlist = fetchPlaylist(url.c_str());
179
180    if (playlist == NULL) {
181        LOGE("unable to fetch master playlist '%s'.", url.c_str());
182
183        mDataSource->queueEOS(ERROR_IO);
184        return;
185    }
186
187    if (playlist->isVariantPlaylist()) {
188        for (size_t i = 0; i < playlist->size(); ++i) {
189            BandwidthItem item;
190
191            sp<AMessage> meta;
192            playlist->itemAt(i, &item.mURI, &meta);
193
194            unsigned long bandwidth;
195            CHECK(meta->findInt32("bandwidth", (int32_t *)&item.mBandwidth));
196
197            mBandwidthItems.push(item);
198        }
199
200        CHECK_GT(mBandwidthItems.size(), 0u);
201
202        mBandwidthItems.sort(SortByBandwidth);
203    }
204
205    postMonitorQueue();
206}
207
208void LiveSession::onDisconnect() {
209    LOGI("onDisconnect");
210
211    mDataSource->queueEOS(ERROR_END_OF_STREAM);
212
213    Mutex::Autolock autoLock(mLock);
214    mDisconnectPending = false;
215}
216
217status_t LiveSession::fetchFile(const char *url, sp<ABuffer> *out) {
218    *out = NULL;
219
220    sp<DataSource> source;
221
222    if (!strncasecmp(url, "file://", 7)) {
223        source = new FileSource(url + 7);
224    } else if (strncasecmp(url, "http://", 7)
225            && strncasecmp(url, "https://", 8)) {
226        return ERROR_UNSUPPORTED;
227    } else {
228        {
229            Mutex::Autolock autoLock(mLock);
230
231            if (mDisconnectPending) {
232                return ERROR_IO;
233            }
234        }
235
236        status_t err = mHTTPDataSource->connect(
237                url, mExtraHeaders.isEmpty() ? NULL : &mExtraHeaders);
238
239        if (err != OK) {
240            return err;
241        }
242
243        source = mHTTPDataSource;
244    }
245
246    off64_t size;
247    status_t err = source->getSize(&size);
248
249    if (err != OK) {
250        size = 65536;
251    }
252
253    sp<ABuffer> buffer = new ABuffer(size);
254    buffer->setRange(0, 0);
255
256    for (;;) {
257        size_t bufferRemaining = buffer->capacity() - buffer->size();
258
259        if (bufferRemaining == 0) {
260            bufferRemaining = 32768;
261
262            LOGV("increasing download buffer to %d bytes",
263                 buffer->size() + bufferRemaining);
264
265            sp<ABuffer> copy = new ABuffer(buffer->size() + bufferRemaining);
266            memcpy(copy->data(), buffer->data(), buffer->size());
267            copy->setRange(0, buffer->size());
268
269            buffer = copy;
270        }
271
272        ssize_t n = source->readAt(
273                buffer->size(), buffer->data() + buffer->size(),
274                bufferRemaining);
275
276        if (n < 0) {
277            return n;
278        }
279
280        if (n == 0) {
281            break;
282        }
283
284        buffer->setRange(0, buffer->size() + (size_t)n);
285    }
286
287    *out = buffer;
288
289    return OK;
290}
291
292sp<M3UParser> LiveSession::fetchPlaylist(const char *url) {
293    sp<ABuffer> buffer;
294    status_t err = fetchFile(url, &buffer);
295
296    if (err != OK) {
297        return NULL;
298    }
299
300    sp<M3UParser> playlist =
301        new M3UParser(url, buffer->data(), buffer->size());
302
303    if (playlist->initCheck() != OK) {
304        LOGE("failed to parse .m3u8 playlist");
305
306        return NULL;
307    }
308
309    return playlist;
310}
311
312static double uniformRand() {
313    return (double)rand() / RAND_MAX;
314}
315
316size_t LiveSession::getBandwidthIndex() {
317    if (mBandwidthItems.size() == 0) {
318        return 0;
319    }
320
321#if 1
322    int32_t bandwidthBps;
323    if (mHTTPDataSource != NULL
324            && mHTTPDataSource->estimateBandwidth(&bandwidthBps)) {
325        LOGV("bandwidth estimated at %.2f kbps", bandwidthBps / 1024.0f);
326    } else {
327        LOGV("no bandwidth estimate.");
328        return 0;  // Pick the lowest bandwidth stream by default.
329    }
330
331    char value[PROPERTY_VALUE_MAX];
332    if (property_get("media.httplive.max-bw", value, NULL)) {
333        char *end;
334        long maxBw = strtoul(value, &end, 10);
335        if (end > value && *end == '\0') {
336            if (maxBw > 0 && bandwidthBps > maxBw) {
337                LOGV("bandwidth capped to %ld bps", maxBw);
338                bandwidthBps = maxBw;
339            }
340        }
341    }
342
343    // Consider only 80% of the available bandwidth usable.
344    bandwidthBps = (bandwidthBps * 8) / 10;
345
346    // Pick the highest bandwidth stream below or equal to estimated bandwidth.
347
348    size_t index = mBandwidthItems.size() - 1;
349    while (index > 0 && mBandwidthItems.itemAt(index).mBandwidth
350                            > (size_t)bandwidthBps) {
351        --index;
352    }
353#elif 0
354    // Change bandwidth at random()
355    size_t index = uniformRand() * mBandwidthItems.size();
356#elif 0
357    // There's a 50% chance to stay on the current bandwidth and
358    // a 50% chance to switch to the next higher bandwidth (wrapping around
359    // to lowest)
360    const size_t kMinIndex = 0;
361
362    size_t index;
363    if (mPrevBandwidthIndex < 0) {
364        index = kMinIndex;
365    } else if (uniformRand() < 0.5) {
366        index = (size_t)mPrevBandwidthIndex;
367    } else {
368        index = mPrevBandwidthIndex + 1;
369        if (index == mBandwidthItems.size()) {
370            index = kMinIndex;
371        }
372    }
373#elif 0
374    // Pick the highest bandwidth stream below or equal to 1.2 Mbit/sec
375
376    size_t index = mBandwidthItems.size() - 1;
377    while (index > 0 && mBandwidthItems.itemAt(index).mBandwidth > 1200000) {
378        --index;
379    }
380#else
381    size_t index = mBandwidthItems.size() - 1;  // Highest bandwidth stream
382#endif
383
384    return index;
385}
386
387void LiveSession::onDownloadNext() {
388    size_t bandwidthIndex = getBandwidthIndex();
389
390rinse_repeat:
391    int64_t nowUs = ALooper::GetNowUs();
392
393    if (mLastPlaylistFetchTimeUs < 0
394            || (ssize_t)bandwidthIndex != mPrevBandwidthIndex
395            || (!mPlaylist->isComplete()
396                && mLastPlaylistFetchTimeUs + kMaxPlaylistAgeUs <= nowUs)) {
397        AString url;
398        if (mBandwidthItems.size() > 0) {
399            url = mBandwidthItems.editItemAt(bandwidthIndex).mURI;
400        } else {
401            url = mMasterURL;
402        }
403
404        bool firstTime = (mPlaylist == NULL);
405
406        mPlaylist = fetchPlaylist(url.c_str());
407        if (mPlaylist == NULL) {
408            LOGE("failed to load playlist at url '%s'", url.c_str());
409            mDataSource->queueEOS(ERROR_IO);
410            return;
411        }
412
413        if (firstTime) {
414            Mutex::Autolock autoLock(mLock);
415
416            if (!mPlaylist->isComplete()) {
417                mDurationUs = -1;
418            } else {
419                mDurationUs = 0;
420                for (size_t i = 0; i < mPlaylist->size(); ++i) {
421                    sp<AMessage> itemMeta;
422                    CHECK(mPlaylist->itemAt(
423                                i, NULL /* uri */, &itemMeta));
424
425                    int64_t itemDurationUs;
426                    CHECK(itemMeta->findInt64("durationUs", &itemDurationUs));
427
428                    mDurationUs += itemDurationUs;
429                }
430            }
431        }
432
433        mLastPlaylistFetchTimeUs = ALooper::GetNowUs();
434    }
435
436    int32_t firstSeqNumberInPlaylist;
437    if (mPlaylist->meta() == NULL || !mPlaylist->meta()->findInt32(
438                "media-sequence", &firstSeqNumberInPlaylist)) {
439        firstSeqNumberInPlaylist = 0;
440    }
441
442    bool explicitDiscontinuity = false;
443    bool bandwidthChanged = false;
444
445    if (mSeekTimeUs >= 0) {
446        if (mPlaylist->isComplete()) {
447            size_t index = 0;
448            int64_t segmentStartUs = 0;
449            while (index < mPlaylist->size()) {
450                sp<AMessage> itemMeta;
451                CHECK(mPlaylist->itemAt(
452                            index, NULL /* uri */, &itemMeta));
453
454                int64_t itemDurationUs;
455                CHECK(itemMeta->findInt64("durationUs", &itemDurationUs));
456
457                if (mSeekTimeUs < segmentStartUs + itemDurationUs) {
458                    break;
459                }
460
461                segmentStartUs += itemDurationUs;
462                ++index;
463            }
464
465            if (index < mPlaylist->size()) {
466                int32_t newSeqNumber = firstSeqNumberInPlaylist + index;
467
468                if (newSeqNumber != mSeqNumber) {
469                    LOGI("seeking to seq no %d", newSeqNumber);
470
471                    mSeqNumber = newSeqNumber;
472
473                    mDataSource->reset();
474
475                    // reseting the data source will have had the
476                    // side effect of discarding any previously queued
477                    // bandwidth change discontinuity.
478                    // Therefore we'll need to treat these explicit
479                    // discontinuities as involving a bandwidth change
480                    // even if they aren't directly.
481                    explicitDiscontinuity = true;
482                    bandwidthChanged = true;
483                }
484            }
485        }
486
487        mSeekTimeUs = -1;
488
489        Mutex::Autolock autoLock(mLock);
490        mSeekDone = true;
491        mCondition.broadcast();
492    }
493
494    if (mSeqNumber < 0) {
495        if (mPlaylist->isComplete()) {
496            mSeqNumber = firstSeqNumberInPlaylist;
497        } else {
498            mSeqNumber = firstSeqNumberInPlaylist + mPlaylist->size() / 2;
499        }
500    }
501
502    int32_t lastSeqNumberInPlaylist =
503        firstSeqNumberInPlaylist + (int32_t)mPlaylist->size() - 1;
504
505    if (mSeqNumber < firstSeqNumberInPlaylist
506            || mSeqNumber > lastSeqNumberInPlaylist) {
507        if (mPrevBandwidthIndex != (ssize_t)bandwidthIndex) {
508            // Go back to the previous bandwidth.
509
510            LOGI("new bandwidth does not have the sequence number "
511                 "we're looking for, switching back to previous bandwidth");
512
513            mLastPlaylistFetchTimeUs = -1;
514            bandwidthIndex = mPrevBandwidthIndex;
515            goto rinse_repeat;
516        }
517
518        if (!mPlaylist->isComplete()
519                && mSeqNumber > lastSeqNumberInPlaylist
520                && mNumRetries < kMaxNumRetries) {
521            ++mNumRetries;
522
523            mLastPlaylistFetchTimeUs = -1;
524            postMonitorQueue(3000000ll);
525            return;
526        }
527
528        LOGE("Cannot find sequence number %d in playlist "
529             "(contains %d - %d)",
530             mSeqNumber, firstSeqNumberInPlaylist,
531             firstSeqNumberInPlaylist + mPlaylist->size() - 1);
532
533        mDataSource->queueEOS(ERROR_END_OF_STREAM);
534        return;
535    }
536
537    mNumRetries = 0;
538
539    AString uri;
540    sp<AMessage> itemMeta;
541    CHECK(mPlaylist->itemAt(
542                mSeqNumber - firstSeqNumberInPlaylist,
543                &uri,
544                &itemMeta));
545
546    int32_t val;
547    if (itemMeta->findInt32("discontinuity", &val) && val != 0) {
548        explicitDiscontinuity = true;
549    }
550
551    sp<ABuffer> buffer;
552    status_t err = fetchFile(uri.c_str(), &buffer);
553    if (err != OK) {
554        LOGE("failed to fetch .ts segment at url '%s'", uri.c_str());
555        mDataSource->queueEOS(err);
556        return;
557    }
558
559    CHECK(buffer != NULL);
560
561    err = decryptBuffer(mSeqNumber - firstSeqNumberInPlaylist, buffer);
562
563    if (err != OK) {
564        LOGE("decryptBuffer failed w/ error %d", err);
565
566        mDataSource->queueEOS(err);
567        return;
568    }
569
570    if (buffer->size() == 0 || buffer->data()[0] != 0x47) {
571        // Not a transport stream???
572
573        LOGE("This doesn't look like a transport stream...");
574
575        mBandwidthItems.removeAt(bandwidthIndex);
576
577        if (mBandwidthItems.isEmpty()) {
578            mDataSource->queueEOS(ERROR_UNSUPPORTED);
579            return;
580        }
581
582        LOGI("Retrying with a different bandwidth stream.");
583
584        mLastPlaylistFetchTimeUs = -1;
585        bandwidthIndex = getBandwidthIndex();
586        mPrevBandwidthIndex = bandwidthIndex;
587        mSeqNumber = -1;
588
589        goto rinse_repeat;
590    }
591
592    if ((size_t)mPrevBandwidthIndex != bandwidthIndex) {
593        bandwidthChanged = true;
594    }
595
596    if (mPrevBandwidthIndex < 0) {
597        // Don't signal a bandwidth change at the very beginning of
598        // playback.
599        bandwidthChanged = false;
600    }
601
602    if (explicitDiscontinuity || bandwidthChanged) {
603        // Signal discontinuity.
604
605        LOGI("queueing discontinuity (explicit=%d, bandwidthChanged=%d)",
606             explicitDiscontinuity, bandwidthChanged);
607
608        sp<ABuffer> tmp = new ABuffer(188);
609        memset(tmp->data(), 0, tmp->size());
610        tmp->data()[1] = bandwidthChanged;
611
612        mDataSource->queueBuffer(tmp);
613    }
614
615    mDataSource->queueBuffer(buffer);
616
617    mPrevBandwidthIndex = bandwidthIndex;
618    ++mSeqNumber;
619
620    postMonitorQueue();
621}
622
623void LiveSession::onMonitorQueue() {
624    if (mSeekTimeUs >= 0
625            || mDataSource->countQueuedBuffers() < kMaxNumQueuedFragments) {
626        onDownloadNext();
627    } else {
628        postMonitorQueue(1000000ll);
629    }
630}
631
632status_t LiveSession::decryptBuffer(
633        size_t playlistIndex, const sp<ABuffer> &buffer) {
634    sp<AMessage> itemMeta;
635    bool found = false;
636    AString method;
637
638    for (ssize_t i = playlistIndex; i >= 0; --i) {
639        AString uri;
640        CHECK(mPlaylist->itemAt(i, &uri, &itemMeta));
641
642        if (itemMeta->findString("cipher-method", &method)) {
643            found = true;
644            break;
645        }
646    }
647
648    if (!found) {
649        method = "NONE";
650    }
651
652    if (method == "NONE") {
653        return OK;
654    } else if (!(method == "AES-128")) {
655        LOGE("Unsupported cipher method '%s'", method.c_str());
656        return ERROR_UNSUPPORTED;
657    }
658
659    AString keyURI;
660    if (!itemMeta->findString("cipher-uri", &keyURI)) {
661        LOGE("Missing key uri");
662        return ERROR_MALFORMED;
663    }
664
665    ssize_t index = mAESKeyForURI.indexOfKey(keyURI);
666
667    sp<ABuffer> key;
668    if (index >= 0) {
669        key = mAESKeyForURI.valueAt(index);
670    } else {
671        key = new ABuffer(16);
672
673        sp<HTTPBase> keySource =
674              HTTPBase::Create(
675                  (mFlags & kFlagIncognito)
676                    ? HTTPBase::kFlagIncognito
677                    : 0);
678
679        if (mUIDValid) {
680            keySource->setUID(mUID);
681        }
682
683        status_t err = keySource->connect(keyURI.c_str());
684
685        if (err == OK) {
686            size_t offset = 0;
687            while (offset < 16) {
688                ssize_t n = keySource->readAt(
689                        offset, key->data() + offset, 16 - offset);
690                if (n <= 0) {
691                    err = ERROR_IO;
692                    break;
693                }
694
695                offset += n;
696            }
697        }
698
699        if (err != OK) {
700            LOGE("failed to fetch cipher key from '%s'.", keyURI.c_str());
701            return ERROR_IO;
702        }
703
704        mAESKeyForURI.add(keyURI, key);
705    }
706
707    AES_KEY aes_key;
708    if (AES_set_decrypt_key(key->data(), 128, &aes_key) != 0) {
709        LOGE("failed to set AES decryption key.");
710        return UNKNOWN_ERROR;
711    }
712
713    unsigned char aes_ivec[16];
714
715    AString iv;
716    if (itemMeta->findString("cipher-iv", &iv)) {
717        if ((!iv.startsWith("0x") && !iv.startsWith("0X"))
718                || iv.size() != 16 * 2 + 2) {
719            LOGE("malformed cipher IV '%s'.", iv.c_str());
720            return ERROR_MALFORMED;
721        }
722
723        memset(aes_ivec, 0, sizeof(aes_ivec));
724        for (size_t i = 0; i < 16; ++i) {
725            char c1 = tolower(iv.c_str()[2 + 2 * i]);
726            char c2 = tolower(iv.c_str()[3 + 2 * i]);
727            if (!isxdigit(c1) || !isxdigit(c2)) {
728                LOGE("malformed cipher IV '%s'.", iv.c_str());
729                return ERROR_MALFORMED;
730            }
731            uint8_t nibble1 = isdigit(c1) ? c1 - '0' : c1 - 'a' + 10;
732            uint8_t nibble2 = isdigit(c2) ? c2 - '0' : c2 - 'a' + 10;
733
734            aes_ivec[i] = nibble1 << 4 | nibble2;
735        }
736    } else {
737        memset(aes_ivec, 0, sizeof(aes_ivec));
738        aes_ivec[15] = mSeqNumber & 0xff;
739        aes_ivec[14] = (mSeqNumber >> 8) & 0xff;
740        aes_ivec[13] = (mSeqNumber >> 16) & 0xff;
741        aes_ivec[12] = (mSeqNumber >> 24) & 0xff;
742    }
743
744    AES_cbc_encrypt(
745            buffer->data(), buffer->data(), buffer->size(),
746            &aes_key, aes_ivec, AES_DECRYPT);
747
748    // hexdump(buffer->data(), buffer->size());
749
750    size_t n = buffer->size();
751    CHECK_GT(n, 0u);
752
753    size_t pad = buffer->data()[n - 1];
754
755    CHECK_GT(pad, 0u);
756    CHECK_LE(pad, 16u);
757    CHECK_GE((size_t)n, pad);
758    for (size_t i = 0; i < pad; ++i) {
759        CHECK_EQ((unsigned)buffer->data()[n - 1 - i], pad);
760    }
761
762    n -= pad;
763
764    buffer->setRange(buffer->offset(), n);
765
766    return OK;
767}
768
769void LiveSession::postMonitorQueue(int64_t delayUs) {
770    sp<AMessage> msg = new AMessage(kWhatMonitorQueue, id());
771    msg->setInt32("generation", ++mMonitorQueueGeneration);
772    msg->post(delayUs);
773}
774
775void LiveSession::onSeek(const sp<AMessage> &msg) {
776    int64_t timeUs;
777    CHECK(msg->findInt64("timeUs", &timeUs));
778
779    mSeekTimeUs = timeUs;
780    postMonitorQueue();
781}
782
783status_t LiveSession::getDuration(int64_t *durationUs) {
784    Mutex::Autolock autoLock(mLock);
785    *durationUs = mDurationUs;
786
787    return OK;
788}
789
790bool LiveSession::isSeekable() {
791    int64_t durationUs;
792    return getDuration(&durationUs) == OK && durationUs >= 0;
793}
794
795}  // namespace android
796
797