diff options
author | Youngsang Cho <youngsang@google.com> | 2016-05-09 18:52:12 -0700 |
---|---|---|
committer | Youngsang Cho <youngsang@google.com> | 2016-05-17 15:14:50 -0700 |
commit | 48dadb49248271b01997862e1335912a4f2e189f (patch) | |
tree | fb402e0e2bda1328fd9858b28a98e1c29563f038 /usbtuner | |
parent | 3a72b93e554bd22a5c64e71a6956d9604ce05108 (diff) | |
download | TV-48dadb49248271b01997862e1335912a4f2e189f.tar.gz |
DO NOT MERGE Sync to joey ub-tv-dev at e7fbaa585b1eb7afec05f05032d2e8d99fb595d4
Bug: 28469968
Change-Id: I74e368f5f58b433755932b806a90178e37bea7f9
Diffstat (limited to 'usbtuner')
53 files changed, 3084 insertions, 2638 deletions
diff --git a/usbtuner/Android.mk b/usbtuner/Android.mk index 37b65ed0..8e5669b2 100644 --- a/usbtuner/Android.mk +++ b/usbtuner/Android.mk @@ -19,9 +19,10 @@ LOCAL_MODULE_TAGS := optional # It's not required but keep it for a compatibility with the previous version. LOCAL_PRIVILEGED_MODULE := true LOCAL_SDK_VERSION := system_current +LOCAL_MIN_SDK_VERSION := 23 # M LOCAL_STATIC_JAVA_LIBRARIES := \ - lib-tv-exoplayer \ + lib-exoplayer \ usbtuner-tvinput LOCAL_RESOURCE_DIR := \ @@ -61,7 +62,7 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ android-support-v7-recyclerview \ android-support-v17-leanback \ icu4j-usbtuner \ - lib-tv-exoplayer \ + lib-exoplayer \ libprotobuf-java-nano \ tv-common @@ -97,17 +98,21 @@ LOCAL_SRC_FILES := \ LOCAL_SDK_VERSION := system_current include $(BUILD_STATIC_JAVA_LIBRARY) - ############################################################# # Pre-built dependency jars ############################################################# +# -------------------------------------------------------------- +# ExoPlayer library version 1.5.6 +# https://github.com/google/ExoPlayer/archive/r1.5.6.zip +# TODO: Add ExoPlayer source code to external/ android repository. + include $(CLEAR_VARS) LOCAL_MODULE_TAGS := optional LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES := \ - lib-tv-exoplayer:libs/tv-exoplayer.jar \ + lib-exoplayer:libs/exoplayer_1.5.6.jar include $(BUILD_MULTI_PREBUILT) diff --git a/usbtuner/AndroidManifest.xml b/usbtuner/AndroidManifest.xml index a763ed2d..200334e8 100644 --- a/usbtuner/AndroidManifest.xml +++ b/usbtuner/AndroidManifest.xml @@ -21,8 +21,8 @@ android:versionName="0.1" > <uses-sdk - android:minSdkVersion="21" - android:targetSdkVersion="21" /> + android:minSdkVersion="23" + android:targetSdkVersion="23" /> <uses-permission android:name="android.permission.DVB_DEVICE" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> diff --git a/usbtuner/icu/icu4j/main/classes/core/src/com/ibm/icu/text/UnicodeDecompressor.java b/usbtuner/icu/icu4j/main/classes/core/src/com/ibm/icu/text/UnicodeDecompressor.java index a9af2b32..e799ea14 100644 --- a/usbtuner/icu/icu4j/main/classes/core/src/com/ibm/icu/text/UnicodeDecompressor.java +++ b/usbtuner/icu/icu4j/main/classes/core/src/com/ibm/icu/text/UnicodeDecompressor.java @@ -1,6 +1,6 @@ /* ******************************************************************************* - * Copyright (C) 1996-2009, International Business Machines Corporation and * + * Copyright (C) 1996-2016, International Business Machines Corporation and * * others. All Rights Reserved. * ******************************************************************************* */ @@ -55,7 +55,7 @@ package com.ibm.icu.text; * // update the no. of chars written * totalCharsWritten += charsWritten; * -* } while(totalBytesDecompressed < len); +* } while(totalBytesDecompressed < len); * * myDecompressor.reset(); // reuse decompressor * </PRE> diff --git a/usbtuner/libs/exoplayer_1.5.6.jar b/usbtuner/libs/exoplayer_1.5.6.jar Binary files differnew file mode 100644 index 00000000..a0b311c9 --- /dev/null +++ b/usbtuner/libs/exoplayer_1.5.6.jar diff --git a/usbtuner/libs/tv-exoplayer.jar b/usbtuner/libs/tv-exoplayer.jar Binary files differdeleted file mode 100644 index a8ca7b2a..00000000 --- a/usbtuner/libs/tv-exoplayer.jar +++ /dev/null diff --git a/usbtuner/res/xml/ut_tvinputservice.xml b/usbtuner/res/xml/ut_tvinputservice.xml index 039d848f..54f56878 100644 --- a/usbtuner/res/xml/ut_tvinputservice.xml +++ b/usbtuner/res/xml/ut_tvinputservice.xml @@ -34,4 +34,6 @@ --> <tv-input xmlns:android="http://schemas.android.com/apk/res/android" - android:setupActivity="com.android.usbtuner.setup.TunerSetupActivity" /> + android:setupActivity="com.android.usbtuner.setup.TunerSetupActivity" + android:canRecord="true" + android:tunerCount="1" /> diff --git a/usbtuner/src/com/android/usbtuner/DvbDeviceAccessor.java b/usbtuner/src/com/android/usbtuner/DvbDeviceAccessor.java index 61185f59..b1cdcfe0 100644 --- a/usbtuner/src/com/android/usbtuner/DvbDeviceAccessor.java +++ b/usbtuner/src/com/android/usbtuner/DvbDeviceAccessor.java @@ -16,14 +16,23 @@ package com.android.usbtuner; +import android.annotation.TargetApi; +import android.content.ComponentName; import android.content.Context; +import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager; +import android.os.Build; import android.os.ParcelFileDescriptor; import android.support.annotation.IntDef; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.os.BuildCompat; import android.util.Log; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.common.feature.CommonFeatures; import com.android.tv.common.recording.RecordingCapability; +import com.android.usbtuner.tvinput.UsbTunerTvInputService; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -128,6 +137,22 @@ public class DvbDeviceAccessor { .build(); } + @Nullable + @TargetApi(Build.VERSION_CODES.N) + public TvInputInfo buildTvInputInfo(Context context) { + List<DvbDeviceInfoWrapper> deviceList = getDvbDeviceList(); + TvInputInfo.Builder builder = new TvInputInfo.Builder(context, new ComponentName(context, + UsbTunerTvInputService.class)); + if (deviceList.size() > 0) { + return builder.setCanRecord( + CommonFeatures.DVR.isEnabled(context) && BuildCompat.isAtLeastN()) + .setTunerCount(deviceList.size()) + .build(); + } else { + return null; + } + } + public static class DvbDeviceInfoWrapper implements Comparable<DvbDeviceInfoWrapper> { private static Method sGetAdapterIdMethod; private static Method sGetDeviceIdMethod; diff --git a/usbtuner/src/com/android/usbtuner/FileDataSource.java b/usbtuner/src/com/android/usbtuner/FileDataSource.java index 163d1048..af831e2f 100644 --- a/usbtuner/src/com/android/usbtuner/FileDataSource.java +++ b/usbtuner/src/com/android/usbtuner/FileDataSource.java @@ -48,6 +48,7 @@ public class FileDataSource extends MediaDataSource implements InputStreamSource private static final int CIRCULAR_BUFFER_SIZE = MIN_READ_UNIT * 4000; // ~ 8MB private static final int PADDING_SIZE = MIN_READ_UNIT * 1000; // ~2MB private static final int READ_TIMEOUT_MS = 10000; // 10 secs. + private static final int BUFFER_UNDERRUN_SLEEP_MS = 10; private static final String FILE_DIR = new File(Environment.getExternalStorageDirectory(), "Streams").getAbsolutePath(); @@ -137,6 +138,7 @@ public class FileDataSource extends MediaDataSource implements InputStreamSource mStreamingThread.join(); } } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } } @@ -262,6 +264,7 @@ public class FileDataSource extends MediaDataSource implements InputStreamSource mCircularBufferMonitor.wait(READ_TIMEOUT_MS); } catch (InterruptedException e) { // Wait again. + Thread.currentThread().interrupt(); } if (initialBytesFetched == mBytesFetched) { Log.w(TAG, "No data update for " + READ_TIMEOUT_MS + "ms. returning -1."); @@ -350,6 +353,7 @@ public class FileDataSource extends MediaDataSource implements InputStreamSource mCircularBufferMonitor.wait(); } catch (InterruptedException e) { // Wait again. + Thread.currentThread().interrupt(); } } if (!mStreaming) { @@ -359,6 +363,13 @@ public class FileDataSource extends MediaDataSource implements InputStreamSource int bytesWritten = mSource.read(dataBuffer); if (bytesWritten <= 0) { + try { + // When buffer is underrun, we sleep for short time to prevent + // unnecessary CPU draining. + sleep(BUFFER_UNDERRUN_SLEEP_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } continue; } diff --git a/usbtuner/src/com/android/usbtuner/TunerHal.java b/usbtuner/src/com/android/usbtuner/TunerHal.java index 102b60fd..fc04b2da 100644 --- a/usbtuner/src/com/android/usbtuner/TunerHal.java +++ b/usbtuner/src/com/android/usbtuner/TunerHal.java @@ -61,13 +61,19 @@ public abstract class TunerHal implements AutoCloseable { System.loadLibrary("tunertvinput_jni"); } - public static TunerHal getInstance(Context context) { + /** + * Creates a TunerHal instance. + * @param context context for creating the TunerHal instance + * @return the TunerHal instance + */ + public static TunerHal createInstance(Context context) { TunerHal tunerHal; if (TisConfiguration.isPackagedWithLiveChannels(context)) { tunerHal = new UsbTunerHal(context); } else { tunerHal = new InternalTunerHal(context); - } if (tunerHal.openFirstAvailable()) { + } + if (tunerHal.openFirstAvailable()) { return tunerHal; } return null; diff --git a/usbtuner/src/com/android/usbtuner/UsbTunerDataSource.java b/usbtuner/src/com/android/usbtuner/UsbTunerDataSource.java index 34558b99..0d139311 100644 --- a/usbtuner/src/com/android/usbtuner/UsbTunerDataSource.java +++ b/usbtuner/src/com/android/usbtuner/UsbTunerDataSource.java @@ -19,7 +19,6 @@ package com.android.usbtuner; import android.media.MediaDataSource; import android.util.Log; -import com.google.android.exoplayer.upstream.DataSource; import com.android.usbtuner.ChannelScanFileParser.ScanChannel; import com.android.usbtuner.data.Channel; import com.android.usbtuner.data.TunerChannel; @@ -32,8 +31,8 @@ import java.util.Locale; import java.util.concurrent.atomic.AtomicLong; /** - * A {@link DataSource} implementation which provides the mpeg2ts stream from the tuner device to - * demux. + * A {@link MediaDataSource} implementation which provides the mpeg2ts stream from the tuner device + * to {@link MediaExtractor}. */ public class UsbTunerDataSource extends MediaDataSource implements InputStreamSource { private static final String TAG = "UsbTunerDataSource"; @@ -43,6 +42,7 @@ public class UsbTunerDataSource extends MediaDataSource implements InputStreamSo private static final int CIRCULAR_BUFFER_SIZE = MIN_READ_UNIT * 20000; // ~ 30MB private static final int READ_TIMEOUT_MS = 5000; // 5 secs. + private static final int BUFFER_UNDERRUN_SLEEP_MS = 10; private static final int CACHE_KEY_VERSION = 1; @@ -138,6 +138,9 @@ public class UsbTunerDataSource extends MediaDataSource implements InputStreamSo mStreamingThread.join(); } } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + mTunerHal.stopTune(); } } @@ -154,7 +157,6 @@ public class UsbTunerDataSource extends MediaDataSource implements InputStreamSo } private class StreamingThread extends Thread { - @Override public void run() { // Buffers for streaming data from the tuner and the internal buffer. @@ -169,6 +171,13 @@ public class UsbTunerDataSource extends MediaDataSource implements InputStreamSo int bytesWritten = mTunerHal.readTsStream(dataBuffer, dataBuffer.length); if (bytesWritten <= 0) { + try { + // When buffer is underrun, we sleep for short time to prevent + // unnecessary CPU draining. + sleep(BUFFER_UNDERRUN_SLEEP_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } continue; } @@ -214,6 +223,7 @@ public class UsbTunerDataSource extends MediaDataSource implements InputStreamSo mCircularBufferMonitor.wait(READ_TIMEOUT_MS); } catch (InterruptedException e) { // Wait again. + Thread.currentThread().interrupt(); } if (initialBytesFetched == mBytesFetched) { Log.w(TAG, "No data update for " + READ_TIMEOUT_MS + "ms. returning -1."); @@ -260,7 +270,8 @@ public class UsbTunerDataSource extends MediaDataSource implements InputStreamSo @Override public void close() { - mTunerHal.stopTune(); + // Called from system MediaExtractor. All the resource should be closed + // in stopStream() already. } @Override diff --git a/usbtuner/src/com/android/usbtuner/UsbTunerPreferences.java b/usbtuner/src/com/android/usbtuner/UsbTunerPreferences.java index 2f4a4764..0394648d 100644 --- a/usbtuner/src/com/android/usbtuner/UsbTunerPreferences.java +++ b/usbtuner/src/com/android/usbtuner/UsbTunerPreferences.java @@ -21,14 +21,20 @@ import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.database.Cursor; +import android.os.AsyncTask; +import android.os.Bundle; +import com.android.tv.common.SoftPreconditions; import com.android.usbtuner.UsbTunerPreferenceProvider.Preferences; import com.android.usbtuner.util.TisConfiguration; /** * A helper class for the USB tuner preferences. */ +// TODO: Change this class to run on the worker thread. public class UsbTunerPreferences { + private static final String TAG = "UsbTunerPreferences"; + private static final String PREFS_KEY_CHANNEL_DATA_VERSION = "channel_data_version"; private static final String PREFS_KEY_SCANNED_CHANNEL_COUNT = "scanned_channel_count"; private static final String PREFS_KEY_SCAN_DONE = "scan_done"; @@ -36,6 +42,8 @@ public class UsbTunerPreferences { private static final String SHARED_PREFS_NAME = "com.android.usbtuner.preferences"; + private static final Bundle PREFERENCE_VALUES = new Bundle(); + private static boolean useContentProvider(Context context) { // If TIS is a part of LC, it should use ContentProvider to resolve multiple process access. return TisConfiguration.isPackagedWithLiveChannels(context); @@ -132,11 +140,16 @@ public class UsbTunerPreferences { if (cursor != null && cursor.moveToFirst()) { return cursor.getString(0); } + } catch (Exception e) { + SoftPreconditions.warn(TAG, "getPreference", "Error querying preference values", e); } return null; } private static int getPreferenceInt(Context context, String key) { + if (PREFERENCE_VALUES.containsKey(key)) { + return PREFERENCE_VALUES.getInt(key); + } try { return Integer.parseInt(getPreference(context, key)); } catch (NumberFormatException e) { @@ -145,22 +158,38 @@ public class UsbTunerPreferences { } private static boolean getPreferenceBoolean(Context context, String key) { + if (PREFERENCE_VALUES.containsKey(key)) { + return PREFERENCE_VALUES.getBoolean(key); + } return Boolean.valueOf(getPreference(context, key)); } - private static void setPreference(Context context, String key, String value) { - ContentResolver resolver = context.getContentResolver(); - ContentValues values = new ContentValues(); - values.put(Preferences.COLUMN_KEY, key); - values.put(Preferences.COLUMN_VALUE, value); - resolver.insert(Preferences.CONTENT_URI, values); + private static void setPreference(final Context context, final String key, final String value) { + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + ContentResolver resolver = context.getContentResolver(); + ContentValues values = new ContentValues(); + values.put(Preferences.COLUMN_KEY, key); + values.put(Preferences.COLUMN_VALUE, value); + try { + resolver.insert(Preferences.CONTENT_URI, values); + } catch (Exception e) { + SoftPreconditions.warn(TAG, "setPreference", "Error writing preference values", + e); + } + return null; + } + }.execute(); } private static void setPreference(Context context, String key, int value) { + PREFERENCE_VALUES.putInt(key, value); setPreference(context, key, Integer.toString(value)); } private static void setPreference(Context context, String key, boolean value) { + PREFERENCE_VALUES.putBoolean(key, value); setPreference(context, key, Boolean.toString(value)); } } diff --git a/usbtuner/src/com/android/usbtuner/UsbTunerTsScannerSource.java b/usbtuner/src/com/android/usbtuner/UsbTunerTsScannerSource.java index afb1ee31..fd7f8838 100644 --- a/usbtuner/src/com/android/usbtuner/UsbTunerTsScannerSource.java +++ b/usbtuner/src/com/android/usbtuner/UsbTunerTsScannerSource.java @@ -49,7 +49,7 @@ public class UsbTunerTsScannerSource implements InputStreamSource { private final AtomicLong mBytesFetched = new AtomicLong(); public UsbTunerTsScannerSource(Context context, EventListener eventListener) { - mTunerHal = TunerHal.getInstance(context); + mTunerHal = TunerHal.createInstance(context); if (mTunerHal == null) { throw new RuntimeException("Failed to open a DVB device"); } diff --git a/usbtuner/src/com/android/usbtuner/cc/CaptionWindowLayout.java b/usbtuner/src/com/android/usbtuner/cc/CaptionWindowLayout.java index 7446b014..26b92493 100644 --- a/usbtuner/src/com/android/usbtuner/cc/CaptionWindowLayout.java +++ b/usbtuner/src/com/android/usbtuner/cc/CaptionWindowLayout.java @@ -91,6 +91,7 @@ public class CaptionWindowLayout extends RelativeLayout implements View.OnLayout private CaptionLayout mCaptionLayout; private CaptionStyleCompat mCaptionStyleCompat; + // TODO: Replace SubtitleView to {@link com.google.android.exoplayer.text.SubtitleLayout}. private final SubtitleView mSubtitleView; private int mRowLimit = 0; private final SpannableStringBuilder mBuilder = new SpannableStringBuilder(); diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/CachedSampleSourceExtractor.java b/usbtuner/src/com/android/usbtuner/exoplayer/CachedSampleSourceExtractor.java deleted file mode 100644 index 654a9f92..00000000 --- a/usbtuner/src/com/android/usbtuner/exoplayer/CachedSampleSourceExtractor.java +++ /dev/null @@ -1,331 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.usbtuner.exoplayer; - -import android.media.MediaCodec; -import android.media.MediaDataSource; -import android.os.ConditionVariable; -import android.os.SystemClock; -import android.util.Log; - -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.SampleSource; -import com.android.usbtuner.tvinput.PlaybackCacheListener; - -import junit.framework.Assert; - -import java.io.IOException; -import java.util.Locale; -import java.util.Random; -import java.util.concurrent.TimeUnit; - -/** - * Extracts samples from {@link MediaDataSource} and stores them on the disk, which enables - * trickplay. - */ -public class CachedSampleSourceExtractor extends BaseSampleSourceExtractor implements - CacheManager.EvictListener { - private static final String TAG = "CachedSampleSourceExt"; - private static final boolean DEBUG = false; - - public static final long CHUNK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500); - - private static final long LIVE_THRESHOLD_US = TimeUnit.SECONDS.toMicros(1); - private static final long CACHE_WRITE_TIMEOUT_MS = 10 * 1000; // 10 seconds - - private final CacheManager mCacheManager; - private final String mId; - - private final PlaybackCacheListener mCacheListener; - private long[] mCacheEndPositionUs; - private SampleCache[] mSampleCaches; - private CachedSampleQueue[] mPlayingSampleQueues; - private final SamplePool mSamplePool = new SamplePool(); - private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US; - private long mCurrentPlaybackPositionUs = 0; - - private class CachedSampleQueue extends SampleQueue { - private SampleCache mCache = null; - - public CachedSampleQueue(SamplePool samplePool) { - super(samplePool); - } - - public void setSource(SampleCache newCache) { - for (SampleCache cache = mCache; cache != null; cache = cache.getNext()) { - cache.clear(); - cache.close(); - } - mCache = newCache; - for (SampleCache cache = mCache; cache != null; cache = cache.getNext()) { - cache.resetRead(); - } - } - - public boolean maybeReadSample() { - if (isDurationGreaterThan(CHUNK_DURATION_US)) { - return false; - } - SampleHolder sample = mCache.maybeReadSample(); - if (sample == null) { - if (!mCache.canReadMore() && mCache.getNext() != null) { - mCache.clear(); - mCache.close(); - mCache = mCache.getNext(); - mCache.resetRead(); - return maybeReadSample(); - } else { - return false; - } - } else { - queueSample(sample); - return true; - } - } - - public int dequeueSample(SampleHolder sample) { - maybeReadSample(); - return super.dequeueSample(sample); - } - - @Override - public void clear() { - super.clear(); - for (SampleCache cache = mCache; cache != null; cache = cache.getNext()) { - cache.clear(); - cache.close(); - } - mCache = null; - } - - public long getSourceStartPositionUs() { - return mCache == null ? -1 : mCache.getStartPositionUs(); - } - } - - public CachedSampleSourceExtractor(MediaDataSource source, CacheManager cacheManager, - PlaybackCacheListener cacheListener) { - super(source); - mCacheManager = cacheManager; - mCacheListener = cacheListener; - mId = Long.toHexString(new Random().nextLong()); - cacheListener.onCacheStateChanged(true); // Enable trickplay - } - - @Override - public void queueSample(int index, SampleHolder sample, ConditionVariable conditionVariable) - throws IOException { - long writeStartTimeNs = SystemClock.elapsedRealtimeNanos(); - synchronized (this) { - SampleCache cache = mSampleCaches[index]; - if ((sample.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { - if (sample.timeUs >= mCacheEndPositionUs[index]) { - try { - SampleCache nextCache = mCacheManager.createNewWriteFile( - getTrackId(index), mCacheEndPositionUs[index], mSamplePool); - cache.finishWrite(nextCache); - mSampleCaches[index] = cache = nextCache; - mCacheEndPositionUs[index] = - ((sample.timeUs / CHUNK_DURATION_US) + 1) * CHUNK_DURATION_US; - } catch (IOException e) { - cache.finishWrite(null); - throw e; - } - } - } - cache.writeSample(sample, conditionVariable); - } - if (!conditionVariable.block(CACHE_WRITE_TIMEOUT_MS)) { - Log.e(TAG, "Error: Serious delay on writing cache"); - conditionVariable.block(); - } - - // Check if the storage has enough bandwidth for trickplay. Otherwise we disable it - // and notify the slowness through the playback cache listener. - mCacheManager.addWriteStat(sample.size, - SystemClock.elapsedRealtimeNanos() - writeStartTimeNs); - if (mCacheManager.isWriteSlow()) { - Log.w(TAG, "Disk is too slow for trickplay. Disable trickplay."); - mCacheManager.disable(); - mCacheListener.onDiskTooSlow(); - } - } - - private String getTrackId(int index) { - return String.format(Locale.ENGLISH, "%s_%x", mId, index); - } - - @Override - public void initOnPrepareLocked(int trackCount) throws IOException { - mSampleCaches = new SampleCache[trackCount]; - mPlayingSampleQueues = new CachedSampleQueue[trackCount]; - mCacheEndPositionUs = new long[trackCount]; - for (int i = 0; i < trackCount; i++) { - mSampleCaches[i] = mCacheManager.createNewWriteFile(getTrackId(i), 0, mSamplePool); - mPlayingSampleQueues[i] = null; - mCacheEndPositionUs[i] = CHUNK_DURATION_US; - } - } - - @Override - public void selectTrack(int index) { - synchronized (this) { - if (mPlayingSampleQueues[index] == null) { - String trackId = getTrackId(index); - mPlayingSampleQueues[index] = new CachedSampleQueue(mSamplePool); - mCacheManager.registerEvictListener(trackId, this); - seekIndividualTrackLocked(index, mCurrentPlaybackPositionUs, - isLiveLocked(mCurrentPlaybackPositionUs)); - mPlayingSampleQueues[index].maybeReadSample(); - } - } - } - - @Override - public void deselectTrack(int index) { - synchronized (this) { - if (mPlayingSampleQueues[index] != null) { - mPlayingSampleQueues[index].clear(); - mPlayingSampleQueues[index] = null; - mCacheManager.unregisterEvictListener(getTrackId(index)); - } - } - } - - @Override - public long getBufferedPositionUs() { - synchronized (this) { - Long result = null; - for (CachedSampleQueue queue : mPlayingSampleQueues) { - if (queue == null) { - continue; - } - Long bufferedPositionUs = queue.getEndPositionUs(); - if (bufferedPositionUs == null) { - continue; - } - if (result == null || result > bufferedPositionUs) { - result = bufferedPositionUs; - } - } - if (result == null) { - return mLastBufferedPositionUs; - } else { - return (mLastBufferedPositionUs = result); - } - } - } - - @Override - public void seekTo(long positionUs) { - synchronized (this) { - boolean isLive = isLiveLocked(positionUs); - - // Seek video track first - for (int i = 0; i < mPlayingSampleQueues.length; ++i) { - CachedSampleQueue queue = mPlayingSampleQueues[i]; - if (queue == null) { - continue; - } - seekIndividualTrackLocked(i, positionUs, isLive); - if (DEBUG) { - Log.d(TAG, "start time = " + queue.getSourceStartPositionUs()); - } - } - mLastBufferedPositionUs = positionUs; - } - } - - private boolean isLiveLocked(long positionUs) { - Long livePositionUs = null; - for (SampleCache cache : mSampleCaches) { - if (livePositionUs == null || livePositionUs < cache.getEndPositionUs()) { - livePositionUs = cache.getEndPositionUs(); - } - } - return (livePositionUs == null - || Math.abs(livePositionUs - positionUs) < LIVE_THRESHOLD_US); - } - - private void seekIndividualTrackLocked(int index, long positionUs, boolean isLive) { - CachedSampleQueue queue = mPlayingSampleQueues[index]; - if (queue == null) { - return; - } - queue.clear(); - if (isLive) { - queue.setSource(mSampleCaches[index]); - } else { - queue.setSource(mCacheManager.getReadFile(getTrackId(index), positionUs)); - } - queue.maybeReadSample(); - } - - @Override - public int readSample(int track, SampleHolder sampleHolder) { - synchronized (this) { - CachedSampleQueue queue = mPlayingSampleQueues[track]; - Assert.assertNotNull(queue); - queue.maybeReadSample(); - int result = queue.dequeueSample(sampleHolder); - if (result != SampleSource.SAMPLE_READ && getEos()) { - return SampleSource.END_OF_STREAM; - } - return result; - } - } - - @Override - public void cleanUpImpl() { - if (mSampleCaches == null) { - return; - } - for (int i = 0; i < mSampleCaches.length; ++i) { - mSampleCaches[i].finishWrite(null); - mCacheManager.unregisterEvictListener(getTrackId(i)); - } - for (int i = 0; i < mSampleCaches.length; ++i) { - mCacheManager.clearTrack(getTrackId(i)); - } - } - - @Override - public boolean continueBuffering(long positionUs) { - synchronized (this) { - boolean hasSamples = true; - mCurrentPlaybackPositionUs = positionUs; - for (CachedSampleQueue queue : mPlayingSampleQueues) { - if (queue == null) { - continue; - } - queue.maybeReadSample(); - if (queue.isEmpty()) { - hasSamples = false; - } - } - return hasSamples; - } - } - - // CacheEvictListener - @Override - public void onCacheEvicted(String id, long createdTimeMs) { - mCacheListener.onCacheStartTimeChanged( - createdTimeMs + TimeUnit.MICROSECONDS.toMillis(CHUNK_DURATION_US)); - } -} diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/Cea708TextTrackRenderer.java b/usbtuner/src/com/android/usbtuner/exoplayer/Cea708TextTrackRenderer.java index 3fdfb34a..a391db50 100644 --- a/usbtuner/src/com/android/usbtuner/exoplayer/Cea708TextTrackRenderer.java +++ b/usbtuner/src/com/android/usbtuner/exoplayer/Cea708TextTrackRenderer.java @@ -19,10 +19,13 @@ package com.android.usbtuner.exoplayer; import android.util.Log; import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.MediaClock; +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; -import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.util.Assertions; import com.android.usbtuner.cc.Cea708Parser; import com.android.usbtuner.cc.Cea708Parser.OnCea708ParserListener; import com.android.usbtuner.data.Cea708Data.CaptionEvent; @@ -41,10 +44,10 @@ public class Cea708TextTrackRenderer extends TrackRenderer implements OnCea708Pa // According to CEA-708B, the maximum value of closed caption bandwidth is 9600bps. private static final int DEFAULT_INPUT_BUFFER_SIZE = 9600 / 8; - private SampleSource mSource; + private SampleSource.SampleSourceReader mSource; private SampleHolder mSampleHolder; + private MediaFormatHolder mFormatHolder; private int mServiceNumber; - private boolean mSourceStateReady; private boolean mInputStreamEnded; private long mCurrentPositionUs; private long mPresentationTimeUs; @@ -58,14 +61,16 @@ public class Cea708TextTrackRenderer extends TrackRenderer implements OnCea708Pa } public Cea708TextTrackRenderer(SampleSource source) { - mSource = source; + mSource = source.register(); + mTrackIndex = -1; mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT); - mSampleHolder.replaceBuffer(DEFAULT_INPUT_BUFFER_SIZE); + mSampleHolder.ensureSpaceForWrite(DEFAULT_INPUT_BUFFER_SIZE); + mFormatHolder = new MediaFormatHolder(); } @Override - protected boolean isTimeSource() { - return false; + protected MediaClock getMediaClock() { + return null; } private boolean handlesMimeType(String mimeType) { @@ -73,31 +78,28 @@ public class Cea708TextTrackRenderer extends TrackRenderer implements OnCea708Pa } @Override - protected int doPrepare(long positionUs) throws ExoPlaybackException { - try { - boolean sourcePrepared = mSource.prepare(positionUs); - if (!sourcePrepared) { - return TrackRenderer.STATE_UNPREPARED; - } - } catch (IOException e) { - throw new ExoPlaybackException(e); + protected boolean doPrepare(long positionUs) throws ExoPlaybackException { + boolean sourcePrepared = mSource.prepare(positionUs); + if (!sourcePrepared) { + return false; } int trackCount = mSource.getTrackCount(); for (int i = 0; i < trackCount; ++i) { - TrackInfo trackInfo = mSource.getTrackInfo(i); - if (handlesMimeType(trackInfo.mimeType)) { + MediaFormat trackFormat = mSource.getFormat(i); + if (handlesMimeType(trackFormat.mimeType)) { mTrackIndex = i; clearDecodeState(); - return TrackRenderer.STATE_PREPARED; + return true; } } - return TrackRenderer.STATE_IGNORE; + // TODO: Check this case. (Source do not have the proper mime type.) + return true; } @Override - protected void onEnabled(long positionUs, boolean joining) { + protected void onEnabled(int track, long positionUs, boolean joining) { + Assertions.checkArgument(mTrackIndex != -1 && track == 0); mSource.enable(mTrackIndex, positionUs); - mSourceStateReady = false; mInputStreamEnded = false; mPresentationTimeUs = positionUs; mCurrentPositionUs = Long.MIN_VALUE; @@ -121,19 +123,34 @@ public class Cea708TextTrackRenderer extends TrackRenderer implements OnCea708Pa @Override protected boolean isReady() { - return mSourceStateReady; + // Since this track will be fed by {@link VideoTrackRenderer}, + // it is not required to control transition between ready state and buffering state. + return true; + } + + @Override + protected int getTrackCount() { + return mTrackIndex < 0 ? 0 : 1; + } + + @Override + protected MediaFormat getFormat(int track) { + Assertions.checkArgument(mTrackIndex != -1 && track == 0); + return mSource.getFormat(mTrackIndex); + } + + @Override + protected void maybeThrowError() throws ExoPlaybackException { + try { + mSource.maybeThrowError(); + } catch (IOException e) { + throw new ExoPlaybackException(e); + } } @Override protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { try { - boolean continueBuffering = mSource.continueBuffering(positionUs); - if (mSourceStateReady != continueBuffering) { - mSourceStateReady = continueBuffering; - if (DEBUG) { - Log.d(TAG, "mSourceStateReady: " + mSourceStateReady); - } - } mPresentationTimeUs = positionUs; if (!mInputStreamEnded) { processOutput(); @@ -155,22 +172,25 @@ public class Cea708TextTrackRenderer extends TrackRenderer implements OnCea708Pa if (mInputStreamEnded) { return false; } + long discontinuity = mSource.readDiscontinuity(mTrackIndex); + if (discontinuity != SampleSource.NO_DISCONTINUITY) { + if (DEBUG) { + Log.d(TAG, "Read discontinuity happened"); + } + + // TODO: handle input discontinuity for trickplay. + clearDecodeState(); + mPresentationTimeUs = discontinuity; + return false; + } mSampleHolder.data.clear(); mSampleHolder.size = 0; - int result = mSource.readData(mTrackIndex, mPresentationTimeUs, null, mSampleHolder, false); + int result = mSource.readData(mTrackIndex, mPresentationTimeUs, + mFormatHolder, mSampleHolder); switch (result) { case SampleSource.NOTHING_READ: { return false; } - case SampleSource.DISCONTINUITY_READ: { - if (DEBUG) { - Log.d(TAG, "Read discontinuity happened"); - } - - // TODO: handle input discontinuity for trickplay. - clearDecodeState(); - return true; - } case SampleSource.FORMAT_READ: { if (DEBUG) { Log.i(TAG, "Format was read again"); @@ -203,26 +223,17 @@ public class Cea708TextTrackRenderer extends TrackRenderer implements OnCea708Pa @Override protected long getDurationUs() { - return mSource.getTrackInfo(mTrackIndex).durationUs; - } - - @Override - protected long getCurrentPositionUs() { - mCurrentPositionUs = Math.max(mCurrentPositionUs, mPresentationTimeUs); - return mCurrentPositionUs; + return mSource.getFormat(mTrackIndex).durationUs; } @Override protected long getBufferedPositionUs() { - long positionUs = mSource.getBufferedPositionUs(); - return positionUs == UNKNOWN_TIME_US || positionUs == END_OF_TRACK_US - ? positionUs : Math.max(positionUs, getCurrentPositionUs()); + return mSource.getBufferedPositionUs(); } @Override protected void seekTo(long currentPositionUs) throws ExoPlaybackException { mSource.seekToUs(currentPositionUs); - mSourceStateReady = false; mInputStreamEnded = false; mPresentationTimeUs = currentPositionUs; mCurrentPositionUs = Long.MIN_VALUE; diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/DefaultSampleSource.java b/usbtuner/src/com/android/usbtuner/exoplayer/DefaultSampleSource.java deleted file mode 100644 index 6800d644..00000000 --- a/usbtuner/src/com/android/usbtuner/exoplayer/DefaultSampleSource.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.usbtuner.exoplayer; - -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.MediaFormatHolder; -import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.SampleSource; -import com.google.android.exoplayer.TrackInfo; -import com.google.android.exoplayer.util.Assertions; - -import java.io.IOException; - -/** {@link SampleSource} that extracts sample data using a {@link SampleExtractor}. */ -public final class DefaultSampleSource implements SampleSource { - - private static final int TRACK_STATE_DISABLED = 0; - private static final int TRACK_STATE_ENABLED = 1; - private static final int TRACK_STATE_FORMAT_SENT = 2; - - private final SampleExtractor sampleExtractor; - - private TrackInfo[] trackInfos; - private boolean prepared; - private int remainingReleaseCount; - private int[] trackStates; - private boolean[] pendingDiscontinuities; - - private long seekPositionUs; - - /** - * Creates a new sample source that extracts samples using {@code sampleExtractor}. Specifies the - * {@code downstreamRendererCount} to ensure that the sample source is released only when all - * downstream renderers have been released. - * - * @param sampleExtractor a sample extractor for accessing media samples - * @param downstreamRendererCount the number of track renderers dependent on this sample source - */ - public DefaultSampleSource(SampleExtractor sampleExtractor, int downstreamRendererCount) { - this.sampleExtractor = Assertions.checkNotNull(sampleExtractor); - this.remainingReleaseCount = downstreamRendererCount; - } - - @Override - public boolean prepare(long positionUs) throws IOException { - if (prepared) { - return true; - } - - if (sampleExtractor.prepare()) { - prepared = true; - trackInfos = sampleExtractor.getTrackInfos(); - trackStates = new int[trackInfos.length]; - pendingDiscontinuities = new boolean[trackInfos.length]; - } - - return prepared; - } - - @Override - public int getTrackCount() { - Assertions.checkState(prepared); - return trackInfos.length; - } - - @Override - public TrackInfo getTrackInfo(int track) { - Assertions.checkState(prepared); - return trackInfos[track]; - } - - @Override - public void enable(int track, long positionUs) { - Assertions.checkState(prepared); - Assertions.checkState(trackStates[track] == TRACK_STATE_DISABLED); - trackStates[track] = TRACK_STATE_ENABLED; - sampleExtractor.selectTrack(track); - seekToUsInternal(positionUs, positionUs != 0); - } - - @Override - public void disable(int track) { - Assertions.checkState(prepared); - Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED); - sampleExtractor.deselectTrack(track); - pendingDiscontinuities[track] = false; - trackStates[track] = TRACK_STATE_DISABLED; - } - - @Override - public boolean continueBuffering(long positionUs) throws IOException { - return sampleExtractor.continueBuffering(positionUs); - } - - @Override - public int readData(int track, long positionUs, MediaFormatHolder formatHolder, - SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException { - Assertions.checkState(prepared); - Assertions.checkState(trackStates[track] != TRACK_STATE_DISABLED); - if (pendingDiscontinuities[track]) { - pendingDiscontinuities[track] = false; - return DISCONTINUITY_READ; - } - if (onlyReadDiscontinuity) { - return NOTHING_READ; - } - if (trackStates[track] != TRACK_STATE_FORMAT_SENT) { - sampleExtractor.getTrackMediaFormat(track, formatHolder); - trackStates[track] = TRACK_STATE_FORMAT_SENT; - return FORMAT_READ; - } - - seekPositionUs = C.UNKNOWN_TIME_US; - return sampleExtractor.readSample(track, sampleHolder); - } - - @Override - public void seekToUs(long positionUs) { - Assertions.checkState(prepared); - seekToUsInternal(positionUs, false); - } - - @Override - public long getBufferedPositionUs() { - Assertions.checkState(prepared); - return sampleExtractor.getBufferedPositionUs(); - } - - @Override - public void release() { - Assertions.checkState(remainingReleaseCount > 0); - if (--remainingReleaseCount == 0) { - sampleExtractor.release(); - } - } - - private void seekToUsInternal(long positionUs, boolean force) { - // Unless forced, avoid duplicate calls to the underlying extractor's seek method in the case - // that there have been no interleaving calls to readSample. - if (force || seekPositionUs != positionUs) { - seekPositionUs = positionUs; - sampleExtractor.seekTo(positionUs); - for (int i = 0; i < trackStates.length; ++i) { - if (trackStates[i] != TRACK_STATE_DISABLED) { - pendingDiscontinuities[i] = true; - } - } - } - } -} diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/MediaCodecVideoTrackRenderer.java b/usbtuner/src/com/android/usbtuner/exoplayer/MediaCodecVideoTrackRenderer.java deleted file mode 100644 index 19a8ff49..00000000 --- a/usbtuner/src/com/android/usbtuner/exoplayer/MediaCodecVideoTrackRenderer.java +++ /dev/null @@ -1,609 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.usbtuner.exoplayer; - -import android.annotation.TargetApi; -import android.media.MediaCodec; -import android.media.MediaCrypto; -import android.os.Handler; -import android.os.SystemClock; -import android.util.Log; -import android.view.Surface; - -import com.google.android.exoplayer.ExoPlaybackException; -import com.google.android.exoplayer.MediaCodecTrackRenderer; -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.MediaFormatHolder; -import com.google.android.exoplayer.SampleSource; -import com.google.android.exoplayer.TrackRenderer; -import com.google.android.exoplayer.drm.DrmSessionManager; -import com.google.android.exoplayer.util.MimeTypes; -import com.google.android.exoplayer.util.TraceUtil; -import com.google.android.exoplayer.util.Util; -import com.android.usbtuner.tvinput.UsbTunerDebug; - -import java.nio.ByteBuffer; - -/** - * Decodes and renders video using {@link MediaCodec}. - */ -@TargetApi(16) -public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer { - private static final String TAG = "MediaCodecVideoTrackRen"; - private static final boolean DEBUG = false; - - /** - * Interface definition for a callback to be notified of {@link MediaCodecVideoTrackRenderer} - * events. - */ - public interface EventListener extends MediaCodecTrackRenderer.EventListener { - - /** - * Invoked to report the number of frames dropped by the renderer. Dropped frames are - * reported whenever the renderer is stopped having dropped frames, and optionally, whenever - * the count reaches a specified threshold whilst the renderer is started. - * - * @param count the number of dropped frames - * @param elapsed a duration in milliseconds over which the frames were dropped. This - * duration is timed from when the renderer was started or from when dropped - * frames were last reported (whichever was more recent), and not from when the - * first of the reported drops occurred. - */ - void onDroppedFrames(int count, long elapsed); - - /** - * Invoked each time there's a change in the size of the video being rendered. - * - * @param width the video width in pixels - * @param height the video height in pixels - * @param pixelWidthHeightRatio the width to height ratio of each pixel. For the normal case - * of square pixels this will be equal to 1.0. Different values are indicative of - * anamorphic content. - */ - void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio); - - /** - * Invoked when a frame is rendered to a {@link Surface} for the first time following that - * {@link Surface} having been set as the target for the renderer. - * - * @param surface a {@link Surface} to which a first frame has been rendered - */ - void onDrawnToSurface(Surface surface); - - } - - /** - * An interface for fine-grained adjustment of frame release times. - */ - public interface FrameReleaseTimeHelper { - - /** - * Enables the helper. - */ - void enable(); - - /** - * Disables the helper. - */ - void disable(); - - /** - * Called to make a fine-grained adjustment to a frame release time. - * - * @param framePresentationTimeUs the frame's media presentation time, in microseconds - * @param unadjustedReleaseTimeNs the frame's unadjusted release time, in nanoseconds and in - * the same time base as {@link System#nanoTime()} - * @return an adjusted release time for the frame, in nanoseconds and in the same time base - * as {@link System#nanoTime()} - */ - long adjustReleaseTime(long framePresentationTimeUs, long unadjustedReleaseTimeNs); - - } - - // TODO: Use MediaFormat constants if these get exposed through the API. - private static final String KEY_CROP_LEFT = "crop-left"; - private static final String KEY_CROP_RIGHT = "crop-right"; - private static final String KEY_CROP_BOTTOM = "crop-bottom"; - private static final String KEY_CROP_TOP = "crop-top"; - - /** - * The type of a message that can be passed to an instance of this class via - * {@link ExoPlayer#sendMessage} or {@link ExoPlayer#blockingSendMessage}. The message object - * should be the target {@link Surface}, or null. - */ - public static final int MSG_SET_SURFACE = 1; - - private final FrameReleaseTimeHelper frameReleaseTimeHelper; - private final EventListener eventListener; - private final long allowedJoiningTimeUs; - private final int videoScalingMode; - private final int maxDroppedFrameCountToNotify; - - private Surface surface; - private boolean reportedDrawnToSurface; - private boolean renderedFirstFrame; - private long joiningDeadlineUs; - private long droppedFrameAccumulationStartTimeMs; - private int droppedFrameCount; - - private int currentWidth; - private int currentHeight; - private float currentPixelWidthHeightRatio; - private int lastReportedWidth; - private int lastReportedHeight; - private float lastReportedPixelWidthHeightRatio; - - /** - * @param source the upstream source from which the renderer obtains samples - * @param videoScalingMode the scaling mode to pass to - * {@link MediaCodec#setVideoScalingMode(int)} - */ - public MediaCodecVideoTrackRenderer(SampleSource source, int videoScalingMode) { - this(source, null, true, videoScalingMode); - } - - /** - * @param source the upstream source from which the renderer obtains samples - * @param drmSessionManager for use with encrypted content. May be null if support for encrypted - * content is not required. - * @param playClearSamplesWithoutKeys encrypted media may contain clear (un-encrypted) regions. - * For example a media file may start with a short clear region so as to allow - * playback to begin in parallel with key acquisition. This parameter specifies - * whether the renderer is permitted to play clear regions of encrypted media files - * before {@code drmSessionManager} has obtained the keys necessary to decrypt - * encrypted regions of the media. - * @param videoScalingMode the scaling mode to pass to - * {@link MediaCodec#setVideoScalingMode(int)} - */ - public MediaCodecVideoTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, int videoScalingMode) { - this(source, drmSessionManager, playClearSamplesWithoutKeys, videoScalingMode, 0); - } - - /** - * @param source the upstream source from which the renderer obtains samples - * @param videoScalingMode the scaling mode to pass to - * {@link MediaCodec#setVideoScalingMode(int)} - * @param allowedJoiningTimeMs the maximum duration in milliseconds for which this video - * renderer can attempt to seamlessly join an ongoing playback - */ - public MediaCodecVideoTrackRenderer(SampleSource source, int videoScalingMode, - long allowedJoiningTimeMs) { - this(source, null, true, videoScalingMode, allowedJoiningTimeMs); - } - - /** - * @param source the upstream source from which the renderer obtains samples - * @param drmSessionManager for use with encrypted content. May be null if support for encrypted - * content is not required. - * @param playClearSamplesWithoutKeys encrypted media may contain clear (un-encrypted) regions. - * For example a media file may start with a short clear region so as to allow - * playback to begin in parallel with key acquisition. This parameter specifies - * whether the renderer is permitted to play clear regions of encrypted media files - * before {@code drmSessionManager} has obtained the keys necessary to decrypt - * encrypted regions of the media. - * @param videoScalingMode the scaling mode to pass to - * {@link MediaCodec#setVideoScalingMode(int)} - * @param allowedJoiningTimeMs the maximum duration in milliseconds for which this video - * renderer can attempt to seamlessly join an ongoing playback - */ - public MediaCodecVideoTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, int videoScalingMode, long allowedJoiningTimeMs) { - this(source, drmSessionManager, playClearSamplesWithoutKeys, videoScalingMode, - allowedJoiningTimeMs, null, null, null, -1); - } - - /** - * @param source the upstream source from which the renderer obtains samples - * @param videoScalingMode the scaling mode to pass to - * {@link MediaCodec#setVideoScalingMode(int)} - * @param allowedJoiningTimeMs the maximum duration in milliseconds for which this video - * renderer can attempt to seamlessly join an ongoing playback - * @param eventHandler a handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener a listener of events. May be null if delivery of events is not required. - * @param maxDroppedFrameCountToNotify the maximum number of frames that can be dropped between - * invocations of {@link EventListener#onDroppedFrames(int, long)} - */ - public MediaCodecVideoTrackRenderer(SampleSource source, int videoScalingMode, - long allowedJoiningTimeMs, Handler eventHandler, EventListener eventListener, - int maxDroppedFrameCountToNotify) { - this(source, null, true, videoScalingMode, allowedJoiningTimeMs, null, eventHandler, - eventListener, maxDroppedFrameCountToNotify); - } - - /** - * @param source the upstream source from which the renderer obtains samples - * @param drmSessionManager for use with encrypted content. May be null if support for encrypted - * content is not required. - * @param playClearSamplesWithoutKeys encrypted media may contain clear (un-encrypted) regions. - * For example a media file may start with a short clear region so as to allow - * playback to begin in parallel with key acquisition. This parameter specifies - * whether the renderer is permitted to play clear regions of encrypted media files - * before {@code drmSessionManager} has obtained the keys necessary to decrypt - * encrypted regions of the media. - * @param videoScalingMode the scaling mode to pass to - * {@link MediaCodec#setVideoScalingMode(int)} - * @param allowedJoiningTimeMs the maximum duration in milliseconds for which this video - * renderer can attempt to seamlessly join an ongoing playback - * @param frameReleaseTimeHelper an optional helper to make fine-grained adjustments to frame - * release times. May be null. - * @param eventHandler a handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener a listener of events. May be null if delivery of events is not required. - * @param maxDroppedFrameCountToNotify the maximum number of frames that can be dropped between - * invocations of {@link EventListener#onDroppedFrames(int, long)}. - */ - public MediaCodecVideoTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, int videoScalingMode, long allowedJoiningTimeMs, - FrameReleaseTimeHelper frameReleaseTimeHelper, Handler eventHandler, - EventListener eventListener, int maxDroppedFrameCountToNotify) { - super(source, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener); - this.videoScalingMode = videoScalingMode; - this.allowedJoiningTimeUs = allowedJoiningTimeMs * 1000; - this.frameReleaseTimeHelper = frameReleaseTimeHelper; - this.eventListener = eventListener; - this.maxDroppedFrameCountToNotify = maxDroppedFrameCountToNotify; - joiningDeadlineUs = -1; - currentWidth = -1; - currentHeight = -1; - currentPixelWidthHeightRatio = -1; - lastReportedWidth = -1; - lastReportedHeight = -1; - lastReportedPixelWidthHeightRatio = -1; - } - - @Override - protected boolean handlesMimeType(String mimeType) { - return MimeTypes.isVideo(mimeType) && super.handlesMimeType(mimeType); - } - - @Override - protected void onEnabled(long positionUs, boolean joining) { - super.onEnabled(positionUs, joining); - renderedFirstFrame = false; - if (joining && allowedJoiningTimeUs > 0) { - joiningDeadlineUs = SystemClock.elapsedRealtime() * 1000L + allowedJoiningTimeUs; - } - if (frameReleaseTimeHelper != null) { - frameReleaseTimeHelper.enable(); - } - } - - @Override - protected void seekTo(long positionUs) throws ExoPlaybackException { - super.seekTo(positionUs); - renderedFirstFrame = false; - joiningDeadlineUs = -1; - } - - @Override - protected boolean isReady() { - if (super.isReady() && (renderedFirstFrame || !codecInitialized() - || getSourceState() == SOURCE_STATE_READY_READ_MAY_FAIL)) { - // Ready. If we were joining then we've now joined, so clear the joining deadline. - joiningDeadlineUs = -1; - return true; - } else if (joiningDeadlineUs == -1) { - // Not joining. - return false; - } else if (SystemClock.elapsedRealtime() * 1000 < joiningDeadlineUs) { - // Joining and still within the joining deadline. - return true; - } else { - // The joining deadline has been exceeded. Give up and clear the deadline. - joiningDeadlineUs = -1; - return false; - } - } - - @Override - protected void onStarted() { - super.onStarted(); - droppedFrameCount = 0; - droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime(); - } - - @Override - protected void onStopped() { - joiningDeadlineUs = -1; - maybeNotifyDroppedFrameCount(); - super.onStopped(); - } - - @Override - public void onDisabled() { - currentWidth = -1; - currentHeight = -1; - currentPixelWidthHeightRatio = -1; - lastReportedWidth = -1; - lastReportedHeight = -1; - lastReportedPixelWidthHeightRatio = -1; - if (frameReleaseTimeHelper != null) { - frameReleaseTimeHelper.disable(); - } - super.onDisabled(); - } - - @Override - public void handleMessage(int messageType, Object message) throws ExoPlaybackException { - if (messageType == MSG_SET_SURFACE) { - setSurface((Surface) message); - } else { - super.handleMessage(messageType, message); - } - } - - /** - * @param surface a {@link Surface} to set - * @throws {@link ExoPlaybackException} - */ - private void setSurface(Surface surface) throws ExoPlaybackException { - if (this.surface == surface) { - return; - } - this.surface = surface; - this.reportedDrawnToSurface = false; - int state = getState(); - if (state == TrackRenderer.STATE_ENABLED || state == TrackRenderer.STATE_STARTED) { - releaseCodec(); - maybeInitCodec(); - } - } - - @Override - protected boolean shouldInitCodec() { - return super.shouldInitCodec() && surface != null && surface.isValid(); - } - - // Override configureCodec to provide the {@link Surface}. - @Override - protected void configureCodec(MediaCodec codec, String codecName, - android.media.MediaFormat format, - MediaCrypto crypto) { - Log.d(TAG, "configureCodec " + format); - codec.configure(format, surface, crypto, 0); - codec.setVideoScalingMode(videoScalingMode); - - // This method is also called from onInputFormatChanged(). For an earlier notification of - // video stream information, invoke updateVideoSize() here. - updateVideoSize(format); - } - - @Override - protected void onInputFormatChanged(MediaFormatHolder holder) throws ExoPlaybackException { - super.onInputFormatChanged(holder); - - // TODO: Ideally this would be read in onOutputFormatChanged, but there doesn't seem - // to be a way to pass a custom key/value pair value through to the output format. - currentPixelWidthHeightRatio = holder.format.pixelWidthHeightRatio == MediaFormat.NO_VALUE - ? 1 : holder.format.pixelWidthHeightRatio; - } - - @Override - protected void onOutputFormatChanged(MediaFormat inputFormat, - android.media.MediaFormat format) { - Log.d(TAG, "onOutputFormatChanged " + format); - updateVideoSize(format); - } - - private void updateVideoSize(android.media.MediaFormat format) { - boolean hasCrop = format.containsKey(KEY_CROP_RIGHT) && format.containsKey(KEY_CROP_LEFT) - && format.containsKey(KEY_CROP_BOTTOM) && format.containsKey(KEY_CROP_TOP); - currentWidth = hasCrop - ? format.getInteger(KEY_CROP_RIGHT) - format.getInteger(KEY_CROP_LEFT) + 1 - : format.getInteger(android.media.MediaFormat.KEY_WIDTH); - currentHeight = hasCrop - ? format.getInteger(KEY_CROP_BOTTOM) - format.getInteger(KEY_CROP_TOP) + 1 - : format.getInteger(android.media.MediaFormat.KEY_HEIGHT); - - maybeNotifyVideoSizeChanged(); - } - - @Override - protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive, - MediaFormat oldFormat, MediaFormat newFormat) { - return newFormat.mimeType.equals(oldFormat.mimeType) - && (codecIsAdaptive - || (oldFormat.width == newFormat.width - && oldFormat.height == newFormat.height)); - } - - @Override - protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec, - ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex, - boolean shouldSkip) { - if (shouldSkip) { - skipOutputBuffer(codec, bufferIndex); - return true; - } - - if (UsbTunerDebug.ENABLED) { - UsbTunerDebug.setAudioPositionUs(positionUs); - UsbTunerDebug.setVideoPtsUs(bufferInfo.presentationTimeUs); - } - - // Compute how many microseconds it is until the buffer's presentation time. - long elapsedSinceStartOfLoopUs = (SystemClock.elapsedRealtime() * 1000) - elapsedRealtimeUs; - long earlyUs = bufferInfo.presentationTimeUs - positionUs - elapsedSinceStartOfLoopUs; - - // Compute the buffer's desired release time in nanoseconds. - long systemTimeNs = System.nanoTime(); - long unadjustedFrameReleaseTimeNs = systemTimeNs + (earlyUs * 1000); - - // Apply a timestamp adjustment, if there is one. - long adjustedReleaseTimeNs; - if (frameReleaseTimeHelper != null) { - adjustedReleaseTimeNs = frameReleaseTimeHelper.adjustReleaseTime( - bufferInfo.presentationTimeUs, unadjustedFrameReleaseTimeNs); - earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000; - } else { - adjustedReleaseTimeNs = unadjustedFrameReleaseTimeNs; - } - - if (earlyUs < -30000) { - // We're more than 30ms late rendering the frame. - dropOutputBuffer(codec, bufferIndex); - if (DEBUG) { - Log.e(TAG, "video frame drop - earlyUs: " + earlyUs); - } - if (UsbTunerDebug.ENABLED) { - UsbTunerDebug.notifyVideoFrameDrop(earlyUs); - } - return true; - } - - if (!renderedFirstFrame) { - renderOutputBufferImmediate(codec, bufferIndex); - renderedFirstFrame = true; - return true; - } - - if (getState() != TrackRenderer.STATE_STARTED) { - return false; - } - - if (Util.SDK_INT >= 21) { - // Let the underlying framework time the release. - if (earlyUs < 50000) { - renderOutputBufferTimedV21(codec, bufferIndex, adjustedReleaseTimeNs); - return true; - } - } else { - // We need to time the release ourselves. - if (earlyUs < 30000) { - if (earlyUs > 11000) { - // We're a little too early to render the frame. Sleep until the frame can be - // rendered. - // Note: The 11ms threshold was chosen fairly arbitrarily. - try { - // Subtracting 10000 rather than 11000 ensures the sleep time will be at - // least 1ms. - Thread.sleep((earlyUs - 10000) / 1000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - renderOutputBufferImmediate(codec, bufferIndex); - return true; - } - } - - // We're either not playing, or it's not time to render the frame yet. - return false; - } - - private void skipOutputBuffer(MediaCodec codec, int bufferIndex) { - TraceUtil.beginSection("skipVideoBuffer"); - codec.releaseOutputBuffer(bufferIndex, false); - TraceUtil.endSection(); - codecCounters.skippedOutputBufferCount++; - } - - private void dropOutputBuffer(MediaCodec codec, int bufferIndex) { - TraceUtil.beginSection("dropVideoBuffer"); - codec.releaseOutputBuffer(bufferIndex, false); - TraceUtil.endSection(); - codecCounters.droppedOutputBufferCount++; - droppedFrameCount++; - if (droppedFrameCount == maxDroppedFrameCountToNotify) { - maybeNotifyDroppedFrameCount(); - } - } - - private void renderOutputBufferImmediate(MediaCodec codec, int bufferIndex) { - TraceUtil.beginSection("renderVideoBufferImmediate"); - codec.releaseOutputBuffer(bufferIndex, true); - TraceUtil.endSection(); - codecCounters.renderedOutputBufferCount++; - maybeNotifyDrawnToSurface(); - } - - @TargetApi(21) - private void renderOutputBufferTimedV21(MediaCodec codec, int bufferIndex, long releaseTimeNs) { - TraceUtil.beginSection("releaseOutputBufferTimed"); - codec.releaseOutputBuffer(bufferIndex, releaseTimeNs); - TraceUtil.endSection(); - codecCounters.renderedOutputBufferCount++; - maybeNotifyDrawnToSurface(); - } - - private void maybeNotifyVideoSizeChanged() { - if (eventHandler == null || eventListener == null - || (lastReportedWidth == currentWidth && lastReportedHeight == currentHeight - && lastReportedPixelWidthHeightRatio == currentPixelWidthHeightRatio)) { - return; - } - - // Make final copies to ensure the runnable reports the correct values. - final int currentWidth = this.currentWidth; - final int currentHeight = this.currentHeight; - final float currentPixelWidthHeightRatio = this.currentPixelWidthHeightRatio; - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onVideoSizeChanged(currentWidth, currentHeight, - currentPixelWidthHeightRatio); - } - }); - - // Update the last reported values. - lastReportedWidth = currentWidth; - lastReportedHeight = currentHeight; - lastReportedPixelWidthHeightRatio = currentPixelWidthHeightRatio; - } - - private void maybeNotifyDrawnToSurface() { - if (eventHandler == null || eventListener == null || reportedDrawnToSurface) { - return; - } - - // Make a final copy to ensure the runnable reports the correct {@link Surface}. - final Surface surface = this.surface; - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDrawnToSurface(surface); - } - }); - - // Record that we have reported that the {@link Surface} has been drawn to. - reportedDrawnToSurface = true; - } - - private void maybeNotifyDroppedFrameCount() { - if (eventHandler == null || eventListener == null || droppedFrameCount == 0) { - return; - } - long now = SystemClock.elapsedRealtime(); - - // Make final copies to ensure the runnable reports the correct values. - final int countToNotify = droppedFrameCount; - final long elapsedToNotify = now - droppedFrameAccumulationStartTimeMs; - eventHandler.post(new Runnable() { - @Override - public void run() { - eventListener.onDroppedFrames(countToNotify, elapsedToNotify); - } - }); - - // Reset the dropped frame tracking. - droppedFrameCount = 0; - droppedFrameAccumulationStartTimeMs = now; - } - -} diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/MpegTsPassthroughAc3RendererBuilder.java b/usbtuner/src/com/android/usbtuner/exoplayer/MpegTsPassthroughAc3RendererBuilder.java index 971be491..70f266d6 100644 --- a/usbtuner/src/com/android/usbtuner/exoplayer/MpegTsPassthroughAc3RendererBuilder.java +++ b/usbtuner/src/com/android/usbtuner/exoplayer/MpegTsPassthroughAc3RendererBuilder.java @@ -16,7 +16,7 @@ package com.android.usbtuner.exoplayer; -import android.media.MediaCodec; +import android.content.Context; import android.media.MediaDataSource; import com.google.android.exoplayer.SampleSource; @@ -24,21 +24,20 @@ import com.google.android.exoplayer.TrackRenderer; import com.android.usbtuner.exoplayer.MpegTsPlayer.RendererBuilder; import com.android.usbtuner.exoplayer.MpegTsPlayer.RendererBuilderCallback; import com.android.usbtuner.exoplayer.ac3.Ac3TrackRenderer; +import com.android.usbtuner.exoplayer.cache.CacheManager; import com.android.usbtuner.tvinput.PlaybackCacheListener; /** * Builder class for AC3 Passthrough track renderer objects. */ public class MpegTsPassthroughAc3RendererBuilder implements RendererBuilder { - private static final int NUM_TRACKS = 3; - private static final int VIDEO_PLAYBACK_DEADLINE_IN_MS = 5000; - private static final int DROPPED_FRAMES_NOTIFICATION_THRESHOLD = 50; - + private final Context mContext; private final CacheManager mCacheManager; private final PlaybackCacheListener mCacheListener; - public MpegTsPassthroughAc3RendererBuilder(CacheManager cacheManager, + public MpegTsPassthroughAc3RendererBuilder(Context context, CacheManager cacheManager, PlaybackCacheListener cacheListener) { + mContext = context; mCacheManager = cacheManager; mCacheListener = cacheListener; } @@ -50,26 +49,17 @@ public class MpegTsPassthroughAc3RendererBuilder implements RendererBuilder { SampleExtractor extractor = dataSource == null ? new MpegTsSampleSourceExtractor(mCacheManager, mCacheListener) : new MpegTsSampleSourceExtractor(dataSource, mCacheManager, mCacheListener); - SampleSource sampleSource = new DefaultSampleSource(extractor, - mpegTsPlayer.isAc3Playable() ? NUM_TRACKS : NUM_TRACKS - 1); - MediaCodecVideoTrackRenderer videoRenderer = - new MediaCodecVideoTrackRenderer(sampleSource, null, true, - MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, VIDEO_PLAYBACK_DEADLINE_IN_MS, - null, mpegTsPlayer.getMainHandler(), mpegTsPlayer, - DROPPED_FRAMES_NOTIFICATION_THRESHOLD); - Ac3TrackRenderer audioRenderer = null; - if (mpegTsPlayer.isAc3Playable()) { - audioRenderer = new Ac3TrackRenderer(sampleSource, - mpegTsPlayer.getMainHandler(), mpegTsPlayer, false); - } else { - mpegTsPlayer.onAudioUnplayable(); - } + SampleSource sampleSource = new MpegTsSampleSource(extractor); + MpegTsVideoTrackRenderer videoRenderer = new MpegTsVideoTrackRenderer(mContext, + sampleSource, mpegTsPlayer.getMainHandler(), mpegTsPlayer); + Ac3TrackRenderer audioRenderer = new Ac3TrackRenderer(sampleSource, + mpegTsPlayer.getMainHandler(), mpegTsPlayer, false); Cea708TextTrackRenderer textRenderer = new Cea708TextTrackRenderer(sampleSource); TrackRenderer[] renderers = new TrackRenderer[MpegTsPlayer.RENDERER_COUNT]; renderers[MpegTsPlayer.TRACK_TYPE_VIDEO] = videoRenderer; renderers[MpegTsPlayer.TRACK_TYPE_AUDIO] = audioRenderer; renderers[MpegTsPlayer.TRACK_TYPE_TEXT] = textRenderer; - callback.onRenderers(null, null, renderers); + callback.onRenderers(null, renderers); } } diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/MpegTsPlayer.java b/usbtuner/src/com/android/usbtuner/exoplayer/MpegTsPlayer.java index db47fb90..34d83875 100644 --- a/usbtuner/src/com/android/usbtuner/exoplayer/MpegTsPlayer.java +++ b/usbtuner/src/com/android/usbtuner/exoplayer/MpegTsPlayer.java @@ -29,10 +29,10 @@ import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.ExoPlayer; import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; +import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.audio.AudioCapabilities; import com.google.android.exoplayer.audio.AudioTrack; -import com.google.android.exoplayer.chunk.MultiTrackChunkSource; import com.android.usbtuner.data.Cea708Data; import com.android.usbtuner.data.Cea708Data.CaptionEvent; import com.android.usbtuner.exoplayer.Cea708TextTrackRenderer.CcListener; @@ -45,8 +45,7 @@ import java.lang.annotation.RetentionPolicy; * MPEG-2 TS stream player implementation using ExoPlayer. */ public class MpegTsPlayer implements ExoPlayer.Listener, - MediaCodecVideoTrackRenderer.EventListener, MediaCodecAudioTrackRenderer.EventListener, - Ac3TrackRenderer.EventListener { + MediaCodecVideoTrackRenderer.EventListener, Ac3TrackRenderer.EventListener { private int mCaptionServiceNumber = Cea708Data.EMPTY_SERVICE_NUMBER; /** @@ -61,8 +60,7 @@ public class MpegTsPlayer implements ExoPlayer.Listener, * Interface definition for {@link RendererBuilder#buildRenderers} to notify the result. */ public interface RendererBuilderCallback { - void onRenderers(String[][] trackNames, MultiTrackChunkSource[] multiTrackSources, - TrackRenderer[] renderers); + void onRenderers(String[][] trackNames, TrackRenderer[] renderers); void onRenderersError(Exception e); } @@ -138,7 +136,6 @@ public class MpegTsPlayer implements ExoPlayer.Listener, private TrackRenderer mVideoRenderer; private TrackRenderer mAudioRenderer; - private MultiTrackChunkSource[] mMultiTrackSources; private String[][] mTrackNames; private int[] mSelectedTracks; @@ -147,7 +144,7 @@ public class MpegTsPlayer implements ExoPlayer.Listener, private VideoEventListener mVideoEventListener; public MpegTsPlayer(int playerGeneration, RendererBuilder rendererBuilder, Handler handler, - AudioCapabilities capabilities) { + AudioCapabilities capabilities, Listener listener) { mRendererBuilder = rendererBuilder; mPlayer = ExoPlayer.Factory.newInstance(RENDERER_COUNT, MIN_BUFFER_MS, MIN_REBUFFER_MS); mPlayer.addListener(this); @@ -158,16 +155,9 @@ public class MpegTsPlayer implements ExoPlayer.Listener, mRendererBuildingState = RENDERER_BUILDING_STATE_IDLE; mSelectedTracks = new int[RENDERER_COUNT]; mCcListener = new MpegTsCcListener(); - } - - public void addListener(Listener listener) { mListener = listener; } - public void removeListener(Listener listener) { - mListener = null; - } - public void setVideoEventListener(VideoEventListener videoEventListener) { mVideoEventListener = videoEventListener; } @@ -223,25 +213,17 @@ public class MpegTsPlayer implements ExoPlayer.Listener, mRendererBuilder.buildRenderers(this, source, mBuilderCallback); } - /* package */ void onRenderers(String[][] trackNames, - MultiTrackChunkSource[] multiTrackSources, TrackRenderer[] renderers) { + /* package */ void onRenderers(String[][] trackNames, TrackRenderer[] renderers) { mBuilderCallback = null; // Normalize the results. if (trackNames == null) { trackNames = new String[RENDERER_COUNT][]; } - if (multiTrackSources == null) { - multiTrackSources = new MultiTrackChunkSource[RENDERER_COUNT]; - } for (int i = 0; i < RENDERER_COUNT; i++) { if (renderers[i] == null) { // Convert a null renderer to a dummy renderer. renderers[i] = new DummyTrackRenderer(); - } else if (trackNames[i] == null) { - int trackCount = multiTrackSources[i] == null - ? 1 : multiTrackSources[i].getTrackCount(); - trackNames[i] = new String[trackCount]; } } mVideoRenderer = renderers[TRACK_TYPE_VIDEO]; @@ -251,13 +233,12 @@ public class MpegTsPlayer implements ExoPlayer.Listener, mPlayer.sendMessage( mTextRenderer, Cea708TextTrackRenderer.MSG_SERVICE_NUMBER, mCaptionServiceNumber); mTrackNames = trackNames; - mMultiTrackSources = multiTrackSources; mRendererBuildingState = RENDERER_BUILDING_STATE_BUILT; pushSurface(false); + mPlayer.prepare(renderers); pushTrackSelection(TRACK_TYPE_VIDEO, true); pushTrackSelection(TRACK_TYPE_AUDIO, true); pushTrackSelection(TRACK_TYPE_TEXT, true); - mPlayer.prepare(renderers); } /* package */ void onRenderersError(Exception e) { @@ -284,6 +265,7 @@ public class MpegTsPlayer implements ExoPlayer.Listener, } mRendererBuildingState = RENDERER_BUILDING_STATE_IDLE; mSurface = null; + mListener = null; mPlayer.release(); } @@ -361,7 +343,8 @@ public class MpegTsPlayer implements ExoPlayer.Listener, } @Override - public void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) { + public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, + float pixelWidthHeightRatio) { if (mListener != null) { mListener.onVideoSizeChanged(mPlayerGeneration, width, height, pixelWidthHeightRatio); } @@ -446,19 +429,7 @@ public class MpegTsPlayer implements ExoPlayer.Listener, if (mRendererBuildingState != RENDERER_BUILDING_STATE_BUILT) { return; } - - int trackIndex = mSelectedTracks[type]; - if (mMultiTrackSources[type] == null) { - mPlayer.setRendererEnabled(type, allowRendererEnable); - } else { - boolean playWhenReady = mPlayer.getPlayWhenReady(); - mPlayer.setPlayWhenReady(false); - mPlayer.setRendererEnabled(type, false); - mPlayer.sendMessage(mMultiTrackSources[type], MultiTrackChunkSource.MSG_SELECT_TRACK, - trackIndex); - mPlayer.setRendererEnabled(type, allowRendererEnable); - mPlayer.setPlayWhenReady(playWhenReady); - } + mPlayer.setSelectedTrack(type, allowRendererEnable ? 0 : -1); } private class MpegTsCcListener implements CcListener { @@ -486,10 +457,9 @@ public class MpegTsPlayer implements ExoPlayer.Listener, } @Override - public void onRenderers(String[][] trackNames, MultiTrackChunkSource[] multiTrackSources, - TrackRenderer[] renderers) { + public void onRenderers(String[][] trackNames, TrackRenderer[] renderers) { if (!canceled) { - MpegTsPlayer.this.onRenderers(trackNames, multiTrackSources, renderers); + MpegTsPlayer.this.onRenderers(trackNames, renderers); } } diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/MpegTsSampleSource.java b/usbtuner/src/com/android/usbtuner/exoplayer/MpegTsSampleSource.java new file mode 100644 index 00000000..a799c9a4 --- /dev/null +++ b/usbtuner/src/com/android/usbtuner/exoplayer/MpegTsSampleSource.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.usbtuner.exoplayer; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.MediaFormat; +import com.google.android.exoplayer.MediaFormatHolder; +import com.google.android.exoplayer.SampleHolder; +import com.google.android.exoplayer.SampleSource; +import com.google.android.exoplayer.SampleSource.SampleSourceReader; +import com.google.android.exoplayer.util.Assertions; + +import java.io.IOException; + +/** {@link SampleSource} that extracts sample data using a {@link SampleExtractor}. */ +public final class MpegTsSampleSource implements SampleSource, SampleSourceReader { + + private static final int TRACK_STATE_DISABLED = 0; + private static final int TRACK_STATE_ENABLED = 1; + private static final int TRACK_STATE_FORMAT_SENT = 2; + + private final SampleExtractor mSampleExtractor; + + private MediaFormat[] mTrackFormats; + private boolean mPrepared; + private IOException mPreparationError; + private int mRemainingReleaseCount; + private int[] mTrackStates; + private boolean[] mPendingDiscontinuities; + + private long mLastSeekPositionUs; + private long mPendingSeekPositionUs; + + /** + * Creates a new sample source that extracts samples using {@code mSampleExtractor}. + * + * @param sampleExtractor a sample extractor for accessing media samples + */ + public MpegTsSampleSource(SampleExtractor sampleExtractor) { + mSampleExtractor = Assertions.checkNotNull(sampleExtractor); + } + + @Override + public SampleSourceReader register() { + mRemainingReleaseCount++; + return this; + } + + @Override + public boolean prepare(long positionUs) { + if (!mPrepared) { + if (mPreparationError != null) { + return false; + } + try { + if (mSampleExtractor.prepare()) { + mTrackFormats = mSampleExtractor.getTrackFormats(); + mTrackStates = new int[mTrackFormats.length]; + mPendingDiscontinuities = new boolean[mTrackStates.length]; + mPrepared = true; + } + } catch (IOException e) { + mPreparationError = e; + return false; + } + } + return true; + } + + @Override + public int getTrackCount() { + Assertions.checkState(mPrepared); + return mTrackFormats.length; + } + + @Override + public MediaFormat getFormat(int track) { + Assertions.checkState(mPrepared); + return mTrackFormats[track]; + } + + @Override + public void enable(int track, long positionUs) { + Assertions.checkState(mPrepared); + Assertions.checkState(mTrackStates[track] == TRACK_STATE_DISABLED); + mTrackStates[track] = TRACK_STATE_ENABLED; + mSampleExtractor.selectTrack(track); + seekToUsInternal(positionUs, positionUs != 0); + } + + @Override + public void disable(int track) { + Assertions.checkState(mPrepared); + Assertions.checkState(mTrackStates[track] != TRACK_STATE_DISABLED); + mSampleExtractor.deselectTrack(track); + mPendingDiscontinuities[track] = false; + mTrackStates[track] = TRACK_STATE_DISABLED; + } + + @Override + public boolean continueBuffering(int track, long positionUs) { + return mSampleExtractor.continueBuffering(positionUs); + } + + @Override + public long readDiscontinuity(int track) { + if (mPendingDiscontinuities[track]) { + mPendingDiscontinuities[track] = false; + return mLastSeekPositionUs; + } + return NO_DISCONTINUITY; + } + + @Override + public int readData(int track, long positionUs, MediaFormatHolder formatHolder, + SampleHolder sampleHolder) { + Assertions.checkState(mPrepared); + Assertions.checkState(mTrackStates[track] != TRACK_STATE_DISABLED); + if (mPendingDiscontinuities[track]) { + return NOTHING_READ; + } + if (mTrackStates[track] != TRACK_STATE_FORMAT_SENT) { + mSampleExtractor.getTrackMediaFormat(track, formatHolder); + mTrackStates[track] = TRACK_STATE_FORMAT_SENT; + return FORMAT_READ; + } + + mPendingSeekPositionUs = C.UNKNOWN_TIME_US; + return mSampleExtractor.readSample(track, sampleHolder); + } + + @Override + public void maybeThrowError() throws IOException { + if (mPreparationError != null) { + throw mPreparationError; + } + } + + @Override + public void seekToUs(long positionUs) { + Assertions.checkState(mPrepared); + seekToUsInternal(positionUs, false); + } + + @Override + public long getBufferedPositionUs() { + Assertions.checkState(mPrepared); + return mSampleExtractor.getBufferedPositionUs(); + } + + @Override + public void release() { + Assertions.checkState(mRemainingReleaseCount > 0); + if (--mRemainingReleaseCount == 0) { + mSampleExtractor.release(); + } + } + + private void seekToUsInternal(long positionUs, boolean force) { + // Unless forced, avoid duplicate calls to the underlying extractor's seek method + // in the case that there have been no interleaving calls to readSample. + if (force || mPendingSeekPositionUs != positionUs) { + mLastSeekPositionUs = positionUs; + mPendingSeekPositionUs = positionUs; + mSampleExtractor.seekTo(positionUs); + for (int i = 0; i < mTrackStates.length; ++i) { + if (mTrackStates[i] != TRACK_STATE_DISABLED) { + mPendingDiscontinuities[i] = true; + } + } + } + } +} diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/MpegTsSampleSourceExtractor.java b/usbtuner/src/com/android/usbtuner/exoplayer/MpegTsSampleSourceExtractor.java index 7a6cbf10..7a7248be 100644 --- a/usbtuner/src/com/android/usbtuner/exoplayer/MpegTsSampleSourceExtractor.java +++ b/usbtuner/src/com/android/usbtuner/exoplayer/MpegTsSampleSourceExtractor.java @@ -18,12 +18,13 @@ package com.android.usbtuner.exoplayer; import android.media.MediaDataSource; -import com.google.android.exoplayer.C; +import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormatHolder; +import com.google.android.exoplayer.MediaFormatUtil; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; -import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.util.MimeTypes; +import com.android.usbtuner.exoplayer.cache.CacheManager; import com.android.usbtuner.tvinput.PlaybackCacheListener; import java.io.IOException; @@ -38,7 +39,7 @@ public final class MpegTsSampleSourceExtractor implements SampleExtractor { private static final int CC_BUFFER_SIZE_IN_BYTES = 9600 / 8; private final SampleExtractor mSampleExtractor; - private TrackInfo[] mTrackInfos; + private MediaFormat[] mTrackFormats; private boolean[] mGotEos; private int mVideoTrackIndex; private int mCea708TextTrackIndex; @@ -56,16 +57,34 @@ public final class MpegTsSampleSourceExtractor implements SampleExtractor { mCea708TextTrackSelected = false; } + /** + * Creates MpegTsSampleSourceExtractor for {@link MediaDataSource}. + * + * @param source the {@link MediaDataSource} to extract from + * @param cacheManager the manager for reading & writing samples backed by physical storage + * @param cacheListener the {@link com.android.usbtuner.tvinput.PlaybackCacheListener} + * to notify cache storage status change + */ public MpegTsSampleSourceExtractor(MediaDataSource source, CacheManager cacheManager, PlaybackCacheListener cacheListener) { if (cacheManager == null || cacheManager.isDisabled()) { - mSampleExtractor = new SimpleSampleSourceExtractor(source, cacheListener); + mSampleExtractor = + new PlaySampleExtractor(source, cacheManager, cacheListener, false); + } else { - mSampleExtractor = new CachedSampleSourceExtractor(source, cacheManager, cacheListener); + mSampleExtractor = + new PlaySampleExtractor(source, cacheManager, cacheListener, true); } init(); } + /** + * Creates MpegTsSampleSourceExtractor for a recorded program. + * + * @param cacheManager the samples provider which is stored in physical storage + * @param cacheListener the {@link com.android.usbtuner.tvinput.PlaybackCacheListener} + * to notify cache storage status change + */ public MpegTsSampleSourceExtractor(CacheManager cacheManager, PlaybackCacheListener cacheListener) { mSampleExtractor = new ReplaySampleSourceExtractor(cacheManager, cacheListener); @@ -74,13 +93,15 @@ public final class MpegTsSampleSourceExtractor implements SampleExtractor { @Override public boolean prepare() throws IOException { - mSampleExtractor.prepare(); - TrackInfo trackInfos[] = mSampleExtractor.getTrackInfos(); - int trackCount = trackInfos.length; + if(!mSampleExtractor.prepare()) { + return false; + } + MediaFormat trackFormats[] = mSampleExtractor.getTrackFormats(); + int trackCount = trackFormats.length; mGotEos = new boolean[trackCount]; for (int i = 0; i < trackCount; ++i) { - String mime = trackInfos[i].mimeType; + String mime = trackFormats[i].mimeType; if (MimeTypes.isVideo(mime) && mVideoTrackIndex == -1) { mVideoTrackIndex = i; if (android.media.MediaFormat.MIMETYPE_VIDEO_MPEG2.equals(mime)) { @@ -94,18 +115,18 @@ public final class MpegTsSampleSourceExtractor implements SampleExtractor { if (mVideoTrackIndex != -1) { mCea708TextTrackIndex = trackCount; } - mTrackInfos = new TrackInfo[mCea708TextTrackIndex < 0 ? trackCount : trackCount + 1]; - System.arraycopy(trackInfos, 0, mTrackInfos, 0, trackCount); + mTrackFormats = new MediaFormat[mCea708TextTrackIndex < 0 ? trackCount : trackCount + 1]; + System.arraycopy(trackFormats, 0, mTrackFormats, 0, trackCount); if (mCea708TextTrackIndex >= 0) { - mTrackInfos[trackCount] = new TrackInfo(MIMETYPE_TEXT_CEA_708, - trackCount > 0 ? mTrackInfos[0].durationUs : C.UNKNOWN_TIME_US); + mTrackFormats[trackCount] = MediaFormatUtil.createTextMediaFormat(MIMETYPE_TEXT_CEA_708, + mTrackFormats[0].durationUs); } return true; } @Override - public TrackInfo[] getTrackInfos() { - return mTrackInfos; + public MediaFormat[] getTrackFormats() { + return mTrackFormats; } @Override @@ -137,9 +158,9 @@ public final class MpegTsSampleSourceExtractor implements SampleExtractor { } @Override - public void getTrackMediaFormat(int track, MediaFormatHolder mediaFormatHolder) { + public void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder) { if (track != mCea708TextTrackIndex) { - mSampleExtractor.getTrackMediaFormat(track, mediaFormatHolder); + mSampleExtractor.getTrackMediaFormat(track, outMediaFormatHolder); } } @@ -163,12 +184,7 @@ public final class MpegTsSampleSourceExtractor implements SampleExtractor { return mGotEos[track] ? SampleSource.END_OF_STREAM : SampleSource.NOTHING_READ; } - int result; - try { - result = mSampleExtractor.readSample(track, sampleHolder); - } catch (IOException ex) { - return SampleSource.NOTHING_READ; - } + int result = mSampleExtractor.readSample(track, sampleHolder); switch (result) { case SampleSource.END_OF_STREAM: { mGotEos[track] = true; diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/MpegTsVideoTrackRenderer.java b/usbtuner/src/com/android/usbtuner/exoplayer/MpegTsVideoTrackRenderer.java new file mode 100644 index 00000000..d2caeaa4 --- /dev/null +++ b/usbtuner/src/com/android/usbtuner/exoplayer/MpegTsVideoTrackRenderer.java @@ -0,0 +1,60 @@ +package com.android.usbtuner.exoplayer; + +import android.content.Context; +import android.media.MediaCodec; +import android.os.Handler; + +import com.google.android.exoplayer.DecoderInfo; +import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.MediaCodecSelector; +import com.google.android.exoplayer.MediaCodecUtil; +import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; +import com.google.android.exoplayer.MediaFormatHolder; +import com.google.android.exoplayer.MediaSoftwareCodecUtil; +import com.google.android.exoplayer.SampleSource; +import com.android.tv.common.feature.CommonFeatures; + +/** + * MPEG-2 TS video track renderer + */ +public class MpegTsVideoTrackRenderer extends MediaCodecVideoTrackRenderer { + + private static final int VIDEO_PLAYBACK_DEADLINE_IN_MS = 5000; + private static final int DROPPED_FRAMES_NOTIFICATION_THRESHOLD = 50; + private static final int MIN_HD_HEIGHT = 720; + private static final String MIMETYPE_MPEG2 = "video/mpeg2"; + + private final boolean mIsSwCodecEnabled; + private boolean mCodecIsSwPreferred; + + public MpegTsVideoTrackRenderer(Context context, SampleSource source, Handler handler, + MediaCodecVideoTrackRenderer.EventListener listener) { + super(context, source, MediaCodecSelector.DEFAULT, + MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, VIDEO_PLAYBACK_DEADLINE_IN_MS, handler, + listener, DROPPED_FRAMES_NOTIFICATION_THRESHOLD); + mIsSwCodecEnabled = CommonFeatures.USE_SW_CODEC_FOR_SD.isEnabled(context); + } + + @Override + protected DecoderInfo getDecoderInfo(MediaCodecSelector codecSelector, String mimeType, + boolean requiresSecureDecoder) throws MediaCodecUtil.DecoderQueryException { + try { + if (mIsSwCodecEnabled && mCodecIsSwPreferred) { + DecoderInfo swCodec = MediaSoftwareCodecUtil.getSoftwareDecoderInfo( + mimeType, requiresSecureDecoder); + if (swCodec != null) { + return swCodec; + } + } + } catch (MediaSoftwareCodecUtil.DecoderQueryException e) { + } + return super.getDecoderInfo(codecSelector, mimeType,requiresSecureDecoder); + } + + @Override + protected void onInputFormatChanged(MediaFormatHolder holder) throws ExoPlaybackException { + mCodecIsSwPreferred = MIMETYPE_MPEG2.equalsIgnoreCase(holder.format.mimeType) + && holder.format.height < MIN_HD_HEIGHT; + super.onInputFormatChanged(holder); + } +} diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/BaseSampleSourceExtractor.java b/usbtuner/src/com/android/usbtuner/exoplayer/PlaySampleExtractor.java index 69e44f21..e249e3cb 100644 --- a/usbtuner/src/com/android/usbtuner/exoplayer/BaseSampleSourceExtractor.java +++ b/usbtuner/src/com/android/usbtuner/exoplayer/PlaySampleExtractor.java @@ -19,37 +19,55 @@ package com.android.usbtuner.exoplayer; import android.media.MediaDataSource; import android.media.MediaExtractor; import android.os.ConditionVariable; +import android.os.SystemClock; import android.util.Log; -import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormatHolder; +import com.google.android.exoplayer.MediaFormatUtil; import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.TrackInfo; +import com.android.usbtuner.exoplayer.cache.CacheManager; +import com.android.usbtuner.exoplayer.cache.RecordingSampleBuffer; +import com.android.usbtuner.exoplayer.cache.SimpleSampleBuffer; +import com.android.usbtuner.tvinput.PlaybackCacheListener; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicLong; /** - * Base class for feeding samples from a given media extractor using a extractor thread. + * A class that plays a live stream from a given media extractor using an extractor thread. */ -public abstract class BaseSampleSourceExtractor implements SampleExtractor { - private static final String TAG = "BaseSampleSourceExt"; +public class PlaySampleExtractor implements SampleExtractor { + private static final String TAG = "PlaySampleExtractor"; // Maximum bandwidth of 1080p channel is about 2.2MB/s. 2MB for a sample will suffice. private static final int SAMPLE_BUFFER_SIZE = 1024 * 1024 * 2; + private static final AtomicLong ID_COUNTER = new AtomicLong(0); private final MediaDataSource mDataSource; private final MediaExtractor mMediaExtractor; private final ExtractorThread mExtractorThread; - private TrackInfo[] mTrackInfos; + private final CacheManager.SampleBuffer mSampleBuffer; + private final long mId; + private MediaFormat[] mTrackFormats; - private boolean mEos = false; private boolean mReleased = false; - public BaseSampleSourceExtractor(MediaDataSource source) { + public PlaySampleExtractor(MediaDataSource source, CacheManager cacheManager, + PlaybackCacheListener cacheListener, boolean useCache) { + mId = ID_COUNTER.incrementAndGet(); mDataSource = source; mMediaExtractor = new MediaExtractor(); mExtractorThread = new ExtractorThread(); + if (useCache) { + mSampleBuffer = new RecordingSampleBuffer(cacheManager, cacheListener, true, + RecordingSampleBuffer.CACHE_REASON_LIVE_PLAYBACK); + } else { + mSampleBuffer = new SimpleSampleBuffer(cacheListener); + } } private class ExtractorThread extends Thread { @@ -62,7 +80,7 @@ public abstract class BaseSampleSourceExtractor implements SampleExtractor { @Override public void run() { SampleHolder sample = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); - sample.replaceBuffer(SAMPLE_BUFFER_SIZE); + sample.ensureSpaceForWrite(SAMPLE_BUFFER_SIZE); ConditionVariable conditionVariable = new ConditionVariable(); while (!mQuitRequested) { fetchSample(sample, conditionVariable); @@ -75,7 +93,7 @@ public abstract class BaseSampleSourceExtractor implements SampleExtractor { if (index < 0) { Log.i(TAG, "EoS"); mQuitRequested = true; - setEos(); + mSampleBuffer.setEos(); return; } sample.data.clear(); @@ -95,7 +113,7 @@ public abstract class BaseSampleSourceExtractor implements SampleExtractor { queueSample(index, sample, conditionVariable); } catch (IOException e) { mQuitRequested = true; - setEos(); + mSampleBuffer.setEos(); } } @@ -104,10 +122,18 @@ public abstract class BaseSampleSourceExtractor implements SampleExtractor { } } - public abstract void queueSample(int index, SampleHolder sample, ConditionVariable - conditionVariable) throws IOException; + public void queueSample(int index, SampleHolder sample, ConditionVariable conditionVariable) + throws IOException { + long writeStartTimeNs = SystemClock.elapsedRealtimeNanos(); + mSampleBuffer.writeSample(index, sample, conditionVariable); - public void initOnPrepareLocked(int trackCount) throws IOException {} + // Check if the storage has enough bandwidth for trickplay. Otherwise we disable it + // and notify the slowness through the playback cache listener. + if (mSampleBuffer.isWriteSpeedSlow(sample.size, + SystemClock.elapsedRealtimeNanos() - writeStartTimeNs)) { + mSampleBuffer.handleWriteSpeedSlow(); + } + } @Override public boolean prepare() throws IOException { @@ -115,41 +141,63 @@ public abstract class BaseSampleSourceExtractor implements SampleExtractor { mMediaExtractor.setDataSource(mDataSource); int trackCount = mMediaExtractor.getTrackCount(); - initOnPrepareLocked(trackCount); - mTrackInfos = new TrackInfo[trackCount]; + mTrackFormats = new MediaFormat[trackCount]; for (int i = 0; i < trackCount; i++) { - android.media.MediaFormat format = mMediaExtractor.getTrackFormat(i); - long durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION) - ? format.getLong(android.media.MediaFormat.KEY_DURATION) - : C.UNKNOWN_TIME_US; - String mime = format.getString(android.media.MediaFormat.KEY_MIME); + mTrackFormats[i] = + MediaFormatUtil.createMediaFormat(mMediaExtractor.getTrackFormat(i)); mMediaExtractor.selectTrack(i); - mTrackInfos[i] = new TrackInfo(mime, durationUs); } + List<String> ids = new ArrayList<>(); + for (int i = 0; i < trackCount; i++) { + ids.add(String.format(Locale.ENGLISH, "%s_%x", Long.toHexString(mId), i)); + + } + mSampleBuffer.init(ids, null); + } mExtractorThread.start(); return true; } @Override - public synchronized TrackInfo[] getTrackInfos() { - return mTrackInfos; + public synchronized MediaFormat[] getTrackFormats() { + return mTrackFormats; } - private synchronized void setEos() { - mEos = true; + @Override + public void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder) { + outMediaFormatHolder.format = mTrackFormats[track]; + outMediaFormatHolder.drmInitData = null; } - public synchronized boolean getEos() { - return mEos; + @Override + public void selectTrack(int index) { + mSampleBuffer.selectTrack(index); } @Override - public void getTrackMediaFormat(int track, MediaFormatHolder mediaFormatHolder) { - mediaFormatHolder.format = - MediaFormat.createFromFrameworkMediaFormatV16(mMediaExtractor - .getTrackFormat(track)); - mediaFormatHolder.drmInitData = null; + public void deselectTrack(int index) { + mSampleBuffer.deselectTrack(index); + } + + @Override + public long getBufferedPositionUs() { + return mSampleBuffer.getBufferedPositionUs(); + } + + @Override + public boolean continueBuffering(long positionUs) { + return mSampleBuffer.continueBuffering(positionUs); + } + + @Override + public void seekTo(long positionUs) { + mSampleBuffer.seekTo(positionUs); + } + + @Override + public int readSample(int track, SampleHolder sampleHolder) { + return mSampleBuffer.readSample(track, sampleHolder); } @Override @@ -166,7 +214,9 @@ public abstract class BaseSampleSourceExtractor implements SampleExtractor { } } - public void cleanUpImpl() {} + public void cleanUpImpl() { + mSampleBuffer.release(); + } public synchronized void cleanUp() { if (!mReleased) { diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/RecordSampleSourceExtractor.java b/usbtuner/src/com/android/usbtuner/exoplayer/RecordSampleSourceExtractor.java deleted file mode 100644 index fa416c25..00000000 --- a/usbtuner/src/com/android/usbtuner/exoplayer/RecordSampleSourceExtractor.java +++ /dev/null @@ -1,328 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.usbtuner.exoplayer; - -import android.media.MediaCodec; -import android.media.MediaDataSource; -import android.media.MediaExtractor; -import android.os.ConditionVariable; -import android.os.SystemClock; -import android.util.Log; -import android.util.Pair; - -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.MediaFormatHolder; -import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.SampleSource; -import com.google.android.exoplayer.TrackInfo; -import com.google.android.exoplayer.TrackRenderer; -import com.google.android.exoplayer.util.MimeTypes; -import com.android.usbtuner.tvinput.PlaybackCacheListener; - -import java.io.IOException; -import java.util.Locale; -import java.util.Random; -import java.util.concurrent.TimeUnit; - -/** - * Records live streams on the disk for DVR. - * <p> - * For the convenience of testing, it implements {@link SampleExtractor}. - */ -public class RecordSampleSourceExtractor implements SampleExtractor, CacheManager.EvictListener { - // TODO: Decouple from {@link SampleExtractor}. Handle recording errors properly. - - private static final String TAG = "RecordSampleSourceExt"; - - // Maximum bandwidth of 1080p channel is about 2.2MB/s. 2MB for a sample will suffice. - private static final int SAMPLE_BUFFER_SIZE = 1024 * 1024 * 2; - - private final MediaDataSource mDataSource; - private final MediaExtractor mMediaExtractor; - private final ExtractorThread mExtractorThread; - private int mTrackCount; - private TrackInfo[] mTrackInfos; - private android.media.MediaFormat[] mMediaFormat; - - private boolean mEos = false; - private boolean mReleased = false; - - public static final long CHUNK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500); - - private static final long CACHE_WRITE_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10); // 10 seconds - - private final CacheManager mCacheManager; - private final String mId; - - private final PlaybackCacheListener mCacheListener; - private long[] mCacheEndPositionsUs; - private volatile long mCacheDurationUs = 0; - private SampleCache[] mSampleCaches; - private final SamplePool mSamplePool; - - public RecordSampleSourceExtractor(MediaDataSource source, CacheManager cacheManager, - PlaybackCacheListener cacheListener) { - mDataSource = source; - mMediaExtractor = new MediaExtractor(); - mExtractorThread = new ExtractorThread(); - mCacheManager = cacheManager; - mCacheListener = cacheListener; - mSamplePool = new SamplePool(); - // TODO: Use UUID afterwards. - mId = Long.toHexString(new Random().nextLong()); - cacheListener.onCacheStateChanged(true); // Enable trickplay - } - - private class ExtractorThread extends Thread { - private volatile boolean mQuitRequested = false; - - public ExtractorThread() { - super("ExtractorThread"); - } - - @Override - public void run() { - SampleHolder sample = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); - sample.replaceBuffer(SAMPLE_BUFFER_SIZE); - ConditionVariable conditionVariable = new ConditionVariable(); - while (!mQuitRequested) { - fetchSample(sample, conditionVariable); - } - cleanUp(); - } - - private void fetchSample(SampleHolder sample, ConditionVariable conditionVariable) { - int index = mMediaExtractor.getSampleTrackIndex(); - if (index < 0) { - Log.i(TAG, "EoS"); - mQuitRequested = true; - setEos(); - return; - } - sample.data.clear(); - sample.size = mMediaExtractor.readSampleData(sample.data, 0); - if (sample.size < 0 || sample.size > SAMPLE_BUFFER_SIZE) { - // Should not happen - Log.e(TAG, "Invalid sample size: " + sample.size); - mMediaExtractor.advance(); - return; - } - sample.data.position(sample.size); - sample.timeUs = mMediaExtractor.getSampleTime(); - if (sample.timeUs > mCacheDurationUs) { - mCacheDurationUs = sample.timeUs; - } - sample.flags = mMediaExtractor.getSampleFlags(); - - mMediaExtractor.advance(); - try { - queueSample(index, sample, conditionVariable); - } catch (IOException e) { - mQuitRequested = true; - setEos(); - } - } - - public void quit() { - mQuitRequested = true; - } - } - - private void queueSample(int index, SampleHolder sample, ConditionVariable conditionVariable) - throws IOException { - long writeStartTimeNs = SystemClock.elapsedRealtimeNanos(); - synchronized (this) { - SampleCache cache = mSampleCaches[index]; - if ((sample.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { - if (sample.timeUs >= mCacheEndPositionsUs[index]) { - try { - SampleCache nextCache = mCacheManager.createNewWriteFile( - getTrackId(index), mCacheEndPositionsUs[index], mSamplePool); - cache.finishWrite(nextCache); - mSampleCaches[index] = cache = nextCache; - mCacheEndPositionsUs[index] = - ((sample.timeUs / CHUNK_DURATION_US) + 1) * CHUNK_DURATION_US; - } catch (IOException e) { - cache.finishWrite(null); - throw e; - } - } - } - cache.writeSample(sample, conditionVariable); - } - if (!conditionVariable.block(CACHE_WRITE_TIMEOUT_MS)) { - Log.e(TAG, "Error: Serious delay on writing cache"); - conditionVariable.block(); - } - - // Check if the storage has enough bandwidth for recording. Otherwise we disable it - // and notify the slowness through the playback cache listener. - mCacheManager.addWriteStat(sample.size, - SystemClock.elapsedRealtimeNanos() - writeStartTimeNs); - if (mCacheManager.isWriteSlow()) { - Log.w(TAG, "Disk is too slow for trickplay. Disable trickplay."); - mCacheManager.disable(); - mCacheListener.onDiskTooSlow(); - } - } - - private String getTrackId(int index) { - return String.format(Locale.ENGLISH, "%s_%x", mId, index); - } - - private void initOnPrepareLocked(int trackCount) throws IOException { - mSampleCaches = new SampleCache[trackCount]; - mCacheEndPositionsUs = new long[trackCount]; - for (int i = 0; i < trackCount; i++) { - mSampleCaches[i] = mCacheManager.createNewWriteFile(getTrackId(i), 0, mSamplePool); - mCacheEndPositionsUs[i] = CHUNK_DURATION_US; - } - } - @Override - public boolean prepare() throws IOException { - synchronized (this) { - mMediaExtractor.setDataSource(mDataSource); - - mTrackCount = mMediaExtractor.getTrackCount(); - initOnPrepareLocked(mTrackCount); - mTrackInfos = new TrackInfo[mTrackCount]; - mMediaFormat = new android.media.MediaFormat[mTrackCount]; - for (int i = 0; i < mTrackCount; i++) { - android.media.MediaFormat format = mMediaExtractor.getTrackFormat(i); - long durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION) - ? format.getLong(android.media.MediaFormat.KEY_DURATION) - : C.UNKNOWN_TIME_US; - String mime = format.getString(android.media.MediaFormat.KEY_MIME); - mMediaExtractor.selectTrack(i); - mTrackInfos[i] = new TrackInfo(mime, durationUs); - mMediaFormat[i] = format; - } - } - mExtractorThread.start(); - return true; - } - - @Override - public synchronized TrackInfo[] getTrackInfos() { - return mTrackInfos; - } - - private synchronized void setEos() { - mEos = true; - } - - /** - * Notifies whether sample extraction met end of stream. - */ - public synchronized boolean getEos() { - return mEos; - } - - @Override - public void getTrackMediaFormat(int track, MediaFormatHolder mediaFormatHolder) { - mediaFormatHolder.format = - MediaFormat.createFromFrameworkMediaFormatV16(mMediaExtractor - .getTrackFormat(track)); - mediaFormatHolder.drmInitData = null; - } - - @Override - public void selectTrack(int index) { - } - - @Override - public void deselectTrack(int index) { - } - - @Override - public void seekTo(long positionUs) { - } - - @Override - public int readSample(int track, SampleHolder sampleHolder) { - return SampleSource.NOTHING_READ; - } - - private void cleanUpInternal() { - if (mSampleCaches == null) { - return; - } - for (int i = 0; i < mSampleCaches.length; ++i) { - mSampleCaches[i].finishWrite(null); - mCacheManager.unregisterEvictListener(getTrackId(i)); - } - if (mTrackCount > 0) { - Pair<String, android.media.MediaFormat> audio = null, video = null; - for (int i = 0; i < mTrackCount; ++i) { - String mime = mTrackInfos[i].mimeType; - mMediaFormat[i].setLong(android.media.MediaFormat.KEY_DURATION, mCacheDurationUs); - if (MimeTypes.isAudio(mime)) { - audio = new Pair<>(getTrackId(i), mMediaFormat[i]); - } - if (MimeTypes.isVideo(mime)) { - video = new Pair<>(getTrackId(i), mMediaFormat[i]); - } - } - mCacheManager.writeMetaFiles(audio, video); - } - for (int i = 0; i < mSampleCaches.length; ++i) { - mCacheManager.clearTrack(getTrackId(i)); - } - } - - @Override - public void release() { - synchronized (this) { - mReleased = true; - } - if (mExtractorThread.isAlive()) { - mExtractorThread.quit(); - - // We don't join here to prevent hang --- MediaExtractor is released at the thread. - } else { - cleanUp(); - } - } - - private synchronized void cleanUp() { - if (!mReleased) { - return; - } - cleanUpInternal(); - mMediaExtractor.release(); - } - - @Override - public long getBufferedPositionUs() { - // This will make player keep alive with no-op. - return TrackRenderer.UNKNOWN_TIME_US; - } - - @Override - public boolean continueBuffering(long positionUs) { - // This will make player keep alive with no-op. - return true; - } - - // CacheEvictListener - @Override - public void onCacheEvicted(String id, long createdTimeMs) { - mCacheListener.onCacheStartTimeChanged( - createdTimeMs + TimeUnit.MICROSECONDS.toMillis(CHUNK_DURATION_US)); - } -} diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/Recorder.java b/usbtuner/src/com/android/usbtuner/exoplayer/Recorder.java new file mode 100644 index 00000000..d7145f26 --- /dev/null +++ b/usbtuner/src/com/android/usbtuner/exoplayer/Recorder.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.usbtuner.exoplayer; + +import android.media.MediaDataSource; +import android.media.MediaExtractor; +import android.os.ConditionVariable; +import android.os.SystemClock; +import android.util.Log; + +import com.google.android.exoplayer.SampleHolder; +import com.android.usbtuner.exoplayer.cache.CacheManager; +import com.android.usbtuner.exoplayer.cache.RecordingSampleBuffer; +import com.android.usbtuner.tvinput.PlaybackCacheListener; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Records live streams on the disk for DVR. + */ +public class Recorder { + private static final String TAG = "Recorder"; + + // Maximum bandwidth of 1080p channel is about 2.2MB/s. 2MB for a sample will suffice. + private static final int SAMPLE_BUFFER_SIZE = 1024 * 1024 * 2; + private static final AtomicLong ID_COUNTER = new AtomicLong(0); + + private final MediaDataSource mDataSource; + private final MediaExtractor mMediaExtractor; + private final ExtractorThread mExtractorThread; + private int mTrackCount; + private List<android.media.MediaFormat> mMediaFormats; + + private final CacheManager.SampleBuffer mSampleBuffer; + + private boolean mReleased = false; + private boolean mResultNotified = false; + private final long mId; + + private final RecordListener mRecordListener; + + /** + * Listeners for events which happens during the recording. + */ + public interface RecordListener { + + /** + * Notifies recording completion. + * + * @param success {@code true} when the recording succeeded, {@code false} otherwise + */ + void notifyRecordingFinished(boolean success); + } + + /** + * Create a recorder for a {@link android.media.MediaDataSource}. + * + * @param source {@link android.media.MediaDataSource} to record from + * @param cacheManager the manager for recording samples to physical storage + * @param cacheListener the {@link com.android.usbtuner.tvinput.PlaybackCacheListener} + * to notify cache storage status change + * @param recordListener RecordListener to notify events during the recording + */ + public Recorder(MediaDataSource source, CacheManager cacheManager, + PlaybackCacheListener cacheListener, RecordListener recordListener) { + mDataSource = source; + mMediaExtractor = new MediaExtractor(); + mExtractorThread = new ExtractorThread(); + mRecordListener = recordListener; + + mSampleBuffer = new RecordingSampleBuffer(cacheManager, cacheListener, false, + RecordingSampleBuffer.CACHE_REASON_RECORDING); + mId = ID_COUNTER.incrementAndGet(); + } + + private class ExtractorThread extends Thread { + private volatile boolean mQuitRequested = false; + + public ExtractorThread() { + super("ExtractorThread"); + } + + @Override + public void run() { + SampleHolder sample = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); + sample.ensureSpaceForWrite(SAMPLE_BUFFER_SIZE); + ConditionVariable conditionVariable = new ConditionVariable(); + while (!mQuitRequested) { + fetchSample(sample, conditionVariable); + } + cleanUp(); + } + + private void fetchSample(SampleHolder sample, ConditionVariable conditionVariable) { + int index = mMediaExtractor.getSampleTrackIndex(); + if (index < 0) { + Log.i(TAG, "EoS"); + mQuitRequested = true; + mSampleBuffer.setEos(); + return; + } + sample.data.clear(); + sample.size = mMediaExtractor.readSampleData(sample.data, 0); + if (sample.size < 0 || sample.size > SAMPLE_BUFFER_SIZE) { + // Should not happen + Log.e(TAG, "Invalid sample size: " + sample.size); + mMediaExtractor.advance(); + return; + } + sample.data.position(sample.size); + sample.timeUs = mMediaExtractor.getSampleTime(); + sample.flags = mMediaExtractor.getSampleFlags(); + + mMediaExtractor.advance(); + try { + queueSample(index, sample, conditionVariable); + } catch (IOException e) { + mQuitRequested = true; + mSampleBuffer.setEos(); + } + } + + public void quit() { + mQuitRequested = true; + } + } + + private void queueSample(int index, SampleHolder sample, ConditionVariable conditionVariable) + throws IOException { + long writeStartTimeNs = SystemClock.elapsedRealtimeNanos(); + mSampleBuffer.writeSample(index, sample, conditionVariable); + + // Check if the storage has enough bandwidth for recording. Otherwise we disable it + // and notify the slowness. + if (mSampleBuffer.isWriteSpeedSlow(sample.size, + SystemClock.elapsedRealtimeNanos() - writeStartTimeNs)) { + Log.w(TAG, "Disk is too slow for trickplay. Disable trickplay."); + throw new IOException("Disk is too slow"); + } + } + + /** + * Prepares a recording. + * + * @return {@code true} when preparation finished successfully, {@code false} otherwise + * @throws IOException + */ + public boolean prepare() throws IOException { + synchronized (this) { + mMediaExtractor.setDataSource(mDataSource); + + mTrackCount = mMediaExtractor.getTrackCount(); + List<String> ids = new ArrayList<>(); + mMediaFormats = new ArrayList<>(); + for (int i = 0; i < mTrackCount; i++) { + ids.add(String.format(Locale.ENGLISH, "%s_%x", Long.toHexString(mId), i)); + android.media.MediaFormat format = mMediaExtractor.getTrackFormat(i); + mMediaExtractor.selectTrack(i); + mMediaFormats.add(format); + } + mSampleBuffer.init(ids, mMediaFormats); + } + mExtractorThread.start(); + return true; + } + + /** + * Releases all the resources which were used in the recording. + */ + public void release() { + synchronized (this) { + mReleased = true; + } + if (mExtractorThread.isAlive()) { + mExtractorThread.quit(); + // We don't join here to prevent hang --- MediaExtractor is released at the thread. + } else { + cleanUp(); + } + } + + private synchronized void cleanUp() { + if (!mReleased) { + if (!mResultNotified) { + mRecordListener.notifyRecordingFinished(false); + mResultNotified = true; + } + return; + } + mSampleBuffer.release(); + if (!mResultNotified) { + mRecordListener.notifyRecordingFinished(true); + mResultNotified = true; + } + mMediaExtractor.release(); + } + +} diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/ReplaySampleSourceExtractor.java b/usbtuner/src/com/android/usbtuner/exoplayer/ReplaySampleSourceExtractor.java index 603f1e68..bbde8863 100644 --- a/usbtuner/src/com/android/usbtuner/exoplayer/ReplaySampleSourceExtractor.java +++ b/usbtuner/src/com/android/usbtuner/exoplayer/ReplaySampleSourceExtractor.java @@ -16,118 +16,44 @@ package com.android.usbtuner.exoplayer; -import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormatHolder; +import com.google.android.exoplayer.MediaFormatUtil; import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.SampleSource; -import com.google.android.exoplayer.TrackInfo; +import com.android.usbtuner.exoplayer.cache.CacheManager; +import com.android.usbtuner.exoplayer.cache.RecordingSampleBuffer; import com.android.usbtuner.tvinput.PlaybackCacheListener; -import junit.framework.Assert; - -import android.util.Log; import android.util.Pair; import java.io.IOException; import java.util.ArrayList; -import java.util.concurrent.TimeUnit; +import java.util.List; /** * A class that plays a recorded stream without using {@link MediaExtractor}, * since all samples are extracted and stored to the permanent storage already. */ -public class ReplaySampleSourceExtractor implements SampleExtractor, CacheManager.EvictListener { +public class ReplaySampleSourceExtractor implements SampleExtractor{ private static final String TAG = "ReplaySampleSourceExt"; private static final boolean DEBUG = false; - public static final long CHUNK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500); - + private int mTrackCount; private android.media.MediaFormat[] mMediaFormats; - private TrackInfo[] mTrackInfos; - private String[] mIds; + private MediaFormat[] mTrackFormats; - private boolean mEos; private boolean mReleased; private final CacheManager mCacheManager; - private final PlaybackCacheListener mCacheListener; - private CachedSampleQueue[] mPlayingSampleQueues; - private final SamplePool mSamplePool = new SamplePool(); - private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US; - private long mCurrentPlaybackPositionUs = 0; - - private class CachedSampleQueue extends SampleQueue { - private SampleCache mCache = null; - - public CachedSampleQueue(SamplePool samplePool) { - super(samplePool); - } - - public void setSource(SampleCache newCache) { - for (SampleCache cache = mCache; cache != null; cache = cache.getNext()) { - cache.clear(); - cache.close(); - } - mCache = newCache; - for (SampleCache cache = mCache; cache != null; cache = cache.getNext()) { - cache.resetRead(); - } - } - - public boolean maybeReadSample() { - if (isDurationGreaterThan(CHUNK_DURATION_US)) { - return false; - } - SampleHolder sample = mCache.maybeReadSample(); - if (sample == null) { - if (!mCache.canReadMore()) { - if (mCache.getNext() == null) { - // reached the end of the recording - setEos(); - return false; - } else { - mCache.clear(); - mCache.close(); - mCache = mCache.getNext(); - mCache.resetRead(); - return maybeReadSample(); - } - } - return false; - } else { - queueSample(sample); - return true; - } - } - - public int dequeueSample(SampleHolder sample) { - maybeReadSample(); - return super.dequeueSample(sample); - } - - @Override - public void clear() { - super.clear(); - for (SampleCache cache = mCache; cache != null; cache = cache.getNext()) { - cache.clear(); - cache.close(); - } - mCache = null; - } - - public long getSourceStartPositionUs() { - return mCache == null ? -1 : mCache.getStartPositionUs(); - } - } + private CacheManager.SampleBuffer mSampleBuffer; public ReplaySampleSourceExtractor( CacheManager cacheManager, PlaybackCacheListener cacheListener) { mCacheManager = cacheManager; mCacheListener = cacheListener; - cacheListener.onCacheStateChanged(true); // Enable trickplay + mTrackCount = -1; } @Override @@ -137,175 +63,69 @@ public class ReplaySampleSourceExtractor implements SampleExtractor, CacheManage if (trackInfos == null || trackInfos.size() <= 0) { return false; } - int trackCount = trackInfos.size(); - mIds = new String[trackCount]; - mMediaFormats = new android.media.MediaFormat[trackCount]; - mTrackInfos = new TrackInfo[trackCount]; - for (int i = 0; i < trackCount; ++i) { + mTrackCount = trackInfos.size(); + List<String> ids = new ArrayList<>(); + mMediaFormats = new android.media.MediaFormat[mTrackCount]; + mTrackFormats = new MediaFormat[mTrackCount]; + for (int i = 0; i < mTrackCount; ++i) { Pair<String, android.media.MediaFormat> pair = trackInfos.get(i); - mIds[i] = pair.first; + ids.add(pair.first); mMediaFormats[i] = pair.second; - - // TODO: save this according to recording length - long durationUs = mMediaFormats[i].containsKey(android.media.MediaFormat.KEY_DURATION) - ? mMediaFormats[i].getLong(android.media.MediaFormat.KEY_DURATION) - : C.UNKNOWN_TIME_US; - String mime = mMediaFormats[i].getString(android.media.MediaFormat.KEY_MIME); - mTrackInfos[i] = new TrackInfo(mime, durationUs); + mTrackFormats[i] = MediaFormatUtil.createMediaFormat(mMediaFormats[i]); } - initOnLoad(trackCount); + mSampleBuffer = new RecordingSampleBuffer(mCacheManager, mCacheListener, true, + RecordingSampleBuffer.CACHE_REASON_RECORDED_PLAYBACK); + mSampleBuffer.init(ids, null); return true; } @Override - public TrackInfo[] getTrackInfos() { - return mTrackInfos; - } - - private void setEos() { - mEos = true; - } - - public boolean getEos() { - return mEos; + public MediaFormat[] getTrackFormats() { + return mTrackFormats; } @Override - public void getTrackMediaFormat(int track, MediaFormatHolder mediaFormatHolder) { - mediaFormatHolder.format = - MediaFormat.createFromFrameworkMediaFormatV16(mMediaFormats[track]); - mediaFormatHolder.drmInitData = null; + public void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder) { + outMediaFormatHolder.format = mTrackFormats[track]; + outMediaFormatHolder.drmInitData = null; } @Override public void release() { if (!mReleased) { - cleanUpImpl(); + mSampleBuffer.release(); } mReleased = true; } - - private String getTrackId(int index) { - return mIds[index]; - } - - public void initOnLoad(int trackCount) throws IOException { - mPlayingSampleQueues = new CachedSampleQueue[trackCount]; - for (int i = 0; i < trackCount; i++) { - mCacheManager.loadTrackFormStorage(mIds[i], mSamplePool); - } - } - @Override public void selectTrack(int index) { - if (mPlayingSampleQueues[index] == null) { - String trackId = getTrackId(index); - mPlayingSampleQueues[index] = new CachedSampleQueue(mSamplePool); - mCacheManager.registerEvictListener(trackId, this); - seekIndividualTrack(index, mCurrentPlaybackPositionUs); - mPlayingSampleQueues[index].maybeReadSample(); - } + mSampleBuffer.selectTrack(index); } @Override public void deselectTrack(int index) { - if (mPlayingSampleQueues[index] != null) { - mPlayingSampleQueues[index].clear(); - mPlayingSampleQueues[index] = null; - mCacheManager.unregisterEvictListener(getTrackId(index)); - } + mSampleBuffer.deselectTrack(index); } @Override public long getBufferedPositionUs() { - Long result = null; - for (CachedSampleQueue queue : mPlayingSampleQueues) { - if (queue == null) { - continue; - } - Long bufferedPositionUs = queue.getEndPositionUs(); - if (bufferedPositionUs == null) { - continue; - } - if (result == null || result > bufferedPositionUs) { - result = bufferedPositionUs; - } - } - if (result == null) { - return mLastBufferedPositionUs; - } else { - return (mLastBufferedPositionUs = result); - } + return mSampleBuffer.getBufferedPositionUs(); } @Override public void seekTo(long positionUs) { - // Seek video track first - for (int i = 0; i < mPlayingSampleQueues.length; ++i) { - CachedSampleQueue queue = mPlayingSampleQueues[i]; - if (queue == null) { - continue; - } - seekIndividualTrack(i, positionUs); - if (DEBUG) { - Log.d(TAG, "start time = " + queue.getSourceStartPositionUs()); - } - } - mLastBufferedPositionUs = positionUs; - } - - private void seekIndividualTrack(int index, long positionUs) { - CachedSampleQueue queue = mPlayingSampleQueues[index]; - if (queue == null) { - return; - } - queue.clear(); - queue.setSource(mCacheManager.getReadFile(getTrackId(index), positionUs)); - queue.maybeReadSample(); + mSampleBuffer.seekTo(positionUs); } @Override public int readSample(int track, SampleHolder sampleHolder) { - CachedSampleQueue queue = mPlayingSampleQueues[track]; - Assert.assertNotNull(queue); - queue.maybeReadSample(); - int result = queue.dequeueSample(sampleHolder); - if (result != SampleSource.SAMPLE_READ && getEos()) { - return SampleSource.END_OF_STREAM; - } - return result; + return mSampleBuffer.readSample(track, sampleHolder); } - public void cleanUpImpl() { - mCacheManager.close(); - for (int i = 0; i < mIds.length; ++i) { - mCacheManager.unregisterEvictListener(getTrackId(i)); - mCacheManager.clearTrack(getTrackId(i)); - } - } @Override public boolean continueBuffering(long positionUs) { - boolean hasSamples = true; - mCurrentPlaybackPositionUs = positionUs; - for (CachedSampleQueue queue : mPlayingSampleQueues) { - if (queue == null) { - continue; - } - queue.maybeReadSample(); - if (queue.isEmpty()) { - hasSamples = false; - } - } - return hasSamples; - } - - // CacheEvictListener - // TODO: Remove this. It will not be called. - @Override - public void onCacheEvicted(String id, long createdTimeMs) { - mCacheListener.onCacheStartTimeChanged( - createdTimeMs + TimeUnit.MICROSECONDS.toMillis(CHUNK_DURATION_US)); + return mSampleBuffer.continueBuffering(positionUs); } } diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/SampleExtractor.java b/usbtuner/src/com/android/usbtuner/exoplayer/SampleExtractor.java index b6559ad1..1a2e55d4 100644 --- a/usbtuner/src/com/android/usbtuner/exoplayer/SampleExtractor.java +++ b/usbtuner/src/com/android/usbtuner/exoplayer/SampleExtractor.java @@ -19,7 +19,6 @@ import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; -import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.TrackRenderer; import java.io.IOException; @@ -28,7 +27,7 @@ import java.io.IOException; * Extractor for reading track metadata and samples stored in tracks. * * <p>Call {@link #prepare} until it returns {@code true}, then access track metadata via - * {@link #getTrackInfos} and {@link #getTrackMediaFormat}. + * {@link #getTrackFormats} and {@link #getTrackMediaFormat}. * * <p>Pass indices of tracks to read from to {@link #selectTrack}. A track can later be deselected * by calling {@link #deselectTrack}. It is safe to select/deselect tracks after reading sample @@ -48,7 +47,7 @@ public interface SampleExtractor { boolean prepare() throws IOException; /** Returns track information about all tracks that can be selected. */ - TrackInfo[] getTrackInfos(); + MediaFormat[] getTrackFormats(); /** Selects the track at {@code index} for reading sample data. */ void selectTrack(int index); @@ -77,7 +76,7 @@ public interface SampleExtractor { void seekTo(long positionUs); /** Stores the {@link MediaFormat} of {@code track}. */ - void getTrackMediaFormat(int track, MediaFormatHolder mediaFormatHolder); + void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder); /** * Reads the next sample in the track at index {@code track} into {@code sampleHolder}, returning @@ -92,9 +91,8 @@ public interface SampleExtractor { * {@link SampleSource#END_OF_STREAM} if the last samples in all tracks have been read, or * {@link SampleSource#NOTHING_READ} if the sample cannot be read immediately as it is not * loaded. - * @throws {@link IOException} thrown if the source can't be read */ - int readSample(int track, SampleHolder sampleHolder) throws IOException; + int readSample(int track, SampleHolder sampleHolder); /** Releases resources associated with this extractor. */ void release(); diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/ac3/Ac3TrackRenderer.java b/usbtuner/src/com/android/usbtuner/exoplayer/ac3/Ac3TrackRenderer.java index 0e11b6f2..05327dba 100644 --- a/usbtuner/src/com/android/usbtuner/exoplayer/ac3/Ac3TrackRenderer.java +++ b/usbtuner/src/com/android/usbtuner/exoplayer/ac3/Ac3TrackRenderer.java @@ -22,9 +22,11 @@ import android.util.Log; import com.google.android.exoplayer.CodecCounters; import com.google.android.exoplayer.ExoPlaybackException; +import com.google.android.exoplayer.MediaClock; import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormatHolder; +import com.google.android.exoplayer.MediaFormatUtil; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackRenderer; @@ -39,7 +41,8 @@ import java.nio.ByteBuffer; /** * Decodes and renders AC3 audio. */ -public class Ac3TrackRenderer extends TrackRenderer implements Ac3Decoder.DecodeListener { +public class Ac3TrackRenderer extends TrackRenderer implements Ac3Decoder.DecodeListener, + MediaClock { public static final int MSG_SET_VOLUME = MediaCodecAudioTrackRenderer.MSG_SET_VOLUME; public static final int MSG_SET_AUDIO_TRACK = MSG_SET_VOLUME + 1; @@ -80,14 +83,14 @@ public class Ac3TrackRenderer extends TrackRenderer implements Ac3Decoder.Decode private static final long ESTIMATED_TRACK_RENDERING_DELAY_US = 500000; private final CodecCounters mCodecCounters; - private final SampleSource mSource; + private final SampleSource.SampleSourceReader mSource; private final SampleHolder mSampleHolder; private final MediaFormatHolder mFormatHolder; private final EventListener mEventListener; private final Handler mEventHandler; private final boolean mIsSoftware; private final AudioTrackMonitor mMonitor; - private final MediaClock mMediaClock; + private final AudioClock mAudioClock; private MediaFormat mFormat; private Ac3Decoder mDecoder; @@ -106,52 +109,62 @@ public class Ac3TrackRenderer extends TrackRenderer implements Ac3Decoder.Decode public Ac3TrackRenderer(SampleSource source, Handler eventHandler, EventListener listener, boolean isSoftware) { - mSource = source; + mSource = source.register(); mEventHandler = eventHandler; mEventListener = listener; mDecoder = Ac3Decoder.createAc3Decoder(isSoftware); mIsSoftware = isSoftware; + mTrackIndex = -1; mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT); - mSampleHolder.replaceBuffer(DEFAULT_INPUT_BUFFER_SIZE); + mSampleHolder.ensureSpaceForWrite(DEFAULT_INPUT_BUFFER_SIZE); mOutputBuffer = ByteBuffer.allocate(DEFAULT_OUTPUT_BUFFER_SIZE); mFormatHolder = new MediaFormatHolder(); AUDIO_TRACK.restart(); mCodecCounters = new CodecCounters(); mMonitor = new AudioTrackMonitor(); - mMediaClock = new MediaClock(); + mAudioClock = new AudioClock(); } @Override - protected boolean isTimeSource() { - return true; + protected MediaClock getMediaClock() { + return this; } private static boolean handlesMimeType(String mimeType) { - return mimeType.equals(MimeTypes.AUDIO_AC3) || mimeType.equals(MimeTypes.AUDIO_EC3); + return mimeType.equals(MimeTypes.AUDIO_AC3) || mimeType.equals(MimeTypes.AUDIO_E_AC3); } @Override - protected int doPrepare(long positionUs) throws ExoPlaybackException { - try { - boolean sourcePrepared = mSource.prepare(positionUs); - if (!sourcePrepared) { - return TrackRenderer.STATE_UNPREPARED; - } - } catch (IOException e) { - throw new ExoPlaybackException(e); + protected boolean doPrepare(long positionUs) throws ExoPlaybackException { + boolean sourcePrepared = mSource.prepare(positionUs); + if (!sourcePrepared) { + return false; } for (int i = 0; i < mSource.getTrackCount(); i++) { - if (handlesMimeType(mSource.getTrackInfo(i).mimeType)) { + if (handlesMimeType(mSource.getFormat(i).mimeType)) { mTrackIndex = i; - return TrackRenderer.STATE_PREPARED; + return true; } } - return TrackRenderer.STATE_IGNORE; + // TODO: Check this case. Source does not have the proper mime type. + return true; + } + + @Override + protected int getTrackCount() { + return mTrackIndex < 0 ? 0 : 1; + } + + @Override + protected MediaFormat getFormat(int track) { + Assertions.checkArgument(mTrackIndex != -1 && track == 0); + return mSource.getFormat(mTrackIndex); } @Override - protected void onEnabled(long positionUs, boolean joining) { + protected void onEnabled(int track, long positionUs, boolean joining) { + Assertions.checkArgument(mTrackIndex != -1 && track == 0); mSource.enable(mTrackIndex, positionUs); mDecoder.startDecoder(this); seekToInternal(positionUs); @@ -171,7 +184,6 @@ public class Ac3TrackRenderer extends TrackRenderer implements Ac3Decoder.Decode mSource.release(); } - @Override protected boolean isEnded() { return mOutputStreamEnded && AUDIO_TRACK.isEnded(); @@ -192,7 +204,7 @@ public class Ac3TrackRenderer extends TrackRenderer implements Ac3Decoder.Decode mPreviousPositionUs = 0; mCurrentPositionUs = Long.MIN_VALUE; mInterpolatedTimeUs = Long.MIN_VALUE; - mMediaClock.setPositionUs(positionUs); + mAudioClock.setPositionUs(positionUs); } @Override @@ -209,13 +221,22 @@ public class Ac3TrackRenderer extends TrackRenderer implements Ac3Decoder.Decode @Override protected void onStarted() { AUDIO_TRACK.play(); - mMediaClock.start(); + mAudioClock.start(); } @Override protected void onStopped() { AUDIO_TRACK.pause(); - mMediaClock.stop(); + mAudioClock.stop(); + } + + @Override + protected void maybeThrowError() throws ExoPlaybackException { + try { + mSource.maybeThrowError(); + } catch (IOException e) { + throw new ExoPlaybackException(e); + } } @Override @@ -231,7 +252,7 @@ public class Ac3TrackRenderer extends TrackRenderer implements Ac3Decoder.Decode throw new ExoPlaybackException("Much time has elapsed after EoS"); } } - boolean continueBuffering = mSource.continueBuffering(positionUs); + boolean continueBuffering = mSource.continueBuffering(mTrackIndex, positionUs); if (mSourceStateReady != continueBuffering) { mSourceStateReady = continueBuffering; if (DEBUG) { @@ -289,7 +310,7 @@ public class Ac3TrackRenderer extends TrackRenderer implements Ac3Decoder.Decode private void readFormat() throws IOException, ExoPlaybackException { int result = mSource.readData(mTrackIndex, mCurrentPositionUs, - mFormatHolder, mSampleHolder, false); + mFormatHolder, mSampleHolder); if (result == SampleSource.FORMAT_READ) { onInputFormatChanged(mFormatHolder); } @@ -299,9 +320,8 @@ public class Ac3TrackRenderer extends TrackRenderer implements Ac3Decoder.Decode throws ExoPlaybackException { MediaFormat format = formatHolder.format; if (mIsSoftware) { - mFormat = MediaFormat.createAudioFormat(MimeTypes.AUDIO_RAW, - MediaFormat.NO_VALUE, format.durationUs, - format.channelCount, format.sampleRate, null); + mFormat = MediaFormatUtil.createAudioMediaFormat(MimeTypes.AUDIO_RAW, format.durationUs, + format.channelCount, format.sampleRate); } else { mFormat = format; } @@ -317,21 +337,25 @@ public class Ac3TrackRenderer extends TrackRenderer implements Ac3Decoder.Decode return false; } + long discontinuity = mSource.readDiscontinuity(mTrackIndex); + if (discontinuity != SampleSource.NO_DISCONTINUITY) { + // TODO: handle input discontinuity for trickplay. + Log.i(TAG, "Read discontinuity happened"); + AUDIO_TRACK.handleDiscontinuity(); + mPresentationTimeUs = discontinuity; + mPresentationCount = 0; + clearDecodeState(); + return false; + } + mSampleHolder.data.clear(); mSampleHolder.size = 0; int result = mSource.readData(mTrackIndex, mPresentationTimeUs, mFormatHolder, - mSampleHolder, false); + mSampleHolder); switch (result) { case SampleSource.NOTHING_READ: { return false; } - case SampleSource.DISCONTINUITY_READ: { - // TODO: handle input discontinuity for trickplay. - Log.i(TAG, "Read discontinuity happened"); - AUDIO_TRACK.handleDiscontinuity(); - clearDecodeState(); - return true; - } case SampleSource.FORMAT_READ: { Log.i(TAG, "Format was read again"); onInputFormatChanged(mFormatHolder); @@ -390,20 +414,20 @@ public class Ac3TrackRenderer extends TrackRenderer implements Ac3Decoder.Decode @Override protected long getDurationUs() { - return mSource.getTrackInfo(mTrackIndex).durationUs; + return mSource.getFormat(mTrackIndex).durationUs; } @Override protected long getBufferedPositionUs() { long pos = mSource.getBufferedPositionUs(); return pos == UNKNOWN_TIME_US || pos == END_OF_TRACK_US - ? pos : Math.max(pos, getCurrentPositionUs()); + ? pos : Math.max(pos, getPositionUs()); } @Override - protected long getCurrentPositionUs() { + public long getPositionUs() { if (!AUDIO_TRACK.isInitialized()) { - return mMediaClock.getPositionUs(); + return mAudioClock.getPositionUs(); } if (!AUDIO_TRACK.isEnabled()) { if (mInterpolatedTimeUs > 0) { return mInterpolatedTimeUs - ESTIMATED_TRACK_RENDERING_DELAY_US; diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/ac3/MediaClock.java b/usbtuner/src/com/android/usbtuner/exoplayer/ac3/AudioClock.java index 5d28f4ef..ae6f2e91 100644 --- a/usbtuner/src/com/android/usbtuner/exoplayer/ac3/MediaClock.java +++ b/usbtuner/src/com/android/usbtuner/exoplayer/ac3/AudioClock.java @@ -25,7 +25,7 @@ import android.os.SystemClock; * its time can be set and retrieved. When started, this clock is based on * {@link SystemClock#elapsedRealtime()}. */ -/* package */ class MediaClock { +/* package */ class AudioClock { private boolean mStarted; /** diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/ac3/AudioTrackWrapper.java b/usbtuner/src/com/android/usbtuner/exoplayer/ac3/AudioTrackWrapper.java index e075dcc0..eb5efcb9 100644 --- a/usbtuner/src/com/android/usbtuner/exoplayer/ac3/AudioTrackWrapper.java +++ b/usbtuner/src/com/android/usbtuner/exoplayer/ac3/AudioTrackWrapper.java @@ -83,14 +83,12 @@ public class AudioTrackWrapper { if (!mIsEnabled) { return true; } - return !mAudioTrack.hasPendingData() || !mAudioTrack.hasEnoughDataToBeginPlayback(); + return !mAudioTrack.hasPendingData(); } public boolean isReady() { - if (!mIsEnabled) { - return false; - } - return mAudioTrack.hasPendingData(); + // In the case of not playing actual audio data, Audio track is always ready. + return !mIsEnabled || mAudioTrack.hasPendingData(); } public void play() { @@ -118,7 +116,15 @@ public class AudioTrackWrapper { if (!mIsEnabled) { return; } - mAudioTrack.reconfigure(format); + // TODO: Handle non-AC3 or non-passthrough audio. + if (MediaFormat.MIMETYPE_AUDIO_AC3.equalsIgnoreCase(format.getString(MediaFormat.KEY_MIME)) + && format.getInteger(MediaFormat.KEY_CHANNEL_COUNT) == 1) { + // Workarounds b/25955476 . + // Since all devices and platforms does not support AC3 mono passthrough, + // It is safe to fake AC3 mono as AC3 stereo which is default passthrough mode. + format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 2); + } + mAudioTrack.configure(format, true); } public void handleDiscontinuity() { diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/CacheManager.java b/usbtuner/src/com/android/usbtuner/exoplayer/cache/CacheManager.java index c52a0a44..0441f288 100644 --- a/usbtuner/src/com/android/usbtuner/exoplayer/CacheManager.java +++ b/usbtuner/src/com/android/usbtuner/exoplayer/cache/CacheManager.java @@ -14,23 +14,28 @@ * limitations under the License. */ -package com.android.usbtuner.exoplayer; +package com.android.usbtuner.exoplayer.cache; import android.media.MediaFormat; +import android.os.ConditionVariable; import android.os.HandlerThread; -import android.os.Looper; import android.support.annotation.VisibleForTesting; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import android.util.Pair; +import com.google.android.exoplayer.SampleHolder; + import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; @@ -51,6 +56,7 @@ public class CacheManager { // Constants for the disk write speed checking private static final long MINIMUM_WRITE_SIZE_FOR_SPEED_CHECK = 10L * 1024 * 1024; // Checks for every 10M disk write + private static final int MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK = 15 * 1024; private static final int MAXIMUM_SPEED_CHECK_COUNT = 5; // Checks only 5 times private static final int MINIMUM_DISK_WRITE_SPEED_MBPS = 3; // 3 Megabytes per second @@ -75,6 +81,7 @@ public class CacheManager { }; private volatile boolean mClosed = false; + private int mMinSampleSizeForSpeedCheck = MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK; private long mTotalWriteSize; private long mTotalWriteTimeNs; private volatile int mSpeedCheckCount; @@ -90,6 +97,88 @@ public class CacheManager { } /** + * Handles I/O + * between CacheManager and {@link com.android.usbtuner.exoplayer.SampleExtractor}. + */ + public interface SampleBuffer { + + /** + * Initializes SampleBuffer. + * @param Ids track identifiers for storage read/write. + * @param mediaFormats meta-data for each track, this will be saved to storage in recording. + * @throws IOException + */ + void init(@NonNull List<String> Ids, @Nullable List<MediaFormat> mediaFormats) + throws IOException; + + /** + * Selects the track {@code index} for reading sample data. + */ + void selectTrack(int index); + + /** + * Deselects the track at {@code index}, + * so that no more samples will be read from the track. + */ + void deselectTrack(int index); + + /** + * Writes sample to storage. + * + * @param index track index + * @param sample sample to write at storage + * @param conditionVariable notifies the completion of writing sample. + * @throws IOException + */ + void writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable) + throws IOException; + + /** + * Checks whether storage write speed is slow. + */ + boolean isWriteSpeedSlow(int sampleSize, long writeDurationNs); + + /** + * Handles when write speed is slow. + */ + void handleWriteSpeedSlow(); + + /** + * Sets the flag when EoS was met. + */ + void setEos(); + + /** + * Reads the next sample in the track at index {@code track} into {@code sampleHolder}, + * returning {@link com.google.android.exoplayer.SampleSource#SAMPLE_READ} + * if it is available. + * If the next sample is not available, + * returns {@link com.google.android.exoplayer.SampleSource#NOTHING_READ}. + */ + int readSample(int index, SampleHolder outSample); + + /** + * Seeks to the specified time in microseconds. + */ + void seekTo(long positionUs); + + /** + * Returns an estimate of the position up to which data is buffered. + */ + long getBufferedPositionUs(); + + /** + * Returns whether there is buffered data. + */ + boolean continueBuffering(long positionUs); + + /** + * Cleans up and releases everything. + */ + void release(); + } + + /** * Storage configuration and policy manager for {@link CacheManager} */ public interface StorageManager { @@ -251,8 +340,7 @@ public class CacheManager { } /** - * Loads a track using - * {@link com.android.usbtuner.exoplayer.CacheManager.StorageManager}. + * Loads a track using {@link CacheManager.StorageManager}. * * @param trackId the name of the track. * @param samplePool {@link SamplePool} for the fast creation of samples. @@ -347,8 +435,7 @@ public class CacheManager { /** * Reads track information which includes {@link MediaFormat}. * - * @return returns all track information which is found by - * {@link com.android.usbtuner.exoplayer.CacheManager.StorageManager}. + * @return returns all track information which is found by {@link CacheManager.StorageManager}. * @throws {@link java.io.IOException} */ public ArrayList<Pair<String, MediaFormat>> readTrackInfoFiles() throws IOException { @@ -438,8 +525,10 @@ public class CacheManager { * Adds a disk write sample size to calculate the average disk write bandwidth. */ public void addWriteStat(long size, long timeNs) { - mTotalWriteSize += size; - mTotalWriteTimeNs += timeNs; + if (size >= mMinSampleSizeForSpeedCheck) { + mTotalWriteSize += size; + mTotalWriteTimeNs += timeNs; + } } /** @@ -496,4 +585,13 @@ public class CacheManager { public boolean hasSpeedCheckDone() { return mSpeedCheckCount > 0; } + + /** + * Sets minimum sample size for write speed check. + * @param sampleSize minimum sample size for write speed check. + */ + @VisibleForTesting + public void setMinimumSampleSizeForSpeedCheck(int sampleSize) { + mMinSampleSizeForSpeedCheck = sampleSize; + } } diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/DvrStorageManager.java b/usbtuner/src/com/android/usbtuner/exoplayer/cache/DvrStorageManager.java index 4ef86e29..b2b601ff 100644 --- a/usbtuner/src/com/android/usbtuner/exoplayer/DvrStorageManager.java +++ b/usbtuner/src/com/android/usbtuner/exoplayer/cache/DvrStorageManager.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.usbtuner.exoplayer; +package com.android.usbtuner.exoplayer.cache; import android.media.MediaFormat; import android.util.Pair; diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/cache/RecordingSampleBuffer.java b/usbtuner/src/com/android/usbtuner/exoplayer/cache/RecordingSampleBuffer.java new file mode 100644 index 00000000..324fba82 --- /dev/null +++ b/usbtuner/src/com/android/usbtuner/exoplayer/cache/RecordingSampleBuffer.java @@ -0,0 +1,419 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.usbtuner.exoplayer.cache; + +import android.media.MediaCodec; +import android.media.MediaFormat; +import android.os.ConditionVariable; +import android.support.annotation.IntDef; +import android.util.Log; +import android.util.Pair; + +import com.google.android.exoplayer.C; +import com.google.android.exoplayer.SampleHolder; +import com.google.android.exoplayer.SampleSource; +import com.google.android.exoplayer.util.MimeTypes; +import com.android.usbtuner.tvinput.PlaybackCacheListener; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import junit.framework.Assert; + +/** + * Handles I/O between {@link com.android.usbtuner.exoplayer.SampleExtractor} and + * {@link CacheManager}.Reads & writes samples from/to {@link SampleCache} which is backed + * by physical storage. + */ +public class RecordingSampleBuffer implements CacheManager.SampleBuffer, + CacheManager.EvictListener { + private static final String TAG = "RecordingSampleBuffer"; + private static final boolean DEBUG = false; + + @IntDef({CACHE_REASON_LIVE_PLAYBACK, CACHE_REASON_RECORDED_PLAYBACK, CACHE_REASON_RECORDING}) + @Retention(RetentionPolicy.SOURCE) + public @interface CacheReason {} + + /** + * A cache reason for live-stream playback. + */ + public static final int CACHE_REASON_LIVE_PLAYBACK = 0; + + /** + * A cache reason for playback of a recorded program. + */ + public static final int CACHE_REASON_RECORDED_PLAYBACK = 1; + + /** + * A cache reason for recording a program. + */ + public static final int CACHE_REASON_RECORDING = 2; + + private static final long CACHE_WRITE_TIMEOUT_MS = 10 * 1000; // 10 seconds + private static final long CHUNK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500); + private static final long LIVE_THRESHOLD_US = TimeUnit.SECONDS.toMicros(1); + + private final CacheManager mCacheManager; + private final PlaybackCacheListener mCacheListener; + private final int mCacheReason; + + private int mTrackCount; + private List<String> mIds; + private List<MediaFormat> mMediaFormats; + private volatile long mCacheDurationUs = 0; + private long[] mCacheEndPositionUs; + // SampleCache to append the latest live sample. + private SampleCache[] mSampleCaches; + private CachedSampleQueue[] mPlayingSampleQueues; + private final SamplePool mSamplePool = new SamplePool(); + private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US; + private long mCurrentPlaybackPositionUs = 0; + private boolean mEos = false; + + private class CachedSampleQueue extends SampleQueue { + private SampleCache mCache = null; + + public CachedSampleQueue(SamplePool samplePool) { + super(samplePool); + } + + public void setSource(SampleCache newCache) { + for (SampleCache cache = mCache; cache != null; cache = cache.getNext()) { + cache.clear(); + cache.close(); + } + mCache = newCache; + for (SampleCache cache = mCache; cache != null; cache = cache.getNext()) { + cache.resetRead(); + } + } + + public boolean maybeReadSample() { + if (isDurationGreaterThan(CHUNK_DURATION_US)) { + return false; + } + SampleHolder sample = mCache.maybeReadSample(); + if (sample == null) { + if (!mCache.canReadMore() && mCache.getNext() != null) { + mCache.clear(); + mCache.close(); + mCache = mCache.getNext(); + mCache.resetRead(); + return maybeReadSample(); + } else { + if (mCacheReason == CACHE_REASON_RECORDED_PLAYBACK + && !mCache.canReadMore() && mCache.getNext() == null) { + // At the end of the recorded playback. + setEos(); + } + return false; + } + } else { + queueSample(sample); + return true; + } + } + + public int dequeueSample(SampleHolder sample) { + maybeReadSample(); + return super.dequeueSample(sample); + } + + @Override + public void clear() { + super.clear(); + for (SampleCache cache = mCache; cache != null; cache = cache.getNext()) { + cache.clear(); + cache.close(); + } + mCache = null; + } + + public long getSourceStartPositionUs() { + return mCache == null ? -1 : mCache.getStartPositionUs(); + } + } + + /** + * Creates {@link com.android.usbtuner.exoplayer.cache.CacheManager.SampleBuffer} with + * cached I/O backed by physical storage (e.g. trickplay,recording,recorded-playback). + * + * @param cacheManager + * @param cacheListener + * @param enableTrickplay {@code true} when trickplay should be enabled + * @param cacheReason the reason for caching samples {@link RecordingSampleBuffer.CacheReason} + */ + public RecordingSampleBuffer(CacheManager cacheManager, PlaybackCacheListener cacheListener, + boolean enableTrickplay, @CacheReason int cacheReason) { + mCacheManager = cacheManager; + mCacheListener = cacheListener; + if (cacheListener != null) { + cacheListener.onCacheStateChanged(enableTrickplay); + } + mCacheReason = cacheReason; + } + + private String getTrackId(int index) { + return mIds.get(index); + } + + @Override + public synchronized void init(List<String> ids, List<MediaFormat> mediaFormats) + throws IOException { + mTrackCount = ids.size(); + if (mTrackCount <= 0) { + throw new IOException("No tracks to initialize"); + } + mIds = ids; + if (mCacheReason == CACHE_REASON_RECORDING && mediaFormats == null) { + throw new IOException("MediaFormat is not provided."); + } + mMediaFormats = mediaFormats; + mSampleCaches = new SampleCache[mTrackCount]; + mPlayingSampleQueues = new CachedSampleQueue[mTrackCount]; + mCacheEndPositionUs = new long[mTrackCount]; + for (int i = 0; i < mTrackCount; i++) { + if (mCacheReason != CACHE_REASON_RECORDED_PLAYBACK) { + mSampleCaches[i] = mCacheManager.createNewWriteFile(getTrackId(i), 0, mSamplePool); + mPlayingSampleQueues[i] = null; + mCacheEndPositionUs[i] = CHUNK_DURATION_US; + } else { + mCacheManager.loadTrackFormStorage(mIds.get(i), mSamplePool); + } + } + } + + private boolean isLiveLocked(long positionUs) { + Long livePositionUs = null; + for (SampleCache cache : mSampleCaches) { + if (livePositionUs == null || livePositionUs < cache.getEndPositionUs()) { + livePositionUs = cache.getEndPositionUs(); + } + } + return (livePositionUs == null + || Math.abs(livePositionUs - positionUs) < LIVE_THRESHOLD_US); + } + + private void seekIndividualTrackLocked(int index, long positionUs, boolean isLive) { + CachedSampleQueue queue = mPlayingSampleQueues[index]; + if (queue == null) { + return; + } + queue.clear(); + if (isLive) { + queue.setSource(mSampleCaches[index]); + } else { + queue.setSource(mCacheManager.getReadFile(getTrackId(index), positionUs)); + } + queue.maybeReadSample(); + } + + @Override + public synchronized void selectTrack(int index) { + if (mPlayingSampleQueues[index] == null) { + String trackId = getTrackId(index); + mPlayingSampleQueues[index] = new CachedSampleQueue(mSamplePool); + mCacheManager.registerEvictListener(trackId, this); + seekIndividualTrackLocked(index, mCurrentPlaybackPositionUs, + mCacheReason != CACHE_REASON_RECORDED_PLAYBACK && isLiveLocked( + mCurrentPlaybackPositionUs)); + mPlayingSampleQueues[index].maybeReadSample(); + } + } + + @Override + public synchronized void deselectTrack(int index) { + if (mPlayingSampleQueues[index] != null) { + mPlayingSampleQueues[index].clear(); + mPlayingSampleQueues[index] = null; + mCacheManager.unregisterEvictListener(getTrackId(index)); + } + } + + @Override + public void writeSample(int index, SampleHolder sample, + ConditionVariable conditionVariable) throws IOException { + synchronized (this) { + SampleCache cache = mSampleCaches[index]; + if ((sample.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { + if (sample.timeUs > mCacheDurationUs) { + mCacheDurationUs = sample.timeUs; + } + if (sample.timeUs >= mCacheEndPositionUs[index]) { + try { + SampleCache nextCache = mCacheManager.createNewWriteFile( + getTrackId(index), mCacheEndPositionUs[index], mSamplePool); + cache.finishWrite(nextCache); + mSampleCaches[index] = cache = nextCache; + mCacheEndPositionUs[index] = + ((sample.timeUs / CHUNK_DURATION_US) + 1) * CHUNK_DURATION_US; + } catch (IOException e) { + cache.finishWrite(null); + throw e; + } + } + } + cache.writeSample(sample, conditionVariable); + } + + if (!conditionVariable.block(CACHE_WRITE_TIMEOUT_MS)) { + Log.e(TAG, "Error: Serious delay on writing cache"); + conditionVariable.block(); + } + } + + @Override + public boolean isWriteSpeedSlow(int sampleSize, long writeDurationNs) { + if (mCacheReason == CACHE_REASON_RECORDED_PLAYBACK) { + return false; + } + mCacheManager.addWriteStat(sampleSize, writeDurationNs); + return mCacheManager.isWriteSlow(); + } + + @Override + public void handleWriteSpeedSlow() { + Log.w(TAG, "Disk is too slow for trickplay. Disable trickplay."); + mCacheManager.disable(); + mCacheListener.onDiskTooSlow(); + } + + @Override + public synchronized void setEos() { + mEos = true; + } + + private synchronized boolean reachedEos() { + return mEos; + } + + @Override + public synchronized int readSample(int track, SampleHolder sampleHolder) { + CachedSampleQueue queue = mPlayingSampleQueues[track]; + Assert.assertNotNull(queue); + queue.maybeReadSample(); + int result = queue.dequeueSample(sampleHolder); + if (result != SampleSource.SAMPLE_READ && reachedEos()) { + return SampleSource.END_OF_STREAM; + } + return result; + } + + @Override + public synchronized void seekTo(long positionUs) { + boolean isLive = mCacheReason != CACHE_REASON_RECORDED_PLAYBACK && isLiveLocked(positionUs); + + // Seek video track first + for (int i = 0; i < mPlayingSampleQueues.length; ++i) { + CachedSampleQueue queue = mPlayingSampleQueues[i]; + if (queue == null) { + continue; + } + seekIndividualTrackLocked(i, positionUs, isLive); + if (DEBUG) { + Log.d(TAG, "start time = " + queue.getSourceStartPositionUs()); + } + } + mLastBufferedPositionUs = positionUs; + } + + @Override + public synchronized long getBufferedPositionUs() { + Long result = null; + for (CachedSampleQueue queue : mPlayingSampleQueues) { + if (queue == null) { + continue; + } + Long bufferedPositionUs = queue.getEndPositionUs(); + if (bufferedPositionUs == null) { + continue; + } + if (result == null || result > bufferedPositionUs) { + result = bufferedPositionUs; + } + } + if (result == null) { + return mLastBufferedPositionUs; + } else { + return (mLastBufferedPositionUs = result); + } + } + + @Override + public synchronized boolean continueBuffering(long positionUs) { + boolean hasSamples = true; + mCurrentPlaybackPositionUs = positionUs; + for (CachedSampleQueue queue : mPlayingSampleQueues) { + if (queue == null) { + continue; + } + queue.maybeReadSample(); + if (queue.isEmpty()) { + hasSamples = false; + } + } + return hasSamples; + } + + @Override + public synchronized void release() { + if (mSampleCaches == null) { + return; + } + if (mCacheReason == CACHE_REASON_RECORDED_PLAYBACK) { + mCacheManager.close(); + } + for (int i = 0; i < mTrackCount; ++i) { + if (mCacheReason != CACHE_REASON_RECORDED_PLAYBACK) { + mSampleCaches[i].finishWrite(null); + } + mCacheManager.unregisterEvictListener(getTrackId(i)); + } + if (mCacheReason == CACHE_REASON_RECORDING && mTrackCount > 0) { + // Saves meta information for recording. + Pair<String, android.media.MediaFormat> audio = null, video = null; + for (int i = 0; i < mTrackCount; ++i) { + MediaFormat mediaFormat = mMediaFormats.get(i); + String mime = mediaFormat.getString(MediaFormat.KEY_MIME); + mediaFormat.setLong(android.media.MediaFormat.KEY_DURATION, mCacheDurationUs); + if (MimeTypes.isAudio(mime)) { + audio = new Pair<>(getTrackId(i), mediaFormat); + } + else if (MimeTypes.isVideo(mime)) { + video = new Pair<>(getTrackId(i), mediaFormat); + } + } + mCacheManager.writeMetaFiles(audio, video); + } + + for (int i = 0; i < mTrackCount; ++i) { + mCacheManager.clearTrack(getTrackId(i)); + } + } + + // CacheEvictListener + @Override + public void onCacheEvicted(String id, long createdTimeMs) { + if (mCacheListener != null) { + mCacheListener.onCacheStartTimeChanged( + createdTimeMs + TimeUnit.MICROSECONDS.toMillis(CHUNK_DURATION_US)); + } + } +} diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/SampleCache.java b/usbtuner/src/com/android/usbtuner/exoplayer/cache/SampleCache.java index f6c06c12..52b7daa9 100644 --- a/usbtuner/src/com/android/usbtuner/exoplayer/SampleCache.java +++ b/usbtuner/src/com/android/usbtuner/exoplayer/cache/SampleCache.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.usbtuner.exoplayer; +package com.android.usbtuner.exoplayer.cache; import android.os.ConditionVariable; import android.os.Handler; diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/SamplePool.java b/usbtuner/src/com/android/usbtuner/exoplayer/cache/SamplePool.java index 20d12d7e..2c18283e 100644 --- a/usbtuner/src/com/android/usbtuner/exoplayer/SamplePool.java +++ b/usbtuner/src/com/android/usbtuner/exoplayer/cache/SamplePool.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.usbtuner.exoplayer; +package com.android.usbtuner.exoplayer.cache; import com.google.android.exoplayer.SampleHolder; @@ -33,7 +33,7 @@ public class SamplePool { public synchronized SampleHolder acquireSample(int size) { if (mSamplePool.isEmpty()) { SampleHolder sample = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); - sample.replaceBuffer(size); + sample.ensureSpaceForWrite(size); return sample; } SampleHolder smallestSufficientSample = null; @@ -55,7 +55,7 @@ public class SamplePool { // If there's no sufficient sample, grab the maximum sample and resize it to size. if (sampleFromPool == null) { sampleFromPool = maxSample; - sampleFromPool.replaceBuffer(size); + sampleFromPool.ensureSpaceForWrite(size); } mSamplePool.remove(sampleFromPool); return sampleFromPool; diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/SampleQueue.java b/usbtuner/src/com/android/usbtuner/exoplayer/cache/SampleQueue.java index 47f961bb..2bddd2c0 100644 --- a/usbtuner/src/com/android/usbtuner/exoplayer/SampleQueue.java +++ b/usbtuner/src/com/android/usbtuner/exoplayer/cache/SampleQueue.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.usbtuner.exoplayer; +package com.android.usbtuner.exoplayer.cache; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/SimpleSampleSourceExtractor.java b/usbtuner/src/com/android/usbtuner/exoplayer/cache/SimpleSampleBuffer.java index 8e954036..29f06aa4 100644 --- a/usbtuner/src/com/android/usbtuner/exoplayer/SimpleSampleSourceExtractor.java +++ b/usbtuner/src/com/android/usbtuner/exoplayer/cache/SimpleSampleBuffer.java @@ -14,10 +14,9 @@ * limitations under the License. */ -package com.android.usbtuner.exoplayer; +package com.android.usbtuner.exoplayer.cache; -import android.media.MediaDataSource; -import android.media.MediaExtractor; +import android.media.MediaFormat; import android.os.ConditionVariable; import com.google.android.exoplayer.C; @@ -25,41 +24,32 @@ import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.android.usbtuner.tvinput.PlaybackCacheListener; +import java.io.IOException; +import java.util.List; + import junit.framework.Assert; /** - * Extracts from samples and keeps them in the memory. + * Handles I/O for {@link com.android.usbtuner.exoplayer.SampleExtractor} when + * physical storage based cache is not used. Trickplay is disabled. */ -public class SimpleSampleSourceExtractor extends BaseSampleSourceExtractor { +public class SimpleSampleBuffer implements CacheManager.SampleBuffer { private final SamplePool mSamplePool = new SamplePool(); private SampleQueue[] mPlayingSampleQueues; private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US; - public SimpleSampleSourceExtractor(MediaDataSource source, - PlaybackCacheListener cacheListener) { - super(source); - cacheListener.onCacheStateChanged(false); // Disable trickplay - } + private volatile boolean mEos; - public void queueSample(int index, SampleHolder sample, ConditionVariable conditionVariable) { - sample.data.position(0).limit(sample.size); - SampleHolder sampleToQueue = mSamplePool.acquireSample(sample.size); - sampleToQueue.size = sample.size; - sampleToQueue.clearData(); - sampleToQueue.data.put(sample.data); - sampleToQueue.timeUs = sample.timeUs; - sampleToQueue.flags = sample.flags; - - synchronized (this) { - if (mPlayingSampleQueues[index] != null) { - mPlayingSampleQueues[index].queueSample(sampleToQueue); - } + public SimpleSampleBuffer(PlaybackCacheListener cacheListener) { + if (cacheListener != null) { + // Disables trickplay. + cacheListener.onCacheStateChanged(false); } - Thread.yield(); } @Override - public void initOnPrepareLocked(int trackCount) { + public synchronized void init(List<String> ids, List<MediaFormat> mediaFormats) { + int trackCount = ids.size(); mPlayingSampleQueues = new SampleQueue[trackCount]; for (int i = 0; i < trackCount; i++) { mPlayingSampleQueues[i] = null; @@ -67,6 +57,15 @@ public class SimpleSampleSourceExtractor extends BaseSampleSourceExtractor { } @Override + public void setEos() { + mEos = true; + } + + private boolean reachedEos() { + return mEos; + } + + @Override public void selectTrack(int index) { synchronized (this) { if (mPlayingSampleQueues[index] == null) { @@ -88,59 +87,88 @@ public class SimpleSampleSourceExtractor extends BaseSampleSourceExtractor { } @Override - public long getBufferedPositionUs() { - synchronized (this) { - Long result = null; - for (SampleQueue queue : mPlayingSampleQueues) { - if (queue == null) { - continue; - } - Long bufferedPositionUs = queue.getEndPositionUs(); - if (bufferedPositionUs == null) { - continue; - } - if (result == null || result > bufferedPositionUs) { - result = bufferedPositionUs; - } + public synchronized long getBufferedPositionUs() { + Long result = null; + for (SampleQueue queue : mPlayingSampleQueues) { + if (queue == null) { + continue; } - if (result == null) { - return mLastBufferedPositionUs; - } else { - return (mLastBufferedPositionUs = result); + Long bufferedPositionUs = queue.getEndPositionUs(); + if (bufferedPositionUs == null) { + continue; + } + if (result == null || result > bufferedPositionUs) { + result = bufferedPositionUs; } } + if (result == null) { + return mLastBufferedPositionUs; + } else { + return (mLastBufferedPositionUs = result); + } } @Override - public int readSample(int track, SampleHolder sampleHolder) { + public synchronized int readSample(int track, SampleHolder sampleHolder) { + SampleQueue queue = mPlayingSampleQueues[track]; + Assert.assertNotNull(queue); + int result = queue.dequeueSample(sampleHolder); + if (result != SampleSource.SAMPLE_READ && reachedEos()) { + return SampleSource.END_OF_STREAM; + } + return result; + } + + @Override + public void writeSample(int index, SampleHolder sample, + ConditionVariable conditionVariable) throws IOException { + sample.data.position(0).limit(sample.size); + SampleHolder sampleToQueue = mSamplePool.acquireSample(sample.size); + sampleToQueue.size = sample.size; + sampleToQueue.clearData(); + sampleToQueue.data.put(sample.data); + sampleToQueue.timeUs = sample.timeUs; + sampleToQueue.flags = sample.flags; + synchronized (this) { - SampleQueue queue = mPlayingSampleQueues[track]; - Assert.assertNotNull(queue); - int result = queue.dequeueSample(sampleHolder); - if (result != SampleSource.SAMPLE_READ && getEos()) { - return SampleSource.END_OF_STREAM; + if (mPlayingSampleQueues[index] != null) { + mPlayingSampleQueues[index].queueSample(sampleToQueue); } - return result; } } @Override - public boolean continueBuffering(long positionUs) { - synchronized (this) { - for (SampleQueue queue : mPlayingSampleQueues) { - if (queue == null) { - continue; - } - if (queue.isEmpty()) { - return false; - } + public boolean isWriteSpeedSlow(int sampleSize, long durationNs) { + // Since SimpleSampleBuffer write samples only to memory (not to physical storage), + // write speed is always fine. + return false; + } + + @Override + public void handleWriteSpeedSlow() { + // no-op + } + + @Override + public synchronized boolean continueBuffering(long positionUs) { + for (SampleQueue queue : mPlayingSampleQueues) { + if (queue == null) { + continue; + } + if (queue.isEmpty()) { + return false; } - return true; } + return true; } @Override public void seekTo(long positionUs) { // Not used. } + + @Override + public void release() { + // Not used. + } } diff --git a/usbtuner/src/com/android/usbtuner/exoplayer/TrickplayStorageManager.java b/usbtuner/src/com/android/usbtuner/exoplayer/cache/TrickplayStorageManager.java index 57af8eba..801ae534 100644 --- a/usbtuner/src/com/android/usbtuner/exoplayer/TrickplayStorageManager.java +++ b/usbtuner/src/com/android/usbtuner/exoplayer/cache/TrickplayStorageManager.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.usbtuner.exoplayer; +package com.android.usbtuner.exoplayer.cache; import android.content.Context; import android.media.MediaFormat; diff --git a/usbtuner/src/com/android/usbtuner/setup/ScanResultFragment.java b/usbtuner/src/com/android/usbtuner/setup/ScanResultFragment.java index 1c32aeb8..bf2ca477 100644 --- a/usbtuner/src/com/android/usbtuner/setup/ScanResultFragment.java +++ b/usbtuner/src/com/android/usbtuner/setup/ScanResultFragment.java @@ -16,6 +16,7 @@ package com.android.usbtuner.setup; +import android.content.Context; import android.content.res.Resources; import android.os.Bundle; import android.support.annotation.NonNull; @@ -64,11 +65,9 @@ public class ScanResultFragment extends SetupMultiPaneFragment { private int mChannelCountOnPreference; @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - mChannelCountOnPreference = UsbTunerPreferences - .getScannedChannelCount(getActivity().getApplicationContext()); - return super.onCreateView(inflater, container, savedInstanceState); + public void onAttach(Context context) { + super.onAttach(context); + mChannelCountOnPreference = UsbTunerPreferences.getScannedChannelCount(context); } @NonNull diff --git a/usbtuner/src/com/android/usbtuner/tvinput/BaseTunerTvInputService.java b/usbtuner/src/com/android/usbtuner/tvinput/BaseTunerTvInputService.java index cb25cfec..a24b6146 100644 --- a/usbtuner/src/com/android/usbtuner/tvinput/BaseTunerTvInputService.java +++ b/usbtuner/src/com/android/usbtuner/tvinput/BaseTunerTvInputService.java @@ -16,44 +16,40 @@ package com.android.usbtuner.tvinput; -import android.os.Handler; +import android.media.tv.TvInputService; import android.util.Log; import com.google.android.exoplayer.audio.AudioCapabilities; import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver; -import com.android.tv.common.recording.RecordingTvInputService; -import com.android.tv.common.recording.TvRecording; -import com.android.usbtuner.exoplayer.CacheManager; +import com.android.usbtuner.exoplayer.cache.CacheManager; import java.util.Collections; import java.util.Set; import java.util.WeakHashMap; /** - * {@link BaseTunerTvInputService} serves TV channels coming from a usb tuner device. + * {@link BaseTunerTvInputService} serves TV channels coming from a tuner device. */ -public abstract class BaseTunerTvInputService extends RecordingTvInputService +public abstract class BaseTunerTvInputService extends TvInputService implements AudioCapabilitiesReceiver.Listener { private static final String TAG = "BaseTunerTvInputService"; private static final boolean DEBUG = false; // WeakContainer for {@link TvInputSessionImpl} - private final Set<TvInputSessionImpl> mTvInputSessions = Collections.newSetFromMap( - new WeakHashMap<TvInputSessionImpl, Boolean>()); + private final Set<TunerSession> mTunerSessions = Collections.newSetFromMap( + new WeakHashMap<TunerSession, Boolean>()); private ChannelDataManager mChannelDataManager; private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver; private AudioCapabilities mAudioCapabilities; - protected CacheManager mCacheManager; + private CacheManager mCacheManager; @Override public void onCreate() { super.onCreate(); - if (DEBUG) { - Log.d(TAG, "onCreate"); - } + if (DEBUG) Log.d(TAG, "onCreate"); mChannelDataManager = new ChannelDataManager(getApplicationContext()); mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(getApplicationContext(), this); mAudioCapabilitiesReceiver.register(); - maybeInitCacheManager(); + mCacheManager = createCacheManager(); if (mCacheManager == null) { Log.i(TAG, "Trickplay is disabled"); } else { @@ -61,13 +57,14 @@ public abstract class BaseTunerTvInputService extends RecordingTvInputService } } - protected abstract void maybeInitCacheManager(); + /** + * Creates {@CacheManager}. It returns null, if storage in not enough. + */ + protected abstract CacheManager createCacheManager(); @Override public void onDestroy() { - if (DEBUG) { - Log.d(TAG, "onDestroy"); - } + if (DEBUG) Log.d(TAG, "onDestroy"); super.onDestroy(); mChannelDataManager.release(); mAudioCapabilitiesReceiver.unregister(); @@ -77,36 +74,27 @@ public abstract class BaseTunerTvInputService extends RecordingTvInputService } @Override - public TvRecording.RecordingSession onCreateDvrSession(String inputId) { - return new RecordingSessionImpl(this, inputId, mChannelDataManager); + public RecordingSession onCreateRecordingSession(String inputId) { + return new TunerRecordingSession(this, inputId, mChannelDataManager); } @Override - public RecordingTvInputService.PlaybackSession onCreatePlaybackSession(String inputId) { - if (DEBUG) { - Log.d(TAG, "onCreateSession"); - } - final TvInputSessionImpl session = new TvInputSessionImpl( + public Session onCreateSession(String inputId) { + if (DEBUG) Log.d(TAG, "onCreateSession"); + final TunerSession session = new TunerSession( this, mChannelDataManager, mCacheManager); - mTvInputSessions.add(session); - session.notifyAudioCapabilitiesChanged(mAudioCapabilities); - new Handler().post(new Runnable() { - @Override - public void run() { - // STOPSHIP(DVR): Session methods cannot be called inside onCreatePlaybackSession. - // If DvrSession is added in API. we can call them inside onCreatePlaybackSession. - session.setOverlayViewEnabled(true); - } - }); + mTunerSessions.add(session); + session.setAudioCapabilities(mAudioCapabilities); + session.setOverlayViewEnabled(true); return session; } @Override public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) { mAudioCapabilities = audioCapabilities; - for (TvInputSessionImpl session : mTvInputSessions) { + for (TunerSession session : mTunerSessions) { if (!session.isReleased()) { - session.notifyAudioCapabilitiesChanged(audioCapabilities); + session.setAudioCapabilities(audioCapabilities); } } } diff --git a/usbtuner/src/com/android/usbtuner/tvinput/ChannelDataManager.java b/usbtuner/src/com/android/usbtuner/tvinput/ChannelDataManager.java index 206d8ba4..57affe22 100644 --- a/usbtuner/src/com/android/usbtuner/tvinput/ChannelDataManager.java +++ b/usbtuner/src/com/android/usbtuner/tvinput/ChannelDataManager.java @@ -321,6 +321,13 @@ public class ChannelDataManager implements Handler.Callback { channel.setChannelId(channelId); long currentTime = System.currentTimeMillis(); List<EitItem> oldItems = getAllProgramsForChannel(channel); + // TODO: Find a right to check if the programs are added outside. + for (EitItem item : oldItems) { + if (item.getEventId() == 0) { + // The event has been added outside TV tuner. Do not update programs. + return; + } + } List<EitItem> outdatedOldItems = new ArrayList<>(); List<EitItem> programsAddedToEPG = new ArrayList<>(); ArrayList<ContentProviderOperation> ops = new ArrayList<>(); diff --git a/usbtuner/src/com/android/usbtuner/tvinput/DvrSessionImplInternal.java b/usbtuner/src/com/android/usbtuner/tvinput/DvrSessionImplInternal.java deleted file mode 100644 index 80c5fca6..00000000 --- a/usbtuner/src/com/android/usbtuner/tvinput/DvrSessionImplInternal.java +++ /dev/null @@ -1,313 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.usbtuner.tvinput; - -import android.content.ContentUris; -import android.content.Context; -import android.media.MediaDataSource; -import android.net.Uri; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Message; -import android.support.annotation.IntDef; -import android.support.annotation.Nullable; -import android.util.Log; -import android.util.Pair; -import android.widget.Toast; - -import com.android.tv.common.recording.RecordingCapability; -import com.android.usbtuner.DvbDeviceAccessor; -import com.android.usbtuner.TunerHal; -import com.android.usbtuner.UsbTunerDataSource; -import com.android.usbtuner.data.PsipData; -import com.android.usbtuner.data.TunerChannel; -import com.android.usbtuner.exoplayer.CacheManager; -import com.android.usbtuner.exoplayer.DvrStorageManager; -import com.android.usbtuner.exoplayer.RecordSampleSourceExtractor; - -import java.io.File; -import java.io.IOException; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.List; -import java.util.concurrent.CountDownLatch; - -/** - * Implements a DVR feature. - */ -public class DvrSessionImplInternal implements PlaybackCacheListener, EventDetector.EventListener, - Handler.Callback { - private static String TAG = "DvrSessionImplInternal"; - private static final boolean DEBUG = true; // STOPSHIP(DVR) - - - private static final int MSG_START_RECORDING = 1; - private static final int MSG_STOP_RECORDING = 2; - private static final int MSG_DELETE_RECORDING = 3; - private static final int MSG_RELEASE = 4; - private final String mInputId; - private RecordingCapability mCapabilities; - - public RecordingCapability getCapabilities() { - return mCapabilities; - } - - @IntDef({STATE_IDLE, STATE_RECORDING}) - @Retention(RetentionPolicy.SOURCE) - public @interface DvrSessionState {} - private static final int STATE_IDLE = 1; - private static final int STATE_RECORDING = 2; - - private static final long CHANNEL_ID_NONE = -1; - - private final Context mContext; - private final ChannelDataManager mChannelDataManager; - private final Handler mHandler; - private final CountDownLatch mReleaseLatch = new CountDownLatch(1); - - private TunerHal mTunerHal; - private UsbTunerDataSource mTunerSource; - private CacheManager mCacheManager; - private RecordSampleSourceExtractor mRecorder; - private DvrEventListener mDvrEventListener; - @DvrSessionState private int mSessionState = STATE_IDLE; - - // For event notification to LiveChannels - public interface DvrEventListener { - void onRecordStarted(Uri mediaUri); - void onRecordUnexpectedlyStopped(Uri mediaUri, int reason); - void onDeleted(Uri mediaUri); - void onDeleteFailed(Uri mediaUri, int reason); - } - - public DvrSessionImplInternal(Context context, String inputId, ChannelDataManager dataManager) { - mContext = context; - mInputId = inputId; - HandlerThread handlerThread = new HandlerThread(TAG); - handlerThread.start(); - mHandler = new Handler(handlerThread.getLooper(), this); - mChannelDataManager = dataManager; - mChannelDataManager.checkDataVersion(context); - mCapabilities = new DvbDeviceAccessor(context).getRecordingCapability(mInputId); - if (DEBUG) Log.d(TAG, mCapabilities.toString()); - } - - // PlaybackCacheListener - @Override - public void onCacheStartTimeChanged(long startTimeMs) { - } - - @Override - public void onCacheStateChanged(boolean available) { - } - - @Override - public void onDiskTooSlow() { - } - - // EventDetector.EventListener - @Override - public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) { - mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime); - } - - @Override - public void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items) { - mChannelDataManager.notifyEventDetected(channel, items); - } - - public void setDvrEventListener(DvrEventListener listener) { - mDvrEventListener = listener; - } - - public void startRecording(Uri channelUri, Uri mediaUri) { - mHandler.obtainMessage( - MSG_START_RECORDING, new Pair<>(channelUri, mediaUri)).sendToTarget(); - } - - public void stopRecording() { - mHandler.sendEmptyMessage(MSG_STOP_RECORDING); - } - - public void deleteRecording(Uri mediaUri) { - mHandler.obtainMessage(MSG_DELETE_RECORDING, mediaUri).sendToTarget(); - } - - public void release() { - mHandler.removeCallbacksAndMessages(null); - mHandler.sendEmptyMessage(MSG_RELEASE); - try { - mReleaseLatch.await(); - } catch (InterruptedException e) { - Log.e(TAG, "Couldn't wait for finish of MSG_RELEASE", e); - } finally { - mHandler.getLooper().quitSafely(); - } - } - - @Override - public boolean handleMessage(Message msg) { - switch (msg.what) { - case MSG_START_RECORDING: { - Pair<Uri, Uri> params = (Pair<Uri, Uri>) msg.obj; - if(onStartRecording(params.first, params.second)) { - Toast.makeText(mContext, "USB TV tuner: onStart is called", - Toast.LENGTH_SHORT).show(); - if (mDvrEventListener != null) { - mDvrEventListener.onRecordStarted(params.second); - } - } - else { - // TODO: apply reason - if (mDvrEventListener != null) { - mDvrEventListener.onRecordUnexpectedlyStopped(params.second, 0); - } - } - return true; - } - case MSG_STOP_RECORDING: { - onStopRecording(); - Toast.makeText(mContext, "USB TV tuner: onStopRecord is called", - Toast.LENGTH_SHORT).show(); - return true; - } - case MSG_DELETE_RECORDING: { - Uri toDelete = (Uri) msg.obj; - onDeleteRecording(toDelete); - return true; - } - case MSG_RELEASE: { - onRelease(); - return true; - } - } - return false; - } - - @Nullable - private TunerChannel getChannel(Uri channelUri) { - long channelId; - try { - channelId = ContentUris.parseId(channelUri); - } catch (UnsupportedOperationException | NumberFormatException e) { - channelId = CHANNEL_ID_NONE; - } - return (channelId == CHANNEL_ID_NONE) ? null : mChannelDataManager.getChannel(channelId); - } - - private File getMediaDir(Uri mediaUri) { - String mediaPath = mediaUri.getPath(); - if (mediaPath == null || mediaPath.length() == 0) { - return null; - } - return new File(mContext.getCacheDir().getAbsolutePath() + "/recording" + - mediaUri.getPath()); - } - - private void resetRecorder() { - if (mRecorder != null) { - mRecorder.release(); - mRecorder = null; - } - if (mCacheManager != null) { - mCacheManager.close(); - mCacheManager = null; - } - if (mTunerSource != null) { - mTunerSource.stopStream(); - mTunerSource = null; - } - if (mTunerHal != null) { - try { - mTunerHal.close(); - } catch (Exception ex) { - Log.e(TAG, "Error on closing tuner HAL.", ex); - } - mTunerHal = null; - } - } - - private boolean onStartRecording(Uri channelUri, Uri mediaUri) { - if (mSessionState != STATE_IDLE) { - return false; - } - TunerChannel channel = getChannel(channelUri); - if (channel == null) { - Log.w(TAG, "Failed to start recording. Couldn't find the channel for " + channelUri); - return false; - } - mTunerHal = TunerHal.getInstance(mContext); - if (mTunerHal == null) { - Log.w(TAG, "Failed to start recording. Couldn't open a DVB device"); - resetRecorder(); - return false; - } - mTunerSource = new UsbTunerDataSource(mTunerHal, this); - if (!mTunerSource.tuneToChannel(channel)) { - Log.w(TAG, "Failed to start recording. Couldn't tune to the channel for " + channel); - resetRecorder(); - return false; - } - File mediaDir = getMediaDir(mediaUri); - if (mediaDir == null) { - Log.w(TAG, "Failed to start recording. mediaUri is not provided properly " + - mediaUri.toString()); - resetRecorder(); - return false; - } - mTunerSource.startStream(); - mCacheManager = new CacheManager(new DvrStorageManager(mediaDir, true)); - mRecorder = new RecordSampleSourceExtractor((MediaDataSource) mTunerSource, - mCacheManager, this); - try { - mRecorder.prepare(); - } catch (IOException e) { - Log.w(TAG, "Failed to start recording. Couldn't prepare a extractor"); - resetRecorder(); - return false; - } - mSessionState = STATE_RECORDING; - return true; - } - - private void onStopRecording() { - // TODO: notify the recording result to LiveChannels - if (mSessionState != STATE_RECORDING) { - return; - } - resetRecorder(); - mSessionState = STATE_IDLE; - } - - private void onDeleteRecording(Uri mediaUri) { - // TODO: notify the deletion result to LiveChannels - File mediaDir = getMediaDir(mediaUri); - if (mediaDir == null) { - return; - } - for(File file: mediaDir.listFiles()) { - file.delete(); - } - mediaDir.delete(); - } - - private void onRelease() { - // Current recording will be canceled. - onStopRecording(); - mReleaseLatch.countDown(); - } -} diff --git a/usbtuner/src/com/android/usbtuner/tvinput/InternalTunerTvInputService.java b/usbtuner/src/com/android/usbtuner/tvinput/InternalTunerTvInputService.java index adc4f14a..b4e97833 100644 --- a/usbtuner/src/com/android/usbtuner/tvinput/InternalTunerTvInputService.java +++ b/usbtuner/src/com/android/usbtuner/tvinput/InternalTunerTvInputService.java @@ -24,8 +24,8 @@ import android.media.tv.TvInputInfo; import android.os.Environment; import android.util.Log; -import com.android.usbtuner.exoplayer.CacheManager; -import com.android.usbtuner.exoplayer.TrickplayStorageManager; +import com.android.usbtuner.exoplayer.cache.CacheManager; +import com.android.usbtuner.exoplayer.cache.TrickplayStorageManager; import com.android.usbtuner.util.SystemPropertiesProxy; import com.android.usbtuner.util.TisConfiguration; import org.xmlpull.v1.XmlPullParserException; @@ -57,7 +57,7 @@ public class InternalTunerTvInputService extends BaseTunerTvInputService { } @Override - protected void maybeInitCacheManager() { + protected CacheManager createCacheManager() { int maxCacheSizeMb = SystemPropertiesProxy.getInt(MAX_CACHE_SIZE_KEY, MAX_CACHE_SIZE_DEF); if (maxCacheSizeMb >= MIN_CACHE_SIZE_DEF) { boolean useExternalStorage = Environment.MEDIA_MOUNTED.equals( @@ -67,21 +67,20 @@ public class InternalTunerTvInputService extends BaseTunerTvInputService { boolean allowToUseInternalStorage = true; if (useExternalStorage || allowToUseInternalStorage) { File baseDir = useExternalStorage ? getExternalCacheDir() : getCacheDir(); - mCacheManager = new CacheManager( + return new CacheManager( new TrickplayStorageManager(getApplicationContext(), baseDir, 1024L * 1024 * maxCacheSizeMb)); } } + return null; } @Override public TvInputInfo onHardwareAdded(TvInputHardwareInfo hardwareInfo) { if (DEBUG) Log.d(TAG, "onHardwareAdded: " + hardwareInfo.toString()); - if (mTvInputId != null) { return null; } - TvInputInfo info = null; if (hardwareInfo.getType() == TvInputHardwareInfo.TV_INPUT_TYPE_TUNER && TisConfiguration.getTunerHwDeviceId(this) == hardwareInfo.getDeviceId()) { @@ -101,7 +100,6 @@ public class InternalTunerTvInputService extends BaseTunerTvInputService { @Override public String onHardwareRemoved(TvInputHardwareInfo hardwareInfo) { if (DEBUG) Log.d(TAG, "onHardwareRemoved: " + hardwareInfo.toString()); - return null; } } diff --git a/usbtuner/src/com/android/usbtuner/tvinput/RecordingSessionImpl.java b/usbtuner/src/com/android/usbtuner/tvinput/RecordingSessionImpl.java deleted file mode 100644 index 0aac1211..00000000 --- a/usbtuner/src/com/android/usbtuner/tvinput/RecordingSessionImpl.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.usbtuner.tvinput; - -import android.content.Context; -import android.net.Uri; - -import com.android.tv.common.recording.RecordingCapability; -import com.android.tv.common.recording.TvRecording; - -/** - * Processes DVR recordings, and deletes the previously recorded contents. - */ -public class RecordingSessionImpl extends TvRecording.RecordingSession implements - DvrSessionImplInternal.DvrEventListener { - // TODO: recording request will be handled here - private final DvrSessionImplInternal mSessionImplInternal; - private final String mInputId; - - public RecordingSessionImpl(Context context, String inputId, - ChannelDataManager channelDataManager) { - super(context); - mInputId = inputId; - mSessionImplInternal = new DvrSessionImplInternal(context, inputId, channelDataManager); - mSessionImplInternal.setDvrEventListener(this); - } - - @Override - public void onStopRecord() { - mSessionImplInternal.stopRecording(); - } - - @Override - public void onStartRecord(Uri channelUri, Uri mediaUri) { - mSessionImplInternal.startRecording(channelUri, mediaUri); - } - - @Override - public void onDelete(Uri mediaUri) { - mSessionImplInternal.deleteRecording(mediaUri); - notifyDeleted(mediaUri); - } - - @Override - public RecordingCapability onGetCapability() { - return mSessionImplInternal.getCapabilities(); - } - - @Override - public void onRelease() { - } - - // DvrSessionImplInternal.DvrEventListener - @Override - public void onRecordStarted(Uri mediaUri) { - notifyRecordStarted(mediaUri); - } - - @Override - public void onRecordUnexpectedlyStopped(Uri mediaUri, int reason) { - notifyRecordUnexpectedlyStopped(mediaUri, reason); - } - - @Override - public void onDeleted(Uri mediaUri) { - notifyDeleted(mediaUri); - } - - @Override - public void onDeleteFailed(Uri mediaUri, int reason) { - notifyDeleteFailed(mediaUri, reason); - } -} diff --git a/usbtuner/src/com/android/usbtuner/tvinput/TunerRecordingSession.java b/usbtuner/src/com/android/usbtuner/tvinput/TunerRecordingSession.java new file mode 100644 index 00000000..3d27a946 --- /dev/null +++ b/usbtuner/src/com/android/usbtuner/tvinput/TunerRecordingSession.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.usbtuner.tvinput; + +import android.content.Context; +import android.media.tv.TvInputManager; +import android.media.tv.TvInputService; +import android.net.Uri; +import android.support.annotation.MainThread; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; +import android.util.Log; + +/** + * Processes DVR recordings, and deletes the previously recorded contents. + */ +public class TunerRecordingSession extends TvInputService.RecordingSession { + private static String TAG = "TunerRecordingSession"; + private static boolean DEBUG = false; + + private final TunerRecordingSessionWorker mSessionWorker; + private final String mInputId; + + public TunerRecordingSession(Context context, String inputId, + ChannelDataManager channelDataManager) { + super(context); + mInputId = inputId; + mSessionWorker = new TunerRecordingSessionWorker(context, inputId, channelDataManager, + this); + } + + // RecordingSession + @MainThread + @Override + public void onTune(Uri channelUri) { + // TODO(dvr): support calling more than once, http://b/27171225 + if (DEBUG) { + Log.d(TAG, "Requesting recording session tune: " + channelUri); + } + mSessionWorker.connect(channelUri); + } + + @MainThread + @Override + public void onRelease() { + if (DEBUG) { + Log.d(TAG, "Requesting recording session release."); + } + mSessionWorker.release(); + } + + @MainThread + @Override + public void onStartRecording(@Nullable Uri programHint) { + if (DEBUG) { + Log.d(TAG, "Requesting start recording."); + } + mSessionWorker.startRecording(); + } + + @MainThread + @Override + public void onStopRecording() { + if (DEBUG) { + Log.d(TAG, "Requesting stop recording."); + } + mSessionWorker.stopRecording(); + } + + // Called from TunerRecordingSessionImpl in a worker thread. + @WorkerThread + public void onTuned(Uri channelUri) { + if (DEBUG) { + Log.d(TAG, "Notifying recording session tuned."); + } + notifyTuned(channelUri); + } + + @WorkerThread + public void onConnectFailed() { + if (DEBUG) { + Log.d(TAG, "Notifying recording session connection failed."); + } + notifyError(TvInputManager.RECORDING_ERROR_UNKNOWN); + } + + @WorkerThread + public void onRecordFinished(final Uri recordedProgramUri) { + if (DEBUG) { + Log.d(TAG, "Notifying record successfully finished."); + } + notifyRecordingStopped(recordedProgramUri); + } + + @WorkerThread + public void onRecordUnexpectedlyStopped(int reason) { + Log.w(TAG, "Notifying record failed: " + reason); + notifyError(reason); + } +} diff --git a/usbtuner/src/com/android/usbtuner/tvinput/TunerRecordingSessionWorker.java b/usbtuner/src/com/android/usbtuner/tvinput/TunerRecordingSessionWorker.java new file mode 100644 index 00000000..3d121a85 --- /dev/null +++ b/usbtuner/src/com/android/usbtuner/tvinput/TunerRecordingSessionWorker.java @@ -0,0 +1,549 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.usbtuner.tvinput; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.media.MediaDataSource; +import android.media.tv.TvContract; +import android.media.tv.TvInputManager; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import android.util.Log; +import android.widget.Toast; + +import com.google.android.exoplayer.util.Assertions; +import com.android.tv.common.recording.RecordedProgram; +import com.android.tv.common.recording.RecordingCapability; +import com.android.usbtuner.DvbDeviceAccessor; +import com.android.usbtuner.TunerHal; +import com.android.usbtuner.UsbTunerDataSource; +import com.android.usbtuner.data.PsipData; +import com.android.usbtuner.data.TunerChannel; +import com.android.usbtuner.exoplayer.Recorder; +import com.android.usbtuner.exoplayer.cache.CacheManager; +import com.android.usbtuner.exoplayer.cache.DvrStorageManager; + +import java.io.File; +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; +import java.util.Locale; +import java.util.Random; + +/** + * Implements a DVR feature. + */ +public class TunerRecordingSessionWorker implements PlaybackCacheListener, + EventDetector.EventListener, Recorder.RecordListener, + Handler.Callback { + private static String TAG = "TunerRecordingSessionWorker"; + private static final boolean DEBUG = false; + + private static final String SORT_BY_TIME = TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS + + ", " + TvContract.Programs.COLUMN_CHANNEL_ID + ", " + + TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS; + private static final int MSG_CONNECT = 1; + private static final int MSG_DISCONNECT = 2; + private static final int MSG_START_RECORDING = 3; + private static final int MSG_STOP_RECORDING = 4; + private static final int MSG_RECORDING_RESULT = 5; + private static final int MSG_DELETE_RECORDING = 6; + private static final int MSG_RELEASE = 7; + private RecordingCapability mCapabilities; + + public RecordingCapability getCapabilities() { + return mCapabilities; + } + + @IntDef({STATE_IDLE, STATE_CONNECTED, STATE_RECORDING}) + @Retention(RetentionPolicy.SOURCE) + public @interface DvrSessionState {} + private static final int STATE_IDLE = 1; + private static final int STATE_CONNECTED = 2; + private static final int STATE_RECORDING = 3; + + private static final long CHANNEL_ID_NONE = -1; + + private final Context mContext; + private final ChannelDataManager mChannelDataManager; + private final Handler mHandler; + private final Random mRandom = new Random(); + + private TunerHal mTunerHal; + private UsbTunerDataSource mTunerSource; + private TunerChannel mChannel; + private File mStorageDir; + private long mRecordStartTime; + private long mRecordEndTime; + private CacheManager mCacheManager; + private Recorder mRecorder; + private final TunerRecordingSession mSession; + @DvrSessionState private int mSessionState = STATE_IDLE; + private final String mInputId; + + public TunerRecordingSessionWorker(Context context, String inputId, + ChannelDataManager dataManager, TunerRecordingSession session) { + mRandom.setSeed(System.nanoTime()); + mContext = context; + HandlerThread handlerThread = new HandlerThread(TAG); + handlerThread.start(); + mHandler = new Handler(handlerThread.getLooper(), this); + mChannelDataManager = dataManager; + mChannelDataManager.checkDataVersion(context); + mCapabilities = new DvbDeviceAccessor(context).getRecordingCapability(inputId); + mInputId = inputId; + if (DEBUG) Log.d(TAG, mCapabilities.toString()); + mSession = session; + } + + // PlaybackCacheListener + @Override + public void onCacheStartTimeChanged(long startTimeMs) { + } + + @Override + public void onCacheStateChanged(boolean available) { + } + + @Override + public void onDiskTooSlow() { + } + + // EventDetector.EventListener + @Override + public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) { + if (mChannel == null || mChannel.compareTo(channel) != 0) { + return; + } + mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime); + } + + @Override + public void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items) { + if (mChannel == null || mChannel.compareTo(channel) != 0) { + return; + } + mChannelDataManager.notifyEventDetected(channel, items); + } + + public void connect(Uri channelUri) { + mHandler.removeCallbacksAndMessages(null); + mHandler.obtainMessage(MSG_CONNECT, channelUri).sendToTarget(); + } + + public void disconnect() { + mHandler.sendEmptyMessage(MSG_DISCONNECT); + } + + public void startRecording() { + mHandler.sendEmptyMessage(MSG_START_RECORDING); + } + + public void stopRecording() { + mHandler.sendEmptyMessage(MSG_STOP_RECORDING); + } + + public void notifyRecordingFinished(boolean success) { + mHandler.obtainMessage(MSG_RECORDING_RESULT, success).sendToTarget(); + } + + public void deleteRecording(Uri mediaUri) { + mHandler.obtainMessage(MSG_DELETE_RECORDING, mediaUri).sendToTarget(); + } + + public void release() { + mHandler.removeCallbacksAndMessages(null); + mHandler.sendEmptyMessage(MSG_RELEASE); + } + + @Override + public boolean handleMessage(Message msg) { + // TODO: Add RecordStopped status + switch (msg.what) { + case MSG_CONNECT: { + Uri channelUri = (Uri) msg.obj; + if (onConnect(channelUri)) { + mSession.onTuned(channelUri); + } else { + Log.w(TAG, "Recording session connect failed"); + mSession.onConnectFailed(); + } + return true; + } + case MSG_START_RECORDING: { + if(onStartRecording()) { + Toast.makeText(mContext, "USB TV tuner: Recording started", + Toast.LENGTH_SHORT).show(); + } + else { + mSession.onRecordUnexpectedlyStopped(TvInputManager.RECORDING_ERROR_UNKNOWN); + } + return true; + } + case MSG_DISCONNECT: { + return true; + } + case MSG_STOP_RECORDING: { + onStopRecording(); + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + Toast.makeText(mContext, "USB TV tuner: Recording stopped", + Toast.LENGTH_SHORT).show(); + } + }); + return true; + } + case MSG_RECORDING_RESULT: { + onRecordingResult((Boolean) msg.obj); + return true; + } + case MSG_DELETE_RECORDING: { + Uri toDelete = (Uri) msg.obj; + onDeleteRecording(toDelete); + return true; + } + case MSG_RELEASE: { + onRelease(); + return true; + } + } + return false; + } + + @Nullable + private TunerChannel getChannel(Uri channelUri) { + if (channelUri == null) { + return null; + } + long channelId; + try { + channelId = ContentUris.parseId(channelUri); + } catch (UnsupportedOperationException | NumberFormatException e) { + channelId = CHANNEL_ID_NONE; + } + return (channelId == CHANNEL_ID_NONE) ? null : mChannelDataManager.getChannel(channelId); + } + + private String getStorageKey() { + long prefix = System.currentTimeMillis(); + int suffix = mRandom.nextInt(); + return String.format(Locale.ENGLISH, "%016x_%016x", prefix, suffix); + } + + private File getMediaDir(String storageKey) { + return new File(mContext.getCacheDir().getAbsolutePath() + "/recording/" + storageKey); + } + + private File getMediaDir(Uri mediaUri) { + String mediaPath = mediaUri.getPath(); + if (mediaPath == null || mediaPath.length() == 0) { + return null; + } + return new File(mContext.getCacheDir().getAbsolutePath() + "/recording" + + mediaUri.getPath()); + } + + private void reset() { + if (mRecorder != null) { + mRecorder.release(); + mRecorder = null; + } + if (mCacheManager != null) { + mCacheManager.close(); + mCacheManager = null; + } + if (mTunerSource != null) { + mTunerSource.stopStream(); + mTunerSource = null; + } + if (mTunerHal != null) { + try { + mTunerHal.close(); + } catch (Exception ex) { + Log.e(TAG, "Error on closing tuner HAL.", ex); + } + mTunerHal = null; + } + mSessionState = STATE_IDLE; + } + + private void resetRecorder() { + Assertions.checkArgument(mSessionState != STATE_IDLE); + if (mRecorder != null) { + mRecorder.release(); + mRecorder = null; + } + if (mCacheManager != null) { + mCacheManager.close(); + mCacheManager = null; + } + if (mTunerSource != null) { + mTunerSource.stopStream(); + mTunerSource = null; + } + mSessionState = STATE_CONNECTED; + } + + private boolean onConnect(Uri channelUri) { + if (mSessionState == STATE_RECORDING) { + return false; + } + mChannel = getChannel(channelUri); + if (mChannel == null) { + Log.w(TAG, "Failed to start recording. Couldn't find the channel for " + mChannel); + return false; + } + if (mSessionState == STATE_CONNECTED) { + return true; + } + mTunerHal = TunerHal.createInstance(mContext); + if (mTunerHal == null) { + Log.w(TAG, "Failed to start recording. Couldn't open a DVB device"); + reset(); + return false; + } + mSessionState = STATE_CONNECTED; + return true; + } + + private boolean onStartRecording() { + if (mSessionState != STATE_CONNECTED) { + return false; + } + mStorageDir = getMediaDir(getStorageKey()); + mTunerSource = new UsbTunerDataSource(mTunerHal, this); + if (!mTunerSource.tuneToChannel(mChannel)) { + Log.w(TAG, "Failed to start recording. Couldn't tune to the channel for " + + mChannel.toString()); + resetRecorder(); + return false; + } + mCacheManager = new CacheManager(new DvrStorageManager(mStorageDir, true)); + mTunerSource.startStream(); + mRecordStartTime = System.currentTimeMillis(); + mRecorder = new Recorder((MediaDataSource) mTunerSource, + mCacheManager, this, this); + try { + mRecorder.prepare(); + } catch (IOException e) { + Log.w(TAG, "Failed to start recording. Couldn't prepare a extractor"); + resetRecorder(); + return false; + } + mSessionState = STATE_RECORDING; + return true; + } + + private void onStopRecording() { + if (mSessionState != STATE_RECORDING) { + return; + } + // Do not change session status. + if (mRecorder != null) { + mRecorder.release(); + mRecordEndTime = System.currentTimeMillis(); + mRecorder = null; + } + } + + private static class Program { + private long mChannelId; + private String mTitle; + private String mEpisodeTitle; + private int mSeasonNumber; + private int mEpisodeNumber; + private String mDescription; + private String mPosterArtUri; + private String mThumbnailUri; + private String mCanonicalGenres; + private String mContentRatings; + private long mStartTimeUtcMillis; + private long mEndTimeUtcMillis; + private long mVideoWidth; + private long mVideoHeight; + + private static final String[] PROJECTION = { + TvContract.Programs.COLUMN_CHANNEL_ID, + TvContract.Programs.COLUMN_TITLE, + TvContract.Programs.COLUMN_EPISODE_TITLE, + TvContract.Programs.COLUMN_SEASON_NUMBER, + TvContract.Programs.COLUMN_EPISODE_NUMBER, + TvContract.Programs.COLUMN_SHORT_DESCRIPTION, + TvContract.Programs.COLUMN_POSTER_ART_URI, + TvContract.Programs.COLUMN_THUMBNAIL_URI, + TvContract.Programs.COLUMN_CANONICAL_GENRE, + TvContract.Programs.COLUMN_CONTENT_RATING, + TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, + TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, + TvContract.Programs.COLUMN_VIDEO_WIDTH, + TvContract.Programs.COLUMN_VIDEO_HEIGHT + }; + + public Program(Cursor cursor) { + int index = 0; + mChannelId = cursor.getLong(index++); + mTitle = cursor.getString(index++); + mEpisodeTitle = cursor.getString(index++); + mSeasonNumber = cursor.getInt(index++); + mEpisodeNumber = cursor.getInt(index++); + mDescription = cursor.getString(index++); + mPosterArtUri = cursor.getString(index++); + mThumbnailUri = cursor.getString(index++); + mCanonicalGenres = cursor.getString(index++); + mContentRatings = cursor.getString(index++); + mStartTimeUtcMillis = cursor.getLong(index++); + mEndTimeUtcMillis = cursor.getLong(index++); + mVideoWidth = cursor.getLong(index++); + mVideoHeight = cursor.getLong(index++); + } + + public Program(long channelId) { + mChannelId = channelId; + mTitle = "Unknown"; + mEpisodeTitle = ""; + mSeasonNumber = 0; + mEpisodeNumber = 0; + mDescription = "Unknown"; + mPosterArtUri = null; + mThumbnailUri = null; + mCanonicalGenres = null; + mContentRatings = null; + mStartTimeUtcMillis = 0; + mEndTimeUtcMillis = 0; + mVideoWidth = 0; + mVideoHeight = 0; + } + + public static Program onQuery(Cursor c) { + Program program = null; + if (c != null && c.moveToNext()) { + program = new Program(c); + } + return program; + } + + public ContentValues buildValues() { + ContentValues values = new ContentValues(); + values.put(PROJECTION[0], mChannelId); + values.put(PROJECTION[1], mTitle); + values.put(PROJECTION[2], mEpisodeTitle); + values.put(PROJECTION[3], mSeasonNumber); + values.put(PROJECTION[4], mEpisodeNumber); + values.put(PROJECTION[5], mDescription); + values.put(PROJECTION[6], mPosterArtUri); + values.put(PROJECTION[7], mThumbnailUri); + values.put(PROJECTION[8], mCanonicalGenres); + values.put(PROJECTION[9], mContentRatings); + values.put(PROJECTION[10], mStartTimeUtcMillis); + values.put(PROJECTION[11], mEndTimeUtcMillis); + values.put(PROJECTION[12], mVideoWidth); + values.put(PROJECTION[13], mVideoHeight); + return values; + } + } + + private Program getRecordedProgram() { + ContentResolver resolver = mContext.getContentResolver(); + long avg = mRecordStartTime / 2 + mRecordEndTime / 2; + Uri programUri = TvContract.buildProgramsUriForChannel(mChannel.getChannelId(), avg, avg); + try (Cursor c = resolver.query(programUri, Program.PROJECTION, null, null, SORT_BY_TIME)) { + if (c != null) { + Program result = Program.onQuery(c); + if (DEBUG) { + Log.v(TAG, "Finished query for " + this); + } + return result; + } else { + if (c == null) { + Log.e(TAG, "Unknown query error for " + this); + } else { + if (DEBUG) { + Log.d(TAG, "Canceled query for " + this); + } + } + return null; + } + } + } + + private Uri insertRecordedProgram(Program program, long channelId, String storageUri, + long totalBytes, long startTime, long endTime) { + RecordedProgram recordedProgram = RecordedProgram.builder() + .setInputId(mInputId) + .setChannelId(channelId) + .setDataUri(storageUri) + .setDurationMillis(endTime - startTime) + .setDataBytes(totalBytes) + .build(); + Uri uri = mContext.getContentResolver().insert(TvContract.RecordedPrograms.CONTENT_URI, + RecordedProgram.toValues(recordedProgram)); + return uri; + } + + private boolean onRecordingResult(boolean success) { + if (mSessionState == STATE_RECORDING && success) { + Uri uri = insertRecordedProgram(getRecordedProgram(), mChannel.getChannelId(), + mStorageDir.toURI().toString(), 1024 * 1024, + mRecordStartTime, mRecordEndTime); + if (uri != null) { + mSession.onRecordFinished(uri); + } + resetRecorder(); + return true; + } + + if (mSessionState == STATE_RECORDING) { + mSession.onRecordUnexpectedlyStopped(TvInputManager.RECORDING_ERROR_UNKNOWN); + Log.w(TAG, "Recording failed: " + mChannel == null ? "" : mChannel.toString()); + resetRecorder(); + } else { + Log.e(TAG, "Recording session status abnormal"); + reset(); + } + return false; + } + + private void onDeleteRecording(Uri mediaUri) { + // TODO: notify the deletion result to LiveChannels + File mediaDir = getMediaDir(mediaUri); + if (mediaDir == null) { + return; + } + for(File file: mediaDir.listFiles()) { + file.delete(); + } + mediaDir.delete(); + } + + private void onRelease() { + // Current recording will be canceled. + reset(); + mHandler.getLooper().quitSafely(); + // TODO: Remove failed recording files. + } +} diff --git a/usbtuner/src/com/android/usbtuner/tvinput/TvInputSessionImpl.java b/usbtuner/src/com/android/usbtuner/tvinput/TunerSession.java index 94a8607a..da3f17f3 100644 --- a/usbtuner/src/com/android/usbtuner/tvinput/TvInputSessionImpl.java +++ b/usbtuner/src/com/android/usbtuner/tvinput/TunerSession.java @@ -16,12 +16,14 @@ package com.android.usbtuner.tvinput; +import android.annotation.TargetApi; import android.content.Context; import android.media.PlaybackParams; import android.media.tv.TvContentRating; import android.media.tv.TvInputManager; -import android.media.tv.TvTrackInfo; +import android.media.tv.TvInputService; import android.net.Uri; +import android.os.Build; import android.os.Handler; import android.os.Message; import android.os.SystemClock; @@ -35,25 +37,22 @@ import android.widget.TextView; import android.widget.Toast; import com.google.android.exoplayer.audio.AudioCapabilities; -import com.android.tv.common.recording.RecordingTvInputService; import com.android.usbtuner.R; import com.android.usbtuner.cc.CaptionLayout; import com.android.usbtuner.cc.CaptionTrackRenderer; import com.android.usbtuner.data.Cea708Data.CaptionEvent; import com.android.usbtuner.data.Track.AtscCaptionTrack; import com.android.usbtuner.data.TunerChannel; -import com.android.usbtuner.exoplayer.CacheManager; +import com.android.usbtuner.exoplayer.cache.CacheManager; import com.android.usbtuner.util.StatusTextUtils; import com.android.usbtuner.util.SystemPropertiesProxy; -import java.util.ArrayList; - /** - * Provides a USB tuner TV input session. + * Provides a USB tuner TV input session. It handles Overlay UI works. Main tuner input functions + * are implemented in {@link TunerSessionWorker}. */ -public class TvInputSessionImpl extends RecordingTvInputService.PlaybackSession - implements Handler.Callback, TvInputSessionImplInternal.InternalListener { - private static final String TAG = "TvInputSessionImpl"; +public class TunerSession extends TvInputService.Session implements Handler.Callback { + private static final String TAG = "TunerSession"; private static final boolean DEBUG = false; private static final String USBTUNER_SHOW_DEBUG = "persist.usbtuner.show_debug"; @@ -76,13 +75,13 @@ public class TvInputSessionImpl extends RecordingTvInputService.PlaybackSession private final TextView mAudioStatusView; private final ViewGroup mMessageLayout; private final CaptionTrackRenderer mCaptionTrackRenderer; - private final TvInputSessionImplInternal mSessionImplInternal; + private final TunerSessionWorker mSessionWorker; private boolean mReleased = false; private boolean mVideoAvailable = false; private boolean mPlayPaused; private long mTuneStartTimestamp; - public TvInputSessionImpl(Context context, ChannelDataManager channelDataManager, + public TunerSession(Context context, ChannelDataManager channelDataManager, CacheManager cacheManager) { super(context); mContext = context; @@ -102,9 +101,8 @@ public class TvInputSessionImpl extends RecordingTvInputService.PlaybackSession context.getString(R.string.ut_ac3_passthrough_unavailable)))); CaptionLayout captionLayout = (CaptionLayout) mOverlayView.findViewById(R.id.caption); mCaptionTrackRenderer = new CaptionTrackRenderer(captionLayout); - mSessionImplInternal = new TvInputSessionImplInternal(context, channelDataManager, - cacheManager); - mSessionImplInternal.setInternalListener(this); + mSessionWorker = new TunerSessionWorker(context, channelDataManager, + cacheManager, this); } public boolean isReleased() { @@ -118,67 +116,61 @@ public class TvInputSessionImpl extends RecordingTvInputService.PlaybackSession @Override public boolean onSelectTrack(int type, String trackId) { - mSessionImplInternal.sendMessage( - TvInputSessionImplInternal.MSG_SELECT_TRACK, type, 0, trackId); + mSessionWorker.sendMessage( + TunerSessionWorker.MSG_SELECT_TRACK, type, 0, trackId); return false; } @Override public void onSetCaptionEnabled(boolean enabled) { - mSessionImplInternal.sendMessage( - TvInputSessionImplInternal.MSG_SET_CAPTION_ENABLED, enabled); + mSessionWorker.sendMessage( + TunerSessionWorker.MSG_SET_CAPTION_ENABLED, enabled); } @Override public void onSetStreamVolume(float volume) { - mSessionImplInternal.sendMessage(TvInputSessionImplInternal.MSG_SET_STREAM_VOLUME, volume); + mSessionWorker.sendMessage(TunerSessionWorker.MSG_SET_STREAM_VOLUME, volume); } @Override public boolean onSetSurface(Surface surface) { - mSessionImplInternal.sendMessage(TvInputSessionImplInternal.MSG_SET_SURFACE, surface); + mSessionWorker.sendMessage(TunerSessionWorker.MSG_SET_SURFACE, surface); return true; } @Override public void onTimeShiftPause() { - mSessionImplInternal.sendMessage(TvInputSessionImplInternal.MSG_TIMESHIFT_PAUSE); + mSessionWorker.sendMessage(TunerSessionWorker.MSG_TIMESHIFT_PAUSE); mPlayPaused = true; } @Override public void onTimeShiftResume() { - mSessionImplInternal.sendMessage(TvInputSessionImplInternal.MSG_TIMESHIFT_RESUME); + mSessionWorker.sendMessage(TunerSessionWorker.MSG_TIMESHIFT_RESUME); mPlayPaused = false; } @Override public void onTimeShiftSeekTo(long timeMs) { - if (DEBUG) { - Log.d(TAG, "Timeshift seekTo requested position: " + timeMs / 1000); - } - mSessionImplInternal.sendMessage(TvInputSessionImplInternal.MSG_TIMESHIFT_SEEK_TO, + if (DEBUG) Log.d(TAG, "Timeshift seekTo requested position: " + timeMs / 1000); + mSessionWorker.sendMessage(TunerSessionWorker.MSG_TIMESHIFT_SEEK_TO, mPlayPaused ? 1 : 0, 0, timeMs); } @Override public void onTimeShiftSetPlaybackParams(PlaybackParams params) { - mSessionImplInternal.sendMessage( - TvInputSessionImplInternal.MSG_TIMESHIFT_SET_PLAYBACKPARAMS, params); + mSessionWorker.sendMessage( + TunerSessionWorker.MSG_TIMESHIFT_SET_PLAYBACKPARAMS, params); } @Override public long onTimeShiftGetStartPosition() { - Long duration = mSessionImplInternal.getDurationForRecording(); - if (duration != null) { - notifyTimeShiftEndPosition(mSessionImplInternal.getStartPosition() + duration); - } - return mSessionImplInternal.getStartPosition(); + return mSessionWorker.getStartPosition(); } @Override public long onTimeShiftGetCurrentPosition() { - return mSessionImplInternal.getCurrentPosition(); + return mSessionWorker.getCurrentPosition(); } @Override @@ -188,30 +180,31 @@ public class TvInputSessionImpl extends RecordingTvInputService.PlaybackSession } if (channelUri == null) { Log.w(TAG, "onTune() is failed due to null channelUri."); - mSessionImplInternal.stopTune(); + mSessionWorker.stopTune(); return false; } mTuneStartTimestamp = SystemClock.elapsedRealtime(); - mSessionImplInternal.tune(channelUri); + mSessionWorker.tune(channelUri); mPlayPaused = false; return true; } + @TargetApi(Build.VERSION_CODES.N) @Override - public void onPlayMedia(Uri recordUri) { + public void onTimeShiftPlay(Uri recordUri) { if (recordUri == null) { - Log.w(TAG, "onPlayMedia() is failed due to null channelUri."); - mSessionImplInternal.stopTune(); + Log.w(TAG, "onTimeShiftPlay() is failed due to null channelUri."); + mSessionWorker.stopTune(); return; } mTuneStartTimestamp = SystemClock.elapsedRealtime(); - mSessionImplInternal.tune(recordUri); + mSessionWorker.tune(recordUri); mPlayPaused = false; } @Override public void onUnblockContent(TvContentRating unblockedRating) { - mSessionImplInternal.sendMessage(TvInputSessionImplInternal.MSG_UNBLOCKED_RATING, + mSessionWorker.sendMessage(TunerSessionWorker.MSG_UNBLOCKED_RATING, unblockedRating); } @@ -221,36 +214,19 @@ public class TvInputSessionImpl extends RecordingTvInputService.PlaybackSession Log.d(TAG, "onRelease"); } mReleased = true; - mSessionImplInternal.release(); + mSessionWorker.release(); mUiHandler.removeCallbacksAndMessages(null); } - public void notifyAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) { - mSessionImplInternal.sendMessage(TvInputSessionImplInternal.MSG_AUDIO_CAPABILITIES_CHANGED, + /** + * Sets {@link AudioCapabilities}. + */ + public void setAudioCapabilities(AudioCapabilities audioCapabilities) { + mSessionWorker.sendMessage(TunerSessionWorker.MSG_AUDIO_CAPABILITIES_CHANGED, audioCapabilities); } @Override - public void notifyContentAllowed() { - super.notifyContentAllowed(); - } - - @Override - public void notifyContentBlocked(TvContentRating rating) { - super.notifyContentBlocked(rating); - } - - @Override - public void notifyTimeShiftStatusChanged(int status) { - super.notifyTimeShiftStatusChanged(status); - } - - @Override - public void notifyTracksChanged(ArrayList<TvTrackInfo> tvTracks) { - super.notifyTracksChanged(tvTracks); - } - - @Override public void notifyVideoAvailable() { super.notifyVideoAvailable(); if (mTuneStartTimestamp != 0) { @@ -267,22 +243,18 @@ public class TvInputSessionImpl extends RecordingTvInputService.PlaybackSession case TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING: case TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING: case TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY: - super.notifyVideoUnavailable(reason); - break; case TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL: - super.notifyVideoAvailable(); - sendUiMessage(TvInputSessionImpl.MSG_UI_SHOW_MESSAGE, - mContext.getString(R.string.ut_no_signal)); + super.notifyVideoUnavailable(reason); break; case TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN: default: super.notifyVideoAvailable(); - TunerChannel channel = mSessionImplInternal.getCurrentChannel(); + TunerChannel channel = mSessionWorker.getCurrentChannel(); if (channel != null) { - sendUiMessage(TvInputSessionImpl.MSG_UI_SHOW_MESSAGE, + sendUiMessage(TunerSession.MSG_UI_SHOW_MESSAGE, mContext.getString(R.string.ut_fail_to_tune, channel.getName())); } else { - sendUiMessage(TvInputSessionImpl.MSG_UI_SHOW_MESSAGE, + sendUiMessage(TunerSession.MSG_UI_SHOW_MESSAGE, mContext.getString(R.string.ut_fail_to_tune_to_unknown_channel)); } break; @@ -293,17 +265,14 @@ public class TvInputSessionImpl extends RecordingTvInputService.PlaybackSession } } - @Override public void sendUiMessage(int message) { mUiHandler.sendEmptyMessage(message); } - @Override public void sendUiMessage(int message, Object object) { mUiHandler.obtainMessage(message, object).sendToTarget(); } - @Override public void sendUiMessage(int message, int arg1, int arg2, Object object) { mUiHandler.obtainMessage(message, arg1, arg2, object).sendToTarget(); } @@ -312,11 +281,6 @@ public class TvInputSessionImpl extends RecordingTvInputService.PlaybackSession public boolean handleMessage(Message msg) { switch (msg.what) { case MSG_UI_SHOW_MESSAGE: { - if (!mVideoAvailable) { - // A workaround to show error message before notifyVideoAvailable(). - mVideoAvailable = true; - super.notifyVideoAvailable(); - } mMessageView.setText((String) msg.obj); mMessageLayout.setVisibility(View.VISIBLE); return true; diff --git a/usbtuner/src/com/android/usbtuner/tvinput/TvInputSessionImplInternal.java b/usbtuner/src/com/android/usbtuner/tvinput/TunerSessionWorker.java index c49d0833..6b71fcf6 100644 --- a/usbtuner/src/com/android/usbtuner/tvinput/TvInputSessionImplInternal.java +++ b/usbtuner/src/com/android/usbtuner/tvinput/TunerSessionWorker.java @@ -16,12 +16,15 @@ package com.android.usbtuner.tvinput; +import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; +import android.database.Cursor; import android.media.MediaDataSource; import android.media.MediaFormat; import android.media.PlaybackParams; import android.media.tv.TvContentRating; +import android.media.tv.TvContract; import android.media.tv.TvInputManager; import android.media.tv.TvTrackInfo; import android.net.Uri; @@ -50,10 +53,10 @@ import com.android.usbtuner.data.PsipData.TvTracksInterface; import com.android.usbtuner.data.Track.AtscAudioTrack; import com.android.usbtuner.data.Track.AtscCaptionTrack; import com.android.usbtuner.data.TunerChannel; -import com.android.usbtuner.exoplayer.CacheManager; -import com.android.usbtuner.exoplayer.DvrStorageManager; import com.android.usbtuner.exoplayer.MpegTsPassthroughAc3RendererBuilder; import com.android.usbtuner.exoplayer.MpegTsPlayer; +import com.android.usbtuner.exoplayer.cache.CacheManager; +import com.android.usbtuner.exoplayer.cache.DvrStorageManager; import com.android.usbtuner.util.IsoUtils; import com.android.usbtuner.util.StatusTextUtils; @@ -69,17 +72,16 @@ import java.util.Objects; import java.util.concurrent.CountDownLatch; /** - * {@link TvInputSessionImplInternal} implements a handler thread which processes TV input jobs + * {@link TunerSessionWorker} implements a handler thread which processes TV input jobs * such as handling {@link ExoPlayer}, managing a tuner device, trickplay, and so on. */ -public class TvInputSessionImplInternal implements PlaybackCacheListener, +public class TunerSessionWorker implements PlaybackCacheListener, MpegTsPlayer.VideoEventListener, MpegTsPlayer.Listener, EventDetector.EventListener, ChannelDataManager.ProgramInfoListener, Handler.Callback { - private static final String TAG = "TvInputSessionInternal"; + private static final String TAG = "TunerSessionWorker"; private static final boolean DEBUG = false; private static final boolean ENABLE_PROFILER = true; private static final String PLAY_FROM_CHANNEL = "channel"; - private static final String PLAY_FROM_RECORDING = "record"; // Public messages public static final int MSG_SELECT_TRACK = 1; @@ -158,7 +160,6 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, private String mRecordingId; private volatile Long mRecordingDuration; private final Handler mHandler; - private final HandlerThread mHandlerThread; private int mRetryCount; private float mVolume; private final ArrayList<TvTrackInfo> mTvTracks; @@ -184,21 +185,24 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, private long mLastPositionInBytes = 0L; private final CacheManager mCacheManager; private final TvContentRatingCache mTvContentRatingCache = TvContentRatingCache.getInstance(); + private final TunerSession mSession; - public TvInputSessionImplInternal(Context context, ChannelDataManager channelDataManager, - CacheManager cacheManager) { + public TunerSessionWorker(Context context, ChannelDataManager channelDataManager, + CacheManager cacheManager, TunerSession tunerSession) { mContext = context; - mTunerHal = TunerHal.getInstance(context); + mTunerHal = TunerHal.createInstance(context); if (mTunerHal == null) { throw new RuntimeException("Failed to open a DVB device"); } // HandlerThread should be set up before it is registered as a listener in the all other // components. - mHandlerThread = new HandlerThread(TAG); - mHandlerThread.start(); - mHandler = new Handler(mHandlerThread.getLooper(), this); + HandlerThread handlerThread = new HandlerThread(TAG); + handlerThread.start(); + mHandler = new Handler(handlerThread.getLooper(), this); + mSession = tunerSession; mChannelDataManager = channelDataManager; + // TODO: need to refactor it for multi-tuner support. mChannelDataManager.setListener(this); mChannelDataManager.checkDataVersion(mContext); mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); @@ -238,7 +242,7 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, private String getRecordingPath() { - return mContext.getCacheDir().getAbsolutePath() + "/recording" + mRecordingId; + return Uri.parse(mRecordingId).getPath(); } public Long getDurationForRecording() { @@ -300,7 +304,7 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, } catch (InterruptedException e) { Log.e(TAG, "Couldn't wait for finish of MSG_RELEASE", e); } finally { - mHandlerThread.quitSafely(); + mHandler.getLooper().quitSafely(); } } @@ -333,7 +337,7 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, // MpegTsPlayer.VideoEventListener @Override public void onEmitCaptionEvent(Cea708Data.CaptionEvent event) { - mInternalListener.sendUiMessage(TvInputSessionImpl.MSG_UI_PROCESS_CAPTION_TRACK, event); + mSession.sendUiMessage(TunerSession.MSG_UI_PROCESS_CAPTION_TRACK, event); } @Override @@ -354,7 +358,7 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, @Override public void onRescanNeeded() { - mInternalListener.sendUiMessage(TvInputSessionImpl.MSG_UI_TOAST_RESCAN_NEEDED); + mSession.sendUiMessage(TunerSession.MSG_UI_TOAST_RESCAN_NEEDED); } @Override @@ -389,90 +393,76 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, mChannelDataManager.notifyEventDetected(channel, items); } - // InternalListener - public interface InternalListener { - void sendUiMessage(int message); - void sendUiMessage(int message, Object object); - void sendUiMessage(int message, int arg1, int arg2, Object object); - void notifyVideoAvailable(); - void notifyVideoUnavailable(int reason); - void notifyTimeShiftStatusChanged(int status); - void notifyContentBlocked(TvContentRating tvContentRating); - void notifyContentAllowed(); - void notifyTracksChanged(ArrayList<TvTrackInfo> tvTracks); - void notifyTrackSelected(int type, String trackId); - } - - public void setInternalListener(TvInputSessionImpl internalListener) { - mInternalListener = internalListener; - } - - private InternalListener mInternalListener = new InternalListener() { - @Override - public void sendUiMessage(int message) { - // do nothing. - } - - @Override - public void sendUiMessage(int message, Object object) { - // do nothing. - } - - @Override - public void sendUiMessage(int message, int arg1, int arg2, Object object) { - // do nothing. - } - - @Override - public void notifyVideoAvailable() { - // do nothing. + private long parseChannel(Uri uri) { + try { + List<String> paths = uri.getPathSegments(); + if (paths.size() > 1 && paths.get(0).equals(PLAY_FROM_CHANNEL)) { + return ContentUris.parseId(uri); + } + } catch (UnsupportedOperationException | NumberFormatException e) { } + return -1; + } - @Override - public void notifyVideoUnavailable(int reason) { - // do nothing. - } + private static class RecordedProgram { + private long mChannelId; + private String mDataUri; - @Override - public void notifyTimeShiftStatusChanged(int status) { - // do nothing. - } + private static final String[] PROJECTION = { + TvContract.Programs.COLUMN_CHANNEL_ID, + TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI, + }; - @Override - public void notifyContentBlocked(TvContentRating tvContentRating) { - // do nothing. + public RecordedProgram(Cursor cursor) { + int index = 0; + mChannelId = cursor.getLong(index++); + mDataUri = cursor.getString(index++); } - @Override - public void notifyContentAllowed() { - // do nothing. + public RecordedProgram(long channelId, String dataUri) { + mChannelId = channelId; + mDataUri = dataUri; } - @Override - public void notifyTracksChanged(ArrayList<TvTrackInfo> tvTracks) { - // do nothing. + public static RecordedProgram onQuery(Cursor c) { + RecordedProgram recording = null; + if (c != null && c.moveToNext()) { + recording = new RecordedProgram(c); + } + return recording; } - @Override - public void notifyTrackSelected(int type, String trackId) { - // do nothing. + public String getDataUri() { + return mDataUri; } - }; + } - private long parseChannel(Uri uri) { - try { - List<String> paths = uri.getPathSegments(); - if (paths.size() > 1 && paths.get(0).equals(PLAY_FROM_CHANNEL)) { - return ContentUris.parseId(uri); + private RecordedProgram getRecordedProgram(Uri recordedUri) { + ContentResolver resolver = mContext.getContentResolver(); + try(Cursor c = resolver.query(recordedUri, RecordedProgram.PROJECTION, null, null, null)) { + if (c != null) { + RecordedProgram result = RecordedProgram.onQuery(c); + if (DEBUG) { + Log.d(TAG, "Finished query for " + this); + } + return result; + } else { + if (c == null) { + Log.e(TAG, "Unknown query error for " + this); + } else { + if (DEBUG) { + Log.d(TAG, "Canceled query for " + this); + } + } + return null; } - } catch (UnsupportedOperationException | NumberFormatException e) { } - return -1; } private String parseRecording(Uri uri) { - if (uri.getScheme().equals(PLAY_FROM_RECORDING)) { - return uri.getPath(); + RecordedProgram recording = getRecordedProgram(uri); + if (recording != null) { + return recording.getDataUri(); } return null; } @@ -499,7 +489,7 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, if (channel == null && recording == null) { Log.w(TAG, "onTune() is failed. Can't find channel for " + channelUri); stopTune(); - mInternalListener.notifyVideoUnavailable( + mSession.notifyVideoUnavailable( TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); return true; } @@ -508,7 +498,9 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, mChannelDataManager.requestProgramsData(channel); } prepareTune(channel, recording); - mInternalListener.notifyContentAllowed(); + // TODO: Need to refactor. notifyContentAllowed() should not be called if parental + // control is turned on. + mSession.notifyContentAllowed(); resetPlayback(); resetTvTracks(); mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS, @@ -527,7 +519,7 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, resetTvTracks(); mTunerHal.stopTune(); mSource = null; - mInternalListener.notifyVideoUnavailable( + mSession.notifyVideoUnavailable( TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); return true; } @@ -564,7 +556,7 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, stopCaptionTrack(); mTunerHal.stopTune(); - mInternalListener.notifyVideoUnavailable( + mSession.notifyVideoUnavailable( TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); // After MAX_RETRY_COUNT, give some delay of an empirically chosen value @@ -642,6 +634,9 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, if (mChannel != null && mChannel.hasVideo()) { updateVideoTrack(size.getWidth(), size.getHeight()); } + if (mRecordingId != null) { + updateVideoTrack(size.getWidth(), size.getHeight()); + } return true; } case MSG_AUDIO_UNPLAYABLE: { @@ -650,8 +645,8 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, return true; } Log.i(TAG, "AC3 audio cannot be played due to device limitation"); - mInternalListener.sendUiMessage( - TvInputSessionImpl.MSG_UI_SHOW_AUDIO_UNPLAYABLE); + mSession.sendUiMessage( + TunerSession.MSG_UI_SHOW_AUDIO_UNPLAYABLE); return true; } case MSG_UPDATE_PROGRAM: { @@ -728,7 +723,7 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, } mCacheStartTimeMs = mRecordStartTimeMs = (mRecordingId != null) ? 0 : System.currentTimeMillis(); - mInternalListener.notifyVideoAvailable(); + mSession.notifyVideoAvailable(); mReportedDrawnToSurface = true; // If surface is drawn successfully, it means that the playback was brought back @@ -774,9 +769,11 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, return true; } case MSG_SELECT_TRACK: { - // TODO : mChannel == null && mRecordingId != null if (mChannel != null) { doSelectTrack(msg.arg1, (String) msg.obj); + } else if (mRecordingId != null) { + // TODO : mChannel == null && mRecordingId != null + Log.d(TAG, "track selected for recording"); } return true; } @@ -855,7 +852,7 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, } case MSG_CACHE_STATE_CHANGED: { boolean available = (boolean) msg.obj; - mInternalListener.notifyTimeShiftStatusChanged(available + mSession.notifyTimeShiftStatusChanged(available ? TvInputManager.TIME_SHIFT_STATUS_AVAILABLE : TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE); return true; @@ -868,7 +865,7 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, long positionInBytes = mSource != null ? mSource.getPosition() : 0L; if (UsbTunerDebug.ENABLED) { UsbTunerDebug.calculateDiff(); - mInternalListener.sendUiMessage(TvInputSessionImpl.MSG_UI_SET_STATUS_TEXT, + mSession.sendUiMessage(TunerSession.MSG_UI_SET_STATUS_TEXT, Html.fromHtml( StatusTextUtils.getStatusWarningInHTML( (limitInBytes - mLastLimitInBytes) @@ -887,17 +884,17 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, Log.d(TAG, String.format("MSG_CHECK_SIGNAL position: %d, limit: %d", positionInBytes, limitInBytes)); } - mInternalListener.sendUiMessage(TvInputSessionImpl.MSG_UI_HIDE_MESSAGE); + mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE); if (mSource != null && mChannel.getType() == Channel.TYPE_TUNER && positionInBytes == mLastPositionInBytes && limitInBytes == mLastLimitInBytes) { - mInternalListener.notifyVideoUnavailable( + mSession.notifyVideoUnavailable( TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); mReportedSignalAvailable = false; } else { if (mReportedDrawnToSurface && !mReportedSignalAvailable) { - mInternalListener.notifyVideoAvailable(); + mSession.notifyVideoAvailable(); mReportedSignalAvailable = true; } } @@ -933,10 +930,10 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, // TODO: Implement a switching between tracks more smoothly. resetPlayback(); } - mInternalListener.notifyTrackSelected(type, trackId); + mSession.notifyTrackSelected(type, trackId); } else if (type == TvTrackInfo.TYPE_SUBTITLE) { if (trackId == null) { - mInternalListener.notifyTrackSelected(type, null); + mSession.notifyTrackSelected(type, null); mCaptionTrack = null; stopCaptionTrack(); return; @@ -945,7 +942,7 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, if (track.getId().equals(trackId)) { // The service number of the caption service is used for track id of a // subtitle track. Passes the following track id on to TsParser. - mInternalListener.notifyTrackSelected(type, trackId); + mSession.notifyTrackSelected(type, trackId); mCaptionTrack = mCaptionTrackMap.get(numTrackId); startCaptionTrack(); return; @@ -961,19 +958,17 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, ++mPlayerGeneration; MpegTsPlayer player = new MpegTsPlayer(mPlayerGeneration, - new MpegTsPassthroughAc3RendererBuilder(cacheManager, this), - mHandler, capabilities); + new MpegTsPassthroughAc3RendererBuilder(mContext, cacheManager, this), + mHandler, capabilities, this); Log.i(TAG, "Passthrough AC3 renderer"); - if (DEBUG) { - Log.d(TAG, "ExoPlayer created: " + mPlayerGeneration); - } + if (DEBUG) Log.d(TAG, "ExoPlayer created: " + mPlayerGeneration); return player; } private void startCaptionTrack() { if (mCaptionEnabled && mCaptionTrack != null) { - mInternalListener.sendUiMessage( - TvInputSessionImpl.MSG_UI_START_CAPTION_TRACK, mCaptionTrack); + mSession.sendUiMessage( + TunerSession.MSG_UI_START_CAPTION_TRACK, mCaptionTrack); if (mPlayer != null) { mPlayer.setCaptionServiceNumber(mCaptionTrack.serviceNumber); } @@ -984,15 +979,15 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, if (mPlayer != null) { mPlayer.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER); } - mInternalListener.sendUiMessage(TvInputSessionImpl.MSG_UI_STOP_CAPTION_TRACK); + mSession.sendUiMessage(TunerSession.MSG_UI_STOP_CAPTION_TRACK); } private void resetTvTracks() { mTvTracks.clear(); mAudioTrackMap.clear(); mCaptionTrackMap.clear(); - mInternalListener.sendUiMessage(TvInputSessionImpl.MSG_UI_RESET_CAPTION_TRACK); - mInternalListener.notifyTracksChanged(mTvTracks); + mSession.sendUiMessage(TunerSession.MSG_UI_RESET_CAPTION_TRACK); + mSession.notifyTracksChanged(mTvTracks); } private void updateTvTracks(TvTracksInterface tvTracksInterface) { @@ -1027,8 +1022,8 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, removeTvTracks(TvTrackInfo.TYPE_VIDEO); mTvTracks.add(new TvTrackInfo.Builder(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID) .setVideoWidth(width).setVideoHeight(height).build()); - mInternalListener.notifyTracksChanged(mTvTracks); - mInternalListener.notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID); + mSession.notifyTracksChanged(mTvTracks); + mSession.notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID); } private void updateAudioTracks(List<AtscAudioTrack> audioTracks) { @@ -1068,7 +1063,7 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, ++index; } } - mInternalListener.notifyTracksChanged(mTvTracks); + mSession.notifyTracksChanged(mTvTracks); } private void updateCaptionTracks(List<AtscCaptionTrack> captionTracks) { @@ -1096,7 +1091,7 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, mCaptionTrackMap.put(captionTrack.serviceNumber, captionTrack); } } - mInternalListener.notifyTracksChanged(mTvTracks); + mSession.notifyTracksChanged(mTvTracks); } private void updateChannelInfo(TunerChannel channel) { @@ -1126,7 +1121,7 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, } } mChannel.selectAudioTrack(index); - mInternalListener.notifyTrackSelected(TvTrackInfo.TYPE_AUDIO, + mSession.notifyTrackSelected(TvTrackInfo.TYPE_AUDIO, index == -1 ? null : AUDIO_TRACK_PREFIX + index); // Reset playback if there is a change in the listening streaming PIDs. @@ -1154,7 +1149,7 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, mPlayerStarted = false; mReportedDrawnToSurface = false; mReportedSignalAvailable = false; - mInternalListener.sendUiMessage(TvInputSessionImpl.MSG_UI_HIDE_AUDIO_UNPLAYABLE); + mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_AUDIO_UNPLAYABLE); } } @@ -1165,7 +1160,7 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, } if (mChannel != null && !mChannel.hasAudio()) { // A channel needs to have a audio stream at least to play in exoPlayer. - mInternalListener.notifyVideoUnavailable( + mSession.notifyVideoUnavailable( TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); return; } @@ -1174,20 +1169,25 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, mPlayer.setPlayWhenReady(true); mPlayer.setVolume(mVolume); if (mChannel != null && !mChannel.hasVideo() && mChannel.hasAudio()) { - mInternalListener.notifyVideoUnavailable( + mSession.notifyVideoUnavailable( TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY); } else { - mInternalListener.notifyVideoUnavailable( + mSession.notifyVideoUnavailable( TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING); } - mInternalListener.sendUiMessage(TvInputSessionImpl.MSG_UI_HIDE_MESSAGE); + mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE); mPlayerStarted = true; } } private void playFromChannel(long timestamp) { long oldTimestamp; - mSource = getDataSource(mChannel.getType()); + mSource = null; + if (mChannel.getType() == Channel.TYPE_TUNER) { + mSource = mTunerSource; + } else if (mChannel.getType() == Channel.TYPE_FILE) { + mSource = mFileSource; + } Assert.assertNotNull(mSource); if (mSource.tuneToChannel(mChannel)) { if (ENABLE_PROFILER) { @@ -1199,7 +1199,6 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, mSource.startStream(); mPlayer = createPlayer(mAudioCapabilities, mCacheManager); mPlayer.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER); - mPlayer.addListener(this); mPlayer.setVideoEventListener(this); mPlayer.setCaptionServiceNumber(mCaptionTrack != null ? mCaptionTrack.serviceNumber : Cea708Data.EMPTY_SERVICE_NUMBER); @@ -1209,7 +1208,7 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, } else { // Close TunerHal when tune fails. mTunerHal.stopTune(); - mInternalListener.notifyVideoUnavailable( + mSession.notifyVideoUnavailable( TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); } } @@ -1221,7 +1220,6 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, mSource = null; mPlayer = createPlayer(mAudioCapabilities, cacheManager); mPlayer.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER); - mPlayer.addListener(this); mPlayer.setVideoEventListener(this); mPlayer.setCaptionServiceNumber(mCaptionTrack != null ? mCaptionTrack.serviceNumber : Cea708Data.EMPTY_SERVICE_NUMBER); @@ -1241,7 +1239,7 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, Log.i(TAG, "[Profiler] stopPlayback() takes " + (timestamp - oldTimestamp) + " ms"); } if (!mChannelBlocked && mSurface != null) { - mInternalListener.notifyVideoUnavailable( + mSession.notifyVideoUnavailable( TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING); if (mChannel != null) { playFromChannel(timestamp); @@ -1251,17 +1249,6 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, } } - private InputStreamSource getDataSource(int type) { - switch (type) { - case Channel.TYPE_TUNER: - return mTunerSource; - case Channel.TYPE_FILE: - return mFileSource; - default: - return null; - } - } - private void prepareTune(TunerChannel channel, String recording) { mChannelBlocked = false; mUnblockedContentRating = null; @@ -1413,7 +1400,7 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, mCaptionTrackMap.put(serviceNumber, captionTrack); mTvTracks.add(new TvTrackInfo.Builder(TvTrackInfo.TYPE_SUBTITLE, SUBTITLE_TRACK_PREFIX + serviceNumber).build()); - mInternalListener.notifyTracksChanged(mTvTracks); + mSession.notifyTracksChanged(mTvTracks); } } @@ -1448,13 +1435,13 @@ public class TvInputSessionImplInternal implements PlaybackCacheListener, stopPlayback(); resetTvTracks(); if (contentRating != null) { - mInternalListener.notifyContentBlocked(contentRating); + mSession.notifyContentBlocked(contentRating); } mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS); } else { mHandler.removeCallbacksAndMessages(null); resetPlayback(); - mInternalListener.notifyContentAllowed(); + mSession.notifyContentAllowed(); mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS, RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS); mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_INITIAL_DELAY_MS); diff --git a/usbtuner/src/com/android/usbtuner/tvinput/UsbTunerDebug.java b/usbtuner/src/com/android/usbtuner/tvinput/UsbTunerDebug.java index 5c24bed7..4732eea1 100644 --- a/usbtuner/src/com/android/usbtuner/tvinput/UsbTunerDebug.java +++ b/usbtuner/src/com/android/usbtuner/tvinput/UsbTunerDebug.java @@ -42,7 +42,6 @@ public class UsbTunerDebug { private long mAudioPtsUsRate; private long mVideoPtsUsRate; - private UsbTunerDebug() { mVideoFrameDrop = 0; mLastCheckTimestampMs = SystemClock.elapsedRealtime(); diff --git a/usbtuner/src/com/android/usbtuner/tvinput/UsbTunerTvInputService.java b/usbtuner/src/com/android/usbtuner/tvinput/UsbTunerTvInputService.java index 2397e5f2..e91d8769 100644 --- a/usbtuner/src/com/android/usbtuner/tvinput/UsbTunerTvInputService.java +++ b/usbtuner/src/com/android/usbtuner/tvinput/UsbTunerTvInputService.java @@ -16,12 +16,14 @@ package com.android.usbtuner.tvinput; +import android.content.ComponentName; +import android.content.Context; +import android.media.tv.TvContract; import android.os.Environment; import android.util.Log; -import com.android.tv.common.recording.TvRecording; -import com.android.usbtuner.exoplayer.CacheManager; -import com.android.usbtuner.exoplayer.TrickplayStorageManager; +import com.android.usbtuner.exoplayer.cache.CacheManager; +import com.android.usbtuner.exoplayer.cache.TrickplayStorageManager; import com.android.usbtuner.util.SystemPropertiesProxy; import java.io.File; @@ -39,7 +41,7 @@ public class UsbTunerTvInputService extends BaseTunerTvInputService { private static final int MIN_CACHE_SIZE_DEF = 256; // 256MB @Override - protected void maybeInitCacheManager() { + protected CacheManager createCacheManager() { int maxCacheSizeMb = SystemPropertiesProxy.getInt(MAX_CACHE_SIZE_KEY, MAX_CACHE_SIZE_DEF); if (maxCacheSizeMb >= MIN_CACHE_SIZE_DEF) { boolean useExternalStorage = Environment.MEDIA_MOUNTED.equals( @@ -49,11 +51,15 @@ public class UsbTunerTvInputService extends BaseTunerTvInputService { boolean allowToUseInternalStorage = true; if (useExternalStorage || allowToUseInternalStorage) { File baseDir = useExternalStorage ? getExternalCacheDir() : getCacheDir(); - mCacheManager = new CacheManager( + return new CacheManager( new TrickplayStorageManager(getApplicationContext(), baseDir, 1024L * 1024 * maxCacheSizeMb)); } } + return null; } + public static String getInputId(Context context) { + return TvContract.buildInputId(new ComponentName(context, UsbTunerTvInputService.class)); + } } diff --git a/usbtuner/src/com/google/android/exoplayer/MediaFormatUtil.java b/usbtuner/src/com/google/android/exoplayer/MediaFormatUtil.java new file mode 100644 index 00000000..5a5713e3 --- /dev/null +++ b/usbtuner/src/com/google/android/exoplayer/MediaFormatUtil.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer; + +import android.support.annotation.Nullable; + +import java.nio.ByteBuffer; +import java.util.ArrayList; + +/** {@link MediaFormat} creation helper util */ +public class MediaFormatUtil { + + /** + * Creates {@link MediaFormat} from {@link android.media.MediaFormat}. + * Since {@link com.google.android.exoplayer.TrackRenderer} uses {@link MediaFormat}, + * {@link android.media.MediaFormat} should be converted to be used with ExoPlayer. + */ + public static MediaFormat createMediaFormat(android.media.MediaFormat format) { + // TODO: Add test for this method. + String mimeType = format.getString(android.media.MediaFormat.KEY_MIME); + String language = getOptionalStringV16(format, android.media.MediaFormat.KEY_LANGUAGE); + int maxInputSize = + getOptionalIntegerV16(format, android.media.MediaFormat.KEY_MAX_INPUT_SIZE); + int width = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_WIDTH); + int height = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT); + int rotationDegrees = getOptionalIntegerV16(format, "rotation-degrees"); + int channelCount = + getOptionalIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT); + int sampleRate = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE); + int encoderDelay = getOptionalIntegerV16(format, "encoder-delay"); + int encoderPadding = getOptionalIntegerV16(format, "encoder-padding"); + ArrayList<byte[]> initializationData = new ArrayList<>(); + for (int i = 0; format.containsKey("csd-" + i); i++) { + ByteBuffer buffer = format.getByteBuffer("csd-" + i); + byte[] data = new byte[buffer.limit()]; + buffer.get(data); + initializationData.add(data); + buffer.flip(); + } + long durationUs = format.containsKey(android.media.MediaFormat.KEY_DURATION) + ? format.getLong(android.media.MediaFormat.KEY_DURATION) : C.UNKNOWN_TIME_US; + MediaFormat mediaFormat = new MediaFormat(null, mimeType, MediaFormat.NO_VALUE, + maxInputSize, durationUs, width, height, rotationDegrees, MediaFormat.NO_VALUE, + channelCount, sampleRate, language, MediaFormat.OFFSET_SAMPLE_RELATIVE, + initializationData, false, MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, encoderDelay, + encoderPadding); + mediaFormat.setFrameworkFormatV16(format); + return mediaFormat; + } + + /** + * Creates {@link MediaFormat} for audio track. + */ + public static MediaFormat createAudioMediaFormat(String mimeType, long durationUs, + int channelCount, int sampleRate) { + return MediaFormat.createAudioFormat(null, mimeType, MediaFormat.NO_VALUE, + MediaFormat.NO_VALUE, durationUs, channelCount, sampleRate, null, ""); + } + + /** + * Creates {@link MediaFormat} for closed caption track. + */ + public static MediaFormat createTextMediaFormat(String mimeType, long durationUs) { + return new MediaFormat(null, mimeType, 0, MediaFormat.NO_VALUE, durationUs, + MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, + MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, "", + MediaFormat.OFFSET_SAMPLE_RELATIVE, null, false, MediaFormat.NO_VALUE, + MediaFormat.NO_VALUE, MediaFormat.NO_VALUE, MediaFormat.NO_VALUE); + } + + @Nullable + private static final String getOptionalStringV16(android.media.MediaFormat format, String key) { + return format.containsKey(key) ? format.getString(key) : null; + } + + private static final int getOptionalIntegerV16(android.media.MediaFormat format, String key) { + return format.containsKey(key) ? format.getInteger(key) : MediaFormat.NO_VALUE; + } + +} diff --git a/usbtuner/src/com/google/android/exoplayer/MediaSoftwareCodecUtil.java b/usbtuner/src/com/google/android/exoplayer/MediaSoftwareCodecUtil.java new file mode 100644 index 00000000..4b605fbb --- /dev/null +++ b/usbtuner/src/com/google/android/exoplayer/MediaSoftwareCodecUtil.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer; + +import com.google.android.exoplayer.util.MimeTypes; + +import android.annotation.TargetApi; +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; +import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; + +import java.util.HashMap; + +/** + * Mostly copied from {@link com.google.android.exoplayer.MediaCodecUtil} in order to choose + * software codec over hardware codec. + */ +public class MediaSoftwareCodecUtil { + private static final String TAG = "MediaSoftwareCodecUtil"; + + /** + * Thrown when an error occurs querying the device for its underlying media capabilities. + * <p> + * Such failures are not expected in normal operation and are normally temporary (e.g. if the + * mediaserver process has crashed and is yet to restart). + */ + public static class DecoderQueryException extends Exception { + + private DecoderQueryException(Throwable cause) { + super("Failed to query underlying media codecs", cause); + } + + } + + private static final HashMap<CodecKey, Pair<String, MediaCodecInfo.CodecCapabilities>> + sSwCodecs = new HashMap<>(); + + /** + * Gets information about the software decoder that will be used for a given mime type. + */ + public static DecoderInfo getSoftwareDecoderInfo(String mimeType, boolean secure) + throws DecoderQueryException { + // TODO: Add a test for this method. + Pair<String, MediaCodecInfo.CodecCapabilities> info = + getMediaSoftwareCodecInfo(mimeType, secure); + if (info == null) { + return null; + } + return new DecoderInfo(info.first, info.second.isFeatureSupported( + MediaCodecInfo.CodecCapabilities.FEATURE_AdaptivePlayback)); + } + + /** + * Returns the name of the software decoder and its capabilities for the given mimeType. + */ + private static synchronized Pair<String, MediaCodecInfo.CodecCapabilities> + getMediaSoftwareCodecInfo(String mimeType, boolean secure) throws DecoderQueryException { + CodecKey key = new CodecKey(mimeType, secure); + if (sSwCodecs.containsKey(key)) { + return sSwCodecs.get(key); + } + MediaCodecListCompat mediaCodecList = new MediaCodecListCompatV21(secure); + Pair<String, MediaCodecInfo.CodecCapabilities> codecInfo = + getMediaSoftwareCodecInfo(key, mediaCodecList); + if (secure && codecInfo == null) { + // Some devices don't list secure decoders on API level 21. Try the legacy path. + mediaCodecList = new MediaCodecListCompatV16(); + codecInfo = getMediaSoftwareCodecInfo(key, mediaCodecList); + if (codecInfo != null) { + Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType + + ". Assuming: " + codecInfo.first); + } + } + return codecInfo; + } + + private static Pair<String, MediaCodecInfo.CodecCapabilities> getMediaSoftwareCodecInfo( + CodecKey key, MediaCodecListCompat mediaCodecList) throws DecoderQueryException { + try { + return getMediaSoftwareCodecInfoInternal(key, mediaCodecList); + } catch (Exception e) { + // If the underlying mediaserver is in a bad state, we may catch an + // IllegalStateException or an IllegalArgumentException here. + throw new DecoderQueryException(e); + } + } + + private static Pair<String, MediaCodecInfo.CodecCapabilities> getMediaSoftwareCodecInfoInternal( + CodecKey key, MediaCodecListCompat mediaCodecList) { + String mimeType = key.mimeType; + int numberOfCodecs = mediaCodecList.getCodecCount(); + boolean secureDecodersExplicit = mediaCodecList.secureDecodersExplicit(); + // Note: MediaCodecList is sorted by the framework such that the best decoders come first. + for (int i = 0; i < numberOfCodecs; i++) { + MediaCodecInfo info = mediaCodecList.getCodecInfoAt(i); + String codecName = info.getName(); + if (!info.isEncoder() && codecName.startsWith("OMX.google.") + && (secureDecodersExplicit || !codecName.endsWith(".secure"))) { + String[] supportedTypes = info.getSupportedTypes(); + for (int j = 0; j < supportedTypes.length; j++) { + String supportedType = supportedTypes[j]; + if (supportedType.equalsIgnoreCase(mimeType)) { + MediaCodecInfo.CodecCapabilities capabilities = + info.getCapabilitiesForType(supportedType); + boolean secure = mediaCodecList.isSecurePlaybackSupported( + key.mimeType, capabilities); + if (!secureDecodersExplicit) { + // Cache variants for both insecure and (if we think it's supported) + // secure playback. + sSwCodecs.put(key.secure ? new CodecKey(mimeType, false) : key, + Pair.create(codecName, capabilities)); + if (secure) { + sSwCodecs.put(key.secure ? key : new CodecKey(mimeType, true), + Pair.create(codecName + ".secure", capabilities)); + } + } else { + // Only cache this variant. If both insecure and secure decoders are + // available, they should both be listed separately. + sSwCodecs.put( + key.secure == secure ? key : new CodecKey(mimeType, secure), + Pair.create(codecName, capabilities)); + } + if (sSwCodecs.containsKey(key)) { + return sSwCodecs.get(key); + } + } + } + } + } + sSwCodecs.put(key, null); + return null; + } + + private interface MediaCodecListCompat { + + /** + * Returns the number of codecs in the list. + */ + public int getCodecCount(); + + /** + * Returns the info at the specified index in the list. + * + * @param index The index. + */ + public MediaCodecInfo getCodecInfoAt(int index); + + /** + * Returns whether secure decoders are explicitly listed, if present. + */ + public boolean secureDecodersExplicit(); + + /** + * Returns true if secure playback is supported for the given + * {@link android.media.MediaCodecInfo.CodecCapabilities}, which should + * have been obtained from a {@link MediaCodecInfo} obtained from this list. + */ + public boolean isSecurePlaybackSupported(String mimeType, + MediaCodecInfo.CodecCapabilities capabilities); + + } + + @TargetApi(21) + private static final class MediaCodecListCompatV21 implements MediaCodecListCompat { + + private final int codecKind; + + private MediaCodecInfo[] mediaCodecInfos; + + public MediaCodecListCompatV21(boolean includeSecure) { + codecKind = includeSecure ? MediaCodecList.ALL_CODECS : MediaCodecList.REGULAR_CODECS; + } + + @Override + public int getCodecCount() { + ensureMediaCodecInfosInitialized(); + return mediaCodecInfos.length; + } + + @Override + public MediaCodecInfo getCodecInfoAt(int index) { + ensureMediaCodecInfosInitialized(); + return mediaCodecInfos[index]; + } + + @Override + public boolean secureDecodersExplicit() { + return true; + } + + @Override + public boolean isSecurePlaybackSupported(String mimeType, + MediaCodecInfo.CodecCapabilities capabilities) { + return capabilities.isFeatureSupported( + MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback); + } + + private void ensureMediaCodecInfosInitialized() { + if (mediaCodecInfos == null) { + mediaCodecInfos = new MediaCodecList(codecKind).getCodecInfos(); + } + } + + } + + @SuppressWarnings("deprecation") + private static final class MediaCodecListCompatV16 implements MediaCodecListCompat { + + @Override + public int getCodecCount() { + return MediaCodecList.getCodecCount(); + } + + @Override + public MediaCodecInfo getCodecInfoAt(int index) { + return MediaCodecList.getCodecInfoAt(index); + } + + @Override + public boolean secureDecodersExplicit() { + return false; + } + + @Override + public boolean isSecurePlaybackSupported(String mimeType, + MediaCodecInfo.CodecCapabilities capabilities) { + // Secure decoders weren't explicitly listed prior to API level 21. We assume that + // a secure H264 decoder exists. + return MimeTypes.VIDEO_H264.equals(mimeType); + } + + } + + private static final class CodecKey { + + public final String mimeType; + public final boolean secure; + + public CodecKey(String mimeType, boolean secure) { + this.mimeType = mimeType; + this.secure = secure; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mimeType == null) ? 0 : mimeType.hashCode()); + result = 2 * result + (secure ? 0 : 1); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof CodecKey)) { + return false; + } + CodecKey other = (CodecKey) obj; + return TextUtils.equals(mimeType, other.mimeType) && secure == other.secure; + } + + } + +} diff --git a/usbtuner/src/com/google/android/exoplayer/text/SubtitleView.java b/usbtuner/src/com/google/android/exoplayer/text/SubtitleView.java new file mode 100644 index 00000000..b21d6be3 --- /dev/null +++ b/usbtuner/src/com/google/android/exoplayer/text/SubtitleView.java @@ -0,0 +1,309 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer.text; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Join; +import android.graphics.Paint.Style; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.text.Layout.Alignment; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.View; + +import com.google.android.exoplayer.util.Util; + +/** + * Since this class does not exist in recent version of ExoPlayer and used by + * {@link com.android.usbtuner.cc.CaptionWindowLayout}, this class is copied from + * older version of ExoPlayer. + * A view for rendering a single caption. + */ +@Deprecated +public class SubtitleView extends View { + // TODO: Change usage of this class to up-to-date class of ExoPlayer. + + /** + * Ratio of inner padding to font size. + */ + private static final float INNER_PADDING_RATIO = 0.125f; + + /** + * Temporary rectangle used for computing line bounds. + */ + private final RectF mLineBounds = new RectF(); + + // Styled dimensions. + private final float mCornerRadius; + private final float mOutlineWidth; + private final float mShadowRadius; + private final float mShadowOffset; + + private TextPaint mTextPaint; + private Paint mPaint; + + private CharSequence mText; + + private int mForegroundColor; + private int mBackgroundColor; + private int mEdgeColor; + private int mEdgeType; + + private boolean mHasMeasurements; + private int mLastMeasuredWidth; + private StaticLayout mLayout; + + private Alignment mAlignment; + private float mSpacingMult; + private float mSpacingAdd; + private int mInnerPaddingX; + + public SubtitleView(Context context) { + this(context, null); + } + + public SubtitleView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SubtitleView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + int[] viewAttr = {android.R.attr.text, android.R.attr.textSize, + android.R.attr.lineSpacingExtra, android.R.attr.lineSpacingMultiplier}; + TypedArray a = context.obtainStyledAttributes(attrs, viewAttr, defStyleAttr, 0); + CharSequence text = a.getText(0); + int textSize = a.getDimensionPixelSize(1, 15); + mSpacingAdd = a.getDimensionPixelSize(2, 0); + mSpacingMult = a.getFloat(3, 1); + a.recycle(); + + Resources resources = getContext().getResources(); + DisplayMetrics displayMetrics = resources.getDisplayMetrics(); + int twoDpInPx = + Math.round((2f * displayMetrics.densityDpi) / DisplayMetrics.DENSITY_DEFAULT); + mCornerRadius = twoDpInPx; + mOutlineWidth = twoDpInPx; + mShadowRadius = twoDpInPx; + mShadowOffset = twoDpInPx; + + mTextPaint = new TextPaint(); + mTextPaint.setAntiAlias(true); + mTextPaint.setSubpixelText(true); + + mAlignment = Alignment.ALIGN_CENTER; + + mPaint = new Paint(); + mPaint.setAntiAlias(true); + + mInnerPaddingX = 0; + setText(text); + setTextSize(textSize); + setStyle(CaptionStyleCompat.DEFAULT); + } + + @Override + public void setBackgroundColor(int color) { + mBackgroundColor = color; + forceUpdate(false); + } + + /** + * Sets the text to be displayed by the view. + * + * @param text The text to display. + */ + public void setText(CharSequence text) { + this.mText = text; + forceUpdate(true); + } + + /** + * Sets the text size in pixels. + * + * @param size The text size in pixels. + */ + public void setTextSize(float size) { + if (mTextPaint.getTextSize() != size) { + mTextPaint.setTextSize(size); + mInnerPaddingX = (int) (size * INNER_PADDING_RATIO + 0.5f); + forceUpdate(true); + } + } + + /** + * Sets the text alignment. + * + * @param textAlignment The text alignment. + */ + public void setTextAlignment(Alignment textAlignment) { + mAlignment = textAlignment; + } + + /** + * Configures the view according to the given style. + * + * @param style A style for the view. + */ + public void setStyle(CaptionStyleCompat style) { + mForegroundColor = style.foregroundColor; + mBackgroundColor = style.backgroundColor; + mEdgeType = style.edgeType; + mEdgeColor = style.edgeColor; + setTypeface(style.typeface); + super.setBackgroundColor(style.windowColor); + forceUpdate(true); + } + + private void setTypeface(Typeface typeface) { + if (mTextPaint.getTypeface() != typeface) { + mTextPaint.setTypeface(typeface); + forceUpdate(true); + } + } + + private void forceUpdate(boolean needsLayout) { + if (needsLayout) { + mHasMeasurements = false; + requestLayout(); + } + invalidate(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int widthSpec = MeasureSpec.getSize(widthMeasureSpec); + + if (computeMeasurements(widthSpec)) { + final StaticLayout layout = this.mLayout; + final int paddingX = getPaddingLeft() + getPaddingRight() + mInnerPaddingX * 2; + final int height = layout.getHeight() + getPaddingTop() + getPaddingBottom(); + int width = 0; + int lineCount = layout.getLineCount(); + for (int i = 0; i < lineCount; i++) { + width = Math.max((int) Math.ceil(layout.getLineWidth(i)), width); + } + width += paddingX; + setMeasuredDimension(width, height); + } else if (Util.SDK_INT >= 11) { + setTooSmallMeasureDimensionV11(); + } else { + setMeasuredDimension(0, 0); + } + } + + @TargetApi(11) + private void setTooSmallMeasureDimensionV11() { + setMeasuredDimension(MEASURED_STATE_TOO_SMALL, MEASURED_STATE_TOO_SMALL); + } + + @Override + public void onLayout(boolean changed, int l, int t, int r, int b) { + final int width = r - l; + computeMeasurements(width); + } + + private boolean computeMeasurements(int maxWidth) { + if (mHasMeasurements && maxWidth == mLastMeasuredWidth) { + return true; + } + + // Account for padding. + final int paddingX = getPaddingLeft() + getPaddingRight() + mInnerPaddingX * 2; + maxWidth -= paddingX; + if (maxWidth <= 0) { + return false; + } + + mHasMeasurements = true; + mLastMeasuredWidth = maxWidth; + mLayout = new StaticLayout(mText, mTextPaint, maxWidth, mAlignment, + mSpacingMult, mSpacingAdd, true); + return true; + } + + @Override + protected void onDraw(Canvas c) { + final StaticLayout layout = this.mLayout; + if (layout == null) { + return; + } + + final int saveCount = c.save(); + final int innerPaddingX = this.mInnerPaddingX; + c.translate(getPaddingLeft() + innerPaddingX, getPaddingTop()); + + final int lineCount = layout.getLineCount(); + final Paint textPaint = this.mTextPaint; + final Paint paint = this.mPaint; + final RectF bounds = mLineBounds; + + if (Color.alpha(mBackgroundColor) > 0) { + final float cornerRadius = this.mCornerRadius; + float previousBottom = layout.getLineTop(0); + + paint.setColor(mBackgroundColor); + paint.setStyle(Style.FILL); + + for (int i = 0; i < lineCount; i++) { + bounds.left = layout.getLineLeft(i) - innerPaddingX; + bounds.right = layout.getLineRight(i) + innerPaddingX; + bounds.top = previousBottom; + bounds.bottom = layout.getLineBottom(i); + previousBottom = bounds.bottom; + + c.drawRoundRect(bounds, cornerRadius, cornerRadius, paint); + } + } + + if (mEdgeType == CaptionStyleCompat.EDGE_TYPE_OUTLINE) { + textPaint.setStrokeJoin(Join.ROUND); + textPaint.setStrokeWidth(mOutlineWidth); + textPaint.setColor(mEdgeColor); + textPaint.setStyle(Style.FILL_AND_STROKE); + layout.draw(c); + } else if (mEdgeType == CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW) { + textPaint.setShadowLayer(mShadowRadius, mShadowOffset, mShadowOffset, mEdgeColor); + } else if (mEdgeType == CaptionStyleCompat.EDGE_TYPE_RAISED + || mEdgeType == CaptionStyleCompat.EDGE_TYPE_DEPRESSED) { + boolean raised = mEdgeType == CaptionStyleCompat.EDGE_TYPE_RAISED; + int colorUp = raised ? Color.WHITE : mEdgeColor; + int colorDown = raised ? mEdgeColor : Color.WHITE; + float offset = mShadowRadius / 2f; + textPaint.setColor(mForegroundColor); + textPaint.setStyle(Style.FILL); + textPaint.setShadowLayer(mShadowRadius, -offset, -offset, colorUp); + layout.draw(c); + textPaint.setShadowLayer(mShadowRadius, offset, offset, colorDown); + } + + textPaint.setColor(mForegroundColor); + textPaint.setStyle(Style.FILL); + layout.draw(c); + textPaint.setShadowLayer(0, 0, 0, 0); + c.restoreToCount(saveCount); + } + +} |