aboutsummaryrefslogtreecommitdiff
path: root/usbtuner
diff options
context:
space:
mode:
authorYoungsang Cho <youngsang@google.com>2016-05-09 18:52:12 -0700
committerYoungsang Cho <youngsang@google.com>2016-05-17 15:14:50 -0700
commit48dadb49248271b01997862e1335912a4f2e189f (patch)
treefb402e0e2bda1328fd9858b28a98e1c29563f038 /usbtuner
parent3a72b93e554bd22a5c64e71a6956d9604ce05108 (diff)
downloadTV-48dadb49248271b01997862e1335912a4f2e189f.tar.gz
DO NOT MERGE Sync to joey ub-tv-dev at e7fbaa585b1eb7afec05f05032d2e8d99fb595d4
Bug: 28469968 Change-Id: I74e368f5f58b433755932b806a90178e37bea7f9
Diffstat (limited to 'usbtuner')
-rw-r--r--usbtuner/Android.mk13
-rw-r--r--usbtuner/AndroidManifest.xml4
-rw-r--r--usbtuner/icu/icu4j/main/classes/core/src/com/ibm/icu/text/UnicodeDecompressor.java4
-rw-r--r--usbtuner/libs/exoplayer_1.5.6.jarbin0 -> 752820 bytes
-rw-r--r--usbtuner/libs/tv-exoplayer.jarbin552865 -> 0 bytes
-rw-r--r--usbtuner/res/xml/ut_tvinputservice.xml4
-rw-r--r--usbtuner/src/com/android/usbtuner/DvbDeviceAccessor.java25
-rw-r--r--usbtuner/src/com/android/usbtuner/FileDataSource.java11
-rw-r--r--usbtuner/src/com/android/usbtuner/TunerHal.java10
-rw-r--r--usbtuner/src/com/android/usbtuner/UsbTunerDataSource.java21
-rw-r--r--usbtuner/src/com/android/usbtuner/UsbTunerPreferences.java41
-rw-r--r--usbtuner/src/com/android/usbtuner/UsbTunerTsScannerSource.java2
-rw-r--r--usbtuner/src/com/android/usbtuner/cc/CaptionWindowLayout.java1
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/CachedSampleSourceExtractor.java331
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/Cea708TextTrackRenderer.java111
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/DefaultSampleSource.java163
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/MediaCodecVideoTrackRenderer.java609
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/MpegTsPassthroughAc3RendererBuilder.java32
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/MpegTsPlayer.java54
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/MpegTsSampleSource.java186
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/MpegTsSampleSourceExtractor.java62
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/MpegTsVideoTrackRenderer.java60
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/PlaySampleExtractor.java (renamed from usbtuner/src/com/android/usbtuner/exoplayer/BaseSampleSourceExtractor.java)118
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/RecordSampleSourceExtractor.java328
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/Recorder.java216
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/ReplaySampleSourceExtractor.java242
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/SampleExtractor.java10
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/ac3/Ac3TrackRenderer.java108
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/ac3/AudioClock.java (renamed from usbtuner/src/com/android/usbtuner/exoplayer/ac3/MediaClock.java)2
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/ac3/AudioTrackWrapper.java18
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/cache/CacheManager.java (renamed from usbtuner/src/com/android/usbtuner/exoplayer/CacheManager.java)114
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/cache/DvrStorageManager.java (renamed from usbtuner/src/com/android/usbtuner/exoplayer/DvrStorageManager.java)2
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/cache/RecordingSampleBuffer.java419
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/cache/SampleCache.java (renamed from usbtuner/src/com/android/usbtuner/exoplayer/SampleCache.java)2
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/cache/SamplePool.java (renamed from usbtuner/src/com/android/usbtuner/exoplayer/SamplePool.java)6
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/cache/SampleQueue.java (renamed from usbtuner/src/com/android/usbtuner/exoplayer/SampleQueue.java)2
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/cache/SimpleSampleBuffer.java (renamed from usbtuner/src/com/android/usbtuner/exoplayer/SimpleSampleSourceExtractor.java)148
-rw-r--r--usbtuner/src/com/android/usbtuner/exoplayer/cache/TrickplayStorageManager.java (renamed from usbtuner/src/com/android/usbtuner/exoplayer/TrickplayStorageManager.java)2
-rw-r--r--usbtuner/src/com/android/usbtuner/setup/ScanResultFragment.java9
-rw-r--r--usbtuner/src/com/android/usbtuner/tvinput/BaseTunerTvInputService.java60
-rw-r--r--usbtuner/src/com/android/usbtuner/tvinput/ChannelDataManager.java7
-rw-r--r--usbtuner/src/com/android/usbtuner/tvinput/DvrSessionImplInternal.java313
-rw-r--r--usbtuner/src/com/android/usbtuner/tvinput/InternalTunerTvInputService.java12
-rw-r--r--usbtuner/src/com/android/usbtuner/tvinput/RecordingSessionImpl.java87
-rw-r--r--usbtuner/src/com/android/usbtuner/tvinput/TunerRecordingSession.java114
-rw-r--r--usbtuner/src/com/android/usbtuner/tvinput/TunerRecordingSessionWorker.java549
-rw-r--r--usbtuner/src/com/android/usbtuner/tvinput/TunerSession.java (renamed from usbtuner/src/com/android/usbtuner/tvinput/TvInputSessionImpl.java)124
-rw-r--r--usbtuner/src/com/android/usbtuner/tvinput/TunerSessionWorker.java (renamed from usbtuner/src/com/android/usbtuner/tvinput/TvInputSessionImplInternal.java)265
-rw-r--r--usbtuner/src/com/android/usbtuner/tvinput/UsbTunerDebug.java1
-rw-r--r--usbtuner/src/com/android/usbtuner/tvinput/UsbTunerTvInputService.java16
-rw-r--r--usbtuner/src/com/google/android/exoplayer/MediaFormatUtil.java93
-rw-r--r--usbtuner/src/com/google/android/exoplayer/MediaSoftwareCodecUtil.java282
-rw-r--r--usbtuner/src/com/google/android/exoplayer/text/SubtitleView.java309
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 &lt; len);
*
* myDecompressor.reset(); // reuse decompressor
* </PRE>
diff --git a/usbtuner/libs/exoplayer_1.5.6.jar b/usbtuner/libs/exoplayer_1.5.6.jar
new file mode 100644
index 00000000..a0b311c9
--- /dev/null
+++ b/usbtuner/libs/exoplayer_1.5.6.jar
Binary files differ
diff --git a/usbtuner/libs/tv-exoplayer.jar b/usbtuner/libs/tv-exoplayer.jar
deleted file mode 100644
index a8ca7b2a..00000000
--- a/usbtuner/libs/tv-exoplayer.jar
+++ /dev/null
Binary files differ
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);
+ }
+
+}