aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/tuner
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/tv/tuner')
-rw-r--r--src/com/android/tv/tuner/ChannelScanFileParser.java2
-rw-r--r--src/com/android/tv/tuner/TunerHal.java43
-rw-r--r--src/com/android/tv/tuner/TunerInputController.java195
-rw-r--r--src/com/android/tv/tuner/TunerPreferences.java97
-rw-r--r--src/com/android/tv/tuner/UsbTunerHal.java6
-rw-r--r--src/com/android/tv/tuner/cc/CaptionLayout.java2
-rw-r--r--src/com/android/tv/tuner/cc/CaptionTrackRenderer.java2
-rw-r--r--src/com/android/tv/tuner/cc/Cea708Parser.java5
-rw-r--r--src/com/android/tv/tuner/data/PsiData.java4
-rw-r--r--src/com/android/tv/tuner/data/PsipData.java6
-rw-r--r--src/com/android/tv/tuner/data/TunerChannel.java130
-rw-r--r--src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java378
-rw-r--r--src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java14
-rw-r--r--src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java62
-rw-r--r--src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java13
-rw-r--r--src/com/android/tv/tuner/exoplayer/ac3/Ac3DefaultTrackRenderer.java (renamed from src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java)114
-rw-r--r--src/com/android/tv/tuner/exoplayer/ac3/Ac3MediaCodecTrackRenderer.java (renamed from src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java)21
-rw-r--r--src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java4
-rw-r--r--src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java20
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java280
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java209
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java23
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java23
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java115
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java1
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java1
-rw-r--r--src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java85
-rw-r--r--src/com/android/tv/tuner/setup/ConnectionTypeFragment.java18
-rw-r--r--src/com/android/tv/tuner/setup/PostalCodeFragment.java184
-rw-r--r--src/com/android/tv/tuner/setup/ScanFragment.java58
-rw-r--r--src/com/android/tv/tuner/setup/ScanResultFragment.java17
-rw-r--r--src/com/android/tv/tuner/setup/TunerSetupActivity.java238
-rw-r--r--src/com/android/tv/tuner/setup/WelcomeFragment.java38
-rw-r--r--src/com/android/tv/tuner/source/FileTsStreamer.java2
-rw-r--r--src/com/android/tv/tuner/source/TsDataSourceManager.java12
-rw-r--r--src/com/android/tv/tuner/source/TunerTsStreamer.java58
-rw-r--r--src/com/android/tv/tuner/source/TunerTsStreamerManager.java26
-rw-r--r--src/com/android/tv/tuner/ts/SectionParser.java20
-rw-r--r--src/com/android/tv/tuner/ts/TsParser.java18
-rw-r--r--src/com/android/tv/tuner/tvinput/ChannelDataManager.java54
-rw-r--r--src/com/android/tv/tuner/tvinput/EventDetector.java52
-rw-r--r--src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java4
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java116
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerSession.java17
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerSessionWorker.java300
-rw-r--r--src/com/android/tv/tuner/tvinput/TunerTvInputService.java30
-rw-r--r--src/com/android/tv/tuner/util/PostalCodeUtils.java89
-rw-r--r--src/com/android/tv/tuner/util/StringUtils.java38
-rw-r--r--src/com/android/tv/tuner/util/SystemPropertiesProxy.java16
-rw-r--r--src/com/android/tv/tuner/util/TunerInputInfoUtils.java46
50 files changed, 2408 insertions, 898 deletions
diff --git a/src/com/android/tv/tuner/ChannelScanFileParser.java b/src/com/android/tv/tuner/ChannelScanFileParser.java
index 2dd36074..8b06aaa9 100644
--- a/src/com/android/tv/tuner/ChannelScanFileParser.java
+++ b/src/com/android/tv/tuner/ChannelScanFileParser.java
@@ -18,7 +18,7 @@ package com.android.tv.tuner;
import android.util.Log;
-import com.android.tv.tuner.data.nano.Channel;
+import com.android.tv.tuner.data.Channel;
import java.io.BufferedReader;
import java.io.IOException;
diff --git a/src/com/android/tv/tuner/TunerHal.java b/src/com/android/tv/tuner/TunerHal.java
index de19766e..64394ea3 100644
--- a/src/com/android/tv/tuner/TunerHal.java
+++ b/src/com/android/tv/tuner/TunerHal.java
@@ -20,6 +20,9 @@ import android.content.Context;
import android.support.annotation.IntDef;
import android.support.annotation.StringDef;
import android.util.Log;
+import android.util.Pair;
+
+import com.android.tv.Features;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -48,6 +51,7 @@ public abstract class TunerHal implements AutoCloseable {
public static final int TUNER_TYPE_BUILT_IN = 1;
public static final int TUNER_TYPE_USB = 2;
+ public static final int TUNER_TYPE_NETWORK = 3;
protected static final int PID_PAT = 0;
protected static final int PID_ATSC_SI_BASE = 0x1ffb;
@@ -69,31 +73,33 @@ public abstract class TunerHal implements AutoCloseable {
*/
public synchronized static TunerHal createInstance(Context context) {
TunerHal tunerHal = null;
- if (getTunerType(context) == TUNER_TYPE_BUILT_IN) {
+ if (useBuiltInTuner(context)) {
}
- if (tunerHal == null) {
+ if (tunerHal == null && UsbTunerHal.getNumberOfDevices(context) > 0) {
tunerHal = new UsbTunerHal(context);
}
- if (tunerHal.openFirstAvailable()) {
- return tunerHal;
- }
- return null;
+ return tunerHal != null && tunerHal.openFirstAvailable() ? tunerHal : null;
}
/**
* Gets the number of tuner devices currently present.
*/
- public static int getTunerCount(Context context) {
- if (getTunerType(context) == TUNER_TYPE_BUILT_IN) {
+ public static Pair<Integer, Integer> getTunerTypeAndCount(Context context) {
+ if (useBuiltInTuner(context)) {
}
- return UsbTunerHal.getNumberOfDevices(context);
+ int usbTunerCount = UsbTunerHal.getNumberOfDevices(context);
+ if (usbTunerCount > 0) {
+ return new Pair<>(TUNER_TYPE_USB, usbTunerCount);
+ }
+ return new Pair<>(null, 0);
}
/**
- * Gets the type of tuner devices currently used.
+ * Returns if tuner input service would use built-in tuners instead of USB tuners or network
+ * tuners.
*/
- public static int getTunerType(Context context) {
- return TUNER_TYPE_USB;
+ static boolean useBuiltInTuner(Context context) {
+ return false;
}
protected TunerHal(Context context) {
@@ -106,6 +112,14 @@ public abstract class TunerHal implements AutoCloseable {
return mIsStreaming;
}
+ /**
+ * Returns {@code true} if this tuner HAL can be reused to save tuning time between channels
+ * of the same frequency.
+ */
+ public boolean isReusable() {
+ return true;
+ }
+
@Override
protected void finalize() throws Throwable {
super.finalize();
@@ -131,9 +145,12 @@ public abstract class TunerHal implements AutoCloseable {
*
* @param frequency a frequency of the channel to tune to
* @param modulation a modulation method of the channel to tune to
+ * @param channelNumber channel number when channel number is already known. Some tuner HAL
+ * may use channelNumber instead of frequency for tune.
* @return {@code true} if the operation was successful, {@code false} otherwise
*/
- public synchronized boolean tune(int frequency, @ModulationType String modulation) {
+ public synchronized boolean tune(int frequency, @ModulationType String modulation,
+ String channelNumber) {
if (!isDeviceOpen()) {
Log.e(TAG, "There's no available device");
return false;
diff --git a/src/com/android/tv/tuner/TunerInputController.java b/src/com/android/tv/tuner/TunerInputController.java
index d89b6a0c..65bbbdd0 100644
--- a/src/com/android/tv/tuner/TunerInputController.java
+++ b/src/com/android/tv/tuner/TunerInputController.java
@@ -16,30 +16,40 @@
package com.android.tv.tuner;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
-import android.media.tv.TvInputInfo;
-import android.media.tv.TvInputManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
-import android.support.v4.os.BuildCompat;
+import android.os.SystemClock;
+import android.preference.PreferenceManager;
+import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import com.android.tv.Features;
+import com.android.tv.R;
import com.android.tv.TvApplication;
-import com.android.tv.tuner.R;
import com.android.tv.tuner.setup.TunerSetupActivity;
import com.android.tv.tuner.tvinput.TunerTvInputService;
+import com.android.tv.tuner.util.SystemPropertiesProxy;
import com.android.tv.tuner.util.TunerInputInfoUtils;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
/**
* Controls the package visibility of {@link TunerTvInputService}.
@@ -51,10 +61,39 @@ import java.util.Map;
public class TunerInputController extends BroadcastReceiver {
private static final boolean DEBUG = true;
private static final String TAG = "TunerInputController";
+ private static final String PREFERENCE_IS_NETWORK_TUNER_ATTACHED = "network_tuner";
+ private static final String SECURITY_PATCH_LEVEL_KEY = "ro.build.version.security_patch";
+ private static final String SECURITY_PATCH_LEVEL_FORMAT = "yyyy-MM-dd";
+
+ /**
+ * Action of {@link Intent} to check network connection repeatedly when it is necessary.
+ */
+ public static final String CHECKING_NETWORK_CONNECTION =
+ "com.android.tv.action.CHECKING_NETWORK_CONNECTION";
+
+ /**
+ * Action of {@link Intent} when network tuner is attached.
+ */
+ public static final String NETWORK_TUNER_ATTACHED =
+ "com.android.tv.action.NETWORK_TUNER_ATTACHED";
+
+ /**
+ * Action of {@link Intent} when network tuner is detached.
+ */
+ public static final String NETWORK_TUNER_DETACHED =
+ "com.android.tv.action.NETWORK_TUNER_DETACHED";
+
+ private static final String EXTRA_CHECKING_DURATION =
+ "com.android.tv.action.extra.CHECKING_DURATION";
+
+ private static final long INITIAL_CHECKING_DURATION_MS = TimeUnit.SECONDS.toMillis(10);
+ private static final long MAXIMUM_CHECKING_DURATION_MS = TimeUnit.MINUTES.toMillis(10);
private static final TunerDevice[] TUNER_DEVICES = {
- new TunerDevice(0x2040, 0xb123), // WinTV-HVR-955Q
- new TunerDevice(0x07ca, 0x0837) // AverTV Volar Hybrid Q
+ new TunerDevice(0x2040, 0xb123, null), // WinTV-HVR-955Q
+ new TunerDevice(0x07ca, 0x0837, null), // AverTV Volar Hybrid Q
+ // WinTV-dualHD (bulk) will be supported after 2017 April security patch.
+ new TunerDevice(0x2040, 0x826d, "2017-04-01"), // WinTV-dualHD (bulk)
};
private static final int MSG_ENABLE_INPUT_SERVICE = 1000;
@@ -70,7 +109,9 @@ public class TunerInputController extends BroadcastReceiver {
if (mDvbDeviceAccessor == null) {
mDvbDeviceAccessor = new DvbDeviceAccessor(context);
}
- enableTunerTvInputService(context, mDvbDeviceAccessor.isDvbDeviceAvailable());
+ boolean enabled = mDvbDeviceAccessor.isDvbDeviceAvailable();
+ enableTunerTvInputService(
+ context, enabled, false, enabled ? TunerHal.TUNER_TYPE_USB : null);
break;
}
}
@@ -84,14 +125,35 @@ public class TunerInputController extends BroadcastReceiver {
private final int vendorId;
private final int productId;
- private TunerDevice(int vendorId, int productId) {
+ // security patch level from which the specific tuner type is supported.
+ private final String minSecurityLevel;
+
+ private TunerDevice(int vendorId, int productId, String minSecurityLevel) {
this.vendorId = vendorId;
this.productId = productId;
+ this.minSecurityLevel = minSecurityLevel;
}
private boolean equals(UsbDevice device) {
return device.getVendorId() == vendorId && device.getProductId() == productId;
}
+
+ private boolean isSupported(String currentSecurityLevel) {
+ if (minSecurityLevel == null) {
+ return true;
+ }
+
+ long supportSecurityLevelTimeStamp = 0;
+ long currentSecurityLevelTimestamp = 0;
+ try {
+ SimpleDateFormat format = new SimpleDateFormat(SECURITY_PATCH_LEVEL_FORMAT);
+ supportSecurityLevelTimeStamp = format.parse(minSecurityLevel).getTime();
+ currentSecurityLevelTimestamp = format.parse(currentSecurityLevel).getTime();
+ } catch (ParseException e) {
+ }
+ return supportSecurityLevelTimeStamp != 0
+ && supportSecurityLevelTimeStamp <= currentSecurityLevelTimestamp;
+ }
}
@Override
@@ -99,17 +161,20 @@ public class TunerInputController extends BroadcastReceiver {
if (DEBUG) Log.d(TAG, "Broadcast intent received:" + intent);
TvApplication.setCurrentRunningProcess(context, true);
if (!Features.TUNER.isEnabled(context)) {
- enableTunerTvInputService(context, false);
+ enableTunerTvInputService(context, false, false, null);
return;
}
+ SharedPreferences sharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(context);
switch (intent.getAction()) {
case Intent.ACTION_BOOT_COMPLETED:
+ executeNetworkTunerDiscoveryAsyncTask(context, INITIAL_CHECKING_DURATION_MS);
case TvApplication.ACTION_APPLICATION_FIRST_LAUNCHED:
case UsbManager.ACTION_USB_DEVICE_ATTACHED:
case UsbManager.ACTION_USB_DEVICE_DETACHED:
- if (TunerInputInfoUtils.isBuiltInTuner(context)) {
- enableTunerTvInputService(context, true);
+ if (TunerHal.useBuiltInTuner(context)) {
+ enableTunerTvInputService(context, true, false, TunerHal.TUNER_TYPE_BUILT_IN);
break;
}
// Falls back to the below to check USB tuner devices.
@@ -123,7 +188,41 @@ public class TunerInputController extends BroadcastReceiver {
mHandler.obtainMessage(MSG_ENABLE_INPUT_SERVICE, context),
DVB_DRIVER_CHECK_DELAY_MS);
} else {
- enableTunerTvInputService(context, false);
+ if (sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)) {
+ // Since network tuner is attached, do not disable TunerTvInput,
+ // just updates the TvInputInfo.
+ TunerInputInfoUtils.updateTunerInputInfo(context);
+ break;
+ }
+ enableTunerTvInputService(context, false, false, TextUtils
+ .equals(intent.getAction(), UsbManager.ACTION_USB_DEVICE_DETACHED) ?
+ TunerHal.TUNER_TYPE_USB : null);
+ }
+ break;
+ case CHECKING_NETWORK_CONNECTION:
+ long repeatedDurationMs = intent.getLongExtra(EXTRA_CHECKING_DURATION,
+ INITIAL_CHECKING_DURATION_MS);
+ executeNetworkTunerDiscoveryAsyncTask(context,
+ Math.min(repeatedDurationMs * 2, MAXIMUM_CHECKING_DURATION_MS));
+ break;
+ case NETWORK_TUNER_ATTACHED:
+ // Network tuner detection is initiated by UI. So the app should not
+ // be killed.
+ sharedPreferences.edit()
+ .putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, true).apply();
+ enableTunerTvInputService(context, true, true, TunerHal.TUNER_TYPE_NETWORK);
+ break;
+ case NETWORK_TUNER_DETACHED:
+ sharedPreferences.edit()
+ .putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false).apply();
+ if(!isUsbTunerConnected(context) && !TunerHal.useBuiltInTuner(context)) {
+ // Network tuner detection is initiated by UI. So the app should not
+ // be killed.
+ enableTunerTvInputService(context, false, true, TunerHal.TUNER_TYPE_NETWORK);
+ } else {
+ // Since USB tuner is attached, do not disable TunerTvInput,
+ // just updates the TvInputInfo.
+ TunerInputInfoUtils.updateTunerInputInfo(context);
}
break;
}
@@ -138,12 +237,15 @@ public class TunerInputController extends BroadcastReceiver {
private boolean isUsbTunerConnected(Context context) {
UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
Map<String, UsbDevice> deviceList = manager.getDeviceList();
+ String currentSecurityLevel =
+ SystemPropertiesProxy.getString(SECURITY_PATCH_LEVEL_KEY, null);
+
for (UsbDevice device : deviceList.values()) {
if (DEBUG) {
Log.d(TAG, "Device: " + device);
}
for (TunerDevice tuner : TUNER_DEVICES) {
- if (tuner.equals(device)) {
+ if (tuner.equals(device) && tuner.isSupported(currentSecurityLevel)) {
Log.i(TAG, "Tuner found");
return true;
}
@@ -158,7 +260,8 @@ public class TunerInputController extends BroadcastReceiver {
* @param context {@link Context} instance
* @param enabled {@code true} to enable the service; otherwise {@code false}
*/
- private void enableTunerTvInputService(Context context, boolean enabled) {
+ private void enableTunerTvInputService(Context context, boolean enabled,
+ boolean forceDontKillApp, Integer tunerType) {
if (DEBUG) Log.d(TAG, "enableTunerTvInputService: " + enabled);
PackageManager pm = context.getPackageManager();
ComponentName componentName = new ComponentName(context, TunerTvInputService.class);
@@ -170,7 +273,8 @@ public class TunerInputController extends BroadcastReceiver {
// Since PackageManager.DONT_KILL_APP delays the operation by 10 seconds
// (PackageManagerService.BROADCAST_DELAY), we'd better avoid using it. It is used only
// when the LiveChannels app is active since we don't want to kill the running app.
- int flags = TvApplication.getSingletons(context).getMainActivityWrapper().isCreated()
+ int flags = forceDontKillApp
+ || TvApplication.getSingletons(context).getMainActivityWrapper().isCreated()
? PackageManager.DONT_KILL_APP : 0;
int newState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
: PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
@@ -179,14 +283,67 @@ public class TunerInputController extends BroadcastReceiver {
TunerSetupActivity.onTvInputEnabled(context, enabled);
// Enable/disable the USB tuner TV input.
pm.setComponentEnabledSetting(componentName, newState, flags);
- if (!enabled) {
- Toast.makeText(
- context, R.string.msg_usb_device_detached, Toast.LENGTH_SHORT).show();
+ if (!enabled && tunerType != null) {
+ if (tunerType == TunerHal.TUNER_TYPE_USB) {
+ Toast.makeText(context, R.string.msg_usb_tuner_disconnected,
+ Toast.LENGTH_SHORT).show();
+ } else if (tunerType == TunerHal.TUNER_TYPE_NETWORK) {
+ Toast.makeText(context, R.string.msg_network_tuner_disconnected,
+ Toast.LENGTH_SHORT).show();
+ }
}
if (DEBUG) Log.d(TAG, "Status updated:" + enabled);
} else if (enabled) {
- // When # of USB tuners is changed or the device just boots.
+ // When # of tuners is changed or the tuner input service is switching from/to using
+ // network tuners or the device just boots.
TunerInputInfoUtils.updateTunerInputInfo(context);
}
}
+
+ /**
+ * Discovers a network tuner. If the network connection is down, it won't repeatedly checking.
+ */
+ public static void executeNetworkTunerDiscoveryAsyncTask(final Context context) {
+ executeNetworkTunerDiscoveryAsyncTask(context, 0);
+ }
+
+ /**
+ * Discovers a network tuner.
+ * @param context {@link Context}
+ * @param repeatedDurationMs the time length to wait to repeatedly check network status to start
+ * finding network tuner when the network connection is not available.
+ * {@code 0} to disable repeatedly checking.
+ */
+ private static void executeNetworkTunerDiscoveryAsyncTask(final Context context,
+ final long repeatedDurationMs) {
+ if (!Features.NETWORK_TUNER.isEnabled(context)) {
+ return;
+ }
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ if (isNetworkConnected(context)) {
+ // Implement and execute network tuner discovery AsyncTask here.
+ } else if (repeatedDurationMs > 0) {
+ AlarmManager alarmManager =
+ (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ Intent networkCheckingIntent = new Intent(context, TunerInputController.class);
+ networkCheckingIntent.setAction(CHECKING_NETWORK_CONNECTION);
+ networkCheckingIntent.putExtra(EXTRA_CHECKING_DURATION, repeatedDurationMs);
+ PendingIntent alarmIntent = PendingIntent.getBroadcast(
+ context, 0, networkCheckingIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ alarmManager.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime()
+ + repeatedDurationMs, alarmIntent);
+ }
+ return null;
+ }
+ }.execute();
+ }
+
+ private static boolean isNetworkConnected(Context context) {
+ ConnectivityManager cm = (ConnectivityManager)
+ context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo networkInfo = cm.getActiveNetworkInfo();
+ return networkInfo != null && networkInfo.isConnected();
+ }
}
diff --git a/src/com/android/tv/tuner/TunerPreferences.java b/src/com/android/tv/tuner/TunerPreferences.java
index 1547e3ae..a387be74 100644
--- a/src/com/android/tv/tuner/TunerPreferences.java
+++ b/src/com/android/tv/tuner/TunerPreferences.java
@@ -39,6 +39,7 @@ public class TunerPreferences {
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_LAST_POSTAL_CODE = "last_postal_code";
private static final String PREFS_KEY_SCAN_DONE = "scan_done";
private static final String PREFS_KEY_LAUNCH_SETUP = "launch_setup";
private static final String PREFS_KEY_STORE_TS_STREAM = "store_ts_stream";
@@ -86,8 +87,7 @@ public class TunerPreferences {
/**
* Releases the resources.
*/
- @MainThread
- public static void release(Context context) {
+ public static synchronized void release(Context context) {
if (useContentProvider(context) && sContentObserver != null) {
context.getContentResolver().unregisterContentObserver(sContentObserver);
}
@@ -99,7 +99,8 @@ public class TunerPreferences {
* This preferences is used across processes, so the preferences should be loaded again when the
* databases changes.
*/
- public static synchronized void loadPreferences(Context context) {
+ @MainThread
+ public static void loadPreferences(Context context) {
if (sLoadPreferencesTask != null
&& sLoadPreferencesTask.getStatus() != AsyncTask.Status.FINISHED) {
sLoadPreferencesTask.cancel(true);
@@ -113,8 +114,7 @@ public class TunerPreferences {
return TisConfiguration.isPackagedWithLiveChannels(context);
}
- @MainThread
- public static int getChannelDataVersion(Context context) {
+ public static synchronized int getChannelDataVersion(Context context) {
SoftPreconditions.checkState(sInitialized);
if (useContentProvider(context)) {
return sPreferenceValues.getInt(PREFS_KEY_CHANNEL_DATA_VERSION,
@@ -126,8 +126,7 @@ public class TunerPreferences {
}
}
- @MainThread
- public static void setChannelDataVersion(Context context, int version) {
+ public static synchronized void setChannelDataVersion(Context context, int version) {
if (useContentProvider(context)) {
setPreference(context, PREFS_KEY_CHANNEL_DATA_VERSION, version);
} else {
@@ -137,8 +136,7 @@ public class TunerPreferences {
}
}
- @MainThread
- public static int getScannedChannelCount(Context context) {
+ public static synchronized int getScannedChannelCount(Context context) {
SoftPreconditions.checkState(sInitialized);
if (useContentProvider(context)) {
return sPreferenceValues.getInt(PREFS_KEY_SCANNED_CHANNEL_COUNT);
@@ -148,8 +146,7 @@ public class TunerPreferences {
}
}
- @MainThread
- public static void setScannedChannelCount(Context context, int channelCount) {
+ public static synchronized void setScannedChannelCount(Context context, int channelCount) {
if (useContentProvider(context)) {
setPreference(context, PREFS_KEY_SCANNED_CHANNEL_COUNT, channelCount);
} else {
@@ -159,8 +156,25 @@ public class TunerPreferences {
}
}
- @MainThread
- public static boolean isScanDone(Context context) {
+ public static synchronized String getLastPostalCode(Context context) {
+ SoftPreconditions.checkState(sInitialized);
+ if (useContentProvider(context)) {
+ return sPreferenceValues.getString(PREFS_KEY_LAST_POSTAL_CODE);
+ } else {
+ return getSharedPreferences(context).getString(PREFS_KEY_LAST_POSTAL_CODE, null);
+ }
+ }
+
+ public static synchronized void setLastPostalCode(Context context, String postalCode) {
+ if (useContentProvider(context)) {
+ setPreference(context, PREFS_KEY_LAST_POSTAL_CODE, postalCode);
+ } else {
+ getSharedPreferences(context).edit()
+ .putString(PREFS_KEY_LAST_POSTAL_CODE, postalCode).apply();
+ }
+ }
+
+ public static synchronized boolean isScanDone(Context context) {
SoftPreconditions.checkState(sInitialized);
if (useContentProvider(context)) {
return sPreferenceValues.getBoolean(PREFS_KEY_SCAN_DONE);
@@ -170,8 +184,7 @@ public class TunerPreferences {
}
}
- @MainThread
- public static void setScanDone(Context context) {
+ public static synchronized void setScanDone(Context context) {
if (useContentProvider(context)) {
setPreference(context, PREFS_KEY_SCAN_DONE, true);
} else {
@@ -181,8 +194,7 @@ public class TunerPreferences {
}
}
- @MainThread
- public static boolean shouldShowSetupActivity(Context context) {
+ public static synchronized boolean shouldShowSetupActivity(Context context) {
SoftPreconditions.checkState(sInitialized);
if (useContentProvider(context)) {
return sPreferenceValues.getBoolean(PREFS_KEY_LAUNCH_SETUP);
@@ -192,8 +204,7 @@ public class TunerPreferences {
}
}
- @MainThread
- public static void setShouldShowSetupActivity(Context context, boolean need) {
+ public static synchronized void setShouldShowSetupActivity(Context context, boolean need) {
if (useContentProvider(context)) {
setPreference(context, PREFS_KEY_LAUNCH_SETUP, need);
} else {
@@ -203,8 +214,7 @@ public class TunerPreferences {
}
}
- @MainThread
- public static boolean getStoreTsStream(Context context) {
+ public static synchronized boolean getStoreTsStream(Context context) {
SoftPreconditions.checkState(sInitialized);
if (useContentProvider(context)) {
return sPreferenceValues.getBoolean(PREFS_KEY_STORE_TS_STREAM, false);
@@ -214,8 +224,7 @@ public class TunerPreferences {
}
}
- @MainThread
- public static void setStoreTsStream(Context context, boolean shouldStore) {
+ public static synchronized void setStoreTsStream(Context context, boolean shouldStore) {
if (useContentProvider(context)) {
setPreference(context, PREFS_KEY_STORE_TS_STREAM, shouldStore);
} else {
@@ -229,8 +238,23 @@ public class TunerPreferences {
return context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
}
- @MainThread
- private static void setPreference(final Context context, final String key, final String value) {
+ private static synchronized void setPreference(Context context, String key, String value) {
+ sPreferenceValues.putString(key, value);
+ savePreference(context, key, value);
+ }
+
+ private static synchronized void setPreference(Context context, String key, int value) {
+ sPreferenceValues.putInt(key, value);
+ savePreference(context, key, Integer.toString(value));
+ }
+
+ private static synchronized void setPreference(Context context, String key, boolean value) {
+ sPreferenceValues.putBoolean(key, value);
+ savePreference(context, key, Boolean.toString(value));
+ }
+
+ private static void savePreference(final Context context, final String key,
+ final String value) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
@@ -249,18 +273,6 @@ public class TunerPreferences {
}.execute();
}
- @MainThread
- private static void setPreference(Context context, String key, int value) {
- sPreferenceValues.putInt(key, value);
- setPreference(context, key, Integer.toString(value));
- }
-
- @MainThread
- private static void setPreference(Context context, String key, boolean value) {
- sPreferenceValues.putBoolean(key, value);
- setPreference(context, key, Boolean.toString(value));
- }
-
private static class LoadPreferencesTask extends AsyncTask<Void, Void, Bundle> {
private final Context mContext;
private LoadPreferencesTask(Context context) {
@@ -292,6 +304,9 @@ public class TunerPreferences {
case PREFS_KEY_STORE_TS_STREAM:
bundle.putBoolean(key, Boolean.parseBoolean(value));
break;
+ case PREFS_KEY_LAST_POSTAL_CODE:
+ bundle.putString(key, value);
+ break;
}
}
}
@@ -303,8 +318,10 @@ public class TunerPreferences {
}
@Override
- protected void onPostExecute(Bundle bundle) {
- sPreferenceValues.putAll(bundle);
+ protected synchronized void onPostExecute(Bundle bundle) {
+ if (bundle != null) {
+ sPreferenceValues.putAll(bundle);
+ }
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/tuner/UsbTunerHal.java b/src/com/android/tv/tuner/UsbTunerHal.java
index 22e35ea1..b1608ede 100644
--- a/src/com/android/tv/tuner/UsbTunerHal.java
+++ b/src/com/android/tv/tuner/UsbTunerHal.java
@@ -169,6 +169,10 @@ public class UsbTunerHal extends TunerHal {
* Gets the number of USB tuner devices currently present.
*/
public static int getNumberOfDevices(Context context) {
- return (new DvbDeviceAccessor(context)).getNumOfDvbDevices();
+ try {
+ return (new DvbDeviceAccessor(context)).getNumOfDvbDevices();
+ } catch (Exception e) {
+ return 0;
+ }
}
}
diff --git a/src/com/android/tv/tuner/cc/CaptionLayout.java b/src/com/android/tv/tuner/cc/CaptionLayout.java
index a88538df..c41f1014 100644
--- a/src/com/android/tv/tuner/cc/CaptionLayout.java
+++ b/src/com/android/tv/tuner/cc/CaptionLayout.java
@@ -19,7 +19,7 @@ package com.android.tv.tuner.cc;
import android.content.Context;
import android.util.AttributeSet;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import com.android.tv.tuner.layout.ScaledLayout;
/**
diff --git a/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java b/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java
index 3c75caa9..3aa40982 100644
--- a/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java
+++ b/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java
@@ -27,7 +27,7 @@ import com.android.tv.tuner.data.Cea708Data.CaptionPenColor;
import com.android.tv.tuner.data.Cea708Data.CaptionPenLocation;
import com.android.tv.tuner.data.Cea708Data.CaptionWindow;
import com.android.tv.tuner.data.Cea708Data.CaptionWindowAttr;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
diff --git a/src/com/android/tv/tuner/cc/Cea708Parser.java b/src/com/android/tv/tuner/cc/Cea708Parser.java
index 92ab0620..c43fe512 100644
--- a/src/com/android/tv/tuner/cc/Cea708Parser.java
+++ b/src/com/android/tv/tuner/cc/Cea708Parser.java
@@ -140,6 +140,7 @@ public class Cea708Parser {
private int mCommand = 0;
private int mListenServiceNumber = 0;
private boolean mDtvCcPacking = false;
+ private boolean mFirstServiceNumberDiscovered;
// Assign a dummy listener in order to avoid null checks.
private OnCea708ParserListener mListener = new OnCea708ParserListener() {
@@ -332,12 +333,14 @@ public class Cea708Parser {
mDiscoveredNumBytes.put(
serviceNumber, blockSize + mDiscoveredNumBytes.get(serviceNumber, 0));
}
- if (mLastDiscoveryLaunchedMs + DISCOVERY_PERIOD_MS < SystemClock.elapsedRealtime()) {
+ if (mLastDiscoveryLaunchedMs + DISCOVERY_PERIOD_MS < SystemClock.elapsedRealtime()
+ || !mFirstServiceNumberDiscovered) {
for (int i = 0; i < mDiscoveredNumBytes.size(); ++i) {
int discoveredNumBytes = mDiscoveredNumBytes.valueAt(i);
if (discoveredNumBytes >= DISCOVERY_NUM_BYTES_THRESHOLD) {
int discoveredServiceNumber = mDiscoveredNumBytes.keyAt(i);
mListener.discoverServiceNumber(discoveredServiceNumber);
+ mFirstServiceNumberDiscovered = true;
}
}
mDiscoveredNumBytes.clear();
diff --git a/src/com/android/tv/tuner/data/PsiData.java b/src/com/android/tv/tuner/data/PsiData.java
index 67700c6a..2c8a52db 100644
--- a/src/com/android/tv/tuner/data/PsiData.java
+++ b/src/com/android/tv/tuner/data/PsiData.java
@@ -17,8 +17,8 @@
package com.android.tv.tuner.data;
-import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import java.util.List;
diff --git a/src/com/android/tv/tuner/data/PsipData.java b/src/com/android/tv/tuner/data/PsipData.java
index aead4be8..ac7fdedb 100644
--- a/src/com/android/tv/tuner/data/PsipData.java
+++ b/src/com/android/tv/tuner/data/PsipData.java
@@ -20,11 +20,11 @@ import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.text.format.DateUtils;
-import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import com.android.tv.tuner.ts.SectionParser;
import com.android.tv.tuner.util.ConvertUtils;
-import com.android.tv.tuner.util.StringUtils;
+import com.android.tv.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
diff --git a/src/com/android/tv/tuner/data/TunerChannel.java b/src/com/android/tv/tuner/data/TunerChannel.java
index 89079d77..41f66e7d 100644
--- a/src/com/android/tv/tuner/data/TunerChannel.java
+++ b/src/com/android/tv/tuner/data/TunerChannel.java
@@ -19,12 +19,11 @@ package com.android.tv.tuner.data;
import android.support.annotation.NonNull;
import android.util.Log;
-import com.android.tv.tuner.data.nano.Channel;
-import com.android.tv.tuner.data.nano.Channel.TunerChannelProto;
-import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Channel.TunerChannelProto;
+import com.android.tv.tuner.data.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import com.android.tv.tuner.util.Ints;
-import com.android.tv.tuner.util.StringUtils;
+import com.android.tv.util.StringUtils;
import com.google.protobuf.nano.MessageNano;
import java.io.IOException;
@@ -40,6 +39,11 @@ import java.util.Objects;
public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracksInterface {
private static final String TAG = "TunerChannel";
+ /**
+ * Channel number separator between major number and minor number.
+ */
+ public static final char CHANNEL_NUMBER_SEPARATOR = '-';
+
// See ATSC Code Points Registry.
private static final String[] ATSC_SERVICE_TYPE_NAMES = new String[] {
"ATSC Reserved",
@@ -63,6 +67,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
// According to ISO13818-1, Mpeg2 StreamType has a range from 0x00 to 0xff.
public static final int INVALID_STREAMTYPE = -1;
+ // @GuardedBy(this) Writing operations and toByteArray will be guarded. b/34197766
private final TunerChannelProto mProto;
private TunerChannel(PsipData.VctItem channel, int programNumber,
@@ -145,6 +150,44 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return new TunerChannel(channel, 0, pmtItems, Channel.TYPE_FILE);
}
+ /**
+ * Create a TunerChannel object suitable for network tuners
+ * @param major Channel number major
+ * @param minor Channel number minor
+ * @param programNumber Program number
+ * @param shortName Short name
+ * @param recordingProhibited Recording prohibition info
+ * @param videoFormat Video format. Should be {@code null} or one of the followings:
+ * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_240P},
+ * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_360P},
+ * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_480I},
+ * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_480P},
+ * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_576I},
+ * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_576P},
+ * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_720P},
+ * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_1080I},
+ * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_1080P},
+ * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_2160P},
+ * {@link android.media.tv.TvContract.Channels#VIDEO_FORMAT_4320P}
+ * @return a TunerChannel object
+ */
+ public static TunerChannel forNetwork(int major, int minor, int programNumber,
+ String shortName, boolean recordingProhibited, String videoFormat) {
+ TunerChannel tunerChannel = new TunerChannel(programNumber, Collections.EMPTY_LIST);
+ tunerChannel.setVirtualMajor(major);
+ tunerChannel.setVirtualMinor(minor);
+ tunerChannel.setShortName(shortName);
+ // Set audio and video pids in order to work around the audio-only channel check.
+ tunerChannel.setAudioPids(new ArrayList<>(Arrays.asList(0)));
+ tunerChannel.selectAudioTrack(0);
+ tunerChannel.setVideoPid(0);
+ tunerChannel.setRecordingProhibited(recordingProhibited);
+ if (videoFormat != null) {
+ tunerChannel.setVideoFormat(videoFormat);
+ }
+ return tunerChannel;
+ }
+
public String getName() {
return (!mProto.shortName.isEmpty()) ? mProto.shortName : mProto.longName;
}
@@ -193,7 +236,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return mProto.videoPid;
}
- public void setVideoPid(int videoPid) {
+ synchronized public void setVideoPid(int videoPid) {
mProto.videoPid = videoPid;
}
@@ -219,7 +262,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return Ints.asList(mProto.audioPids);
}
- public void setAudioPids(List<Integer> audioPids) {
+ synchronized public void setAudioPids(List<Integer> audioPids) {
mProto.audioPids = Ints.toArray(audioPids);
}
@@ -227,7 +270,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return Ints.asList(mProto.audioStreamTypes);
}
- public void setAudioStreamTypes(List<Integer> audioStreamTypes) {
+ synchronized public void setAudioStreamTypes(List<Integer> audioStreamTypes) {
mProto.audioStreamTypes = Ints.toArray(audioStreamTypes);
}
@@ -239,32 +282,32 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return mProto.type;
}
- public void setFilepath(String filepath) {
- mProto.filepath = filepath;
+ synchronized public void setFilepath(String filepath) {
+ mProto.filepath = filepath == null ? "" : filepath;
}
public String getFilepath() {
return mProto.filepath;
}
- public void setVirtualMajor(int virtualMajor) {
+ synchronized public void setVirtualMajor(int virtualMajor) {
mProto.virtualMajor = virtualMajor;
}
- public void setVirtualMinor(int virtualMinor) {
+ synchronized public void setVirtualMinor(int virtualMinor) {
mProto.virtualMinor = virtualMinor;
}
- public void setShortName(String shortName) {
- mProto.shortName = shortName;
+ synchronized public void setShortName(String shortName) {
+ mProto.shortName = shortName == null ? "" : shortName;
}
- public void setFrequency(int frequency) {
+ synchronized public void setFrequency(int frequency) {
mProto.frequency = frequency;
}
- public void setModulation(String modulation) {
- mProto.modulation = modulation;
+ synchronized public void setModulation(String modulation) {
+ mProto.modulation = modulation == null ? "" : modulation;
}
public boolean hasVideo() {
@@ -279,13 +322,18 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return mProto.channelId;
}
- public void setChannelId(long channelId) {
+ synchronized public void setChannelId(long channelId) {
mProto.channelId = channelId;
}
public String getDisplayNumber() {
- if (mProto.virtualMajor != 0 && mProto.virtualMinor != 0) {
- return String.format("%d-%d", mProto.virtualMajor, mProto.virtualMinor);
+ return getDisplayNumber(true);
+ }
+
+ public String getDisplayNumber(boolean ignoreZeroMinorNumber) {
+ if (mProto.virtualMajor != 0 && (mProto.virtualMinor != 0 || !ignoreZeroMinorNumber)) {
+ return String.format("%d%c%d", mProto.virtualMajor, CHANNEL_NUMBER_SEPARATOR,
+ mProto.virtualMinor);
} else if (mProto.virtualMajor != 0) {
return Integer.toString(mProto.virtualMajor);
} else {
@@ -298,7 +346,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
}
@Override
- public void setHasCaptionTrack() {
+ synchronized public void setHasCaptionTrack() {
mProto.hasCaptionTrack = true;
}
@@ -312,7 +360,7 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return Collections.unmodifiableList(Arrays.asList(mProto.audioTracks));
}
- public void setAudioTracks(List<AtscAudioTrack> audioTracks) {
+ synchronized public void setAudioTracks(List<AtscAudioTrack> audioTracks) {
mProto.audioTracks = audioTracks.toArray(new AtscAudioTrack[audioTracks.size()]);
}
@@ -321,11 +369,11 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
return Collections.unmodifiableList(Arrays.asList(mProto.captionTracks));
}
- public void setCaptionTracks(List<AtscCaptionTrack> captionTracks) {
+ synchronized public void setCaptionTracks(List<AtscCaptionTrack> captionTracks) {
mProto.captionTracks = captionTracks.toArray(new AtscCaptionTrack[captionTracks.size()]);
}
- public void selectAudioTrack(int index) {
+ synchronized public void selectAudioTrack(int index) {
if (0 <= index && index < mProto.audioPids.length) {
mProto.audioTrackIndex = index;
} else {
@@ -333,6 +381,22 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
}
}
+ synchronized public void setRecordingProhibited(boolean recordingProhibited) {
+ mProto.recordingProhibited = recordingProhibited;
+ }
+
+ public boolean isRecordingProhibited() {
+ return mProto.recordingProhibited;
+ }
+
+ synchronized public void setVideoFormat(String videoFormat) {
+ mProto.videoFormat = videoFormat == null ? "" : videoFormat;
+ }
+
+ public String getVideoFormat() {
+ return mProto.videoFormat;
+ }
+
@Override
public String toString() {
switch (mProto.type) {
@@ -359,7 +423,10 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
if (ret != 0) {
return ret;
}
-
+ ret = StringUtils.compare(getName(), channel.getName());
+ if (ret != 0) {
+ return ret;
+ }
// For FileTsStreamer, file paths should be compared.
return StringUtils.compare(getFilepath(), channel.getFilepath());
}
@@ -374,12 +441,19 @@ public class TunerChannel implements Comparable<TunerChannel>, PsipData.TvTracks
@Override
public int hashCode() {
- return Objects.hash(getFrequency(), getProgramNumber(), getFilepath());
+ return Objects.hash(getFrequency(), getProgramNumber(), getName(), getFilepath());
}
// Serialization
- public byte[] toByteArray() {
- return MessageNano.toByteArray(mProto);
+ synchronized public byte[] toByteArray() {
+ try {
+ return MessageNano.toByteArray(mProto);
+ } catch (Exception e) {
+ // Retry toByteArray. b/34197766
+ Log.w(TAG, "TunerChannel or its variables are modified in multiple thread without lock",
+ e);
+ return MessageNano.toByteArray(mProto);
+ }
}
public static TunerChannel parseFrom(byte[] data) {
diff --git a/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
index c105e222..89641530 100644
--- a/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
+++ b/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java
@@ -23,17 +23,29 @@ import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
+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.extractor.ExtractorSampleSource;
-import com.google.android.exoplayer.extractor.ExtractorSampleSource.EventListener;
-import com.google.android.exoplayer.upstream.Allocator;
import com.google.android.exoplayer.upstream.DataSource;
-import com.google.android.exoplayer.upstream.DefaultAllocator;
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.Format;
+import com.google.android.exoplayer2.FormatHolder;
+import com.google.android.exoplayer2.Timeline;
+import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
+import com.google.android.exoplayer2.source.ExtractorMediaSource;
+import com.google.android.exoplayer2.source.ExtractorMediaSource.EventListener;
+import com.google.android.exoplayer2.source.MediaPeriod;
+import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.SampleStream;
+import com.google.android.exoplayer2.source.TrackGroupArray;
+import com.google.android.exoplayer2.trackselection.FixedTrackSelection;
+import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.upstream.DataSpec;
+import com.google.android.exoplayer2.upstream.DefaultAllocator;
+import com.android.tv.tuner.exoplayer.ac3.Ac3DefaultTrackRenderer;
import com.android.tv.tuner.exoplayer.buffer.BufferManager;
import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer;
import com.android.tv.tuner.exoplayer.buffer.SimpleSampleBuffer;
@@ -42,10 +54,11 @@ import com.android.tv.tuner.tvinput.PlaybackBufferListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicLong;
/**
* A class that extracts samples from a live broadcast stream while storing the sample on the disk.
@@ -54,11 +67,7 @@ import java.util.concurrent.atomic.AtomicLong;
public class ExoPlayerSampleExtractor implements SampleExtractor {
private static final String TAG = "ExoPlayerSampleExtracto";
- // Buffer segment size for memory allocator. Copied from demo implementation of ExoPlayer.
- private static final int BUFFER_SEGMENT_SIZE_IN_BYTES = 64 * 1024;
- // Buffer segment count for sample source. Copied from demo implementation of ExoPlayer.
- private static final int BUFFER_SEGMENT_COUNT = 256;
-
+ private static final int INVALID_TRACK_INDEX = -1;
private final HandlerThread mSourceReaderThread;
private final long mId;
@@ -70,36 +79,69 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
private AtomicBoolean mOnCompletionCalled = new AtomicBoolean();
private IOException mExceptionOnPrepare;
private List<MediaFormat> mTrackFormats;
+ private int mVideoTrackIndex = INVALID_TRACK_INDEX;
+ private boolean mVideoTrackMet;
+ private long mBaseSamplePts = Long.MIN_VALUE;
private HashMap<Integer, Long> mLastExtractedPositionUsMap = new HashMap<>();
+ private final List<Pair<Integer, SampleHolder>> mPendingSamples = new LinkedList<>();
private OnCompletionListener mOnCompletionListener;
private Handler mOnCompletionListenerHandler;
private IOException mError;
- public ExoPlayerSampleExtractor(Uri uri, DataSource source, BufferManager bufferManager,
+ public ExoPlayerSampleExtractor(Uri uri, final DataSource source, BufferManager bufferManager,
PlaybackBufferListener bufferListener, boolean isRecording) {
// It'll be used as a timeshift file chunk name's prefix.
mId = System.currentTimeMillis();
- Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE_IN_BYTES);
EventListener eventListener = new EventListener() {
-
@Override
- public void onLoadError(int sourceId, IOException e) {
- mError = e;
+ public void onLoadError(IOException error) {
+ mError = error;
}
};
mSourceReaderThread = new HandlerThread("SourceReaderThread");
- mSourceReaderWorker = new SourceReaderWorker(new ExtractorSampleSource(uri, source,
- allocator, BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE_IN_BYTES,
+ mSourceReaderWorker = new SourceReaderWorker(new ExtractorMediaSource(uri,
+ new com.google.android.exoplayer2.upstream.DataSource.Factory() {
+ @Override
+ public com.google.android.exoplayer2.upstream.DataSource createDataSource() {
+ // Returns an adapter implementation for ExoPlayer V2 DataSource interface.
+ return new com.google.android.exoplayer2.upstream.DataSource() {
+ @Override
+ public long open(DataSpec dataSpec) throws IOException {
+ return source.open(
+ new com.google.android.exoplayer.upstream.DataSpec(
+ dataSpec.uri, dataSpec.postBody,
+ dataSpec.absoluteStreamPosition, dataSpec.position,
+ dataSpec.length, dataSpec.key, dataSpec.flags));
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int readLength)
+ throws IOException {
+ return source.read(buffer, offset, readLength);
+ }
+
+ @Override
+ public Uri getUri() {
+ return null;
+ }
+
+ @Override
+ public void close() throws IOException {
+ source.close();
+ }
+ };
+ }
+ },
+ new DefaultExtractorsFactory(),
// Do not create a handler if we not on a looper. e.g. test.
- Looper.myLooper() != null ? new Handler() : null,
- eventListener, 0));
+ Looper.myLooper() != null ? new Handler() : null, eventListener));
if (isRecording) {
mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, false,
RecordingSampleBuffer.BUFFER_REASON_RECORDING);
} else {
- if (bufferManager == null || bufferManager.isDisabled()) {
+ if (bufferManager == null) {
mSampleBuffer = new SimpleSampleBuffer(bufferListener);
} else {
mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, true,
@@ -114,43 +156,141 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
mOnCompletionListenerHandler = handler;
}
- private class SourceReaderWorker implements Handler.Callback {
+ private class SourceReaderWorker implements Handler.Callback, MediaPeriod.Callback {
public static final int MSG_PREPARE = 1;
public static final int MSG_FETCH_SAMPLES = 2;
public static final int MSG_RELEASE = 3;
private static final int RETRY_INTERVAL_MS = 50;
- private final SampleSource mSampleSource;
- private SampleSource.SampleSourceReader mSampleSourceReader;
+ private final MediaSource mSampleSource;
+ private MediaPeriod mMediaPeriod;
+ private SampleStream[] mStreams;
private boolean[] mTrackMetEos;
private boolean mMetEos = false;
private long mCurrentPosition;
+ private DecoderInputBuffer mDecoderInputBuffer;
+ private SampleHolder mSampleHolder;
+ private boolean mPrepareRequested;
- public SourceReaderWorker(SampleSource sampleSource) {
+ public SourceReaderWorker(MediaSource sampleSource) {
mSampleSource = sampleSource;
+ mSampleSource.prepareSource(null, false, new MediaSource.Listener() {
+ @Override
+ public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
+ // Dynamic stream change is not supported yet. b/28169263
+ // For now, this will cause EOS and playback reset.
+ }
+ });
+ mDecoderInputBuffer = new DecoderInputBuffer(
+ DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
+ mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
+ }
+
+ MediaFormat convertFormat(Format format) {
+ if (format.sampleMimeType.startsWith("audio/")) {
+ return MediaFormat.createAudioFormat(format.id, format.sampleMimeType,
+ format.bitrate, format.maxInputSize,
+ com.google.android.exoplayer.C.UNKNOWN_TIME_US, format.channelCount,
+ format.sampleRate, format.initializationData, format.language,
+ format.pcmEncoding);
+ } else if (format.sampleMimeType.startsWith("video/")) {
+ return MediaFormat.createVideoFormat(
+ format.id, format.sampleMimeType, format.bitrate, format.maxInputSize,
+ com.google.android.exoplayer.C.UNKNOWN_TIME_US, format.width, format.height,
+ format.initializationData, format.rotationDegrees,
+ format.pixelWidthHeightRatio, format.projectionData, format.stereoMode);
+ } else if (format.sampleMimeType.endsWith("/cea-608")
+ || format.sampleMimeType.startsWith("text/")) {
+ return MediaFormat.createTextFormat(
+ format.id, format.sampleMimeType, format.bitrate,
+ com.google.android.exoplayer.C.UNKNOWN_TIME_US, format.language);
+ } else {
+ return MediaFormat.createFormatForMimeType(
+ format.id, format.sampleMimeType, format.bitrate,
+ com.google.android.exoplayer.C.UNKNOWN_TIME_US);
+ }
+ }
+
+ @Override
+ public void onPrepared(MediaPeriod mediaPeriod) {
+ if (mMediaPeriod == null) {
+ // This instance is already released while the extractor is preparing.
+ return;
+ }
+ TrackSelection.Factory selectionFactory = new FixedTrackSelection.Factory();
+ TrackGroupArray trackGroupArray = mMediaPeriod.getTrackGroups();
+ TrackSelection[] selections = new TrackSelection[trackGroupArray.length];
+ for (int i = 0; i < selections.length; ++i) {
+ selections[i] = selectionFactory.createTrackSelection(trackGroupArray.get(i), 0);
+ }
+ boolean retain[] = new boolean[trackGroupArray.length];
+ boolean reset[] = new boolean[trackGroupArray.length];
+ mStreams = new SampleStream[trackGroupArray.length];
+ mMediaPeriod.selectTracks(selections, retain, mStreams, reset, 0);
+ if (mTrackFormats == null) {
+ int trackCount = trackGroupArray.length;
+ mTrackMetEos = new boolean[trackCount];
+ List<MediaFormat> trackFormats = new ArrayList<>();
+ int videoTrackCount = 0;
+ for (int i = 0; i < trackCount; i++) {
+ Format format = trackGroupArray.get(i).getFormat(0);
+ if (format.sampleMimeType.startsWith("video/")) {
+ videoTrackCount++;
+ mVideoTrackIndex = i;
+ }
+ trackFormats.add(convertFormat(format));
+ }
+ if (videoTrackCount > 1) {
+ // Disable dropping samples when there are multiple video tracks.
+ mVideoTrackIndex = INVALID_TRACK_INDEX;
+ }
+ mTrackFormats = trackFormats;
+ List<String> ids = new ArrayList<>();
+ for (int i = 0; i < mTrackFormats.size(); i++) {
+ ids.add(String.format(Locale.ENGLISH, "%s_%x", Long.toHexString(mId), i));
+ }
+ try {
+ mSampleBuffer.init(ids, mTrackFormats);
+ } catch (IOException e) {
+ // In this case, we will not schedule any further operation.
+ // mExceptionOnPrepare will be notified to ExoPlayer, and ExoPlayer will
+ // call release() eventually.
+ mExceptionOnPrepare = e;
+ return;
+ }
+ mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES);
+ mPrepared = true;
+ }
+ }
+
+ @Override
+ public void onContinueLoadingRequested(MediaPeriod source) {
+ source.continueLoading(mCurrentPosition);
}
@Override
public boolean handleMessage(Message message) {
switch (message.what) {
case MSG_PREPARE:
- mPrepared = prepare();
- if (!mPrepared && mExceptionOnPrepare == null) {
- mSourceReaderHandler
- .sendEmptyMessageDelayed(MSG_PREPARE, RETRY_INTERVAL_MS);
- } else{
- mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES);
+ if (!mPrepareRequested) {
+ mPrepareRequested = true;
+ mMediaPeriod = mSampleSource.createPeriod(0,
+ new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE), 0);
+ mMediaPeriod.prepare(this);
+ try {
+ mMediaPeriod.maybeThrowPrepareError();
+ } catch (IOException e) {
+ mError = e;
+ }
}
return true;
case MSG_FETCH_SAMPLES:
boolean didSomething = false;
- SampleHolder sample = new SampleHolder(
- SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
ConditionVariable conditionVariable = new ConditionVariable();
- int trackCount = mSampleSourceReader.getTrackCount();
+ int trackCount = mStreams.length;
for (int i = 0; i < trackCount; ++i) {
- if (!mTrackMetEos[i] && SampleSource.NOTHING_READ
- != fetchSample(i, sample, conditionVariable)) {
+ if (!mTrackMetEos[i] && C.RESULT_NOTHING_READ
+ != fetchSample(i, mSampleHolder, conditionVariable)) {
if (mMetEos) {
// If mMetEos was on during fetchSample() due to an error,
// fetching from other tracks is not necessary.
@@ -159,6 +299,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
didSomething = true;
}
}
+ mMediaPeriod.continueLoading(mCurrentPosition);
if (!mMetEos) {
if (didSomething) {
mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES);
@@ -171,17 +312,10 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
}
return true;
case MSG_RELEASE:
- if (mSampleSourceReader != null) {
- if (mPrepared) {
- // ExtractorSampleSource expects all the tracks should be disabled
- // before releasing.
- int count = mSampleSourceReader.getTrackCount();
- for (int i = 0; i < count; ++i) {
- mSampleSourceReader.disable(i);
- }
- }
- mSampleSourceReader.release();
- mSampleSourceReader = null;
+ if (mMediaPeriod != null) {
+ mSampleSource.releasePeriod(mMediaPeriod);
+ mSampleSource.releaseSource();
+ mMediaPeriod = null;
}
cleanUp();
mSourceReaderHandler.removeCallbacksAndMessages(null);
@@ -190,91 +324,109 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
return false;
}
- private boolean prepare() {
- if (mSampleSourceReader == null) {
- mSampleSourceReader = mSampleSource.register();
- }
- if(!mSampleSourceReader.prepare(0)) {
- return false;
- }
- if (mTrackFormats == null) {
- int trackCount = mSampleSourceReader.getTrackCount();
- mTrackMetEos = new boolean[trackCount];
- List<MediaFormat> trackFormats = new ArrayList<>();
- for (int i = 0; i < trackCount; i++) {
- trackFormats.add(mSampleSourceReader.getFormat(i));
- mSampleSourceReader.enable(i, 0);
-
- }
- mTrackFormats = trackFormats;
- List<String> ids = new ArrayList<>();
- for (int i = 0; i < mTrackFormats.size(); i++) {
- ids.add(String.format(Locale.ENGLISH, "%s_%x", Long.toHexString(mId), i));
- }
- try {
- mSampleBuffer.init(ids, mTrackFormats);
- } catch (IOException e) {
- // In this case, we will not schedule any further operation.
- // mExceptionOnPrepare will be notified to ExoPlayer, and ExoPlayer will
- // call release() eventually.
- mExceptionOnPrepare = e;
- return false;
- }
- }
- return true;
- }
-
private int fetchSample(int track, SampleHolder sample,
ConditionVariable conditionVariable) {
- mSampleSourceReader.continueBuffering(track, mCurrentPosition);
-
- MediaFormatHolder formatHolder = new MediaFormatHolder();
- sample.clearData();
- int ret = mSampleSourceReader.readData(track, mCurrentPosition, formatHolder, sample);
- if (ret == SampleSource.SAMPLE_READ) {
- if (mCurrentPosition < sample.timeUs) {
- mCurrentPosition = sample.timeUs;
+ FormatHolder dummyFormatHolder = new FormatHolder();
+ mDecoderInputBuffer.clear();
+ int ret = mStreams[track].readData(dummyFormatHolder, mDecoderInputBuffer);
+ if (ret == C.RESULT_BUFFER_READ
+ // Double-check if the extractor provided the data to prevent NPE. b/33758354
+ && mDecoderInputBuffer.data != null) {
+ if (mCurrentPosition < mDecoderInputBuffer.timeUs) {
+ mCurrentPosition = mDecoderInputBuffer.timeUs;
}
try {
Long lastExtractedPositionUs = mLastExtractedPositionUsMap.get(track);
if (lastExtractedPositionUs == null) {
- mLastExtractedPositionUsMap.put(track, sample.timeUs);
+ mLastExtractedPositionUsMap.put(track, mDecoderInputBuffer.timeUs);
} else {
mLastExtractedPositionUsMap.put(track,
- Math.max(lastExtractedPositionUs, sample.timeUs));
+ Math.max(lastExtractedPositionUs, mDecoderInputBuffer.timeUs));
}
- queueSample(track, sample, conditionVariable);
+ queueSample(track, conditionVariable);
} catch (IOException e) {
mLastExtractedPositionUsMap.clear();
mMetEos = true;
mSampleBuffer.setEos();
}
- } else if (ret == SampleSource.END_OF_STREAM) {
+ } else if (ret == C.RESULT_END_OF_INPUT) {
mTrackMetEos[track] = true;
for (int i = 0; i < mTrackMetEos.length; ++i) {
if (!mTrackMetEos[i]) {
break;
}
- if (i == mTrackMetEos.length -1) {
+ if (i == mTrackMetEos.length - 1) {
mMetEos = true;
mSampleBuffer.setEos();
}
}
}
- // TODO: Handle SampleSource.FORMAT_READ for dynamic resolution change. b/28169263
+ // TODO: Handle C.RESULT_FORMAT_READ for dynamic resolution change. b/28169263
return ret;
}
- }
-
- private void queueSample(int index, SampleHolder sample, ConditionVariable conditionVariable)
- throws IOException {
- long writeStartTimeNs = SystemClock.elapsedRealtimeNanos();
- mSampleBuffer.writeSample(index, sample, conditionVariable);
- // Checks whether the storage has enough bandwidth for recording samples.
- if (mSampleBuffer.isWriteSpeedSlow(sample.size,
- SystemClock.elapsedRealtimeNanos() - writeStartTimeNs)) {
- mSampleBuffer.handleWriteSpeedSlow();
+ private void queueSample(int index, ConditionVariable conditionVariable)
+ throws IOException {
+ if (mVideoTrackIndex != INVALID_TRACK_INDEX) {
+ if (!mVideoTrackMet) {
+ if (index != mVideoTrackIndex) {
+ SampleHolder sample =
+ new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL);
+ mSampleHolder.flags =
+ (mDecoderInputBuffer.isKeyFrame()
+ ? com.google.android.exoplayer.C.SAMPLE_FLAG_SYNC : 0)
+ | (mDecoderInputBuffer.isDecodeOnly()
+ ? com.google.android.exoplayer.C.SAMPLE_FLAG_DECODE_ONLY
+ : 0);
+ sample.timeUs = mDecoderInputBuffer.timeUs;
+ sample.size = mDecoderInputBuffer.data.position();
+ sample.ensureSpaceForWrite(sample.size);
+ mDecoderInputBuffer.flip();
+ sample.data.position(0);
+ sample.data.put(mDecoderInputBuffer.data);
+ sample.data.flip();
+ mPendingSamples.add(new Pair<>(index, sample));
+ return;
+ }
+ mVideoTrackMet = true;
+ mBaseSamplePts =
+ mDecoderInputBuffer.timeUs
+ - Ac3DefaultTrackRenderer.INITIAL_AUDIO_BUFFERING_TIME_US;
+ for (Pair<Integer, SampleHolder> pair : mPendingSamples) {
+ if (pair.second.timeUs >= mBaseSamplePts) {
+ mSampleBuffer.writeSample(pair.first, pair.second, conditionVariable);
+ }
+ }
+ mPendingSamples.clear();
+ } else {
+ if (mDecoderInputBuffer.timeUs < mBaseSamplePts
+ && mVideoTrackIndex != index) {
+ return;
+ }
+ }
+ }
+ // Copy the decoder input to the sample holder.
+ mSampleHolder.clearData();
+ mSampleHolder.flags =
+ (mDecoderInputBuffer.isKeyFrame()
+ ? com.google.android.exoplayer.C.SAMPLE_FLAG_SYNC : 0)
+ | (mDecoderInputBuffer.isDecodeOnly()
+ ? com.google.android.exoplayer.C.SAMPLE_FLAG_DECODE_ONLY : 0);
+ mSampleHolder.timeUs = mDecoderInputBuffer.timeUs;
+ mSampleHolder.size = mDecoderInputBuffer.data.position();
+ mSampleHolder.ensureSpaceForWrite(mSampleHolder.size);
+ mDecoderInputBuffer.flip();
+ mSampleHolder.data.position(0);
+ mSampleHolder.data.put(mDecoderInputBuffer.data);
+ mSampleHolder.data.flip();
+ long writeStartTimeNs = SystemClock.elapsedRealtimeNanos();
+ mSampleBuffer.writeSample(index, mSampleHolder, conditionVariable);
+
+ // Checks whether the storage has enough bandwidth for recording samples.
+ if (mSampleBuffer.isWriteSpeedSlow(mSampleHolder.size,
+ SystemClock.elapsedRealtimeNanos() - writeStartTimeNs)) {
+ mSampleBuffer.handleWriteSpeedSlow();
+ }
}
}
@@ -328,7 +480,7 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
}
@Override
- public boolean continueBuffering(long positionUs) {
+ public boolean continueBuffering(long positionUs) {
return mSampleBuffer.continueBuffering(positionUs);
}
@@ -386,12 +538,14 @@ public class ExoPlayerSampleExtractor implements SampleExtractor {
}
private long getLastExtractedPositionUs() {
- long lastExtractedPositionUs = Long.MAX_VALUE;
- for (long value : mLastExtractedPositionUsMap.values()) {
- lastExtractedPositionUs = Math.min(lastExtractedPositionUs, value);
+ long lastExtractedPositionUs = Long.MIN_VALUE;
+ for (Map.Entry<Integer, Long> entry : mLastExtractedPositionUsMap.entrySet()) {
+ if (mVideoTrackIndex != entry.getKey()) {
+ lastExtractedPositionUs = Math.max(lastExtractedPositionUs, entry.getValue());
+ }
}
- if (lastExtractedPositionUs == Long.MAX_VALUE) {
- lastExtractedPositionUs = C.UNKNOWN_TIME_US;
+ if (lastExtractedPositionUs == Long.MIN_VALUE) {
+ lastExtractedPositionUs = com.google.android.exoplayer.C.UNKNOWN_TIME_US;
}
return lastExtractedPositionUs;
}
diff --git a/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java
index ec7b4b16..b7e42a7c 100644
--- a/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java
+++ b/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java
@@ -25,7 +25,6 @@ import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer;
import com.android.tv.tuner.tvinput.PlaybackBufferListener;
import android.os.Handler;
-import android.util.Pair;
import java.io.IOException;
import java.util.ArrayList;
@@ -61,18 +60,17 @@ public class FileSampleExtractor implements SampleExtractor{
@Override
public boolean prepare() throws IOException {
- ArrayList<Pair<String, android.media.MediaFormat>> trackInfos =
- mBufferManager.readTrackInfoFiles();
- if (trackInfos == null || trackInfos.isEmpty()) {
+ List<BufferManager.TrackFormat> trackFormatList = mBufferManager.readTrackInfoFiles();
+ if (trackFormatList == null || trackFormatList.isEmpty()) {
throw new IOException("Cannot find meta files for the recording.");
}
- mTrackCount = trackInfos.size();
+ mTrackCount = trackFormatList.size();
List<String> ids = new ArrayList<>();
mTrackFormats.clear();
for (int i = 0; i < mTrackCount; ++i) {
- Pair<String, android.media.MediaFormat> pair = trackInfos.get(i);
- ids.add(pair.first);
- mTrackFormats.add(MediaFormatUtil.createMediaFormat(pair.second));
+ BufferManager.TrackFormat trackFormat = trackFormatList.get(i);
+ ids.add(trackFormat.trackId);
+ mTrackFormats.add(MediaFormatUtil.createMediaFormat(trackFormat.format));
}
mSampleBuffer = new RecordingSampleBuffer(mBufferManager, mBufferListener, true,
RecordingSampleBuffer.BUFFER_REASON_RECORDED_PLAYBACK);
diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java b/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
index 381b22e9..ba0edf20 100644
--- a/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
+++ b/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java
@@ -39,8 +39,8 @@ import com.android.tv.common.SoftPreconditions;
import com.android.tv.tuner.data.Cea708Data;
import com.android.tv.tuner.data.Cea708Data.CaptionEvent;
import com.android.tv.tuner.data.TunerChannel;
-import com.android.tv.tuner.exoplayer.ac3.Ac3PassthroughTrackRenderer;
-import com.android.tv.tuner.exoplayer.ac3.Ac3TrackRenderer;
+import com.android.tv.tuner.exoplayer.ac3.Ac3DefaultTrackRenderer;
+import com.android.tv.tuner.exoplayer.ac3.Ac3MediaCodecTrackRenderer;
import com.android.tv.tuner.source.TsDataSource;
import com.android.tv.tuner.source.TsDataSourceManager;
import com.android.tv.tuner.tvinput.EventDetector;
@@ -48,11 +48,12 @@ import com.android.tv.tuner.tvinput.EventDetector;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-/**
- * MPEG-2 TS stream player implementation using ExoPlayer.
- */
-public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRenderer.EventListener,
- Ac3PassthroughTrackRenderer.EventListener, Ac3TrackRenderer.Ac3EventListener {
+/** MPEG-2 TS stream player implementation using ExoPlayer. */
+public class MpegTsPlayer
+ implements ExoPlayer.Listener,
+ MediaCodecVideoTrackRenderer.EventListener,
+ Ac3DefaultTrackRenderer.EventListener,
+ Ac3MediaCodecTrackRenderer.Ac3EventListener {
private int mCaptionServiceNumber = Cea708Data.EMPTY_SERVICE_NUMBER;
/**
@@ -304,8 +305,10 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
SoftPreconditions.checkState(supportSmoothTrickPlay(playbackParams.getSpeed()));
mPlayer.setPlayWhenReady(true);
mTrickplayRunning = true;
- if (mAudioRenderer instanceof Ac3PassthroughTrackRenderer) {
- mPlayer.sendMessage(mAudioRenderer, Ac3PassthroughTrackRenderer.MSG_SET_PLAYBACK_SPEED,
+ if (mAudioRenderer instanceof Ac3DefaultTrackRenderer) {
+ mPlayer.sendMessage(
+ mAudioRenderer,
+ Ac3DefaultTrackRenderer.MSG_SET_PLAYBACK_SPEED,
playbackParams.getSpeed());
} else {
mPlayer.sendMessage(mAudioRenderer,
@@ -317,10 +320,9 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
private void stopSmoothTrickplay(boolean calledBySeek) {
if (mTrickplayRunning) {
mTrickplayRunning = false;
- if (mAudioRenderer instanceof Ac3PassthroughTrackRenderer) {
- mPlayer.sendMessage(mAudioRenderer,
- Ac3PassthroughTrackRenderer.MSG_SET_PLAYBACK_SPEED,
- 1.0f);
+ if (mAudioRenderer instanceof Ac3DefaultTrackRenderer) {
+ mPlayer.sendMessage(
+ mAudioRenderer, Ac3DefaultTrackRenderer.MSG_SET_PLAYBACK_SPEED, 1.0f);
} else {
mPlayer.sendMessage(mAudioRenderer,
MediaCodecAudioTrackRenderer.MSG_SET_PLAYBACK_PARAMS,
@@ -423,8 +425,8 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
*/
public void setVolume(float volume) {
mVolume = volume;
- if (mAudioRenderer instanceof Ac3PassthroughTrackRenderer) {
- mPlayer.sendMessage(mAudioRenderer, Ac3PassthroughTrackRenderer.MSG_SET_VOLUME, volume);
+ if (mAudioRenderer instanceof Ac3DefaultTrackRenderer) {
+ mPlayer.sendMessage(mAudioRenderer, Ac3DefaultTrackRenderer.MSG_SET_VOLUME, volume);
} else {
mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME,
volume);
@@ -437,9 +439,9 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
* @param enable enables the audio when {@code true}, disables otherwise.
*/
public void setAudioTrack(boolean enable) {
- if (mAudioRenderer instanceof Ac3PassthroughTrackRenderer) {
- mPlayer.sendMessage(mAudioRenderer, Ac3PassthroughTrackRenderer.MSG_SET_AUDIO_TRACK,
- enable ? 1 : 0);
+ if (mAudioRenderer instanceof Ac3DefaultTrackRenderer) {
+ mPlayer.sendMessage(
+ mAudioRenderer, Ac3DefaultTrackRenderer.MSG_SET_AUDIO_TRACK, enable ? 1 : 0);
} else {
mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME,
enable ? mVolume : 0.0f);
@@ -495,6 +497,28 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
}
/**
+ * Returns the index of the currently selected track for the specified renderer.
+ *
+ * @param rendererIndex The index of the renderer.
+ * @return The selected track. A negative value or a value greater than or equal to the renderer's
+ * track count indicates that the renderer is disabled.
+ */
+ public int getSelectedTrack(int rendererIndex) {
+ return mPlayer.getSelectedTrack(rendererIndex);
+ }
+
+ /**
+ * Returns the format of a track.
+ *
+ * @param rendererIndex The index of the renderer.
+ * @param trackIndex The index of the track.
+ * @return The format of the track.
+ */
+ public MediaFormat getTrackFormat(int rendererIndex, int trackIndex) {
+ return mPlayer.getTrackFormat(rendererIndex, trackIndex);
+ }
+
+ /**
* Gets the main handler of the player.
*/
/* package */ Handler getMainHandler() {
@@ -650,4 +674,4 @@ public class MpegTsPlayer implements ExoPlayer.Listener, MediaCodecVideoTrackRen
}
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java b/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
index 0e46c9cf..a1a97d3d 100644
--- a/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
+++ b/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java
@@ -21,9 +21,10 @@ import android.content.Context;
import com.google.android.exoplayer.SampleSource;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.upstream.DataSource;
+import com.android.tv.Features;
import com.android.tv.tuner.exoplayer.MpegTsPlayer.RendererBuilder;
import com.android.tv.tuner.exoplayer.MpegTsPlayer.RendererBuilderCallback;
-import com.android.tv.tuner.exoplayer.ac3.Ac3PassthroughTrackRenderer;
+import com.android.tv.tuner.exoplayer.ac3.Ac3DefaultTrackRenderer;
import com.android.tv.tuner.exoplayer.buffer.BufferManager;
import com.android.tv.tuner.tvinput.PlaybackBufferListener;
@@ -52,10 +53,12 @@ public class MpegTsRendererBuilder implements RendererBuilder {
SampleSource sampleSource = new MpegTsSampleSource(extractor);
MpegTsVideoTrackRenderer videoRenderer = new MpegTsVideoTrackRenderer(mContext,
sampleSource, mpegTsPlayer.getMainHandler(), mpegTsPlayer);
- // TODO: Only using Ac3PassthroughTrackRenderer for A/V sync issue. We will use
- // {@link Ac3TrackRenderer} when we use ExoPlayer's extractor.
- TrackRenderer audioRenderer = new Ac3PassthroughTrackRenderer(sampleSource,
- mpegTsPlayer.getMainHandler(), mpegTsPlayer);
+ // TODO: Only using Ac3DefaultTrackRenderer for A/V sync issue. We will use
+ // {@link Ac3MediaCodecTrackRenderer} when we use ExoPlayer's extractor.
+ TrackRenderer audioRenderer =
+ new Ac3DefaultTrackRenderer(
+ sampleSource, mpegTsPlayer.getMainHandler(), mpegTsPlayer,
+ !Features.AC3_SOFTWARE_DECODE.isEnabled(mContext));
Cea708TextTrackRenderer textRenderer = new Cea708TextTrackRenderer(sampleSource);
TrackRenderer[] renderers = new TrackRenderer[MpegTsPlayer.RENDERER_COUNT];
diff --git a/src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/ac3/Ac3DefaultTrackRenderer.java
index 9dae2e34..d442fde8 100644
--- a/src/com/android/tv/tuner/exoplayer/ac3/Ac3PassthroughTrackRenderer.java
+++ b/src/com/android/tv/tuner/exoplayer/ac3/Ac3DefaultTrackRenderer.java
@@ -23,16 +23,15 @@ 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;
import com.google.android.exoplayer.audio.AudioTrack;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.MimeTypes;
+import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
import com.android.tv.tuner.tvinput.TunerDebug;
import java.io.IOException;
@@ -40,9 +39,9 @@ import java.nio.ByteBuffer;
import java.util.ArrayList;
/**
- * Decodes and renders AC3 audio.
+ * Decodes and renders AC3 audio. Supports passthrough playback and ffmpeg based software decoding.
*/
-public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaClock {
+public class Ac3DefaultTrackRenderer extends TrackRenderer implements MediaClock {
public static final int MSG_SET_VOLUME = 10000;
public static final int MSG_SET_AUDIO_TRACK = MSG_SET_VOLUME + 1;
public static final int MSG_SET_PLAYBACK_SPEED = MSG_SET_VOLUME + 2;
@@ -51,7 +50,14 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
// One AC3 sample has 1536 frames, and its duration is 32ms.
public static final long AC3_SAMPLE_DURATION_US = 32000;
- private static final String TAG = "Ac3PassthroughTrackRenderer";
+ // This is around 150ms, 150ms is big enough not to under-run AudioTrack,
+ // and 150ms is also small enough to fill the buffer rapidly.
+ static int BUFFERED_SAMPLES_IN_AUDIOTRACK = 5;
+ public static final long INITIAL_AUDIO_BUFFERING_TIME_US =
+ BUFFERED_SAMPLES_IN_AUDIOTRACK * AC3_SAMPLE_DURATION_US;
+
+
+ private static final String TAG = "Ac3DefaultTrackRenderer";
private static final boolean DEBUG = false;
/**
@@ -93,6 +99,8 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
private final AudioClock mAudioClock;
private MediaFormat mFormat;
+ private boolean mFormatConfigured;
+ private int mSampleSize;
private final ByteBuffer mOutputBuffer;
private boolean mOutputReady;
private int mTrackIndex;
@@ -106,10 +114,15 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
private long mInterpolatedTimeUs;
private long mPreviousPositionUs;
private boolean mIsStopped;
+ private boolean mEnabled = true;
+ private boolean mIsMuted;
private ArrayList<Integer> mTracksIndex;
- public Ac3PassthroughTrackRenderer(SampleSource source, Handler eventHandler,
- EventListener listener) {
+ public Ac3DefaultTrackRenderer(
+ SampleSource source,
+ Handler eventHandler,
+ EventListener listener,
+ boolean usePassthrough) {
mSource = source.register();
mEventHandler = eventHandler;
mEventListener = listener;
@@ -325,14 +338,38 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
}
}
+ private MediaFormat convertMediaFormatToRaw(MediaFormat format) {
+ return MediaFormat.createAudioFormat(
+ format.trackId,
+ MimeTypes.AUDIO_RAW,
+ format.bitrate,
+ format.maxInputSize,
+ format.durationUs,
+ format.channelCount,
+ format.sampleRate,
+ format.initializationData,
+ format.language);
+ }
+
private void onInputFormatChanged(MediaFormatHolder formatHolder)
throws ExoPlaybackException {
- mFormat = formatHolder.format;
+ mFormat = formatHolder.format;
+ mFormatConfigured = true;
if (DEBUG) {
Log.d(TAG, "AudioTrack was configured to FORMAT: " + mFormat.toString());
}
clearDecodeState();
- AUDIO_TRACK.reconfigure(mFormat.getFrameworkMediaFormatV16());
+ AUDIO_TRACK.reconfigure(mFormat.getFrameworkMediaFormatV16(), 0);
+ }
+
+ private void onSampleSizeChanged(int sampleSize) {
+ if (DEBUG) {
+ Log.d(TAG, "Sample size was changed to : " + sampleSize);
+ }
+ clearDecodeState();
+ int audioBufferSize = sampleSize * BUFFERED_SAMPLES_IN_AUDIOTRACK;
+ mSampleSize = sampleSize;
+ AUDIO_TRACK.reconfigure(mFormat.getFrameworkMediaFormatV16(), audioBufferSize);
}
private boolean feedInputBuffer() throws IOException, ExoPlaybackException {
@@ -359,8 +396,11 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
return false;
}
default: {
+ if (mSampleHolder.size != mSampleSize && mFormatConfigured) {
+ onSampleSizeChanged(mSampleHolder.size);
+ }
mSampleHolder.data.flip();
- decodeDone(mSampleHolder.data, mSampleHolder.timeUs);
+ decodeDone(mSampleHolder.data, mSampleHolder.timeUs);
return true;
}
}
@@ -511,24 +551,29 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
switch (messageType) {
case MSG_SET_VOLUME:
- AUDIO_TRACK.setVolume((Float) message);
+ float volume = (Float) message;
+ // Workaround: we cannot mute the audio track by setting the volume to 0, we need to
+ // disable the AUDIO_TRACK for this intent. However, enabling/disabling audio track
+ // whenever volume is being set might cause side effects, therefore we only handle
+ // "explicit mute operations", i.e., only after certain non-zero volume has been
+ // set, the subsequent volume setting operations will be consider as mute/un-mute
+ // operations and thus enable/disable the audio track.
+ if (mIsMuted && volume > 0) {
+ mIsMuted = false;
+ if (mEnabled) {
+ setStatus(true);
+ }
+ } else if (!mIsMuted && volume == 0) {
+ mIsMuted = true;
+ if (mEnabled) {
+ setStatus(false);
+ }
+ }
+ AUDIO_TRACK.setVolume(volume);
break;
case MSG_SET_AUDIO_TRACK:
- boolean enabled = (Integer) message == 1;
- if (enabled == AUDIO_TRACK.isEnabled()) {
- return;
- }
- if (!enabled) {
- // mAudioClock can be different from getPositionUs. In order to sync them,
- // we set mAudioClock.
- mAudioClock.setPositionUs(getPositionUs());
- }
- AUDIO_TRACK.setStatus(enabled);
- if (enabled) {
- // When AUDIO_TRACK is enabled, we need to clear AUDIO_TRACK and seek to
- // the current position. If not, AUDIO_TRACK has the obsolete data.
- seekTo(mAudioClock.getPositionUs());
- }
+ mEnabled = (Integer) message == 1;
+ setStatus(mEnabled);
break;
case MSG_SET_PLAYBACK_SPEED:
mAudioClock.setPlaybackSpeed((Float) message);
@@ -537,4 +582,21 @@ public class Ac3PassthroughTrackRenderer extends TrackRenderer implements MediaC
super.handleMessage(messageType, message);
}
}
+
+ private void setStatus(boolean enabled) {
+ if (enabled == AUDIO_TRACK.isEnabled()) {
+ return;
+ }
+ if (!enabled) {
+ // mAudioClock can be different from getPositionUs. In order to sync them,
+ // we set mAudioClock.
+ mAudioClock.setPositionUs(getPositionUs());
+ }
+ AUDIO_TRACK.setStatus(enabled);
+ if (enabled) {
+ // When AUDIO_TRACK is enabled, we need to clear AUDIO_TRACK and seek to
+ // the current position. If not, AUDIO_TRACK has the obsolete data.
+ seekTo(mAudioClock.getPositionUs());
+ }
+ }
}
diff --git a/src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java b/src/com/android/tv/tuner/exoplayer/ac3/Ac3MediaCodecTrackRenderer.java
index 2bf86b5a..604959d1 100644
--- a/src/com/android/tv/tuner/exoplayer/ac3/Ac3TrackRenderer.java
+++ b/src/com/android/tv/tuner/exoplayer/ac3/Ac3MediaCodecTrackRenderer.java
@@ -25,14 +25,14 @@ import com.google.android.exoplayer.SampleSource;
/**
* MPEG-2 TS audio track renderer.
- * <p>Since the audio output from {@link android.media.MediaExtractor} contains extra samples at
- * the beginning, using original {@link MediaCodecAudioTrackRenderer} as audio renderer causes
- * asynchronous Audio/Video outputs.
- * This class calculates the offset of audio data and adjust the presentation times to avoid the
- * asynchronous Audio/Video problem.
+ *
+ * <p>Since the audio output from {@link android.media.MediaExtractor} contains extra samples at the
+ * beginning, using original {@link MediaCodecAudioTrackRenderer} as audio renderer causes
+ * asynchronous Audio/Video outputs. This class calculates the offset of audio data and adjust the
+ * presentation times to avoid the asynchronous Audio/Video problem.
*/
-public class Ac3TrackRenderer extends MediaCodecAudioTrackRenderer {
- private final String TAG = "Ac3TrackRenderer";
+public class Ac3MediaCodecTrackRenderer extends MediaCodecAudioTrackRenderer {
+ private final String TAG = "Ac3MediaCodecTrackRenderer";
private final boolean DEBUG = false;
private final Ac3EventListener mListener;
@@ -47,8 +47,11 @@ public class Ac3TrackRenderer extends MediaCodecAudioTrackRenderer {
void onAudioTrackSetPlaybackParamsError(IllegalArgumentException e);
}
- public Ac3TrackRenderer(SampleSource source, MediaCodecSelector mediaCodecSelector,
- Handler eventHandler, EventListener eventListener) {
+ public Ac3MediaCodecTrackRenderer(
+ SampleSource source,
+ MediaCodecSelector mediaCodecSelector,
+ Handler eventHandler,
+ EventListener eventListener) {
super(source, mediaCodecSelector, eventHandler, eventListener);
mListener = (Ac3EventListener) eventListener;
}
diff --git a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java b/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java
index bfdf08ac..6f152490 100644
--- a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java
+++ b/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackMonitor.java
@@ -98,8 +98,8 @@ public class AudioTrackMonitor {
long now = SystemClock.elapsedRealtime();
if (mExpireMs != 0 && now >= mExpireMs) {
if (DEBUG) {
- long sampleDuration = (mTotalCount - 1) *
- Ac3PassthroughTrackRenderer.AC3_SAMPLE_DURATION_US / 1000;
+ long sampleDuration =
+ (mTotalCount - 1) * Ac3DefaultTrackRenderer.AC3_SAMPLE_DURATION_US / 1000;
long totalDuration = now - mStartMs;
StringBuilder ptsBuilder = new StringBuilder();
ptsBuilder.append("PTS received ").append(mSampleCount).append(", ")
diff --git a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java b/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java
index bc3c5d00..393e12c3 100644
--- a/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java
+++ b/src/com/android/tv/tuner/exoplayer/ac3/AudioTrackWrapper.java
@@ -18,6 +18,7 @@ package com.android.tv.tuner.exoplayer.ac3;
import android.media.MediaFormat;
+import com.google.android.exoplayer.C;
import com.google.android.exoplayer.audio.AudioTrack;
import java.nio.ByteBuffer;
@@ -28,6 +29,10 @@ import java.nio.ByteBuffer;
* This wrapper class will do nothing in disabled status for those operations.
*/
public class AudioTrackWrapper {
+ private static final int PCM16_FRAME_BYTES = 2;
+ private static final int AC3_FRAMES_IN_ONE_SAMPLE = 1536;
+ private static final int BUFFERED_SAMPLES_IN_AUDIOTRACK =
+ Ac3DefaultTrackRenderer.BUFFERED_SAMPLES_IN_AUDIOTRACK;
private final AudioTrack mAudioTrack = new AudioTrack();
private int mAudioSessionID;
private boolean mIsEnabled;
@@ -106,7 +111,7 @@ public class AudioTrackWrapper {
mAudioTrack.setVolume(volume);
}
- public void reconfigure(MediaFormat format) {
+ public void reconfigure(MediaFormat format, int audioBufferSize) {
if (!mIsEnabled || format == null) {
return;
}
@@ -117,9 +122,9 @@ public class AudioTrackWrapper {
try {
pcmEncoding = format.getInteger(MediaFormat.KEY_PCM_ENCODING);
} catch (Exception e) {
- pcmEncoding = com.google.android.exoplayer.MediaFormat.NO_VALUE;
+ pcmEncoding = C.ENCODING_PCM_16BIT;
}
- // TODO: Handle non-AC3 or non-passthrough audio.
+ // TODO: Handle non-AC3.
if (MediaFormat.MIMETYPE_AUDIO_AC3.equalsIgnoreCase(mimeType) && channelCount != 2) {
// Workarounds b/25955476.
// Since all devices and platforms does not support passthrough for non-stereo AC3,
@@ -127,7 +132,14 @@ public class AudioTrackWrapper {
// In other words, the channel count should be always 2.
channelCount = 2;
}
- mAudioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding);
+ if (MediaFormat.MIMETYPE_AUDIO_RAW.equalsIgnoreCase(mimeType)) {
+ audioBufferSize =
+ channelCount
+ * PCM16_FRAME_BYTES
+ * AC3_FRAMES_IN_ONE_SAMPLE
+ * BUFFERED_SAMPLES_IN_AUDIOTRACK;
+ }
+ mAudioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, audioBufferSize);
}
public void handleDiscontinuity() {
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
index eb596e93..112e9dc4 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java
@@ -25,13 +25,14 @@ import android.util.Log;
import android.util.Pair;
import com.google.android.exoplayer.SampleHolder;
+import com.android.tv.common.SoftPreconditions;
import com.android.tv.tuner.exoplayer.SampleExtractor;
import com.android.tv.util.Utils;
import java.io.File;
-import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.ConcurrentModificationException;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
@@ -59,7 +60,8 @@ public class BufferManager {
private final SampleChunk.SampleChunkCreator mSampleChunkCreator;
// Maps from track name to a map which maps from starting position to {@link SampleChunk}.
- private final Map<String, SortedMap<Long, SampleChunk>> mChunkMap = new ArrayMap<>();
+ private final Map<String, SortedMap<Long, Pair<SampleChunk, Integer>>> mChunkMap =
+ new ArrayMap<>();
private final Map<String, Long> mStartPositionMap = new ArrayMap<>();
private final Map<String, ChunkEvictedListener> mEvictListeners = new ArrayMap<>();
private final StorageManager mStorageManager;
@@ -77,13 +79,11 @@ public class BufferManager {
}
};
- private volatile boolean mClosed = false;
private int mMinSampleSizeForSpeedCheck = MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK;
private long mTotalWriteSize;
private long mTotalWriteTimeNs;
private float mWriteBandwidth = 0.0f;
private volatile int mSpeedCheckCount;
- private boolean mDisabled = false;
public interface ChunkEvictedListener {
void onChunkEvicted(String id, long createdTimeMs);
@@ -174,6 +174,66 @@ public class BufferManager {
}
/**
+ * A Track format which will be loaded and saved from the permanent storage for recordings.
+ */
+ public static class TrackFormat {
+
+ /**
+ * The track id for the specified track. The track id will be used as a track identifier
+ * for recordings.
+ */
+ public final String trackId;
+
+ /**
+ * The {@link MediaFormat} for the specified track.
+ */
+ public final MediaFormat format;
+
+ /**
+ * Creates TrackFormat.
+ * @param trackId
+ * @param format
+ */
+ public TrackFormat(String trackId, MediaFormat format) {
+ this.trackId = trackId;
+ this.format = format;
+ }
+ }
+
+ /**
+ * A Holder for a sample position which will be loaded from the index file for recordings.
+ */
+ public static class PositionHolder {
+
+ /**
+ * The current sample position in microseconds.
+ * The position is identical to the PTS(presentation time stamp) of the sample.
+ */
+ public final long positionUs;
+
+ /**
+ * Base sample position for the current {@link SampleChunk}.
+ */
+ public final long basePositionUs;
+
+ /**
+ * The file offset for the current sample in the current {@link SampleChunk}.
+ */
+ public final int offset;
+
+ /**
+ * Creates a holder for a specific position in the recording.
+ * @param positionUs
+ * @param offset
+ */
+ public PositionHolder(long positionUs, long basePositionUs, int offset) {
+ this.positionUs = positionUs;
+ this.basePositionUs = basePositionUs;
+ this.offset = offset;
+ }
+ }
+
+ /**
* Storage configuration and policy manager for {@link BufferManager}
*/
public interface StorageManager {
@@ -186,11 +246,6 @@ public class BufferManager {
File getBufferDir();
/**
- * Cleans up storage.
- */
- void clearStorage();
-
- /**
* Informs whether the storage is used for persistent use. (eg. dvr recording/play)
*
* @return {@code true} if stored files are persistent
@@ -220,29 +275,27 @@ public class BufferManager {
* Reads track name & {@link MediaFormat} from storage.
*
* @param isAudio {@code true} if it is for audio track
- * @return {@link Pair} of track name & {@link MediaFormat}
- * @throws IOException
+ * @return {@link List} of TrackFormat
*/
- Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) throws IOException;
+ List<TrackFormat> readTrackInfoFiles(boolean isAudio);
/**
- * Reads sample indexes for each written sample from storage.
+ * Reads key sample positions for each written sample from storage.
*
* @param trackId track name
* @return indexes of the specified track
* @throws IOException
*/
- ArrayList<Long> readIndexFile(String trackId) throws IOException;
+ ArrayList<PositionHolder> readIndexFile(String trackId) throws IOException;
/**
* Writes track information to storage.
*
- * @param trackId track name
- * @param format {@link android.media.MediaFormat} of the track
+ * @param formatList {@list List} of TrackFormat
* @param isAudio {@code true} if it is for audio track
* @throws IOException
*/
- void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio)
+ void writeTrackInfoFiles(List<TrackFormat> formatList, boolean isAudio)
throws IOException;
/**
@@ -252,7 +305,7 @@ public class BufferManager {
* @param index {@link SampleChunk} container
* @throws IOException
*/
- void writeIndexFile(String trackName, SortedMap<Long, SampleChunk> index)
+ void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index)
throws IOException;
}
@@ -307,7 +360,6 @@ public class BufferManager {
SampleChunk.SampleChunkCreator sampleChunkCreator) {
mStorageManager = storageManager;
mSampleChunkCreator = sampleChunkCreator;
- clearBuffer(true);
}
public void registerChunkEvictedListener(String id, ChunkEvictedListener listener) {
@@ -318,44 +370,44 @@ public class BufferManager {
mEvictListeners.remove(id);
}
- private void clearBuffer(boolean deleteFiles) {
- mChunkMap.clear();
- if (deleteFiles) {
- mStorageManager.clearStorage();
- }
- mBufferSize = 0;
- }
-
private static String getFileName(String id, long positionUs) {
return String.format(Locale.ENGLISH, "%s_%016x.chunk", id, positionUs);
}
/**
- * Creates a new {@link SampleChunk} for caching samples.
+ * Creates a new {@link SampleChunk} for caching samples if it is needed.
*
* @param id the name of the track
- * @param positionUs starting position of the {@link SampleChunk} in micro seconds.
+ * @param positionUs current position to write a sample in micro seconds.
* @param samplePool {@link SamplePool} for the fast creation of samples.
+ * @param currentChunk the current {@link SampleChunk} to write, {@code null} when to create
+ * a new {@link SampleChunk}.
+ * @param currentOffset the current offset to write.
* @return returns the created {@link SampleChunk}.
* @throws IOException
*/
- public SampleChunk createNewWriteFile(String id, long positionUs,
- SamplePool samplePool) throws IOException {
+ public SampleChunk createNewWriteFileIfNeeded(String id, long positionUs, SamplePool samplePool,
+ SampleChunk currentChunk, int currentOffset) throws IOException {
if (!maybeEvictChunk()) {
throw new IOException("Not enough storage space");
}
- SortedMap<Long, SampleChunk> map = mChunkMap.get(id);
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(id);
if (map == null) {
map = new TreeMap<>();
mChunkMap.put(id, map);
mStartPositionMap.put(id, positionUs);
mPendingDelete.init(id);
}
- File file = new File(mStorageManager.getBufferDir(), getFileName(id, positionUs));
- SampleChunk sampleChunk = mSampleChunkCreator.createSampleChunk(samplePool, file,
- positionUs, mChunkCallback);
- map.put(positionUs, sampleChunk);
- return sampleChunk;
+ if (currentChunk == null) {
+ File file = new File(mStorageManager.getBufferDir(), getFileName(id, positionUs));
+ SampleChunk sampleChunk = mSampleChunkCreator
+ .createSampleChunk(samplePool, file, positionUs, mChunkCallback);
+ map.put(positionUs, new Pair(sampleChunk, 0));
+ return sampleChunk;
+ } else {
+ map.put(positionUs, new Pair(currentChunk, currentOffset));
+ return null;
+ }
}
/**
@@ -366,10 +418,10 @@ public class BufferManager {
* @throws IOException
*/
public void loadTrackFromStorage(String trackId, SamplePool samplePool) throws IOException {
- ArrayList<Long> keyPositions = mStorageManager.readIndexFile(trackId);
- long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0) : 0;
+ ArrayList<PositionHolder> keyPositions = mStorageManager.readIndexFile(trackId);
+ long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0).positionUs : 0;
- SortedMap<Long, SampleChunk> map = mChunkMap.get(trackId);
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(trackId);
if (map == null) {
map = new TreeMap<>();
mChunkMap.put(trackId, map);
@@ -377,11 +429,15 @@ public class BufferManager {
mPendingDelete.init(trackId);
}
SampleChunk chunk = null;
- for (long positionUs: keyPositions) {
- chunk = mSampleChunkCreator.loadSampleChunkFromFile(samplePool,
- mStorageManager.getBufferDir(), getFileName(trackId, positionUs), positionUs,
- mChunkCallback, chunk);
- map.put(positionUs, chunk);
+ long basePositionUs = -1;
+ for (PositionHolder position: keyPositions) {
+ if (position.basePositionUs != basePositionUs) {
+ chunk = mSampleChunkCreator.loadSampleChunkFromFile(samplePool,
+ mStorageManager.getBufferDir(), getFileName(trackId, position.positionUs),
+ position.positionUs, mChunkCallback, chunk);
+ basePositionUs = position.basePositionUs;
+ }
+ map.put(position.positionUs, new Pair(chunk, position.offset));
}
}
@@ -392,19 +448,19 @@ public class BufferManager {
* @param positionUs the position.
* @return returns the found {@link SampleChunk}.
*/
- public SampleChunk getReadFile(String id, long positionUs) {
- SortedMap<Long, SampleChunk> map = mChunkMap.get(id);
+ public Pair<SampleChunk, Integer> getReadFile(String id, long positionUs) {
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(id);
if (map == null) {
return null;
}
- SampleChunk sampleChunk;
- SortedMap<Long, SampleChunk> headMap = map.headMap(positionUs + 1);
+ Pair<SampleChunk, Integer> ret;
+ SortedMap<Long, Pair<SampleChunk, Integer>> headMap = map.headMap(positionUs + 1);
if (!headMap.isEmpty()) {
- sampleChunk = headMap.get(headMap.lastKey());
+ ret = headMap.get(headMap.lastKey());
} else {
- sampleChunk = map.get(map.firstKey());
+ ret = map.get(map.firstKey());
}
- return sampleChunk;
+ return ret;
}
/**
@@ -439,15 +495,16 @@ public class BufferManager {
// Since chunks are persistent, we cannot evict chunks.
return false;
}
- SortedMap<Long, SampleChunk> earliestChunkMap = null;
+ SortedMap<Long, Pair<SampleChunk, Integer>> earliestChunkMap = null;
SampleChunk earliestChunk = null;
String earliestChunkId = null;
- for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) {
- SortedMap<Long, SampleChunk> map = entry.getValue();
+ for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry :
+ mChunkMap.entrySet()) {
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = entry.getValue();
if (map.isEmpty()) {
continue;
}
- SampleChunk chunk = map.get(map.firstKey());
+ SampleChunk chunk = map.get(map.firstKey()).first;
if (earliestChunk == null
|| chunk.getCreatedTimeMs() < earliestChunk.getCreatedTimeMs()) {
earliestChunkMap = map;
@@ -473,8 +530,9 @@ public class BufferManager {
}
pendingDelete = mPendingDelete.getSize();
}
- for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) {
- SortedMap<Long, SampleChunk> map = entry.getValue();
+ for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry :
+ mChunkMap.entrySet()) {
+ SortedMap<Long, Pair<SampleChunk, Integer>> map = entry.getValue();
if (map.isEmpty()) {
continue;
}
@@ -489,70 +547,74 @@ public class BufferManager {
* @return returns all track information which is found by {@link BufferManager.StorageManager}.
* @throws IOException
*/
- public ArrayList<Pair<String, MediaFormat>> readTrackInfoFiles() throws IOException {
- ArrayList<Pair<String, MediaFormat>> trackInfos = new ArrayList<>();
- try {
- trackInfos.add(mStorageManager.readTrackInfoFile(false));
- } catch (FileNotFoundException e) {
- // There can be a single track only recording. (eg. audio-only, video-only)
- // So the exception should not stop the read.
+ public List<TrackFormat> readTrackInfoFiles() throws IOException {
+ List<TrackFormat> trackFormatList = new ArrayList<>();
+ trackFormatList.addAll(mStorageManager.readTrackInfoFiles(false));
+ trackFormatList.addAll(mStorageManager.readTrackInfoFiles(true));
+ if (trackFormatList.isEmpty()) {
+ throw new IOException("No track information to load");
}
- try {
- trackInfos.add(mStorageManager.readTrackInfoFile(true));
- } catch (FileNotFoundException e) {
- // See above catch block.
- }
- return trackInfos;
+ return trackFormatList;
}
/**
* Writes track information and index information for all tracks.
*
- * @param audio audio information.
- * @param video video information.
+ * @param audios list of audio track information
+ * @param videos list of audio track information
* @throws IOException
*/
- public void writeMetaFiles(Pair<String, MediaFormat> audio, Pair<String, MediaFormat> video)
+ public void writeMetaFiles(List<TrackFormat> audios, List<TrackFormat> videos)
throws IOException {
- if (audio != null) {
- mStorageManager.writeTrackInfoFile(audio.first, audio.second, true);
- SortedMap<Long, SampleChunk> map = mChunkMap.get(audio.first);
- if (map == null) {
- throw new IOException("Audio track index missing");
+ if (audios.isEmpty() && videos.isEmpty()) {
+ throw new IOException("No track information to save");
+ }
+ if (!audios.isEmpty()) {
+ mStorageManager.writeTrackInfoFiles(audios, true);
+ for (TrackFormat trackFormat : audios) {
+ SortedMap<Long, Pair<SampleChunk, Integer>> map =
+ mChunkMap.get(trackFormat.trackId);
+ if (map == null) {
+ throw new IOException("Audio track index missing");
+ }
+ mStorageManager.writeIndexFile(trackFormat.trackId, map);
}
- mStorageManager.writeIndexFile(audio.first, map);
}
- if (video != null) {
- mStorageManager.writeTrackInfoFile(video.first, video.second, false);
- SortedMap<Long, SampleChunk> map = mChunkMap.get(video.first);
- if (map == null) {
- throw new IOException("Video track index missing");
+ if (!videos.isEmpty()) {
+ mStorageManager.writeTrackInfoFiles(videos, false);
+ for (TrackFormat trackFormat : videos) {
+ SortedMap<Long, Pair<SampleChunk, Integer>> map =
+ mChunkMap.get(trackFormat.trackId);
+ if (map == null) {
+ throw new IOException("Video track index missing");
+ }
+ mStorageManager.writeIndexFile(trackFormat.trackId, map);
}
- mStorageManager.writeIndexFile(video.first, map);
}
}
/**
- * Marks it is closed and it is not used anymore.
- */
- public void close() {
- // Clean-up may happen after this is called.
- mClosed = true;
- }
-
- /**
* Releases all the resources.
*/
public void release() {
- mPendingDelete.release();
- for (Map.Entry<String, SortedMap<Long, SampleChunk>> entry : mChunkMap.entrySet()) {
- for (SampleChunk chunk : entry.getValue().values()) {
- SampleChunk.IoState.release(chunk, !mStorageManager.isPersistent());
+ try {
+ mPendingDelete.release();
+ for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry :
+ mChunkMap.entrySet()) {
+ SampleChunk toRelease = null;
+ for (Pair<SampleChunk, Integer> positions : entry.getValue().values()) {
+ if (toRelease != positions.first) {
+ toRelease = positions.first;
+ SampleChunk.IoState.release(toRelease, !mStorageManager.isPersistent());
+ }
+ }
}
- }
- mChunkMap.clear();
- if (mClosed) {
- clearBuffer(!mStorageManager.isPersistent());
+ mChunkMap.clear();
+ } catch (ConcurrentModificationException | NullPointerException e) {
+ // TODO: remove this after it it confirmed that race condition issues are resolved.
+ // b/32492258, b/32373376
+ SoftPreconditions.checkState(false, "Exception on BufferManager#release: ",
+ e.toString());
}
}
@@ -611,20 +673,6 @@ public class BufferManager {
}
/**
- * Marks {@link BufferManager} object disabled to prevent it from the future use.
- */
- public void disable() {
- mDisabled = true;
- }
-
- /**
- * Returns if {@link BufferManager} object is disabled.
- */
- public boolean isDisabled() {
- return mDisabled;
- }
-
- /**
* Returns if {@link BufferManager} has checked the write speed,
* which is suitable for Trickplay.
*/
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
index 6a0502a7..bea3defd 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java
@@ -17,8 +17,12 @@
package com.android.tv.tuner.exoplayer.buffer;
import android.media.MediaFormat;
+import android.util.Log;
import android.util.Pair;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
+import com.google.protobuf.nano.MessageNano;
+
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
@@ -28,18 +32,25 @@ import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
import java.util.SortedMap;
/**
* Manages DVR storage.
*/
public class DvrStorageManager implements BufferManager.StorageManager {
+ private static final String TAG = "DvrStorageManager";
// TODO: make serializable classes and use protobuf after internal data structure is finalized.
private static final String KEY_PIXEL_WIDTH_HEIGHT_RATIO =
"com.google.android.videos.pixelWidthHeightRatio";
+ private static final String META_FILE_TYPE_AUDIO = "audio";
+ private static final String META_FILE_TYPE_VIDEO = "video";
+ private static final String META_FILE_TYPE_CAPTION = "caption";
private static final String META_FILE_SUFFIX = ".meta";
private static final String IDX_FILE_SUFFIX = ".idx";
+ private static final String IDX_FILE_SUFFIX_V2 = IDX_FILE_SUFFIX + "2";
// Size of minimum reserved storage buffer which will be used to save meta files
// and index files after actual recording finished.
@@ -59,18 +70,6 @@ public class DvrStorageManager implements BufferManager.StorageManager {
}
@Override
- public void clearStorage() {
- if (mIsRecording) {
- File[] files = mBufferDir.listFiles();
- if (files != null && files.length > 0) {
- for (File file : files) {
- file.delete();
- }
- }
- }
- }
-
- @Override
public File getBufferDir() {
return mBufferDir;
}
@@ -132,6 +131,17 @@ public class DvrStorageManager implements BufferManager.StorageManager {
}
}
+ private void readFormatStringOptional(DataInputStream in, MediaFormat format, String key) {
+ try {
+ String str = readString(in);
+ if (str != null) {
+ format.setString(key, str);
+ }
+ } catch (IOException e) {
+ // Since we are reading optional field, ignore the exception.
+ }
+ }
+
private ByteBuffer readByteBuffer(DataInputStream in) throws IOException {
int len = in.readInt();
if (len <= 0) {
@@ -155,39 +165,104 @@ public class DvrStorageManager implements BufferManager.StorageManager {
}
@Override
- public Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) throws IOException {
- File file = new File(getBufferDir(), (isAudio ? "audio" : "video") + META_FILE_SUFFIX);
- try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
- String name = readString(in);
- MediaFormat format = new MediaFormat();
- readFormatString(in, format, MediaFormat.KEY_MIME);
- readFormatInt(in, format, MediaFormat.KEY_MAX_INPUT_SIZE);
- readFormatInt(in, format, MediaFormat.KEY_WIDTH);
- readFormatInt(in, format, MediaFormat.KEY_HEIGHT);
- readFormatInt(in, format, MediaFormat.KEY_CHANNEL_COUNT);
- readFormatInt(in, format, MediaFormat.KEY_SAMPLE_RATE);
- readFormatFloat(in, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
- for (int i = 0; i < 3; ++i) {
- readFormatByteBuffer(in, format, "csd-" + i);
+ public List<BufferManager.TrackFormat> readTrackInfoFiles(boolean isAudio) {
+ List<BufferManager.TrackFormat> trackFormatList = new ArrayList<>();
+ int index = 0;
+ boolean trackNotFound = false;
+ do {
+ String fileName = (isAudio ? META_FILE_TYPE_AUDIO : META_FILE_TYPE_VIDEO)
+ + ((index == 0) ? META_FILE_SUFFIX : (index + META_FILE_SUFFIX));
+ File file = new File(getBufferDir(), fileName);
+ try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
+ String name = readString(in);
+ MediaFormat format = new MediaFormat();
+ readFormatString(in, format, MediaFormat.KEY_MIME);
+ readFormatInt(in, format, MediaFormat.KEY_MAX_INPUT_SIZE);
+ readFormatInt(in, format, MediaFormat.KEY_WIDTH);
+ readFormatInt(in, format, MediaFormat.KEY_HEIGHT);
+ readFormatInt(in, format, MediaFormat.KEY_CHANNEL_COUNT);
+ readFormatInt(in, format, MediaFormat.KEY_SAMPLE_RATE);
+ readFormatFloat(in, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
+ for (int i = 0; i < 3; ++i) {
+ readFormatByteBuffer(in, format, "csd-" + i);
+ }
+ readFormatLong(in, format, MediaFormat.KEY_DURATION);
+
+ // This is optional since language field is added later.
+ readFormatStringOptional(in, format, MediaFormat.KEY_LANGUAGE);
+ trackFormatList.add(new BufferManager.TrackFormat(name, format));
+ } catch (IOException e) {
+ trackNotFound = true;
}
- readFormatLong(in, format, MediaFormat.KEY_DURATION);
- return new Pair<>(name, format);
+ index++;
+ } while(!trackNotFound);
+ return trackFormatList;
+ }
+
+ /**
+ * Reads caption information from files.
+ *
+ * @return a list of {@link AtscCaptionTrack} objects which store caption information.
+ */
+ public List<AtscCaptionTrack> readCaptionInfoFiles() {
+ List<AtscCaptionTrack> tracks = new ArrayList<>();
+ int index = 0;
+ boolean trackNotFound = false;
+ do {
+ String fileName = META_FILE_TYPE_CAPTION +
+ ((index == 0) ? META_FILE_SUFFIX : (index + META_FILE_SUFFIX));
+ File file = new File(getBufferDir(), fileName);
+ try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
+ byte[] data = new byte[(int) file.length()];
+ in.read(data);
+ tracks.add(AtscCaptionTrack.parseFrom(data));
+ } catch (IOException e) {
+ trackNotFound = true;
+ }
+ index++;
+ } while(!trackNotFound);
+ return tracks;
+ }
+
+ private ArrayList<BufferManager.PositionHolder> readOldIndexFile(File indexFile)
+ throws IOException {
+ ArrayList<BufferManager.PositionHolder> indices = new ArrayList<>();
+ try (DataInputStream in = new DataInputStream(new FileInputStream(indexFile))) {
+ long count = in.readLong();
+ for (long i = 0; i < count; ++i) {
+ long positionUs = in.readLong();
+ indices.add(new BufferManager.PositionHolder(positionUs, positionUs, 0));
+ }
+ return indices;
}
}
- @Override
- public ArrayList<Long> readIndexFile(String trackId) throws IOException {
- ArrayList<Long> indices = new ArrayList<>();
- File file = new File(getBufferDir(), trackId + IDX_FILE_SUFFIX);
- try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
+ private ArrayList<BufferManager.PositionHolder> readNewIndexFile(File indexFile)
+ throws IOException {
+ ArrayList<BufferManager.PositionHolder> indices = new ArrayList<>();
+ try (DataInputStream in = new DataInputStream(new FileInputStream(indexFile))) {
long count = in.readLong();
for (long i = 0; i < count; ++i) {
- indices.add(in.readLong());
+ long positionUs = in.readLong();
+ long basePositionUs = in.readLong();
+ int offset = in.readInt();
+ indices.add(new BufferManager.PositionHolder(positionUs, basePositionUs, offset));
}
return indices;
}
}
+ @Override
+ public ArrayList<BufferManager.PositionHolder> readIndexFile(String trackId)
+ throws IOException {
+ File file = new File(getBufferDir(), trackId + IDX_FILE_SUFFIX_V2);
+ if (file.exists()) {
+ return readNewIndexFile(file);
+ } else {
+ return readOldIndexFile(new File(getBufferDir(),trackId + IDX_FILE_SUFFIX));
+ }
+ }
+
private void writeFormatInt(DataOutputStream out, MediaFormat format, String key)
throws IOException {
if (format.containsKey(key)) {
@@ -254,33 +329,63 @@ public class DvrStorageManager implements BufferManager.StorageManager {
}
@Override
- public void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio)
+ public void writeTrackInfoFiles(List<BufferManager.TrackFormat> formatList, boolean isAudio)
throws IOException {
- File file = new File(getBufferDir(), (isAudio ? "audio" : "video") + META_FILE_SUFFIX);
- try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
- writeString(out, trackId);
- writeFormatString(out, format, MediaFormat.KEY_MIME);
- writeFormatInt(out, format, MediaFormat.KEY_MAX_INPUT_SIZE);
- writeFormatInt(out, format, MediaFormat.KEY_WIDTH);
- writeFormatInt(out, format, MediaFormat.KEY_HEIGHT);
- writeFormatInt(out, format, MediaFormat.KEY_CHANNEL_COUNT);
- writeFormatInt(out, format, MediaFormat.KEY_SAMPLE_RATE);
- writeFormatFloat(out, format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
- for (int i = 0; i < 3; ++i) {
- writeFormatByteBuffer(out, format, "csd-" + i);
+ for (int i = 0; i < formatList.size() ; ++i) {
+ BufferManager.TrackFormat trackFormat = formatList.get(i);
+ String fileName = (isAudio ? META_FILE_TYPE_AUDIO : META_FILE_TYPE_VIDEO)
+ + ((i == 0) ? META_FILE_SUFFIX : (i + META_FILE_SUFFIX));
+ File file = new File(getBufferDir(), fileName);
+ try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
+ writeString(out, trackFormat.trackId);
+ writeFormatString(out, trackFormat.format, MediaFormat.KEY_MIME);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_MAX_INPUT_SIZE);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_WIDTH);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_HEIGHT);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_CHANNEL_COUNT);
+ writeFormatInt(out, trackFormat.format, MediaFormat.KEY_SAMPLE_RATE);
+ writeFormatFloat(out, trackFormat.format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
+ for (int j = 0; j < 3; ++j) {
+ writeFormatByteBuffer(out, trackFormat.format, "csd-" + j);
+ }
+ writeFormatLong(out, trackFormat.format, MediaFormat.KEY_DURATION);
+ writeFormatString(out, trackFormat.format, MediaFormat.KEY_LANGUAGE);
+ }
+ }
+ }
+
+ /**
+ * Writes caption information to files.
+ *
+ * @param tracks a list of {@link AtscCaptionTrack} objects which store caption information.
+ */
+ public void writeCaptionInfoFiles(List<AtscCaptionTrack> tracks) {
+ if (tracks == null || tracks.isEmpty()) {
+ return;
+ }
+ for (int i = 0; i < tracks.size(); i++) {
+ AtscCaptionTrack track = tracks.get(i);
+ String fileName = META_FILE_TYPE_CAPTION +
+ ((i == 0) ? META_FILE_SUFFIX : (i + META_FILE_SUFFIX));
+ File file = new File(getBufferDir(), fileName);
+ try (DataOutputStream out = new DataOutputStream(new FileOutputStream(file))) {
+ out.write(MessageNano.toByteArray(track));
+ } catch (Exception e) {
+ Log.e(TAG, "Fail to write caption info to files", e);
}
- writeFormatLong(out, format, MediaFormat.KEY_DURATION);
}
}
@Override
- public void writeIndexFile(String trackName, SortedMap<Long, SampleChunk> index)
+ public void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index)
throws IOException {
- File indexFile = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX);
+ File indexFile = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX_V2);
try (DataOutputStream out = new DataOutputStream(new FileOutputStream(indexFile))) {
out.writeLong(index.size());
- for (Long key : index.keySet()) {
- out.writeLong(key);
+ for (Map.Entry<Long, Pair<SampleChunk, Integer>> entry : index.entrySet()) {
+ out.writeLong(entry.getKey());
+ out.writeLong(entry.getValue().first.getStartPositionUs());
+ out.writeInt(entry.getValue().second);
}
}
}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java b/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
index 4869b49f..af0c3f0d 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java
@@ -66,9 +66,14 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
public static final int BUFFER_REASON_RECORDING = 2;
/**
- * The duration of a chunk of samples, {@link SampleChunk}.
+ * The minimum duration to support seek in Trickplay.
*/
- static final long CHUNK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500);
+ static final long MIN_SEEK_DURATION_US = TimeUnit.MILLISECONDS.toMicros(500);
+
+ /**
+ * The duration of a {@link SampleChunk} for recordings.
+ */
+ static final long RECORDING_CHUNK_DURATION_US = MIN_SEEK_DURATION_US * 1200; // 10 minutes
private static final long BUFFER_WRITE_TIMEOUT_MS = 10 * 1000; // 10 seconds
private static final long BUFFER_NEEDED_US =
1000L * Math.max(MpegTsPlayer.MIN_BUFFER_MS, MpegTsPlayer.MIN_REBUFFER_MS);
@@ -79,7 +84,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
private int mTrackCount;
private boolean[] mTrackSelected;
- private List<String> mIds;
private List<SampleQueue> mReadSampleQueues;
private final SamplePool mSamplePool = new SamplePool();
private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US;
@@ -130,7 +134,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
if (mTrackCount <= 0) {
throw new IOException("No tracks to initialize");
}
- mIds = ids;
mTrackSelected = new boolean[mTrackCount];
mReadSampleQueues = new ArrayList<>();
mSampleChunkIoHelper = new SampleChunkIoHelper(ids, mediaFormats, mBufferReason,
@@ -139,6 +142,9 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
mReadSampleQueues.add(i, new SampleQueue(mSamplePool));
}
mSampleChunkIoHelper.init();
+ for (int i = 0; i < mTrackCount; ++i) {
+ mBufferManager.registerChunkEvictedListener(ids.get(i), RecordingSampleBuffer.this);
+ }
}
@Override
@@ -146,8 +152,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
if (!mTrackSelected[index]) {
mTrackSelected[index] = true;
mReadSampleQueues.get(index).clear();
- mBufferManager.registerChunkEvictedListener(mIds.get(index),
- RecordingSampleBuffer.this);
mSampleChunkIoHelper.openRead(index, mCurrentPlaybackPositionUs);
}
}
@@ -157,7 +161,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
if (mTrackSelected[index]) {
mTrackSelected[index] = false;
mReadSampleQueues.get(index).clear();
- mBufferManager.unregisterChunkEvictedListener(mIds.get(index));
+ mSampleChunkIoHelper.closeRead(index);
}
}
@@ -193,7 +197,6 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
}
// Disables buffering samples afterwards, and notifies the disk speed is slow.
Log.w(TAG, "Disk is too slow for trickplay");
- mBufferManager.disable();
mBufferListener.onDiskTooSlow();
}
@@ -205,7 +208,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
private boolean maybeReadSample(SampleQueue queue, int index) {
if (queue.getLastQueuedPositionUs() != null
&& queue.getLastQueuedPositionUs() > mCurrentPlaybackPositionUs + BUFFER_NEEDED_US
- && queue.isDurationGreaterThan(CHUNK_DURATION_US)) {
+ && queue.isDurationGreaterThan(MIN_SEEK_DURATION_US)) {
// The speed of queuing samples can be higher than the playback speed.
// If the duration of the samples in the queue is not limited,
// samples can be accumulated and there can be out-of-memory issues.
@@ -300,7 +303,7 @@ public class RecordingSampleBuffer implements BufferManager.SampleBuffer,
public void onChunkEvicted(String id, long createdTimeMs) {
if (mBufferListener != null) {
mBufferListener.onBufferStartTimeChanged(
- createdTimeMs + TimeUnit.MICROSECONDS.toMillis(CHUNK_DURATION_US));
+ createdTimeMs + TimeUnit.MICROSECONDS.toMillis(MIN_SEEK_DURATION_US));
}
}
}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java
index 552caaef..ab6d1a75 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java
@@ -151,18 +151,23 @@ public class SampleChunk {
mCurrentOffset = 0;
}
+ private void reset(SampleChunk chunk, long offset) {
+ mChunk = chunk;
+ mCurrentOffset = offset;
+ }
+
/**
* Prepares for read I/O operation from a new SampleChunk.
*
* @param chunk the new SampleChunk to read from
* @throws IOException
*/
- void openRead(SampleChunk chunk) throws IOException {
+ void openRead(SampleChunk chunk, long offset) throws IOException {
if (mChunk != null) {
mChunk.closeRead();
}
chunk.openRead();
- reset(chunk);
+ reset(chunk, offset);
}
/**
@@ -241,6 +246,20 @@ public class SampleChunk {
}
/**
+ * Returns the current SampleChunk for subsequent I/O operation.
+ */
+ SampleChunk getChunk() {
+ return mChunk;
+ }
+
+ /**
+ * Returns the current offset of the current SampleChunk for subsequent I/O operation.
+ */
+ long getOffset() {
+ return mCurrentOffset;
+ }
+
+ /**
* Releases SampleChunk. the SampleChunk will not be used anymore.
*
* @param chunk to release
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
index 37ae4022..ca97a91a 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java
@@ -21,6 +21,7 @@ import android.os.ConditionVariable;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
+import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
@@ -31,7 +32,9 @@ import com.android.tv.common.SoftPreconditions;
import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer.BufferReason;
import java.io.IOException;
+import java.util.LinkedList;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
@@ -46,11 +49,13 @@ public class SampleChunkIoHelper implements Handler.Callback {
private static final int MSG_OPEN_READ = 1;
private static final int MSG_OPEN_WRITE = 2;
- private static final int MSG_CLOSE_WRITE = 3;
- private static final int MSG_READ = 4;
- private static final int MSG_WRITE = 5;
- private static final int MSG_RELEASE = 6;
+ private static final int MSG_CLOSE_READ = 3;
+ private static final int MSG_CLOSE_WRITE = 4;
+ private static final int MSG_READ = 5;
+ private static final int MSG_WRITE = 6;
+ private static final int MSG_RELEASE = 7;
+ private final long mSampleChunkDurationUs;
private final int mTrackCount;
private final List<String> mIds;
private final List<MediaFormat> mMediaFormats;
@@ -62,9 +67,11 @@ public class SampleChunkIoHelper implements Handler.Callback {
private Handler mIoHandler;
private final ConcurrentLinkedQueue<SampleHolder> mReadSampleBuffers[];
private final ConcurrentLinkedQueue<SampleHolder> mHandlerReadSampleBuffers[];
- private final long[] mWriteEndPositionUs;
+ private final long[] mWriteIndexEndPositionUs;
+ private final long[] mWriteChunkEndPositionUs;
private final SampleChunk.IoState[] mReadIoStates;
private final SampleChunk.IoState[] mWriteIoStates;
+ private final Set<Integer> mSelectedTracks = new ArraySet<>();
private long mBufferDurationUs = 0;
private boolean mWriteEnded;
private boolean mErrorNotified;
@@ -129,11 +136,20 @@ public class SampleChunkIoHelper implements Handler.Callback {
mReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount];
mHandlerReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount];
- mWriteEndPositionUs = new long[mTrackCount];
+ mWriteIndexEndPositionUs = new long[mTrackCount];
+ mWriteChunkEndPositionUs = new long[mTrackCount];
mReadIoStates = new SampleChunk.IoState[mTrackCount];
mWriteIoStates = new SampleChunk.IoState[mTrackCount];
+
+ // Small chunk duration for live playback will give more fine grained storage usage
+ // and eviction handling for trickplay.
+ mSampleChunkDurationUs =
+ bufferReason == RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK ?
+ RecordingSampleBuffer.MIN_SEEK_DURATION_US :
+ RecordingSampleBuffer.RECORDING_CHUNK_DURATION_US;
for (int i = 0; i < mTrackCount; ++i) {
- mWriteEndPositionUs[i] = RecordingSampleBuffer.CHUNK_DURATION_US;
+ mWriteIndexEndPositionUs[i] = RecordingSampleBuffer.MIN_SEEK_DURATION_US;
+ mWriteChunkEndPositionUs[i] = mSampleChunkDurationUs;
mReadIoStates[i] = new SampleChunk.IoState();
mWriteIoStates[i] = new SampleChunk.IoState();
}
@@ -204,6 +220,15 @@ public class SampleChunkIoHelper implements Handler.Callback {
}
/**
+ * Closes read from the specified track.
+ *
+ * @param index track index
+ */
+ public void closeRead(int index) {
+ mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_CLOSE_READ, index));
+ }
+
+ /**
* Notifies writes are finished.
*/
public void closeWrite() {
@@ -229,21 +254,19 @@ public class SampleChunkIoHelper implements Handler.Callback {
try {
if (mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDING && mTrackCount > 0) {
// Saves meta information for recording.
- Pair<String, android.media.MediaFormat> audio = null, video = null;
+ List<BufferManager.TrackFormat> audios = new LinkedList<>();
+ List<BufferManager.TrackFormat> videos = new LinkedList<>();
for (int i = 0; i < mTrackCount; ++i) {
android.media.MediaFormat format =
mMediaFormats.get(i).getFrameworkMediaFormatV16();
format.setLong(android.media.MediaFormat.KEY_DURATION, mBufferDurationUs);
- if (audio == null && MimeTypes.isAudio(mMediaFormats.get(i).mimeType)) {
- audio = new Pair<>(mIds.get(i), format);
- } else if (video == null && MimeTypes.isVideo(mMediaFormats.get(i).mimeType)) {
- video = new Pair<>(mIds.get(i), format);
- }
- if (audio != null && video != null) {
- break;
+ if (MimeTypes.isAudio(mMediaFormats.get(i).mimeType)) {
+ audios.add(new BufferManager.TrackFormat(mIds.get(i), format));
+ } else if (MimeTypes.isVideo(mMediaFormats.get(i).mimeType)) {
+ videos.add(new BufferManager.TrackFormat(mIds.get(i), format));
}
}
- mBufferManager.writeMetaFiles(audio, video);
+ mBufferManager.writeMetaFiles(audios, videos);
}
} finally {
mBufferManager.release();
@@ -265,6 +288,9 @@ public class SampleChunkIoHelper implements Handler.Callback {
case MSG_OPEN_WRITE:
doOpenWrite((int) message.obj);
return true;
+ case MSG_CLOSE_READ:
+ doCloseRead((int) message.obj);
+ return true;
case MSG_CLOSE_WRITE:
doCloseWrite();
return true;
@@ -291,14 +317,16 @@ public class SampleChunkIoHelper implements Handler.Callback {
private void doOpenRead(IoParams params) throws IOException {
int index = params.index;
mIoHandler.removeMessages(MSG_READ, index);
- SampleChunk chunk = mBufferManager.getReadFile(mIds.get(index), params.positionUs);
- if (chunk == null) {
+ Pair<SampleChunk, Integer> readPosition =
+ mBufferManager.getReadFile(mIds.get(index), params.positionUs);
+ if (readPosition == null) {
String errorMessage = "Chunk ID:" + mIds.get(index) + " pos:" + params.positionUs
+ "is not found";
- SoftPreconditions.checkNotNull(chunk, TAG, errorMessage);
+ SoftPreconditions.checkNotNull(readPosition, TAG, errorMessage);
throw new IOException(errorMessage);
}
- mReadIoStates[index].openRead(chunk);
+ mSelectedTracks.add(index);
+ mReadIoStates[index].openRead(readPosition.first, (long) readPosition.second);
if (mHandlerReadSampleBuffers[index] != null) {
SampleHolder sample;
while ((sample = mHandlerReadSampleBuffers[index].poll()) != null) {
@@ -310,10 +338,22 @@ public class SampleChunkIoHelper implements Handler.Callback {
}
private void doOpenWrite(int index) throws IOException {
- SampleChunk chunk = mBufferManager.createNewWriteFile(mIds.get(index), 0, mSamplePool);
+ SampleChunk chunk = mBufferManager.createNewWriteFileIfNeeded(mIds.get(index), 0,
+ mSamplePool, null, 0);
mWriteIoStates[index].openWrite(chunk);
}
+ private void doCloseRead(int index) {
+ mSelectedTracks.remove(index);
+ if (mHandlerReadSampleBuffers[index] != null) {
+ SampleHolder sample;
+ while ((sample = mHandlerReadSampleBuffers[index].poll()) != null) {
+ mSamplePool.releaseSample(sample);
+ }
+ }
+ mIoHandler.removeMessages(MSG_READ, index);
+ }
+
private void doRead(int index) throws IOException {
mIoHandler.removeMessages(MSG_READ, index);
if (mHandlerReadSampleBuffers[index].size() >= MAX_READ_BUFFER_SAMPLES) {
@@ -357,13 +397,21 @@ public class SampleChunkIoHelper implements Handler.Callback {
if (sample.timeUs > mBufferDurationUs) {
mBufferDurationUs = sample.timeUs;
}
-
- if (sample.timeUs >= mWriteEndPositionUs[index]) {
- nextChunk = mBufferManager.createNewWriteFile(mIds.get(index),
- mWriteEndPositionUs[index], mSamplePool);
- mWriteEndPositionUs[index] =
- ((sample.timeUs / RecordingSampleBuffer.CHUNK_DURATION_US) + 1) *
- RecordingSampleBuffer.CHUNK_DURATION_US;
+ if (sample.timeUs >= mWriteIndexEndPositionUs[index]) {
+ SampleChunk currentChunk = sample.timeUs >= mWriteChunkEndPositionUs[index] ?
+ null : mWriteIoStates[params.index].getChunk();
+ int currentOffset = (int) mWriteIoStates[params.index].getOffset();
+ nextChunk = mBufferManager.createNewWriteFileIfNeeded(
+ mIds.get(index), mWriteIndexEndPositionUs[index], mSamplePool,
+ currentChunk, currentOffset);
+ mWriteIndexEndPositionUs[index] =
+ ((sample.timeUs / RecordingSampleBuffer.MIN_SEEK_DURATION_US) + 1) *
+ RecordingSampleBuffer.MIN_SEEK_DURATION_US;
+ if (nextChunk != null) {
+ mWriteChunkEndPositionUs[index] =
+ ((sample.timeUs / mSampleChunkDurationUs) + 1)
+ * mSampleChunkDurationUs;
+ }
}
}
mWriteIoStates[params.index].write(params.sample, nextChunk);
@@ -391,15 +439,22 @@ public class SampleChunkIoHelper implements Handler.Callback {
mIoHandler.removeCallbacksAndMessages(null);
mFinished = true;
conditionVariable.open();
+ mSelectedTracks.clear();
}
private void releaseEvictedChunks() {
- if (mBufferReason != RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK) {
+ if (mBufferReason != RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK
+ || mSelectedTracks.isEmpty()) {
return;
}
+ long currentStartPositionUs = Long.MAX_VALUE;
+ for (int trackIndex : mSelectedTracks) {
+ currentStartPositionUs = Math.min(currentStartPositionUs,
+ mReadIoStates[trackIndex].getStartPositionUs());
+ }
for (int i = 0; i < mTrackCount; ++i) {
long evictEndPositionUs = Math.min(mBufferManager.getStartPositionUs(mIds.get(i)),
- mReadIoStates[i].getStartPositionUs());
+ currentStartPositionUs);
mBufferManager.evictChunks(mIds.get(i), evictEndPositionUs);
}
}
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java
index 7b098f40..75eac5a2 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java
@@ -43,6 +43,7 @@ public class SampleQueue {
if (sampleFromQueue == null) {
return SampleSource.NOTHING_READ;
}
+ sample.ensureSpaceForWrite(sampleFromQueue.size);
sample.size = sampleFromQueue.size;
sample.flags = sampleFromQueue.flags;
sample.timeUs = sampleFromQueue.timeUs;
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java b/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java
index 40c4ef95..0b219b41 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java
@@ -19,6 +19,7 @@ package com.android.tv.tuner.exoplayer.buffer;
import android.os.ConditionVariable;
import android.support.annotation.NonNull;
+
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.SampleHolder;
diff --git a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
index 258a5cd0..9fe921b8 100644
--- a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
+++ b/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java
@@ -17,20 +17,23 @@
package com.android.tv.tuner.exoplayer.buffer;
import android.content.Context;
-import android.media.MediaFormat;
import android.os.AsyncTask;
-import android.os.Looper;
import android.provider.Settings;
+import android.support.annotation.NonNull;
import android.util.Pair;
+import com.android.tv.common.SoftPreconditions;
+
import java.io.File;
import java.util.ArrayList;
+import java.util.List;
import java.util.SortedMap;
/**
* Manages Trickplay storage.
*/
public class TrickplayStorageManager implements BufferManager.StorageManager {
+ // TODO: Support multi-sessions.
private static final String BUFFER_DIR = "timeshift";
// Copied from android.provider.Settings.Global (hidden fields)
@@ -43,53 +46,68 @@ public class TrickplayStorageManager implements BufferManager.StorageManager {
private static final int DEFAULT_THRESHOLD_PERCENTAGE = 10;
private static final long DEFAULT_THRESHOLD_MAX_BYTES = 500L * 1024 * 1024;
- private final File mBufferDir;
+ private static AsyncTask<Void, Void, Void> sLastCacheCleanUpTask;
+ private static File sBufferDir;
+ private static long sStorageBufferBytes;
+
private final long mMaxBufferSize;
- private final long mStorageBufferBytes;
- private static long getStorageBufferBytes(Context context, File path) {
+ private static void initParamsIfNeeded(Context context, @NonNull File path) {
+ // TODO: Support multi-sessions.
+ SoftPreconditions.checkState(
+ sBufferDir == null || sBufferDir.equals(path));
+ if (path.equals(sBufferDir)) {
+ return;
+ }
+ sBufferDir = path;
long lowPercentage = Settings.Global.getInt(context.getContentResolver(),
SYS_STORAGE_THRESHOLD_PERCENTAGE, DEFAULT_THRESHOLD_PERCENTAGE);
- long lowBytes = path.getTotalSpace() * lowPercentage / 100;
+ long lowPercentageToBytes = path.getTotalSpace() * lowPercentage / 100;
long maxLowBytes = Settings.Global.getLong(context.getContentResolver(),
SYS_STORAGE_THRESHOLD_MAX_BYTES, DEFAULT_THRESHOLD_MAX_BYTES);
- return Math.min(lowBytes, maxLowBytes);
+ sStorageBufferBytes = Math.min(lowPercentageToBytes, maxLowBytes);
}
- public TrickplayStorageManager(Context context, File baseDir, long maxBufferSize) {
- mBufferDir = new File(baseDir, BUFFER_DIR);
- mBufferDir.mkdirs();
+ public TrickplayStorageManager(Context context, @NonNull File baseDir, long maxBufferSize) {
+ initParamsIfNeeded(context, new File(baseDir, BUFFER_DIR));
+ sBufferDir.mkdirs();
mMaxBufferSize = maxBufferSize;
clearStorage();
- mStorageBufferBytes = getStorageBufferBytes(context, mBufferDir);
}
- @Override
- public void clearStorage() {
- File files[] = mBufferDir.listFiles();
- if (files == null || files.length == 0) {
- return;
+ private void clearStorage() {
+ long now = System.currentTimeMillis();
+ if (sLastCacheCleanUpTask != null) {
+ sLastCacheCleanUpTask.cancel(true);
}
- if (Looper.myLooper() == Looper.getMainLooper()) {
- new AsyncTask<Void, Void, Void>() {
- @Override
- protected Void doInBackground(Void... params) {
- for (File file : files) {
+ sLastCacheCleanUpTask = new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ if (isCancelled()) {
+ return null;
+ }
+ File files[] = sBufferDir.listFiles();
+ if (files == null || files.length == 0) {
+ return null;
+ }
+ for (File file : files) {
+ if (isCancelled()) {
+ break;
+ }
+ long lastModified = file.lastModified();
+ if (lastModified != 0 && lastModified < now) {
file.delete();
}
- return null;
}
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- } else {
- for (File file : files) {
- file.delete();
+ return null;
}
- }
+ };
+ sLastCacheCleanUpTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@Override
public File getBufferDir() {
- return mBufferDir;
+ return sBufferDir;
}
@Override
@@ -104,25 +122,26 @@ public class TrickplayStorageManager implements BufferManager.StorageManager {
@Override
public boolean hasEnoughBuffer(long pendingDelete) {
- return mBufferDir.getUsableSpace() + pendingDelete >= mStorageBufferBytes;
+ return sBufferDir.getUsableSpace() + pendingDelete >= sStorageBufferBytes;
}
@Override
- public Pair<String, MediaFormat> readTrackInfoFile(boolean isAudio) {
+ public List<BufferManager.TrackFormat> readTrackInfoFiles(boolean isAudio) {
return null;
}
@Override
- public ArrayList<Long> readIndexFile(String trackId) {
+ public ArrayList<BufferManager.PositionHolder> readIndexFile(String trackId) {
return null;
}
@Override
- public void writeTrackInfoFile(String trackId, MediaFormat format, boolean isAudio) {
+ public void writeTrackInfoFiles(List<BufferManager.TrackFormat> formatList, boolean isAudio) {
}
@Override
- public void writeIndexFile(String trackName, SortedMap<Long, SampleChunk> index) {
+ public void writeIndexFile(String trackName,
+ SortedMap<Long, Pair<SampleChunk, Integer>> index) {
}
}
diff --git a/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java b/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java
index 97d9ece3..53678a85 100644
--- a/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java
+++ b/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java
@@ -36,6 +36,24 @@ public class ConnectionTypeFragment extends SetupMultiPaneFragment {
"com.android.tv.tuner.setup.ConnectionTypeFragment";
@Override
+ public void onCreate(Bundle savedInstanceState) {
+ ((TunerSetupActivity) getActivity()).generateTunerHal();
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public void onResume() {
+ ((TunerSetupActivity) getActivity()).generateTunerHal();
+ super.onResume();
+ }
+
+ @Override
+ public void onDestroy() {
+ ((TunerSetupActivity) getActivity()).clearTunerHal();
+ super.onDestroy();
+ }
+
+ @Override
protected SetupGuidedStepFragment onCreateContentFragment() {
return new ContentFragment();
}
diff --git a/src/com/android/tv/tuner/setup/PostalCodeFragment.java b/src/com/android/tv/tuner/setup/PostalCodeFragment.java
new file mode 100644
index 00000000..a4dd494c
--- /dev/null
+++ b/src/com/android/tv/tuner/setup/PostalCodeFragment.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2017 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.tv.tuner.setup;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
+import android.support.v17.leanback.widget.GuidedAction;
+import android.support.v17.leanback.widget.GuidedActionsStylist;
+import android.text.InputFilter;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.tv.R;
+import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
+import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
+import com.android.tv.tuner.util.PostalCodeUtils;
+
+import java.util.List;
+
+/**
+ * A fragment for initial screen.
+ */
+public class PostalCodeFragment extends SetupMultiPaneFragment {
+ public static final String ACTION_CATEGORY =
+ "com.android.tv.tuner.setup.PostalCodeFragment";
+ private static final int VIEW_TYPE_EDITABLE = 1;
+
+ @Override
+ protected SetupGuidedStepFragment onCreateContentFragment() {
+ ContentFragment fragment = new ContentFragment();
+ Bundle arguments = new Bundle();
+ arguments.putBoolean(SetupGuidedStepFragment.KEY_THREE_PANE, true);
+ fragment.setArguments(arguments);
+ return fragment;
+ }
+
+ @Override
+ protected String getActionCategory() {
+ return ACTION_CATEGORY;
+ }
+
+ @Override
+ protected boolean needsDoneButton() {
+ return true;
+ }
+
+ @Override
+ protected boolean needsSkipButton() {
+ return true;
+ }
+
+ @Override
+ protected void setOnClickAction(View view, final String category, final int actionId) {
+ if (actionId == ACTION_DONE) {
+ view.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ CharSequence postalCode =
+ ((ContentFragment) getContentFragment()).mEditAction.getTitle();
+ if (postalCode != null && postalCode.length() == 5) {
+ PostalCodeUtils.setLastPostalCode(getContext(), postalCode.toString());
+ onActionClick(category, actionId);
+ } else {
+ ContentFragment contentFragment = (ContentFragment) getContentFragment();
+ contentFragment.mEditAction.setDescription(
+ getString(R.string.postal_code_invalid_warning));
+ contentFragment.notifyActionChanged(0);
+ contentFragment.mEditedActionView.performClick();
+ }
+ }
+ });
+ } else if (actionId == ACTION_SKIP) {
+ super.setOnClickAction(view, category, ACTION_SKIP);
+ }
+ }
+
+ public static class ContentFragment extends SetupGuidedStepFragment {
+ private GuidedAction mEditAction;
+ private View mEditedActionView;
+ private View mDoneActionView;
+ private boolean mProceed;
+
+ @Override
+ public void onGuidedActionFocused(GuidedAction action) {
+ if (action.equals(mEditAction)) {
+ if (mProceed) {
+ // "NEXT" in IME was just clicked, moves focus to Done button.
+ if (mDoneActionView == null) {
+ mDoneActionView = getActivity().findViewById(R.id.button_done);
+ }
+ mDoneActionView.requestFocus();
+ mProceed = false;
+ } else {
+ // Directly opens IME to input postal/zip code.
+ if (mEditedActionView == null) {
+ mEditedActionView = getView().findViewById(R.id.guidedactions_editable);
+ ((TextView) mEditedActionView.findViewById(R.id.guidedactions_item_title))
+ .setFilters(new InputFilter[]{new InputFilter() {
+ @Override
+ public CharSequence filter(CharSequence source, int start,
+ int end, Spanned dest, int dstart, int dend) {
+ try {
+ Integer.parseInt(source.toString());
+ return null;
+ } catch (NumberFormatException e) {
+ return "";
+ }
+ }
+ }, new InputFilter.LengthFilter(5)});
+ }
+ mEditedActionView.performClick();
+ }
+ }
+ }
+
+ @Override
+ public long onGuidedActionEditedAndProceed(GuidedAction action) {
+ mProceed = true;
+ return 0;
+ }
+
+ @NonNull
+ @Override
+ public Guidance onCreateGuidance(Bundle savedInstanceState) {
+ String title = getString(R.string.postal_code_guidance_title);
+ String description = getString(R.string.postal_code_guidance_description);
+ String breadcrumb = getString(R.string.ut_setup_breadcrumb);
+ return new Guidance(title, description, breadcrumb, null);
+ }
+
+ @Override
+ public void onCreateActions(@NonNull List<GuidedAction> actions,
+ Bundle savedInstanceState) {
+ String description = getString(R.string.postal_code_action_description);
+ mEditAction = new GuidedAction.Builder(getActivity()).id(0).editable(true)
+ .description(description).build();
+ actions.add(mEditAction);
+ }
+
+ @Override
+ protected String getActionCategory() {
+ return ACTION_CATEGORY;
+ }
+
+ @Override
+ public GuidedActionsStylist onCreateActionsStylist() {
+ return new GuidedActionsStylist() {
+ @Override
+ public int getItemViewType(GuidedAction action) {
+ if (action.isEditable()) {
+ return VIEW_TYPE_EDITABLE;
+ }
+ return super.getItemViewType(action);
+ }
+
+ @Override
+ public int onProvideItemLayoutId(int viewType) {
+ if (viewType == VIEW_TYPE_EDITABLE) {
+ return R.layout.guided_action_editable;
+ }
+ return super.onProvideItemLayoutId(viewType);
+ }
+ };
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/tuner/setup/ScanFragment.java b/src/com/android/tv/tuner/setup/ScanFragment.java
index 3b61debb..75b28e32 100644
--- a/src/com/android/tv/tuner/setup/ScanFragment.java
+++ b/src/com/android/tv/tuner/setup/ScanFragment.java
@@ -21,6 +21,7 @@ import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.AsyncTask;
+import android.os.Build;
import android.os.Bundle;
import android.os.ConditionVariable;
import android.os.Handler;
@@ -35,14 +36,13 @@ import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
-import com.android.tv.common.AutoCloseableUtils;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.ui.setup.SetupFragment;
import com.android.tv.tuner.ChannelScanFileParser;
-import com.android.tv.tuner.TunerHal;
import com.android.tv.tuner.R;
+import com.android.tv.tuner.TunerHal;
import com.android.tv.tuner.TunerPreferences;
-import com.android.tv.tuner.data.nano.Channel;
+import com.android.tv.tuner.data.Channel;
import com.android.tv.tuner.data.PsipData;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.source.FileTsStreamer;
@@ -51,7 +51,6 @@ import com.android.tv.tuner.source.TsStreamer;
import com.android.tv.tuner.source.TunerTsStreamer;
import com.android.tv.tuner.tvinput.ChannelDataManager;
import com.android.tv.tuner.tvinput.EventDetector;
-import com.android.tv.tuner.util.TunerInputInfoUtils;
import junit.framework.Assert;
@@ -67,6 +66,7 @@ import java.util.concurrent.TimeUnit;
public class ScanFragment extends SetupFragment {
private static final String TAG = "ScanFragment";
private static final boolean DEBUG = false;
+
// In the fake mode, the connection to antenna or cable is not necessary.
// Instead dummy channels are added.
private static final boolean FAKE_MODE = false;
@@ -98,6 +98,7 @@ public class ScanFragment extends SetupFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
+ if (DEBUG) Log.d(TAG, "onCreateView");
View view = super.onCreateView(inflater, container, savedInstanceState);
mChannelDataManager = new ChannelDataManager(getActivity());
mChannelDataManager.checkDataVersion(getActivity());
@@ -120,13 +121,19 @@ public class ScanFragment extends SetupFragment {
}
});
Bundle args = getArguments();
+ int tunerType = (args == null ? 0 : args.getInt(TunerSetupActivity.KEY_TUNER_TYPE, 0));
// TODO: Handle the case when the fragment is restored.
startScan(args == null ? 0 : args.getInt(EXTRA_FOR_CHANNEL_SCAN_FILE, 0));
TextView scanTitleView = (TextView) view.findViewById(R.id.tune_title);
- if (TunerInputInfoUtils.isBuiltInTuner(getActivity())){
- scanTitleView.setText(R.string.bt_channel_scan);
- } else {
- scanTitleView.setText(R.string.ut_channel_scan);
+ switch (tunerType) {
+ case TunerHal.TUNER_TYPE_USB:
+ scanTitleView.setText(R.string.ut_channel_scan);
+ break;
+ case TunerHal.TUNER_TYPE_NETWORK:
+ scanTitleView.setText(R.string.nt_channel_scan);
+ break;
+ default:
+ scanTitleView.setText(R.string.bt_channel_scan);
}
return view;
}
@@ -147,12 +154,14 @@ public class ScanFragment extends SetupFragment {
}
@Override
- public void onDetach() {
+ public void onPause() {
+ Log.d(TAG, "onPause");
if (mChannelScanTask != null) {
// Ensure scan task will stop.
+ Log.w(TAG, "The activity went to the background. Stopping channel scan.");
mChannelScanTask.stopScan();
}
- super.onDetach();
+ super.onPause();
}
/**
@@ -168,7 +177,9 @@ public class ScanFragment extends SetupFragment {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
- mChannelScanTask.showFinishingProgressDialog();
+ if (mChannelScanTask != null) {
+ mChannelScanTask.showFinishingProgressDialog();
+ }
}
}, SHOW_PROGRESS_DIALOG_DELAY_MS);
@@ -255,7 +266,7 @@ public class ScanFragment extends SetupFragment {
if (FAKE_MODE) {
mScanTsStreamer = new FakeTsStreamer(this);
} else {
- TunerHal hal = TunerHal.createInstance(mActivity.getApplicationContext());
+ TunerHal hal = ((TunerSetupActivity) mActivity).getTunerHal();
if (hal == null) {
throw new RuntimeException("Failed to open a DVB device");
}
@@ -316,10 +327,17 @@ public class ScanFragment extends SetupFragment {
@Override
protected void onProgressUpdate(Integer... values) {
- mProgressBar.setProgress(values[0]);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ mProgressBar.setProgress(values[0], true);
+ } else {
+ mProgressBar.setProgress(values[0]);
+ }
}
private void stopScan() {
+ if (mLatch != null) {
+ mLatch.countDown();
+ }
mConditionStopped.open();
}
@@ -360,11 +378,7 @@ public class ScanFragment extends SetupFragment {
if (mConditionStopped.block(-1)) {
break;
}
- onProgressUpdate(MAX_PROGRESS * i++ / mScanChannelList.size());
- }
- if (mScanTsStreamer instanceof TunerTsStreamer) {
- AutoCloseableUtils.closeQuietly(
- ((TunerTsStreamer) mScanTsStreamer).getTunerHal());
+ publishProgress(MAX_PROGRESS * i++ / mScanChannelList.size());
}
mChannelDataManager.notifyScanCompleted();
if (!mConditionStopped.block(-1)) {
@@ -454,7 +468,13 @@ public class ScanFragment extends SetupFragment {
if (mFinishingProgressDialog != null) {
mFinishingProgressDialog.dismiss();
}
- onActionClick(ACTION_CATEGORY, mIsCanceled ? ACTION_CANCEL : ACTION_FINISH);
+ // If the fragment is not resumed, the next fragment (scan result page) can't be
+ // displayed. In that case, just close the activity.
+ if (isResumed()) {
+ onActionClick(ACTION_CATEGORY, mIsCanceled ? ACTION_CANCEL : ACTION_FINISH);
+ } else if (getActivity() != null) {
+ getActivity().finish();
+ }
mChannelScanTask = null;
}
}
diff --git a/src/com/android/tv/tuner/setup/ScanResultFragment.java b/src/com/android/tv/tuner/setup/ScanResultFragment.java
index 068543cd..3b8cd823 100644
--- a/src/com/android/tv/tuner/setup/ScanResultFragment.java
+++ b/src/com/android/tv/tuner/setup/ScanResultFragment.java
@@ -26,6 +26,7 @@ import android.support.v17.leanback.widget.GuidedAction;
import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
import com.android.tv.tuner.R;
+import com.android.tv.tuner.TunerHal;
import com.android.tv.tuner.TunerPreferences;
import com.android.tv.tuner.util.TunerInputInfoUtils;
@@ -76,11 +77,19 @@ public class ScanResultFragment extends SetupMultiPaneFragment {
mChannelCountOnPreference, mChannelCountOnPreference);
breadcrumb = null;
} else {
+ Bundle args = getArguments();
+ int tunerType =
+ (args == null ? 0 : args.getInt(TunerSetupActivity.KEY_TUNER_TYPE, 0));
title = getString(R.string.ut_result_not_found_title);
- if (TunerInputInfoUtils.isBuiltInTuner(getActivity())) {
- description = getString(R.string.bt_result_not_found_description);
- } else {
- description = getString(R.string.ut_result_not_found_description);
+ switch (tunerType) {
+ case TunerHal.TUNER_TYPE_USB:
+ description = getString(R.string.ut_result_not_found_description);
+ break;
+ case TunerHal.TUNER_TYPE_NETWORK:
+ description = getString(R.string.nt_result_not_found_description);
+ break;
+ default:
+ description = getString(R.string.bt_result_not_found_description);
}
breadcrumb = getString(R.string.ut_setup_breadcrumb);
}
diff --git a/src/com/android/tv/tuner/setup/TunerSetupActivity.java b/src/com/android/tv/tuner/setup/TunerSetupActivity.java
index 78121bc5..f618c699 100644
--- a/src/com/android/tv/tuner/setup/TunerSetupActivity.java
+++ b/src/com/android/tv/tuner/setup/TunerSetupActivity.java
@@ -29,35 +29,53 @@ import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.tv.TvContract;
+import android.os.AsyncTask;
import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
import android.support.v4.app.NotificationCompat;
+import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.widget.Toast;
import com.android.tv.TvApplication;
+import com.android.tv.common.AutoCloseableUtils;
import com.android.tv.common.TvCommonConstants;
import com.android.tv.common.TvCommonUtils;
import com.android.tv.common.ui.setup.SetupActivity;
import com.android.tv.common.ui.setup.SetupFragment;
import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
+import com.android.tv.experiments.Experiments;
import com.android.tv.tuner.R;
import com.android.tv.tuner.TunerHal;
import com.android.tv.tuner.TunerPreferences;
import com.android.tv.tuner.tvinput.TunerTvInputService;
-import com.android.tv.tuner.util.TunerInputInfoUtils;
+import com.android.tv.tuner.util.PostalCodeUtils;
+import com.android.tv.util.LocationUtils;
+
+import java.util.Locale;
+import java.util.concurrent.Executor;
/**
* An activity that serves tuner setup process.
*/
public class TunerSetupActivity extends SetupActivity {
- private final String TAG = "TunerSetupActivity";
+ private static final String TAG = "TunerSetupActivity";
+ private static final boolean DEBUG = false;
+
+ /**
+ * Key for passing tuner type to sub-fragments.
+ */
+ public static final String KEY_TUNER_TYPE = "TunerSetupActivity.tunerType";
+
// For the recommendation card
private static final String TV_ACTIVITY_CLASS_NAME = "com.android.tv.TvActivity";
private static final String NOTIFY_TAG = "TunerSetup";
private static final int NOTIFY_ID = 1000;
private static final String TAG_DRAWABLE = "drawable";
private static final String TAG_ICON = "ic_launcher_s";
+ private static final int PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION = 1;
private static final int CHANNEL_MAP_SCAN_FILE[] = {
R.raw.ut_us_atsc_center_frequencies_8vsb,
@@ -69,9 +87,13 @@ public class TunerSetupActivity extends SetupActivity {
R.raw.ut_kr_dev_cj_cable_center_frequencies_qam256};
private ScanFragment mLastScanFragment;
+ private Integer mTunerType;
+ private TunerHalFactory mTunerHalFactory;
+ private boolean mNeedToShowPostalCodeFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
+ if (DEBUG) Log.d(TAG, "onCreate");
TvApplication.setCurrentRunningProcess(this, false);
super.onCreate(savedInstanceState);
// TODO: check {@link shouldShowRequestPermissionRationale}.
@@ -79,13 +101,49 @@ public class TunerSetupActivity extends SetupActivity {
!= PackageManager.PERMISSION_GRANTED) {
// No need to check the request result.
requestPermissions(new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION},
- 0);
+ PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION);
+ }
+ mTunerType = TunerHal.getTunerTypeAndCount(this).first;
+ if (mTunerType == null) {
+ finish();
+ } else {
+ mTunerHalFactory = new TunerHalFactory(getApplicationContext());
+ }
+ try {
+ // Updating postal code takes time, therefore we called it here for "warm-up".
+ PostalCodeUtils.setLastPostalCode(this, null);
+ PostalCodeUtils.updatePostalCode(this);
+ } catch (Exception e) {
+ // Do nothing. If the last known postal code is null, we'll show guided fragment to
+ // prompt users to input postal code before ConnectionTypeFragment is shown.
+ Log.i(TAG, "Can't get postal code:" + e);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
+ @NonNull int[] grantResults) {
+ if (requestCode == PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED
+ && Experiments.CLOUD_EPG.get()) {
+ try {
+ // Updating postal code takes time, therefore we should update postal code
+ // right after the permission is granted, so that the subsequent operations,
+ // especially EPG fetcher, could get the newly updated postal code.
+ PostalCodeUtils.updatePostalCode(this);
+ } catch (Exception e) {
+ // Do nothing
+ }
+ }
}
}
@Override
protected Fragment onCreateInitialFragment() {
SetupFragment fragment = new WelcomeFragment();
+ Bundle args = new Bundle();
+ args.putInt(KEY_TUNER_TYPE, mTunerType);
+ fragment.setArguments(args);
fragment.setShortDistance(SetupFragment.FRAGMENT_EXIT_TRANSITION
| SetupFragment.FRAGMENT_REENTER_TRANSITION);
return fragment;
@@ -102,33 +160,41 @@ public class TunerSetupActivity extends SetupActivity {
finish();
break;
default: {
- SetupFragment fragment = new ConnectionTypeFragment();
- fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION
- | SetupFragment.FRAGMENT_RETURN_TRANSITION);
- showFragment(fragment, true);
+ if (mNeedToShowPostalCodeFragment
+ || Locale.US.getCountry().equalsIgnoreCase(
+ LocationUtils.getCurrentCountry(getApplicationContext()))
+ && TextUtils.isEmpty(PostalCodeUtils.getLastPostalCode(this))) {
+ // We cannot get postal code automatically. Postal code input fragment
+ // should always be shown even if users have input some valid postal
+ // code in this activity before.
+ mNeedToShowPostalCodeFragment = true;
+ showPostalCodeFragment();
+ } else {
+ showConnectionTypeFragment();
+ }
break;
}
}
return true;
+ case PostalCodeFragment.ACTION_CATEGORY:
+ if (actionId == SetupMultiPaneFragment.ACTION_DONE
+ || actionId == SetupMultiPaneFragment.ACTION_SKIP) {
+ showConnectionTypeFragment();
+ }
+ return true;
case ConnectionTypeFragment.ACTION_CATEGORY:
- TunerHal hal = TunerHal.createInstance(getApplicationContext());
- if (hal == null) {
+ if (mTunerHalFactory.get() == null) {
finish();
Toast.makeText(getApplicationContext(),
R.string.ut_channel_scan_tuner_unavailable,Toast.LENGTH_LONG).show();
return true;
}
- try {
- hal.close();
- } catch (Exception e) {
- Log.e(TAG, "Tuner hal close failed", e);
- return true;
- }
mLastScanFragment = new ScanFragment();
- Bundle args = new Bundle();
- args.putInt(ScanFragment.EXTRA_FOR_CHANNEL_SCAN_FILE,
+ Bundle args1 = new Bundle();
+ args1.putInt(ScanFragment.EXTRA_FOR_CHANNEL_SCAN_FILE,
CHANNEL_MAP_SCAN_FILE[actionId]);
- mLastScanFragment.setArguments(args);
+ args1.putInt(KEY_TUNER_TYPE, mTunerType);
+ mLastScanFragment.setArguments(args1);
showFragment(mLastScanFragment, true);
return true;
case ScanFragment.ACTION_CATEGORY:
@@ -137,7 +203,11 @@ public class TunerSetupActivity extends SetupActivity {
getFragmentManager().popBackStack();
return true;
case ScanFragment.ACTION_FINISH:
+ mTunerHalFactory.clear();
SetupFragment fragment = new ScanResultFragment();
+ Bundle args2 = new Bundle();
+ args2.putInt(KEY_TUNER_TYPE, mTunerType);
+ fragment.setArguments(args2);
fragment.setShortDistance(SetupFragment.FRAGMENT_EXIT_TRANSITION
| SetupFragment.FRAGMENT_REENTER_TRANSITION);
showFragment(fragment, true);
@@ -213,7 +283,7 @@ public class TunerSetupActivity extends SetupActivity {
String inputId = TvContract.buildInputId(new ComponentName(context.getPackageName(),
TunerTvInputService.class.getName()));
- // Make an intent to launch the setup activity of USB tuner TV input.
+ // Make an intent to launch the setup activity of TV tuner input.
Intent intent = TvCommonUtils.createSetupIntent(
new Intent(context, TunerSetupActivity.class), inputId);
intent.putExtra(TvCommonConstants.EXTRA_INPUT_ID, inputId);
@@ -224,6 +294,27 @@ public class TunerSetupActivity extends SetupActivity {
}
/**
+ * Gets the currently used tuner HAL.
+ */
+ TunerHal getTunerHal() {
+ return mTunerHalFactory.get();
+ }
+
+ /**
+ * Generates tuner HAL.
+ */
+ void generateTunerHal() {
+ mTunerHalFactory.generate();
+ }
+
+ /**
+ * Clears the currently used tuner HAL.
+ */
+ void clearTunerHal() {
+ mTunerHalFactory.clear();
+ }
+
+ /**
* Returns a {@link PendingIntent} to launch the tuner TV input service.
*
* @param context a {@link Context} instance
@@ -242,12 +333,19 @@ public class TunerSetupActivity extends SetupActivity {
Resources resources = context.getResources();
String focusedTitle = resources.getString(
R.string.ut_setup_recommendation_card_focused_title);
- String title;
- if (TunerInputInfoUtils.isBuiltInTuner(context)) {
- title = resources.getString(R.string.bt_setup_recommendation_card_title);
- } else {
- title = resources.getString(R.string.ut_setup_recommendation_card_title);
+ int titleStringId = 0;
+ switch (TunerHal.getTunerTypeAndCount(context).first) {
+ case TunerHal.TUNER_TYPE_BUILT_IN:
+ titleStringId = R.string.bt_setup_recommendation_card_title;
+ break;
+ case TunerHal.TUNER_TYPE_USB:
+ titleStringId = R.string.ut_setup_recommendation_card_title;
+ break;
+ case TunerHal.TUNER_TYPE_NETWORK:
+ titleStringId = R.string.nt_setup_recommendation_card_title;
+ break;
}
+ String title = resources.getString(titleStringId);
Bitmap largeIcon = BitmapFactory.decodeResource(resources,
R.drawable.recommendation_antenna);
@@ -269,6 +367,20 @@ public class TunerSetupActivity extends SetupActivity {
notificationManager.notify(NOTIFY_TAG, NOTIFY_ID, notification);
}
+ private void showPostalCodeFragment() {
+ SetupFragment fragment = new PostalCodeFragment();
+ fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION
+ | SetupFragment.FRAGMENT_RETURN_TRANSITION);
+ showFragment(fragment, true);
+ }
+
+ private void showConnectionTypeFragment() {
+ SetupFragment fragment = new ConnectionTypeFragment();
+ fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION
+ | SetupFragment.FRAGMENT_RETURN_TRANSITION);
+ showFragment(fragment, true);
+ }
+
/**
* Cancels the previously shown recommendation card.
*
@@ -279,4 +391,80 @@ public class TunerSetupActivity extends SetupActivity {
.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(NOTIFY_TAG, NOTIFY_ID);
}
-}
+
+ @VisibleForTesting
+ static class TunerHalFactory {
+ private Context mContext;
+ @VisibleForTesting
+ TunerHal mTunerHal;
+ private GenerateTunerHalTask mGenerateTunerHalTask;
+ private final Executor mExecutor;
+
+ TunerHalFactory(Context context) {
+ this(context, AsyncTask.SERIAL_EXECUTOR);
+ }
+
+ TunerHalFactory(Context context, Executor executor) {
+ mContext = context;
+ mExecutor = executor;
+ }
+
+ /**
+ * Returns tuner HAL currently used. If it's {@code null} and tuner HAL is not generated
+ * before, tries to generate it synchronously.
+ */
+ TunerHal get() {
+ if (mGenerateTunerHalTask != null
+ && mGenerateTunerHalTask.getStatus() != AsyncTask.Status.FINISHED) {
+ try {
+ return mGenerateTunerHalTask.get();
+ } catch (Exception e) {
+ Log.e(TAG, "Cannot get Tuner HAL: " + e);
+ }
+ } else if (mGenerateTunerHalTask == null && mTunerHal == null) {
+ mTunerHal = createInstance();
+ }
+ return mTunerHal;
+ }
+
+ /**
+ * Generates tuner hal for scanning with asynchronous tasks.
+ */
+ void generate() {
+ if (mGenerateTunerHalTask == null && mTunerHal == null) {
+ mGenerateTunerHalTask = new GenerateTunerHalTask();
+ mGenerateTunerHalTask.executeOnExecutor(mExecutor);
+ }
+ }
+
+ /**
+ * Clears the currently used tuner hal.
+ */
+ void clear() {
+ if (mGenerateTunerHalTask != null) {
+ mGenerateTunerHalTask.cancel(true);
+ mGenerateTunerHalTask = null;
+ }
+ if (mTunerHal != null) {
+ AutoCloseableUtils.closeQuietly(mTunerHal);
+ mTunerHal = null;
+ }
+ }
+
+ protected TunerHal createInstance() {
+ return TunerHal.createInstance(mContext);
+ }
+
+ class GenerateTunerHalTask extends AsyncTask<Void, Void, TunerHal> {
+ @Override
+ protected TunerHal doInBackground(Void... args) {
+ return createInstance();
+ }
+
+ @Override
+ protected void onPostExecute(TunerHal tunerHal) {
+ mTunerHal = tunerHal;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/tuner/setup/WelcomeFragment.java b/src/com/android/tv/tuner/setup/WelcomeFragment.java
index 7e809411..a3dddc72 100644
--- a/src/com/android/tv/tuner/setup/WelcomeFragment.java
+++ b/src/com/android/tv/tuner/setup/WelcomeFragment.java
@@ -27,6 +27,7 @@ import android.view.ViewGroup;
import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
import com.android.tv.tuner.R;
+import com.android.tv.tuner.TunerHal;
import com.android.tv.tuner.TunerPreferences;
import com.android.tv.tuner.util.TunerInputInfoUtils;
@@ -41,7 +42,9 @@ public class WelcomeFragment extends SetupMultiPaneFragment {
@Override
protected SetupGuidedStepFragment onCreateContentFragment() {
- return new ContentFragment();
+ ContentFragment fragment = new ContentFragment();
+ fragment.setArguments(getArguments());
+ return fragment;
}
@Override
@@ -70,20 +73,33 @@ public class WelcomeFragment extends SetupMultiPaneFragment {
public Guidance onCreateGuidance(Bundle savedInstanceState) {
String title;
String description;
+ int tunerType = getArguments().getInt(TunerSetupActivity.KEY_TUNER_TYPE,
+ TunerHal.TUNER_TYPE_BUILT_IN);
if (mChannelCountOnPreference == 0) {
- if (TunerInputInfoUtils.isBuiltInTuner(getActivity())) {
- title = getString(R.string.bt_setup_new_title);
- description = getString(R.string.bt_setup_new_description);
- } else {
- title = getString(R.string.ut_setup_new_title);
- description = getString(R.string.ut_setup_new_description);
+ switch (tunerType) {
+ case TunerHal.TUNER_TYPE_USB:
+ title = getString(R.string.ut_setup_new_title);
+ description = getString(R.string.ut_setup_new_description);
+ break;
+ case TunerHal.TUNER_TYPE_NETWORK:
+ title = getString(R.string.nt_setup_new_title);
+ description = getString(R.string.nt_setup_new_description);
+ break;
+ default:
+ title = getString(R.string.bt_setup_new_title);
+ description = getString(R.string.bt_setup_new_description);
}
} else {
title = getString(R.string.bt_setup_again_title);
- if (TunerInputInfoUtils.isBuiltInTuner(getActivity())) {
- description = getString(R.string.bt_setup_again_description);
- } else {
- description = getString(R.string.ut_setup_again_description);
+ switch (tunerType) {
+ case TunerHal.TUNER_TYPE_USB:
+ description = getString(R.string.ut_setup_again_description);
+ break;
+ case TunerHal.TUNER_TYPE_NETWORK:
+ description = getString(R.string.nt_setup_again_description);
+ break;
+ default:
+ description = getString(R.string.bt_setup_again_description);
}
}
return new Guidance(title, description, null, null);
diff --git a/src/com/android/tv/tuner/source/FileTsStreamer.java b/src/com/android/tv/tuner/source/FileTsStreamer.java
index 14997ee4..80ec8a56 100644
--- a/src/com/android/tv/tuner/source/FileTsStreamer.java
+++ b/src/com/android/tv/tuner/source/FileTsStreamer.java
@@ -256,7 +256,7 @@ public class FileTsStreamer implements TsStreamer {
* Returns whether the current pid filter is empty or not.
*/
public boolean isFilterEmpty() {
- return mPids.size() > 0;
+ return mPids.size() == 0;
}
/**
diff --git a/src/com/android/tv/tuner/source/TsDataSourceManager.java b/src/com/android/tv/tuner/source/TsDataSourceManager.java
index ccbb75ba..32504b95 100644
--- a/src/com/android/tv/tuner/source/TsDataSourceManager.java
+++ b/src/com/android/tv/tuner/source/TsDataSourceManager.java
@@ -17,8 +17,10 @@
package com.android.tv.tuner.source;
import android.content.Context;
+import android.support.annotation.VisibleForTesting;
-import com.android.tv.tuner.data.nano.Channel;
+import com.android.tv.tuner.TunerHal;
+import com.android.tv.tuner.data.Channel;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.tvinput.EventDetector;
@@ -127,6 +129,14 @@ public class TsDataSourceManager {
}
/**
+ * Add tuner hal into TunerTsStreamerManager for test.
+ */
+ @VisibleForTesting
+ public void addTunerHalForTest(TunerHal tunerHal) {
+ mTunerStreamerManager.addTunerHal(tunerHal, mId);
+ }
+
+ /**
* Releases persistent resources.
*/
public void release() {
diff --git a/src/com/android/tv/tuner/source/TunerTsStreamer.java b/src/com/android/tv/tuner/source/TunerTsStreamer.java
index b24048e6..65b11a5a 100644
--- a/src/com/android/tv/tuner/source/TunerTsStreamer.java
+++ b/src/com/android/tv/tuner/source/TunerTsStreamer.java
@@ -42,15 +42,17 @@ public class TunerTsStreamer implements TsStreamer {
private static final int MIN_READ_UNIT = 1500;
private static final int READ_BUFFER_SIZE = MIN_READ_UNIT * 10; // ~15KB
private static final int CIRCULAR_BUFFER_SIZE = MIN_READ_UNIT * 20000; // ~ 30MB
+ private static final int TS_PACKET_SIZE = 188;
private static final int READ_TIMEOUT_MS = 5000; // 5 secs.
private static final int BUFFER_UNDERRUN_SLEEP_MS = 10;
+ private static final int READ_ERROR_STREAMING_ENDED = -1;
+ private static final int READ_ERROR_BUFFER_OVERWRITTEN = -2;
private final Object mCircularBufferMonitor = new Object();
private final byte[] mCircularBuffer = new byte[CIRCULAR_BUFFER_SIZE];
private long mBytesFetched;
private final AtomicLong mLastReadPosition = new AtomicLong();
- private boolean mEndOfStreamSent;
private boolean mStreaming;
private final TunerHal mTunerHal;
@@ -59,6 +61,7 @@ public class TunerTsStreamer implements TsStreamer {
private final EventDetector mEventDetector;
private final TsStreamWriter mTsStreamWriter;
+ private String mChannelNumber;
public static class TunerDataSource extends TsDataSource {
private final TunerTsStreamer mTsStreamer;
@@ -103,6 +106,15 @@ public class TunerTsStreamer implements TsStreamer {
offset, readLength);
if (ret > 0) {
mLastReadPosition.addAndGet(ret);
+ } else if (ret == READ_ERROR_BUFFER_OVERWRITTEN) {
+ long currentPosition = mStartBufferedPosition + mLastReadPosition.get();
+ long endPosition = mTsStreamer.getBufferedPosition();
+ long diff = ((endPosition - currentPosition + TS_PACKET_SIZE - 1) / TS_PACKET_SIZE)
+ * TS_PACKET_SIZE;
+ Log.w(TAG, "Demux position jump by overwritten buffer: " + diff);
+ mStartBufferedPosition = currentPosition + diff;
+ mLastReadPosition.set(0);
+ return 0;
}
return ret;
}
@@ -114,7 +126,10 @@ public class TunerTsStreamer implements TsStreamer {
*/
public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener, Context context) {
mTunerHal = tunerHal;
- mEventDetector = new EventDetector(mTunerHal, eventListener);
+ mEventDetector = new EventDetector(mTunerHal);
+ if (eventListener != null) {
+ mEventDetector.registerListener(eventListener);
+ }
mTsStreamWriter = context != null && TunerPreferences.getStoreTsStream(context) ?
new TsStreamWriter(context) : null;
}
@@ -125,7 +140,8 @@ public class TunerTsStreamer implements TsStreamer {
@Override
public boolean startStream(TunerChannel channel) {
- if (mTunerHal.tune(channel.getFrequency(), channel.getModulation())) {
+ if (mTunerHal.tune(channel.getFrequency(), channel.getModulation(),
+ channel.getDisplayNumber(false))) {
if (channel.hasVideo()) {
mTunerHal.addPidFilter(channel.getVideoPid(),
TunerHal.FILTER_TYPE_VIDEO);
@@ -148,6 +164,7 @@ public class TunerTsStreamer implements TsStreamer {
channel.getProgramNumber());
}
mChannel = channel;
+ mChannelNumber = channel.getDisplayNumber();
synchronized (mCircularBufferMonitor) {
if (mStreaming) {
Log.w(TAG, "Streaming should be stopped before start streaming");
@@ -156,7 +173,6 @@ public class TunerTsStreamer implements TsStreamer {
mStreaming = true;
mBytesFetched = 0;
mLastReadPosition.set(0L);
- mEndOfStreamSent = false;
}
if (mTsStreamWriter != null) {
mTsStreamWriter.setChannel(mChannel);
@@ -172,7 +188,7 @@ public class TunerTsStreamer implements TsStreamer {
@Override
public boolean startStream(ChannelScanFileParser.ScanChannel channel) {
- if (mTunerHal.tune(channel.frequency, channel.modulation)) {
+ if (mTunerHal.tune(channel.frequency, channel.modulation, null)) {
mEventDetector.startDetecting(
channel.frequency, channel.modulation, EventDetector.ALL_PROGRAM_NUMBERS);
synchronized (mCircularBufferMonitor) {
@@ -183,7 +199,6 @@ public class TunerTsStreamer implements TsStreamer {
mStreaming = true;
mBytesFetched = 0;
mLastReadPosition.set(0L);
- mEndOfStreamSent = false;
}
mStreamingThread = new StreamingThread();
mStreamingThread.start();
@@ -258,6 +273,22 @@ public class TunerTsStreamer implements TsStreamer {
}
}
+ public String getStreamerInfo() {
+ return "Channel: " + mChannelNumber + ", Streaming: " + mStreaming;
+ }
+
+ public void registerListener(EventListener listener) {
+ if (mEventDetector != null && listener != null) {
+ mEventDetector.registerListener(listener);
+ }
+ }
+
+ public void unregisterListener(EventListener listener) {
+ if (mEventDetector != null) {
+ mEventDetector.unregisterListener(listener);
+ }
+ }
+
private class StreamingThread extends Thread {
@Override
public void run() {
@@ -321,21 +352,14 @@ public class TunerTsStreamer implements TsStreamer {
* @throws IOException
*/
public int readAt(long pos, byte[] buffer, int offset, int amount) throws IOException {
- long readStartTime = System.currentTimeMillis();
while (true) {
synchronized (mCircularBufferMonitor) {
- if (mEndOfStreamSent || !mStreaming) {
- return -1;
+ if (!mStreaming) {
+ return READ_ERROR_STREAMING_ENDED;
}
if (mBytesFetched - CIRCULAR_BUFFER_SIZE > pos) {
- Log.e(TAG, "Demux is requesting the data which is already overwritten.");
- return -1;
- }
- if (System.currentTimeMillis() - readStartTime > READ_TIMEOUT_MS) {
- // Nothing was received during READ_TIMEOUT_MS before.
- mEndOfStreamSent = true;
- mCircularBufferMonitor.notifyAll();
- return -1;
+ Log.w(TAG, "Demux is requesting the data which is already overwritten.");
+ return READ_ERROR_BUFFER_OVERWRITTEN;
}
if (mBytesFetched < pos + amount) {
try {
diff --git a/src/com/android/tv/tuner/source/TunerTsStreamerManager.java b/src/com/android/tv/tuner/source/TunerTsStreamerManager.java
index cf1f6dcf..fcd14116 100644
--- a/src/com/android/tv/tuner/source/TunerTsStreamerManager.java
+++ b/src/com/android/tv/tuner/source/TunerTsStreamerManager.java
@@ -42,6 +42,7 @@ class TunerTsStreamerManager {
private final Object mCancelLock = new Object();
private final StreamerFinder mStreamerFinder = new StreamerFinder();
private final Map<Integer, TsStreamerCreator> mCreators = new HashMap<>();
+ private final Map<Integer, EventDetector.EventListener> mListeners = new HashMap<>();
private final Map<TsDataSource, TunerTsStreamer> mSourceToStreamerMap = new HashMap<>();
private final TunerHalManager mTunerHalManager = new TunerHalManager();
private static TunerTsStreamerManager sInstance;
@@ -68,6 +69,8 @@ class TunerTsStreamerManager {
mStreamerFinder.appendSessionLocked(channel, sessionId);
TunerTsStreamer streamer = mStreamerFinder.getStreamerLocked(channel);
TsDataSource source = streamer.createDataSource();
+ mListeners.put(sessionId, listener);
+ streamer.registerListener(listener);
mSourceToStreamerMap.put(source, streamer);
return source;
}
@@ -83,6 +86,8 @@ class TunerTsStreamerManager {
if (!creator.isCancelledLocked()) {
mStreamerFinder.putLocked(channel, sessionId, streamer);
TsDataSource source = streamer.createDataSource();
+ mListeners.put(sessionId, listener);
+ streamer.registerListener(listener);
mSourceToStreamerMap.put(source, streamer);
return source;
}
@@ -104,6 +109,8 @@ class TunerTsStreamerManager {
if (streamer == null) {
return;
}
+ EventDetector.EventListener listener = mListeners.remove(sessionId);
+ streamer.unregisterListener(listener);
TunerChannel channel = streamer.getChannel();
SoftPreconditions.checkState(channel != null);
mStreamerFinder.removeSessionLocked(channel, sessionId);
@@ -125,6 +132,13 @@ class TunerTsStreamerManager {
}
}
+ /**
+ * Add tuner hal into TunerHalManager for test.
+ */
+ void addTunerHal(TunerHal tunerHal, int sessionId) {
+ mTunerHalManager.addTunerHal(tunerHal, sessionId);
+ }
+
synchronized void release(int sessionId) {
mTunerHalManager.releaseCachedHal(sessionId);
}
@@ -261,16 +275,16 @@ class TunerTsStreamerManager {
}
private void releaseTunerHal(TunerHal hal, int sessionId, boolean reuse) {
- if (!reuse) {
+ if (!reuse || !hal.isReusable()) {
AutoCloseableUtils.closeQuietly(hal);
return;
}
TunerHal cachedHal = mTunerHals.get(sessionId);
if (cachedHal != hal) {
mTunerHals.put(sessionId, hal);
- }
- if (cachedHal != null && cachedHal != hal) {
- AutoCloseableUtils.closeQuietly(cachedHal);
+ if (cachedHal != null) {
+ AutoCloseableUtils.closeQuietly(cachedHal);
+ }
}
}
@@ -283,5 +297,9 @@ class TunerTsStreamerManager {
AutoCloseableUtils.closeQuietly(hal);
}
}
+
+ private void addTunerHal(TunerHal tunerHal, int sessionId) {
+ mTunerHals.put(sessionId, tunerHal);
+ }
}
} \ No newline at end of file
diff --git a/src/com/android/tv/tuner/ts/SectionParser.java b/src/com/android/tv/tuner/ts/SectionParser.java
index 8c1f6a1b..fe972cd1 100644
--- a/src/com/android/tv/tuner/ts/SectionParser.java
+++ b/src/com/android/tv/tuner/ts/SectionParser.java
@@ -22,7 +22,7 @@ import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
-import com.android.tv.tuner.data.nano.Channel;
+import com.android.tv.tuner.data.Channel;
import com.android.tv.tuner.data.PsiData.PatItem;
import com.android.tv.tuner.data.PsiData.PmtItem;
import com.android.tv.tuner.data.PsipData.Ac3AudioDescriptor;
@@ -39,8 +39,8 @@ import com.android.tv.tuner.data.PsipData.RatingRegion;
import com.android.tv.tuner.data.PsipData.RegionalRating;
import com.android.tv.tuner.data.PsipData.TsDescriptor;
import com.android.tv.tuner.data.PsipData.VctItem;
-import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import com.android.tv.tuner.util.ByteArrayBuffer;
import com.ibm.icu.text.UnicodeDecompressor;
@@ -367,6 +367,10 @@ public class SectionParser {
mParsedEttItems.clear();
}
+ public void resetVersionNumbers() {
+ mSectionVersionMap.clear();
+ }
+
private void parseSection(byte[] data) {
if (!checkSanity(data)) {
Log.d(TAG, "Bad CRC!");
@@ -510,10 +514,8 @@ public class SectionParser {
pos += 11 + descriptorsLength;
results.add(new MgtItem(tableType, tableTypePid));
}
- if ((data[pos] & 0xf0) != 0xf0) {
- Log.e(TAG, "Broken MGT.");
- return false;
- }
+ // Skip the remaining descriptor part which we don't use.
+
if (mListener != null) {
mListener.onMgtParsed(results);
}
@@ -717,6 +719,9 @@ public class SectionParser {
if (audioDescriptor.getLanguage() != null) {
audioTrack.language = audioDescriptor.getLanguage();
}
+ if (audioTrack.language == null) {
+ audioTrack.language = "";
+ }
audioTrack.audioType = AtscAudioTrack.AUDIOTYPE_UNDEFINED;
audioTrack.channelCount = audioDescriptor.getNumChannels();
audioTrack.sampleRate = audioDescriptor.getSampleRate();
@@ -948,6 +953,7 @@ public class SectionParser {
pos += 3;
boolean ccType = (data[pos] & 0x80) != 0;
if (!ccType) {
+ pos +=3;
continue;
}
int captionServiceNumber = data[pos] & 0x3f;
diff --git a/src/com/android/tv/tuner/ts/TsParser.java b/src/com/android/tv/tuner/ts/TsParser.java
index c24c2a21..21b5a942 100644
--- a/src/com/android/tv/tuner/ts/TsParser.java
+++ b/src/com/android/tv/tuner/ts/TsParser.java
@@ -102,6 +102,7 @@ public class TsParser {
}
protected abstract void handleData(byte[] data, boolean startIndicator);
+ protected abstract void resetDataVersions();
}
private class SectionStream extends Stream {
@@ -138,6 +139,11 @@ public class TsParser {
mSectionParser.parseSections(mPacket);
}
+ @Override
+ protected void resetDataVersions() {
+ mSectionParser.resetVersionNumbers();
+ }
+
private final OutputListener mSectionListener = new OutputListener() {
@Override
public void onPatParsed(List<PatItem> items) {
@@ -451,4 +457,16 @@ public class TsParser {
}
return incompleteChannels;
}
+
+ /**
+ * Reset the versions so that data with old version number can be handled.
+ */
+ public void resetDataVersions() {
+ for (int eitPid : mEITPids) {
+ Stream stream = mStreamMap.get(eitPid);
+ if (stream != null) {
+ stream.resetDataVersions();
+ }
+ }
+ }
}
diff --git a/src/com/android/tv/tuner/tvinput/ChannelDataManager.java b/src/com/android/tv/tuner/tvinput/ChannelDataManager.java
index a16bc522..885cef9f 100644
--- a/src/com/android/tv/tuner/tvinput/ChannelDataManager.java
+++ b/src/com/android/tv/tuner/tvinput/ChannelDataManager.java
@@ -30,6 +30,7 @@ import android.os.HandlerThread;
import android.os.Message;
import android.os.RemoteException;
import android.support.annotation.Nullable;
+import android.support.v4.os.BuildCompat;
import android.text.format.DateUtils;
import android.util.Log;
@@ -37,6 +38,7 @@ import com.android.tv.tuner.TunerPreferences;
import com.android.tv.tuner.data.PsipData.EitItem;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.util.ConvertUtils;
+import com.android.tv.util.PermissionUtils;
import java.util.ArrayList;
import java.util.Collections;
@@ -192,11 +194,14 @@ public class ChannelDataManager implements Handler.Callback {
public void release() {
mHandler.removeCallbacksAndMessages(null);
- mHandlerThread.quitSafely();
+ releaseSafely();
}
public void releaseSafely() {
mHandlerThread.quitSafely();
+ mListener = null;
+ mChannelScanListener = null;
+ mChannelScanHandler = null;
}
public TunerChannel getChannel(long channelId) {
@@ -435,7 +440,7 @@ public class ChannelDataManager implements Handler.Callback {
}
}
ops.add(buildContentProviderOperation(ContentProviderOperation.newInsert(
- TvContract.Programs.CONTENT_URI), newItem, channel.getChannelId()));
+ TvContract.Programs.CONTENT_URI), newItem, channel));
if (ops.size() >= BATCH_OPERATION_COUNT) {
applyBatch(channel.getName(), ops);
ops.clear();
@@ -505,7 +510,7 @@ public class ChannelDataManager implements Handler.Callback {
continue;
}
ops.add(buildContentProviderOperation(ContentProviderOperation.newInsert(
- TvContract.Programs.CONTENT_URI), item, channel.getChannelId()));
+ TvContract.Programs.CONTENT_URI), item, channel));
if (ops.size() >= BATCH_OPERATION_COUNT) {
applyBatch(channel.getName(), ops);
ops.clear();
@@ -516,9 +521,13 @@ public class ChannelDataManager implements Handler.Callback {
}
private ContentProviderOperation buildContentProviderOperation(
- ContentProviderOperation.Builder builder, EitItem item, Long channelId) {
- if (channelId != null) {
- builder.withValue(TvContract.Programs.COLUMN_CHANNEL_ID, channelId);
+ ContentProviderOperation.Builder builder, EitItem item, TunerChannel channel) {
+ if (channel != null) {
+ builder.withValue(TvContract.Programs.COLUMN_CHANNEL_ID, channel.getChannelId());
+ if (BuildCompat.isAtLeastN()) {
+ builder.withValue(TvContract.Programs.COLUMN_RECORDING_PROHIBITED,
+ channel.isRecordingProhibited() ? 1 : 0);
+ }
}
if (item != null) {
builder.withValue(TvContract.Programs.COLUMN_TITLE, item.getTitleText())
@@ -556,7 +565,10 @@ public class ChannelDataManager implements Handler.Callback {
values.put(TvContract.Channels.COLUMN_DISPLAY_NAME, channel.getName());
values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, channel.toByteArray());
values.put(TvContract.Channels.COLUMN_DESCRIPTION, channel.getDescription());
+ values.put(TvContract.Channels.COLUMN_VIDEO_FORMAT, channel.getVideoFormat());
values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1, VERSION);
+ values.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG2,
+ channel.isRecordingProhibited() ? 1 : 0);
if (channelId <= 0) {
values.put(TvContract.Channels.COLUMN_INPUT_ID, mInputId);
@@ -598,13 +610,29 @@ public class ChannelDataManager implements Handler.Callback {
}
private void checkVersion() {
- String selection = TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + "<>?";
- try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri,
- CHANNEL_DATA_SELECTION_ARGS, selection,
- new String[] {Integer.toString(VERSION)}, null)) {
- if (cursor != null && cursor.moveToFirst()) {
- // The stored channel data seem outdated. Delete them all.
- clearChannels();
+ if (PermissionUtils.hasAccessAllEpg(mContext)) {
+ String selection = TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 + "<>?";
+ try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri,
+ CHANNEL_DATA_SELECTION_ARGS, selection,
+ new String[] {Integer.toString(VERSION)}, null)) {
+ if (cursor != null && cursor.moveToFirst()) {
+ // The stored channel data seem outdated. Delete them all.
+ clearChannels();
+ }
+ }
+ } else {
+ try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri,
+ new String[] { TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1 },
+ null, null, null)) {
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ int version = cursor.getInt(0);
+ if (version != VERSION) {
+ clearChannels();
+ break;
+ }
+ }
+ }
}
}
}
diff --git a/src/com/android/tv/tuner/tvinput/EventDetector.java b/src/com/android/tv/tuner/tvinput/EventDetector.java
index a132398f..96b20a4b 100644
--- a/src/com/android/tv/tuner/tvinput/EventDetector.java
+++ b/src/com/android/tv/tuner/tvinput/EventDetector.java
@@ -21,8 +21,8 @@ import android.util.SparseArray;
import android.util.SparseBooleanArray;
import com.android.tv.tuner.TunerHal;
-import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.ts.TsParser;
import com.android.tv.tuner.data.PsiData;
@@ -51,7 +51,7 @@ public class EventDetector {
private final SparseArray<TunerChannel> mChannelMap = new SparseArray<>();
private final SparseBooleanArray mVctCaptionTracksFound = new SparseBooleanArray();
private final SparseBooleanArray mEitCaptionTracksFound = new SparseBooleanArray();
- private final EventListener mEventListener;
+ private final List<EventListener> mEventListeners = new ArrayList<>();
private int mFrequency;
private String mModulation;
private int mProgramNumber = ALL_PROGRAM_NUMBERS;
@@ -105,8 +105,10 @@ public class EventDetector {
item.setHasCaptionTrack();
}
}
- if (tunerChannel != null && mEventListener != null) {
- mEventListener.onEventDetected(tunerChannel, items);
+ if (tunerChannel != null && !mEventListeners.isEmpty()) {
+ for (EventListener eventListener : mEventListeners) {
+ eventListener.onEventDetected(tunerChannel, items);
+ }
}
}
@@ -117,8 +119,10 @@ public class EventDetector {
@Override
public void onAllVctItemsParsed() {
- if (mEventListener != null) {
- mEventListener.onChannelScanDone();
+ if (!mEventListeners.isEmpty()) {
+ for (EventListener eventListener : mEventListeners) {
+ eventListener.onChannelScanDone();
+ }
}
}
@@ -161,8 +165,10 @@ public class EventDetector {
if (!found) {
mVctProgramNumberSet.add(channelProgramNumber);
}
- if (mEventListener != null) {
- mEventListener.onChannelDetected(tunerChannel, !found);
+ if (!mEventListeners.isEmpty()) {
+ for (EventListener eventListener : mEventListeners) {
+ eventListener.onChannelDetected(tunerChannel, !found);
+ }
}
}
};
@@ -197,11 +203,9 @@ public class EventDetector {
/**
* Creates a detector for ATSC TV channles and program information.
* @param usbTunerInteface {@link TunerHal}
- * @param listener for ATSC TV channels and program information
*/
- public EventDetector(TunerHal usbTunerInteface, EventListener listener) {
+ public EventDetector(TunerHal usbTunerInteface) {
mTunerHal = usbTunerInteface;
- mEventListener = listener;
}
private void reset() {
@@ -258,4 +262,28 @@ public class EventDetector {
public List<TunerChannel> getMalFormedChannels() {
return mTsParser.getMalFormedChannels();
}
+
+ /**
+ * Registers an EventListener.
+ * @param eventListener the listener to be registered
+ */
+ public void registerListener(EventListener eventListener) {
+ if (mTsParser != null) {
+ // Resets the version numbers so that the new listener can receive the EIT items.
+ // Otherwise, each EIT session is handled only once unless there is a new version.
+ mTsParser.resetDataVersions();
+ }
+ mEventListeners.add(eventListener);
+ }
+
+ /**
+ * Unregisters an EventListener.
+ * @param eventListener the listener to be unregistered
+ */
+ public void unregisterListener(EventListener eventListener) {
+ boolean removed = mEventListeners.remove(eventListener);
+ if (!removed && DEBUG) {
+ Log.d(TAG, "Cannot unregister a non-registered listener!");
+ }
+ }
}
diff --git a/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java b/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java
index 61de24f4..46ff4ea1 100644
--- a/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java
+++ b/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java
@@ -24,8 +24,8 @@ import com.android.tv.tuner.data.PsiData.PatItem;
import com.android.tv.tuner.data.PsiData.PmtItem;
import com.android.tv.tuner.data.PsipData.EitItem;
import com.android.tv.tuner.data.PsipData.VctItem;
-import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.source.FileTsStreamer;
import com.android.tv.tuner.ts.TsParser;
diff --git a/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java b/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
index 6ec55e4f..0be29f25 100644
--- a/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
+++ b/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java
@@ -33,13 +33,17 @@ import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import android.util.Log;
+import android.util.Pair;
+import com.google.android.exoplayer.C;
import com.android.tv.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.recording.RecordingCapability;
import com.android.tv.dvr.DvrStorageStatusManager;
-import com.android.tv.dvr.RecordedProgram;
+import com.android.tv.dvr.data.RecordedProgram;
import com.android.tv.tuner.DvbDeviceAccessor;
import com.android.tv.tuner.data.PsipData;
+import com.android.tv.tuner.data.PsipData.EitItem;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.exoplayer.ExoPlayerSampleExtractor;
import com.android.tv.tuner.exoplayer.SampleExtractor;
@@ -53,10 +57,10 @@ import java.io.File;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Random;
-import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
@@ -71,6 +75,7 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
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 long TUNING_RETRY_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4);
private static final long STORAGE_MONITOR_INTERVAL_MS = TimeUnit.SECONDS.toMillis(4);
private static final long MIN_PARTIAL_RECORDING_DURATION_MS = TimeUnit.SECONDS.toMillis(10);
private static final long PREPARE_RECORDER_POLL_MS = 50;
@@ -80,20 +85,23 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
private static final int MSG_STOP_RECORDING = 4;
private static final int MSG_MONITOR_STORAGE_STATUS = 5;
private static final int MSG_RELEASE = 6;
+ private static final int MSG_UPDATE_CC_INFO = 7;
private final RecordingCapability mCapabilities;
public RecordingCapability getCapabilities() {
return mCapabilities;
}
- @IntDef({STATE_IDLE, STATE_TUNED, STATE_RECORDING})
+ @IntDef({STATE_IDLE, STATE_TUNING, STATE_TUNED, STATE_RECORDING})
@Retention(RetentionPolicy.SOURCE)
public @interface DvrSessionState {}
private static final int STATE_IDLE = 1;
- private static final int STATE_TUNED = 2;
- private static final int STATE_RECORDING = 3;
+ private static final int STATE_TUNING = 2;
+ private static final int STATE_TUNED = 3;
+ private static final int STATE_RECORDING = 4;
private static final long CHANNEL_ID_NONE = -1;
+ private static final int MAX_TUNING_RETRY = 6;
private final Context mContext;
private final ChannelDataManager mChannelDataManager;
@@ -108,13 +116,16 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
private long mRecordStartTime;
private long mRecordEndTime;
private boolean mRecorderRunning;
- private BufferManager mBufferManager;
private SampleExtractor mRecorder;
private final TunerRecordingSession mSession;
@DvrSessionState private int mSessionState = STATE_IDLE;
private final String mInputId;
private Uri mProgramUri;
+ private PsipData.EitItem mCurrenProgram;
+ private List<AtscCaptionTrack> mCaptionTracks;
+ private DvrStorageManager mDvrStorageManager;
+
public TunerRecordingSessionWorker(Context context, String inputId,
ChannelDataManager dataManager, TunerRecordingSession session) {
mRandom.setSeed(System.nanoTime());
@@ -157,6 +168,7 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
if (mChannel == null || mChannel.compareTo(channel) != 0) {
return;
}
+ mHandler.obtainMessage(MSG_UPDATE_CC_INFO, new Pair<>(channel, items)).sendToTarget();
mChannelDataManager.notifyEventDetected(channel, items);
}
@@ -178,7 +190,7 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
@MainThread
public void tune(Uri channelUri) {
mHandler.removeCallbacksAndMessages(null);
- mHandler.obtainMessage(MSG_TUNE, channelUri).sendToTarget();
+ mHandler.obtainMessage(MSG_TUNE, 0, 0, channelUri).sendToTarget();
}
/**
@@ -211,11 +223,22 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
switch (msg.what) {
case MSG_TUNE: {
Uri channelUri = (Uri) msg.obj;
+ int retryCount = msg.arg1;
if (DEBUG) Log.d(TAG, "Tune to " + channelUri);
if (doTune(channelUri)) {
- mSession.onTuned(channelUri);
- } else {
- reset();
+ if (mSessionState == STATE_TUNED) {
+ mSession.onTuned(channelUri);
+ } else {
+ Log.w(TAG, "Tuner stream cannot be created due to resource shortage.");
+ if (retryCount < MAX_TUNING_RETRY) {
+ Message tuneMsg =
+ mHandler.obtainMessage(MSG_TUNE, retryCount + 1, 0, channelUri);
+ mHandler.sendMessageDelayed(tuneMsg, TUNING_RETRY_INTERVAL_MS);
+ } else {
+ mSession.onError(TvInputManager.RECORDING_ERROR_RESOURCE_BUSY);
+ reset();
+ }
+ }
}
return true;
}
@@ -281,6 +304,12 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
mHandler.getLooper().quitSafely();
return true;
}
+ case MSG_UPDATE_CC_INFO: {
+ Pair<TunerChannel, List<EitItem>> pair =
+ (Pair<TunerChannel, List<EitItem>>) msg.obj;
+ updateCaptionTracks(pair.first, pair.second);
+ return true;
+ }
}
return false;
}
@@ -310,20 +339,17 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
mRecorder.release();
mRecorder = null;
}
- if (mBufferManager != null) {
- mBufferManager.close();
- mBufferManager = null;
- }
if (mTunerSource != null) {
mSourceManager.releaseDataSource(mTunerSource);
mTunerSource = null;
}
+ mDvrStorageManager = null;
mSessionState = STATE_IDLE;
mRecorderRunning = false;
}
private boolean doTune(Uri channelUri) {
- if (mSessionState != STATE_IDLE) {
+ if (mSessionState != STATE_IDLE && mSessionState != STATE_TUNING) {
mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
Log.e(TAG, "Tuning was requested from wrong status.");
return false;
@@ -333,6 +359,10 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
Log.w(TAG, "Failed to start recording. Couldn't find the channel for " + mChannel);
return false;
+ } else if (mChannel.isRecordingProhibited()) {
+ mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
+ Log.w(TAG, "Failed to start recording. Not a recordable channel: " + mChannel);
+ return false;
}
if (!mDvrStorageStatusManager.isStorageSufficient()) {
mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
@@ -341,9 +371,9 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
}
mTunerSource = mSourceManager.createDataSource(mContext, mChannel, this);
if (mTunerSource == null) {
- mSession.onError(TvInputManager.RECORDING_ERROR_RESOURCE_BUSY);
- Log.w(TAG, "Tuner stream cannot be created due to resource shortage.");
- return false;
+ // Retry tuning in this case.
+ mSessionState = STATE_TUNING;
+ return true;
}
mSessionState = STATE_TUNED;
return true;
@@ -365,10 +395,10 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
}
// Since tuning might be happened a while ago, shifts the start position of tuned source.
mTunerSource.shiftStartPosition(mTunerSource.getBufferedPosition());
- mBufferManager = new BufferManager(new DvrStorageManager(mStorageDir, true));
mRecordStartTime = System.currentTimeMillis();
- mRecorder = new ExoPlayerSampleExtractor(Uri.EMPTY, mTunerSource, mBufferManager, this,
- true);
+ mDvrStorageManager = new DvrStorageManager(mStorageDir, true);
+ mRecorder = new ExoPlayerSampleExtractor(Uri.EMPTY, mTunerSource,
+ new BufferManager(mDvrStorageManager), this, true);
mRecorder.setOnCompletionListener(this, mHandler);
mProgramUri = programUri;
mSessionState = STATE_RECORDING;
@@ -392,6 +422,34 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
Log.i(TAG, "Recording stopped");
}
+ private void updateCaptionTracks(TunerChannel channel, List<PsipData.EitItem> items) {
+ if (mChannel == null || channel == null || mChannel.compareTo(channel) != 0
+ || items == null || items.isEmpty()) {
+ return;
+ }
+ PsipData.EitItem currentProgram = getCurrentProgram(items);
+ if (currentProgram == null || !currentProgram.hasCaptionTrack()
+ || mCurrenProgram != null && mCurrenProgram.compareTo(currentProgram) == 0) {
+ return;
+ }
+ mCurrenProgram = currentProgram;
+ mCaptionTracks = new ArrayList<>(currentProgram.getCaptionTracks());
+ if (DEBUG) {
+ Log.d(TAG, "updated " + mCaptionTracks.size() + " caption tracks for "
+ + currentProgram);
+ }
+ }
+
+ private PsipData.EitItem getCurrentProgram(List<PsipData.EitItem> items) {
+ for (PsipData.EitItem item : items) {
+ if (mRecordStartTime >= item.getStartTimeUtcMillis()
+ && mRecordStartTime < item.getEndTimeUtcMillis()) {
+ return item;
+ }
+ }
+ return null;
+ }
+
private static class Program {
private final long mChannelId;
private final String mTitle;
@@ -566,15 +624,25 @@ public class TunerRecordingSessionWorker implements PlaybackBufferListener,
return;
}
Log.i(TAG, "recording finished " + (success ? "completely" : "partially"));
- Uri uri = insertRecordedProgram(getRecordedProgram(), mChannel.getChannelId(),
- Uri.fromFile(mStorageDir).toString(), 1024 * 1024, mRecordStartTime,
- mRecordStartTime + TimeUnit.MICROSECONDS.toMillis(lastExtractedPositionUs));
+ long recordEndTime =
+ (lastExtractedPositionUs == C.UNKNOWN_TIME_US)
+ ? System.currentTimeMillis()
+ : mRecordStartTime + lastExtractedPositionUs / 1000;
+ Uri uri =
+ insertRecordedProgram(
+ getRecordedProgram(),
+ mChannel.getChannelId(),
+ Uri.fromFile(mStorageDir).toString(),
+ 1024 * 1024,
+ mRecordStartTime,
+ recordEndTime);
if (uri == null) {
new DeleteRecordingTask().execute(mStorageDir);
mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN);
Log.e(TAG, "Inserting a recording to DB failed");
return;
}
+ mDvrStorageManager.writeCaptionInfoFiles(mCaptionTracks);
mSession.onRecordFinished(uri);
}
diff --git a/src/com/android/tv/tuner/tvinput/TunerSession.java b/src/com/android/tv/tuner/tvinput/TunerSession.java
index 5c61402e..d1ee3c6f 100644
--- a/src/com/android/tv/tuner/tvinput/TunerSession.java
+++ b/src/com/android/tv/tuner/tvinput/TunerSession.java
@@ -41,7 +41,7 @@ import com.android.tv.tuner.R;
import com.android.tv.tuner.cc.CaptionLayout;
import com.android.tv.tuner.cc.CaptionTrackRenderer;
import com.android.tv.tuner.data.Cea708Data.CaptionEvent;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import com.android.tv.tuner.exoplayer.buffer.BufferManager;
import com.android.tv.tuner.data.TunerChannel;
import com.android.tv.tuner.util.GlobalSettingsUtils;
@@ -81,8 +81,7 @@ public class TunerSession extends TvInputService.Session implements Handler.Call
private boolean mPlayPaused;
private long mTuneStartTimestamp;
- public TunerSession(Context context, ChannelDataManager channelDataManager,
- BufferManager bufferManager) {
+ public TunerSession(Context context, ChannelDataManager channelDataManager) {
super(context);
mContext = context;
mUiHandler = new Handler(this);
@@ -97,12 +96,9 @@ public class TunerSession extends TvInputService.Session implements Handler.Call
mStatusView.setVisibility(showDebug ? View.VISIBLE : View.INVISIBLE);
mAudioStatusView = (TextView) mOverlayView.findViewById(R.id.audio_status);
mAudioStatusView.setVisibility(View.INVISIBLE);
- mAudioStatusView.setText(Html.fromHtml(StatusTextUtils.getAudioWarningInHTML(
- context.getString(R.string.ut_surround_sound_disabled))));
CaptionLayout captionLayout = (CaptionLayout) mOverlayView.findViewById(R.id.caption);
mCaptionTrackRenderer = new CaptionTrackRenderer(captionLayout);
- mSessionWorker = new TunerSessionWorker(context, channelDataManager,
- bufferManager, this);
+ mSessionWorker = new TunerSessionWorker(context, channelDataManager, this);
}
public boolean isReleased() {
@@ -272,10 +268,13 @@ public class TunerSession extends TvInputService.Session implements Handler.Call
// setting is "never".
final int value = GlobalSettingsUtils.getEncodedSurroundOutputSettings(mContext);
if (value == GlobalSettingsUtils.ENCODED_SURROUND_OUTPUT_NEVER) {
- mAudioStatusView.setVisibility(View.VISIBLE);
+ mAudioStatusView.setText(Html.fromHtml(StatusTextUtils.getAudioWarningInHTML(
+ mContext.getString(R.string.ut_surround_sound_disabled))));
} else {
- Log.e(TAG, "Audio is unavailable, surround sound setting is " + value);
+ mAudioStatusView.setText(Html.fromHtml(StatusTextUtils.getAudioWarningInHTML(
+ mContext.getString(R.string.audio_passthrough_not_supported))));
}
+ mAudioStatusView.setVisibility(View.VISIBLE);
return true;
}
case MSG_UI_HIDE_AUDIO_UNPLAYABLE: {
diff --git a/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java b/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java
index 5230298e..41f8ce5f 100644
--- a/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java
+++ b/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java
@@ -35,6 +35,7 @@ import android.support.annotation.AnyThread;
import android.support.annotation.MainThread;
import android.support.annotation.WorkerThread;
import android.text.Html;
+import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
@@ -47,28 +48,30 @@ import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.TvContentRatingCache;
import com.android.tv.tuner.TunerPreferences;
import com.android.tv.tuner.data.Cea708Data;
+import com.android.tv.tuner.data.Channel;
import com.android.tv.tuner.data.PsipData.EitItem;
import com.android.tv.tuner.data.PsipData.TvTracksInterface;
+import com.android.tv.tuner.data.Track.AtscAudioTrack;
+import com.android.tv.tuner.data.Track.AtscCaptionTrack;
import com.android.tv.tuner.data.TunerChannel;
-import com.android.tv.tuner.data.nano.Channel;
-import com.android.tv.tuner.data.nano.Track.AtscAudioTrack;
-import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack;
import com.android.tv.tuner.exoplayer.MpegTsRendererBuilder;
import com.android.tv.tuner.exoplayer.buffer.BufferManager;
+import com.android.tv.tuner.exoplayer.buffer.BufferManager.StorageManager;
import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager;
import com.android.tv.tuner.exoplayer.MpegTsPlayer;
+import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager;
import com.android.tv.tuner.source.TsDataSource;
import com.android.tv.tuner.source.TsDataSourceManager;
import com.android.tv.tuner.util.StatusTextUtils;
+import com.android.tv.tuner.util.SystemPropertiesProxy;
import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
-import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
/**
* {@link TunerSessionWorker} implements a handler thread which processes TV input jobs
@@ -82,6 +85,9 @@ public class TunerSessionWorker implements PlaybackBufferListener,
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 MAX_BUFFER_SIZE_KEY = "tv.tuner.buffersize_mbytes";
+ private static final int MAX_BUFFER_SIZE_DEF = 2 * 1024; // 2GB
+ private static final int MIN_BUFFER_SIZE_DEF = 256; // 256MB
// Public messages
public static final int MSG_SELECT_TRACK = 1;
@@ -147,10 +153,18 @@ public class TunerSessionWorker implements PlaybackBufferListener,
private static final int EXPECTED_KEY_FRAME_INTERVAL_MS = 500;
private static final int MIN_TRICKPLAY_SEEK_INTERVAL_MS = 20;
private static final int TRICKPLAY_MONITOR_INTERVAL_MS = 250;
+ private static final int RELEASE_WAIT_INTERVAL_MS = 50;
+
+ // Since release() is done asynchronously, synchronization between multiple TunerSessionWorker
+ // creation/release is required.
+ // This is used to guarantee that at most one active TunerSessionWorker exists at any give time.
+ private static Semaphore sActiveSessionSemaphore = new Semaphore(1);
private final Context mContext;
private final ChannelDataManager mChannelDataManager;
private final TsDataSourceManager mSourceManager;
+ private final int mMaxTrickplayBufferSizeMb;
+ private final File mTrickplayBufferDir;
private volatile Surface mSurface;
private volatile float mVolume = 1.0f;
private volatile boolean mCaptionEnabled;
@@ -159,6 +173,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
private volatile Long mRecordingDuration;
private volatile long mRecordStartTimeMs;
private volatile long mBufferStartTimeMs;
+ private volatile boolean mTrickplayDisabled;
private String mRecordingId;
private final Handler mHandler;
private int mRetryCount;
@@ -177,19 +192,19 @@ public class TunerSessionWorker implements PlaybackBufferListener,
private TvContentRating mUnblockedContentRating;
private long mLastPositionMs;
private AudioCapabilities mAudioCapabilities;
- private final CountDownLatch mReleaseLatch = new CountDownLatch(1);
private long mLastLimitInBytes;
- private long mLastPositionInBytes;
- private final BufferManager mBufferManager;
private final TvContentRatingCache mTvContentRatingCache = TvContentRatingCache.getInstance();
private final TunerSession mSession;
private int mPlayerState = ExoPlayer.STATE_IDLE;
private long mPreparingStartTimeMs;
private long mBufferingStartTimeMs;
private long mReadyStartTimeMs;
+ private boolean mIsActiveSession;
+ private boolean mReleaseRequested; // Guarded by mReleaseLock
+ private final Object mReleaseLock = new Object();
public TunerSessionWorker(Context context, ChannelDataManager channelDataManager,
- BufferManager bufferManager, TunerSession tunerSession) {
+ TunerSession tunerSession) {
if (DEBUG) Log.d(TAG, "TunerSessionWorker created");
mContext = context;
@@ -211,7 +226,10 @@ public class TunerSessionWorker implements PlaybackBufferListener,
(CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
mCaptionEnabled = captioningManager.isEnabled();
mPlaybackParams.setSpeed(1.0f);
- mBufferManager = bufferManager;
+ mMaxTrickplayBufferSizeMb =
+ SystemPropertiesProxy.getInt(MAX_BUFFER_SIZE_KEY, MAX_BUFFER_SIZE_DEF);
+ mTrickplayBufferDir = context.getCacheDir();
+ mTrickplayDisabled = mTrickplayBufferDir == null;
mPreparingStartTimeMs = INVALID_TIME;
mBufferingStartTimeMs = INVALID_TIME;
mReadyStartTimeMs = INVALID_TIME;
@@ -285,24 +303,21 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
private Long getDurationForRecording(String recordingId) {
- try {
- DvrStorageManager storageManager =
+ DvrStorageManager storageManager =
new DvrStorageManager(new File(getRecordingPath()), false);
- Pair<String, MediaFormat> trackInfo = null;
- try {
- trackInfo = storageManager.readTrackInfoFile(false);
- } catch (FileNotFoundException e) {
- }
- if (trackInfo == null) {
- trackInfo = storageManager.readTrackInfoFile(true);
- }
- Long durationUs = trackInfo.second.getLong(MediaFormat.KEY_DURATION);
+ List<BufferManager.TrackFormat> trackFormatList =
+ storageManager.readTrackInfoFiles(false);
+ if (trackFormatList.isEmpty()) {
+ trackFormatList = storageManager.readTrackInfoFiles(true);
+ }
+ if (!trackFormatList.isEmpty()) {
+ BufferManager.TrackFormat trackFormat = trackFormatList.get(0);
+ Long durationUs = trackFormat.format.getLong(MediaFormat.KEY_DURATION);
// we need duration by milli for trickplay notification.
return durationUs != null ? durationUs / 1000 : null;
- } catch (IOException e) {
- Log.e(TAG, "meta file for recording was not found: " + recordingId);
- return null;
}
+ Log.e(TAG, "meta file for recording was not found: " + recordingId);
+ return null;
}
@MainThread
@@ -341,16 +356,12 @@ public class TunerSessionWorker implements PlaybackBufferListener,
@MainThread
public void release() {
if (DEBUG) Log.d(TAG, "release()");
+ synchronized (mReleaseLock) {
+ mReleaseRequested = true;
+ }
mChannelDataManager.setListener(null);
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();
- }
}
// MpegTsPlayer.Listener
@@ -367,7 +378,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
if (playbackState == ExoPlayer.STATE_READY) {
if (DEBUG) Log.d(TAG, "ExoPlayer ready");
if (!mPlayerStarted) {
- sendMessage(MSG_START_PLAYBACK, mPlayer);
+ sendMessage(MSG_START_PLAYBACK, System.identityHashCode(mPlayer));
}
mReadyStartTimeMs = SystemClock.elapsedRealtime();
} else if (playbackState == ExoPlayer.STATE_PREPARING) {
@@ -379,7 +390,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
// notification of STATE_ENDED from MpegTsPlayer will be ignored afterwards.
Log.i(TAG, "Player ended: end of stream");
if (mChannel != null) {
- sendMessage(MSG_RETRY_PLAYBACK, mPlayer);
+ sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer));
}
}
mPlayerState = playbackState;
@@ -397,7 +408,8 @@ public class TunerSessionWorker implements PlaybackBufferListener,
// If we are playing live stream, retrying playback maybe helpful. But for recorded stream,
// retrying playback is not helpful.
if (mChannel != null) {
- mHandler.obtainMessage(MSG_RETRY_PLAYBACK, mPlayer).sendToTarget();
+ mHandler.obtainMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer))
+ .sendToTarget();
}
}
@@ -415,8 +427,12 @@ public class TunerSessionWorker implements PlaybackBufferListener,
public void onDrawnToSurface(MpegTsPlayer player, Surface surface) {
if (mSurface != null && mPlayerStarted) {
if (DEBUG) Log.d(TAG, "MSG_DRAWN_TO_SURFACE");
- mBufferStartTimeMs = mRecordStartTimeMs =
- (mRecordingId != null) ? 0 : System.currentTimeMillis();
+ if (mRecordingId != null) {
+ // Workaround of b/33298048: set it to 1 instead of 0.
+ mBufferStartTimeMs = mRecordStartTimeMs = 1;
+ } else {
+ mBufferStartTimeMs = mRecordStartTimeMs = System.currentTimeMillis();
+ }
notifyVideoAvailable();
mReportedDrawnToSurface = true;
@@ -499,7 +515,8 @@ public class TunerSessionWorker implements PlaybackBufferListener,
@Override
public void onDiskTooSlow() {
- sendMessage(MSG_RETRY_PLAYBACK, mPlayer);
+ mTrickplayDisabled = true;
+ sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer));
}
// EventDetector.EventListener
@@ -602,6 +619,28 @@ public class TunerSessionWorker implements PlaybackBufferListener,
return true;
}
notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING);
+ if (!mIsActiveSession) {
+ // Wait until release is finished if there is a pending release.
+ try {
+ while (!sActiveSessionSemaphore.tryAcquire(
+ RELEASE_WAIT_INTERVAL_MS, TimeUnit.MILLISECONDS)) {
+ synchronized (mReleaseLock) {
+ if (mReleaseRequested) {
+ return true;
+ }
+ }
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ synchronized (mReleaseLock) {
+ if (mReleaseRequested) {
+ sActiveSessionSemaphore.release();
+ return true;
+ }
+ }
+ mIsActiveSession = true;
+ }
Uri channelUri = (Uri) msg.obj;
String recording = null;
long channelId = parseChannel(channelUri);
@@ -616,7 +655,8 @@ public class TunerSessionWorker implements PlaybackBufferListener,
notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
return true;
}
- mHandler.removeCallbacksAndMessages(null);
+ clearCallbacksAndMessagesSafely();
+ mChannelDataManager.removeAllCallbacksAndMessages();
if (channel != null) {
mChannelDataManager.requestProgramsData(channel);
}
@@ -624,8 +664,8 @@ public class TunerSessionWorker implements PlaybackBufferListener,
// TODO: Need to refactor. notifyContentAllowed() should not be called if parental
// control is turned on.
mSession.notifyContentAllowed();
- resetPlayback();
resetTvTracks();
+ resetPlayback();
mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS,
RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS);
return true;
@@ -633,7 +673,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
case MSG_STOP_TUNE: {
if (DEBUG) Log.d(TAG, "MSG_STOP_TUNE");
mChannel = null;
- stopPlayback();
+ stopPlayback(true);
stopCaptionTrack();
resetTvTracks();
notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN);
@@ -642,14 +682,17 @@ public class TunerSessionWorker implements PlaybackBufferListener,
case MSG_RELEASE: {
if (DEBUG) Log.d(TAG, "MSG_RELEASE");
mHandler.removeCallbacksAndMessages(null);
- stopPlayback();
+ stopPlayback(true);
stopCaptionTrack();
mSourceManager.release();
- mReleaseLatch.countDown();
+ mHandler.getLooper().quitSafely();
+ if (mIsActiveSession) {
+ sActiveSessionSemaphore.release();
+ }
return true;
}
case MSG_RETRY_PLAYBACK: {
- if (mPlayer == msg.obj) {
+ if (System.identityHashCode(mPlayer) == (int) msg.obj) {
Log.i(TAG, "Retrying the playback for channel: " + mChannel);
mHandler.removeMessages(MSG_RETRY_PLAYBACK);
// When there is a request of retrying playback, don't reuse TunerHal.
@@ -658,13 +701,14 @@ public class TunerSessionWorker implements PlaybackBufferListener,
if (DEBUG) {
Log.d(TAG, "MSG_RETRY_PLAYBACK " + mRetryCount);
}
+ mChannelDataManager.removeAllCallbacksAndMessages();
if (mRetryCount <= MAX_IMMEDIATE_RETRY_COUNT) {
resetPlayback();
} else {
// When it reaches this point, it may be due to an error that occurred in
// the tuner device. Calling stopPlayback() resets the tuner device
// to recover from the error.
- stopPlayback();
+ stopPlayback(false);
stopCaptionTrack();
notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
@@ -679,13 +723,14 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
case MSG_RESET_PLAYBACK: {
if (DEBUG) Log.d(TAG, "MSG_RESET_PLAYBACK");
+ mChannelDataManager.removeAllCallbacksAndMessages();
resetPlayback();
return true;
}
case MSG_START_PLAYBACK: {
if (DEBUG) Log.d(TAG, "MSG_START_PLAYBACK");
if (mChannel != null || mRecordingId != null) {
- startPlayback(msg.obj);
+ startPlayback((int) msg.obj);
}
return true;
}
@@ -790,7 +835,11 @@ public class TunerSessionWorker implements PlaybackBufferListener,
return true;
}
case MSG_RESCHEDULE_PROGRAMS: {
- doReschedulePrograms();
+ if (mHandler.hasMessages(MSG_SCHEDULE_OF_PROGRAMS)) {
+ mHandler.sendEmptyMessage(MSG_RESCHEDULE_PROGRAMS);
+ } else {
+ doReschedulePrograms();
+ }
return true;
}
case MSG_PARENTAL_CONTROLS: {
@@ -814,11 +863,8 @@ public class TunerSessionWorker implements PlaybackBufferListener,
return true;
}
case MSG_SELECT_TRACK: {
- if (mChannel != null) {
+ if (mChannel != null || mRecordingId != 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;
}
@@ -909,7 +955,6 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
TsDataSource source = mPlayer.getDataSource();
long limitInBytes = source != null ? source.getBufferedPosition() : 0L;
- long positionInBytes = source != null ? source.getLastReadPosition() : 0L;
if (TunerDebug.ENABLED) {
TunerDebug.calculateDiff();
mSession.sendUiMessage(TunerSession.MSG_UI_SET_STATUS_TEXT,
@@ -927,14 +972,8 @@ public class TunerSessionWorker implements PlaybackBufferListener,
TunerDebug.getVideoPtsUsRate()
)));
}
- if (DEBUG) {
- Log.d(TAG, String.format("MSG_CHECK_SIGNAL position: %d, limit: %d",
- positionInBytes, limitInBytes));
- }
mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE);
long currentTime = SystemClock.elapsedRealtime();
- boolean noBufferRead = positionInBytes == mLastPositionInBytes
- && limitInBytes == mLastLimitInBytes;
boolean isBufferingTooLong = mBufferingStartTimeMs != INVALID_TIME
&& currentTime - mBufferingStartTimeMs
> PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
@@ -943,11 +982,11 @@ public class TunerSessionWorker implements PlaybackBufferListener,
> PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS;
boolean isWeakSignal = source != null
&& mChannel.getType() == Channel.TYPE_TUNER
- && (noBufferRead || isBufferingTooLong || isPreparingTooLong);
+ && (isBufferingTooLong || isPreparingTooLong);
if (isWeakSignal && !mReportedWeakSignal) {
if (!mHandler.hasMessages(MSG_RETRY_PLAYBACK)) {
- mHandler.sendMessageDelayed(mHandler.obtainMessage(
- MSG_RETRY_PLAYBACK, mPlayer), PLAYBACK_RETRY_DELAY_MS);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY_PLAYBACK,
+ System.identityHashCode(mPlayer)), PLAYBACK_RETRY_DELAY_MS);
}
if (mPlayer != null) {
mPlayer.setAudioTrack(false);
@@ -966,7 +1005,6 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
}
mLastLimitInBytes = limitInBytes;
- mLastPositionInBytes = positionInBytes;
mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_PERIOD_MS);
return true;
}
@@ -999,15 +1037,8 @@ public class TunerSessionWorker implements PlaybackBufferListener,
if (trackId == null) {
return;
}
- AtscAudioTrack audioTrack = mAudioTrackMap.get(numTrackId);
- if (audioTrack == null) {
- return;
- }
- int oldAudioPid = mChannel.getAudioPid();
- mChannel.selectAudioTrack(audioTrack.index);
- int newAudioPid = mChannel.getAudioPid();
- if (oldAudioPid != newAudioPid) {
- mPlayer.setSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO, audioTrack.index);
+ if (numTrackId != mPlayer.getSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO)) {
+ mPlayer.setSelectedTrack(MpegTsPlayer.TRACK_TYPE_AUDIO, numTrackId);
}
mSession.notifyTrackSelected(type, trackId);
} else if (type == TvTrackInfo.TYPE_SUBTITLE) {
@@ -1030,11 +1061,22 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
}
- private MpegTsPlayer createPlayer(AudioCapabilities capabilities, BufferManager bufferManager) {
+ private MpegTsPlayer createPlayer(AudioCapabilities capabilities) {
if (capabilities == null) {
Log.w(TAG, "No Audio Capabilities");
}
-
+ BufferManager bufferManager = null;
+ if (mRecordingId != null) {
+ StorageManager storageManager =
+ new DvrStorageManager(new File(getRecordingPath()), false);
+ bufferManager = new BufferManager(storageManager);
+ updateCaptionTracks(((DvrStorageManager)storageManager).readCaptionInfoFiles());
+ } else if (!mTrickplayDisabled && mMaxTrickplayBufferSizeMb >= MIN_BUFFER_SIZE_DEF) {
+ bufferManager = new BufferManager(new TrickplayStorageManager(mContext,
+ mTrickplayBufferDir, 1024L * 1024 * mMaxTrickplayBufferSizeMb));
+ } else {
+ Log.w(TAG, "Trickplay is disabled.");
+ }
MpegTsPlayer player = new MpegTsPlayer(
new MpegTsRendererBuilder(mContext, bufferManager, this),
mHandler, mSourceManager, capabilities, this);
@@ -1069,24 +1111,26 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
private void updateTvTracks(TvTracksInterface tvTracksInterface, boolean fromPmt) {
- if (DEBUG) {
- Log.d(TAG, "UpdateTvTracks " + tvTracksInterface);
- }
- List<AtscAudioTrack> audioTracks = tvTracksInterface.getAudioTracks();
- List<AtscCaptionTrack> captionTracks = tvTracksInterface.getCaptionTracks();
- // According to ATSC A/69 chapter 6.9, both PMT and EIT should have descriptors for audio
- // tracks, but in real world, we see some bogus audio track info in EIT, so, we trust audio
- // track info in PMT more and use info in EIT only when we have nothing.
- if (audioTracks != null && !audioTracks.isEmpty()
- && (mChannel.getAudioTracks() == null || fromPmt)) {
- updateAudioTracks(audioTracks);
- }
- if (captionTracks == null || captionTracks.isEmpty()) {
- if (tvTracksInterface.hasCaptionTrack()) {
+ synchronized (tvTracksInterface) {
+ if (DEBUG) {
+ Log.d(TAG, "UpdateTvTracks " + tvTracksInterface);
+ }
+ List<AtscAudioTrack> audioTracks = tvTracksInterface.getAudioTracks();
+ List<AtscCaptionTrack> captionTracks = tvTracksInterface.getCaptionTracks();
+ // According to ATSC A/69 chapter 6.9, both PMT and EIT should have descriptors for audio
+ // tracks, but in real world, we see some bogus audio track info in EIT, so, we trust audio
+ // track info in PMT more and use info in EIT only when we have nothing.
+ if (audioTracks != null && !audioTracks.isEmpty()
+ && (mChannel == null || mChannel.getAudioTracks() == null || fromPmt)) {
+ updateAudioTracks(audioTracks);
+ }
+ if (captionTracks == null || captionTracks.isEmpty()) {
+ if (tvTracksInterface.hasCaptionTrack()) {
+ updateCaptionTracks(captionTracks);
+ }
+ } else {
updateCaptionTracks(captionTracks);
}
- } else {
- updateCaptionTracks(captionTracks);
}
}
@@ -1132,25 +1176,24 @@ public class TunerSessionWorker implements PlaybackBufferListener,
int audioTrackCount = mPlayer.getTrackCount(MpegTsPlayer.TRACK_TYPE_AUDIO);
removeTvTracks(TvTrackInfo.TYPE_AUDIO);
for (int i = 0; i < audioTrackCount; i++) {
- AtscAudioTrack audioTrack = mAudioTrackMap.get(i);
- if (audioTrack == null) {
- continue;
- }
- String language = audioTrack.language;
- if (language == null && mChannel.getAudioTracks() != null
- && mChannel.getAudioTracks().size() == mAudioTrackMap.size()) {
- // If a language is not present, use a language field in PMT section parsed.
- language = mChannel.getAudioTracks().get(i).language;
- }
- // Save the index to the audio track.
- // Later, when an audio track is selected, both the audio pid and its audio stream
- // type reside in the selected index position of the tuner channel's audio data.
- audioTrack.index = i;
+ // We use language information from EIT/VCT only when the player does not provide
+ // languages.
+ com.google.android.exoplayer.MediaFormat infoFromPlayer =
+ mPlayer.getTrackFormat(MpegTsPlayer.TRACK_TYPE_AUDIO, i);
+ AtscAudioTrack infoFromEit = mAudioTrackMap.get(i);
+ AtscAudioTrack infoFromVct = (mChannel != null
+ && mChannel.getAudioTracks().size() == mAudioTrackMap.size()
+ && i < mChannel.getAudioTracks().size())
+ ? mChannel.getAudioTracks().get(i) : null;
+ String language = !TextUtils.isEmpty(infoFromPlayer.language) ? infoFromPlayer.language
+ : (infoFromEit != null && infoFromEit.language != null) ? infoFromEit.language
+ : (infoFromVct != null && infoFromVct.language != null)
+ ? infoFromVct.language : null;
TvTrackInfo.Builder builder = new TvTrackInfo.Builder(
TvTrackInfo.TYPE_AUDIO, AUDIO_TRACK_PREFIX + i);
builder.setLanguage(language);
- builder.setAudioChannelCount(audioTrack.channelCount);
- builder.setAudioSampleRate(audioTrack.sampleRate);
+ builder.setAudioChannelCount(infoFromPlayer.channelCount);
+ builder.setAudioSampleRate(infoFromPlayer.sampleRate);
TvTrackInfo track = builder.build();
mTvTracks.add(track);
}
@@ -1226,8 +1269,10 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
}
- private void stopPlayback() {
- mChannelDataManager.removeAllCallbacksAndMessages();
+ private void stopPlayback(boolean removeChannelDataCallbacks) {
+ if (removeChannelDataCallbacks) {
+ mChannelDataManager.removeAllCallbacksAndMessages();
+ }
if (mPlayer != null) {
mPlayer.setPlayWhenReady(false);
mPlayer.release();
@@ -1244,9 +1289,9 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
}
- private void startPlayback(Object playerObj) {
+ private void startPlayback(int playerHashCode) {
// TODO: provide hasAudio()/hasVideo() for play recordings.
- if (mPlayer == null || mPlayer != playerObj) {
+ if (mPlayer == null || System.identityHashCode(mPlayer) != playerHashCode) {
return;
}
if (mChannel != null && !mChannel.hasAudio()) {
@@ -1259,7 +1304,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
if (mChannel != null && ((mChannel.hasAudio() && !mPlayer.hasAudio())
|| (mChannel.hasVideo() && !mPlayer.hasVideo()))) {
// Tracks haven't been detected in the extractor. Try again.
- sendMessage(MSG_RETRY_PLAYBACK, mPlayer);
+ sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer));
return;
}
// Since mSurface is volatile, we define a local variable surface to keep the same value
@@ -1286,9 +1331,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
return;
}
mSourceManager.setKeepTuneStatus(true);
- BufferManager bufferManager = mChannel != null ? mBufferManager : new BufferManager(
- new DvrStorageManager(new File(getRecordingPath()), false));
- MpegTsPlayer player = createPlayer(mAudioCapabilities, bufferManager);
+ MpegTsPlayer player = createPlayer(mAudioCapabilities);
player.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER);
player.setVideoEventListener(this);
player.setCaptionServiceNumber(mCaptionTrack != null ?
@@ -1300,8 +1343,8 @@ public class TunerSessionWorker implements PlaybackBufferListener,
// When prepare failed, there may be some errors related to hardware. In that
// case, retry playback immediately may not help.
notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL);
- mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY_PLAYBACK, mPlayer),
- PLAYBACK_RETRY_DELAY_MS);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY_PLAYBACK,
+ System.identityHashCode(mPlayer)), PLAYBACK_RETRY_DELAY_MS);
}
} else {
mPlayer = player;
@@ -1314,7 +1357,7 @@ public class TunerSessionWorker implements PlaybackBufferListener,
private void resetPlayback() {
long timestamp, oldTimestamp;
timestamp = SystemClock.elapsedRealtime();
- stopPlayback();
+ stopPlayback(false);
stopCaptionTrack();
if (ENABLE_PROFILER) {
oldTimestamp = timestamp;
@@ -1336,8 +1379,12 @@ public class TunerSessionWorker implements PlaybackBufferListener,
mRecordingDuration = recording != null ? getDurationForRecording(recording) : null;
mProgram = null;
mPrograms = null;
- mBufferStartTimeMs = mRecordStartTimeMs =
- (mRecordingId != null) ? 0 : System.currentTimeMillis();
+ if (mRecordingId != null) {
+ // Workaround of b/33298048: set it to 1 instead of 0.
+ mBufferStartTimeMs = mRecordStartTimeMs = 1;
+ } else {
+ mBufferStartTimeMs = mRecordStartTimeMs = System.currentTimeMillis();
+ }
mLastPositionMs = 0;
mCaptionTrack = null;
mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS);
@@ -1544,15 +1591,15 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
mChannelBlocked = channelBlocked;
if (mChannelBlocked) {
- mHandler.removeCallbacksAndMessages(null);
- stopPlayback();
+ clearCallbacksAndMessagesSafely();
+ stopPlayback(true);
resetTvTracks();
if (contentRating != null) {
mSession.notifyContentBlocked(contentRating);
}
mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS);
} else {
- mHandler.removeCallbacksAndMessages(null);
+ clearCallbacksAndMessagesSafely();
resetPlayback();
mSession.notifyContentAllowed();
mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS,
@@ -1562,6 +1609,17 @@ public class TunerSessionWorker implements PlaybackBufferListener,
}
}
+ @WorkerThread
+ private void clearCallbacksAndMessagesSafely() {
+ // If MSG_RELEASE is removed, TunerSessionWorker will hang forever.
+ // Do not remove messages, after release is requested from MainThread.
+ synchronized (mReleaseLock) {
+ if (!mReleaseRequested) {
+ mHandler.removeCallbacksAndMessages(null);
+ }
+ }
+ }
+
private boolean hasEnoughBackwardBuffer() {
return mPlayer.getCurrentPosition() + BUFFER_UNDERFLOW_BUFFER_MS
>= mBufferStartTimeMs - mRecordStartTimeMs;
diff --git a/src/com/android/tv/tuner/tvinput/TunerTvInputService.java b/src/com/android/tv/tuner/tvinput/TunerTvInputService.java
index 684ebdbd..6594e089 100644
--- a/src/com/android/tv/tuner/tvinput/TunerTvInputService.java
+++ b/src/com/android/tv/tuner/tvinput/TunerTvInputService.java
@@ -28,9 +28,6 @@ import com.google.android.exoplayer.audio.AudioCapabilities;
import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver;
import com.android.tv.TvApplication;
import com.android.tv.common.feature.CommonFeatures;
-import com.android.tv.tuner.exoplayer.buffer.BufferManager;
-import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager;
-import com.android.tv.tuner.util.SystemPropertiesProxy;
import java.util.Collections;
import java.util.Set;
@@ -45,9 +42,6 @@ public class TunerTvInputService extends TvInputService
private static final String TAG = "TunerTvInputService";
private static final boolean DEBUG = false;
- private static final String MAX_BUFFER_SIZE_KEY = "tv.tuner.buffersize_mbytes";
- private static final int MAX_BUFFER_SIZE_DEF = 2 * 1024; // 2GB
- private static final int MIN_BUFFER_SIZE_DEF = 256; // 256MB
private static final int DVR_STORAGE_CLEANUP_JOB_ID = 100;
// WeakContainer for {@link TvInputSessionImpl}
@@ -55,7 +49,6 @@ public class TunerTvInputService extends TvInputService
private ChannelDataManager mChannelDataManager;
private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver;
private AudioCapabilities mAudioCapabilities;
- private BufferManager mBufferManager;
@Override
public void onCreate() {
@@ -65,7 +58,6 @@ public class TunerTvInputService extends TvInputService
mChannelDataManager = new ChannelDataManager(getApplicationContext());
mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(getApplicationContext(), this);
mAudioCapabilitiesReceiver.register();
- mBufferManager = createBufferManager();
if (CommonFeatures.DVR.isEnabled(this)) {
JobScheduler jobScheduler =
(JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
@@ -79,11 +71,6 @@ public class TunerTvInputService extends TvInputService
jobScheduler.schedule(job);
}
}
- if (mBufferManager == null) {
- Log.i(TAG, "Trickplay is disabled");
- } else {
- Log.i(TAG, "Trickplay is enabled");
- }
}
@Override
@@ -92,9 +79,6 @@ public class TunerTvInputService extends TvInputService
super.onDestroy();
mChannelDataManager.release();
mAudioCapabilitiesReceiver.unregister();
- if (mBufferManager != null) {
- mBufferManager.close();
- }
}
@Override
@@ -106,8 +90,7 @@ public class TunerTvInputService extends TvInputService
public Session onCreateSession(String inputId) {
if (DEBUG) Log.d(TAG, "onCreateSession");
try {
- final TunerSession session = new TunerSession(
- this, mChannelDataManager, mBufferManager);
+ final TunerSession session = new TunerSession(this, mChannelDataManager);
mTunerSessions.add(session);
session.setAudioCapabilities(mAudioCapabilities);
session.setOverlayViewEnabled(true);
@@ -129,17 +112,6 @@ public class TunerTvInputService extends TvInputService
}
}
- private BufferManager createBufferManager() {
- int maxBufferSizeMb =
- SystemPropertiesProxy.getInt(MAX_BUFFER_SIZE_KEY, MAX_BUFFER_SIZE_DEF);
- if (maxBufferSizeMb >= MIN_BUFFER_SIZE_DEF) {
- return new BufferManager(
- new TrickplayStorageManager(getApplicationContext(), getCacheDir(),
- 1024L * 1024 * maxBufferSizeMb));
- }
- return null;
- }
-
public static String getInputId(Context context) {
return TvContract.buildInputId(new ComponentName(context, TunerTvInputService.class));
}
diff --git a/src/com/android/tv/tuner/util/PostalCodeUtils.java b/src/com/android/tv/tuner/util/PostalCodeUtils.java
new file mode 100644
index 00000000..3942ce95
--- /dev/null
+++ b/src/com/android/tv/tuner/util/PostalCodeUtils.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2017 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.tv.tuner.util;
+
+import android.content.Context;
+import android.location.Address;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.tv.tuner.TunerPreferences;
+import com.android.tv.util.LocationUtils;
+
+import java.io.IOException;
+import java.util.Locale;
+
+/**
+ * A utility class to update, get, and set the last known postal or zip code.
+ */
+public class PostalCodeUtils {
+ private static final String TAG = "PostalCodeUtils";
+ private static final String SUPPORTED_COUNTRY_CODE = Locale.US.getCountry();
+
+ /** Returns {@code true} if postal code has been changed */
+ public static boolean updatePostalCode(Context context)
+ throws IOException, SecurityException, NoPostalCodeException {
+ String postalCode = getPostalCode(context);
+ String lastPostalCode = getLastPostalCode(context);
+ if (TextUtils.isEmpty(postalCode)) {
+ if (TextUtils.isEmpty(lastPostalCode)) {
+ throw new NoPostalCodeException();
+ }
+ } else if (!TextUtils.equals(postalCode, lastPostalCode)) {
+ setLastPostalCode(context, postalCode);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Gets the last stored postal or zip code, which might be decided by {@link LocationUtils} or
+ * input by users.
+ */
+ public static String getLastPostalCode(Context context) {
+ return TunerPreferences.getLastPostalCode(context);
+ }
+
+ /**
+ * Sets the last stored postal or zip code. This method will overwrite the value written by
+ * calling {@link #updatePostalCode(Context)}.
+ */
+ public static void setLastPostalCode(Context context, String postalCode) {
+ Log.i(TAG, "Set Postal Code:" + postalCode);
+ TunerPreferences.setLastPostalCode(context, postalCode);
+ }
+
+ @Nullable
+ private static String getPostalCode(Context context) throws IOException, SecurityException {
+ Address address = LocationUtils.getCurrentAddress(context);
+ if (address != null) {
+ Log.i(TAG, "Current country and postal code is " + address.getCountryName() + ", "
+ + address.getPostalCode());
+ if (TextUtils.equals(address.getCountryCode(), SUPPORTED_COUNTRY_CODE)) {
+ return address.getPostalCode();
+ }
+ }
+ return null;
+ }
+
+ /** An {@link java.lang.Exception} class to notify no valid postal or zip code is available. */
+ public static class NoPostalCodeException extends Exception {
+ public NoPostalCodeException() {
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/tv/tuner/util/StringUtils.java b/src/com/android/tv/tuner/util/StringUtils.java
deleted file mode 100644
index 15571e75..00000000
--- a/src/com/android/tv/tuner/util/StringUtils.java
+++ /dev/null
@@ -1,38 +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.tv.tuner.util;
-
-/**
- * Utility class for handling {@link String}.
- */
-public final class StringUtils {
-
- private StringUtils() { }
-
- /**
- * Returns compares two strings lexicographically and handles null values quietly.
- */
- public static int compare(String a, String b) {
- if (a == null) {
- return b == null ? 0 : -1;
- }
- if (b == null) {
- return 1;
- }
- return a.compareTo(b);
- }
-}
diff --git a/src/com/android/tv/tuner/util/SystemPropertiesProxy.java b/src/com/android/tv/tuner/util/SystemPropertiesProxy.java
index 62a64361..2817ccbf 100644
--- a/src/com/android/tv/tuner/util/SystemPropertiesProxy.java
+++ b/src/com/android/tv/tuner/util/SystemPropertiesProxy.java
@@ -58,4 +58,20 @@ public class SystemPropertiesProxy {
}
return def;
}
+
+ public static String getString(String key, String def) throws IllegalArgumentException {
+ try {
+ Class SystemPropertiesClass = Class.forName("android.os.SystemProperties");
+ Method getIntMethod =
+ SystemPropertiesClass.getDeclaredMethod("get", String.class, String.class);
+ getIntMethod.setAccessible(true);
+ return (String) getIntMethod.invoke(SystemPropertiesClass, key, def);
+ } catch (InvocationTargetException
+ | IllegalAccessException
+ | NoSuchMethodException
+ | ClassNotFoundException e) {
+ Log.e(TAG, "Failed to invoke SystemProperties.get()", e);
+ }
+ return def;
+ }
}
diff --git a/src/com/android/tv/tuner/util/TunerInputInfoUtils.java b/src/com/android/tv/tuner/util/TunerInputInfoUtils.java
index 5c411f64..fd9ec77d 100644
--- a/src/com/android/tv/tuner/util/TunerInputInfoUtils.java
+++ b/src/com/android/tv/tuner/util/TunerInputInfoUtils.java
@@ -25,6 +25,7 @@ import android.os.Build;
import android.support.annotation.Nullable;
import android.support.v4.os.BuildCompat;
import android.util.Log;
+import android.util.Pair;
import com.android.tv.common.feature.CommonFeatures;
import com.android.tv.tuner.R;
@@ -43,23 +44,31 @@ public class TunerInputInfoUtils {
*/
@Nullable
@TargetApi(Build.VERSION_CODES.N)
- public static TvInputInfo buildTunerInputInfo(Context context, boolean fromBuiltInTuner) {
- int numOfDevices = TunerHal.getTunerCount(context);
- if (numOfDevices == 0) {
+ public static TvInputInfo buildTunerInputInfo(Context context) {
+ Pair<Integer, Integer> tunerTypeAndCount = TunerHal.getTunerTypeAndCount(context);
+ if (tunerTypeAndCount.first == null || tunerTypeAndCount.second == 0) {
return null;
}
- TvInputInfo.Builder builder = new TvInputInfo.Builder(context, new ComponentName(context,
- TunerTvInputService.class));
- if (fromBuiltInTuner) {
- builder.setLabel(R.string.bt_app_name);
- } else {
- builder.setLabel(R.string.ut_app_name);
+ int inputLabelId = 0;
+ switch (tunerTypeAndCount.first) {
+ case TunerHal.TUNER_TYPE_BUILT_IN:
+ inputLabelId = R.string.bt_app_name;
+ break;
+ case TunerHal.TUNER_TYPE_USB:
+ inputLabelId = R.string.ut_app_name;
+ break;
+ case TunerHal.TUNER_TYPE_NETWORK:
+ inputLabelId = R.string.nt_app_name;
+ break;
}
try {
- return builder.setCanRecord(CommonFeatures.DVR.isEnabled(context))
- .setTunerCount(numOfDevices)
+ TvInputInfo.Builder builder = new TvInputInfo.Builder(context,
+ new ComponentName(context, TunerTvInputService.class));
+ return builder.setLabel(inputLabelId)
+ .setCanRecord(CommonFeatures.DVR.isEnabled(context))
+ .setTunerCount(tunerTypeAndCount.second)
.build();
- } catch (NullPointerException e) {
+ } catch (IllegalArgumentException | NullPointerException e) {
// TunerTvInputService is not enabled.
return null;
}
@@ -73,7 +82,7 @@ public class TunerInputInfoUtils {
public static void updateTunerInputInfo(Context context) {
if (BuildCompat.isAtLeastN()) {
if (DEBUG) Log.d(TAG, "updateTunerInputInfo()");
- TvInputInfo info = buildTunerInputInfo(context, isBuiltInTuner(context));
+ TvInputInfo info = buildTunerInputInfo(context);
if (info != null) {
((TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE))
.updateTvInputInfo(info);
@@ -88,13 +97,4 @@ public class TunerInputInfoUtils {
}
}
}
-
- /**
- * Returns if the current tuner service is for a built-in tuner.
- *
- * @param context {@link Context} instance
- */
- public static boolean isBuiltInTuner(Context context) {
- return TunerHal.getTunerType(context) == TunerHal.TUNER_TYPE_BUILT_IN;
- }
-}
+} \ No newline at end of file