diff options
Diffstat (limited to 'src/com/android/tv/tuner')
80 files changed, 329 insertions, 23833 deletions
diff --git a/src/com/android/tv/tuner/ChannelScanFileParser.java b/src/com/android/tv/tuner/ChannelScanFileParser.java deleted file mode 100644 index a255de3e..00000000 --- a/src/com/android/tv/tuner/ChannelScanFileParser.java +++ /dev/null @@ -1,101 +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; - -import android.util.Log; - -import com.android.tv.tuner.data.nano.Channel; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.List; - -/** - * Parses plain text formatted scan files, which contain the list of channels. - */ -public class ChannelScanFileParser { - private static final String TAG = "ChannelScanFileParser"; - - public static final class ScanChannel { - public final int type; - public final int frequency; - public final String modulation; - public final String filename; - /** - * Radio frequency (channel) number specified at - * https://en.wikipedia.org/wiki/North_American_television_frequencies - * This can be {@code null} for cases like cable signal. - */ - public final Integer radioFrequencyNumber; - - public static ScanChannel forTuner(int frequency, String modulation, - Integer radioFrequencyNumber) { - return new ScanChannel(Channel.TYPE_TUNER, frequency, modulation, null, - radioFrequencyNumber); - } - - public static ScanChannel forFile(int frequency, String filename) { - return new ScanChannel(Channel.TYPE_FILE, frequency, "file:", filename, null); - } - - private ScanChannel(int type, int frequency, String modulation, String filename, - Integer radioFrequencyNumber) { - this.type = type; - this.frequency = frequency; - this.modulation = modulation; - this.filename = filename; - this.radioFrequencyNumber = radioFrequencyNumber; - } - } - - /** - * Parses a given scan file and returns the list of {@link ScanChannel} objects. - * - * @param is {@link InputStream} of a scan file. Each line matches one channel. - * The line format of the scan file is as follows:<br> - * "A <frequency> <modulation>". - * @return a list of {@link ScanChannel} objects parsed - */ - public static List<ScanChannel> parseScanFile(InputStream is) { - BufferedReader in = new BufferedReader(new InputStreamReader(is)); - String line; - List<ScanChannel> scanChannelList = new ArrayList<>(); - try { - while ((line = in.readLine()) != null) { - if (line.isEmpty()) { - continue; - } - if (line.charAt(0) == '#') { - // Skip comment line - continue; - } - String[] tokens = line.split("\\s+"); - if (tokens.length != 3 && tokens.length != 4) { - continue; - } - scanChannelList.add(ScanChannel.forTuner(Integer.parseInt(tokens[1]), tokens[2], - tokens.length == 4 ? Integer.parseInt(tokens[3]) : null)); - } - } catch (IOException e) { - Log.e(TAG, "error on parseScanFile()", e); - } - return scanChannelList; - } -} diff --git a/src/com/android/tv/tuner/DvbDeviceAccessor.java b/src/com/android/tv/tuner/DvbDeviceAccessor.java deleted file mode 100644 index 4f5d8ee4..00000000 --- a/src/com/android/tv/tuner/DvbDeviceAccessor.java +++ /dev/null @@ -1,223 +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; - -import android.content.Context; -import android.media.tv.TvInputManager; -import android.os.ParcelFileDescriptor; -import android.support.annotation.IntDef; -import android.support.annotation.NonNull; -import android.util.Log; - -import com.android.tv.common.recording.RecordingCapability; -import com.android.tv.tuner.tvinput.TunerTvInputService; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Locale; - -/** - * Provides with the file descriptors to access DVB device. - */ -public class DvbDeviceAccessor { - private static final String TAG = "DvbDeviceAccessor"; - - @IntDef({DVB_DEVICE_DEMUX, DVB_DEVICE_DVR, DVB_DEVICE_FRONTEND}) - @Retention(RetentionPolicy.SOURCE) - public @interface DvbDevice {} - public static final int DVB_DEVICE_DEMUX = 0; // TvInputManager.DVB_DEVICE_DEMUX; - public static final int DVB_DEVICE_DVR = 1; // TvInputManager.DVB_DEVICE_DVR; - public static final int DVB_DEVICE_FRONTEND = 2; // TvInputManager.DVB_DEVICE_FRONTEND; - - private static Method sGetDvbDeviceListMethod; - private static Method sOpenDvbDeviceMethod; - - private final TvInputManager mTvInputManager; - - static { - try { - Class tvInputManagerClass = Class.forName("android.media.tv.TvInputManager"); - Class dvbDeviceInfoClass = Class.forName("android.media.tv.DvbDeviceInfo"); - sGetDvbDeviceListMethod = tvInputManagerClass.getDeclaredMethod("getDvbDeviceList"); - sGetDvbDeviceListMethod.setAccessible(true); - sOpenDvbDeviceMethod = tvInputManagerClass.getDeclaredMethod( - "openDvbDevice", dvbDeviceInfoClass, Integer.TYPE); - sOpenDvbDeviceMethod.setAccessible(true); - } catch (ClassNotFoundException e) { - Log.e(TAG, "Couldn't find class", e); - } catch (NoSuchMethodException e) { - Log.e(TAG, "Couldn't find method", e); - } - } - - public DvbDeviceAccessor(Context context) { - mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); - } - - public List<DvbDeviceInfoWrapper> getDvbDeviceList() { - try { - List<DvbDeviceInfoWrapper> wrapperList = new ArrayList<>(); - List dvbDeviceInfoList = (List) sGetDvbDeviceListMethod.invoke(mTvInputManager); - for (Object dvbDeviceInfo : dvbDeviceInfoList) { - wrapperList.add(new DvbDeviceInfoWrapper(dvbDeviceInfo)); - } - Collections.sort(wrapperList); - return wrapperList; - } catch (IllegalAccessException e) { - Log.e(TAG, "Couldn't access", e); - } catch (InvocationTargetException e) { - Log.e(TAG, "Couldn't invoke", e); - } - return null; - } - - /** - * Returns the number of currently connected DVB devices. - */ - public int getNumOfDvbDevices() { - List<DvbDeviceInfoWrapper> dvbDeviceList = getDvbDeviceList(); - return dvbDeviceList == null ? 0 : dvbDeviceList.size(); - } - - public boolean isDvbDeviceAvailable() { - try { - List dvbDeviceInfoList = (List) sGetDvbDeviceListMethod.invoke(mTvInputManager); - return (!dvbDeviceInfoList.isEmpty()); - } catch (IllegalAccessException e) { - Log.e(TAG, "Couldn't access", e); - } catch (InvocationTargetException e) { - Log.e(TAG, "Couldn't invoke", e); - } - return false; - } - - public ParcelFileDescriptor openDvbDevice(DvbDeviceInfoWrapper deviceInfo, - @DvbDevice int device) { - try { - return (ParcelFileDescriptor) sOpenDvbDeviceMethod.invoke( - mTvInputManager, deviceInfo.getDvbDeviceInfo(), device); - } catch (IllegalAccessException e) { - Log.e(TAG, "Couldn't access", e); - } catch (InvocationTargetException e) { - Log.e(TAG, "Couldn't invoke", e); - } - return null; - } - - /** - * Returns the current recording capability for USB tuner. - * @param inputId the input id to use. - */ - public RecordingCapability getRecordingCapability(String inputId) { - List<DvbDeviceInfoWrapper> deviceList = getDvbDeviceList(); - // TODO(DVR) implement accurate capabilities and updating values when needed. - return RecordingCapability.builder() - .setInputId(inputId) - .setMaxConcurrentPlayingSessions(1) - .setMaxConcurrentTunedSessions(deviceList.size()) - .setMaxConcurrentSessionsOfAllTypes(deviceList.size() + 1) - .build(); - } - - public static class DvbDeviceInfoWrapper implements Comparable<DvbDeviceInfoWrapper> { - private static Method sGetAdapterIdMethod; - private static Method sGetDeviceIdMethod; - private final Object mDvbDeviceInfo; - private final int mAdapterId; - private final int mDeviceId; - private final long mId; - - static { - try { - Class dvbDeviceInfoClass = Class.forName("android.media.tv.DvbDeviceInfo"); - sGetAdapterIdMethod = dvbDeviceInfoClass.getDeclaredMethod("getAdapterId"); - sGetAdapterIdMethod.setAccessible(true); - sGetDeviceIdMethod = dvbDeviceInfoClass.getDeclaredMethod("getDeviceId"); - sGetDeviceIdMethod.setAccessible(true); - } catch (ClassNotFoundException e) { - Log.e(TAG, "Couldn't find class", e); - } catch (NoSuchMethodException e) { - Log.e(TAG, "Couldn't find method", e); - } - } - - public DvbDeviceInfoWrapper(Object dvbDeviceInfo) { - mDvbDeviceInfo = dvbDeviceInfo; - mAdapterId = initAdapterId(); - mDeviceId = initDeviceId(); - mId = (((long) getAdapterId()) << 32) | (getDeviceId() & 0xffffffffL); - } - - public long getId() { - return mId; - } - - public int getAdapterId() { - return mAdapterId; - } - - private int initAdapterId() { - try { - return (int) sGetAdapterIdMethod.invoke(mDvbDeviceInfo); - } catch (InvocationTargetException e) { - Log.e(TAG, "Couldn't invoke", e); - } catch (IllegalAccessException e) { - Log.e(TAG, "Couldn't access", e); - } - return -1; - } - - public int getDeviceId() { - return mDeviceId; - } - - private int initDeviceId() { - try { - return (int) sGetDeviceIdMethod.invoke(mDvbDeviceInfo); - } catch (InvocationTargetException e) { - Log.e(TAG, "Couldn't invoke", e); - } catch (IllegalAccessException e) { - Log.e(TAG, "Couldn't access", e); - } - return -1; - } - - public Object getDvbDeviceInfo() { - return mDvbDeviceInfo; - } - - @Override - public int compareTo(@NonNull DvbDeviceInfoWrapper another) { - if (getAdapterId() != another.getAdapterId()) { - return getAdapterId() - another.getAdapterId(); - } - return getDeviceId() - another.getDeviceId(); - } - - @Override - public String toString() { - return String.format(Locale.US, "DvbDeviceInfo {adapterId: %d, deviceId: %d}", - getAdapterId(), - getDeviceId()); - } - } -} diff --git a/src/com/android/tv/tuner/DvbTunerHal.java b/src/com/android/tv/tuner/DvbTunerHal.java deleted file mode 100644 index ea977230..00000000 --- a/src/com/android/tv/tuner/DvbTunerHal.java +++ /dev/null @@ -1,179 +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; - -import android.content.Context; -import android.os.ParcelFileDescriptor; -import android.util.Log; -import com.android.tv.tuner.DvbDeviceAccessor.DvbDeviceInfoWrapper; - -import java.util.List; -import java.util.SortedSet; -import java.util.TreeSet; - -/** - * A class to handle a hardware Linux DVB API supported tuner device. - */ -public class DvbTunerHal extends TunerHal { - - private static final Object sLock = new Object(); - // @GuardedBy("sLock") - private static final SortedSet<DvbDeviceInfoWrapper> sUsedDvbDevices = new TreeSet<>(); - - private final DvbDeviceAccessor mDvbDeviceAccessor; - private DvbDeviceInfoWrapper mDvbDeviceInfo; - - protected DvbTunerHal(Context context) { - super(context); - mDvbDeviceAccessor = new DvbDeviceAccessor(context); - } - - @Override - protected boolean openFirstAvailable() { - List<DvbDeviceInfoWrapper> deviceInfoList = mDvbDeviceAccessor.getDvbDeviceList(); - if (deviceInfoList == null || deviceInfoList.isEmpty()) { - Log.e(TAG, "There's no dvb device attached"); - return false; - } - synchronized (sLock) { - for (DvbDeviceInfoWrapper deviceInfo : deviceInfoList) { - if (!sUsedDvbDevices.contains(deviceInfo)) { - if (DEBUG) Log.d(TAG, "Available device info: " + deviceInfo); - mDvbDeviceInfo = deviceInfo; - sUsedDvbDevices.add(deviceInfo); - getDeliverySystemTypeFromDevice(); - return true; - } - } - } - Log.e(TAG, "There's no available dvb devices"); - return false; - } - - /** - * Acquires the tuner device. The requested device will be locked to the current instance if - * it's not acquired by others. - * - * @param deviceInfo a tuner device to open - * @return {@code true} if the operation was successful, {@code false} otherwise - */ - protected boolean open(DvbDeviceInfoWrapper deviceInfo) { - if (deviceInfo == null) { - Log.e(TAG, "Device info should not be null"); - return false; - } - if (mDvbDeviceInfo != null) { - Log.e(TAG, "Already acquired"); - return false; - } - List<DvbDeviceInfoWrapper> deviceInfoList = mDvbDeviceAccessor.getDvbDeviceList(); - if (deviceInfoList == null || deviceInfoList.isEmpty()) { - Log.e(TAG, "There's no dvb device attached"); - return false; - } - for (DvbDeviceInfoWrapper deviceInfoWrapper : deviceInfoList) { - if (deviceInfoWrapper.compareTo(deviceInfo) == 0) { - synchronized (sLock) { - if (sUsedDvbDevices.contains(deviceInfo)) { - Log.e(TAG, deviceInfo + " is already taken"); - return false; - } - sUsedDvbDevices.add(deviceInfo); - } - if (DEBUG) Log.d(TAG, "Available device info: " + deviceInfo); - mDvbDeviceInfo = deviceInfo; - return true; - } - } - Log.e(TAG, "There's no such dvb device attached"); - return false; - } - - @Override - public void close() { - if (mDvbDeviceInfo != null) { - if (isStreaming()) { - stopTune(); - } - nativeFinalize(mDvbDeviceInfo.getId()); - synchronized (sLock) { - sUsedDvbDevices.remove(mDvbDeviceInfo); - } - mDvbDeviceInfo = null; - } - } - - @Override - protected boolean isDeviceOpen() { - return (mDvbDeviceInfo != null); - } - - @Override - protected long getDeviceId() { - if (mDvbDeviceInfo != null) { - return mDvbDeviceInfo.getId(); - } - return -1; - } - - @Override - protected int openDvbFrontEndFd() { - if (mDvbDeviceInfo != null) { - ParcelFileDescriptor descriptor = mDvbDeviceAccessor.openDvbDevice( - mDvbDeviceInfo, DvbDeviceAccessor.DVB_DEVICE_FRONTEND); - if (descriptor != null) { - return descriptor.detachFd(); - } - } - return -1; - } - - @Override - protected int openDvbDemuxFd() { - if (mDvbDeviceInfo != null) { - ParcelFileDescriptor descriptor = mDvbDeviceAccessor.openDvbDevice( - mDvbDeviceInfo, DvbDeviceAccessor.DVB_DEVICE_DEMUX); - if (descriptor != null) { - return descriptor.detachFd(); - } - } - return -1; - } - - @Override - protected int openDvbDvrFd() { - if (mDvbDeviceInfo != null) { - ParcelFileDescriptor descriptor = mDvbDeviceAccessor.openDvbDevice( - mDvbDeviceInfo, DvbDeviceAccessor.DVB_DEVICE_DVR); - if (descriptor != null) { - return descriptor.detachFd(); - } - } - return -1; - } - - /** - * Gets the number of USB tuner devices currently present. - */ - public static int getNumberOfDevices(Context context) { - try { - return (new DvbDeviceAccessor(context)).getNumOfDvbDevices(); - } catch (Exception e) { - return 0; - } - } -} diff --git a/src/com/android/tv/tuner/TunerHal.java b/src/com/android/tv/tuner/TunerHal.java deleted file mode 100644 index 1176cdf0..00000000 --- a/src/com/android/tv/tuner/TunerHal.java +++ /dev/null @@ -1,352 +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; - -import android.content.Context; -import android.support.annotation.IntDef; -import android.support.annotation.StringDef; -import android.support.annotation.WorkerThread; -import android.util.Log; -import android.util.Pair; - -import com.android.tv.Features; -import com.android.tv.customization.TvCustomizationManager; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.Objects; - -/** - * A base class to handle a hardware tuner device. - */ -public abstract class TunerHal implements AutoCloseable { - protected static final String TAG = "TunerHal"; - protected static final boolean DEBUG = false; - - @IntDef({ FILTER_TYPE_OTHER, FILTER_TYPE_AUDIO, FILTER_TYPE_VIDEO, FILTER_TYPE_PCR }) - @Retention(RetentionPolicy.SOURCE) - public @interface FilterType {} - public static final int FILTER_TYPE_OTHER = 0; - public static final int FILTER_TYPE_AUDIO = 1; - public static final int FILTER_TYPE_VIDEO = 2; - public static final int FILTER_TYPE_PCR = 3; - - @StringDef({ MODULATION_8VSB, MODULATION_QAM256 }) - @Retention(RetentionPolicy.SOURCE) - public @interface ModulationType {} - public static final String MODULATION_8VSB = "8VSB"; - public static final String MODULATION_QAM256 = "QAM256"; - - @IntDef({ DELIVERY_SYSTEM_UNDEFINED, DELIVERY_SYSTEM_ATSC, DELIVERY_SYSTEM_DVBC, - DELIVERY_SYSTEM_DVBS, DELIVERY_SYSTEM_DVBS2, DELIVERY_SYSTEM_DVBT, - DELIVERY_SYSTEM_DVBT2 }) - @Retention(RetentionPolicy.SOURCE) - public @interface DeliverySystemType {} - public static final int DELIVERY_SYSTEM_UNDEFINED = 0; - public static final int DELIVERY_SYSTEM_ATSC = 1; - public static final int DELIVERY_SYSTEM_DVBC = 2; - public static final int DELIVERY_SYSTEM_DVBS = 3; - public static final int DELIVERY_SYSTEM_DVBS2 = 4; - public static final int DELIVERY_SYSTEM_DVBT = 5; - public static final int DELIVERY_SYSTEM_DVBT2 = 6; - - @IntDef({ TUNER_TYPE_BUILT_IN, TUNER_TYPE_USB, TUNER_TYPE_NETWORK }) - @Retention(RetentionPolicy.SOURCE) - public @interface TunerType {} - 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; - protected static final int PID_DVB_SDT = 0x0011; - protected static final int PID_DVB_EIT = 0x0012; - protected static final int DEFAULT_VSB_TUNE_TIMEOUT_MS = 2000; - protected static final int DEFAULT_QAM_TUNE_TIMEOUT_MS = 4000; // Some device takes time for - // QAM256 tuning. - @IntDef({ - BUILT_IN_TUNER_TYPE_LINUX_DVB - }) - @Retention(RetentionPolicy.SOURCE) - private @interface BuiltInTunerType {} - private static final int BUILT_IN_TUNER_TYPE_LINUX_DVB = 1; - - private static Integer sBuiltInTunerType; - - protected @DeliverySystemType int mDeliverySystemType; - private boolean mIsStreaming; - private int mFrequency; - private String mModulation; - - static { - System.loadLibrary("tunertvinput_jni"); - } - - /** - * Creates a TunerHal instance. - * @param context context for creating the TunerHal instance - * @return the TunerHal instance - */ - @WorkerThread - public synchronized static TunerHal createInstance(Context context) { - TunerHal tunerHal = null; - if (DvbTunerHal.getNumberOfDevices(context) > 0) { - if (DEBUG) Log.d(TAG, "Use DvbTunerHal"); - tunerHal = new DvbTunerHal(context); - } - return tunerHal != null && tunerHal.openFirstAvailable() ? tunerHal : null; - } - - /** - * Gets the number of tuner devices currently present. - */ - @WorkerThread - public static Pair<Integer, Integer> getTunerTypeAndCount(Context context) { - if (useBuiltInTuner(context)) { - if (getBuiltInTunerType(context) == BUILT_IN_TUNER_TYPE_LINUX_DVB) { - return new Pair<>(TUNER_TYPE_BUILT_IN, DvbTunerHal.getNumberOfDevices(context)); - } - } else { - int usbTunerCount = DvbTunerHal.getNumberOfDevices(context); - if (usbTunerCount > 0) { - return new Pair<>(TUNER_TYPE_USB, usbTunerCount); - } - } - return new Pair<>(null, 0); - } - - /** - * Check a delivery system is for DVB or not. - */ - public static boolean isDvbDeliverySystem(@DeliverySystemType int deliverySystemType) { - return deliverySystemType == DELIVERY_SYSTEM_DVBC - || deliverySystemType == DELIVERY_SYSTEM_DVBS - || deliverySystemType == DELIVERY_SYSTEM_DVBS2 - || deliverySystemType == DELIVERY_SYSTEM_DVBT - || deliverySystemType == DELIVERY_SYSTEM_DVBT2; - } - - /** - * Returns if tuner input service would use built-in tuners instead of USB tuners or network - * tuners. - */ - static boolean useBuiltInTuner(Context context) { - return getBuiltInTunerType(context) != 0; - } - - private static @BuiltInTunerType int getBuiltInTunerType(Context context) { - if (sBuiltInTunerType == null) { - sBuiltInTunerType = 0; - if (TvCustomizationManager.hasLinuxDvbBuiltInTuner(context) - && DvbTunerHal.getNumberOfDevices(context) > 0) { - sBuiltInTunerType = BUILT_IN_TUNER_TYPE_LINUX_DVB; - } - } - return sBuiltInTunerType; - } - - protected TunerHal(Context context) { - mIsStreaming = false; - mFrequency = -1; - mModulation = null; - } - - protected boolean isStreaming() { - return mIsStreaming; - } - - protected void getDeliverySystemTypeFromDevice() { - if (mDeliverySystemType == DELIVERY_SYSTEM_UNDEFINED) { - mDeliverySystemType = nativeGetDeliverySystemType(getDeviceId()); - } - } - - /** - * 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(); - close(); - } - - protected native void nativeFinalize(long deviceId); - - /** - * Acquires the first available tuner device. If there is a tuner device that is available, the - * tuner device will be locked to the current instance. - * - * @return {@code true} if the operation was successful, {@code false} otherwise - */ - protected abstract boolean openFirstAvailable(); - - protected abstract boolean isDeviceOpen(); - - protected abstract long getDeviceId(); - - /** - * Sets the tuner channel. This should be called after acquiring a tuner device. - * - * @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, - String channelNumber) { - if (!isDeviceOpen()) { - Log.e(TAG, "There's no available device"); - return false; - } - if (mIsStreaming) { - nativeCloseAllPidFilters(getDeviceId()); - mIsStreaming = false; - } - - // When tuning to a new channel in the same frequency, there's no need to stop current tuner - // device completely and the only thing necessary for tuning is reopening pid filters. - if (mFrequency == frequency && Objects.equals(mModulation, modulation)) { - addPidFilter(PID_PAT, FILTER_TYPE_OTHER); - addPidFilter(PID_ATSC_SI_BASE, FILTER_TYPE_OTHER); - if (isDvbDeliverySystem(mDeliverySystemType)) { - addPidFilter(PID_DVB_SDT, FILTER_TYPE_OTHER); - addPidFilter(PID_DVB_EIT, FILTER_TYPE_OTHER); - } - mIsStreaming = true; - return true; - } - int timeout_ms = modulation.equals(MODULATION_8VSB) ? DEFAULT_VSB_TUNE_TIMEOUT_MS - : DEFAULT_QAM_TUNE_TIMEOUT_MS; - if (nativeTune(getDeviceId(), frequency, modulation, timeout_ms)) { - addPidFilter(PID_PAT, FILTER_TYPE_OTHER); - addPidFilter(PID_ATSC_SI_BASE, FILTER_TYPE_OTHER); - if (isDvbDeliverySystem(mDeliverySystemType)) { - addPidFilter(PID_DVB_SDT, FILTER_TYPE_OTHER); - addPidFilter(PID_DVB_EIT, FILTER_TYPE_OTHER); - } - mFrequency = frequency; - mModulation = modulation; - mIsStreaming = true; - return true; - } - return false; - } - - protected native boolean nativeTune(long deviceId, int frequency, - @ModulationType String modulation, int timeout_ms); - - /** - * Sets a pid filter. This should be set after setting a channel. - * - * @param pid a pid number to be added to filter list - * @param filterType a type of pid. Must be one of (FILTER_TYPE_XXX) - * @return {@code true} if the operation was successful, {@code false} otherwise - */ - public synchronized boolean addPidFilter(int pid, @FilterType int filterType) { - if (!isDeviceOpen()) { - Log.e(TAG, "There's no available device"); - return false; - } - if (pid >= 0 && pid <= 0x1fff) { - nativeAddPidFilter(getDeviceId(), pid, filterType); - return true; - } - return false; - } - - protected native void nativeAddPidFilter(long deviceId, int pid, @FilterType int filterType); - protected native void nativeCloseAllPidFilters(long deviceId); - protected native void nativeSetHasPendingTune(long deviceId, boolean hasPendingTune); - protected native int nativeGetDeliverySystemType(long deviceId); - - /** - * Stops current tuning. The tuner device and pid filters will be reset by this call and make - * the tuner ready to accept another tune request. - */ - public synchronized void stopTune() { - if (isDeviceOpen()) { - if (mIsStreaming) { - nativeCloseAllPidFilters(getDeviceId()); - } - nativeStopTune(getDeviceId()); - } - mIsStreaming = false; - mFrequency = -1; - mModulation = null; - } - - public void setHasPendingTune(boolean hasPendingTune) { - nativeSetHasPendingTune(getDeviceId(), hasPendingTune); - } - - public int getDeliverySystemType() { - return mDeliverySystemType; - } - - protected native void nativeStopTune(long deviceId); - - /** - * This method must be called after {@link TunerHal#tune} and before - * {@link TunerHal#stopTune}. Writes at most maxSize TS frames in a buffer - * provided by the user. The frames employ MPEG encoding. - * - * @param javaBuffer a buffer to write the video data in - * @param javaBufferSize the max amount of bytes to write in this buffer. Usually this number - * should be equal to the length of the buffer. - * @return the amount of bytes written in the buffer. Note that this value could be 0 if no new - * frames have been obtained since the last call. - */ - public synchronized int readTsStream(byte[] javaBuffer, int javaBufferSize) { - if (isDeviceOpen()) { - return nativeWriteInBuffer(getDeviceId(), javaBuffer, javaBufferSize); - } else { - return 0; - } - } - - protected native int nativeWriteInBuffer(long deviceId, byte[] javaBuffer, int javaBufferSize); - - /** - * Opens Linux DVB frontend device. This method is called from native JNI and used only for - * DvbTunerHal. - */ - protected int openDvbFrontEndFd() { - return -1; - } - - /** - * Opens Linux DVB demux device. This method is called from native JNI and used only for - * DvbTunerHal. - */ - protected int openDvbDemuxFd() { - return -1; - } - - /** - * Opens Linux DVB dvr device. This method is called from native JNI and used only for - * DvbTunerHal. - */ - protected int openDvbDvrFd() { - return -1; - } -} diff --git a/src/com/android/tv/tuner/TunerInputController.java b/src/com/android/tv/tuner/TunerInputController.java index e06b9b4a..02611bbf 100644 --- a/src/com/android/tv/tuner/TunerInputController.java +++ b/src/com/android/tv/tuner/TunerInputController.java @@ -17,6 +17,9 @@ package com.android.tv.tuner; import android.app.AlarmManager; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -24,10 +27,14 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbManager; import android.net.ConnectivityManager; import android.net.NetworkInfo; +import android.net.Uri; import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; @@ -38,118 +45,132 @@ import android.support.annotation.NonNull; 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.Starter; import com.android.tv.TvApplication; -import com.android.tv.common.SoftPreconditions; -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 com.android.tv.TvSingletons; +import com.android.tv.common.BuildConfig; +import com.android.tv.common.util.SystemPropertiesProxy; + +import com.android.tv.tuner.setup.BaseTunerSetupActivity; +import com.android.tv.tuner.util.TunerInputInfoUtils; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; /** - * Controls the package visibility of {@link TunerTvInputService}. - * <p> - * Listens to broadcast intent for {@link Intent#ACTION_BOOT_COMPLETED}, - * {@code UsbManager.ACTION_USB_DEVICE_ATTACHED}, and {@code UsbManager.ACTION_USB_DEVICE_ATTACHED} - * to update the connection status of the supported USB TV tuners. + * Controls the package visibility of {@link BaseTunerTvInputService}. + * + * <p>Listens to broadcast intent for {@link Intent#ACTION_BOOT_COMPLETED}, {@code + * UsbManager.ACTION_USB_DEVICE_ATTACHED}, and {@code UsbManager.ACTION_USB_DEVICE_ATTACHED} to + * update the connection status of the supported USB TV tuners. */ public class TunerInputController { - private static final boolean DEBUG = true; + private static final boolean DEBUG = false; 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"; + private static final String PLAY_STORE_LINK_TEMPLATE = "market://details?id=%s"; - /** - * Action of {@link Intent} to check network connection repeatedly when it is necessary. - */ - private static final String CHECKING_NETWORK_CONNECTION = - "com.android.tv.action.CHECKING_NETWORK_CONNECTION"; + /** Action of {@link Intent} to check network connection repeatedly when it is necessary. */ + private static final String CHECKING_NETWORK_TUNER_STATUS = + "com.android.tv.action.CHECKING_NETWORK_TUNER_STATUS"; private static final String EXTRA_CHECKING_DURATION = "com.android.tv.action.extra.CHECKING_DURATION"; + private static final String EXTRA_DEVICE_IP = "com.android.tv.action.extra.DEVICE_IP"; 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 String NOTIFICATION_CHANNEL_ID = "tuner_discovery_notification"; + // TODO: Load settings from XML file private static final TunerDevice[] TUNER_DEVICES = { 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) - // STOPSHIP: Add WinTV-soloHD (Isoc) temporary for test. Remove this after test complete. new TunerDevice(0x2040, 0x0264, null), }; private static final int MSG_ENABLE_INPUT_SERVICE = 1000; private static final long DVB_DRIVER_CHECK_DELAY_MS = 300; - /** - * Checks status of USB devices to see if there are available USB tuners connected. - */ - public static void onCheckingUsbTunerStatus(Context context, String action) { - onCheckingUsbTunerStatus(context, action, new CheckDvbDeviceHandler()); - } + private final ComponentName usbTunerComponent; + private final ComponentName networkTunerComponent; + private final ComponentName builtInTunerComponent; + private final Map<TunerDevice, ComponentName> mTunerServiceMapping = new HashMap<>(); - private static void onCheckingUsbTunerStatus(Context context, String action, - @NonNull CheckDvbDeviceHandler handler) { - SharedPreferences sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(context); - if (TunerHal.useBuiltInTuner(context)) { - enableTunerTvInputService(context, true, false, TunerHal.TUNER_TYPE_BUILT_IN); - return; + private final Map<ComponentName, String> mTunerApplicationNames = new HashMap<>(); + private final Map<ComponentName, String> mNotificationMessages = new HashMap<>(); + private final Map<ComponentName, Bitmap> mNotificationLargeIcons = new HashMap<>(); + + private final CheckDvbDeviceHandler mHandler = new CheckDvbDeviceHandler(this); + + public TunerInputController(ComponentName embeddedTuner) { + usbTunerComponent = embeddedTuner; + networkTunerComponent = usbTunerComponent; + builtInTunerComponent = usbTunerComponent; + for (TunerDevice device : TUNER_DEVICES) { + mTunerServiceMapping.put(device, usbTunerComponent); } - // Falls back to the below to check USB tuner devices. - boolean enabled = isUsbTunerConnected(context); + } + + /** Checks status of USB devices to see if there are available USB tuners connected. */ + public void onCheckingUsbTunerStatus(Context context, String action) { + onCheckingUsbTunerStatus(context, action, mHandler); + } + + private void onCheckingUsbTunerStatus( + Context context, String action, @NonNull CheckDvbDeviceHandler handler) { + Set<TunerDevice> connectedUsbTuners = getConnectedUsbTuners(context); handler.removeMessages(MSG_ENABLE_INPUT_SERVICE); - if (enabled) { + if (!connectedUsbTuners.isEmpty()) { // Need to check if DVB driver is accessible. Since the driver creation // could be happen after the USB event, delay the checking by // DVB_DRIVER_CHECK_DELAY_MS. - handler.sendMessageDelayed(handler.obtainMessage(MSG_ENABLE_INPUT_SERVICE, context), + handler.sendMessageDelayed( + handler.obtainMessage(MSG_ENABLE_INPUT_SERVICE, context), DVB_DRIVER_CHECK_DELAY_MS); } else { - 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); - return; - } - enableTunerTvInputService(context, false, false, TextUtils - .equals(action, UsbManager.ACTION_USB_DEVICE_DETACHED) ? - TunerHal.TUNER_TYPE_USB : null); + handleTunerStatusChanged( + context, + false, + connectedUsbTuners, + TextUtils.equals(action, UsbManager.ACTION_USB_DEVICE_DETACHED) + ? TunerHal.TUNER_TYPE_USB + : null); } } - private static void onNetworkTunerChanged(Context context, boolean enabled) { + private void onNetworkTunerChanged(Context context, boolean enabled) { SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + if (sharedPreferences.contains(PREFERENCE_IS_NETWORK_TUNER_ATTACHED) + && sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false) + == enabled) { + // the status is not changed + return; + } if (enabled) { - // 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); } else { - 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); - } + sharedPreferences + .edit() + .putBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false) + .apply(); } + // Network tuner detection is initiated by UI. So the app should not + // be killed. + handleTunerStatusChanged( + context, true, getConnectedUsbTuners(context), TunerHal.TUNER_TYPE_NETWORK); } /** @@ -158,66 +179,131 @@ public class TunerInputController { * @param context {@link Context} instance * @return {@code true} if any tuner device we support is plugged in */ - private static boolean isUsbTunerConnected(Context context) { + private Set<TunerDevice> getConnectedUsbTuners(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); + Set<TunerDevice> devices = new HashSet<>(); for (UsbDevice device : deviceList.values()) { if (DEBUG) { Log.d(TAG, "Device: " + device); } for (TunerDevice tuner : TUNER_DEVICES) { - if (tuner.equals(device) && tuner.isSupported(currentSecurityLevel)) { + if (tuner.equalsTo(device) && tuner.isSupported(currentSecurityLevel)) { Log.i(TAG, "Tuner found"); - return true; + devices.add(tuner); } } } - return false; + return devices; + } + + private void handleTunerStatusChanged( + Context context, + boolean forceDontKillApp, + Set<TunerDevice> connectedUsbTuners, + Integer triggerType) { + Map<ComponentName, Integer> serviceToEnable = new HashMap<>(); + Set<ComponentName> serviceToDisable = new HashSet<>(); + serviceToDisable.add(builtInTunerComponent); + serviceToDisable.add(networkTunerComponent); + if (TunerFeatures.TUNER.isEnabled(context)) { + // TODO: support both built-in tuner and other tuners at the same time? + if (TunerHal.useBuiltInTuner(context)) { + enableTunerTvInputService( + context, true, false, TunerHal.TUNER_TYPE_BUILT_IN, builtInTunerComponent); + return; + } + SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(context); + if (sharedPreferences.getBoolean(PREFERENCE_IS_NETWORK_TUNER_ATTACHED, false)) { + serviceToEnable.put(networkTunerComponent, TunerHal.TUNER_TYPE_NETWORK); + } + } + for (TunerDevice device : TUNER_DEVICES) { + if (TunerFeatures.TUNER.isEnabled(context) && connectedUsbTuners.contains(device)) { + serviceToEnable.put(mTunerServiceMapping.get(device), TunerHal.TUNER_TYPE_USB); + } else { + serviceToDisable.add(mTunerServiceMapping.get(device)); + } + } + serviceToDisable.removeAll(serviceToEnable.keySet()); + for (ComponentName serviceComponent : serviceToEnable.keySet()) { + if (isTunerPackageInstalled(context, serviceComponent)) { + enableTunerTvInputService( + context, + true, + forceDontKillApp, + serviceToEnable.get(serviceComponent), + serviceComponent); + } else { + sendNotificationToInstallPackage(context, serviceComponent); + } + } + for (ComponentName serviceComponent : serviceToDisable) { + if (isTunerPackageInstalled(context, serviceComponent)) { + enableTunerTvInputService( + context, false, forceDontKillApp, triggerType, serviceComponent); + } else { + cancelNotificationToInstallPackage(context, serviceComponent); + } + } } /** - * Enable/disable the component {@link TunerTvInputService}. + * Enable/disable the component {@link BaseTunerTvInputService}. * * @param context {@link Context} instance * @param enabled {@code true} to enable the service; otherwise {@code false} */ - private static void enableTunerTvInputService(Context context, boolean enabled, - boolean forceDontKillApp, Integer tunerType) { + private static void enableTunerTvInputService( + Context context, + boolean enabled, + boolean forceDontKillApp, + Integer tunerType, + ComponentName serviceComponent) { if (DEBUG) Log.d(TAG, "enableTunerTvInputService: " + enabled); - PackageManager pm = context.getPackageManager(); - ComponentName componentName = new ComponentName(context, TunerTvInputService.class); - - // Don't kill app by enabling/disabling TvActivity. If LC is killed by enabling/disabling - // TvActivity, the following pm.setComponentEnabledSetting doesn't work. - ((TvApplication) context.getApplicationContext()).handleInputCountChanged( - true, enabled, true); - // 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 = forceDontKillApp - || TvApplication.getSingletons(context).getMainActivityWrapper().isCreated() - ? PackageManager.DONT_KILL_APP : 0; - int newState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED - : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; - if (newState != pm.getComponentEnabledSetting(componentName)) { - // Send/cancel the USB tuner TV input setup notification. - TunerSetupActivity.onTvInputEnabled(context, enabled, tunerType); - // Enable/disable the USB tuner TV input. - pm.setComponentEnabledSetting(componentName, newState, flags); - 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(); + PackageManager pm = context.getPackageManager(); + int newState = + enabled + ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED + : PackageManager.COMPONENT_ENABLED_STATE_DISABLED; + if (newState != pm.getComponentEnabledSetting(serviceComponent)) { + int flags = forceDontKillApp ? PackageManager.DONT_KILL_APP : 0; + if (serviceComponent.getPackageName().equals(context.getPackageName())) { + // Don't kill APP when handling input count changing. Or the following + // setComponentEnabledSetting() call won't work. + ((TvApplication) context.getApplicationContext()) + .handleInputCountChanged(true, enabled, true); + // Bundled input. Don't kill app if LiveChannels app is active since we don't want + // to kill the running app. + if (TvSingletons.getSingletons(context).getMainActivityWrapper().isCreated()) { + flags |= PackageManager.DONT_KILL_APP; + } + // Send/cancel the USB tuner TV input setup notification. + BaseTunerSetupActivity.onTvInputEnabled(context, enabled, tunerType); + 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(); + } } } + // Enable/disable the USB tuner TV input. + pm.setComponentEnabledSetting(serviceComponent, newState, flags); if (DEBUG) Log.d(TAG, "Status updated:" + enabled); - } else if (enabled) { + } else if (enabled && serviceComponent.getPackageName().equals(context.getPackageName())) { // 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); @@ -227,96 +313,179 @@ public class TunerInputController { /** * Discovers a network tuner. If the network connection is down, it won't repeatedly checking. */ - public static void executeNetworkTunerDiscoveryAsyncTask(final Context context) { - boolean runningInMainProcess = - TvApplication.getSingletons(context).isRunningInMainProcess(); - SoftPreconditions.checkState(runningInMainProcess); - if (!runningInMainProcess) { - return; - } - executeNetworkTunerDiscoveryAsyncTask(context, 0); + public void executeNetworkTunerDiscoveryAsyncTask(final Context context) { + executeNetworkTunerDiscoveryAsyncTask(context, 0, 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. + * @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. + * @param deviceIp The previous discovered device IP, 0 if none. */ - private static void executeNetworkTunerDiscoveryAsyncTask(final Context context, - final long repeatedDurationMs) { - if (!Features.NETWORK_TUNER.isEnabled(context)) { + private void executeNetworkTunerDiscoveryAsyncTask( + final Context context, final long repeatedDurationMs, final int deviceIp) { + if (!TunerFeatures.NETWORK_TUNER.isEnabled(context)) { return; } - new AsyncTask<Void, Void, Boolean>() { - @Override - protected Boolean doInBackground(Void... params) { - if (isNetworkConnected(context)) { + final Intent networkCheckingIntent = new Intent(context, IntentReceiver.class); + networkCheckingIntent.setAction(CHECKING_NETWORK_TUNER_STATUS); + if (!isNetworkConnected(context) && repeatedDurationMs > 0) { + sendCheckingAlarm(context, networkCheckingIntent, repeatedDurationMs); + } else { + new AsyncTask<Void, Void, Boolean>() { + @Override + protected Boolean doInBackground(Void... params) { + Boolean result = null; // 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, IntentReceiver.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 result; } - return null; - } - @Override - protected void onPostExecute(Boolean result) { - if (result == null) { - return; + @Override + protected void onPostExecute(Boolean foundNetworkTuner) { + if (foundNetworkTuner == null) { + return; + } + sendCheckingAlarm( + context, + networkCheckingIntent, + foundNetworkTuner ? INITIAL_CHECKING_DURATION_MS : repeatedDurationMs); + onNetworkTunerChanged(context, foundNetworkTuner); } - onNetworkTunerChanged(context, result); - } - }.execute(); + }.execute(); + } } private static boolean isNetworkConnected(Context context) { - ConnectivityManager cm = (ConnectivityManager) - context.getSystemService(Context.CONNECTIVITY_SERVICE); + ConnectivityManager cm = + (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = cm.getActiveNetworkInfo(); return networkInfo != null && networkInfo.isConnected(); } + private static void sendCheckingAlarm(Context context, Intent intent, long delayMs) { + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + intent.putExtra(EXTRA_CHECKING_DURATION, delayMs); + PendingIntent alarmIntent = + PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + alarmManager.set( + AlarmManager.ELAPSED_REALTIME, + SystemClock.elapsedRealtime() + delayMs, + alarmIntent); + } + + private static boolean isTunerPackageInstalled( + Context context, ComponentName serviceComponent) { + try { + context.getPackageManager().getPackageInfo(serviceComponent.getPackageName(), 0); + return true; + } catch (NameNotFoundException e) { + return false; + } + } + + private void sendNotificationToInstallPackage(Context context, ComponentName serviceComponent) { + if (!BuildConfig.ENG) { + return; + } + String applicationName = mTunerApplicationNames.get(serviceComponent); + if (applicationName == null) { + applicationName = context.getString(R.string.tuner_install_default_application_name); + } + String contentTitle = + context.getString( + R.string.tuner_install_notification_content_title, applicationName); + String contentText = mNotificationMessages.get(serviceComponent); + if (contentText == null) { + contentText = context.getString(R.string.tuner_install_notification_content_text); + } + Bitmap largeIcon = mNotificationLargeIcons.get(serviceComponent); + if (largeIcon == null) { + // TODO: Make a better default image. + largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_store); + } + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) == null) { + createNotificationChannel(context, notificationManager); + } + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData( + Uri.parse( + String.format( + PLAY_STORE_LINK_TEMPLATE, serviceComponent.getPackageName()))); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Notification.Builder builder = new Notification.Builder(context, NOTIFICATION_CHANNEL_ID); + builder.setAutoCancel(true) + .setSmallIcon(R.drawable.ic_launcher_s) + .setLargeIcon(largeIcon) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setCategory(Notification.CATEGORY_RECOMMENDATION) + .setContentIntent(PendingIntent.getActivity(context, 0, intent, 0)); + notificationManager.notify(serviceComponent.getPackageName(), 0, builder.build()); + } + + private static void cancelNotificationToInstallPackage( + Context context, ComponentName serviceComponent) { + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(serviceComponent.getPackageName(), 0); + } + + private static void createNotificationChannel( + Context context, NotificationManager notificationManager) { + notificationManager.createNotificationChannel( + new NotificationChannel( + NOTIFICATION_CHANNEL_ID, + context.getResources() + .getString(R.string.ut_setup_notification_channel_name), + NotificationManager.IMPORTANCE_HIGH)); + } + public static class IntentReceiver extends BroadcastReceiver { - private final CheckDvbDeviceHandler mHandler = new CheckDvbDeviceHandler(); @Override public void onReceive(Context context, Intent intent) { if (DEBUG) Log.d(TAG, "Broadcast intent received:" + intent); - TvApplication.setCurrentRunningProcess(context, true); - if (!Features.TUNER.isEnabled(context)) { - enableTunerTvInputService(context, false, false, null); + Starter.start(context); + TunerInputController tunerInputController = + TvSingletons.getSingletons(context).getTunerInputController(); + if (!TunerFeatures.TUNER.isEnabled(context)) { + tunerInputController.handleTunerStatusChanged( + context, false, Collections.emptySet(), null); return; } switch (intent.getAction()) { case Intent.ACTION_BOOT_COMPLETED: - executeNetworkTunerDiscoveryAsyncTask(context, INITIAL_CHECKING_DURATION_MS); + tunerInputController.executeNetworkTunerDiscoveryAsyncTask( + context, INITIAL_CHECKING_DURATION_MS, 0); + // fall through case TvApplication.ACTION_APPLICATION_FIRST_LAUNCHED: case UsbManager.ACTION_USB_DEVICE_ATTACHED: case UsbManager.ACTION_USB_DEVICE_DETACHED: - onCheckingUsbTunerStatus(context, intent.getAction(), mHandler); + tunerInputController.onCheckingUsbTunerStatus(context, intent.getAction()); 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)); + case CHECKING_NETWORK_TUNER_STATUS: + long repeatedDurationMs = + intent.getLongExtra( + EXTRA_CHECKING_DURATION, INITIAL_CHECKING_DURATION_MS); + tunerInputController.executeNetworkTunerDiscoveryAsyncTask( + context, + Math.min(repeatedDurationMs * 2, MAXIMUM_CHECKING_DURATION_MS), + intent.getIntExtra(EXTRA_DEVICE_IP, 0)); break; + default: // fall out } } } /** - * Simple data holder for a USB device. Used to represent a tuner model, and compare - * against {@link UsbDevice}. + * Simple data holder for a USB device. Used to represent a tuner model, and compare against + * {@link UsbDevice}. */ private static class TunerDevice { private final int vendorId; @@ -331,7 +500,7 @@ public class TunerInputController { this.minSecurityLevel = minSecurityLevel; } - private boolean equals(UsbDevice device) { + private boolean equalsTo(UsbDevice device) { return device.getVendorId() == vendorId && device.getProductId() == productId; } @@ -354,10 +523,13 @@ public class TunerInputController { } private static class CheckDvbDeviceHandler extends Handler { + + private final TunerInputController mTunerInputController; private DvbDeviceAccessor mDvbDeviceAccessor; - CheckDvbDeviceHandler() { + CheckDvbDeviceHandler(TunerInputController tunerInputController) { super(Looper.getMainLooper()); + this.mTunerInputController = tunerInputController; } @Override @@ -369,9 +541,15 @@ public class TunerInputController { mDvbDeviceAccessor = new DvbDeviceAccessor(context); } boolean enabled = mDvbDeviceAccessor.isDvbDeviceAvailable(); - enableTunerTvInputService( - context, enabled, false, enabled ? TunerHal.TUNER_TYPE_USB : null); + mTunerInputController.handleTunerStatusChanged( + context, + false, + enabled + ? mTunerInputController.getConnectedUsbTuners(context) + : Collections.emptySet(), + TunerHal.TUNER_TYPE_USB); break; + default: // fall out } } } diff --git a/src/com/android/tv/tuner/TunerPreferenceProvider.java b/src/com/android/tv/tuner/TunerPreferenceProvider.java deleted file mode 100644 index 3a3561b6..00000000 --- a/src/com/android/tv/tuner/TunerPreferenceProvider.java +++ /dev/null @@ -1,203 +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; - -import android.content.ContentProvider; -import android.content.ContentValues; -import android.content.Context; -import android.content.UriMatcher; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteException; -import android.database.sqlite.SQLiteOpenHelper; -import android.net.Uri; - -/** - * A content provider for storing the preferences. It's used across TV app and USB tuner TV input. - */ -public class TunerPreferenceProvider extends ContentProvider { - /** The authority of the provider */ - public static final String AUTHORITY = "com.android.tv.tuner.preferences"; - - private static final String PATH_PREFERENCES = "preferences"; - - private static final int DATABASE_VERSION = 1; - private static final String DATABASE_NAME = "usbtuner_preferences.db"; - private static final String PREFERENCES_TABLE = "preferences"; - - private static final int MATCH_PREFERENCE = 1; - private static final int MATCH_PREFERENCE_KEY = 2; - - private static final UriMatcher sUriMatcher; - - private DatabaseOpenHelper mDatabaseOpenHelper; - - static { - sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); - sUriMatcher.addURI(AUTHORITY, "preferences", MATCH_PREFERENCE); - sUriMatcher.addURI(AUTHORITY, "preferences/*", MATCH_PREFERENCE_KEY); - } - - /** - * Builds a Uri that points to a specific preference. - - * @param key a key of the preference to point to - */ - public static Uri buildPreferenceUri(String key) { - return Preferences.CONTENT_URI.buildUpon().appendPath(key).build(); - } - - /** - * Columns definitions for the preferences table. - */ - public interface Preferences { - - /** - * The content:// style for the preferences table. - */ - Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + PATH_PREFERENCES); - - /** - * The MIME type of a directory of preferences. - */ - String CONTENT_TYPE = "vnd.android.cursor.dir/preferences"; - - /** - * The MIME type of a single preference. - */ - String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/preferences"; - - /** - * The ID of this preference. - * - * <p>This is auto-incremented. - * - * <p>Type: INTEGER - */ - String _ID = "_id"; - - /** - * The key of this preference. - * - * <p>Should be unique. - * - * <p>Type: TEXT - */ - String COLUMN_KEY = "key"; - - /** - * The value of this preference. - * - * <p>Type: TEXT - */ - String COLUMN_VALUE = "value"; - } - - private static class DatabaseOpenHelper extends SQLiteOpenHelper { - public DatabaseOpenHelper(Context context) { - super(context, DATABASE_NAME, null, DATABASE_VERSION); - } - - @Override - public void onCreate(SQLiteDatabase db) { - db.execSQL("CREATE TABLE " + PREFERENCES_TABLE + " (" - + Preferences._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," - + Preferences.COLUMN_KEY + " TEXT NOT NULL," - + Preferences.COLUMN_VALUE + " TEXT);"); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - // No-op - } - } - - @Override - public boolean onCreate() { - mDatabaseOpenHelper = new DatabaseOpenHelper(getContext()); - return true; - } - - @Override - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { - int match = sUriMatcher.match(uri); - if (match != MATCH_PREFERENCE && match != MATCH_PREFERENCE_KEY) { - throw new UnsupportedOperationException(); - } - SQLiteDatabase db = mDatabaseOpenHelper.getReadableDatabase(); - Cursor cursor = db.query(PREFERENCES_TABLE, projection, selection, selectionArgs, - null, null, sortOrder); - cursor.setNotificationUri(getContext().getContentResolver(), uri); - return cursor; - } - - @Override - public String getType(Uri uri) { - switch (sUriMatcher.match(uri)) { - case MATCH_PREFERENCE: - return Preferences.CONTENT_TYPE; - case MATCH_PREFERENCE_KEY: - return Preferences.CONTENT_ITEM_TYPE; - } - throw new IllegalArgumentException("Unknown URI " + uri); - } - - /** - * Inserts a preference row into the preference table. - * - * If a key is already exists in the table, it removes the old row and inserts a new row. - * - * @param uri the URL of the table to insert into - * @param values the initial values for the newly inserted row - * @return the URL of the newly created row - */ - @Override - public Uri insert(Uri uri, ContentValues values) { - if (sUriMatcher.match(uri) != MATCH_PREFERENCE) { - throw new UnsupportedOperationException(); - } - return insertRow(uri, values); - } - - private Uri insertRow(Uri uri, ContentValues values) { - SQLiteDatabase db = mDatabaseOpenHelper.getWritableDatabase(); - - // Remove the old row. - db.delete(PREFERENCES_TABLE, Preferences.COLUMN_KEY + " like ?", - new String[]{values.getAsString(Preferences.COLUMN_KEY)}); - - long rowId = db.insert(PREFERENCES_TABLE, null, values); - if (rowId > 0) { - Uri rowUri = buildPreferenceUri(values.getAsString(Preferences.COLUMN_KEY)); - getContext().getContentResolver().notifyChange(rowUri, null); - return rowUri; - } - - throw new SQLiteException("Failed to insert row into " + uri); - } - - @Override - public int delete(Uri uri, String selection, String[] selectionArgs) { - throw new UnsupportedOperationException(); - } - - @Override - public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { - throw new UnsupportedOperationException(); - } -} diff --git a/src/com/android/tv/tuner/TunerPreferences.java b/src/com/android/tv/tuner/TunerPreferences.java deleted file mode 100644 index 11a6a969..00000000 --- a/src/com/android/tv/tuner/TunerPreferences.java +++ /dev/null @@ -1,428 +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; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.SharedPreferences; -import android.database.ContentObserver; -import android.database.Cursor; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Handler; -import android.support.annotation.GuardedBy; -import android.support.annotation.IntDef; -import android.support.annotation.MainThread; - -import com.android.tv.common.SoftPreconditions; -import com.android.tv.tuner.TunerPreferenceProvider.Preferences; -import com.android.tv.tuner.util.TisConfiguration; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -/** - * A helper class for the USB tuner preferences. - */ -public class TunerPreferences { - private static final String TAG = "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"; - private static final String PREFS_KEY_TRICKPLAY_SETTING = "trickplay_setting"; - private static final String PREFS_KEY_TRICKPLAY_EXPIRED_MS = "trickplay_expired_ms"; - - private static final String SHARED_PREFS_NAME = "com.android.tv.tuner.preferences"; - - public static final int CHANNEL_DATA_VERSION_NOT_SET = -1; - - @IntDef({TRICKPLAY_SETTING_NOT_SET, TRICKPLAY_SETTING_DISABLED, TRICKPLAY_SETTING_ENABLED}) - @Retention(RetentionPolicy.SOURCE) - public @interface TrickplaySetting { - } - - /** - * Trickplay setting is not changed by a user. Trickplay will be enabled in this case. - */ - public static final int TRICKPLAY_SETTING_NOT_SET = -1; - - /** - * Trickplay setting is disabled. - */ - public static final int TRICKPLAY_SETTING_DISABLED = 0; - - /** - * Trickplay setting is enabled. - */ - public static final int TRICKPLAY_SETTING_ENABLED = 1; - - @GuardedBy("TunerPreferences.class") - private static final Bundle sPreferenceValues = new Bundle(); - private static LoadPreferencesTask sLoadPreferencesTask; - private static ContentObserver sContentObserver; - private static TunerPreferencesChangedListener sPreferencesChangedListener = null; - - private static boolean sInitialized; - - /** - * Listeners for TunerPreferences change. - */ - public interface TunerPreferencesChangedListener { - void onTunerPreferencesChanged(); - } - - /** - * Initializes the USB tuner preferences. - */ - @MainThread - public static void initialize(final Context context) { - if (sInitialized) { - return; - } - sInitialized = true; - if (useContentProvider(context)) { - loadPreferences(context); - sContentObserver = new ContentObserver(new Handler()) { - @Override - public void onChange(boolean selfChange) { - loadPreferences(context); - } - }; - context.getContentResolver().registerContentObserver( - TunerPreferenceProvider.Preferences.CONTENT_URI, true, sContentObserver); - } else { - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - getSharedPreferences(context); - return null; - } - }.execute(); - } - } - - /** - * Releases the resources. - */ - public static synchronized void release(Context context) { - if (useContentProvider(context) && sContentObserver != null) { - context.getContentResolver().unregisterContentObserver(sContentObserver); - } - setTunerPreferencesChangedListener(null); - } - - /** - * Sets the listener for TunerPreferences change. - */ - public static void setTunerPreferencesChangedListener( - TunerPreferencesChangedListener listener) { - sPreferencesChangedListener = listener; - } - - /** - * Loads the preferences from database. - * <p> - * This preferences is used across processes, so the preferences should be loaded again when the - * databases changes. - */ - @MainThread - public static void loadPreferences(Context context) { - if (sLoadPreferencesTask != null - && sLoadPreferencesTask.getStatus() != AsyncTask.Status.FINISHED) { - sLoadPreferencesTask.cancel(true); - } - sLoadPreferencesTask = new LoadPreferencesTask(context); - sLoadPreferencesTask.execute(); - } - - private static boolean useContentProvider(Context context) { - // If TIS is a part of LC, it should use ContentProvider to resolve multiple process access. - return TisConfiguration.isPackagedWithLiveChannels(context); - } - - public static synchronized int getChannelDataVersion(Context context) { - SoftPreconditions.checkState(sInitialized); - if (useContentProvider(context)) { - return sPreferenceValues.getInt(PREFS_KEY_CHANNEL_DATA_VERSION, - CHANNEL_DATA_VERSION_NOT_SET); - } else { - return getSharedPreferences(context) - .getInt(TunerPreferences.PREFS_KEY_CHANNEL_DATA_VERSION, - CHANNEL_DATA_VERSION_NOT_SET); - } - } - - public static synchronized void setChannelDataVersion(Context context, int version) { - if (useContentProvider(context)) { - setPreference(context, PREFS_KEY_CHANNEL_DATA_VERSION, version); - } else { - getSharedPreferences(context).edit() - .putInt(TunerPreferences.PREFS_KEY_CHANNEL_DATA_VERSION, version) - .apply(); - } - } - - public static synchronized int getScannedChannelCount(Context context) { - SoftPreconditions.checkState(sInitialized); - if (useContentProvider(context)) { - return sPreferenceValues.getInt(PREFS_KEY_SCANNED_CHANNEL_COUNT); - } else { - return getSharedPreferences(context) - .getInt(TunerPreferences.PREFS_KEY_SCANNED_CHANNEL_COUNT, 0); - } - } - - public static synchronized void setScannedChannelCount(Context context, int channelCount) { - if (useContentProvider(context)) { - setPreference(context, PREFS_KEY_SCANNED_CHANNEL_COUNT, channelCount); - } else { - getSharedPreferences(context).edit() - .putInt(TunerPreferences.PREFS_KEY_SCANNED_CHANNEL_COUNT, channelCount) - .apply(); - } - } - - 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); - } else { - return getSharedPreferences(context) - .getBoolean(TunerPreferences.PREFS_KEY_SCAN_DONE, false); - } - } - - public static synchronized void setScanDone(Context context) { - if (useContentProvider(context)) { - setPreference(context, PREFS_KEY_SCAN_DONE, true); - } else { - getSharedPreferences(context).edit() - .putBoolean(TunerPreferences.PREFS_KEY_SCAN_DONE, true) - .apply(); - } - } - - public static synchronized boolean shouldShowSetupActivity(Context context) { - SoftPreconditions.checkState(sInitialized); - if (useContentProvider(context)) { - return sPreferenceValues.getBoolean(PREFS_KEY_LAUNCH_SETUP); - } else { - return getSharedPreferences(context) - .getBoolean(TunerPreferences.PREFS_KEY_LAUNCH_SETUP, false); - } - } - - public static synchronized void setShouldShowSetupActivity(Context context, boolean need) { - if (useContentProvider(context)) { - setPreference(context, PREFS_KEY_LAUNCH_SETUP, need); - } else { - getSharedPreferences(context).edit() - .putBoolean(TunerPreferences.PREFS_KEY_LAUNCH_SETUP, need) - .apply(); - } - } - - public static synchronized long getTrickplayExpiredMs(Context context) { - SoftPreconditions.checkState(sInitialized); - if (useContentProvider(context)) { - return sPreferenceValues.getLong(PREFS_KEY_TRICKPLAY_EXPIRED_MS, 0); - } else { - return getSharedPreferences(context) - .getLong(TunerPreferences.PREFS_KEY_TRICKPLAY_EXPIRED_MS, 0); - } - } - - public static synchronized void setTrickplayExpiredMs(Context context, long timeMs) { - if (useContentProvider(context)) { - setPreference(context, PREFS_KEY_TRICKPLAY_EXPIRED_MS, timeMs); - } else { - getSharedPreferences(context).edit() - .putLong(TunerPreferences.PREFS_KEY_TRICKPLAY_EXPIRED_MS, timeMs) - .apply(); - } - } - - public static synchronized @TrickplaySetting int getTrickplaySetting(Context context) { - SoftPreconditions.checkState(sInitialized); - if (useContentProvider(context)) { - return sPreferenceValues.getInt(PREFS_KEY_TRICKPLAY_SETTING, TRICKPLAY_SETTING_NOT_SET); - } else { - return getSharedPreferences(context) - .getInt(TunerPreferences.PREFS_KEY_TRICKPLAY_SETTING, TRICKPLAY_SETTING_NOT_SET); - } - } - - public static synchronized void setTrickplaySetting(Context context, - @TrickplaySetting int trickplaySetting) { - SoftPreconditions.checkState(sInitialized); - SoftPreconditions.checkArgument(trickplaySetting != TRICKPLAY_SETTING_NOT_SET); - if (useContentProvider(context)) { - setPreference(context, PREFS_KEY_TRICKPLAY_SETTING, trickplaySetting); - } else { - getSharedPreferences(context).edit() - .putInt(TunerPreferences.PREFS_KEY_TRICKPLAY_SETTING, trickplaySetting) - .apply(); - } - } - - public static synchronized boolean getStoreTsStream(Context context) { - SoftPreconditions.checkState(sInitialized); - if (useContentProvider(context)) { - return sPreferenceValues.getBoolean(PREFS_KEY_STORE_TS_STREAM, false); - } else { - return getSharedPreferences(context) - .getBoolean(TunerPreferences.PREFS_KEY_STORE_TS_STREAM, false); - } - } - - public static synchronized void setStoreTsStream(Context context, boolean shouldStore) { - if (useContentProvider(context)) { - setPreference(context, PREFS_KEY_STORE_TS_STREAM, shouldStore); - } else { - getSharedPreferences(context).edit() - .putBoolean(TunerPreferences.PREFS_KEY_STORE_TS_STREAM, shouldStore) - .apply(); - } - } - - private static SharedPreferences getSharedPreferences(Context context) { - return context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); - } - - 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, long value) { - sPreferenceValues.putLong(key, value); - savePreference(context, key, Long.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) { - ContentResolver resolver = context.getContentResolver(); - ContentValues values = new ContentValues(); - values.put(Preferences.COLUMN_KEY, key); - values.put(Preferences.COLUMN_VALUE, value); - try { - resolver.insert(Preferences.CONTENT_URI, values); - } catch (Exception e) { - SoftPreconditions.warn(TAG, "setPreference", "Error writing preference values", - e); - } - return null; - } - }.execute(); - } - - private static class LoadPreferencesTask extends AsyncTask<Void, Void, Bundle> { - private final Context mContext; - private LoadPreferencesTask(Context context) { - mContext = context; - } - - @Override - protected Bundle doInBackground(Void... params) { - Bundle bundle = new Bundle(); - ContentResolver resolver = mContext.getContentResolver(); - String[] projection = new String[] { Preferences.COLUMN_KEY, Preferences.COLUMN_VALUE }; - try (Cursor cursor = resolver.query(Preferences.CONTENT_URI, projection, null, null, - null)) { - if (cursor != null) { - while (!isCancelled() && cursor.moveToNext()) { - String key = cursor.getString(0); - String value = cursor.getString(1); - switch (key) { - case PREFS_KEY_TRICKPLAY_EXPIRED_MS: - bundle.putLong(key, Long.parseLong(value)); - break; - case PREFS_KEY_CHANNEL_DATA_VERSION: - case PREFS_KEY_SCANNED_CHANNEL_COUNT: - case PREFS_KEY_TRICKPLAY_SETTING: - try { - bundle.putInt(key, Integer.parseInt(value)); - } catch (NumberFormatException e) { - // Does nothing. - } - break; - case PREFS_KEY_SCAN_DONE: - case PREFS_KEY_LAUNCH_SETUP: - case PREFS_KEY_STORE_TS_STREAM: - bundle.putBoolean(key, Boolean.parseBoolean(value)); - break; - case PREFS_KEY_LAST_POSTAL_CODE: - bundle.putString(key, value); - break; - } - } - } - } catch (Exception e) { - SoftPreconditions.warn(TAG, "getPreference", "Error querying preference values", e); - return null; - } - return bundle; - } - - @Override - protected void onPostExecute(Bundle bundle) { - synchronized (TunerPreferences.class) { - if (bundle != null) { - sPreferenceValues.putAll(bundle); - } - } - if (sPreferencesChangedListener != null) { - sPreferencesChangedListener.onTunerPreferencesChanged(); - } - } - } -}
\ No newline at end of file diff --git a/src/com/android/tv/tuner/cc/CaptionLayout.java b/src/com/android/tv/tuner/cc/CaptionLayout.java deleted file mode 100644 index a88538df..00000000 --- a/src/com/android/tv/tuner/cc/CaptionLayout.java +++ /dev/null @@ -1,76 +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.cc; - -import android.content.Context; -import android.util.AttributeSet; - -import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; -import com.android.tv.tuner.layout.ScaledLayout; - -/** - * Layout containing the safe title area that helps the closed captions look more prominent. - * This is required by CEA-708B. - */ -public class CaptionLayout extends ScaledLayout { - // The safe title area has 10% margins of the screen. - private static final float SAFE_TITLE_AREA_SCALE_START_X = 0.1f; - private static final float SAFE_TITLE_AREA_SCALE_END_X = 0.9f; - private static final float SAFE_TITLE_AREA_SCALE_START_Y = 0.1f; - private static final float SAFE_TITLE_AREA_SCALE_END_Y = 0.9f; - - private final ScaledLayout mSafeTitleAreaLayout; - private AtscCaptionTrack mCaptionTrack; - - public CaptionLayout(Context context) { - this(context, null); - } - - public CaptionLayout(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public CaptionLayout(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - mSafeTitleAreaLayout = new ScaledLayout(context); - addView(mSafeTitleAreaLayout, new ScaledLayoutParams( - SAFE_TITLE_AREA_SCALE_START_X, SAFE_TITLE_AREA_SCALE_END_X, - SAFE_TITLE_AREA_SCALE_START_Y, SAFE_TITLE_AREA_SCALE_END_Y)); - } - - public void addOrUpdateViewToSafeTitleArea(CaptionWindowLayout captionWindowLayout, - ScaledLayoutParams scaledLayoutParams) { - int index = mSafeTitleAreaLayout.indexOfChild(captionWindowLayout); - if (index < 0) { - mSafeTitleAreaLayout.addView(captionWindowLayout, scaledLayoutParams); - return; - } - mSafeTitleAreaLayout.updateViewLayout(captionWindowLayout, scaledLayoutParams); - } - - public void removeViewFromSafeTitleArea(CaptionWindowLayout captionWindowLayout) { - mSafeTitleAreaLayout.removeView(captionWindowLayout); - } - - public void setCaptionTrack(AtscCaptionTrack captionTrack) { - mCaptionTrack = captionTrack; - } - - public AtscCaptionTrack getCaptionTrack() { - return mCaptionTrack; - } -} diff --git a/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java b/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java deleted file mode 100644 index 24a0f354..00000000 --- a/src/com/android/tv/tuner/cc/CaptionTrackRenderer.java +++ /dev/null @@ -1,344 +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.cc; - -import android.os.Handler; -import android.os.Message; -import android.util.Log; -import android.view.View; - -import com.android.tv.tuner.data.Cea708Data.CaptionEvent; -import com.android.tv.tuner.data.Cea708Data.CaptionPenAttr; -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 java.util.ArrayList; -import java.util.concurrent.TimeUnit; - -/** - * Decodes and renders CEA-708. - */ -public class CaptionTrackRenderer implements Handler.Callback { - // TODO: Remaining works - // CaptionTrackRenderer does not support the full spec of CEA-708. The remaining works are - // described in the follows. - // C0 Table: Backspace, FF, and HCR are not supported. The rule for P16 is not standardized but - // it is handled as EUC-KR charset for korea broadcasting. - // C1 Table: All styles of windows and pens except underline, italic, pen size, and pen offset - // specified in CEA-708 are ignored and this follows system wide cc preferences for - // look and feel. SetPenLocation is not implemented. - // G2 Table: TSP, NBTSP and BLK are not supported. - // Text/commands: Word wrapping, fonts, row and column locking are not supported. - - private static final String TAG = "CaptionTrackRenderer"; - private static final boolean DEBUG = false; - - private static final long DELAY_IN_MILLIS = TimeUnit.MILLISECONDS.toMillis(100); - - // According to CEA-708B, there can exist up to 8 caption windows. - private static final int CAPTION_WINDOWS_MAX = 8; - private static final int CAPTION_ALL_WINDOWS_BITMAP = 255; - - private static final int MSG_DELAY_CANCEL = 1; - private static final int MSG_CAPTION_CLEAR = 2; - - private static final long CAPTION_CLEAR_INTERVAL_MS = 60000; - - private final CaptionLayout mCaptionLayout; - private boolean mIsDelayed = false; - private CaptionWindowLayout mCurrentWindowLayout; - private final CaptionWindowLayout[] mCaptionWindowLayouts = - new CaptionWindowLayout[CAPTION_WINDOWS_MAX]; - private final ArrayList<CaptionEvent> mPendingCaptionEvents = new ArrayList<>(); - private final Handler mHandler; - - public CaptionTrackRenderer(CaptionLayout captionLayout) { - mCaptionLayout = captionLayout; - mHandler = new Handler(this); - } - - @Override - public boolean handleMessage(Message msg) { - switch (msg.what) { - case MSG_DELAY_CANCEL: - delayCancel(); - return true; - case MSG_CAPTION_CLEAR: - clearWindows(CAPTION_ALL_WINDOWS_BITMAP); - return true; - } - return false; - } - - public void start(AtscCaptionTrack captionTrack) { - if (captionTrack == null) { - stop(); - return; - } - if (DEBUG) { - Log.d(TAG, "Start captionTrack " + captionTrack.language); - } - reset(); - mCaptionLayout.setCaptionTrack(captionTrack); - mCaptionLayout.setVisibility(View.VISIBLE); - } - - public void stop() { - if (DEBUG) { - Log.d(TAG, "Stop captionTrack"); - } - mCaptionLayout.setVisibility(View.INVISIBLE); - mHandler.removeMessages(MSG_CAPTION_CLEAR); - } - - public void processCaptionEvent(CaptionEvent event) { - if (mIsDelayed) { - mPendingCaptionEvents.add(event); - return; - } - switch (event.type) { - case Cea708Parser.CAPTION_EMIT_TYPE_BUFFER: - sendBufferToCurrentWindow((String) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_CONTROL: - sendControlToCurrentWindow((char) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_CWX: - setCurrentWindowLayout((int) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_CLW: - clearWindows((int) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DSW: - displayWindows((int) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_HDW: - hideWindows((int) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_TGW: - toggleWindows((int) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DLW: - deleteWindows((int) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DLY: - delay((int) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DLC: - delayCancel(); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_RST: - reset(); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SPA: - setPenAttr((CaptionPenAttr) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SPC: - setPenColor((CaptionPenColor) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SPL: - setPenLocation((CaptionPenLocation) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_SWA: - setWindowAttr((CaptionWindowAttr) event.obj); - break; - case Cea708Parser.CAPTION_EMIT_TYPE_COMMAND_DFX: - defineWindow((CaptionWindow) event.obj); - break; - } - } - - // The window related caption commands - private void setCurrentWindowLayout(int windowId) { - if (windowId < 0 || windowId >= mCaptionWindowLayouts.length) { - return; - } - CaptionWindowLayout windowLayout = mCaptionWindowLayouts[windowId]; - if (windowLayout == null) { - return; - } - if (DEBUG) { - Log.d(TAG, "setCurrentWindowLayout to " + windowId); - } - mCurrentWindowLayout = windowLayout; - } - - // Each bit of windowBitmap indicates a window. - // If a bit is set, the window id is the same as the number of the trailing zeros of the bit. - private ArrayList<CaptionWindowLayout> getWindowsFromBitmap(int windowBitmap) { - ArrayList<CaptionWindowLayout> windows = new ArrayList<>(); - for (int i = 0; i < CAPTION_WINDOWS_MAX; ++i) { - if ((windowBitmap & (1 << i)) != 0) { - CaptionWindowLayout windowLayout = mCaptionWindowLayouts[i]; - if (windowLayout != null) { - windows.add(windowLayout); - } - } - } - return windows; - } - - private void clearWindows(int windowBitmap) { - if (windowBitmap == 0) { - return; - } - for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { - windowLayout.clear(); - } - } - - private void displayWindows(int windowBitmap) { - if (windowBitmap == 0) { - return; - } - for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { - windowLayout.show(); - } - } - - private void hideWindows(int windowBitmap) { - if (windowBitmap == 0) { - return; - } - for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { - windowLayout.hide(); - } - } - - private void toggleWindows(int windowBitmap) { - if (windowBitmap == 0) { - return; - } - for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { - if (windowLayout.isShown()) { - windowLayout.hide(); - } else { - windowLayout.show(); - } - } - } - - private void deleteWindows(int windowBitmap) { - if (windowBitmap == 0) { - return; - } - for (CaptionWindowLayout windowLayout : getWindowsFromBitmap(windowBitmap)) { - windowLayout.removeFromCaptionView(); - mCaptionWindowLayouts[windowLayout.getCaptionWindowId()] = null; - } - } - - public void clear() { - mHandler.sendEmptyMessage(MSG_CAPTION_CLEAR); - } - - public void reset() { - mCurrentWindowLayout = null; - mIsDelayed = false; - mPendingCaptionEvents.clear(); - for (int i = 0; i < CAPTION_WINDOWS_MAX; ++i) { - if (mCaptionWindowLayouts[i] != null) { - mCaptionWindowLayouts[i].removeFromCaptionView(); - } - mCaptionWindowLayouts[i] = null; - } - mCaptionLayout.setVisibility(View.INVISIBLE); - mHandler.removeMessages(MSG_CAPTION_CLEAR); - } - - private void setWindowAttr(CaptionWindowAttr windowAttr) { - if (mCurrentWindowLayout != null) { - mCurrentWindowLayout.setWindowAttr(windowAttr); - } - } - - private void defineWindow(CaptionWindow window) { - if (window == null) { - return; - } - int windowId = window.id; - if (windowId < 0 || windowId >= mCaptionWindowLayouts.length) { - return; - } - CaptionWindowLayout windowLayout = mCaptionWindowLayouts[windowId]; - if (windowLayout == null) { - windowLayout = new CaptionWindowLayout(mCaptionLayout.getContext()); - } - windowLayout.initWindow(mCaptionLayout, window); - mCurrentWindowLayout = mCaptionWindowLayouts[windowId] = windowLayout; - } - - // The job related caption commands - private void delay(int tenthsOfSeconds) { - if (tenthsOfSeconds < 0 || tenthsOfSeconds > 255) { - return; - } - mIsDelayed = true; - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_DELAY_CANCEL), - tenthsOfSeconds * DELAY_IN_MILLIS); - } - - private void delayCancel() { - mIsDelayed = false; - processPendingBuffer(); - } - - private void processPendingBuffer() { - for (CaptionEvent event : mPendingCaptionEvents) { - processCaptionEvent(event); - } - mPendingCaptionEvents.clear(); - } - - // The implicit write caption commands - private void sendControlToCurrentWindow(char control) { - if (mCurrentWindowLayout != null) { - mCurrentWindowLayout.sendControl(control); - } - } - - private void sendBufferToCurrentWindow(String buffer) { - if (mCurrentWindowLayout != null) { - mCurrentWindowLayout.sendBuffer(buffer); - mHandler.removeMessages(MSG_CAPTION_CLEAR); - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CAPTION_CLEAR), - CAPTION_CLEAR_INTERVAL_MS); - } - } - - // The pen related caption commands - private void setPenAttr(CaptionPenAttr attr) { - if (mCurrentWindowLayout != null) { - mCurrentWindowLayout.setPenAttr(attr); - } - } - - private void setPenColor(CaptionPenColor color) { - if (mCurrentWindowLayout != null) { - mCurrentWindowLayout.setPenColor(color); - } - } - - private void setPenLocation(CaptionPenLocation location) { - if (mCurrentWindowLayout != null) { - mCurrentWindowLayout.setPenLocation(location.row, location.column); - } - } -} diff --git a/src/com/android/tv/tuner/cc/CaptionWindowLayout.java b/src/com/android/tv/tuner/cc/CaptionWindowLayout.java deleted file mode 100644 index 6f42b506..00000000 --- a/src/com/android/tv/tuner/cc/CaptionWindowLayout.java +++ /dev/null @@ -1,650 +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.cc; - -import android.content.Context; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.Typeface; -import android.text.Layout.Alignment; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.TextUtils; -import android.text.style.CharacterStyle; -import android.text.style.RelativeSizeSpan; -import android.text.style.StyleSpan; -import android.text.style.SubscriptSpan; -import android.text.style.SuperscriptSpan; -import android.text.style.UnderlineSpan; -import android.util.AttributeSet; -import android.util.Log; -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; -import android.view.accessibility.CaptioningManager; -import android.view.accessibility.CaptioningManager.CaptionStyle; -import android.view.accessibility.CaptioningManager.CaptioningChangeListener; -import android.widget.RelativeLayout; - -import com.google.android.exoplayer.text.CaptionStyleCompat; -import com.google.android.exoplayer.text.SubtitleView; -import com.android.tv.tuner.data.Cea708Data.CaptionPenAttr; -import com.android.tv.tuner.data.Cea708Data.CaptionPenColor; -import com.android.tv.tuner.data.Cea708Data.CaptionWindow; -import com.android.tv.tuner.data.Cea708Data.CaptionWindowAttr; -import com.android.tv.tuner.layout.ScaledLayout; - -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** - * Layout which renders a caption window of CEA-708B. It contains a {@link SubtitleView} that - * takes care of displaying the actual cc text. - */ -public class CaptionWindowLayout extends RelativeLayout implements View.OnLayoutChangeListener { - private static final String TAG = "CaptionWindowLayout"; - private static final boolean DEBUG = false; - - private static final float PROPORTION_PEN_SIZE_SMALL = .75f; - private static final float PROPORTION_PEN_SIZE_LARGE = 1.25f; - - // The following values indicates the maximum cell number of a window. - private static final int ANCHOR_RELATIVE_POSITIONING_MAX = 99; - private static final int ANCHOR_VERTICAL_MAX = 74; - private static final int ANCHOR_HORIZONTAL_4_3_MAX = 159; - private static final int ANCHOR_HORIZONTAL_16_9_MAX = 209; - - // The following values indicates a gravity of a window. - private static final int ANCHOR_MODE_DIVIDER = 3; - private static final int ANCHOR_HORIZONTAL_MODE_LEFT = 0; - private static final int ANCHOR_HORIZONTAL_MODE_CENTER = 1; - private static final int ANCHOR_HORIZONTAL_MODE_RIGHT = 2; - private static final int ANCHOR_VERTICAL_MODE_TOP = 0; - private static final int ANCHOR_VERTICAL_MODE_CENTER = 1; - private static final int ANCHOR_VERTICAL_MODE_BOTTOM = 2; - - private static final int US_MAX_COLUMN_COUNT_16_9 = 42; - private static final int US_MAX_COLUMN_COUNT_4_3 = 32; - private static final int KR_MAX_COLUMN_COUNT_16_9 = 52; - private static final int KR_MAX_COLUMN_COUNT_4_3 = 40; - private static final int MAX_ROW_COUNT = 15; - - private static final String KOR_ALPHABET = - new String("\uAC00".getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); - private static final float WIDE_SCREEN_ASPECT_RATIO_THRESHOLD = 1.6f; - - private CaptionLayout mCaptionLayout; - private CaptionStyleCompat mCaptionStyleCompat; - - // TODO: Replace SubtitleView to {@link com.google.android.exoplayer.text.SubtitleLayout}. - private final SubtitleView mSubtitleView; - private int mRowLimit = 0; - private final SpannableStringBuilder mBuilder = new SpannableStringBuilder(); - private final List<CharacterStyle> mCharacterStyles = new ArrayList<>(); - private int mCaptionWindowId; - private int mCurrentTextRow = -1; - private float mFontScale; - private float mTextSize; - private String mWidestChar; - private int mLastCaptionLayoutWidth; - private int mLastCaptionLayoutHeight; - private int mWindowJustify; - private int mPrintDirection; - - private class SystemWideCaptioningChangeListener extends CaptioningChangeListener { - @Override - public void onUserStyleChanged(CaptionStyle userStyle) { - mCaptionStyleCompat = CaptionStyleCompat.createFromCaptionStyle(userStyle); - mSubtitleView.setStyle(mCaptionStyleCompat); - updateWidestChar(); - } - - @Override - public void onFontScaleChanged(float fontScale) { - mFontScale = fontScale; - updateTextSize(); - } - } - - public CaptionWindowLayout(Context context) { - this(context, null); - } - - public CaptionWindowLayout(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public CaptionWindowLayout(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - - // Add a subtitle view to the layout. - mSubtitleView = new SubtitleView(context); - LayoutParams params = new RelativeLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - addView(mSubtitleView, params); - - // Set the system wide cc preferences to the subtitle view. - CaptioningManager captioningManager = - (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); - mFontScale = captioningManager.getFontScale(); - mCaptionStyleCompat = - CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle()); - mSubtitleView.setStyle(mCaptionStyleCompat); - mSubtitleView.setText(""); - captioningManager.addCaptioningChangeListener(new SystemWideCaptioningChangeListener()); - updateWidestChar(); - } - - public int getCaptionWindowId() { - return mCaptionWindowId; - } - - public void setCaptionWindowId(int captionWindowId) { - mCaptionWindowId = captionWindowId; - } - - public void clear() { - clearText(); - hide(); - } - - public void show() { - setVisibility(View.VISIBLE); - requestLayout(); - } - - public void hide() { - setVisibility(View.INVISIBLE); - requestLayout(); - } - - public void setPenAttr(CaptionPenAttr penAttr) { - mCharacterStyles.clear(); - if (penAttr.italic) { - mCharacterStyles.add(new StyleSpan(Typeface.ITALIC)); - } - if (penAttr.underline) { - mCharacterStyles.add(new UnderlineSpan()); - } - switch (penAttr.penSize) { - case CaptionPenAttr.PEN_SIZE_SMALL: - mCharacterStyles.add(new RelativeSizeSpan(PROPORTION_PEN_SIZE_SMALL)); - break; - case CaptionPenAttr.PEN_SIZE_LARGE: - mCharacterStyles.add(new RelativeSizeSpan(PROPORTION_PEN_SIZE_LARGE)); - break; - } - switch (penAttr.penOffset) { - case CaptionPenAttr.OFFSET_SUBSCRIPT: - mCharacterStyles.add(new SubscriptSpan()); - break; - case CaptionPenAttr.OFFSET_SUPERSCRIPT: - mCharacterStyles.add(new SuperscriptSpan()); - break; - } - } - - public void setPenColor(CaptionPenColor penColor) { - // TODO: apply pen colors or skip this and use the style of system wide cc style as is. - } - - public void setPenLocation(int row, int column) { - // TODO: change the location of pen when window's justify isn't left. - // According to the CEA708B spec 8.7, setPenLocation means set the pen cursor within - // window's text buffer. When row > mCurrentTextRow, we add "\n" to make the cursor locate - // at row. Adding white space to make cursor locate at column. - if (mWindowJustify == CaptionWindowAttr.JUSTIFY_LEFT) { - if (mCurrentTextRow >= 0) { - for (int r = mCurrentTextRow; r < row; ++r) { - appendText("\n"); - } - if (mCurrentTextRow <= row) { - for (int i = 0; i < column; ++i) { - appendText(" "); - } - } - } - } - mCurrentTextRow = row; - } - - public void setWindowAttr(CaptionWindowAttr windowAttr) { - // TODO: apply window attrs or skip this and use the style of system wide cc style as is. - mWindowJustify = windowAttr.justify; - mPrintDirection = windowAttr.printDirection; - } - - public void sendBuffer(String buffer) { - appendText(buffer); - } - - public void sendControl(char control) { - // TODO: there are a bunch of ASCII-style control codes. - } - - /** - * This method places the window on a given CaptionLayout along with the anchor of the window. - * <p> - * According to CEA-708B, the anchor id indicates the gravity of the window as the follows. - * For example, A value 7 of a anchor id says that a window is align with its parent bottom and - * is located at the center horizontally of its parent. - * </p> - * <h4>Anchor id and the gravity of a window</h4> - * <table> - * <tr> - * <th>GRAVITY</th> - * <th>LEFT</th> - * <th>CENTER_HORIZONTAL</th> - * <th>RIGHT</th> - * </tr> - * <tr> - * <th>TOP</th> - * <td>0</td> - * <td>1</td> - * <td>2</td> - * </tr> - * <tr> - * <th>CENTER_VERTICAL</th> - * <td>3</td> - * <td>4</td> - * <td>5</td> - * </tr> - * <tr> - * <th>BOTTOM</th> - * <td>6</td> - * <td>7</td> - * <td>8</td> - * </tr> - * </table> - * <p> - * In order to handle the gravity of a window, there are two steps. First, set the size of the - * window. Since the window will be positioned at {@link ScaledLayout}, the size factors are - * determined in a ratio. Second, set the gravity of the window. {@link CaptionWindowLayout} is - * inherited from {@link RelativeLayout}. Hence, we could set the gravity of its child view, - * {@link SubtitleView}. - * </p> - * <p> - * The gravity of the window is also related to its size. When it should be pushed to a one of - * the end of the window, like LEFT, RIGHT, TOP or BOTTOM, the anchor point should be a boundary - * of the window. When it should be pushed in the horizontal/vertical center of its container, - * the horizontal/vertical center point of the window should be the same as the anchor point. - * </p> - * - * @param captionLayout a given {@link CaptionLayout}, which contains a safe title area - * @param captionWindow a given {@link CaptionWindow}, which stores the construction info of the - * window - */ - public void initWindow(CaptionLayout captionLayout, CaptionWindow captionWindow) { - if (DEBUG) { - Log.d(TAG, "initWindow with " - + (captionLayout != null ? captionLayout.getCaptionTrack() : null)); - } - if (mCaptionLayout != captionLayout) { - if (mCaptionLayout != null) { - mCaptionLayout.removeOnLayoutChangeListener(this); - } - mCaptionLayout = captionLayout; - mCaptionLayout.addOnLayoutChangeListener(this); - updateWidestChar(); - } - - // Both anchor vertical and horizontal indicates the position cell number of the window. - float scaleRow = (float) captionWindow.anchorVertical / (captionWindow.relativePositioning - ? ANCHOR_RELATIVE_POSITIONING_MAX : ANCHOR_VERTICAL_MAX); - float scaleCol = (float) captionWindow.anchorHorizontal / - (captionWindow.relativePositioning ? ANCHOR_RELATIVE_POSITIONING_MAX - : (isWideAspectRatio() - ? ANCHOR_HORIZONTAL_16_9_MAX : ANCHOR_HORIZONTAL_4_3_MAX)); - - // The range of scaleRow/Col need to be verified to be in [0, 1]. - // Otherwise a {@link RuntimeException} will be raised in {@link ScaledLayout}. - if (scaleRow < 0 || scaleRow > 1) { - Log.i(TAG, "The vertical position of the anchor point should be at the range of 0 and 1" - + " but " + scaleRow); - scaleRow = Math.max(0, Math.min(scaleRow, 1)); - } - if (scaleCol < 0 || scaleCol > 1) { - Log.i(TAG, "The horizontal position of the anchor point should be at the range of 0 and" - + " 1 but " + scaleCol); - scaleCol = Math.max(0, Math.min(scaleCol, 1)); - } - int gravity = Gravity.CENTER; - int horizontalMode = captionWindow.anchorId % ANCHOR_MODE_DIVIDER; - int verticalMode = captionWindow.anchorId / ANCHOR_MODE_DIVIDER; - float scaleStartRow = 0; - float scaleEndRow = 1; - float scaleStartCol = 0; - float scaleEndCol = 1; - switch (horizontalMode) { - case ANCHOR_HORIZONTAL_MODE_LEFT: - gravity = Gravity.LEFT; - mSubtitleView.setTextAlignment(Alignment.ALIGN_NORMAL); - scaleStartCol = scaleCol; - break; - case ANCHOR_HORIZONTAL_MODE_CENTER: - float gap = Math.min(1 - scaleCol, scaleCol); - - // Since all TV sets use left text alignment instead of center text alignment - // for this case, we follow the industry convention if possible. - int columnCount = captionWindow.columnCount + 1; - if (isKoreanLanguageTrack()) { - columnCount /= 2; - } - columnCount = Math.min(getScreenColumnCount(), columnCount); - StringBuilder widestTextBuilder = new StringBuilder(); - for (int i = 0; i < columnCount; ++i) { - widestTextBuilder.append(mWidestChar); - } - Paint paint = new Paint(); - paint.setTypeface(mCaptionStyleCompat.typeface); - paint.setTextSize(mTextSize); - float maxWindowWidth = paint.measureText(widestTextBuilder.toString()); - float halfMaxWidthScale = mCaptionLayout.getWidth() > 0 - ? maxWindowWidth / 2.0f / (mCaptionLayout.getWidth() * 0.8f) : 0.0f; - if (halfMaxWidthScale > 0f && halfMaxWidthScale < scaleCol) { - // Calculate the expected max window size based on the column count of the - // caption window multiplied by average alphabets char width, then align the - // left side of the window with the left side of the expected max window. - gravity = Gravity.LEFT; - mSubtitleView.setTextAlignment(Alignment.ALIGN_NORMAL); - scaleStartCol = scaleCol - halfMaxWidthScale; - scaleEndCol = 1.0f; - } else { - // The gap will be the minimum distance value of the distances from both - // horizontal end points to the anchor point. - // If scaleCol <= 0.5, the range of scaleCol is [0, the anchor point * 2]. - // If scaleCol > 0.5, the range of scaleCol is [(1 - the anchor point) * 2, 1]. - // The anchor point is located at the horizontal center of the window in both - // cases. - gravity = Gravity.CENTER_HORIZONTAL; - mSubtitleView.setTextAlignment(Alignment.ALIGN_CENTER); - scaleStartCol = scaleCol - gap; - scaleEndCol = scaleCol + gap; - } - break; - case ANCHOR_HORIZONTAL_MODE_RIGHT: - gravity = Gravity.RIGHT; - mSubtitleView.setTextAlignment(Alignment.ALIGN_OPPOSITE); - scaleEndCol = scaleCol; - break; - } - switch (verticalMode) { - case ANCHOR_VERTICAL_MODE_TOP: - gravity |= Gravity.TOP; - scaleStartRow = scaleRow; - break; - case ANCHOR_VERTICAL_MODE_CENTER: - gravity |= Gravity.CENTER_VERTICAL; - - // See the above comment. - float gap = Math.min(1 - scaleRow, scaleRow); - scaleStartRow = scaleRow - gap; - scaleEndRow = scaleRow + gap; - break; - case ANCHOR_VERTICAL_MODE_BOTTOM: - gravity |= Gravity.BOTTOM; - scaleEndRow = scaleRow; - break; - } - mCaptionLayout.addOrUpdateViewToSafeTitleArea(this, new ScaledLayout - .ScaledLayoutParams(scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol)); - setCaptionWindowId(captionWindow.id); - setRowLimit(captionWindow.rowCount); - setGravity(gravity); - setWindowStyle(captionWindow.windowStyle); - if (mWindowJustify == CaptionWindowAttr.JUSTIFY_CENTER) { - mSubtitleView.setTextAlignment(Alignment.ALIGN_CENTER); - } - if (captionWindow.visible) { - show(); - } else { - hide(); - } - } - - @Override - public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, - int oldTop, int oldRight, int oldBottom) { - int width = right - left; - int height = bottom - top; - if (width != mLastCaptionLayoutWidth || height != mLastCaptionLayoutHeight) { - mLastCaptionLayoutWidth = width; - mLastCaptionLayoutHeight = height; - updateTextSize(); - } - } - - private boolean isKoreanLanguageTrack() { - return mCaptionLayout != null && mCaptionLayout.getCaptionTrack() != null - && mCaptionLayout.getCaptionTrack().language != null - && "KOR".compareToIgnoreCase(mCaptionLayout.getCaptionTrack().language) == 0; - } - - private boolean isWideAspectRatio() { - return mCaptionLayout != null && mCaptionLayout.getCaptionTrack() != null - && mCaptionLayout.getCaptionTrack().wideAspectRatio; - } - - private void updateWidestChar() { - if (isKoreanLanguageTrack()) { - mWidestChar = KOR_ALPHABET; - } else { - Paint paint = new Paint(); - paint.setTypeface(mCaptionStyleCompat.typeface); - Charset latin1 = Charset.forName("ISO-8859-1"); - float widestCharWidth = 0f; - for (int i = 0; i < 256; ++i) { - String ch = new String(new byte[]{(byte) i}, latin1); - float charWidth = paint.measureText(ch); - if (widestCharWidth < charWidth) { - widestCharWidth = charWidth; - mWidestChar = ch; - } - } - } - updateTextSize(); - } - - private void updateTextSize() { - if (mCaptionLayout == null) return; - - // Calculate text size based on the max window size. - StringBuilder widestTextBuilder = new StringBuilder(); - int screenColumnCount = getScreenColumnCount(); - for (int i = 0; i < screenColumnCount; ++i) { - widestTextBuilder.append(mWidestChar); - } - String widestText = widestTextBuilder.toString(); - Paint paint = new Paint(); - paint.setTypeface(mCaptionStyleCompat.typeface); - float startFontSize = 0f; - float endFontSize = 255f; - Rect boundRect = new Rect(); - while (startFontSize < endFontSize) { - float testTextSize = (startFontSize + endFontSize) / 2f; - paint.setTextSize(testTextSize); - float width = paint.measureText(widestText); - paint.getTextBounds(widestText, 0, widestText.length(), boundRect); - float height = boundRect.height() + width - boundRect.width(); - // According to CEA-708B Section 9.13, the height of standard font size shouldn't taller - // than 1/15 of the height of the safe-title area, and the width shouldn't wider than - // 1/{@code getScreenColumnCount()} of the width of the safe-title area. - if (mCaptionLayout.getWidth() * 0.8f > width - && mCaptionLayout.getHeight() * 0.8f / MAX_ROW_COUNT > height) { - startFontSize = testTextSize + 0.01f; - } else { - endFontSize = testTextSize - 0.01f; - } - } - mTextSize = endFontSize * mFontScale; - paint.setTextSize(mTextSize); - float whiteSpaceWidth = paint.measureText(" "); - mSubtitleView.setWhiteSpaceWidth(whiteSpaceWidth); - mSubtitleView.setTextSize(mTextSize); - } - - private int getScreenColumnCount() { - float screenAspectRatio = (float) mCaptionLayout.getWidth() / mCaptionLayout.getHeight(); - boolean isWideAspectRationScreen = screenAspectRatio > WIDE_SCREEN_ASPECT_RATIO_THRESHOLD; - if (isKoreanLanguageTrack()) { - // Each korean character consumes two slots. - if (isWideAspectRationScreen || isWideAspectRatio()) { - return KR_MAX_COLUMN_COUNT_16_9 / 2; - } else { - return KR_MAX_COLUMN_COUNT_4_3 / 2; - } - } else { - if (isWideAspectRationScreen || isWideAspectRatio()) { - return US_MAX_COLUMN_COUNT_16_9; - } else { - return US_MAX_COLUMN_COUNT_4_3; - } - } - } - - public void removeFromCaptionView() { - if (mCaptionLayout != null) { - mCaptionLayout.removeViewFromSafeTitleArea(this); - mCaptionLayout.removeOnLayoutChangeListener(this); - mCaptionLayout = null; - } - } - - public void setText(String text) { - updateText(text, false); - } - - public void appendText(String text) { - updateText(text, true); - } - - public void clearText() { - mBuilder.clear(); - mSubtitleView.setText(""); - } - - private void updateText(String text, boolean appended) { - if (!appended) { - mBuilder.clear(); - } - if (text != null && text.length() > 0) { - int length = mBuilder.length(); - mBuilder.append(text); - for (CharacterStyle characterStyle : mCharacterStyles) { - mBuilder.setSpan(characterStyle, length, mBuilder.length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - String[] lines = TextUtils.split(mBuilder.toString(), "\n"); - - // Truncate text not to exceed the row limit. - // Plus one here since the range of the rows is [0, mRowLimit]. - int startRow = Math.max(0, lines.length - (mRowLimit + 1)); - String truncatedText = TextUtils.join("\n", Arrays.copyOfRange( - lines, startRow, lines.length)); - mBuilder.delete(0, mBuilder.length() - truncatedText.length()); - mCurrentTextRow = lines.length - startRow - 1; - - // Trim the buffer first then set text to {@link SubtitleView}. - int start = 0, last = mBuilder.length() - 1; - int end = last; - while ((start <= end) && (mBuilder.charAt(start) <= ' ')) { - ++start; - } - while (start - 1 >= 0 && start <= end && mBuilder.charAt(start - 1) != '\n') { - --start; - } - while ((end >= start) && (mBuilder.charAt(end) <= ' ')) { - --end; - } - if (start == 0 && end == last) { - mSubtitleView.setPrefixSpaces(getPrefixSpaces(mBuilder)); - mSubtitleView.setText(mBuilder); - } else { - SpannableStringBuilder trim = new SpannableStringBuilder(); - trim.append(mBuilder); - if (end < last) { - trim.delete(end + 1, last + 1); - } - if (start > 0) { - trim.delete(0, start); - } - mSubtitleView.setPrefixSpaces(getPrefixSpaces(trim)); - mSubtitleView.setText(trim); - } - } - - private static ArrayList<Integer> getPrefixSpaces(SpannableStringBuilder builder) { - ArrayList<Integer> prefixSpaces = new ArrayList<>(); - String[] lines = TextUtils.split(builder.toString(), "\n"); - for (String line : lines) { - int start = 0; - while (start < line.length() && line.charAt(start) <= ' ') { - start++; - } - prefixSpaces.add(start); - } - return prefixSpaces; - } - - public void setRowLimit(int rowLimit) { - if (rowLimit < 0) { - throw new IllegalArgumentException("A rowLimit should have a positive number"); - } - mRowLimit = rowLimit; - } - - private void setWindowStyle(int windowStyle) { - // TODO: Set other attributes of window style. Like fill opacity and fill color. - switch (windowStyle) { - case 2: - mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT; - mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT; - break; - case 3: - mWindowJustify = CaptionWindowAttr.JUSTIFY_CENTER; - mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT; - break; - case 4: - mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT; - mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT; - break; - case 5: - mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT; - mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT; - break; - case 6: - mWindowJustify = CaptionWindowAttr.JUSTIFY_CENTER; - mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT; - break; - case 7: - mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT; - mPrintDirection = CaptionWindowAttr.PRINT_TOP_TO_BOTTOM; - break; - default: - if (windowStyle != 0 && windowStyle != 1) { - Log.e(TAG, "Error predefined window style:" + windowStyle); - } - mWindowJustify = CaptionWindowAttr.JUSTIFY_LEFT; - mPrintDirection = CaptionWindowAttr.PRINT_LEFT_TO_RIGHT; - break; - } - } -} diff --git a/src/com/android/tv/tuner/cc/Cea708Parser.java b/src/com/android/tv/tuner/cc/Cea708Parser.java deleted file mode 100644 index d0f6cf11..00000000 --- a/src/com/android/tv/tuner/cc/Cea708Parser.java +++ /dev/null @@ -1,820 +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.cc; - -import android.os.SystemClock; -import android.support.annotation.IntDef; -import android.util.Log; -import android.util.SparseIntArray; - -import com.android.tv.tuner.data.Cea708Data; -import com.android.tv.tuner.data.Cea708Data.CaptionColor; -import com.android.tv.tuner.data.Cea708Data.CaptionEvent; -import com.android.tv.tuner.data.Cea708Data.CaptionPenAttr; -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.Cea708Data.CcPacket; -import com.android.tv.tuner.util.ByteArrayBuffer; - -import java.io.UnsupportedEncodingException; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.TreeSet; - -/** - * A class for parsing CEA-708, which is the standard for closed captioning for ATSC DTV. - * - * <p>ATSC DTV closed caption data are carried on picture user data of video streams. - * This class starts to parse from picture user data payload, so extraction process of user_data - * from video streams is up to outside of this code. - * - * <p>There are 4 steps to decode user_data to provide closed caption services. - * - * <h3>Step 1. user_data -> CcPacket ({@link #parseClosedCaption} method)</h3> - * - * <p>First, user_data consists of cc_data packets, which are 3-byte segments. Here, CcPacket is a - * collection of cc_data packets in a frame along with same presentation timestamp. Because cc_data - * packets must be reassembled in the frame display order, CcPackets are reordered. - * - * <h3>Step 2. CcPacket -> DTVCC packet ({@link #parseCcPacket} method)</h3> - * - * <p>Each cc_data packet has a one byte for declaring a type of itself and data validity, and the - * subsequent two bytes for input data of a DTVCC packet. There are 4 types for cc_data packet. - * We're interested in DTVCC_PACKET_START(type 3) and DTVCC_PACKET_DATA(type 2). Each DTVCC packet - * begins with DTVCC_PACKET_START(type 3) and the following cc_data packets which has - * DTVCC_PACKET_DATA(type 2) are appended into the DTVCC packet being assembled. - * - * <h3>Step 3. DTVCC packet -> Service Blocks ({@link #parseDtvCcPacket} method)</h3> - * - * <p>A DTVCC packet consists of multiple service blocks. Each service block represents a caption - * track and has a service number, which ranges from 1 to 63, that denotes caption track identity. - * In here, we listen at most one chosen caption track by {@link #mListenServiceNumber}. - * Otherwise, just skip the other service blocks. - * - * <h3>Step 4. Interpreting Service Block Data ({@link #parseServiceBlockData}, {@code parseXX}, - * and {@link #parseExt1} methods)</h3> - * - * <p>Service block data is actual caption stream. it looks similar to telnet. It uses most parts of - * ASCII table and consists of specially defined commands and some ASCII control codes which work - * in a behavior slightly different from their original purpose. ASCII control codes and caption - * commands are explicit instructions that control the state of a closed caption service and the - * other ASCII and text codes are implicit instructions that send their characters to buffer. - * - * <p>There are 4 main code groups and 4 extended code groups. Both the range of code groups are the - * same as the range of a byte. - * - * <p>4 main code groups: C0, C1, G0, G1 - * <br>4 extended code groups: C2, C3, G2, G3 - * - * <p>Each code group has its own handle method. For example, {@link #parseC0} handles C0 code group - * and so on. And {@link #parseServiceBlockData} method maps a stream on the main code groups while - * {@link #parseExt1} method maps on the extended code groups. - * - * <p>The main code groups: - * <ul> - * <li>C0 - contains modified ASCII control codes. It is not intended by CEA-708 but Korea TTA - * standard for ATSC CC uses P16 character heavily, which is unclear entity in CEA-708 doc, - * even for the alphanumeric characters instead of ASCII characters.</li> - * <li>C1 - contains the caption commands. There are 3 categories of a caption command.</li> - * <ul> - * <li>Window commands: The window commands control a caption window which is addressable area being - * with in the Safe title area. (CWX, CLW, DSW, HDW, TGW, DLW, SWA, DFX)</li> - * <li>Pen commands: Th pen commands control text style and location. (SPA, SPC, SPL)</li> - * <li>Job commands: The job commands make a delay and recover from the delay. (DLY, DLC, RST)</li> - * </ul> - * <li>G0 - same as printable ASCII character set except music note character.</li> - * <li>G1 - same as ISO 8859-1 Latin 1 character set.</li> - * </ul> - * <p>Most of the extended code groups are being skipped. - * - */ -public class Cea708Parser { - private static final String TAG = "Cea708Parser"; - private static final boolean DEBUG = false; - - // According to CEA-708B, the maximum value of closed caption bandwidth is 9600bps. - private static final int MAX_ALLOCATED_SIZE = 9600 / 8; - private static final String MUSIC_NOTE_CHAR = new String( - "\u266B".getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); - - // The following values are denoting the type of closed caption data. - // See CEA-708B section 4.4.1. - private static final int CC_TYPE_DTVCC_PACKET_START = 3; - private static final int CC_TYPE_DTVCC_PACKET_DATA = 2; - - // The following values are defined in CEA-708B Figure 4 and 6. - private static final int DTVCC_MAX_PACKET_SIZE = 64; - private static final int DTVCC_PACKET_SIZE_SCALE_FACTOR = 2; - private static final int DTVCC_EXTENDED_SERVICE_NUMBER_POINT = 7; - - // The following values are for seeking closed caption tracks. - private static final int DISCOVERY_PERIOD_MS = 10000; // 10 sec - private static final int DISCOVERY_NUM_BYTES_THRESHOLD = 10; // 10 bytes - private static final int DISCOVERY_CC_SERVICE_NUMBER_START = 1; // CC1 - private static final int DISCOVERY_CC_SERVICE_NUMBER_END = 4; // CC4 - - private final ByteArrayBuffer mDtvCcPacket = new ByteArrayBuffer(MAX_ALLOCATED_SIZE); - private final TreeSet<CcPacket> mCcPackets = new TreeSet<>(); - private final StringBuffer mBuffer = new StringBuffer(); - private final SparseIntArray mDiscoveredNumBytes = new SparseIntArray(); // per service number - private long mLastDiscoveryLaunchedMs = SystemClock.elapsedRealtime(); - 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() { - @Override - public void emitEvent(CaptionEvent event) { - // do nothing - } - - @Override - public void discoverServiceNumber(int serviceNumber) { - // do nothing - } - }; - - /** - * {@link Cea708Parser} emits caption event of three different types. - * {@link OnCea708ParserListener#emitEvent} is invoked with the parameter - * {@link CaptionEvent} to pass all the results to an observer of the decoding process. - * - * <p>{@link CaptionEvent#type} determines the type of the result and - * {@link CaptionEvent#obj} contains the output value of a caption event. - * The observer must do the casting to the corresponding type. - * - * <ul><li>{@code CAPTION_EMIT_TYPE_BUFFER}: Passes a caption text buffer to a observer. - * {@code obj} must be of {@link String}.</li> - * - * <li>{@code CAPTION_EMIT_TYPE_CONTROL}: Passes a caption character control code to a observer. - * {@code obj} must be of {@link Character}.</li> - * - * <li>{@code CAPTION_EMIT_TYPE_CLEAR_COMMAND}: Passes a clear command to a observer. - * {@code obj} must be {@code NULL}.</li></ul> - */ - @IntDef({CAPTION_EMIT_TYPE_BUFFER, CAPTION_EMIT_TYPE_CONTROL, CAPTION_EMIT_TYPE_COMMAND_CWX, - CAPTION_EMIT_TYPE_COMMAND_CLW, CAPTION_EMIT_TYPE_COMMAND_DSW, CAPTION_EMIT_TYPE_COMMAND_HDW, - CAPTION_EMIT_TYPE_COMMAND_TGW, CAPTION_EMIT_TYPE_COMMAND_DLW, CAPTION_EMIT_TYPE_COMMAND_DLY, - CAPTION_EMIT_TYPE_COMMAND_DLC, CAPTION_EMIT_TYPE_COMMAND_RST, CAPTION_EMIT_TYPE_COMMAND_SPA, - CAPTION_EMIT_TYPE_COMMAND_SPC, CAPTION_EMIT_TYPE_COMMAND_SPL, CAPTION_EMIT_TYPE_COMMAND_SWA, - CAPTION_EMIT_TYPE_COMMAND_DFX}) - @Retention(RetentionPolicy.SOURCE) - public @interface CaptionEmitType {} - public static final int CAPTION_EMIT_TYPE_BUFFER = 1; - public static final int CAPTION_EMIT_TYPE_CONTROL = 2; - public static final int CAPTION_EMIT_TYPE_COMMAND_CWX = 3; - public static final int CAPTION_EMIT_TYPE_COMMAND_CLW = 4; - public static final int CAPTION_EMIT_TYPE_COMMAND_DSW = 5; - public static final int CAPTION_EMIT_TYPE_COMMAND_HDW = 6; - public static final int CAPTION_EMIT_TYPE_COMMAND_TGW = 7; - public static final int CAPTION_EMIT_TYPE_COMMAND_DLW = 8; - public static final int CAPTION_EMIT_TYPE_COMMAND_DLY = 9; - public static final int CAPTION_EMIT_TYPE_COMMAND_DLC = 10; - public static final int CAPTION_EMIT_TYPE_COMMAND_RST = 11; - public static final int CAPTION_EMIT_TYPE_COMMAND_SPA = 12; - public static final int CAPTION_EMIT_TYPE_COMMAND_SPC = 13; - public static final int CAPTION_EMIT_TYPE_COMMAND_SPL = 14; - public static final int CAPTION_EMIT_TYPE_COMMAND_SWA = 15; - public static final int CAPTION_EMIT_TYPE_COMMAND_DFX = 16; - - public interface OnCea708ParserListener { - void emitEvent(CaptionEvent event); - void discoverServiceNumber(int serviceNumber); - } - - public void setListener(OnCea708ParserListener listener) { - if (listener != null) { - mListener = listener; - } - } - - public void clear() { - mDtvCcPacket.clear(); - mCcPackets.clear(); - mBuffer.setLength(0); - mDiscoveredNumBytes.clear(); - mCommand = 0; - mDtvCcPacking = false; - } - - public void setListenServiceNumber(int serviceNumber) { - mListenServiceNumber = serviceNumber; - } - - private void emitCaptionEvent(CaptionEvent captionEvent) { - // Emit the existing string buffer before a new event is arrived. - emitCaptionBuffer(); - mListener.emitEvent(captionEvent); - } - - private void emitCaptionBuffer() { - if (mBuffer.length() > 0) { - mListener.emitEvent(new CaptionEvent(CAPTION_EMIT_TYPE_BUFFER, mBuffer.toString())); - mBuffer.setLength(0); - } - } - - // Step 1. user_data -> CcPacket ({@link #parseClosedCaption} method) - public void parseClosedCaption(ByteBuffer data, long framePtsUs) { - int ccCount = data.limit() / 3; - byte[] ccBytes = new byte[3 * ccCount]; - for (int i = 0; i < 3 * ccCount; i++) { - ccBytes[i] = data.get(i); - } - CcPacket ccPacket = new CcPacket(ccBytes, ccCount, framePtsUs); - mCcPackets.add(ccPacket); - } - - public boolean processClosedCaptions(long framePtsUs) { - // To get the sorted cc packets that have lower frame pts than current frame pts, - // the following offset divides off the lower side of the packets. - CcPacket offsetPacket = new CcPacket(new byte[0], 0, framePtsUs); - offsetPacket = mCcPackets.lower(offsetPacket); - boolean processed = false; - if (offsetPacket != null) { - while (!mCcPackets.isEmpty() && offsetPacket.compareTo(mCcPackets.first()) >= 0) { - CcPacket packet = mCcPackets.pollFirst(); - parseCcPacket(packet); - processed = true; - } - } - return processed; - } - - // Step 2. CcPacket -> DTVCC packet ({@link #parseCcPacket} method) - private void parseCcPacket(CcPacket ccPacket) { - // For the details of cc packet, see ATSC TSG-676 - Table A8. - byte[] bytes = ccPacket.bytes; - int pos = 0; - for (int i = 0; i < ccPacket.ccCount; ++i) { - boolean ccValid = (bytes[pos] & 0x04) != 0; - int ccType = bytes[pos] & 0x03; - - // The dtvcc should be considered complete: - // - if either ccValid is set and ccType is 3 - // - or ccValid is clear and ccType is 2 or 3. - if (ccValid) { - if (ccType == CC_TYPE_DTVCC_PACKET_START) { - if (mDtvCcPacking) { - parseDtvCcPacket(mDtvCcPacket.buffer(), mDtvCcPacket.length()); - mDtvCcPacket.clear(); - } - mDtvCcPacking = true; - mDtvCcPacket.append(bytes[pos + 1]); - mDtvCcPacket.append(bytes[pos + 2]); - } else if (mDtvCcPacking && ccType == CC_TYPE_DTVCC_PACKET_DATA) { - mDtvCcPacket.append(bytes[pos + 1]); - mDtvCcPacket.append(bytes[pos + 2]); - } - } else { - if ((ccType == CC_TYPE_DTVCC_PACKET_START || ccType == CC_TYPE_DTVCC_PACKET_DATA) - && mDtvCcPacking) { - mDtvCcPacking = false; - parseDtvCcPacket(mDtvCcPacket.buffer(), mDtvCcPacket.length()); - mDtvCcPacket.clear(); - } - } - pos += 3; - } - } - - // Step 3. DTVCC packet -> Service Blocks ({@link #parseDtvCcPacket} method) - private void parseDtvCcPacket(byte[] data, int limit) { - // For the details of DTVCC packet, see CEA-708B Figure 4. - int pos = 0; - int packetSize = data[pos] & 0x3f; - if (packetSize == 0) { - packetSize = DTVCC_MAX_PACKET_SIZE; - } - int calculatedPacketSize = packetSize * DTVCC_PACKET_SIZE_SCALE_FACTOR; - if (limit != calculatedPacketSize) { - return; - } - ++pos; - int len = pos + calculatedPacketSize; - while (pos < len) { - // For the details of Service Block, see CEA-708B Figure 5 and 6. - int serviceNumber = (data[pos] & 0xe0) >> 5; - int blockSize = data[pos] & 0x1f; - ++pos; - if (serviceNumber == DTVCC_EXTENDED_SERVICE_NUMBER_POINT) { - serviceNumber = (data[pos] & 0x3f); - ++pos; - - // Return if invalid service number - if (serviceNumber < DTVCC_EXTENDED_SERVICE_NUMBER_POINT) { - return; - } - } - if (pos + blockSize > limit) { - return; - } - - // Send parsed service number in order to find unveiled closed caption tracks which - // are not specified in any ATSC PSIP sections. Since some broadcasts send empty closed - // caption tracks, it detects the proper closed caption tracks by counting the number of - // bytes sent with the same service number during a discovery period. - // The viewer in most TV sets chooses between CC1, CC2, CC3, CC4 to view different - // language captions. Therefore, only CC1, CC2, CC3, CC4 are allowed to be reported. - if (blockSize > 0 && serviceNumber >= DISCOVERY_CC_SERVICE_NUMBER_START - && serviceNumber <= DISCOVERY_CC_SERVICE_NUMBER_END) { - mDiscoveredNumBytes.put( - serviceNumber, blockSize + mDiscoveredNumBytes.get(serviceNumber, 0)); - } - 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(); - mLastDiscoveryLaunchedMs = SystemClock.elapsedRealtime(); - } - - // Skip current service block if either there is no block data or the service number - // is not same as listening service number. - if (blockSize == 0 || serviceNumber != mListenServiceNumber) { - pos += blockSize; - continue; - } - - // From this point, starts to read DTVCC coding layer. - // First, identify code groups, which is defined in CEA-708B Section 7.1. - int blockLimit = pos + blockSize; - while (pos < blockLimit) { - pos = parseServiceBlockData(data, pos); - } - - // Emit the buffer after reading codes. - emitCaptionBuffer(); - pos = blockLimit; - } - } - - // Step 4. Main code groups - private int parseServiceBlockData(byte[] data, int pos) { - // For the details of the ranges of DTVCC code groups, see CEA-708B Table 6. - mCommand = data[pos] & 0xff; - ++pos; - if (mCommand == Cea708Data.CODE_C0_EXT1) { - pos = parseExt1(data, pos); - } else if (mCommand >= Cea708Data.CODE_C0_RANGE_START - && mCommand <= Cea708Data.CODE_C0_RANGE_END) { - pos = parseC0(data, pos); - } else if (mCommand >= Cea708Data.CODE_C1_RANGE_START - && mCommand <= Cea708Data.CODE_C1_RANGE_END) { - pos = parseC1(data, pos); - } else if (mCommand >= Cea708Data.CODE_G0_RANGE_START - && mCommand <= Cea708Data.CODE_G0_RANGE_END) { - pos = parseG0(data, pos); - } else if (mCommand >= Cea708Data.CODE_G1_RANGE_START - && mCommand <= Cea708Data.CODE_G1_RANGE_END) { - pos = parseG1(data, pos); - } - return pos; - } - - private int parseC0(byte[] data, int pos) { - // For the details of C0 code group, see CEA-708B Section 7.4.1. - // CL Group: C0 Subset of ASCII Control codes - if (mCommand >= Cea708Data.CODE_C0_SKIP2_RANGE_START - && mCommand <= Cea708Data.CODE_C0_SKIP2_RANGE_END) { - if (mCommand == Cea708Data.CODE_C0_P16) { - // TODO : P16 escapes next two bytes for the large character maps.(no standard rule) - // TODO : For korea broadcasting, express whole letters by using this. - try { - if (data[pos] == 0) { - mBuffer.append((char) data[pos + 1]); - } else { - String value = new String( - Arrays.copyOfRange(data, pos, pos + 2), - "EUC-KR"); - mBuffer.append(value); - } - } catch (UnsupportedEncodingException e) { - Log.e(TAG, "P16 Code - Could not find supported encoding", e); - } - } - pos += 2; - } else if (mCommand >= Cea708Data.CODE_C0_SKIP1_RANGE_START - && mCommand <= Cea708Data.CODE_C0_SKIP1_RANGE_END) { - ++pos; - } else { - // NUL, BS, FF, CR interpreted as they are in ASCII control codes. - // HCR moves the pen location to th beginning of the current line and deletes contents. - // FF clears the screen and moves the pen location to (0,0). - // ETX is the NULL command which is used to flush text to the current window when no - // other command is pending. - switch (mCommand) { - case Cea708Data.CODE_C0_NUL: - break; - case Cea708Data.CODE_C0_ETX: - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand)); - break; - case Cea708Data.CODE_C0_BS: - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand)); - break; - case Cea708Data.CODE_C0_FF: - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand)); - break; - case Cea708Data.CODE_C0_CR: - mBuffer.append('\n'); - break; - case Cea708Data.CODE_C0_HCR: - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_CONTROL, (char) mCommand)); - break; - default: - break; - } - } - return pos; - } - - private int parseC1(byte[] data, int pos) { - // For the details of C1 code group, see CEA-708B Section 8.10. - // CR Group: C1 Caption Control Codes - switch (mCommand) { - case Cea708Data.CODE_C1_CW0: - case Cea708Data.CODE_C1_CW1: - case Cea708Data.CODE_C1_CW2: - case Cea708Data.CODE_C1_CW3: - case Cea708Data.CODE_C1_CW4: - case Cea708Data.CODE_C1_CW5: - case Cea708Data.CODE_C1_CW6: - case Cea708Data.CODE_C1_CW7: { - // SetCurrentWindow0-7 - int windowId = mCommand - Cea708Data.CODE_C1_CW0; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_CWX, windowId)); - if (DEBUG) { - Log.d(TAG, String.format("CaptionCommand CWX windowId: %d", windowId)); - } - break; - } - - case Cea708Data.CODE_C1_CLW: { - // ClearWindows - int windowBitmap = data[pos] & 0xff; - ++pos; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_CLW, windowBitmap)); - if (DEBUG) { - Log.d(TAG, String.format("CaptionCommand CLW windowBitmap: %d", windowBitmap)); - } - break; - } - - case Cea708Data.CODE_C1_DSW: { - // DisplayWindows - int windowBitmap = data[pos] & 0xff; - ++pos; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DSW, windowBitmap)); - if (DEBUG) { - Log.d(TAG, String.format("CaptionCommand DSW windowBitmap: %d", windowBitmap)); - } - break; - } - - case Cea708Data.CODE_C1_HDW: { - // HideWindows - int windowBitmap = data[pos] & 0xff; - ++pos; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_HDW, windowBitmap)); - if (DEBUG) { - Log.d(TAG, String.format("CaptionCommand HDW windowBitmap: %d", windowBitmap)); - } - break; - } - - case Cea708Data.CODE_C1_TGW: { - // ToggleWindows - int windowBitmap = data[pos] & 0xff; - ++pos; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_TGW, windowBitmap)); - if (DEBUG) { - Log.d(TAG, String.format("CaptionCommand TGW windowBitmap: %d", windowBitmap)); - } - break; - } - - case Cea708Data.CODE_C1_DLW: { - // DeleteWindows - int windowBitmap = data[pos] & 0xff; - ++pos; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DLW, windowBitmap)); - if (DEBUG) { - Log.d(TAG, String.format("CaptionCommand DLW windowBitmap: %d", windowBitmap)); - } - break; - } - - case Cea708Data.CODE_C1_DLY: { - // Delay - int tenthsOfSeconds = data[pos] & 0xff; - ++pos; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DLY, tenthsOfSeconds)); - if (DEBUG) { - Log.d(TAG, String.format("CaptionCommand DLY %d tenths of seconds", - tenthsOfSeconds)); - } - break; - } - case Cea708Data.CODE_C1_DLC: { - // DelayCancel - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DLC, null)); - if (DEBUG) { - Log.d(TAG, "CaptionCommand DLC"); - } - break; - } - - case Cea708Data.CODE_C1_RST: { - // Reset - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_RST, null)); - if (DEBUG) { - Log.d(TAG, "CaptionCommand RST"); - } - break; - } - - case Cea708Data.CODE_C1_SPA: { - // SetPenAttributes - int textTag = (data[pos] & 0xf0) >> 4; - int penSize = data[pos] & 0x03; - int penOffset = (data[pos] & 0x0c) >> 2; - boolean italic = (data[pos + 1] & 0x80) != 0; - boolean underline = (data[pos + 1] & 0x40) != 0; - int edgeType = (data[pos + 1] & 0x38) >> 3; - int fontTag = data[pos + 1] & 0x7; - pos += 2; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_SPA, - new CaptionPenAttr(penSize, penOffset, textTag, fontTag, edgeType, - underline, italic))); - if (DEBUG) { - Log.d(TAG, String.format( - "CaptionCommand SPA penSize: %d, penOffset: %d, textTag: %d, " - + "fontTag: %d, edgeType: %d, underline: %s, italic: %s", - penSize, penOffset, textTag, fontTag, edgeType, underline, italic)); - } - break; - } - - case Cea708Data.CODE_C1_SPC: { - // SetPenColor - int opacity = (data[pos] & 0xc0) >> 6; - int red = (data[pos] & 0x30) >> 4; - int green = (data[pos] & 0x0c) >> 2; - int blue = data[pos] & 0x03; - CaptionColor foregroundColor = new CaptionColor(opacity, red, green, blue); - ++pos; - opacity = (data[pos] & 0xc0) >> 6; - red = (data[pos] & 0x30) >> 4; - green = (data[pos] & 0x0c) >> 2; - blue = data[pos] & 0x03; - CaptionColor backgroundColor = new CaptionColor(opacity, red, green, blue); - ++pos; - red = (data[pos] & 0x30) >> 4; - green = (data[pos] & 0x0c) >> 2; - blue = data[pos] & 0x03; - CaptionColor edgeColor = new CaptionColor( - CaptionColor.OPACITY_SOLID, red, green, blue); - ++pos; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_SPC, - new CaptionPenColor(foregroundColor, backgroundColor, edgeColor))); - if (DEBUG) { - Log.d(TAG, String.format( - "CaptionCommand SPC foregroundColor %s backgroundColor %s edgeColor %s", - foregroundColor, backgroundColor, edgeColor)); - } - break; - } - - case Cea708Data.CODE_C1_SPL: { - // SetPenLocation - // column is normally 0-31 for 4:3 formats, and 0-41 for 16:9 formats - int row = data[pos] & 0x0f; - int column = data[pos + 1] & 0x3f; - pos += 2; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_SPL, - new CaptionPenLocation(row, column))); - if (DEBUG) { - Log.d(TAG, String.format("CaptionCommand SPL row: %d, column: %d", - row, column)); - } - break; - } - - case Cea708Data.CODE_C1_SWA: { - // SetWindowAttributes - int opacity = (data[pos] & 0xc0) >> 6; - int red = (data[pos] & 0x30) >> 4; - int green = (data[pos] & 0x0c) >> 2; - int blue = data[pos] & 0x03; - CaptionColor fillColor = new CaptionColor(opacity, red, green, blue); - int borderType = (data[pos + 1] & 0xc0) >> 6 | (data[pos + 2] & 0x80) >> 5; - red = (data[pos + 1] & 0x30) >> 4; - green = (data[pos + 1] & 0x0c) >> 2; - blue = data[pos + 1] & 0x03; - CaptionColor borderColor = new CaptionColor( - CaptionColor.OPACITY_SOLID, red, green, blue); - boolean wordWrap = (data[pos + 2] & 0x40) != 0; - int printDirection = (data[pos + 2] & 0x30) >> 4; - int scrollDirection = (data[pos + 2] & 0x0c) >> 2; - int justify = (data[pos + 2] & 0x03); - int effectSpeed = (data[pos + 3] & 0xf0) >> 4; - int effectDirection = (data[pos + 3] & 0x0c) >> 2; - int displayEffect = data[pos + 3] & 0x3; - pos += 4; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_SWA, - new CaptionWindowAttr(fillColor, borderColor, borderType, wordWrap, - printDirection, scrollDirection, justify, - effectDirection, effectSpeed, displayEffect))); - if (DEBUG) { - Log.d(TAG, String.format( - "CaptionCommand SWA fillColor: %s, borderColor: %s, borderType: %d" - + "wordWrap: %s, printDirection: %d, scrollDirection: %d, " - + "justify: %s, effectDirection: %d, effectSpeed: %d, " - + "displayEffect: %d", - fillColor, borderColor, borderType, wordWrap, printDirection, - scrollDirection, justify, effectDirection, effectSpeed, displayEffect)); - } - break; - } - - case Cea708Data.CODE_C1_DF0: - case Cea708Data.CODE_C1_DF1: - case Cea708Data.CODE_C1_DF2: - case Cea708Data.CODE_C1_DF3: - case Cea708Data.CODE_C1_DF4: - case Cea708Data.CODE_C1_DF5: - case Cea708Data.CODE_C1_DF6: - case Cea708Data.CODE_C1_DF7: { - // DefineWindow0-7 - int windowId = mCommand - Cea708Data.CODE_C1_DF0; - boolean visible = (data[pos] & 0x20) != 0; - boolean rowLock = (data[pos] & 0x10) != 0; - boolean columnLock = (data[pos] & 0x08) != 0; - int priority = data[pos] & 0x07; - boolean relativePositioning = (data[pos + 1] & 0x80) != 0; - int anchorVertical = data[pos + 1] & 0x7f; - int anchorHorizontal = data[pos + 2] & 0xff; - int anchorId = (data[pos + 3] & 0xf0) >> 4; - int rowCount = data[pos + 3] & 0x0f; - int columnCount = data[pos + 4] & 0x3f; - int windowStyle = (data[pos + 5] & 0x38) >> 3; - int penStyle = data[pos + 5] & 0x07; - pos += 6; - emitCaptionEvent(new CaptionEvent(CAPTION_EMIT_TYPE_COMMAND_DFX, - new CaptionWindow(windowId, visible, rowLock, columnLock, priority, - relativePositioning, anchorVertical, anchorHorizontal, anchorId, - rowCount, columnCount, penStyle, windowStyle))); - if (DEBUG) { - Log.d(TAG, String.format( - "CaptionCommand DFx windowId: %d, priority: %d, columnLock: %s, " - + "rowLock: %s, visible: %s, anchorVertical: %d, " - + "relativePositioning: %s, anchorHorizontal: %d, " - + "rowCount: %d, anchorId: %d, columnCount: %d, penStyle: %d, " - + "windowStyle: %d", - windowId, priority, columnLock, rowLock, visible, anchorVertical, - relativePositioning, anchorHorizontal, rowCount, anchorId, columnCount, - penStyle, windowStyle)); - } - break; - } - - default: - break; - } - return pos; - } - - private int parseG0(byte[] data, int pos) { - // For the details of G0 code group, see CEA-708B Section 7.4.3. - // GL Group: G0 Modified version of ANSI X3.4 Printable Character Set (ASCII) - if (mCommand == Cea708Data.CODE_G0_MUSICNOTE) { - // Music note. - mBuffer.append(MUSIC_NOTE_CHAR); - } else { - // Put ASCII code into buffer. - mBuffer.append((char) mCommand); - } - return pos; - } - - private int parseG1(byte[] data, int pos) { - // For the details of G0 code group, see CEA-708B Section 7.4.4. - // GR Group: G1 ISO 8859-1 Latin 1 Characters - // Put ASCII Extended character set into buffer. - mBuffer.append((char) mCommand); - return pos; - } - - // Step 4. Extended code groups - private int parseExt1(byte[] data, int pos) { - // For the details of EXT1 code group, see CEA-708B Section 7.2. - mCommand = data[pos] & 0xff; - ++pos; - if (mCommand >= Cea708Data.CODE_C2_RANGE_START - && mCommand <= Cea708Data.CODE_C2_RANGE_END) { - pos = parseC2(data, pos); - } else if (mCommand >= Cea708Data.CODE_C3_RANGE_START - && mCommand <= Cea708Data.CODE_C3_RANGE_END) { - pos = parseC3(data, pos); - } else if (mCommand >= Cea708Data.CODE_G2_RANGE_START - && mCommand <= Cea708Data.CODE_G2_RANGE_END) { - pos = parseG2(data, pos); - } else if (mCommand >= Cea708Data.CODE_G3_RANGE_START - && mCommand <= Cea708Data.CODE_G3_RANGE_END) { - pos = parseG3(data ,pos); - } - return pos; - } - - private int parseC2(byte[] data, int pos) { - // For the details of C2 code group, see CEA-708B Section 7.4.7. - // Extended Miscellaneous Control Codes - // C2 Table : No commands as of CEA-708B. A decoder must skip. - if (mCommand >= Cea708Data.CODE_C2_SKIP0_RANGE_START - && mCommand <= Cea708Data.CODE_C2_SKIP0_RANGE_END) { - // Do nothing. - } else if (mCommand >= Cea708Data.CODE_C2_SKIP1_RANGE_START - && mCommand <= Cea708Data.CODE_C2_SKIP1_RANGE_END) { - ++pos; - } else if (mCommand >= Cea708Data.CODE_C2_SKIP2_RANGE_START - && mCommand <= Cea708Data.CODE_C2_SKIP2_RANGE_END) { - pos += 2; - } else if (mCommand >= Cea708Data.CODE_C2_SKIP3_RANGE_START - && mCommand <= Cea708Data.CODE_C2_SKIP3_RANGE_END) { - pos += 3; - } - return pos; - } - - private int parseC3(byte[] data, int pos) { - // For the details of C3 code group, see CEA-708B Section 7.4.8. - // Extended Control Code Set 2 - // C3 Table : No commands as of CEA-708B. A decoder must skip. - if (mCommand >= Cea708Data.CODE_C3_SKIP4_RANGE_START - && mCommand <= Cea708Data.CODE_C3_SKIP4_RANGE_END) { - pos += 4; - } else if (mCommand >= Cea708Data.CODE_C3_SKIP5_RANGE_START - && mCommand <= Cea708Data.CODE_C3_SKIP5_RANGE_END) { - pos += 5; - } - return pos; - } - - private int parseG2(byte[] data, int pos) { - // For the details of C3 code group, see CEA-708B Section 7.4.5. - // Extended Control Code Set 1(G2 Table) - switch (mCommand) { - case Cea708Data.CODE_G2_TSP: - // TODO : TSP is the Transparent space - break; - case Cea708Data.CODE_G2_NBTSP: - // TODO : NBTSP is Non-Breaking Transparent Space. - break; - case Cea708Data.CODE_G2_BLK: - // TODO : BLK indicates a solid block which fills the entire character block - // TODO : with a solid foreground color. - break; - default: - break; - } - return pos; - } - - private int parseG3(byte[] data, int pos) { - // For the details of C3 code group, see CEA-708B Section 7.4.6. - // Future characters and icons(G3 Table) - if (mCommand == Cea708Data.CODE_G3_CC) { - // TODO : [CC] icon with square corners - } - - // Do nothing - return pos; - } -} diff --git a/src/com/android/tv/tuner/data/Cea708Data.java b/src/com/android/tv/tuner/data/Cea708Data.java deleted file mode 100644 index 6350d63c..00000000 --- a/src/com/android/tv/tuner/data/Cea708Data.java +++ /dev/null @@ -1,320 +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.data; - -import com.android.tv.tuner.cc.Cea708Parser; - -import android.graphics.Color; -import android.support.annotation.NonNull; - -/** - * Collection of CEA-708 structures. - */ -public class Cea708Data { - - private Cea708Data() { - } - - // According to CEA-708B, the range of valid service number is between 1 and 63. - public static final int EMPTY_SERVICE_NUMBER = 0; - - // For the details of the ranges of DTVCC code groups, see CEA-708B Table 6. - public static final int CODE_C0_RANGE_START = 0x00; - public static final int CODE_C0_RANGE_END = 0x1f; - public static final int CODE_C1_RANGE_START = 0x80; - public static final int CODE_C1_RANGE_END = 0x9f; - public static final int CODE_G0_RANGE_START = 0x20; - public static final int CODE_G0_RANGE_END = 0x7f; - public static final int CODE_G1_RANGE_START = 0xa0; - public static final int CODE_G1_RANGE_END = 0xff; - public static final int CODE_C2_RANGE_START = 0x00; - public static final int CODE_C2_RANGE_END = 0x1f; - public static final int CODE_C3_RANGE_START = 0x80; - public static final int CODE_C3_RANGE_END = 0x9f; - public static final int CODE_G2_RANGE_START = 0x20; - public static final int CODE_G2_RANGE_END = 0x7f; - public static final int CODE_G3_RANGE_START = 0xa0; - public static final int CODE_G3_RANGE_END = 0xff; - - // The following ranges are defined in CEA-708B Section 7.4.1. - public static final int CODE_C0_SKIP2_RANGE_START = 0x18; - public static final int CODE_C0_SKIP2_RANGE_END = 0x1f; - public static final int CODE_C0_SKIP1_RANGE_START = 0x10; - public static final int CODE_C0_SKIP1_RANGE_END = 0x17; - - // The following ranges are defined in CEA-708B Section 7.4.7. - public static final int CODE_C2_SKIP0_RANGE_START = 0x00; - public static final int CODE_C2_SKIP0_RANGE_END = 0x07; - public static final int CODE_C2_SKIP1_RANGE_START = 0x08; - public static final int CODE_C2_SKIP1_RANGE_END = 0x0f; - public static final int CODE_C2_SKIP2_RANGE_START = 0x10; - public static final int CODE_C2_SKIP2_RANGE_END = 0x17; - public static final int CODE_C2_SKIP3_RANGE_START = 0x18; - public static final int CODE_C2_SKIP3_RANGE_END = 0x1f; - - // The following ranges are defined in CEA-708B Section 7.4.8. - public static final int CODE_C3_SKIP4_RANGE_START = 0x80; - public static final int CODE_C3_SKIP4_RANGE_END = 0x87; - public static final int CODE_C3_SKIP5_RANGE_START = 0x88; - public static final int CODE_C3_SKIP5_RANGE_END = 0x8f; - - // The following values are the special characters of CEA-708 spec. - public static final int CODE_C0_NUL = 0x00; - public static final int CODE_C0_ETX = 0x03; - public static final int CODE_C0_BS = 0x08; - public static final int CODE_C0_FF = 0x0c; - public static final int CODE_C0_CR = 0x0d; - public static final int CODE_C0_HCR = 0x0e; - public static final int CODE_C0_EXT1 = 0x10; - public static final int CODE_C0_P16 = 0x18; - public static final int CODE_G0_MUSICNOTE = 0x7f; - public static final int CODE_G2_TSP = 0x20; - public static final int CODE_G2_NBTSP = 0x21; - public static final int CODE_G2_BLK = 0x30; - public static final int CODE_G3_CC = 0xa0; - - // The following values are the command bits of CEA-708 spec. - public static final int CODE_C1_CW0 = 0x80; - public static final int CODE_C1_CW1 = 0x81; - public static final int CODE_C1_CW2 = 0x82; - public static final int CODE_C1_CW3 = 0x83; - public static final int CODE_C1_CW4 = 0x84; - public static final int CODE_C1_CW5 = 0x85; - public static final int CODE_C1_CW6 = 0x86; - public static final int CODE_C1_CW7 = 0x87; - public static final int CODE_C1_CLW = 0x88; - public static final int CODE_C1_DSW = 0x89; - public static final int CODE_C1_HDW = 0x8a; - public static final int CODE_C1_TGW = 0x8b; - public static final int CODE_C1_DLW = 0x8c; - public static final int CODE_C1_DLY = 0x8d; - public static final int CODE_C1_DLC = 0x8e; - public static final int CODE_C1_RST = 0x8f; - public static final int CODE_C1_SPA = 0x90; - public static final int CODE_C1_SPC = 0x91; - public static final int CODE_C1_SPL = 0x92; - public static final int CODE_C1_SWA = 0x97; - public static final int CODE_C1_DF0 = 0x98; - public static final int CODE_C1_DF1 = 0x99; - public static final int CODE_C1_DF2 = 0x9a; - public static final int CODE_C1_DF3 = 0x9b; - public static final int CODE_C1_DF4 = 0x9c; - public static final int CODE_C1_DF5 = 0x9d; - public static final int CODE_C1_DF6 = 0x9e; - public static final int CODE_C1_DF7 = 0x9f; - - public static class CcPacket implements Comparable<CcPacket> { - public final byte[] bytes; - public final int ccCount; - public final long pts; - - public CcPacket(byte[] bytes, int ccCount, long pts) { - this.bytes = bytes; - this.ccCount = ccCount; - this.pts = pts; - } - - @Override - public int compareTo(@NonNull CcPacket another) { - return Long.compare(pts, another.pts); - } - } - - /** - * CEA-708B-specific color. - */ - public static class CaptionColor { - public static final int OPACITY_SOLID = 0; - public static final int OPACITY_FLASH = 1; - public static final int OPACITY_TRANSLUCENT = 2; - public static final int OPACITY_TRANSPARENT = 3; - - private static final int[] COLOR_MAP = new int[] { 0x00, 0x0f, 0xf0, 0xff }; - private static final int[] OPACITY_MAP = new int[] { 0xff, 0xfe, 0x80, 0x00 }; - - public final int opacity; - public final int red; - public final int green; - public final int blue; - - public CaptionColor(int opacity, int red, int green, int blue) { - this.opacity = opacity; - this.red = red; - this.green = green; - this.blue = blue; - } - - public int getArgbValue() { - return Color.argb( - OPACITY_MAP[opacity], COLOR_MAP[red], COLOR_MAP[green], COLOR_MAP[blue]); - } - } - - /** - * Caption event generated by {@link Cea708Parser}. - */ - public static class CaptionEvent { - @Cea708Parser.CaptionEmitType public final int type; - public final Object obj; - - public CaptionEvent(int type, Object obj) { - this.type = type; - this.obj = obj; - } - } - - /** - * Pen style information. - */ - public static class CaptionPenAttr { - // Pen sizes - public static final int PEN_SIZE_SMALL = 0; - public static final int PEN_SIZE_STANDARD = 1; - public static final int PEN_SIZE_LARGE = 2; - - // Offsets - public static final int OFFSET_SUBSCRIPT = 0; - public static final int OFFSET_NORMAL = 1; - public static final int OFFSET_SUPERSCRIPT = 2; - - public final int penSize; - public final int penOffset; - public final int textTag; - public final int fontTag; - public final int edgeType; - public final boolean underline; - public final boolean italic; - - public CaptionPenAttr(int penSize, int penOffset, int textTag, int fontTag, int edgeType, - boolean underline, boolean italic) { - this.penSize = penSize; - this.penOffset = penOffset; - this.textTag = textTag; - this.fontTag = fontTag; - this.edgeType = edgeType; - this.underline = underline; - this.italic = italic; - } - } - - /** - * {@link CaptionColor} objects that indicate the foreground, background, and edge color of a - * pen. - */ - public static class CaptionPenColor { - public final CaptionColor foregroundColor; - public final CaptionColor backgroundColor; - public final CaptionColor edgeColor; - - public CaptionPenColor(CaptionColor foregroundColor, CaptionColor backgroundColor, - CaptionColor edgeColor) { - this.foregroundColor = foregroundColor; - this.backgroundColor = backgroundColor; - this.edgeColor = edgeColor; - } - } - - /** - * Location information of a pen. - */ - public static class CaptionPenLocation { - public final int row; - public final int column; - - public CaptionPenLocation(int row, int column) { - this.row = row; - this.column = column; - } - } - - /** - * Attributes of a caption window, which is defined in CEA-708B. - */ - public static class CaptionWindowAttr { - public static final int JUSTIFY_LEFT = 0; - public static final int JUSTIFY_CENTER = 2; - public static final int PRINT_LEFT_TO_RIGHT = 0; - public static final int PRINT_RIGHT_TO_LEFT = 1; - public static final int PRINT_TOP_TO_BOTTOM = 2; - public static final int PRINT_BOTTOM_TO_TOP = 3; - - public final CaptionColor fillColor; - public final CaptionColor borderColor; - public final int borderType; - public final boolean wordWrap; - public final int printDirection; - public final int scrollDirection; - public final int justify; - public final int effectDirection; - public final int effectSpeed; - public final int displayEffect; - - public CaptionWindowAttr(CaptionColor fillColor, CaptionColor borderColor, int borderType, - boolean wordWrap, int printDirection, int scrollDirection, int justify, - int effectDirection, - int effectSpeed, int displayEffect) { - this.fillColor = fillColor; - this.borderColor = borderColor; - this.borderType = borderType; - this.wordWrap = wordWrap; - this.printDirection = printDirection; - this.scrollDirection = scrollDirection; - this.justify = justify; - this.effectDirection = effectDirection; - this.effectSpeed = effectSpeed; - this.displayEffect = displayEffect; - } - } - - /** - * Construction information of the caption window of CEA-708B. - */ - public static class CaptionWindow { - public final int id; - public final boolean visible; - public final boolean rowLock; - public final boolean columnLock; - public final int priority; - public final boolean relativePositioning; - public final int anchorVertical; - public final int anchorHorizontal; - public final int anchorId; - public final int rowCount; - public final int columnCount; - public final int penStyle; - public final int windowStyle; - - public CaptionWindow(int id, boolean visible, - boolean rowLock, boolean columnLock, int priority, boolean relativePositioning, - int anchorVertical, int anchorHorizontal, int anchorId, - int rowCount, int columnCount, int penStyle, int windowStyle) { - this.id = id; - this.visible = visible; - this.rowLock = rowLock; - this.columnLock = columnLock; - this.priority = priority; - this.relativePositioning = relativePositioning; - this.anchorVertical = anchorVertical; - this.anchorHorizontal = anchorHorizontal; - this.anchorId = anchorId; - this.rowCount = rowCount; - this.columnCount = columnCount; - this.penStyle = penStyle; - this.windowStyle = windowStyle; - } - } -} diff --git a/src/com/android/tv/tuner/data/PsiData.java b/src/com/android/tv/tuner/data/PsiData.java deleted file mode 100644 index 67700c6a..00000000 --- a/src/com/android/tv/tuner/data/PsiData.java +++ /dev/null @@ -1,94 +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.data; - -import com.android.tv.tuner.data.nano.Track.AtscAudioTrack; -import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; - -import java.util.List; - -/** - * Collection of MPEG PSI table items. - */ -public class PsiData { - - private PsiData() { - } - - public static class PatItem { - private final int mProgramNo; - private final int mPmtPid; - - public PatItem(int programNo, int pmtPid) { - mProgramNo = programNo; - mPmtPid = pmtPid; - } - - public int getProgramNo() { - return mProgramNo; - } - - public int getPmtPid() { - return mPmtPid; - } - - @Override - public String toString() { - return String.format("Program No: %x PMT Pid: %x", mProgramNo, mPmtPid); - } - } - - public static class PmtItem { - public static final int ES_PID_PCR = 0x100; - - private final int mStreamType; - private final int mEsPid; - private final List<AtscAudioTrack> mAudioTracks; - private final List<AtscCaptionTrack> mCaptionTracks; - - public PmtItem(int streamType, int esPid, - List<AtscAudioTrack> audioTracks, List<AtscCaptionTrack> captionTracks) { - mStreamType = streamType; - mEsPid = esPid; - mAudioTracks = audioTracks; - mCaptionTracks = captionTracks; - } - - public int getStreamType() { - return mStreamType; - } - - public int getEsPid() { - return mEsPid; - } - - public List<AtscAudioTrack> getAudioTracks() { - return mAudioTracks; - } - - public List<AtscCaptionTrack> getCaptionTracks() { - return mCaptionTracks; - } - - @Override - public String toString() { - return String.format("Stream Type: %x ES Pid: %x AudioTracks: %s CaptionTracks: %s", - mStreamType, mEsPid, mAudioTracks, mCaptionTracks); - } - } -} diff --git a/src/com/android/tv/tuner/data/PsipData.java b/src/com/android/tv/tuner/data/PsipData.java deleted file mode 100644 index 8f98e67c..00000000 --- a/src/com/android/tv/tuner/data/PsipData.java +++ /dev/null @@ -1,820 +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.data; - -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.ts.SectionParser; -import com.android.tv.tuner.util.ConvertUtils; -import com.android.tv.util.StringUtils; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; - -/** - * Collection of ATSC PSIP table items. - */ -public class PsipData { - - private PsipData() { - } - - public static class PsipSection { - private final int mTableId; - private final int mTableIdExtension; - private final int mSectionNumber; - private final boolean mCurrentNextIndicator; - - public static PsipSection create(byte[] data) { - if (data.length < 9) { - return null; - } - int tableId = data[0] & 0xff; - int tableIdExtension = (data[3] & 0xff) << 8 | (data[4] & 0xff); - int sectionNumber = data[6] & 0xff; - boolean currentNextIndicator = (data[5] & 0x01) != 0; - return new PsipSection(tableId, tableIdExtension, sectionNumber, currentNextIndicator); - } - - private PsipSection(int tableId, int tableIdExtension, int sectionNumber, - boolean currentNextIndicator) { - mTableId = tableId; - mTableIdExtension = tableIdExtension; - mSectionNumber = sectionNumber; - mCurrentNextIndicator = currentNextIndicator; - } - - public int getTableId() { - return mTableId; - } - - public int getTableIdExtension() { - return mTableIdExtension; - } - - public int getSectionNumber() { - return mSectionNumber; - } - - // This is for indicating that the section sent is applicable. - // We only consider a situation where currentNextIndicator is expected to have a true value. - // So, we are not going to compare this variable in hashCode() and equals() methods. - public boolean getCurrentNextIndicator() { - return mCurrentNextIndicator; - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + mTableId; - result = 31 * result + mTableIdExtension; - result = 31 * result + mSectionNumber; - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof PsipSection) { - PsipSection another = (PsipSection) obj; - return mTableId == another.getTableId() - && mTableIdExtension == another.getTableIdExtension() - && mSectionNumber == another.getSectionNumber(); - } - return false; - } - } - - /** - * {@link TvTracksInterface} for serving the audio and caption tracks. - */ - public interface TvTracksInterface { - /** - * Set the flag that tells the caption tracks have been found in this section container. - */ - void setHasCaptionTrack(); - - /** - * Returns whether or not the caption tracks have been found in this section container. - * If true, zero caption track will be interpreted as a clearance of the caption tracks. - */ - boolean hasCaptionTrack(); - - /** - * Returns the audio tracks received. - */ - List<AtscAudioTrack> getAudioTracks(); - - /** - * Returns the caption tracks received. - */ - List<AtscCaptionTrack> getCaptionTracks(); - } - - public static class MgtItem { - public static final int TABLE_TYPE_EIT_RANGE_START = 0x0100; - public static final int TABLE_TYPE_EIT_RANGE_END = 0x017f; - public static final int TABLE_TYPE_CHANNEL_ETT = 0x0004; - public static final int TABLE_TYPE_ETT_RANGE_START = 0x0200; - public static final int TABLE_TYPE_ETT_RANGE_END = 0x027f; - - private final int mTableType; - private final int mTableTypePid; - - public MgtItem(int tableType, int tableTypePid) { - mTableType = tableType; - mTableTypePid = tableTypePid; - } - - public int getTableType() { - return mTableType; - } - - public int getTableTypePid() { - return mTableTypePid; - } - } - - public static class VctItem { - private final String mShortName; - private final String mLongName; - private final int mServiceType; - private final int mChannelTsid; - private final int mProgramNumber; - private final int mMajorChannelNumber; - private final int mMinorChannelNumber; - private final int mSourceId; - private String mDescription; - - public VctItem(String shortName, String longName, int serviceType, int channelTsid, - int programNumber, int majorChannelNumber, int minorChannelNumber, int sourceId) { - mShortName = shortName; - mLongName = longName; - mServiceType = serviceType; - mChannelTsid = channelTsid; - mProgramNumber = programNumber; - mMajorChannelNumber = majorChannelNumber; - mMinorChannelNumber = minorChannelNumber; - mSourceId = sourceId; - } - - public String getShortName() { - return mShortName; - } - - public String getLongName() { - return mLongName; - } - - public int getServiceType() { - return mServiceType; - } - - public int getChannelTsid() { - return mChannelTsid; - } - - public int getProgramNumber() { - return mProgramNumber; - } - - public int getMajorChannelNumber() { - return mMajorChannelNumber; - } - - public int getMinorChannelNumber() { - return mMinorChannelNumber; - } - - public int getSourceId() { - return mSourceId; - } - - @Override - public String toString() { - return String - .format(Locale.US, "ShortName: %s LongName: %s ServiceType: %d ChannelTsid: %x " - + "ProgramNumber:%d %d-%d SourceId: %x", - mShortName, mLongName, mServiceType, mChannelTsid, - mProgramNumber, mMajorChannelNumber, mMinorChannelNumber, mSourceId); - } - - public void setDescription(String description) { - mDescription = description; - } - - public String getDescription() { - return mDescription; - } - } - - public static class SdtItem { - private final String mServiceName; - private final String mServiceProviderName; - private final int mServiceType; - private final int mServiceId; - private final int mOriginalNetWorkId; - - public SdtItem(String serviceName, String serviceProviderName, int serviceType, - int serviceId, int originalNetWorkId) { - mServiceName = serviceName; - mServiceProviderName = serviceProviderName; - mServiceType = serviceType; - mServiceId = serviceId; - mOriginalNetWorkId = originalNetWorkId; - } - - public String getServiceName() { - return mServiceName; - } - - public String getServiceProviderName() { - return mServiceProviderName; - } - - public int getServiceType() { - return mServiceType; - } - - public int getServiceId() { - return mServiceId; - } - - public int getOriginalNetworkId() { - return mOriginalNetWorkId; - } - - @Override - public String toString() { - return String.format("ServiceName: %s ServiceProviderName:%s ServiceType:%d " - + "OriginalNetworkId:%d", - mServiceName, mServiceProviderName, mServiceType, mOriginalNetWorkId); - } - } - - /** - * A base class for descriptors of Ts packets. - */ - public abstract static class TsDescriptor { - public abstract int getTag(); - } - - public static class ContentAdvisoryDescriptor extends TsDescriptor { - private final List<RatingRegion> mRatingRegions; - - public ContentAdvisoryDescriptor(List<RatingRegion> ratingRegions) { - mRatingRegions = ratingRegions; - } - - @Override - public int getTag() { - return SectionParser.DESCRIPTOR_TAG_CONTENT_ADVISORY; - } - - public List<RatingRegion> getRatingRegions() { - return mRatingRegions; - } - } - - public static class CaptionServiceDescriptor extends TsDescriptor { - private final List<AtscCaptionTrack> mCaptionTracks; - - public CaptionServiceDescriptor(List<AtscCaptionTrack> captionTracks) { - mCaptionTracks = captionTracks; - } - - @Override - public int getTag() { - return SectionParser.DESCRIPTOR_TAG_CAPTION_SERVICE; - } - - public List<AtscCaptionTrack> getCaptionTracks() { - return mCaptionTracks; - } - } - - public static class ExtendedChannelNameDescriptor extends TsDescriptor { - private final String mLongChannelName; - - public ExtendedChannelNameDescriptor(String longChannelName) { - mLongChannelName = longChannelName; - } - - @Override - public int getTag() { - return SectionParser.DESCRIPTOR_TAG_EXTENDED_CHANNEL_NAME; - } - - public String getLongChannelName() { - return mLongChannelName; - } - } - - public static class GenreDescriptor extends TsDescriptor { - private final String[] mBroadcastGenres; - private final String[] mCanonicalGenres; - - public GenreDescriptor(String[] broadcastGenres, String[] canonicalGenres) { - mBroadcastGenres = broadcastGenres; - mCanonicalGenres = canonicalGenres; - } - - @Override - public int getTag() { - return SectionParser.DESCRIPTOR_TAG_GENRE; - } - - public String[] getBroadcastGenres() { - return mBroadcastGenres; - } - - public String[] getCanonicalGenres() { - return mCanonicalGenres; - } - } - - public static class Ac3AudioDescriptor extends TsDescriptor { - // See A/52 Annex A. Table A4.2 - private static final byte SAMPLE_RATE_CODE_48000HZ = 0; - private static final byte SAMPLE_RATE_CODE_44100HZ = 1; - private static final byte SAMPLE_RATE_CODE_32000HZ = 2; - - private final byte mSampleRateCode; - private final byte mBsid; - private final byte mBitRateCode; - private final byte mSurroundMode; - private final byte mBsmod; - private final int mNumChannels; - private final boolean mFullSvc; - private final byte mLangCod; - private final byte mLangCod2; - private final byte mMainId; - private final byte mPriority; - private final byte mAsvcflags; - private final String mText; - private final String mLanguage; - private final String mLanguage2; - - public Ac3AudioDescriptor(byte sampleRateCode, byte bsid, byte bitRateCode, - byte surroundMode, byte bsmod, int numChannels, boolean fullSvc, byte langCod, - byte langCod2, byte mainId, byte priority, byte asvcflags, String text, - String language, String language2) { - mSampleRateCode = sampleRateCode; - mBsid = bsid; - mBitRateCode = bitRateCode; - mSurroundMode = surroundMode; - mBsmod = bsmod; - mNumChannels = numChannels; - mFullSvc = fullSvc; - mLangCod = langCod; - mLangCod2 = langCod2; - mMainId = mainId; - mPriority = priority; - mAsvcflags = asvcflags; - mText = text; - mLanguage = language; - mLanguage2 = language2; - } - - @Override - public int getTag() { - return SectionParser.DESCRIPTOR_TAG_AC3_AUDIO_STREAM; - } - - public byte getSampleRateCode() { - return mSampleRateCode; - } - - public int getSampleRate() { - switch (mSampleRateCode) { - case SAMPLE_RATE_CODE_48000HZ: - return 48000; - case SAMPLE_RATE_CODE_44100HZ: - return 44100; - case SAMPLE_RATE_CODE_32000HZ: - return 32000; - default: - return 0; - } - } - - public byte getBsid() { - return mBsid; - } - - public byte getBitRateCode() { - return mBitRateCode; - } - - public byte getSurroundMode() { - return mSurroundMode; - } - - public byte getBsmod() { - return mBsmod; - } - - public int getNumChannels() { - return mNumChannels; - } - - public boolean isFullSvc() { - return mFullSvc; - } - - public byte getLangCod() { - return mLangCod; - } - - public byte getLangCod2() { - return mLangCod2; - } - - public byte getMainId() { - return mMainId; - } - - public byte getPriority() { - return mPriority; - } - - public byte getAsvcflags() { - return mAsvcflags; - } - - public String getText() { - return mText; - } - - public String getLanguage() { - return mLanguage; - } - - public String getLanguage2() { - return mLanguage2; - } - - @Override - public String toString() { - return String.format(Locale.US, - "AC3 audio stream sampleRateCode: %d, bsid: %d, bitRateCode: %d, " - + "surroundMode: %d, bsmod: %d, numChannels: %d, fullSvc: %s, langCod: %d, " - + "langCod2: %d, mainId: %d, priority: %d, avcflags: %d, text: %s, language: %s" - + ", language2: %s", mSampleRateCode, mBsid, mBitRateCode, mSurroundMode, - mBsmod, mNumChannels, mFullSvc, mLangCod, mLangCod2, mMainId, mPriority, - mAsvcflags, mText, mLanguage, mLanguage2); - } - } - - public static class Iso639LanguageDescriptor extends TsDescriptor { - private final List<AtscAudioTrack> mAudioTracks; - - public Iso639LanguageDescriptor(List<AtscAudioTrack> audioTracks) { - mAudioTracks = audioTracks; - } - - @Override - public int getTag() { - return SectionParser.DESCRIPTOR_TAG_ISO639LANGUAGE; - } - - public List<AtscAudioTrack> getAudioTracks() { - return mAudioTracks; - } - - @Override - public String toString() { - return String.format("%s %s", getClass().getName(), mAudioTracks); - } - } - - public static class ServiceDescriptor extends TsDescriptor { - private final int mServiceType; - private final String mServiceProviderName; - private final String mServiceName; - - public ServiceDescriptor(int serviceType, String serviceProviderName, String serviceName) { - mServiceType = serviceType; - mServiceProviderName = serviceProviderName; - mServiceName = serviceName; - } - - @Override - public int getTag() { - return SectionParser.DVB_DESCRIPTOR_TAG_SERVICE; - } - - public int getServiceType() { - return mServiceType; - } - - public String getServiceProviderName() { - return mServiceProviderName; - } - - public String getServiceName() { - return mServiceName; - } - - @Override - public String toString() { - return String.format( - "Service descriptor, service type: %d, " - + "service provider name: %s, " - + "service name: %s", mServiceType, mServiceProviderName, mServiceName); - } - } - - public static class ShortEventDescriptor extends TsDescriptor { - private final String mLanguage; - private final String mEventName; - private final String mText; - - public ShortEventDescriptor(String language, String eventName, String text) { - mLanguage = language; - mEventName = eventName; - mText = text; - } - - public String getEventName() { - return mEventName; - } - - @Override - public int getTag() { - return SectionParser.DVB_DESCRIPTOR_TAG_SHORT_EVENT; - } - - @Override - public String toString() { - return String.format("ShortEvent Descriptor, language:%s, event name: %s, " - + "text:%s", mLanguage, mEventName, mText); - } - } - - public static class ParentalRatingDescriptor extends TsDescriptor { - private final HashMap<String, Integer> mRatings; - - public ParentalRatingDescriptor(HashMap<String, Integer> ratings) { - mRatings = ratings; - } - - @Override - public int getTag() { - return SectionParser.DVB_DESCRIPTOR_TAG_PARENTAL_RATING; - } - - public HashMap<String, Integer> getRatings() { - return mRatings; - } - - @Override - public String toString() { - return String.format("Parental rating descriptor, ratings:" + mRatings); - } - } - - public static class RatingRegion { - private final int mName; - private final String mDescription; - private final List<RegionalRating> mRegionalRatings; - - public RatingRegion(int name, String description, List<RegionalRating> regionalRatings) { - mName = name; - mDescription = description; - mRegionalRatings = regionalRatings; - } - - public int getName() { - return mName; - } - - public String getDescription() { - return mDescription; - } - - public List<RegionalRating> getRegionalRatings() { - return mRegionalRatings; - } - } - - public static class RegionalRating { - private final int mDimension; - private final int mRating; - - public RegionalRating(int dimension, int rating) { - mDimension = dimension; - mRating = rating; - } - - public int getDimension() { - return mDimension; - } - - public int getRating() { - return mRating; - } - } - - public static class EitItem implements Comparable<EitItem>, TvTracksInterface { - public static final long INVALID_PROGRAM_ID = -1; - - // A program id is a primary key of TvContract.Programs table. So it must be positive. - private final long mProgramId; - private final int mEventId; - private final String mTitleText; - private String mDescription; - private final long mStartTime; - private final int mLengthInSecond; - private final String mContentRating; - private final List<AtscAudioTrack> mAudioTracks; - private final List<AtscCaptionTrack> mCaptionTracks; - private boolean mHasCaptionTrack; - private final String mBroadcastGenre; - private final String mCanonicalGenre; - - public EitItem(long programId, int eventId, String titleText, long startTime, - int lengthInSecond, String contentRating, List<AtscAudioTrack> audioTracks, - List<AtscCaptionTrack> captionTracks, String broadcastGenre, String canonicalGenre, - String description) { - mProgramId = programId; - mEventId = eventId; - mTitleText = titleText; - mStartTime = startTime; - mLengthInSecond = lengthInSecond; - mContentRating = contentRating; - mAudioTracks = audioTracks; - mCaptionTracks = captionTracks; - mBroadcastGenre = broadcastGenre; - mCanonicalGenre = canonicalGenre; - mDescription = description; - } - - public long getProgramId() { - return mProgramId; - } - - public int getEventId() { - return mEventId; - } - - public String getTitleText() { - return mTitleText; - } - - public void setDescription(String description) { - mDescription = description; - } - - public String getDescription() { - return mDescription; - } - - public long getStartTime() { - return mStartTime; - } - - public int getLengthInSecond() { - return mLengthInSecond; - } - - public long getStartTimeUtcMillis() { - return ConvertUtils.convertGPSTimeToUnixEpoch(mStartTime) * DateUtils.SECOND_IN_MILLIS; - } - - public long getEndTimeUtcMillis() { - return ConvertUtils.convertGPSTimeToUnixEpoch( - mStartTime + mLengthInSecond) * DateUtils.SECOND_IN_MILLIS; - } - - public String getContentRating() { - return mContentRating; - } - - @Override - public List<AtscAudioTrack> getAudioTracks() { - return mAudioTracks; - } - - @Override - public List<AtscCaptionTrack> getCaptionTracks() { - return mCaptionTracks; - } - - public String getBroadcastGenre() { - return mBroadcastGenre; - } - - public String getCanonicalGenre() { - return mCanonicalGenre; - } - - @Override - public void setHasCaptionTrack() { - mHasCaptionTrack = true; - } - - @Override - public boolean hasCaptionTrack() { - return mHasCaptionTrack; - } - - @Override - public int compareTo(@NonNull EitItem item) { - // The list of caption tracks and the program ids are not compared in here because the - // channels in TIF have the concept of the caption and audio tracks while the programs - // do not and the programs in TIF only have a program id since they are the rows of - // Content Provider. - int ret = mEventId - item.getEventId(); - if (ret != 0) { - return ret; - } - ret = StringUtils.compare(mTitleText, item.getTitleText()); - if (ret != 0) { - return ret; - } - if (mStartTime > item.getStartTime()) { - return 1; - } else if (mStartTime < item.getStartTime()) { - return -1; - } - if (mLengthInSecond > item.getLengthInSecond()) { - return 1; - } else if (mLengthInSecond < item.getLengthInSecond()) { - return -1; - } - - // Compares content ratings - ret = StringUtils.compare(mContentRating, item.getContentRating()); - if (ret != 0) { - return ret; - } - - // Compares broadcast genres - ret = StringUtils.compare(mBroadcastGenre, item.getBroadcastGenre()); - if (ret != 0) { - return ret; - } - // Compares canonical genres - ret = StringUtils.compare(mCanonicalGenre, item.getCanonicalGenre()); - if (ret != 0) { - return ret; - } - - // Compares descriptions - return StringUtils.compare(mDescription, item.getDescription()); - } - - public String getAudioLanguage() { - if (mAudioTracks == null) { - return ""; - } - ArrayList<String> languages = new ArrayList<>(); - for (AtscAudioTrack audioTrack : mAudioTracks) { - languages.add(audioTrack.language); - } - return TextUtils.join(",", languages); - } - - @Override - public String toString() { - return String.format(Locale.US, - "EitItem programId: %d, eventId: %d, title: %s, startTime: %10d, " - + "length: %6d, rating: %s, audio tracks: %d, caption tracks: %d, " - + "genres (broadcast: %s, canonical: %s), description: %s", - mProgramId, mEventId, mTitleText, mStartTime, mLengthInSecond, mContentRating, - mAudioTracks != null ? mAudioTracks.size() : 0, - mCaptionTracks != null ? mCaptionTracks.size() : 0, - mBroadcastGenre, mCanonicalGenre, mDescription); - } - } - - public static class EttItem { - public final int eventId; - public final String text; - - public EttItem(int eventId, String text) { - this.eventId = eventId; - this.text = text; - } - } -} diff --git a/src/com/android/tv/tuner/data/TunerChannel.java b/src/com/android/tv/tuner/data/TunerChannel.java deleted file mode 100644 index 1cf514c1..00000000 --- a/src/com/android/tv/tuner/data/TunerChannel.java +++ /dev/null @@ -1,511 +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.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.util.Ints; -import com.android.tv.util.StringUtils; -import com.google.protobuf.nano.MessageNano; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -/** - * A class that represents a single channel accessible through a tuner. - */ -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", - "Analog television channels", - "ATSC_digital_television", - "ATSC_audio", - "ATSC_data_only_service", - "Software Download", - "Unassociated/Small Screen Service", - "Parameterized Service", - "ATSC NRT Service", - "Extended Parameterized Service" }; - private static final String ATSC_SERVICE_TYPE_NAME_RESERVED = - ATSC_SERVICE_TYPE_NAMES[Channel.SERVICE_TYPE_ATSC_RESERVED]; - - public static final int INVALID_FREQUENCY = -1; - - // According to RFC4259, The number of available PIDs ranges from 0 to 8191. - public static final int INVALID_PID = -1; - - // 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, - List<PsiData.PmtItem> pmtItems, int type) { - mProto = new TunerChannelProto(); - if (channel == null) { - mProto.shortName = ""; - mProto.tsid = 0; - mProto.programNumber = programNumber; - mProto.virtualMajor = 0; - mProto.virtualMinor = 0; - } else { - mProto.shortName = channel.getShortName(); - if (channel.getLongName() != null) { - mProto.longName = channel.getLongName(); - } - mProto.tsid = channel.getChannelTsid(); - mProto.programNumber = channel.getProgramNumber(); - mProto.virtualMajor = channel.getMajorChannelNumber(); - mProto.virtualMinor = channel.getMinorChannelNumber(); - if (channel.getDescription() != null) { - mProto.description = channel.getDescription(); - } - mProto.serviceType = channel.getServiceType(); - } - initProto(pmtItems, type); - } - - private void initProto(List<PsiData.PmtItem> pmtItems, int type) { - mProto.type = type; - mProto.channelId = -1L; - mProto.frequency = INVALID_FREQUENCY; - mProto.videoPid = INVALID_PID; - mProto.videoStreamType = INVALID_STREAMTYPE; - List<Integer> audioPids = new ArrayList<>(); - List<Integer> audioStreamTypes = new ArrayList<>(); - for (PsiData.PmtItem pmt : pmtItems) { - switch (pmt.getStreamType()) { - // MPEG ES stream video types - case Channel.MPEG1: - case Channel.MPEG2: - case Channel.H263: - case Channel.H264: - case Channel.H265: - mProto.videoPid = pmt.getEsPid(); - mProto.videoStreamType = pmt.getStreamType(); - break; - - // MPEG ES stream audio types - case Channel.MPEG1AUDIO: - case Channel.MPEG2AUDIO: - case Channel.MPEG2AACAUDIO: - case Channel.MPEG4LATMAACAUDIO: - case Channel.A52AC3AUDIO: - case Channel.EAC3AUDIO: - audioPids.add(pmt.getEsPid()); - audioStreamTypes.add(pmt.getStreamType()); - break; - - // Non MPEG ES stream types - case 0x100: // PmtItem.ES_PID_PCR: - mProto.pcrPid = pmt.getEsPid(); - break; - } - } - mProto.audioPids = Ints.toArray(audioPids); - mProto.audioStreamTypes = Ints.toArray(audioStreamTypes); - mProto.audioTrackIndex = (audioPids.size() > 0) ? 0 : -1; - } - - private TunerChannel(int programNumber, int type, PsipData.SdtItem channel, - List<PsiData.PmtItem> pmtItems) { - mProto = new TunerChannelProto(); - mProto.tsid = 0; - mProto.virtualMajor = 0; - mProto.virtualMinor = 0; - if (channel == null) { - mProto.shortName = ""; - mProto.programNumber = programNumber; - } else { - mProto.shortName = channel.getServiceName(); - mProto.programNumber = channel.getServiceId(); - mProto.serviceType = channel.getServiceType(); - } - initProto(pmtItems, type); - } - - /** - * Initialize tuner channel with VCT items and PMT items. - */ - public TunerChannel(PsipData.VctItem channel, List<PsiData.PmtItem> pmtItems) { - this(channel, 0, pmtItems, Channel.TYPE_TUNER); - } - - /** - * Initialize tuner channel with program number and PMT items. - */ - public TunerChannel(int programNumber, List<PsiData.PmtItem> pmtItems) { - this(null, programNumber, pmtItems, Channel.TYPE_TUNER); - } - - /** - * Initialize tuner channel with SDT items and PMT items. - */ - public TunerChannel(PsipData.SdtItem channel, List<PsiData.PmtItem> pmtItems) { - this(0, Channel.TYPE_TUNER, channel, pmtItems); - } - - private TunerChannel(TunerChannelProto tunerChannelProto) { - mProto = tunerChannelProto; - } - - public static TunerChannel forFile(PsipData.VctItem channel, List<PsiData.PmtItem> pmtItems) { - return new TunerChannel(channel, 0, pmtItems, Channel.TYPE_FILE); - } - - public static TunerChannel forDvbFile( - PsipData.SdtItem channel, List<PsiData.PmtItem> pmtItems) { - return new TunerChannel(0, Channel.TYPE_FILE, channel, pmtItems); - } - - /** - * 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( - null, programNumber, Collections.EMPTY_LIST, Channel.TYPE_NETWORK); - 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; - } - - public String getShortName() { - return mProto.shortName; - } - - public int getProgramNumber() { - return mProto.programNumber; - } - - public int getServiceType() { - return mProto.serviceType; - } - - public String getServiceTypeName() { - int serviceType = mProto.serviceType; - if (serviceType >= 0 && serviceType < ATSC_SERVICE_TYPE_NAMES.length) { - return ATSC_SERVICE_TYPE_NAMES[serviceType]; - } - return ATSC_SERVICE_TYPE_NAME_RESERVED; - } - - public int getVirtualMajor() { - return mProto.virtualMajor; - } - - public int getVirtualMinor() { - return mProto.virtualMinor; - } - - public int getFrequency() { - return mProto.frequency; - } - - public String getModulation() { - return mProto.modulation; - } - - public int getTsid() { - return mProto.tsid; - } - - public int getVideoPid() { - return mProto.videoPid; - } - - synchronized public void setVideoPid(int videoPid) { - mProto.videoPid = videoPid; - } - - public int getVideoStreamType() { - return mProto.videoStreamType; - } - - public int getAudioPid() { - if (mProto.audioTrackIndex == -1) { - return INVALID_PID; - } - return mProto.audioPids[mProto.audioTrackIndex]; - } - - public int getAudioStreamType() { - if (mProto.audioTrackIndex == -1) { - return INVALID_STREAMTYPE; - } - return mProto.audioStreamTypes[mProto.audioTrackIndex]; - } - - public List<Integer> getAudioPids() { - return Ints.asList(mProto.audioPids); - } - - synchronized public void setAudioPids(List<Integer> audioPids) { - mProto.audioPids = Ints.toArray(audioPids); - } - - public List<Integer> getAudioStreamTypes() { - return Ints.asList(mProto.audioStreamTypes); - } - - synchronized public void setAudioStreamTypes(List<Integer> audioStreamTypes) { - mProto.audioStreamTypes = Ints.toArray(audioStreamTypes); - } - - public int getPcrPid() { - return mProto.pcrPid; - } - - public int getType() { - return mProto.type; - } - - synchronized public void setFilepath(String filepath) { - mProto.filepath = filepath == null ? "" : filepath; - } - - public String getFilepath() { - return mProto.filepath; - } - - synchronized public void setVirtualMajor(int virtualMajor) { - mProto.virtualMajor = virtualMajor; - } - - synchronized public void setVirtualMinor(int virtualMinor) { - mProto.virtualMinor = virtualMinor; - } - - synchronized public void setShortName(String shortName) { - mProto.shortName = shortName == null ? "" : shortName; - } - - synchronized public void setFrequency(int frequency) { - mProto.frequency = frequency; - } - - synchronized public void setModulation(String modulation) { - mProto.modulation = modulation == null ? "" : modulation; - } - - public boolean hasVideo() { - return mProto.videoPid != INVALID_PID; - } - - public boolean hasAudio() { - return getAudioPid() != INVALID_PID; - } - - public long getChannelId() { - return mProto.channelId; - } - - synchronized public void setChannelId(long channelId) { - mProto.channelId = channelId; - } - - public String getDisplayNumber() { - 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 { - return Integer.toString(mProto.programNumber); - } - } - - public String getDescription() { - return mProto.description; - } - - @Override - synchronized public void setHasCaptionTrack() { - mProto.hasCaptionTrack = true; - } - - @Override - public boolean hasCaptionTrack() { - return mProto.hasCaptionTrack; - } - - @Override - public List<AtscAudioTrack> getAudioTracks() { - return Collections.unmodifiableList(Arrays.asList(mProto.audioTracks)); - } - - synchronized public void setAudioTracks(List<AtscAudioTrack> audioTracks) { - mProto.audioTracks = audioTracks.toArray(new AtscAudioTrack[audioTracks.size()]); - } - - @Override - public List<AtscCaptionTrack> getCaptionTracks() { - return Collections.unmodifiableList(Arrays.asList(mProto.captionTracks)); - } - - synchronized public void setCaptionTracks(List<AtscCaptionTrack> captionTracks) { - mProto.captionTracks = captionTracks.toArray(new AtscCaptionTrack[captionTracks.size()]); - } - - synchronized public void selectAudioTrack(int index) { - if (0 <= index && index < mProto.audioPids.length) { - mProto.audioTrackIndex = index; - } else { - mProto.audioTrackIndex = -1; - } - } - - 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) { - case Channel.TYPE_FILE: - return String.format("{%d-%d %s} Filepath: %s, ProgramNumber %d", - mProto.virtualMajor, mProto.virtualMinor, mProto.shortName, - mProto.filepath, mProto.programNumber); - //case Channel.TYPE_TUNER: - default: - return String.format("{%d-%d %s} Frequency: %d, ProgramNumber %d", - mProto.virtualMajor, mProto.virtualMinor, mProto.shortName, - mProto.frequency, mProto.programNumber); - } - } - - @Override - public int compareTo(@NonNull TunerChannel channel) { - // In the same frequency, the program number acts as the sub-channel number. - int ret = getFrequency() - channel.getFrequency(); - if (ret != 0) { - return ret; - } - ret = getProgramNumber() - channel.getProgramNumber(); - 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()); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof TunerChannel)) { - return false; - } - return compareTo((TunerChannel) o) == 0; - } - - @Override - public int hashCode() { - return Objects.hash(getFrequency(), getProgramNumber(), getName(), getFilepath()); - } - - // Serialization - 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) { - if (data == null) { - return null; - } - try { - return new TunerChannel(TunerChannelProto.parseFrom(data)); - } catch (IOException e) { - Log.e(TAG, "Could not parse from byte array", e); - return null; - } - } -} diff --git a/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java deleted file mode 100644 index 5f536708..00000000 --- a/src/com/android/tv/tuner/exoplayer/Cea708TextTrackRenderer.java +++ /dev/null @@ -1,302 +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.exoplayer; - -import android.util.Log; - -import com.google.android.exoplayer.ExoPlaybackException; -import com.google.android.exoplayer.MediaClock; -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.MediaFormatHolder; -import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.SampleSource; -import com.google.android.exoplayer.TrackRenderer; -import com.google.android.exoplayer.util.Assertions; -import com.android.tv.tuner.cc.Cea708Parser; -import com.android.tv.tuner.data.Cea708Data.CaptionEvent; - -import java.io.IOException; - -/** - * A {@link TrackRenderer} for CEA-708 textual subtitles. - */ -public class Cea708TextTrackRenderer extends TrackRenderer implements - Cea708Parser.OnCea708ParserListener { - private static final String TAG = "Cea708TextTrackRenderer"; - private static final boolean DEBUG = false; - - public static final int MSG_SERVICE_NUMBER = 1; - public static final int MSG_ENABLE_CLOSED_CAPTION = 2; - - // According to CEA-708B, the maximum value of closed caption bandwidth is 9600bps. - private static final int DEFAULT_INPUT_BUFFER_SIZE = 9600 / 8; - - private final SampleSource.SampleSourceReader mSource; - private final SampleHolder mSampleHolder; - private final MediaFormatHolder mFormatHolder; - private int mServiceNumber; - private boolean mInputStreamEnded; - private long mCurrentPositionUs; - private long mPresentationTimeUs; - private int mTrackIndex; - private boolean mRenderingDisabled; - private Cea708Parser mCea708Parser; - private CcListener mCcListener; - - public interface CcListener { - void emitEvent(CaptionEvent captionEvent); - void clearCaption(); - void discoverServiceNumber(int serviceNumber); - } - - public Cea708TextTrackRenderer(SampleSource source) { - mSource = source.register(); - mTrackIndex = -1; - mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT); - mSampleHolder.ensureSpaceForWrite(DEFAULT_INPUT_BUFFER_SIZE); - mFormatHolder = new MediaFormatHolder(); - } - - @Override - protected MediaClock getMediaClock() { - return null; - } - - private boolean handlesMimeType(String mimeType) { - return mimeType.equals(MpegTsSampleExtractor.MIMETYPE_TEXT_CEA_708); - } - - @Override - protected boolean doPrepare(long positionUs) throws ExoPlaybackException { - boolean sourcePrepared = mSource.prepare(positionUs); - if (!sourcePrepared) { - return false; - } - int trackCount = mSource.getTrackCount(); - for (int i = 0; i < trackCount; ++i) { - MediaFormat trackFormat = mSource.getFormat(i); - if (handlesMimeType(trackFormat.mimeType)) { - mTrackIndex = i; - clearDecodeState(); - return true; - } - } - // TODO: Check this case. (Source do not have the proper mime type.) - return true; - } - - @Override - protected void onEnabled(int track, long positionUs, boolean joining) { - Assertions.checkArgument(mTrackIndex != -1 && track == 0); - mSource.enable(mTrackIndex, positionUs); - mInputStreamEnded = false; - mPresentationTimeUs = positionUs; - mCurrentPositionUs = Long.MIN_VALUE; - } - - @Override - protected void onDisabled() { - mSource.disable(mTrackIndex); - } - - @Override - protected void onReleased() { - mSource.release(); - mCea708Parser = null; - } - - @Override - protected boolean isEnded() { - return mInputStreamEnded; - } - - @Override - protected boolean isReady() { - // Since this track will be fed by {@link VideoTrackRenderer}, - // it is not required to control transition between ready state and buffering state. - return true; - } - - @Override - protected int getTrackCount() { - return mTrackIndex < 0 ? 0 : 1; - } - - @Override - protected MediaFormat getFormat(int track) { - Assertions.checkArgument(mTrackIndex != -1 && track == 0); - return mSource.getFormat(mTrackIndex); - } - - @Override - protected void maybeThrowError() throws ExoPlaybackException { - try { - mSource.maybeThrowError(); - } catch (IOException e) { - throw new ExoPlaybackException(e); - } - } - - @Override - protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - try { - mPresentationTimeUs = positionUs; - if (!mInputStreamEnded) { - processOutput(); - feedInputBuffer(); - } - } catch (IOException e) { - throw new ExoPlaybackException(e); - } - } - - private boolean processOutput() { - return !mInputStreamEnded && mCea708Parser != null && - mCea708Parser.processClosedCaptions(mPresentationTimeUs); - } - - private boolean feedInputBuffer() throws IOException, ExoPlaybackException { - if (mInputStreamEnded) { - return false; - } - long discontinuity = mSource.readDiscontinuity(mTrackIndex); - if (discontinuity != SampleSource.NO_DISCONTINUITY) { - if (DEBUG) { - Log.d(TAG, "Read discontinuity happened"); - } - - // TODO: handle input discontinuity for trickplay. - clearDecodeState(); - mPresentationTimeUs = discontinuity; - return false; - } - mSampleHolder.data.clear(); - mSampleHolder.size = 0; - int result = mSource.readData(mTrackIndex, mPresentationTimeUs, - mFormatHolder, mSampleHolder); - switch (result) { - case SampleSource.NOTHING_READ: { - return false; - } - case SampleSource.FORMAT_READ: { - if (DEBUG) { - Log.i(TAG, "Format was read again"); - } - return true; - } - case SampleSource.END_OF_STREAM: { - if (DEBUG) { - Log.i(TAG, "End of stream from SampleSource"); - } - mInputStreamEnded = true; - return false; - } - case SampleSource.SAMPLE_READ: { - mSampleHolder.data.flip(); - if (mCea708Parser != null && !mRenderingDisabled) { - mCea708Parser.parseClosedCaption(mSampleHolder.data, mSampleHolder.timeUs); - } - return true; - } - } - return false; - } - - private void clearDecodeState() { - mCea708Parser = new Cea708Parser(); - mCea708Parser.setListener(this); - mCea708Parser.setListenServiceNumber(mServiceNumber); - } - - @Override - protected long getDurationUs() { - return mSource.getFormat(mTrackIndex).durationUs; - } - - @Override - protected long getBufferedPositionUs() { - return mSource.getBufferedPositionUs(); - } - - @Override - protected void seekTo(long currentPositionUs) throws ExoPlaybackException { - mSource.seekToUs(currentPositionUs); - mInputStreamEnded = false; - mPresentationTimeUs = currentPositionUs; - mCurrentPositionUs = Long.MIN_VALUE; - } - - @Override - protected void onStarted() { - // do nothing. - } - - @Override - protected void onStopped() { - // do nothing. - } - - private void setServiceNumber(int serviceNumber) { - mServiceNumber = serviceNumber; - if (mCea708Parser != null) { - mCea708Parser.setListenServiceNumber(serviceNumber); - } - } - - @Override - public void emitEvent(CaptionEvent event) { - if (mCcListener != null) { - mCcListener.emitEvent(event); - } - } - - @Override - public void discoverServiceNumber(int serviceNumber) { - if (mCcListener != null) { - mCcListener.discoverServiceNumber(serviceNumber); - } - } - - public void setCcListener(CcListener ccListener) { - mCcListener = ccListener; - } - - @Override - public void handleMessage(int messageType, Object message) throws ExoPlaybackException { - switch (messageType) { - case MSG_SERVICE_NUMBER: - setServiceNumber((int) message); - break; - case MSG_ENABLE_CLOSED_CAPTION: - boolean renderingDisabled = (Boolean) message == false; - if (mRenderingDisabled != renderingDisabled) { - mRenderingDisabled = renderingDisabled; - if (mRenderingDisabled) { - if (mCea708Parser != null) { - mCea708Parser.clear(); - } - if (mCcListener != null) { - mCcListener.clearCaption(); - } - } - } - break; - default: - super.handleMessage(messageType, message); - } - } -} diff --git a/src/com/android/tv/tuner/exoplayer/ExoPlayerExtractorsFactory.java b/src/com/android/tv/tuner/exoplayer/ExoPlayerExtractorsFactory.java deleted file mode 100644 index 0ab6d8c4..00000000 --- a/src/com/android/tv/tuner/exoplayer/ExoPlayerExtractorsFactory.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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.exoplayer; - -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.extractor.Extractor; -import com.google.android.exoplayer2.extractor.ExtractorsFactory; -import com.google.android.exoplayer2.extractor.TimestampAdjuster; -import com.google.android.exoplayer2.extractor.ts.DefaultTsPayloadReaderFactory; -import com.google.android.exoplayer2.extractor.ts.TsExtractor; - -import java.util.ArrayList; -import java.util.List; - -/** - * Extractor factory, mainly aim at create TsExtractor with FLAG_ALLOW_NON_IDR_KEYFRAMES flags for - * H.264 stream - */ -public final class ExoPlayerExtractorsFactory implements ExtractorsFactory { - @Override - public Extractor[] createExtractors() { - // Only create TsExtractor since we only target MPEG2TS stream. - Extractor[] extractors = { - new TsExtractor(new TimestampAdjuster(0), new DefaultTsPayloadReaderFactory( - DefaultTsPayloadReaderFactory.FLAG_ALLOW_NON_IDR_KEYFRAMES), false) }; - return extractors; - } -} diff --git a/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java deleted file mode 100644 index 0b648400..00000000 --- a/src/com/android/tv/tuner/exoplayer/ExoPlayerSampleExtractor.java +++ /dev/null @@ -1,552 +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.exoplayer; - -import android.net.Uri; -import android.os.ConditionVariable; -import android.os.Handler; -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.MediaFormat; -import com.google.android.exoplayer.MediaFormatHolder; -import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.upstream.DataSource; -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.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.audio.MpegTsDefaultAudioTrackRenderer; -import com.android.tv.tuner.exoplayer.buffer.BufferManager; -import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer; -import com.android.tv.tuner.exoplayer.buffer.SimpleSampleBuffer; -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; - -/** - * A class that extracts samples from a live broadcast stream while storing the sample on the disk. - * For demux, this class relies on {@link com.google.android.exoplayer.extractor.ts.TsExtractor}. - */ -public class ExoPlayerSampleExtractor implements SampleExtractor { - private static final String TAG = "ExoPlayerSampleExtracto"; - - private static final int INVALID_TRACK_INDEX = -1; - private final HandlerThread mSourceReaderThread; - private final long mId; - - private final Handler.Callback mSourceReaderWorker; - - private BufferManager.SampleBuffer mSampleBuffer; - private Handler mSourceReaderHandler; - private volatile boolean mPrepared; - 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, final DataSource source, BufferManager bufferManager, - PlaybackBufferListener bufferListener, boolean isRecording) { - // It'll be used as a timeshift file chunk name's prefix. - mId = System.currentTimeMillis(); - - EventListener eventListener = new EventListener() { - @Override - public void onLoadError(IOException error) { - mError = error; - } - }; - - mSourceReaderThread = new HandlerThread("SourceReaderThread"); - 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 ExoPlayerExtractorsFactory(), - // Do not create a handler if we not on a looper. e.g. test. - Looper.myLooper() != null ? new Handler() : null, eventListener)); - if (isRecording) { - mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, false, - RecordingSampleBuffer.BUFFER_REASON_RECORDING); - } else { - if (bufferManager == null) { - mSampleBuffer = new SimpleSampleBuffer(bufferListener); - } else { - mSampleBuffer = new RecordingSampleBuffer(bufferManager, bufferListener, true, - RecordingSampleBuffer.BUFFER_REASON_LIVE_PLAYBACK); - } - } - } - - @Override - public void setOnCompletionListener(OnCompletionListener listener, Handler handler) { - mOnCompletionListener = listener; - mOnCompletionListenerHandler = handler; - } - - 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 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(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: - 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; - ConditionVariable conditionVariable = new ConditionVariable(); - int trackCount = mStreams.length; - for (int i = 0; i < trackCount; ++i) { - 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. - break; - } - didSomething = true; - } - } - mMediaPeriod.continueLoading(mCurrentPosition); - if (!mMetEos) { - if (didSomething) { - mSourceReaderHandler.sendEmptyMessage(MSG_FETCH_SAMPLES); - } else { - mSourceReaderHandler.sendEmptyMessageDelayed(MSG_FETCH_SAMPLES, - RETRY_INTERVAL_MS); - } - } else { - notifyCompletionIfNeeded(false); - } - return true; - case MSG_RELEASE: - if (mMediaPeriod != null) { - mSampleSource.releasePeriod(mMediaPeriod); - mSampleSource.releaseSource(); - mMediaPeriod = null; - } - cleanUp(); - mSourceReaderHandler.removeCallbacksAndMessages(null); - return true; - } - return false; - } - - private int fetchSample(int track, SampleHolder sample, - ConditionVariable conditionVariable) { - 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, mDecoderInputBuffer.timeUs); - } else { - mLastExtractedPositionUsMap.put(track, - Math.max(lastExtractedPositionUs, mDecoderInputBuffer.timeUs)); - } - queueSample(track, conditionVariable); - } catch (IOException e) { - mLastExtractedPositionUsMap.clear(); - mMetEos = true; - mSampleBuffer.setEos(); - } - } 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) { - mMetEos = true; - mSampleBuffer.setEos(); - } - } - } - // TODO: Handle C.RESULT_FORMAT_READ for dynamic resolution change. b/28169263 - return ret; - } - - 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 - - MpegTsDefaultAudioTrackRenderer - .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(); - } - } - } - - @Override - public void maybeThrowError() throws IOException { - if (mError != null) { - IOException e = mError; - mError = null; - throw e; - } - } - - @Override - public boolean prepare() throws IOException { - if (!mSourceReaderThread.isAlive()) { - mSourceReaderThread.start(); - mSourceReaderHandler = new Handler(mSourceReaderThread.getLooper(), - mSourceReaderWorker); - mSourceReaderHandler.sendEmptyMessage(SourceReaderWorker.MSG_PREPARE); - } - if (mExceptionOnPrepare != null) { - throw mExceptionOnPrepare; - } - return mPrepared; - } - - @Override - public List<MediaFormat> getTrackFormats() { - return mTrackFormats; - } - - @Override - public void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder) { - outMediaFormatHolder.format = mTrackFormats.get(track); - outMediaFormatHolder.drmInitData = null; - } - - @Override - public void selectTrack(int index) { - mSampleBuffer.selectTrack(index); - } - - @Override - public void deselectTrack(int index) { - mSampleBuffer.deselectTrack(index); - } - - @Override - public long getBufferedPositionUs() { - return mSampleBuffer.getBufferedPositionUs(); - } - - @Override - public boolean continueBuffering(long positionUs) { - return mSampleBuffer.continueBuffering(positionUs); - } - - @Override - public void seekTo(long positionUs) { - mSampleBuffer.seekTo(positionUs); - } - - @Override - public int readSample(int track, SampleHolder sampleHolder) { - return mSampleBuffer.readSample(track, sampleHolder); - } - - @Override - public void release() { - if (mSourceReaderThread.isAlive()) { - mSourceReaderHandler.removeCallbacksAndMessages(null); - mSourceReaderHandler.sendEmptyMessage(SourceReaderWorker.MSG_RELEASE); - mSourceReaderThread.quitSafely(); - // Return early in this case so that session worker can start working on the next - // request as early as it can. The clean up will be done in the reader thread while - // handling MSG_RELEASE. - } else { - cleanUp(); - } - } - - private void cleanUp() { - boolean result = true; - try { - if (mSampleBuffer != null) { - mSampleBuffer.release(); - mSampleBuffer = null; - } - } catch (IOException e) { - result = false; - } - notifyCompletionIfNeeded(result); - setOnCompletionListener(null, null); - } - - private void notifyCompletionIfNeeded(final boolean result) { - if (!mOnCompletionCalled.getAndSet(true)) { - final OnCompletionListener listener = mOnCompletionListener; - final long lastExtractedPositionUs = getLastExtractedPositionUs(); - if (mOnCompletionListenerHandler != null && mOnCompletionListener != null) { - mOnCompletionListenerHandler.post(new Runnable() { - @Override - public void run() { - listener.onCompletion(result, lastExtractedPositionUs); - } - }); - } - } - } - - private long getLastExtractedPositionUs() { - 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.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 deleted file mode 100644 index b7e42a7c..00000000 --- a/src/com/android/tv/tuner/exoplayer/FileSampleExtractor.java +++ /dev/null @@ -1,138 +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.exoplayer; - -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.android.tv.tuner.exoplayer.buffer.BufferManager; -import com.android.tv.tuner.exoplayer.buffer.RecordingSampleBuffer; -import com.android.tv.tuner.tvinput.PlaybackBufferListener; - -import android.os.Handler; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -/** - * A class that plays a recorded stream without using {@link android.media.MediaExtractor}, - * since all samples are extracted and stored to the permanent storage already. - */ -public class FileSampleExtractor implements SampleExtractor{ - private static final String TAG = "FileSampleExtractor"; - private static final boolean DEBUG = false; - - private int mTrackCount; - private boolean mReleased; - - private final List<MediaFormat> mTrackFormats = new ArrayList<>(); - private final BufferManager mBufferManager; - private final PlaybackBufferListener mBufferListener; - private BufferManager.SampleBuffer mSampleBuffer; - - public FileSampleExtractor( - BufferManager bufferManager, PlaybackBufferListener bufferListener) { - mBufferManager = bufferManager; - mBufferListener = bufferListener; - mTrackCount = -1; - } - - @Override - public void maybeThrowError() throws IOException { - // Do nothing. - } - - @Override - public boolean prepare() throws IOException { - List<BufferManager.TrackFormat> trackFormatList = mBufferManager.readTrackInfoFiles(); - if (trackFormatList == null || trackFormatList.isEmpty()) { - throw new IOException("Cannot find meta files for the recording."); - } - mTrackCount = trackFormatList.size(); - List<String> ids = new ArrayList<>(); - mTrackFormats.clear(); - for (int i = 0; i < mTrackCount; ++i) { - 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); - mSampleBuffer.init(ids, mTrackFormats); - return true; - } - - @Override - public List<MediaFormat> getTrackFormats() { - return mTrackFormats; - } - - @Override - public void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder) { - outMediaFormatHolder.format = mTrackFormats.get(track); - outMediaFormatHolder.drmInitData = null; - } - - @Override - public void release() { - if (!mReleased) { - if (mSampleBuffer != null) { - try { - mSampleBuffer.release(); - } catch (IOException e) { - // Do nothing. Playback ends now. - } - } - } - mReleased = true; - } - - @Override - public void selectTrack(int index) { - mSampleBuffer.selectTrack(index); - } - - @Override - public void deselectTrack(int index) { - mSampleBuffer.deselectTrack(index); - } - - @Override - public long getBufferedPositionUs() { - return mSampleBuffer.getBufferedPositionUs(); - } - - @Override - public void seekTo(long positionUs) { - mSampleBuffer.seekTo(positionUs); - } - - @Override - public int readSample(int track, SampleHolder sampleHolder) { - return mSampleBuffer.readSample(track, sampleHolder); - } - - @Override - public boolean continueBuffering(long positionUs) { - return mSampleBuffer.continueBuffering(positionUs); - } - - @Override - public void setOnCompletionListener(OnCompletionListener listener, Handler handler) { } -} diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java b/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java deleted file mode 100644 index 2694298a..00000000 --- a/src/com/android/tv/tuner/exoplayer/MpegTsPlayer.java +++ /dev/null @@ -1,696 +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.exoplayer; - -import android.content.Context; -import android.media.AudioFormat; -import android.media.MediaCodec.CryptoException; -import android.media.PlaybackParams; -import android.os.Handler; -import android.support.annotation.IntDef; -import android.view.Surface; - -import com.google.android.exoplayer.DummyTrackRenderer; -import com.google.android.exoplayer.ExoPlaybackException; -import com.google.android.exoplayer.ExoPlayer; -import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; -import com.google.android.exoplayer.MediaCodecTrackRenderer.DecoderInitializationException; -import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.TrackRenderer; -import com.google.android.exoplayer.audio.AudioCapabilities; -import com.google.android.exoplayer.audio.AudioTrack; -import com.google.android.exoplayer.upstream.DataSource; -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.audio.MpegTsDefaultAudioTrackRenderer; -import com.android.tv.tuner.exoplayer.audio.MpegTsMediaCodecAudioTrackRenderer; -import com.android.tv.tuner.source.TsDataSource; -import com.android.tv.tuner.source.TsDataSourceManager; -import com.android.tv.tuner.tvinput.EventDetector; -import com.android.tv.tuner.tvinput.TunerDebug; - -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, - MpegTsDefaultAudioTrackRenderer.EventListener, - MpegTsMediaCodecAudioTrackRenderer.Ac3EventListener { - private int mCaptionServiceNumber = Cea708Data.EMPTY_SERVICE_NUMBER; - - /** - * Interface definition for building specific track renderers. - */ - public interface RendererBuilder { - void buildRenderers(MpegTsPlayer mpegTsPlayer, DataSource dataSource, - boolean hasSoftwareAudioDecoder, RendererBuilderCallback callback); - } - - /** - * Interface definition for {@link RendererBuilder#buildRenderers} to notify the result. - */ - public interface RendererBuilderCallback { - void onRenderers(String[][] trackNames, TrackRenderer[] renderers); - void onRenderersError(Exception e); - } - - /** - * Interface definition for a callback to be notified of changes in player state. - */ - public interface Listener { - void onStateChanged(boolean playWhenReady, int playbackState); - void onError(Exception e); - void onVideoSizeChanged(int width, int height, - float pixelWidthHeightRatio); - void onDrawnToSurface(MpegTsPlayer player, Surface surface); - void onAudioUnplayable(); - void onSmoothTrickplayForceStopped(); - } - - /** - * Interface definition for a callback to be notified of changes on video display. - */ - public interface VideoEventListener { - /** - * Notifies the caption event. - */ - void onEmitCaptionEvent(CaptionEvent event); - - /** - * Notifies clearing up whole closed caption event. - */ - void onClearCaptionEvent(); - - /** - * Notifies the discovered caption service number. - */ - void onDiscoverCaptionServiceNumber(int serviceNumber); - } - - public static final int RENDERER_COUNT = 3; - public static final int MIN_BUFFER_MS = 0; - public static final int MIN_REBUFFER_MS = 500; - - @IntDef({TRACK_TYPE_VIDEO, TRACK_TYPE_AUDIO, TRACK_TYPE_TEXT}) - @Retention(RetentionPolicy.SOURCE) - public @interface TrackType {} - public static final int TRACK_TYPE_VIDEO = 0; - public static final int TRACK_TYPE_AUDIO = 1; - public static final int TRACK_TYPE_TEXT = 2; - - @IntDef({RENDERER_BUILDING_STATE_IDLE, RENDERER_BUILDING_STATE_BUILDING, - RENDERER_BUILDING_STATE_BUILT}) - @Retention(RetentionPolicy.SOURCE) - public @interface RendererBuildingState {} - private static final int RENDERER_BUILDING_STATE_IDLE = 1; - private static final int RENDERER_BUILDING_STATE_BUILDING = 2; - private static final int RENDERER_BUILDING_STATE_BUILT = 3; - - private static final float MAX_SMOOTH_TRICKPLAY_SPEED = 9.0f; - private static final float MIN_SMOOTH_TRICKPLAY_SPEED = 0.1f; - - private final RendererBuilder mRendererBuilder; - private final ExoPlayer mPlayer; - private final Handler mMainHandler; - private final AudioCapabilities mAudioCapabilities; - private final TsDataSourceManager mSourceManager; - - private Listener mListener; - @RendererBuildingState private int mRendererBuildingState; - - private Surface mSurface; - private TsDataSource mDataSource; - private InternalRendererBuilderCallback mBuilderCallback; - private TrackRenderer mVideoRenderer; - private TrackRenderer mAudioRenderer; - private Cea708TextTrackRenderer mTextRenderer; - private final Cea708TextTrackRenderer.CcListener mCcListener; - private VideoEventListener mVideoEventListener; - private boolean mTrickplayRunning; - private float mVolume; - - /** - * Creates MPEG2-TS stream player. - * - * @param rendererBuilder the builder of track renderers - * @param handler the handler for the playback events in track renderers - * @param sourceManager the manager for {@link DataSource} - * @param capabilities the {@link AudioCapabilities} of the current device - * @param listener the listener for playback state changes - */ - public MpegTsPlayer(RendererBuilder rendererBuilder, Handler handler, - TsDataSourceManager sourceManager, AudioCapabilities capabilities, - Listener listener) { - mRendererBuilder = rendererBuilder; - mPlayer = ExoPlayer.Factory.newInstance(RENDERER_COUNT, MIN_BUFFER_MS, MIN_REBUFFER_MS); - mPlayer.addListener(this); - mMainHandler = handler; - mAudioCapabilities = capabilities; - mRendererBuildingState = RENDERER_BUILDING_STATE_IDLE; - mCcListener = new MpegTsCcListener(); - mSourceManager = sourceManager; - mListener = listener; - } - - /** - * Sets the video event listener. - * - * @param videoEventListener the listener for video events - */ - public void setVideoEventListener(VideoEventListener videoEventListener) { - mVideoEventListener = videoEventListener; - } - - /** - * Sets the closed caption service number. - * - * @param captionServiceNumber the service number of CEA-708 closed caption - */ - public void setCaptionServiceNumber(int captionServiceNumber) { - mCaptionServiceNumber = captionServiceNumber; - if (mTextRenderer != null) { - mPlayer.sendMessage(mTextRenderer, - Cea708TextTrackRenderer.MSG_SERVICE_NUMBER, mCaptionServiceNumber); - } - } - - /** - * Sets the surface for the player. - * - * @param surface the {@link Surface} to render video - */ - public void setSurface(Surface surface) { - mSurface = surface; - pushSurface(false); - } - - /** - * Returns the current surface of the player. - */ - public Surface getSurface() { - return mSurface; - } - - /** - * Clears the surface and waits until the surface is being cleaned. - */ - public void blockingClearSurface() { - mSurface = null; - pushSurface(true); - } - - /** - * Creates renderers and {@link DataSource} and initializes player. - * @param context a {@link Context} instance - * @param channel to play - * @param hasSoftwareAudioDecoder {@code true} if there is connected software decoder - * @param eventListener for program information which will be scanned from MPEG2-TS stream - * @return true when everything is created and initialized well, false otherwise - */ - public boolean prepare(Context context, TunerChannel channel, boolean hasSoftwareAudioDecoder, - EventDetector.EventListener eventListener) { - TsDataSource source = null; - if (channel != null) { - source = mSourceManager.createDataSource(context, channel, eventListener); - if (source == null) { - return false; - } - } - mDataSource = source; - if (mRendererBuildingState == RENDERER_BUILDING_STATE_BUILT) { - mPlayer.stop(); - } - if (mBuilderCallback != null) { - mBuilderCallback.cancel(); - } - mRendererBuildingState = RENDERER_BUILDING_STATE_BUILDING; - mBuilderCallback = new InternalRendererBuilderCallback(); - mRendererBuilder.buildRenderers(this, source, hasSoftwareAudioDecoder, mBuilderCallback); - return true; - } - - /** - * Returns {@link TsDataSource} which provides MPEG2-TS stream. - */ - public TsDataSource getDataSource() { - return mDataSource; - } - - private void onRenderers(TrackRenderer[] renderers) { - mBuilderCallback = null; - for (int i = 0; i < RENDERER_COUNT; i++) { - if (renderers[i] == null) { - // Convert a null renderer to a dummy renderer. - renderers[i] = new DummyTrackRenderer(); - } - } - mVideoRenderer = renderers[TRACK_TYPE_VIDEO]; - mAudioRenderer = renderers[TRACK_TYPE_AUDIO]; - mTextRenderer = (Cea708TextTrackRenderer) renderers[TRACK_TYPE_TEXT]; - mTextRenderer.setCcListener(mCcListener); - mPlayer.sendMessage( - mTextRenderer, Cea708TextTrackRenderer.MSG_SERVICE_NUMBER, mCaptionServiceNumber); - mRendererBuildingState = RENDERER_BUILDING_STATE_BUILT; - pushSurface(false); - mPlayer.prepare(renderers); - pushTrackSelection(TRACK_TYPE_VIDEO, true); - pushTrackSelection(TRACK_TYPE_AUDIO, true); - pushTrackSelection(TRACK_TYPE_TEXT, true); - } - - private void onRenderersError(Exception e) { - mBuilderCallback = null; - mRendererBuildingState = RENDERER_BUILDING_STATE_IDLE; - if (mListener != null) { - mListener.onError(e); - } - } - - /** - * Sets the player state to pause or play. - * - * @param playWhenReady sets the player state to being ready to play when {@code true}, - * sets the player state to being paused when {@code false} - * - */ - public void setPlayWhenReady(boolean playWhenReady) { - mPlayer.setPlayWhenReady(playWhenReady); - stopSmoothTrickplay(false); - } - - /** - * Returns true, if trickplay is supported. - */ - public boolean supportSmoothTrickPlay(float playbackSpeed) { - return playbackSpeed > MIN_SMOOTH_TRICKPLAY_SPEED - && playbackSpeed < MAX_SMOOTH_TRICKPLAY_SPEED; - } - - /** - * Starts trickplay. It'll be reset, if {@link #seekTo} or {@link #setPlayWhenReady} is called. - */ - public void startSmoothTrickplay(PlaybackParams playbackParams) { - SoftPreconditions.checkState(supportSmoothTrickPlay(playbackParams.getSpeed())); - mPlayer.setPlayWhenReady(true); - mTrickplayRunning = true; - if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) { - mPlayer.sendMessage( - mAudioRenderer, - MpegTsDefaultAudioTrackRenderer.MSG_SET_PLAYBACK_SPEED, - playbackParams.getSpeed()); - } else { - mPlayer.sendMessage(mAudioRenderer, - MediaCodecAudioTrackRenderer.MSG_SET_PLAYBACK_PARAMS, - playbackParams); - } - } - - private void stopSmoothTrickplay(boolean calledBySeek) { - if (mTrickplayRunning) { - mTrickplayRunning = false; - if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) { - mPlayer.sendMessage( - mAudioRenderer, MpegTsDefaultAudioTrackRenderer.MSG_SET_PLAYBACK_SPEED, - 1.0f); - } else { - mPlayer.sendMessage(mAudioRenderer, - MediaCodecAudioTrackRenderer.MSG_SET_PLAYBACK_PARAMS, - new PlaybackParams().setSpeed(1.0f)); - } - if (!calledBySeek) { - mPlayer.seekTo(mPlayer.getCurrentPosition()); - } - } - } - - /** - * Seeks to the specified position of the current playback. - * - * @param positionMs the specified position in milli seconds. - */ - public void seekTo(long positionMs) { - mPlayer.seekTo(positionMs); - stopSmoothTrickplay(true); - } - - /** - * Releases the player. - */ - public void release() { - if (mDataSource != null) { - mSourceManager.releaseDataSource(mDataSource); - mDataSource = null; - } - if (mBuilderCallback != null) { - mBuilderCallback.cancel(); - mBuilderCallback = null; - } - mRendererBuildingState = RENDERER_BUILDING_STATE_IDLE; - mSurface = null; - mListener = null; - mPlayer.release(); - } - - /** - * Returns the current status of the player. - */ - public int getPlaybackState() { - if (mRendererBuildingState == RENDERER_BUILDING_STATE_BUILDING) { - return ExoPlayer.STATE_PREPARING; - } - return mPlayer.getPlaybackState(); - } - - /** - * Returns {@code true} when the player is prepared to play, {@code false} otherwise. - */ - public boolean isPrepared() { - int state = getPlaybackState(); - return state == ExoPlayer.STATE_READY || state == ExoPlayer.STATE_BUFFERING; - } - - /** - * Returns {@code true} when the player is being ready to play, {@code false} otherwise. - */ - public boolean isPlaying() { - int state = getPlaybackState(); - return (state == ExoPlayer.STATE_READY || state == ExoPlayer.STATE_BUFFERING) - && mPlayer.getPlayWhenReady(); - } - - /** - * Returns {@code true} when the player is buffering, {@code false} otherwise. - */ - public boolean isBuffering() { - return getPlaybackState() == ExoPlayer.STATE_BUFFERING; - } - - /** - * Returns the current position of the playback in milli seconds. - */ - public long getCurrentPosition() { - return mPlayer.getCurrentPosition(); - } - - /** - * Returns the total duration of the playback. - */ - public long getDuration() { - return mPlayer.getDuration(); - } - - /** - * Returns {@code true} when the player is being ready to play, - * {@code false} when the player is paused. - */ - public boolean getPlayWhenReady() { - return mPlayer.getPlayWhenReady(); - } - - /** - * Sets the volume of the audio. - * - * @param volume see also {@link AudioTrack#setVolume(float)} - */ - public void setVolume(float volume) { - mVolume = volume; - if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) { - mPlayer.sendMessage(mAudioRenderer, MpegTsDefaultAudioTrackRenderer.MSG_SET_VOLUME, - volume); - } else { - mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME, - volume); - } - } - - /** - * Enables or disables audio and closed caption. - * - * @param enable enables the audio and closed caption when {@code true}, disables otherwise. - */ - public void setAudioTrackAndClosedCaption(boolean enable) { - if (mAudioRenderer instanceof MpegTsDefaultAudioTrackRenderer) { - mPlayer.sendMessage(mAudioRenderer, MpegTsDefaultAudioTrackRenderer.MSG_SET_AUDIO_TRACK, - enable ? 1 : 0); - } else { - mPlayer.sendMessage(mAudioRenderer, MediaCodecAudioTrackRenderer.MSG_SET_VOLUME, - enable ? mVolume : 0.0f); - } - mPlayer.sendMessage(mTextRenderer, Cea708TextTrackRenderer.MSG_ENABLE_CLOSED_CAPTION, - enable); - } - - /** - * Returns {@code true} when AC3 audio can be played, {@code false} otherwise. - */ - public boolean isAc3Playable() { - return mAudioCapabilities != null - && mAudioCapabilities.supportsEncoding(AudioFormat.ENCODING_AC3); - } - - /** - * Notifies when the audio cannot be played by the current device. - */ - public void onAudioUnplayable() { - if (mListener != null) { - mListener.onAudioUnplayable(); - } - } - - /** - * Returns {@code true} if the player has any video track, {@code false} otherwise. - */ - public boolean hasVideo() { - return mPlayer.getTrackCount(TRACK_TYPE_VIDEO) > 0; - } - - /** - * Returns {@code true} if the player has any audio trock, {@code false} otherwise. - */ - public boolean hasAudio() { - return mPlayer.getTrackCount(TRACK_TYPE_AUDIO) > 0; - } - - /** - * Returns the number of tracks exposed by the specified renderer. - */ - public int getTrackCount(int rendererIndex) { - return mPlayer.getTrackCount(rendererIndex); - } - - /** - * Selects a track for the specified renderer. - */ - public void setSelectedTrack(int rendererIndex, int trackIndex) { - if (trackIndex >= getTrackCount(rendererIndex)) { - return; - } - mPlayer.setSelectedTrack(rendererIndex, trackIndex); - } - - /** - * 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() { - return mMainHandler; - } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int state) { - if (mListener == null) { - return; - } - mListener.onStateChanged(playWhenReady, state); - if (state == ExoPlayer.STATE_READY && mPlayer.getTrackCount(TRACK_TYPE_VIDEO) > 0 - && playWhenReady) { - MediaFormat format = mPlayer.getTrackFormat(TRACK_TYPE_VIDEO, 0); - mListener.onVideoSizeChanged(format.width, - format.height, format.pixelWidthHeightRatio); - } - } - - @Override - public void onPlayerError(ExoPlaybackException exception) { - mRendererBuildingState = RENDERER_BUILDING_STATE_IDLE; - if (mListener != null) { - mListener.onError(exception); - } - } - - @Override - public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio) { - if (mListener != null) { - mListener.onVideoSizeChanged(width, height, pixelWidthHeightRatio); - } - } - - @Override - public void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, - long initializationDurationMs) { - // Do nothing. - } - - @Override - public void onDecoderInitializationError(DecoderInitializationException e) { - // Do nothing. - } - - @Override - public void onAudioTrackInitializationError(AudioTrack.InitializationException e) { - if (mListener != null) { - mListener.onAudioUnplayable(); - } - } - - @Override - public void onAudioTrackWriteError(AudioTrack.WriteException e) { - // Do nothing. - } - - @Override - public void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs, - long elapsedSinceLastFeedMs) { - // Do nothing. - } - - @Override - public void onCryptoError(CryptoException e) { - // Do nothing. - } - - @Override - public void onPlayWhenReadyCommitted() { - // Do nothing. - } - - @Override - public void onDrawnToSurface(Surface surface) { - if (mListener != null) { - mListener.onDrawnToSurface(this, surface); - } - } - - @Override - public void onDroppedFrames(int count, long elapsed) { - TunerDebug.notifyVideoFrameDrop(count, elapsed); - if (mTrickplayRunning && mListener != null) { - mListener.onSmoothTrickplayForceStopped(); - } - } - - @Override - public void onAudioTrackSetPlaybackParamsError(IllegalArgumentException e) { - if (mTrickplayRunning && mListener != null) { - mListener.onSmoothTrickplayForceStopped(); - } - } - - private void pushSurface(boolean blockForSurfacePush) { - if (mRendererBuildingState != RENDERER_BUILDING_STATE_BUILT) { - return; - } - - if (blockForSurfacePush) { - mPlayer.blockingSendMessage( - mVideoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, mSurface); - } else { - mPlayer.sendMessage( - mVideoRenderer, MediaCodecVideoTrackRenderer.MSG_SET_SURFACE, mSurface); - } - } - - private void pushTrackSelection(@TrackType int type, boolean allowRendererEnable) { - if (mRendererBuildingState != RENDERER_BUILDING_STATE_BUILT) { - return; - } - mPlayer.setSelectedTrack(type, allowRendererEnable ? 0 : -1); - } - - private class MpegTsCcListener implements Cea708TextTrackRenderer.CcListener { - - @Override - public void emitEvent(CaptionEvent captionEvent) { - if (mVideoEventListener != null) { - mVideoEventListener.onEmitCaptionEvent(captionEvent); - } - } - - @Override - public void clearCaption() { - if (mVideoEventListener != null) { - mVideoEventListener.onClearCaptionEvent(); - } - } - - @Override - public void discoverServiceNumber(int serviceNumber) { - if (mVideoEventListener != null) { - mVideoEventListener.onDiscoverCaptionServiceNumber(serviceNumber); - } - } - } - - private class InternalRendererBuilderCallback implements RendererBuilderCallback { - private boolean canceled; - - public void cancel() { - canceled = true; - } - - @Override - public void onRenderers(String[][] trackNames, TrackRenderer[] renderers) { - if (!canceled) { - MpegTsPlayer.this.onRenderers(renderers); - } - } - - @Override - public void onRenderersError(Exception e) { - if (!canceled) { - MpegTsPlayer.this.onRenderersError(e); - } - } - } -}
\ 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 deleted file mode 100644 index 006ccac2..00000000 --- a/src/com/android/tv/tuner/exoplayer/MpegTsRendererBuilder.java +++ /dev/null @@ -1,75 +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.exoplayer; - -import android.content.Context; - -import com.google.android.exoplayer.MediaCodecSelector; -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.audio.MpegTsDefaultAudioTrackRenderer; -import com.android.tv.tuner.exoplayer.buffer.BufferManager; -import com.android.tv.tuner.tvinput.PlaybackBufferListener; - -/** - * Builder for renderer objects for {@link MpegTsPlayer}. - */ -public class MpegTsRendererBuilder implements RendererBuilder { - private final Context mContext; - private final BufferManager mBufferManager; - private final PlaybackBufferListener mBufferListener; - - public MpegTsRendererBuilder(Context context, BufferManager bufferManager, - PlaybackBufferListener bufferListener) { - mContext = context; - mBufferManager = bufferManager; - mBufferListener = bufferListener; - } - - @Override - public void buildRenderers(MpegTsPlayer mpegTsPlayer, DataSource dataSource, - boolean mHasSoftwareAudioDecoder, RendererBuilderCallback callback) { - // Build the video and audio renderers. - SampleExtractor extractor = dataSource == null ? - new MpegTsSampleExtractor(mBufferManager, mBufferListener) : - new MpegTsSampleExtractor(dataSource, mBufferManager, mBufferListener); - SampleSource sampleSource = new MpegTsSampleSource(extractor); - MpegTsVideoTrackRenderer videoRenderer = new MpegTsVideoTrackRenderer(mContext, - sampleSource, mpegTsPlayer.getMainHandler(), mpegTsPlayer); - // TODO: Only using MpegTsDefaultAudioTrackRenderer for A/V sync issue. We will use - // {@link MpegTsMediaCodecAudioTrackRenderer} when we use ExoPlayer's extractor. - TrackRenderer audioRenderer = - new MpegTsDefaultAudioTrackRenderer( - sampleSource, - MediaCodecSelector.DEFAULT, - mpegTsPlayer.getMainHandler(), - mpegTsPlayer, - mHasSoftwareAudioDecoder, - !Features.AC3_SOFTWARE_DECODE.isEnabled(mContext)); - Cea708TextTrackRenderer textRenderer = new Cea708TextTrackRenderer(sampleSource); - - TrackRenderer[] renderers = new TrackRenderer[MpegTsPlayer.RENDERER_COUNT]; - renderers[MpegTsPlayer.TRACK_TYPE_VIDEO] = videoRenderer; - renderers[MpegTsPlayer.TRACK_TYPE_AUDIO] = audioRenderer; - renderers[MpegTsPlayer.TRACK_TYPE_TEXT] = textRenderer; - callback.onRenderers(null, renderers); - } -} diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java b/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java deleted file mode 100644 index 7bf116c8..00000000 --- a/src/com/android/tv/tuner/exoplayer/MpegTsSampleExtractor.java +++ /dev/null @@ -1,335 +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.exoplayer; - -import android.net.Uri; -import android.os.Handler; - -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.upstream.DataSource; -import com.google.android.exoplayer.util.MimeTypes; -import com.android.tv.tuner.exoplayer.buffer.BufferManager; -import com.android.tv.tuner.exoplayer.buffer.SamplePool; -import com.android.tv.tuner.tvinput.PlaybackBufferListener; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; - -/** - * Extracts samples from {@link DataSource} for MPEG-TS streams. - */ -public final class MpegTsSampleExtractor implements SampleExtractor { - public static final String MIMETYPE_TEXT_CEA_708 = "text/cea-708"; - - private static final int CC_BUFFER_SIZE_IN_BYTES = 9600 / 8; - - private final SampleExtractor mSampleExtractor; - private final List<MediaFormat> mTrackFormats = new ArrayList<>(); - private final List<Boolean> mReachedEos = new ArrayList<>(); - private int mVideoTrackIndex; - private final SamplePool mCcSamplePool = new SamplePool(); - private final List<SampleHolder> mPendingCcSamples = new LinkedList<>(); - - private int mCea708TextTrackIndex; - private boolean mCea708TextTrackSelected; - - private CcParser mCcParser; - - private void init() { - mVideoTrackIndex = -1; - mCea708TextTrackIndex = -1; - mCea708TextTrackSelected = false; - } - - /** - * Creates MpegTsSampleExtractor for {@link DataSource}. - * - * @param source the {@link DataSource} to extract from - * @param bufferManager the manager for reading & writing samples backed by physical storage - * @param bufferListener the {@link PlaybackBufferListener} - * to notify buffer storage status change - */ - public MpegTsSampleExtractor(DataSource source, BufferManager bufferManager, - PlaybackBufferListener bufferListener) { - mSampleExtractor = new ExoPlayerSampleExtractor(Uri.EMPTY, source, bufferManager, - bufferListener, false); - init(); - } - - /** - * Creates MpegTsSampleExtractor for a recorded program. - * - * @param bufferManager the samples provider which is stored in physical storage - * @param bufferListener the {@link PlaybackBufferListener} - * to notify buffer storage status change - */ - public MpegTsSampleExtractor(BufferManager bufferManager, - PlaybackBufferListener bufferListener) { - mSampleExtractor = new FileSampleExtractor(bufferManager, bufferListener); - init(); - } - - @Override - public void maybeThrowError() throws IOException { - if (mSampleExtractor != null) { - mSampleExtractor.maybeThrowError(); - } - } - - @Override - public boolean prepare() throws IOException { - if(!mSampleExtractor.prepare()) { - return false; - } - List<MediaFormat> formats = mSampleExtractor.getTrackFormats(); - int trackCount = formats.size(); - mTrackFormats.clear(); - mReachedEos.clear(); - - for (int i = 0; i < trackCount; ++i) { - mTrackFormats.add(formats.get(i)); - mReachedEos.add(false); - String mime = formats.get(i).mimeType; - if (MimeTypes.isVideo(mime) && mVideoTrackIndex == -1) { - mVideoTrackIndex = i; - if (android.media.MediaFormat.MIMETYPE_VIDEO_MPEG2.equals(mime)) { - mCcParser = new Mpeg2CcParser(); - } else if (android.media.MediaFormat.MIMETYPE_VIDEO_AVC.equals(mime)) { - mCcParser = new H264CcParser(); - } - } - } - - if (mVideoTrackIndex != -1) { - mCea708TextTrackIndex = trackCount; - } - if (mCea708TextTrackIndex >= 0) { - mTrackFormats.add(MediaFormat.createTextFormat(null, MIMETYPE_TEXT_CEA_708, 0, - mTrackFormats.get(0).durationUs, "")); - } - return true; - } - - @Override - public List<MediaFormat> getTrackFormats() { - return mTrackFormats; - } - - @Override - public void selectTrack(int index) { - if (index == mCea708TextTrackIndex) { - mCea708TextTrackSelected = true; - return; - } - mSampleExtractor.selectTrack(index); - } - - @Override - public void deselectTrack(int index) { - if (index == mCea708TextTrackIndex) { - mCea708TextTrackSelected = false; - return; - } - mSampleExtractor.deselectTrack(index); - } - - @Override - public long getBufferedPositionUs() { - return mSampleExtractor.getBufferedPositionUs(); - } - - @Override - public void seekTo(long positionUs) { - mSampleExtractor.seekTo(positionUs); - for (SampleHolder holder : mPendingCcSamples) { - mCcSamplePool.releaseSample(holder); - } - mPendingCcSamples.clear(); - } - - @Override - public void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder) { - if (track != mCea708TextTrackIndex) { - mSampleExtractor.getTrackMediaFormat(track, outMediaFormatHolder); - } - } - - @Override - public int readSample(int track, SampleHolder sampleHolder) { - if (track == mCea708TextTrackIndex) { - if (mCea708TextTrackSelected && !mPendingCcSamples.isEmpty()) { - SampleHolder holder = mPendingCcSamples.remove(0); - holder.data.flip(); - sampleHolder.timeUs = holder.timeUs; - sampleHolder.data.put(holder.data); - mCcSamplePool.releaseSample(holder); - return SampleSource.SAMPLE_READ; - } else { - return mVideoTrackIndex < 0 || mReachedEos.get(mVideoTrackIndex) - ? SampleSource.END_OF_STREAM : SampleSource.NOTHING_READ; - } - } - - int result = mSampleExtractor.readSample(track, sampleHolder); - switch (result) { - case SampleSource.END_OF_STREAM: { - mReachedEos.set(track, true); - break; - } - case SampleSource.SAMPLE_READ: { - if (mCea708TextTrackSelected && track == mVideoTrackIndex - && sampleHolder.data != null) { - mCcParser.mayParseClosedCaption(sampleHolder.data, sampleHolder.timeUs); - } - break; - } - } - return result; - } - - @Override - public void release() { - mSampleExtractor.release(); - mVideoTrackIndex = -1; - mCea708TextTrackIndex = -1; - mCea708TextTrackSelected = false; - } - - @Override - public boolean continueBuffering(long positionUs) { - return mSampleExtractor.continueBuffering(positionUs); - } - - @Override - public void setOnCompletionListener(OnCompletionListener listener, Handler handler) { } - - private abstract class CcParser { - // Interim buffer for reduce direct access to ByteBuffer which is expensive. Using - // relatively small buffer size in order to minimize memory footprint increase. - protected final byte[] mBuffer = new byte[1024]; - - abstract void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs); - - protected int parseClosedCaption(ByteBuffer buffer, int offset, long presentationTimeUs) { - // For the details of user_data_type_structure, see ATSC A/53 Part 4 - Table 6.9. - int pos = offset; - if (pos + 2 >= buffer.position()) { - return offset; - } - boolean processCcDataFlag = (buffer.get(pos) & 64) != 0; - int ccCount = buffer.get(pos) & 0x1f; - pos += 2; - if (!processCcDataFlag || pos + 3 * ccCount >= buffer.position() || ccCount == 0) { - return offset; - } - SampleHolder holder = mCcSamplePool.acquireSample(CC_BUFFER_SIZE_IN_BYTES); - for (int i = 0; i < 3 * ccCount; i++) { - holder.data.put(buffer.get(pos++)); - } - holder.timeUs = presentationTimeUs; - mPendingCcSamples.add(holder); - return pos; - } - } - - private class Mpeg2CcParser extends CcParser { - private static final int PATTERN_LENGTH = 9; - - @Override - public void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs) { - int totalSize = buffer.position(); - // Reading the frame in bulk to reduce the overhead from ByteBuffer.get() with - // overlapping to handle the case that the pattern exists in the boundary. - for (int i = 0; i < totalSize; i += mBuffer.length - PATTERN_LENGTH) { - buffer.position(i); - int size = Math.min(totalSize - i, mBuffer.length); - buffer.get(mBuffer, 0, size); - int j = 0; - while (j < size - PATTERN_LENGTH) { - // Find the start prefix code of private user data. - if (mBuffer[j] == 0 - && mBuffer[j + 1] == 0 - && mBuffer[j + 2] == 1 - && (mBuffer[j + 3] & 0xff) == 0xb2) { - // ATSC closed caption data embedded in MPEG2VIDEO stream has 'GA94' user - // identifier and user data type code 3. - if (mBuffer[j + 4] == 'G' - && mBuffer[j + 5] == 'A' - && mBuffer[j + 6] == '9' - && mBuffer[j + 7] == '4' - && mBuffer[j + 8] == 3) { - j = parseClosedCaption(buffer, i + j + PATTERN_LENGTH, - presentationTimeUs) - i; - } else { - j += PATTERN_LENGTH; - } - } else { - ++j; - } - } - } - buffer.position(totalSize); - } - } - - private class H264CcParser extends CcParser { - private static final int PATTERN_LENGTH = 14; - - @Override - public void mayParseClosedCaption(ByteBuffer buffer, long presentationTimeUs) { - int totalSize = buffer.position(); - // Reading the frame in bulk to reduce the overhead from ByteBuffer.get() with - // overlapping to handle the case that the pattern exists in the boundary. - for (int i = 0; i < totalSize; i += mBuffer.length - PATTERN_LENGTH) { - buffer.position(i); - int size = Math.min(totalSize - i, mBuffer.length); - buffer.get(mBuffer, 0, size); - int j = 0; - while (j < size - PATTERN_LENGTH) { - // Find the start prefix code of a NAL Unit. - if (mBuffer[j] == 0 - && mBuffer[j + 1] == 0 - && mBuffer[j + 2] == 1) { - int nalType = mBuffer[j + 3] & 0x1f; - int payloadType = mBuffer[j + 4] & 0xff; - - // ATSC closed caption data embedded in H264 private user data has NAL type - // 6, payload type 4, and 'GA94' user identifier for ATSC. - if (nalType == 6 && payloadType == 4 && mBuffer[j + 9] == 'G' - && mBuffer[j + 10] == 'A' - && mBuffer[j + 11] == '9' - && mBuffer[j + 12] == '4') { - j = parseClosedCaption(buffer, i + j + PATTERN_LENGTH, - presentationTimeUs) - i; - } else { - j += 7; - } - } else { - ++j; - } - } - } - buffer.position(totalSize); - } - } -} diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsSampleSource.java b/src/com/android/tv/tuner/exoplayer/MpegTsSampleSource.java deleted file mode 100644 index 6007b0be..00000000 --- a/src/com/android/tv/tuner/exoplayer/MpegTsSampleSource.java +++ /dev/null @@ -1,196 +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.exoplayer; - -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.MediaFormatHolder; -import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.SampleSource; -import com.google.android.exoplayer.SampleSource.SampleSourceReader; -import com.google.android.exoplayer.util.Assertions; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -/** {@link SampleSource} that extracts sample data using a {@link SampleExtractor}. */ -public final class MpegTsSampleSource implements SampleSource, SampleSourceReader { - - private static final int TRACK_STATE_DISABLED = 0; - private static final int TRACK_STATE_ENABLED = 1; - private static final int TRACK_STATE_FORMAT_SENT = 2; - - private final SampleExtractor mSampleExtractor; - private final List<Integer> mTrackStates = new ArrayList<>(); - private final List<Boolean> mPendingDiscontinuities = new ArrayList<>(); - - private boolean mPrepared; - private IOException mPreparationError; - private int mRemainingReleaseCount; - - private long mLastSeekPositionUs; - private long mPendingSeekPositionUs; - - /** - * Creates a new sample source that extracts samples using {@code mSampleExtractor}. - * - * @param sampleExtractor a sample extractor for accessing media samples - */ - public MpegTsSampleSource(SampleExtractor sampleExtractor) { - mSampleExtractor = Assertions.checkNotNull(sampleExtractor); - } - - @Override - public SampleSourceReader register() { - mRemainingReleaseCount++; - return this; - } - - @Override - public boolean prepare(long positionUs) { - if (!mPrepared) { - if (mPreparationError != null) { - return false; - } - try { - if (mSampleExtractor.prepare()) { - int trackCount = mSampleExtractor.getTrackFormats().size(); - mTrackStates.clear(); - mPendingDiscontinuities.clear(); - for (int i = 0; i < trackCount; ++i) { - mTrackStates.add(i, TRACK_STATE_DISABLED); - mPendingDiscontinuities.add(i, false); - } - mPrepared = true; - } else { - return false; - } - } catch (IOException e) { - mPreparationError = e; - return false; - } - } - return true; - } - - @Override - public int getTrackCount() { - Assertions.checkState(mPrepared); - return mSampleExtractor.getTrackFormats().size(); - } - - @Override - public MediaFormat getFormat(int track) { - Assertions.checkState(mPrepared); - return mSampleExtractor.getTrackFormats().get(track); - } - - @Override - public void enable(int track, long positionUs) { - Assertions.checkState(mPrepared); - Assertions.checkState(mTrackStates.get(track) == TRACK_STATE_DISABLED); - mTrackStates.set(track, TRACK_STATE_ENABLED); - mSampleExtractor.selectTrack(track); - seekToUsInternal(positionUs, positionUs != 0); - } - - @Override - public void disable(int track) { - Assertions.checkState(mPrepared); - Assertions.checkState(mTrackStates.get(track) != TRACK_STATE_DISABLED); - mSampleExtractor.deselectTrack(track); - mPendingDiscontinuities.set(track, false); - mTrackStates.set(track, TRACK_STATE_DISABLED); - } - - @Override - public boolean continueBuffering(int track, long positionUs) { - return mSampleExtractor.continueBuffering(positionUs); - } - - @Override - public long readDiscontinuity(int track) { - if (mPendingDiscontinuities.get(track)) { - mPendingDiscontinuities.set(track, false); - return mLastSeekPositionUs; - } - return NO_DISCONTINUITY; - } - - @Override - public int readData(int track, long positionUs, MediaFormatHolder formatHolder, - SampleHolder sampleHolder) { - Assertions.checkState(mPrepared); - Assertions.checkState(mTrackStates.get(track) != TRACK_STATE_DISABLED); - if (mPendingDiscontinuities.get(track)) { - return NOTHING_READ; - } - if (mTrackStates.get(track) != TRACK_STATE_FORMAT_SENT) { - mSampleExtractor.getTrackMediaFormat(track, formatHolder); - mTrackStates.set(track, TRACK_STATE_FORMAT_SENT); - return FORMAT_READ; - } - - mPendingSeekPositionUs = C.UNKNOWN_TIME_US; - return mSampleExtractor.readSample(track, sampleHolder); - } - - @Override - public void maybeThrowError() throws IOException { - if (mPreparationError != null) { - throw mPreparationError; - } - if (mSampleExtractor != null) { - mSampleExtractor.maybeThrowError(); - } - } - - @Override - public void seekToUs(long positionUs) { - Assertions.checkState(mPrepared); - seekToUsInternal(positionUs, false); - } - - @Override - public long getBufferedPositionUs() { - Assertions.checkState(mPrepared); - return mSampleExtractor.getBufferedPositionUs(); - } - - @Override - public void release() { - Assertions.checkState(mRemainingReleaseCount > 0); - if (--mRemainingReleaseCount == 0) { - mSampleExtractor.release(); - } - } - - private void seekToUsInternal(long positionUs, boolean force) { - // Unless forced, avoid duplicate calls to the underlying extractor's seek method - // in the case that there have been no interleaving calls to readSample. - if (force || mPendingSeekPositionUs != positionUs) { - mLastSeekPositionUs = positionUs; - mPendingSeekPositionUs = positionUs; - mSampleExtractor.seekTo(positionUs); - for (int i = 0; i < mTrackStates.size(); ++i) { - if (mTrackStates.get(i) != TRACK_STATE_DISABLED) { - mPendingDiscontinuities.set(i, true); - } - } - } - } -} diff --git a/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java deleted file mode 100644 index 19360c69..00000000 --- a/src/com/android/tv/tuner/exoplayer/MpegTsVideoTrackRenderer.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.android.tv.tuner.exoplayer; - -import android.content.Context; -import android.media.MediaCodec; -import android.os.Handler; -import android.util.Log; - -import com.google.android.exoplayer.DecoderInfo; -import com.google.android.exoplayer.ExoPlaybackException; -import com.google.android.exoplayer.MediaCodecSelector; -import com.google.android.exoplayer.MediaCodecUtil; -import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; -import com.google.android.exoplayer.MediaFormatHolder; -import com.google.android.exoplayer.MediaSoftwareCodecUtil; -import com.google.android.exoplayer.SampleSource; -import com.android.tv.common.feature.CommonFeatures; - -import java.lang.reflect.Field; - -/** - * MPEG-2 TS video track renderer - */ -public class MpegTsVideoTrackRenderer extends MediaCodecVideoTrackRenderer { - private static final String TAG = "MpegTsVideoTrackRender"; - - private static final int VIDEO_PLAYBACK_DEADLINE_IN_MS = 5000; - // If DROPPED_FRAMES_NOTIFICATION_THRESHOLD frames are consecutively dropped, it'll be notified. - private static final int DROPPED_FRAMES_NOTIFICATION_THRESHOLD = 10; - private static final int MIN_HD_HEIGHT = 720; - private static final String MIMETYPE_MPEG2 = "video/mpeg2"; - private static Field sRenderedFirstFrameField; - - private final boolean mIsSwCodecEnabled; - private boolean mCodecIsSwPreferred; - private boolean mSetRenderedFirstFrame; - - static { - // Remove the reflection below once b/31223646 is resolved. - try { - sRenderedFirstFrameField = MediaCodecVideoTrackRenderer.class.getDeclaredField( - "renderedFirstFrame"); - sRenderedFirstFrameField.setAccessible(true); - } catch (NoSuchFieldException e) { - // Null-checking for {@code sRenderedFirstFrameField} will do the error handling. - } - } - - public MpegTsVideoTrackRenderer(Context context, SampleSource source, Handler handler, - MediaCodecVideoTrackRenderer.EventListener listener) { - super(context, source, MediaCodecSelector.DEFAULT, - MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, VIDEO_PLAYBACK_DEADLINE_IN_MS, handler, - listener, DROPPED_FRAMES_NOTIFICATION_THRESHOLD); - mIsSwCodecEnabled = CommonFeatures.USE_SW_CODEC_FOR_SD.isEnabled(context); - } - - @Override - protected DecoderInfo getDecoderInfo(MediaCodecSelector codecSelector, String mimeType, - boolean requiresSecureDecoder) throws MediaCodecUtil.DecoderQueryException { - try { - if (mIsSwCodecEnabled && mCodecIsSwPreferred) { - DecoderInfo swCodec = MediaSoftwareCodecUtil.getSoftwareDecoderInfo( - mimeType, requiresSecureDecoder); - if (swCodec != null) { - return swCodec; - } - } - } catch (MediaSoftwareCodecUtil.DecoderQueryException e) { - } - return super.getDecoderInfo(codecSelector, mimeType,requiresSecureDecoder); - } - - @Override - protected void onInputFormatChanged(MediaFormatHolder holder) throws ExoPlaybackException { - mCodecIsSwPreferred = MIMETYPE_MPEG2.equalsIgnoreCase(holder.format.mimeType) - && holder.format.height < MIN_HD_HEIGHT; - super.onInputFormatChanged(holder); - } - - @Override - protected void onDiscontinuity(long positionUs) throws ExoPlaybackException { - super.onDiscontinuity(positionUs); - // Disabling pre-rendering of the first frame in order to avoid a frozen picture when - // starting the playback. We do this only once, when the renderer is enabled at first, since - // we need to pre-render the frame in advance when we do trickplay backed by seeking. - if (!mSetRenderedFirstFrame) { - setRenderedFirstFrame(true); - mSetRenderedFirstFrame = true; - } - } - - private void setRenderedFirstFrame(boolean renderedFirstFrame) { - if (sRenderedFirstFrameField != null) { - try { - sRenderedFirstFrameField.setBoolean(this, renderedFirstFrame); - } catch (IllegalAccessException e) { - Log.w(TAG, "renderedFirstFrame is not accessible. Playback may start with a frozen" - +" picture."); - } - } - } -} diff --git a/src/com/android/tv/tuner/exoplayer/SampleExtractor.java b/src/com/android/tv/tuner/exoplayer/SampleExtractor.java deleted file mode 100644 index 543588c7..00000000 --- a/src/com/android/tv/tuner/exoplayer/SampleExtractor.java +++ /dev/null @@ -1,136 +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.exoplayer; - -import android.os.Handler; - -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.TrackRenderer; - -import java.io.IOException; -import java.util.List; - -/** - * Extractor for reading track metadata and samples stored in tracks. - * - * <p>Call {@link #prepare} until it returns {@code true}, then access track metadata via - * {@link #getTrackFormats} and {@link #getTrackMediaFormat}. - * - * <p>Pass indices of tracks to read from to {@link #selectTrack}. A track can later be deselected - * by calling {@link #deselectTrack}. It is safe to select/deselect tracks after reading sample - * data or seeking. Initially, all tracks are deselected. - * - * <p>Call {@link #release()} when the extractor is no longer needed to free resources. - */ -public interface SampleExtractor { - - /** - * If the extractor is currently having difficulty preparing or loading samples, then this - * method throws the underlying error. Otherwise does nothing. - * - * @throws IOException The underlying error. - */ - void maybeThrowError() throws IOException; - - /** - * Prepares the extractor for reading track metadata and samples. - * - * @return whether the source is ready; if {@code false}, this method must be called again. - * @throws IOException thrown if the source can't be read - */ - boolean prepare() throws IOException; - - /** Returns track information about all tracks that can be selected. */ - List<MediaFormat> getTrackFormats(); - - /** Selects the track at {@code index} for reading sample data. */ - void selectTrack(int index); - - /** Deselects the track at {@code index}, so no more samples will be read from that track. */ - void deselectTrack(int index); - - /** - * Returns an estimate of the position up to which data is buffered. - * - * <p>This method should not be called until after the extractor has been successfully prepared. - * - * @return an estimate of the absolute position in microseconds up to which data is buffered, - * or {@link TrackRenderer#END_OF_TRACK_US} if data is buffered to the end of the stream, or - * {@link TrackRenderer#UNKNOWN_TIME_US} if no estimate is available. - */ - long getBufferedPositionUs(); - - /** - * Seeks to the specified time in microseconds. - * - * <p>This method should not be called until after the extractor has been successfully prepared. - * - * @param positionUs the seek position in microseconds - */ - void seekTo(long positionUs); - - /** Stores the {@link MediaFormat} of {@code track}. */ - void getTrackMediaFormat(int track, MediaFormatHolder outMediaFormatHolder); - - /** - * Reads the next sample in the track at index {@code track} into {@code sampleHolder}, returning - * {@link SampleSource#SAMPLE_READ} if it is available. - * - * <p>Advances to the next sample if a sample was read. - * - * @param track the index of the track from which to read a sample - * @param sampleHolder the holder for read sample data, if {@link SampleSource#SAMPLE_READ} is - * returned - * @return {@link SampleSource#SAMPLE_READ} if a sample was read into {@code sampleHolder}, or - * {@link SampleSource#END_OF_STREAM} if the last samples in all tracks have been read, or - * {@link SampleSource#NOTHING_READ} if the sample cannot be read immediately as it is not - * loaded. - */ - int readSample(int track, SampleHolder sampleHolder); - - /** Releases resources associated with this extractor. */ - void release(); - - /** Indicates to the source that it should still be buffering data. */ - boolean continueBuffering(long positionUs); - - /** - * Sets OnCompletionListener for notifying the completion of SampleExtractor. - * - * @param listener the OnCompletionListener - * @param handler the {@link Handler} for {@link Handler#post(Runnable)} of OnCompletionListener - */ - void setOnCompletionListener(OnCompletionListener listener, Handler handler); - - /** - * The listener for SampleExtractor being completed. - */ - interface OnCompletionListener { - - /** - * Called when sample extraction is completed. - * - * @param result {@code true} when the extractor is finished without an error, - * {@code false} otherwise (storage error, weak signal, being reached at EoS - * prematurely, etc.) - * @param lastExtractedPositionUs the last extracted position when extractor is completed - */ - void onCompletion(boolean result, long lastExtractedPositionUs); - } -} diff --git a/src/com/android/tv/tuner/exoplayer/audio/AudioClock.java b/src/com/android/tv/tuner/exoplayer/audio/AudioClock.java deleted file mode 100644 index 5666c5b9..00000000 --- a/src/com/android/tv/tuner/exoplayer/audio/AudioClock.java +++ /dev/null @@ -1,107 +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.exoplayer.audio; - -import com.android.tv.common.SoftPreconditions; - -import android.os.SystemClock; - -/** - * Copy of {@link com.google.android.exoplayer.MediaClock}. - * <p> - * A simple clock for tracking the progression of media time. The clock can be started, stopped and - * its time can be set and retrieved. When started, this clock is based on - * {@link SystemClock#elapsedRealtime()}. - */ -/* package */ class AudioClock { - private boolean mStarted; - - /** - * The media time when the clock was last set or stopped. - */ - private long mPositionUs; - - /** - * The difference between {@link SystemClock#elapsedRealtime()} and {@link #mPositionUs} - * when the clock was last set or mStarted. - */ - private long mDeltaUs; - - private float mPlaybackSpeed = 1.0f; - private long mDeltaUpdatedTimeUs; - - /** - * Starts the clock. Does nothing if the clock is already started. - */ - public void start() { - if (!mStarted) { - mStarted = true; - mDeltaUs = elapsedRealtimeMinus(mPositionUs); - mDeltaUpdatedTimeUs = SystemClock.elapsedRealtime() * 1000; - } - } - - /** - * Stops the clock. Does nothing if the clock is already stopped. - */ - public void stop() { - if (mStarted) { - mPositionUs = elapsedRealtimeMinus(mDeltaUs); - mStarted = false; - } - } - - /** - * @param timeUs The position to set in microseconds. - */ - public void setPositionUs(long timeUs) { - this.mPositionUs = timeUs; - mDeltaUs = elapsedRealtimeMinus(timeUs); - mDeltaUpdatedTimeUs = SystemClock.elapsedRealtime() * 1000; - } - - /** - * @return The current position in microseconds. - */ - public long getPositionUs() { - if (!mStarted) { - return mPositionUs; - } - if (mPlaybackSpeed != 1.0f) { - long elapsedTimeFromPlaybackSpeedChanged = SystemClock.elapsedRealtime() * 1000 - - mDeltaUpdatedTimeUs; - return elapsedRealtimeMinus(mDeltaUs) - + (long) ((mPlaybackSpeed - 1.0f) * elapsedTimeFromPlaybackSpeedChanged); - } else { - return elapsedRealtimeMinus(mDeltaUs); - } - } - - /** - * Sets playback speed. {@code speed} should be positive. - */ - public void setPlaybackSpeed(float speed) { - SoftPreconditions.checkState(speed > 0); - mDeltaUs = elapsedRealtimeMinus(getPositionUs()); - mDeltaUpdatedTimeUs = SystemClock.elapsedRealtime() * 1000; - mPlaybackSpeed = speed; - } - - private long elapsedRealtimeMinus(long toSubtractUs) { - return SystemClock.elapsedRealtime() * 1000 - toSubtractUs; - } -} diff --git a/src/com/android/tv/tuner/exoplayer/audio/AudioDecoder.java b/src/com/android/tv/tuner/exoplayer/audio/AudioDecoder.java deleted file mode 100644 index e581092a..00000000 --- a/src/com/android/tv/tuner/exoplayer/audio/AudioDecoder.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.exoplayer.audio; - -import com.google.android.exoplayer.ExoPlaybackException; -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.SampleHolder; - -import java.nio.ByteBuffer; - -/** A base class for audio decoders. */ -public abstract class AudioDecoder { - - /** - * Decodes an audio sample. - * - * @param sampleHolder a holder that contains the sample data and corresponding metadata - */ - public abstract void decode(SampleHolder sampleHolder); - - /** Returns a decoded sample from decoder. */ - public abstract ByteBuffer getDecodedSample(); - - /** Returns the presentation time for the decoded sample. */ - public abstract long getDecodedTimeUs(); - - /** - * Clear previous decode state if any. Prepares to decode samples of the specified encoding. - * This method should be called before using decode. - * - * @param mime audio encoding - */ - public abstract void resetDecoderState(String mimeType); - - /** Releases all the resource. */ - public abstract void release(); - - /** - * Init decoder if needed. - * - * @param format the format used to initialize decoder - */ - public void maybeInitDecoder(MediaFormat format) throws ExoPlaybackException { - // Do nothing. - } - - /** Returns input buffer that will be used in decoder. */ - public ByteBuffer getInputBuffer() { - return null; - } - - /** Returns the output format. */ - public android.media.MediaFormat getOutputFormat() { - return null; - } -} diff --git a/src/com/android/tv/tuner/exoplayer/audio/AudioTrackMonitor.java b/src/com/android/tv/tuner/exoplayer/audio/AudioTrackMonitor.java deleted file mode 100644 index ec616b13..00000000 --- a/src/com/android/tv/tuner/exoplayer/audio/AudioTrackMonitor.java +++ /dev/null @@ -1,129 +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.exoplayer.audio; - -import android.os.SystemClock; -import android.util.Log; -import android.util.Pair; - -import com.google.android.exoplayer.util.MimeTypes; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Set; - -/** - * Monitors the rendering position of {@link AudioTrack}. - */ -public class AudioTrackMonitor { - private static final String TAG = "AudioTrackMonitor"; - private static final boolean DEBUG = false; - - // For fetched audio samples - private final ArrayList<Pair<Long, Integer>> mPtsList = new ArrayList<>(); - private final Set<Integer> mSampleSize = new HashSet<>(); - private final Set<Integer> mCurSampleSize = new HashSet<>(); - private final Set<Integer> mHeader = new HashSet<>(); - - private long mExpireMs; - private long mDuration; - private long mSampleCount; - private long mTotalCount; - private long mStartMs; - - private boolean mIsMp2; - - private void flush() { - mExpireMs += mDuration; - mSampleCount = 0; - mCurSampleSize.clear(); - mPtsList.clear(); - } - - /** - * Resets and initializes {@link AudioTrackMonitor}. - * - * @param duration the frequency of monitoring in milliseconds - */ - public void reset(long duration) { - mExpireMs = SystemClock.elapsedRealtime(); - mDuration = duration; - mTotalCount = 0; - mStartMs = 0; - mSampleSize.clear(); - mHeader.clear(); - flush(); - } - - public void setEncoding(String mime) { - mIsMp2 = MimeTypes.AUDIO_MPEG_L2.equalsIgnoreCase(mime); - } - - /** - * Adds an audio sample information for monitoring. - * - * @param pts the presentation timestamp of the sample - * @param sampleSize the size in bytes of the sample - * @param header the bitrate & sampling information header of the sample - */ - public void addPts(long pts, int sampleSize, int header) { - mTotalCount++; - mSampleCount++; - mSampleSize.add(sampleSize); - mHeader.add(header); - mCurSampleSize.add(sampleSize); - if (mTotalCount == 1) { - mStartMs = SystemClock.elapsedRealtime(); - } - if (mPtsList.isEmpty() || mPtsList.get(mPtsList.size() - 1).first != pts) { - mPtsList.add(Pair.create(pts, 1)); - return; - } - Pair<Long, Integer> pair = mPtsList.get(mPtsList.size() - 1); - mPtsList.set(mPtsList.size() - 1, Pair.create(pair.first, pair.second + 1)); - } - - /** - * Logs if interested events are present. - * <p> - * Periodic logging is not enabled in release mode in order to avoid verbose logging. - */ - public void maybeLog() { - long now = SystemClock.elapsedRealtime(); - if (mExpireMs != 0 && now >= mExpireMs) { - if (DEBUG) { - long unitDuration = mIsMp2 ? MpegTsDefaultAudioTrackRenderer.MP2_SAMPLE_DURATION_US - : MpegTsDefaultAudioTrackRenderer.AC3_SAMPLE_DURATION_US; - long sampleDuration = (mTotalCount - 1) * unitDuration / 1000; - long totalDuration = now - mStartMs; - StringBuilder ptsBuilder = new StringBuilder(); - ptsBuilder.append("PTS received ").append(mSampleCount).append(", ") - .append(totalDuration - sampleDuration).append(' '); - - for (Pair<Long, Integer> pair : mPtsList) { - ptsBuilder.append('[').append(pair.first).append(':').append(pair.second) - .append("], "); - } - Log.d(TAG, ptsBuilder.toString()); - } - if (DEBUG || mCurSampleSize.size() > 1) { - Log.d(TAG, "PTS received sample size: " - + String.valueOf(mSampleSize) + mCurSampleSize + mHeader); - } - flush(); - } - } -} diff --git a/src/com/android/tv/tuner/exoplayer/audio/AudioTrackWrapper.java b/src/com/android/tv/tuner/exoplayer/audio/AudioTrackWrapper.java deleted file mode 100644 index 953c9fc4..00000000 --- a/src/com/android/tv/tuner/exoplayer/audio/AudioTrackWrapper.java +++ /dev/null @@ -1,176 +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.exoplayer.audio; - -import android.media.MediaFormat; - -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.audio.AudioTrack; - -import java.nio.ByteBuffer; - -/** - * {@link AudioTrack} wrapper class for trickplay operations including FF/RW. - * FF/RW trickplay operations do not need framework {@link AudioTrack}. - * 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 = - MpegTsDefaultAudioTrackRenderer.BUFFERED_SAMPLES_IN_AUDIOTRACK; - private final AudioTrack mAudioTrack = new AudioTrack(); - private int mAudioSessionID; - private boolean mIsEnabled; - - AudioTrackWrapper() { - mIsEnabled = true; - } - - public void resetSessionId() { - mAudioSessionID = AudioTrack.SESSION_ID_NOT_SET; - } - - public boolean isInitialized() { - return mIsEnabled && mAudioTrack.isInitialized(); - } - - public void restart() { - if (mAudioTrack.isInitialized()) { - mAudioTrack.release(); - } - mIsEnabled = true; - resetSessionId(); - } - - public void release() { - if (mAudioSessionID != AudioTrack.SESSION_ID_NOT_SET) { - mAudioTrack.release(); - } - } - - public void initialize() throws AudioTrack.InitializationException { - if (!mIsEnabled) { - return; - } - if (mAudioSessionID != AudioTrack.SESSION_ID_NOT_SET) { - mAudioTrack.initialize(mAudioSessionID); - } else { - mAudioSessionID = mAudioTrack.initialize(); - } - } - - public void reset() { - if (!mIsEnabled) { - return; - } - mAudioTrack.reset(); - } - - public boolean isEnded() { - return !mIsEnabled || !mAudioTrack.hasPendingData(); - } - - public boolean isReady() { - // In the case of not playing actual audio data, Audio track is always ready. - return !mIsEnabled || mAudioTrack.hasPendingData(); - } - - public void play() { - if (!mIsEnabled) { - return; - } - mAudioTrack.play(); - } - - public void pause() { - if (!mIsEnabled) { - return; - } - mAudioTrack.pause(); - } - - public void setVolume(float volume) { - if (!mIsEnabled) { - return; - } - mAudioTrack.setVolume(volume); - } - - public void reconfigure(MediaFormat format, int audioBufferSize) { - if (!mIsEnabled || format == null) { - return; - } - String mimeType = format.getString(MediaFormat.KEY_MIME); - int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); - int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); - int pcmEncoding; - try { - pcmEncoding = format.getInteger(MediaFormat.KEY_PCM_ENCODING); - } catch (Exception e) { - pcmEncoding = C.ENCODING_PCM_16BIT; - } - // 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, - // It is safe to fake non-stereo AC3 as AC3 stereo which is default passthrough mode. - // In other words, the channel count should be always 2. - channelCount = 2; - } - 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() { - if (!mIsEnabled) { - return; - } - mAudioTrack.handleDiscontinuity(); - } - - public int handleBuffer(ByteBuffer buffer, int offset, int size, long presentationTimeUs) - throws AudioTrack.WriteException { - if (!mIsEnabled) { - return AudioTrack.RESULT_BUFFER_CONSUMED; - } - return mAudioTrack.handleBuffer(buffer, offset, size, presentationTimeUs); - } - - public void setStatus(boolean enable) { - if (enable == mIsEnabled) { - return; - } - mAudioTrack.reset(); - mIsEnabled = enable; - } - - public boolean isEnabled() { - return mIsEnabled; - } - - // This should be used only in case of being enabled. - public long getCurrentPositionUs(boolean isEnded) { - return mAudioTrack.getCurrentPositionUs(isEnded); - } -} diff --git a/src/com/android/tv/tuner/exoplayer/audio/MediaCodecAudioDecoder.java b/src/com/android/tv/tuner/exoplayer/audio/MediaCodecAudioDecoder.java deleted file mode 100644 index 72bc68b6..00000000 --- a/src/com/android/tv/tuner/exoplayer/audio/MediaCodecAudioDecoder.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * 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.exoplayer.audio; - -import android.media.MediaCodec; -import android.util.Log; - -import com.google.android.exoplayer.CodecCounters; -import com.google.android.exoplayer.DecoderInfo; -import com.google.android.exoplayer.ExoPlaybackException; -import com.google.android.exoplayer.MediaCodecSelector; -import com.google.android.exoplayer.MediaCodecUtil; -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.SampleHolder; - -import java.nio.ByteBuffer; -import java.util.ArrayList; - -/** A decoder to use MediaCodec for decoding audio stream. */ -public class MediaCodecAudioDecoder extends AudioDecoder { - private static final String TAG = "MediaCodecAudioDecoder"; - - public static final int INDEX_INVALID = -1; - - private final CodecCounters mCodecCounters; - private final MediaCodecSelector mSelector; - - private MediaCodec mCodec; - private MediaCodec.BufferInfo mOutputBufferInfo; - private ByteBuffer mMediaCodecOutputBuffer; - private ArrayList<Long> mDecodeOnlyPresentationTimestamps; - private boolean mWaitingForFirstSyncFrame; - private boolean mIsNewIndex; - private int mInputIndex; - private int mOutputIndex; - - /** Creates a MediaCodec based audio decoder. */ - public MediaCodecAudioDecoder(MediaCodecSelector selector) { - mSelector = selector; - mOutputBufferInfo = new MediaCodec.BufferInfo(); - mCodecCounters = new CodecCounters(); - mDecodeOnlyPresentationTimestamps = new ArrayList<>(); - } - - /** Returns {@code true} if there is decoder for {@code mimeType}. */ - public static boolean supportMimeType(MediaCodecSelector selector, String mimeType) { - if (selector == null) { - return false; - } - return getDecoderInfo(selector, mimeType) != null; - } - - private static DecoderInfo getDecoderInfo(MediaCodecSelector selector, String mimeType) { - try { - return selector.getDecoderInfo(mimeType, false); - } catch (MediaCodecUtil.DecoderQueryException e) { - Log.e(TAG, "Select decoder error:" + e); - return null; - } - } - - private boolean shouldInitCodec(MediaFormat format) { - return format != null && mCodec == null; - } - - @Override - public void maybeInitDecoder(MediaFormat format) throws ExoPlaybackException { - if (!shouldInitCodec(format)) { - return; - } - - String mimeType = format.mimeType; - DecoderInfo decoderInfo = getDecoderInfo(mSelector, mimeType); - if (decoderInfo == null) { - Log.i(TAG, "There is not decoder found for " + mimeType); - return; - } - - String codecName = decoderInfo.name; - try { - mCodec = MediaCodec.createByCodecName(codecName); - mCodec.configure(format.getFrameworkMediaFormatV16(), null, null, 0); - mCodec.start(); - } catch (Exception e) { - Log.e(TAG, "Failed when configure or start codec:" + e); - throw new ExoPlaybackException(e); - } - mInputIndex = INDEX_INVALID; - mOutputIndex = INDEX_INVALID; - mWaitingForFirstSyncFrame = true; - mCodecCounters.codecInitCount++; - } - - @Override - public void resetDecoderState(String mimeType) { - if (mCodec == null) { - return; - } - mInputIndex = INDEX_INVALID; - mOutputIndex = INDEX_INVALID; - mDecodeOnlyPresentationTimestamps.clear(); - mCodec.flush(); - mWaitingForFirstSyncFrame = true; - } - - @Override - public void release() { - if (mCodec != null) { - mDecodeOnlyPresentationTimestamps.clear(); - mInputIndex = INDEX_INVALID; - mOutputIndex = INDEX_INVALID; - mCodecCounters.codecReleaseCount++; - try { - mCodec.stop(); - } finally { - try { - mCodec.release(); - } finally { - mCodec = null; - } - } - } - } - - /** Returns the index of input buffer which is ready for using. */ - public int getInputIndex() { - return mInputIndex; - } - - @Override - public ByteBuffer getInputBuffer() { - if (mInputIndex < 0) { - mInputIndex = mCodec.dequeueInputBuffer(0); - if (mInputIndex < 0) { - return null; - } - return mCodec.getInputBuffer(mInputIndex); - } - return mCodec.getInputBuffer(mInputIndex); - } - - @Override - public void decode(SampleHolder sampleHolder) { - if (mWaitingForFirstSyncFrame) { - if (!sampleHolder.isSyncFrame()) { - sampleHolder.clearData(); - return; - } - mWaitingForFirstSyncFrame = false; - } - long presentationTimeUs = sampleHolder.timeUs; - if (sampleHolder.isDecodeOnly()) { - mDecodeOnlyPresentationTimestamps.add(presentationTimeUs); - } - mCodec.queueInputBuffer(mInputIndex, 0, sampleHolder.data.limit(), presentationTimeUs, 0); - mInputIndex = INDEX_INVALID; - mCodecCounters.inputBufferCount++; - } - - private int getDecodeOnlyIndex(long presentationTimeUs) { - final int size = mDecodeOnlyPresentationTimestamps.size(); - for (int i = 0; i < size; i++) { - if (mDecodeOnlyPresentationTimestamps.get(i).longValue() == presentationTimeUs) { - return i; - } - } - return INDEX_INVALID; - } - - /** Returns the index of output buffer which is ready for using. */ - public int getOutputIndex() { - if (mOutputIndex < 0) { - mOutputIndex = mCodec.dequeueOutputBuffer(mOutputBufferInfo, 0); - mIsNewIndex = true; - } else { - mIsNewIndex = false; - } - return mOutputIndex; - } - - @Override - public android.media.MediaFormat getOutputFormat() { - return mCodec.getOutputFormat(); - } - - /** Returns {@code true} if the output is only for decoding but not for rendering. */ - public boolean maybeDecodeOnlyIndex() { - int decodeOnlyIndex = getDecodeOnlyIndex(mOutputBufferInfo.presentationTimeUs); - if (decodeOnlyIndex != INDEX_INVALID) { - mCodec.releaseOutputBuffer(mOutputIndex, false); - mCodecCounters.skippedOutputBufferCount++; - mDecodeOnlyPresentationTimestamps.remove(decodeOnlyIndex); - mOutputIndex = INDEX_INVALID; - return true; - } - return false; - } - - @Override - public ByteBuffer getDecodedSample() { - if (maybeDecodeOnlyIndex() || mOutputIndex < 0) { - return null; - } - if (mIsNewIndex) { - mMediaCodecOutputBuffer = mCodec.getOutputBuffer(mOutputIndex); - } - return mMediaCodecOutputBuffer; - } - - @Override - public long getDecodedTimeUs() { - return mOutputBufferInfo.presentationTimeUs; - } - - /** Releases the output buffer after rendering. */ - public void releaseOutputBuffer() { - mCodecCounters.renderedOutputBufferCount++; - mCodec.releaseOutputBuffer(mOutputIndex, false); - mOutputIndex = INDEX_INVALID; - } -} diff --git a/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java deleted file mode 100644 index 77170419..00000000 --- a/src/com/android/tv/tuner/exoplayer/audio/MpegTsDefaultAudioTrackRenderer.java +++ /dev/null @@ -1,735 +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.exoplayer.audio; - -import android.media.MediaCodec; -import android.os.Build; -import android.os.Handler; -import android.os.SystemClock; -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.MediaCodecSelector; -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.TrackRenderer; -import com.google.android.exoplayer.audio.AudioTrack; -import com.google.android.exoplayer.util.Assertions; -import com.google.android.exoplayer.util.MimeTypes; -import com.android.tv.tuner.exoplayer.ffmpeg.FfmpegDecoderClient; -import com.android.tv.tuner.tvinput.TunerDebug; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; - -/** - * Decodes and renders DTV audio. Supports MediaCodec based decoding, passthrough playback and - * ffmpeg based software decoding (AC3, MP2). - */ -public class MpegTsDefaultAudioTrackRenderer 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; - - // ATSC/53 allows sample rate to be only 48Khz. - // One AC3 sample has 1536 frames, and its duration is 32ms. - public static final long AC3_SAMPLE_DURATION_US = 32000; - - // TODO: Check whether DVB broadcasting uses sample rate other than 48Khz. - // MPEG-1 audio Layer II and III has 1152 frames per sample. - // 1152 frames duration is 24ms when sample rate is 48Khz. - static final long MP2_SAMPLE_DURATION_US = 24000; - - // 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 = "MpegTsDefaultAudioTrac"; - private static final boolean DEBUG = false; - - /** - * Interface definition for a callback to be notified of - * {@link com.google.android.exoplayer.audio.AudioTrack} error. - */ - public interface EventListener { - void onAudioTrackInitializationError(AudioTrack.InitializationException e); - void onAudioTrackWriteError(AudioTrack.WriteException e); - } - - private static final int DEFAULT_INPUT_BUFFER_SIZE = 16384 * 2; - private static final int DEFAULT_OUTPUT_BUFFER_SIZE = 1024*1024; - private static final int MONITOR_DURATION_MS = 1000; - private static final int AC3_HEADER_BITRATE_OFFSET = 4; - private static final int MP2_HEADER_BITRATE_OFFSET = 2; - private static final int MP2_HEADER_BITRATE_MASK = 0xfc; - - // Keep this as static in order to prevent new framework AudioTrack creation - // while old AudioTrack is being released. - private static final AudioTrackWrapper AUDIO_TRACK = new AudioTrackWrapper(); - private static final long KEEP_ALIVE_AFTER_EOS_DURATION_MS = 3000; - - // Ignore AudioTrack backward movement if duration of movement is below the threshold. - private static final long BACKWARD_AUDIO_TRACK_MOVE_THRESHOLD_US = 3000; - - // AudioTrack position cannot go ahead beyond this limit. - private static final long CURRENT_POSITION_FROM_PTS_LIMIT_US = 1000000; - - // Since MediaCodec processing and AudioTrack playing add delay, - // PTS interpolated time should be delayed reasonably when AudioTrack is not used. - private static final long ESTIMATED_TRACK_RENDERING_DELAY_US = 500000; - - private final MediaCodecSelector mSelector; - - private final CodecCounters mCodecCounters; - private final SampleSource.SampleSourceReader mSource; - private final MediaFormatHolder mFormatHolder; - private final EventListener mEventListener; - private final Handler mEventHandler; - private final AudioTrackMonitor mMonitor; - private final AudioClock mAudioClock; - private final boolean mAc3Passthrough; - private final boolean mSoftwareDecoderAvailable; - - private MediaFormat mFormat; - private SampleHolder mSampleHolder; - private String mDecodingMime; - private boolean mFormatConfigured; - private int mSampleSize; - private final ByteBuffer mOutputBuffer; - private AudioDecoder mAudioDecoder; - private boolean mOutputReady; - private int mTrackIndex; - private boolean mSourceStateReady; - private boolean mInputStreamEnded; - private boolean mOutputStreamEnded; - private long mEndOfStreamMs; - private long mCurrentPositionUs; - private int mPresentationCount; - private long mPresentationTimeUs; - private long mInterpolatedTimeUs; - private long mPreviousPositionUs; - private boolean mIsStopped; - private boolean mEnabled = true; - private boolean mIsMuted; - private ArrayList<Integer> mTracksIndex; - private boolean mUseFrameworkDecoder; - - public MpegTsDefaultAudioTrackRenderer( - SampleSource source, - MediaCodecSelector selector, - Handler eventHandler, - EventListener listener, - boolean hasSoftwareAudioDecoder, - boolean usePassthrough) { - mSource = source.register(); - mSelector = selector; - mEventHandler = eventHandler; - mEventListener = listener; - mTrackIndex = -1; - mOutputBuffer = ByteBuffer.allocate(DEFAULT_OUTPUT_BUFFER_SIZE); - mFormatHolder = new MediaFormatHolder(); - AUDIO_TRACK.restart(); - mCodecCounters = new CodecCounters(); - mMonitor = new AudioTrackMonitor(); - mAudioClock = new AudioClock(); - mTracksIndex = new ArrayList<>(); - mAc3Passthrough = usePassthrough; - mSoftwareDecoderAvailable = hasSoftwareAudioDecoder && FfmpegDecoderClient.isAvailable(); - } - - @Override - protected MediaClock getMediaClock() { - return this; - } - - private boolean handlesMimeType(String mimeType) { - return mimeType.equals(MimeTypes.AUDIO_AC3) - || mimeType.equals(MimeTypes.AUDIO_E_AC3) - || mimeType.equals(MimeTypes.AUDIO_MPEG_L2) - || MediaCodecAudioDecoder.supportMimeType(mSelector, mimeType); - } - - @Override - protected boolean doPrepare(long positionUs) throws ExoPlaybackException { - boolean sourcePrepared = mSource.prepare(positionUs); - if (!sourcePrepared) { - return false; - } - for (int i = 0; i < mSource.getTrackCount(); i++) { - String mimeType = mSource.getFormat(i).mimeType; - if (MimeTypes.isAudio(mimeType) && handlesMimeType(mimeType)) { - if (mTrackIndex < 0) { - mTrackIndex = i; - } - mTracksIndex.add(i); - } - } - - // TODO: Check this case. Source does not have the proper mime type. - return true; - } - - @Override - protected int getTrackCount() { - return mTracksIndex.size(); - } - - @Override - protected MediaFormat getFormat(int track) { - Assertions.checkArgument(track >= 0 && track < mTracksIndex.size()); - return mSource.getFormat(mTracksIndex.get(track)); - } - - @Override - protected void onEnabled(int track, long positionUs, boolean joining) { - Assertions.checkArgument(track >= 0 && track < mTracksIndex.size()); - mTrackIndex = mTracksIndex.get(track); - mSource.enable(mTrackIndex, positionUs); - seekToInternal(positionUs); - } - - @Override - protected void onDisabled() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - AUDIO_TRACK.resetSessionId(); - } - clearDecodeState(); - mFormat = null; - mSource.disable(mTrackIndex); - } - - @Override - protected void onReleased() { - releaseDecoder(); - AUDIO_TRACK.release(); - mSource.release(); - } - - @Override - protected boolean isEnded() { - return mOutputStreamEnded && AUDIO_TRACK.isEnded(); - } - - @Override - protected boolean isReady() { - return AUDIO_TRACK.isReady() || (mFormat != null && (mSourceStateReady || mOutputReady)); - } - - private void seekToInternal(long positionUs) { - mMonitor.reset(MONITOR_DURATION_MS); - mSourceStateReady = false; - mInputStreamEnded = false; - mOutputStreamEnded = false; - mPresentationTimeUs = positionUs; - mPresentationCount = 0; - mPreviousPositionUs = 0; - mCurrentPositionUs = Long.MIN_VALUE; - mInterpolatedTimeUs = Long.MIN_VALUE; - mAudioClock.setPositionUs(positionUs); - } - - @Override - protected void seekTo(long positionUs) { - mSource.seekToUs(positionUs); - AUDIO_TRACK.reset(); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - // resetSessionId() will create a new framework AudioTrack instead of reusing old one. - AUDIO_TRACK.resetSessionId(); - } - seekToInternal(positionUs); - clearDecodeState(); - } - - @Override - protected void onStarted() { - AUDIO_TRACK.play(); - mAudioClock.start(); - mIsStopped = false; - } - - @Override - protected void onStopped() { - AUDIO_TRACK.pause(); - mAudioClock.stop(); - mIsStopped = true; - } - - @Override - protected void maybeThrowError() throws ExoPlaybackException { - try { - mSource.maybeThrowError(); - } catch (IOException e) { - throw new ExoPlaybackException(e); - } - } - - @Override - protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { - mMonitor.maybeLog(); - try { - if (mEndOfStreamMs != 0) { - // Ensure playback stops, after EoS was notified. - // Sometimes MediaCodecTrackRenderer does not fetch EoS timely - // after EoS was notified here long before. - long diff = SystemClock.elapsedRealtime() - mEndOfStreamMs; - if (diff >= KEEP_ALIVE_AFTER_EOS_DURATION_MS && !mIsStopped) { - throw new ExoPlaybackException("Much time has elapsed after EoS"); - } - } - boolean continueBuffering = mSource.continueBuffering(mTrackIndex, positionUs); - if (mSourceStateReady != continueBuffering) { - mSourceStateReady = continueBuffering; - if (DEBUG) { - Log.d(TAG, "mSourceStateReady: " + String.valueOf(mSourceStateReady)); - } - } - long discontinuity = mSource.readDiscontinuity(mTrackIndex); - if (discontinuity != SampleSource.NO_DISCONTINUITY) { - AUDIO_TRACK.handleDiscontinuity(); - mPresentationTimeUs = discontinuity; - mPresentationCount = 0; - clearDecodeState(); - return; - } - if (mFormat == null) { - readFormat(); - return; - } - - if (mAudioDecoder != null) { - mAudioDecoder.maybeInitDecoder(mFormat); - } - // Process only one sample at a time for doSomeWork() when using FFmpeg decoder. - if (processOutput()) { - if (!mOutputReady) { - while (feedInputBuffer()) { - if (mOutputReady) break; - } - } - } - mCodecCounters.ensureUpdated(); - } catch (IOException e) { - throw new ExoPlaybackException(e); - } - } - - private void ensureAudioTrackInitialized() { - if (!AUDIO_TRACK.isInitialized()) { - try { - if (DEBUG) { - Log.d(TAG, "AudioTrack initialized"); - } - AUDIO_TRACK.initialize(); - } catch (AudioTrack.InitializationException e) { - Log.e(TAG, "Error on AudioTrack initialization", e); - notifyAudioTrackInitializationError(e); - - // Do not throw exception here but just disabling audioTrack to keep playing - // video without audio. - AUDIO_TRACK.setStatus(false); - } - if (getState() == TrackRenderer.STATE_STARTED) { - if (DEBUG) { - Log.d(TAG, "AudioTrack played"); - } - AUDIO_TRACK.play(); - } - } - } - - private void clearDecodeState() { - mOutputReady = false; - if (mAudioDecoder != null) { - mAudioDecoder.resetDecoderState(mDecodingMime); - } - AUDIO_TRACK.reset(); - } - - private void releaseDecoder() { - if (mAudioDecoder != null) { - mAudioDecoder.release(); - } - } - - private void readFormat() throws IOException, ExoPlaybackException { - int result = mSource.readData(mTrackIndex, mCurrentPositionUs, - mFormatHolder, mSampleHolder); - if (result == SampleSource.FORMAT_READ) { - onInputFormatChanged(mFormatHolder); - } - } - - 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 { - String mimeType = formatHolder.format.mimeType; - mUseFrameworkDecoder = MediaCodecAudioDecoder.supportMimeType(mSelector, mimeType); - if (mUseFrameworkDecoder) { - mAudioDecoder = new MediaCodecAudioDecoder(mSelector); - mFormat = formatHolder.format; - mAudioDecoder.maybeInitDecoder(mFormat); - mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED); - } else if (mSoftwareDecoderAvailable - && (MimeTypes.AUDIO_MPEG_L2.equalsIgnoreCase(mimeType) - || MimeTypes.AUDIO_AC3.equalsIgnoreCase(mimeType) && !mAc3Passthrough)) { - releaseDecoder(); - mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT); - mSampleHolder.ensureSpaceForWrite(DEFAULT_INPUT_BUFFER_SIZE); - mAudioDecoder = FfmpegDecoderClient.getInstance(); - mDecodingMime = mimeType; - mFormat = convertMediaFormatToRaw(formatHolder.format); - } else { - mSampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DIRECT); - mSampleHolder.ensureSpaceForWrite(DEFAULT_INPUT_BUFFER_SIZE); - mFormat = formatHolder.format; - releaseDecoder(); - } - mFormatConfigured = true; - mMonitor.setEncoding(mimeType); - if (DEBUG && !mUseFrameworkDecoder) { - Log.d(TAG, "AudioTrack was configured to FORMAT: " + mFormat.toString()); - } - clearDecodeState(); - if (!mUseFrameworkDecoder) { - 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 void onOutputFormatChanged(android.media.MediaFormat format) { - if (DEBUG) { - Log.d(TAG, "AudioTrack was configured to FORMAT: " + format.toString()); - } - AUDIO_TRACK.reconfigure(format, 0); - } - - private boolean feedInputBuffer() throws IOException, ExoPlaybackException { - if (mInputStreamEnded) { - return false; - } - - if (mUseFrameworkDecoder) { - boolean indexChanged = - ((MediaCodecAudioDecoder) mAudioDecoder).getInputIndex() - == MediaCodecAudioDecoder.INDEX_INVALID; - if (indexChanged) { - mSampleHolder.data = mAudioDecoder.getInputBuffer(); - if (mSampleHolder.data != null) { - mSampleHolder.clearData(); - } else { - return false; - } - } - } else { - mSampleHolder.data.clear(); - mSampleHolder.size = 0; - } - int result = - mSource.readData(mTrackIndex, mPresentationTimeUs, mFormatHolder, mSampleHolder); - switch (result) { - case SampleSource.NOTHING_READ: { - return false; - } - case SampleSource.FORMAT_READ: { - Log.i(TAG, "Format was read again"); - onInputFormatChanged(mFormatHolder); - return true; - } - case SampleSource.END_OF_STREAM: { - Log.i(TAG, "End of stream from SampleSource"); - mInputStreamEnded = true; - return false; - } - default: { - if (mSampleHolder.size != mSampleSize - && mFormatConfigured - && !mUseFrameworkDecoder) { - onSampleSizeChanged(mSampleHolder.size); - } - mSampleHolder.data.flip(); - if (!mUseFrameworkDecoder) { - if (MimeTypes.AUDIO_MPEG_L2.equalsIgnoreCase(mDecodingMime)) { - mMonitor.addPts( - mSampleHolder.timeUs, - mOutputBuffer.position(), - mSampleHolder.data.get(MP2_HEADER_BITRATE_OFFSET) - & MP2_HEADER_BITRATE_MASK); - } else { - mMonitor.addPts( - mSampleHolder.timeUs, - mOutputBuffer.position(), - mSampleHolder.data.get(AC3_HEADER_BITRATE_OFFSET) & 0xff); - } - } - if (mAudioDecoder != null) { - mAudioDecoder.decode(mSampleHolder); - if (mUseFrameworkDecoder) { - int outputIndex = - ((MediaCodecAudioDecoder) mAudioDecoder).getOutputIndex(); - if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { - onOutputFormatChanged(mAudioDecoder.getOutputFormat()); - return true; - } else if (outputIndex < 0) { - return true; - } - if (((MediaCodecAudioDecoder) mAudioDecoder).maybeDecodeOnlyIndex()) { - AUDIO_TRACK.handleDiscontinuity(); - return true; - } - } - ByteBuffer outputBuffer = mAudioDecoder.getDecodedSample(); - long presentationTimeUs = mAudioDecoder.getDecodedTimeUs(); - decodeDone(outputBuffer, presentationTimeUs); - } else { - decodeDone(mSampleHolder.data, mSampleHolder.timeUs); - } - return true; - } - } - } - - private boolean processOutput() throws ExoPlaybackException { - if (mOutputStreamEnded) { - return false; - } - if (!mOutputReady) { - if (mInputStreamEnded) { - mOutputStreamEnded = true; - mEndOfStreamMs = SystemClock.elapsedRealtime(); - return false; - } - return true; - } - - ensureAudioTrackInitialized(); - int handleBufferResult; - try { - // To reduce discontinuity, interpolate presentation time. - if (MimeTypes.AUDIO_MPEG_L2.equalsIgnoreCase(mDecodingMime)) { - mInterpolatedTimeUs = mPresentationTimeUs - + mPresentationCount * MP2_SAMPLE_DURATION_US; - } else if (!mUseFrameworkDecoder) { - mInterpolatedTimeUs = mPresentationTimeUs - + mPresentationCount * AC3_SAMPLE_DURATION_US; - } else { - mInterpolatedTimeUs = mPresentationTimeUs; - } - handleBufferResult = - AUDIO_TRACK.handleBuffer( - mOutputBuffer, 0, mOutputBuffer.limit(), mInterpolatedTimeUs); - } catch (AudioTrack.WriteException e) { - notifyAudioTrackWriteError(e); - throw new ExoPlaybackException(e); - } - if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) { - Log.i(TAG, "Play discontinuity happened"); - mCurrentPositionUs = Long.MIN_VALUE; - } - if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) { - mCodecCounters.renderedOutputBufferCount++; - mOutputReady = false; - if (mUseFrameworkDecoder) { - ((MediaCodecAudioDecoder) mAudioDecoder).releaseOutputBuffer(); - } - return true; - } - return false; - } - - @Override - protected long getDurationUs() { - return mSource.getFormat(mTrackIndex).durationUs; - } - - @Override - protected long getBufferedPositionUs() { - long pos = mSource.getBufferedPositionUs(); - return pos == UNKNOWN_TIME_US || pos == END_OF_TRACK_US - ? pos : Math.max(pos, getPositionUs()); - } - - @Override - public long getPositionUs() { - if (!AUDIO_TRACK.isInitialized()) { - return mAudioClock.getPositionUs(); - } else if (!AUDIO_TRACK.isEnabled()) { - if (mInterpolatedTimeUs > 0 && !mUseFrameworkDecoder) { - return mInterpolatedTimeUs - ESTIMATED_TRACK_RENDERING_DELAY_US; - } - return mPresentationTimeUs; - } - long audioTrackCurrentPositionUs = AUDIO_TRACK.getCurrentPositionUs(isEnded()); - if (audioTrackCurrentPositionUs == AudioTrack.CURRENT_POSITION_NOT_SET) { - mPreviousPositionUs = 0L; - if (DEBUG) { - long oldPositionUs = Math.max(mCurrentPositionUs, 0); - long currentPositionUs = Math.max(mPresentationTimeUs, mCurrentPositionUs); - Log.d(TAG, "Audio position is not set, diff in us: " - + String.valueOf(currentPositionUs - oldPositionUs)); - } - mCurrentPositionUs = Math.max(mPresentationTimeUs, mCurrentPositionUs); - } else { - if (mPreviousPositionUs - > audioTrackCurrentPositionUs + BACKWARD_AUDIO_TRACK_MOVE_THRESHOLD_US) { - Log.e(TAG, "audio_position BACK JUMP: " - + (mPreviousPositionUs - audioTrackCurrentPositionUs)); - mCurrentPositionUs = audioTrackCurrentPositionUs; - } else { - mCurrentPositionUs = Math.max(mCurrentPositionUs, audioTrackCurrentPositionUs); - } - mPreviousPositionUs = audioTrackCurrentPositionUs; - } - long upperBound = mPresentationTimeUs + CURRENT_POSITION_FROM_PTS_LIMIT_US; - if (mCurrentPositionUs > upperBound) { - mCurrentPositionUs = upperBound; - } - return mCurrentPositionUs; - } - - private void decodeDone(ByteBuffer outputBuffer, long presentationTimeUs) { - if (outputBuffer == null || mOutputBuffer == null) { - return; - } - if (presentationTimeUs < 0) { - Log.e(TAG, "decodeDone - invalid presentationTimeUs"); - return; - } - - if (TunerDebug.ENABLED) { - TunerDebug.setAudioPtsUs(presentationTimeUs); - } - - mOutputBuffer.clear(); - Assertions.checkState(mOutputBuffer.remaining() >= outputBuffer.limit()); - - mOutputBuffer.put(outputBuffer); - if (presentationTimeUs == mPresentationTimeUs) { - mPresentationCount++; - } else { - mPresentationCount = 0; - mPresentationTimeUs = presentationTimeUs; - } - mOutputBuffer.flip(); - mOutputReady = true; - } - - private void notifyAudioTrackInitializationError(final AudioTrack.InitializationException e) { - if (mEventHandler == null || mEventListener == null) { - return; - } - mEventHandler.post(new Runnable() { - @Override - public void run() { - mEventListener.onAudioTrackInitializationError(e); - } - }); - } - - private void notifyAudioTrackWriteError(final AudioTrack.WriteException e) { - if (mEventHandler == null || mEventListener == null) { - return; - } - mEventHandler.post(new Runnable() { - @Override - public void run() { - mEventListener.onAudioTrackWriteError(e); - } - }); - } - - @Override - public void handleMessage(int messageType, Object message) throws ExoPlaybackException { - switch (messageType) { - case MSG_SET_VOLUME: - 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: - mEnabled = (Integer) message == 1; - setStatus(mEnabled); - break; - case MSG_SET_PLAYBACK_SPEED: - mAudioClock.setPlaybackSpeed((Float) message); - break; - default: - 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/audio/MpegTsMediaCodecAudioTrackRenderer.java b/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java deleted file mode 100644 index 142aa9b2..00000000 --- a/src/com/android/tv/tuner/exoplayer/audio/MpegTsMediaCodecAudioTrackRenderer.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer.audio; - -import android.os.Handler; - -import com.google.android.exoplayer.ExoPlaybackException; -import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; -import com.google.android.exoplayer.MediaCodecSelector; -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. - */ -public class MpegTsMediaCodecAudioTrackRenderer extends MediaCodecAudioTrackRenderer { - private final Ac3EventListener mListener; - - public interface Ac3EventListener extends EventListener { - /** - * Invoked when a {@link android.media.PlaybackParams} set to an - * {@link android.media.AudioTrack} is not valid. - * - * @param e The corresponding exception. - */ - void onAudioTrackSetPlaybackParamsError(IllegalArgumentException e); - } - - public MpegTsMediaCodecAudioTrackRenderer( - SampleSource source, - MediaCodecSelector mediaCodecSelector, - Handler eventHandler, - EventListener eventListener) { - super(source, mediaCodecSelector, eventHandler, eventListener); - mListener = (Ac3EventListener) eventListener; - } - - @Override - public void handleMessage(int messageType, Object message) throws ExoPlaybackException { - if (messageType == MSG_SET_PLAYBACK_PARAMS) { - try { - super.handleMessage(messageType, message); - } catch (IllegalArgumentException e) { - if (isAudioTrackSetPlaybackParamsError(e)) { - notifyAudioTrackSetPlaybackParamsError(e); - } - } - return; - } - super.handleMessage(messageType, message); - } - - private void notifyAudioTrackSetPlaybackParamsError(final IllegalArgumentException e) { - if (eventHandler != null && mListener != null) { - eventHandler.post(new Runnable() { - @Override - public void run() { - mListener.onAudioTrackSetPlaybackParamsError(e); - } - }); - } - } - - static private boolean isAudioTrackSetPlaybackParamsError(IllegalArgumentException e) { - if (e.getStackTrace() == null || e.getStackTrace().length < 1) { - return false; - } - for (StackTraceElement element : e.getStackTrace()) { - String elementString = element.toString(); - if (elementString.startsWith("android.media.AudioTrack.setPlaybackParams")) { - return true; - } - } - return false; - } -}
\ No newline at end of file diff --git a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java b/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java deleted file mode 100644 index 112e9dc4..00000000 --- a/src/com/android/tv/tuner/exoplayer/buffer/BufferManager.java +++ /dev/null @@ -1,692 +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.exoplayer.buffer; - -import android.media.MediaFormat; -import android.os.ConditionVariable; -import android.support.annotation.NonNull; -import android.support.annotation.VisibleForTesting; -import android.util.ArrayMap; -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.IOException; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.SortedMap; -import java.util.TreeMap; - -/** - * Manages {@link SampleChunk} objects. - * <p> - * The buffer manager can be disabled, while running, if the write throughput to the associated - * external storage is detected to be lower than a threshold {@code MINIMUM_DISK_WRITE_SPEED_MBPS}". - * This leads to restarting playback flow. - */ -public class BufferManager { - private static final String TAG = "BufferManager"; - private static final boolean DEBUG = false; - - // Constants for the disk write speed checking - private static final long MINIMUM_WRITE_SIZE_FOR_SPEED_CHECK = - 10L * 1024 * 1024; // Checks for every 10M disk write - private static final int MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK = 15 * 1024; - private static final int MAXIMUM_SPEED_CHECK_COUNT = 5; // Checks only 5 times - private static final int MINIMUM_DISK_WRITE_SPEED_MBPS = 3; // 3 Megabytes per second - - 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, 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; - private long mBufferSize = 0; - private final EvictChunkQueueMap mPendingDelete = new EvictChunkQueueMap(); - private final SampleChunk.ChunkCallback mChunkCallback = new SampleChunk.ChunkCallback() { - @Override - public void onChunkWrite(SampleChunk chunk) { - mBufferSize += chunk.getSize(); - } - - @Override - public void onChunkDelete(SampleChunk chunk) { - mBufferSize -= chunk.getSize(); - } - }; - - private int mMinSampleSizeForSpeedCheck = MINIMUM_SAMPLE_SIZE_FOR_SPEED_CHECK; - private long mTotalWriteSize; - private long mTotalWriteTimeNs; - private float mWriteBandwidth = 0.0f; - private volatile int mSpeedCheckCount; - - public interface ChunkEvictedListener { - void onChunkEvicted(String id, long createdTimeMs); - } - /** - * Handles I/O - * between BufferManager and {@link SampleExtractor}. - */ - public interface SampleBuffer { - - /** - * Initializes SampleBuffer. - * @param Ids track identifiers for storage read/write. - * @param mediaFormats meta-data for each track. - * @throws IOException - */ - void init(@NonNull List<String> Ids, - @NonNull List<com.google.android.exoplayer.MediaFormat> mediaFormats) - throws IOException; - - /** - * Selects the track {@code index} for reading sample data. - */ - void selectTrack(int index); - - /** - * Deselects the track at {@code index}, - * so that no more samples will be read from the track. - */ - void deselectTrack(int index); - - /** - * Writes sample to storage. - * - * @param index track index - * @param sample sample to write at storage - * @param conditionVariable notifies the completion of writing sample. - * @throws IOException - */ - void writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable) - throws IOException; - - /** - * Checks whether storage write speed is slow. - */ - boolean isWriteSpeedSlow(int sampleSize, long writeDurationNs); - - /** - * Handles when write speed is slow. - * @throws IOException - */ - void handleWriteSpeedSlow() throws IOException; - - /** - * Sets the flag when EoS was reached. - */ - void setEos(); - - /** - * Reads the next sample in the track at index {@code track} into {@code sampleHolder}, - * returning {@link com.google.android.exoplayer.SampleSource#SAMPLE_READ} - * if it is available. - * If the next sample is not available, - * returns {@link com.google.android.exoplayer.SampleSource#NOTHING_READ}. - */ - int readSample(int index, SampleHolder outSample); - - /** - * Seeks to the specified time in microseconds. - */ - void seekTo(long positionUs); - - /** - * Returns an estimate of the position up to which data is buffered. - */ - long getBufferedPositionUs(); - - /** - * Returns whether there is buffered data. - */ - boolean continueBuffering(long positionUs); - - /** - * Cleans up and releases everything. - * @throws IOException - */ - void release() throws IOException; - } - - /** - * 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 { - - /** - * Provides eligible storage directory for {@link BufferManager}. - * - * @return a directory to save buffer(chunks) and meta files - */ - File getBufferDir(); - - /** - * Informs whether the storage is used for persistent use. (eg. dvr recording/play) - * - * @return {@code true} if stored files are persistent - */ - boolean isPersistent(); - - /** - * Informs whether the storage usage exceeds pre-determined size. - * - * @param bufferSize the current total usage of Storage in bytes. - * @param pendingDelete the current storage usage which will be deleted in near future by - * bytes - * @return {@code true} if it reached pre-determined max size - */ - boolean reachedStorageMax(long bufferSize, long pendingDelete); - - /** - * Informs whether the storage has enough remained space. - * - * @param pendingDelete the current storage usage which will be deleted in near future by - * bytes - * @return {@code true} if it has enough space - */ - boolean hasEnoughBuffer(long pendingDelete); - - /** - * Reads track name & {@link MediaFormat} from storage. - * - * @param isAudio {@code true} if it is for audio track - * @return {@link List} of TrackFormat - */ - List<TrackFormat> readTrackInfoFiles(boolean isAudio); - - /** - * Reads key sample positions for each written sample from storage. - * - * @param trackId track name - * @return indexes of the specified track - * @throws IOException - */ - ArrayList<PositionHolder> readIndexFile(String trackId) throws IOException; - - /** - * Writes track information to storage. - * - * @param formatList {@list List} of TrackFormat - * @param isAudio {@code true} if it is for audio track - * @throws IOException - */ - void writeTrackInfoFiles(List<TrackFormat> formatList, boolean isAudio) - throws IOException; - - /** - * Writes index file to storage. - * - * @param trackName track name - * @param index {@link SampleChunk} container - * @throws IOException - */ - void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index) - throws IOException; - } - - private static class EvictChunkQueueMap { - private final Map<String, LinkedList<SampleChunk>> mEvictMap = new ArrayMap<>(); - private long mSize; - - private void init(String key) { - mEvictMap.put(key, new LinkedList<>()); - } - - private void add(String key, SampleChunk chunk) { - LinkedList<SampleChunk> queue = mEvictMap.get(key); - if (queue != null) { - mSize += chunk.getSize(); - queue.add(chunk); - } - } - - private SampleChunk poll(String key, long startPositionUs) { - LinkedList<SampleChunk> queue = mEvictMap.get(key); - if (queue != null) { - SampleChunk chunk = queue.peek(); - if (chunk != null && chunk.getStartPositionUs() < startPositionUs) { - mSize -= chunk.getSize(); - return queue.poll(); - } - } - return null; - } - - private long getSize() { - return mSize; - } - - private void release() { - for (Map.Entry<String, LinkedList<SampleChunk>> entry : mEvictMap.entrySet()) { - for (SampleChunk chunk : entry.getValue()) { - SampleChunk.IoState.release(chunk, true); - } - } - mEvictMap.clear(); - mSize = 0; - } - } - - public BufferManager(StorageManager storageManager) { - this(storageManager, new SampleChunk.SampleChunkCreator()); - } - - public BufferManager(StorageManager storageManager, - SampleChunk.SampleChunkCreator sampleChunkCreator) { - mStorageManager = storageManager; - mSampleChunkCreator = sampleChunkCreator; - } - - public void registerChunkEvictedListener(String id, ChunkEvictedListener listener) { - mEvictListeners.put(id, listener); - } - - public void unregisterChunkEvictedListener(String id) { - mEvictListeners.remove(id); - } - - 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 if it is needed. - * - * @param id the name of the track - * @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 createNewWriteFileIfNeeded(String id, long positionUs, SamplePool samplePool, - SampleChunk currentChunk, int currentOffset) throws IOException { - if (!maybeEvictChunk()) { - throw new IOException("Not enough storage space"); - } - 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); - } - 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; - } - } - - /** - * Loads a track using {@link BufferManager.StorageManager}. - * - * @param trackId the name of the track. - * @param samplePool {@link SamplePool} for the fast creation of samples. - * @throws IOException - */ - public void loadTrackFromStorage(String trackId, SamplePool samplePool) throws IOException { - ArrayList<PositionHolder> keyPositions = mStorageManager.readIndexFile(trackId); - long startPositionUs = keyPositions.size() > 0 ? keyPositions.get(0).positionUs : 0; - - SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(trackId); - if (map == null) { - map = new TreeMap<>(); - mChunkMap.put(trackId, map); - mStartPositionMap.put(trackId, startPositionUs); - mPendingDelete.init(trackId); - } - SampleChunk chunk = null; - 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)); - } - } - - /** - * Finds a {@link SampleChunk} for the specified track name and the position. - * - * @param id the name of the track. - * @param positionUs the position. - * @return returns the found {@link SampleChunk}. - */ - public Pair<SampleChunk, Integer> getReadFile(String id, long positionUs) { - SortedMap<Long, Pair<SampleChunk, Integer>> map = mChunkMap.get(id); - if (map == null) { - return null; - } - Pair<SampleChunk, Integer> ret; - SortedMap<Long, Pair<SampleChunk, Integer>> headMap = map.headMap(positionUs + 1); - if (!headMap.isEmpty()) { - ret = headMap.get(headMap.lastKey()); - } else { - ret = map.get(map.firstKey()); - } - return ret; - } - - /** - * Evicts chunks which are ready to be evicted for the specified track - * - * @param id the specified track - * @param earlierThanPositionUs the start position of the {@link SampleChunk} - * should be earlier than - */ - public void evictChunks(String id, long earlierThanPositionUs) { - SampleChunk chunk = null; - while ((chunk = mPendingDelete.poll(id, earlierThanPositionUs)) != null) { - SampleChunk.IoState.release(chunk, !mStorageManager.isPersistent()) ; - } - } - - /** - * Returns the start position of the specified track in micro seconds. - * - * @param id the specified track - */ - public long getStartPositionUs(String id) { - Long ret = mStartPositionMap.get(id); - return ret == null ? 0 : ret; - } - - private boolean maybeEvictChunk() { - long pendingDelete = mPendingDelete.getSize(); - while (mStorageManager.reachedStorageMax(mBufferSize, pendingDelete) - || !mStorageManager.hasEnoughBuffer(pendingDelete)) { - if (mStorageManager.isPersistent()) { - // Since chunks are persistent, we cannot evict chunks. - return false; - } - SortedMap<Long, Pair<SampleChunk, Integer>> earliestChunkMap = null; - SampleChunk earliestChunk = null; - String earliestChunkId = null; - 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()).first; - if (earliestChunk == null - || chunk.getCreatedTimeMs() < earliestChunk.getCreatedTimeMs()) { - earliestChunkMap = map; - earliestChunk = chunk; - earliestChunkId = entry.getKey(); - } - } - if (earliestChunk == null) { - break; - } - mPendingDelete.add(earliestChunkId, earliestChunk); - earliestChunkMap.remove(earliestChunk.getStartPositionUs()); - if (DEBUG) { - Log.d(TAG, String.format("bufferSize = %d; pendingDelete = %b; " - + "earliestChunk size = %d; %s@%d (%s)", - mBufferSize, pendingDelete, earliestChunk.getSize(), earliestChunkId, - earliestChunk.getStartPositionUs(), - Utils.toIsoDateTimeString(earliestChunk.getCreatedTimeMs()))); - } - ChunkEvictedListener listener = mEvictListeners.get(earliestChunkId); - if (listener != null) { - listener.onChunkEvicted(earliestChunkId, earliestChunk.getCreatedTimeMs()); - } - pendingDelete = mPendingDelete.getSize(); - } - for (Map.Entry<String, SortedMap<Long, Pair<SampleChunk, Integer>>> entry : - mChunkMap.entrySet()) { - SortedMap<Long, Pair<SampleChunk, Integer>> map = entry.getValue(); - if (map.isEmpty()) { - continue; - } - mStartPositionMap.put(entry.getKey(), map.firstKey()); - } - return true; - } - - /** - * Reads track information which includes {@link MediaFormat}. - * - * @return returns all track information which is found by {@link BufferManager.StorageManager}. - * @throws IOException - */ - 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"); - } - return trackFormatList; - } - - /** - * Writes track information and index information for all tracks. - * - * @param audios list of audio track information - * @param videos list of audio track information - * @throws IOException - */ - public void writeMetaFiles(List<TrackFormat> audios, List<TrackFormat> videos) - throws IOException { - 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); - } - } - 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); - } - } - } - - /** - * Releases all the resources. - */ - public void release() { - 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(); - } 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()); - } - } - - private void resetWriteStat(float writeBandwidth) { - mWriteBandwidth = writeBandwidth; - mTotalWriteSize = 0; - mTotalWriteTimeNs = 0; - } - - /** - * Adds a disk write sample size to calculate the average disk write bandwidth. - */ - public void addWriteStat(long size, long timeNs) { - if (size >= mMinSampleSizeForSpeedCheck) { - mTotalWriteSize += size; - mTotalWriteTimeNs += timeNs; - } - } - - /** - * Returns if the average disk write bandwidth is slower than - * threshold {@code MINIMUM_DISK_WRITE_SPEED_MBPS}. - */ - public boolean isWriteSlow() { - if (mTotalWriteSize < MINIMUM_WRITE_SIZE_FOR_SPEED_CHECK) { - return false; - } - - // Checks write speed for only MAXIMUM_SPEED_CHECK_COUNT times to ignore outliers - // by temporary system overloading during the playback. - if (mSpeedCheckCount > MAXIMUM_SPEED_CHECK_COUNT) { - return false; - } - mSpeedCheckCount++; - float megabytePerSecond = calculateWriteBandwidth(); - resetWriteStat(megabytePerSecond); - if (DEBUG) { - Log.d(TAG, "Measured disk write performance: " + megabytePerSecond + "MBps"); - } - return megabytePerSecond < MINIMUM_DISK_WRITE_SPEED_MBPS; - } - - /** - * Returns recent write bandwidth in MBps. If recent bandwidth is not available, - * returns {float -1.0f}. - */ - public float getWriteBandwidth() { - return mWriteBandwidth == 0.0f ? -1.0f : mWriteBandwidth; - } - - private float calculateWriteBandwidth() { - if (mTotalWriteTimeNs == 0) { - return -1; - } - return ((float) mTotalWriteSize * 1000 / mTotalWriteTimeNs); - } - - /** - * Returns if {@link BufferManager} has checked the write speed, - * which is suitable for Trickplay. - */ - @VisibleForTesting - public boolean hasSpeedCheckDone() { - return mSpeedCheckCount > 0; - } - - /** - * Sets minimum sample size for write speed check. - * @param sampleSize minimum sample size for write speed check. - */ - @VisibleForTesting - public void setMinimumSampleSizeForSpeedCheck(int sampleSize) { - mMinSampleSizeForSpeedCheck = sampleSize; - } -} diff --git a/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java deleted file mode 100644 index 6a09016c..00000000 --- a/src/com/android/tv/tuner/exoplayer/buffer/DvrStorageManager.java +++ /dev/null @@ -1,392 +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.exoplayer.buffer; - -import android.media.MediaFormat; -import android.util.Log; -import android.util.Pair; - -import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; -import com.google.protobuf.nano.MessageNano; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -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. - private static final long MIN_BUFFER_BYTES = 256L * 1024 * 1024; - private static final int NO_VALUE = -1; - private static final long NO_VALUE_LONG = -1L; - - private final File mBufferDir; - - // {@code true} when this is for recording, {@code false} when this is for replaying. - private final boolean mIsRecording; - - public DvrStorageManager(File file, boolean isRecording) { - mBufferDir = file; - mBufferDir.mkdirs(); - mIsRecording = isRecording; - } - - @Override - public File getBufferDir() { - return mBufferDir; - } - - @Override - public boolean isPersistent() { - return true; - } - - @Override - public boolean reachedStorageMax(long bufferSize, long pendingDelete) { - return false; - } - - @Override - public boolean hasEnoughBuffer(long pendingDelete) { - return !mIsRecording || mBufferDir.getUsableSpace() >= MIN_BUFFER_BYTES; - } - - private void readFormatInt(DataInputStream in, MediaFormat format, String key) - throws IOException { - int val = in.readInt(); - if (val != NO_VALUE) { - format.setInteger(key, val); - } - } - - private void readFormatLong(DataInputStream in, MediaFormat format, String key) - throws IOException { - long val = in.readLong(); - if (val != NO_VALUE_LONG) { - format.setLong(key, val); - } - } - - private void readFormatFloat(DataInputStream in, MediaFormat format, String key) - throws IOException { - float val = in.readFloat(); - if (val != NO_VALUE) { - format.setFloat(key, val); - } - } - - private String readString(DataInputStream in) throws IOException { - int len = in.readInt(); - if (len <= 0) { - return null; - } - byte [] strBytes = new byte[len]; - in.readFully(strBytes); - return new String(strBytes, StandardCharsets.UTF_8); - } - - private void readFormatString(DataInputStream in, MediaFormat format, String key) - throws IOException { - String str = readString(in); - if (str != null) { - format.setString(key, str); - } - } - - 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) { - return null; - } - byte [] bytes = new byte[len]; - in.readFully(bytes); - ByteBuffer buffer = ByteBuffer.allocate(len); - buffer.put(bytes); - buffer.flip(); - - return buffer; - } - - private void readFormatByteBuffer(DataInputStream in, MediaFormat format, String key) - throws IOException { - ByteBuffer buffer = readByteBuffer(in); - if (buffer != null) { - format.setByteBuffer(key, buffer); - } - } - - @Override - 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; - } - 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; - } - } - - 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) { - 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)) { - out.writeInt(format.getInteger(key)); - } else { - out.writeInt(NO_VALUE); - } - } - - private void writeFormatLong(DataOutputStream out, MediaFormat format, String key) - throws IOException { - if (format.containsKey(key)) { - out.writeLong(format.getLong(key)); - } else { - out.writeLong(NO_VALUE_LONG); - } - } - - private void writeFormatFloat(DataOutputStream out, MediaFormat format, String key) - throws IOException { - if (format.containsKey(key)) { - out.writeFloat(format.getFloat(key)); - } else { - out.writeFloat(NO_VALUE); - } - } - - private void writeString(DataOutputStream out, String str) throws IOException { - byte [] data = str.getBytes(StandardCharsets.UTF_8); - out.writeInt(data.length); - if (data.length > 0) { - out.write(data); - } - } - - private void writeFormatString(DataOutputStream out, MediaFormat format, String key) - throws IOException { - if (format.containsKey(key)) { - writeString(out, format.getString(key)); - } else { - out.writeInt(0); - } - } - - private void writeByteBuffer(DataOutputStream out, ByteBuffer buffer) throws IOException { - byte [] data = new byte[buffer.limit()]; - buffer.get(data); - buffer.flip(); - out.writeInt(data.length); - if (data.length > 0) { - out.write(data); - } else { - out.writeInt(0); - } - } - - private void writeFormatByteBuffer(DataOutputStream out, MediaFormat format, String key) - throws IOException { - if (format.containsKey(key)) { - writeByteBuffer(out, format.getByteBuffer(key)); - } else { - out.writeInt(0); - } - } - - @Override - public void writeTrackInfoFiles(List<BufferManager.TrackFormat> formatList, boolean isAudio) - throws IOException { - 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); - } - } - } - - @Override - public void writeIndexFile(String trackName, SortedMap<Long, Pair<SampleChunk, Integer>> index) - throws IOException { - File indexFile = new File(getBufferDir(), trackName + IDX_FILE_SUFFIX_V2); - try (DataOutputStream out = new DataOutputStream(new FileOutputStream(indexFile))) { - out.writeLong(index.size()); - 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 deleted file mode 100644 index af0c3f0d..00000000 --- a/src/com/android/tv/tuner/exoplayer/buffer/RecordingSampleBuffer.java +++ /dev/null @@ -1,309 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer.buffer; - -import android.os.ConditionVariable; -import android.support.annotation.IntDef; -import android.support.annotation.NonNull; -import android.util.Log; - -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.SampleSource; -import com.google.android.exoplayer.util.Assertions; -import com.android.tv.tuner.exoplayer.MpegTsPlayer; -import com.android.tv.tuner.tvinput.PlaybackBufferListener; -import com.android.tv.tuner.exoplayer.SampleExtractor; - -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.concurrent.TimeUnit; - -/** - * Handles I/O between {@link SampleExtractor} and - * {@link BufferManager}.Reads & writes samples from/to {@link SampleChunk} which is backed - * by physical storage. - */ -public class RecordingSampleBuffer implements BufferManager.SampleBuffer, - BufferManager.ChunkEvictedListener { - private static final String TAG = "RecordingSampleBuffer"; - - @IntDef({BUFFER_REASON_LIVE_PLAYBACK, BUFFER_REASON_RECORDED_PLAYBACK, BUFFER_REASON_RECORDING}) - @Retention(RetentionPolicy.SOURCE) - public @interface BufferReason {} - - /** - * A buffer reason for live-stream playback. - */ - public static final int BUFFER_REASON_LIVE_PLAYBACK = 0; - - /** - * A buffer reason for playback of a recorded program. - */ - public static final int BUFFER_REASON_RECORDED_PLAYBACK = 1; - - /** - * A buffer reason for recording a program. - */ - public static final int BUFFER_REASON_RECORDING = 2; - - /** - * The minimum duration to support seek in Trickplay. - */ - 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); - - private final BufferManager mBufferManager; - private final PlaybackBufferListener mBufferListener; - private final @BufferReason int mBufferReason; - - private int mTrackCount; - private boolean[] mTrackSelected; - private List<SampleQueue> mReadSampleQueues; - private final SamplePool mSamplePool = new SamplePool(); - private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US; - private long mCurrentPlaybackPositionUs = 0; - - // An error in I/O thread of {@link SampleChunkIoHelper} will be notified. - private volatile boolean mError; - - // Eos was reached in I/O thread of {@link SampleChunkIoHelper}. - private volatile boolean mEos; - private SampleChunkIoHelper mSampleChunkIoHelper; - private final SampleChunkIoHelper.IoCallback mIoCallback = - new SampleChunkIoHelper.IoCallback() { - @Override - public void onIoReachedEos() { - mEos = true; - } - - @Override - public void onIoError() { - mError = true; - } - }; - - /** - * Creates {@link BufferManager.SampleBuffer} with - * cached I/O backed by physical storage (e.g. trickplay,recording,recorded-playback). - * - * @param bufferManager the manager of {@link SampleChunk} - * @param bufferListener the listener for buffer I/O event - * @param enableTrickplay {@code true} when trickplay should be enabled - * @param bufferReason the reason for caching samples {@link RecordingSampleBuffer.BufferReason} - */ - public RecordingSampleBuffer(BufferManager bufferManager, PlaybackBufferListener bufferListener, - boolean enableTrickplay, @BufferReason int bufferReason) { - mBufferManager = bufferManager; - mBufferListener = bufferListener; - if (bufferListener != null) { - bufferListener.onBufferStateChanged(enableTrickplay); - } - mBufferReason = bufferReason; - } - - @Override - public void init(@NonNull List<String> ids, @NonNull List<MediaFormat> mediaFormats) - throws IOException { - mTrackCount = ids.size(); - if (mTrackCount <= 0) { - throw new IOException("No tracks to initialize"); - } - mTrackSelected = new boolean[mTrackCount]; - mReadSampleQueues = new ArrayList<>(); - mSampleChunkIoHelper = new SampleChunkIoHelper(ids, mediaFormats, mBufferReason, - mBufferManager, mSamplePool, mIoCallback); - for (int i = 0; i < mTrackCount; ++i) { - mReadSampleQueues.add(i, new SampleQueue(mSamplePool)); - } - mSampleChunkIoHelper.init(); - for (int i = 0; i < mTrackCount; ++i) { - mBufferManager.registerChunkEvictedListener(ids.get(i), RecordingSampleBuffer.this); - } - } - - @Override - public void selectTrack(int index) { - if (!mTrackSelected[index]) { - mTrackSelected[index] = true; - mReadSampleQueues.get(index).clear(); - mSampleChunkIoHelper.openRead(index, mCurrentPlaybackPositionUs); - } - } - - @Override - public void deselectTrack(int index) { - if (mTrackSelected[index]) { - mTrackSelected[index] = false; - mReadSampleQueues.get(index).clear(); - mSampleChunkIoHelper.closeRead(index); - } - } - - @Override - public void writeSample(int index, SampleHolder sample, ConditionVariable conditionVariable) - throws IOException { - mSampleChunkIoHelper.writeSample(index, sample, conditionVariable); - - if (!conditionVariable.block(BUFFER_WRITE_TIMEOUT_MS)) { - Log.e(TAG, "Error: Serious delay on writing buffer"); - conditionVariable.block(); - } - } - - @Override - public boolean isWriteSpeedSlow(int sampleSize, long writeDurationNs) { - if (mBufferReason == BUFFER_REASON_RECORDED_PLAYBACK) { - return false; - } - mBufferManager.addWriteStat(sampleSize, writeDurationNs); - return mBufferManager.isWriteSlow(); - } - - @Override - public void handleWriteSpeedSlow() throws IOException{ - if (mBufferReason == BUFFER_REASON_RECORDING) { - // Recording does not need to stop because I/O speed is slow temporarily. - // If fixed size buffer of TsStreamer overflows, TsDataSource will reach EoS. - // Reaching EoS will stop recording eventually. - Log.w(TAG, "Disk I/O speed is slow for recording temporarily: " - + mBufferManager.getWriteBandwidth() + "MBps"); - return; - } - // Disables buffering samples afterwards, and notifies the disk speed is slow. - Log.w(TAG, "Disk is too slow for trickplay"); - mBufferListener.onDiskTooSlow(); - } - - @Override - public void setEos() { - mSampleChunkIoHelper.closeWrite(); - } - - private boolean maybeReadSample(SampleQueue queue, int index) { - if (queue.getLastQueuedPositionUs() != null - && queue.getLastQueuedPositionUs() > mCurrentPlaybackPositionUs + BUFFER_NEEDED_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. - // But, the throttling should provide enough samples for the player to - // finish the buffering state. - return false; - } - SampleHolder sample = mSampleChunkIoHelper.readSample(index); - if (sample != null) { - queue.queueSample(sample); - return true; - } - return false; - } - - @Override - public int readSample(int track, SampleHolder outSample) { - Assertions.checkState(mTrackSelected[track]); - maybeReadSample(mReadSampleQueues.get(track), track); - int result = mReadSampleQueues.get(track).dequeueSample(outSample); - if ((result != SampleSource.SAMPLE_READ && mEos) || mError) { - return SampleSource.END_OF_STREAM; - } - return result; - } - - @Override - public void seekTo(long positionUs) { - for (int i = 0; i < mTrackCount; ++i) { - if (mTrackSelected[i]) { - mReadSampleQueues.get(i).clear(); - mSampleChunkIoHelper.openRead(i, positionUs); - } - } - mLastBufferedPositionUs = positionUs; - } - - @Override - public long getBufferedPositionUs() { - Long result = null; - for (int i = 0; i < mTrackCount; ++i) { - if (!mTrackSelected[i]) { - continue; - } - Long lastQueuedSamplePositionUs = - mReadSampleQueues.get(i).getLastQueuedPositionUs(); - if (lastQueuedSamplePositionUs == null) { - // No sample has been queued. - result = mLastBufferedPositionUs; - continue; - } - if (result == null || result > lastQueuedSamplePositionUs) { - result = lastQueuedSamplePositionUs; - } - } - if (result == null) { - return mLastBufferedPositionUs; - } - return (mLastBufferedPositionUs = result); - } - - @Override - public boolean continueBuffering(long positionUs) { - mCurrentPlaybackPositionUs = positionUs; - for (int i = 0; i < mTrackCount; ++i) { - if (!mTrackSelected[i]) { - continue; - } - SampleQueue queue = mReadSampleQueues.get(i); - maybeReadSample(queue, i); - if (queue.getLastQueuedPositionUs() == null - || positionUs > queue.getLastQueuedPositionUs()) { - // No more buffered data. - return false; - } - } - return true; - } - - @Override - public void release() throws IOException { - if (mTrackCount <= 0) { - return; - } - if (mSampleChunkIoHelper != null) { - mSampleChunkIoHelper.release(); - } - } - - // onChunkEvictedListener - @Override - public void onChunkEvicted(String id, long createdTimeMs) { - if (mBufferListener != null) { - mBufferListener.onBufferStartTimeChanged( - 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 deleted file mode 100644 index 04b5a071..00000000 --- a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunk.java +++ /dev/null @@ -1,437 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer.buffer; - -import android.support.annotation.Nullable; -import android.support.annotation.VisibleForTesting; -import android.util.Log; - -import com.google.android.exoplayer.SampleHolder; - -import java.io.File; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.channels.FileChannel; - -/** - * {@link SampleChunk} stores samples into file and makes them available for read. - * Stored file = { Header, Sample } * N - * Header = sample size : int, sample flag : int, sample PTS in micro second : long - */ -public class SampleChunk { - private static final String TAG = "SampleChunk"; - private static final boolean DEBUG = false; - - private final long mCreatedTimeMs; - private final long mStartPositionUs; - private SampleChunk mNextChunk; - - // Header = sample size : int, sample flag : int, sample PTS in micro second : long - private static final int SAMPLE_HEADER_LENGTH = 16; - - private final File mFile; - private final ChunkCallback mChunkCallback; - private final SamplePool mSamplePool; - private RandomAccessFile mAccessFile; - private long mWriteOffset; - private boolean mWriteFinished; - private boolean mIsReading; - private boolean mIsWriting; - - /** - * A callback for chunks being committed to permanent storage. - */ - public static abstract class ChunkCallback { - - /** - * Notifies when writing a SampleChunk is completed. - * - * @param chunk SampleChunk which is written completely - */ - public void onChunkWrite(SampleChunk chunk) { - - } - - /** - * Notifies when a SampleChunk is deleted. - * - * @param chunk SampleChunk which is deleted from storage - */ - public void onChunkDelete(SampleChunk chunk) { - } - } - - /** - * A class for SampleChunk creation. - */ - public static class SampleChunkCreator { - - /** - * Returns a newly created SampleChunk to read & write samples. - * - * @param samplePool sample allocator - * @param file filename which will be created newly - * @param startPositionUs the start position of the earliest sample to be stored - * @param chunkCallback for total storage usage change notification - */ - SampleChunk createSampleChunk(SamplePool samplePool, File file, - long startPositionUs, ChunkCallback chunkCallback) { - return new SampleChunk(samplePool, file, startPositionUs, System.currentTimeMillis(), - chunkCallback); - } - - /** - * Returns a newly created SampleChunk which is backed by an existing file. - * Created SampleChunk is read-only. - * - * @param samplePool sample allocator - * @param bufferDir the directory where the file to read is located - * @param filename the filename which will be read afterwards - * @param startPositionUs the start position of the earliest sample in the file - * @param chunkCallback for total storage usage change notification - * @param prev the previous SampleChunk just before the newly created SampleChunk - * @throws IOException - */ - SampleChunk loadSampleChunkFromFile(SamplePool samplePool, File bufferDir, - String filename, long startPositionUs, ChunkCallback chunkCallback, - SampleChunk prev) throws IOException { - File file = new File(bufferDir, filename); - SampleChunk chunk = - new SampleChunk(samplePool, file, startPositionUs, chunkCallback); - if (prev != null) { - prev.mNextChunk = chunk; - } - return chunk; - } - } - - /** - * Handles I/O for SampleChunk. - * Maintains current SampleChunk and the current offset for next I/O operation. - */ - static class IoState { - private SampleChunk mChunk; - private long mCurrentOffset; - - private boolean equals(SampleChunk chunk, long offset) { - return chunk == mChunk && mCurrentOffset == offset; - } - - /** - * Returns whether read I/O operation is finished. - */ - boolean isReadFinished() { - return mChunk == null; - } - - /** - * Returns the start position of the current SampleChunk - */ - long getStartPositionUs() { - return mChunk == null ? 0 : mChunk.getStartPositionUs(); - } - - private void reset(@Nullable SampleChunk chunk) { - mChunk = chunk; - 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, long offset) throws IOException { - if (mChunk != null) { - mChunk.closeRead(); - } - chunk.openRead(); - reset(chunk, offset); - } - - /** - * Prepares for write I/O operation to a new SampleChunk. - * - * @param chunk the new SampleChunk to write samples afterwards - * @throws IOException - */ - void openWrite(SampleChunk chunk) throws IOException{ - if (mChunk != null) { - mChunk.closeWrite(chunk); - } - chunk.openWrite(); - reset(chunk); - } - - /** - * Reads a sample if it is available. - * - * @return Returns a sample if it is available, null otherwise. - * @throws IOException - */ - SampleHolder read() throws IOException { - if (mChunk != null && mChunk.isReadFinished(this)) { - SampleChunk next = mChunk.mNextChunk; - mChunk.closeRead(); - if (next != null) { - next.openRead(); - } - reset(next); - } - if (mChunk != null) { - try { - return mChunk.read(this); - } catch (IllegalStateException e) { - // Write is finished and there is no additional buffer to read. - Log.w(TAG, "Tried to read sample over EOS."); - return null; - } - } else { - return null; - } - } - - /** - * Writes a sample. - * - * @param sample to write - * @param nextChunk if this is {@code null} writes at the current SampleChunk, - * otherwise close current SampleChunk and writes at this - * @throws IOException - */ - void write(SampleHolder sample, SampleChunk nextChunk) - throws IOException { - if (nextChunk != null) { - if (mChunk == null || mChunk.mNextChunk != null) { - throw new IllegalStateException("Requested write for wrong SampleChunk"); - } - mChunk.closeWrite(nextChunk); - mChunk.mChunkCallback.onChunkWrite(mChunk); - nextChunk.openWrite(); - reset(nextChunk); - } - mChunk.write(sample, this); - } - - /** - * Finishes write I/O operation. - * - * @throws IOException - */ - void closeWrite() throws IOException { - if (mChunk != null) { - mChunk.closeWrite(null); - } - } - - /** - * 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 - * @param delete {@code true} when the backed file needs to be deleted, - * {@code false} otherwise. - */ - static void release(SampleChunk chunk, boolean delete) { - chunk.release(delete); - } - } - - @VisibleForTesting - protected SampleChunk(SamplePool samplePool, File file, long startPositionUs, - long createdTimeMs, ChunkCallback chunkCallback) { - mStartPositionUs = startPositionUs; - mCreatedTimeMs = createdTimeMs; - mSamplePool = samplePool; - mFile = file; - mChunkCallback = chunkCallback; - } - - // Constructor of SampleChunk which is backed by the given existing file. - private SampleChunk(SamplePool samplePool, File file, long startPositionUs, - ChunkCallback chunkCallback) throws IOException { - mStartPositionUs = startPositionUs; - mCreatedTimeMs = mStartPositionUs / 1000; - mSamplePool = samplePool; - mFile = file; - mChunkCallback = chunkCallback; - mWriteFinished = true; - } - - private void openRead() throws IOException { - if (!mIsReading) { - if (mAccessFile == null) { - mAccessFile = new RandomAccessFile(mFile, "r"); - } - if (mWriteFinished && mWriteOffset == 0) { - // Lazy loading of write offset, in order not to load - // all SampleChunk's write offset at start time of recorded playback. - mWriteOffset = mAccessFile.length(); - } - mIsReading = true; - } - } - - private void openWrite() throws IOException { - if (mWriteFinished) { - throw new IllegalStateException("Opened for write though write is already finished"); - } - if (!mIsWriting) { - if (mIsReading) { - throw new IllegalStateException("Write is requested for " - + "an already opened SampleChunk"); - } - mAccessFile = new RandomAccessFile(mFile, "rw"); - mIsWriting = true; - } - } - - private void CloseAccessFileIfNeeded() throws IOException { - if (!mIsReading && !mIsWriting) { - try { - if (mAccessFile != null) { - mAccessFile.close(); - } - } finally { - mAccessFile = null; - } - } - } - - private void closeRead() throws IOException{ - if (mIsReading) { - mIsReading = false; - CloseAccessFileIfNeeded(); - } - } - - private void closeWrite(SampleChunk nextChunk) - throws IOException { - if (mIsWriting) { - mNextChunk = nextChunk; - mIsWriting = false; - mWriteFinished = true; - CloseAccessFileIfNeeded(); - } - } - - private boolean isReadFinished(IoState state) { - return mWriteFinished && state.equals(this, mWriteOffset); - } - - private SampleHolder read(IoState state) throws IOException { - if (mAccessFile == null || state.mChunk != this) { - throw new IllegalStateException("Requested read for wrong SampleChunk"); - } - long offset = state.mCurrentOffset; - if (offset >= mWriteOffset) { - if (mWriteFinished) { - throw new IllegalStateException("Requested read for wrong range"); - } else { - if (offset != mWriteOffset) { - Log.e(TAG, "This should not happen!"); - } - return null; - } - } - mAccessFile.seek(offset); - int size = mAccessFile.readInt(); - SampleHolder sample = mSamplePool.acquireSample(size); - sample.size = size; - sample.flags = mAccessFile.readInt(); - sample.timeUs = mAccessFile.readLong(); - sample.clearData(); - sample.data.put(mAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, - offset + SAMPLE_HEADER_LENGTH, sample.size)); - offset += sample.size + SAMPLE_HEADER_LENGTH; - state.mCurrentOffset = offset; - return sample; - } - - @VisibleForTesting - protected void write(SampleHolder sample, IoState state) - throws IOException { - if (mAccessFile == null || mNextChunk != null || !state.equals(this, mWriteOffset)) { - throw new IllegalStateException("Requested write for wrong SampleChunk"); - } - - mAccessFile.seek(mWriteOffset); - mAccessFile.writeInt(sample.size); - mAccessFile.writeInt(sample.flags); - mAccessFile.writeLong(sample.timeUs); - sample.data.position(0).limit(sample.size); - mAccessFile.getChannel().position(mWriteOffset + SAMPLE_HEADER_LENGTH).write(sample.data); - mWriteOffset += sample.size + SAMPLE_HEADER_LENGTH; - state.mCurrentOffset = mWriteOffset; - } - - private void release(boolean delete) { - mWriteFinished = true; - mIsReading = mIsWriting = false; - try { - if (mAccessFile != null) { - mAccessFile.close(); - } - } catch (IOException e) { - // Since the SampleChunk will not be reused, ignore exception. - } - if (delete) { - mFile.delete(); - mChunkCallback.onChunkDelete(this); - } - } - - /** - * Returns the start position. - */ - public long getStartPositionUs() { - return mStartPositionUs; - } - - /** - * Returns the creation time. - */ - public long getCreatedTimeMs() { - return mCreatedTimeMs; - } - - /** - * Returns the current size. - */ - public long getSize() { - return mWriteOffset; - } -} diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java deleted file mode 100644 index ca97a91a..00000000 --- a/src/com/android/tv/tuner/exoplayer/buffer/SampleChunkIoHelper.java +++ /dev/null @@ -1,461 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.exoplayer.buffer; - -import android.media.MediaCodec; -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; - -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.util.MimeTypes; -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; - -/** - * Handles all {@link SampleChunk} I/O operations. - * An I/O dedicated thread handles all I/O operations for synchronization. - */ -public class SampleChunkIoHelper implements Handler.Callback { - private static final String TAG = "SampleChunkIoHelper"; - - private static final int MAX_READ_BUFFER_SAMPLES = 3; - private static final int READ_RESCHEDULING_DELAY_MS = 10; - - private static final int MSG_OPEN_READ = 1; - private static final int MSG_OPEN_WRITE = 2; - 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; - private final @BufferReason int mBufferReason; - private final BufferManager mBufferManager; - private final SamplePool mSamplePool; - private final IoCallback mIoCallback; - - private Handler mIoHandler; - private final ConcurrentLinkedQueue<SampleHolder> mReadSampleBuffers[]; - private final ConcurrentLinkedQueue<SampleHolder> mHandlerReadSampleBuffers[]; - 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; - private boolean mFinished; - - /** - * A Callback for I/O events. - */ - public static abstract class IoCallback { - - /** - * Called when there is no sample to read. - */ - public void onIoReachedEos() { - } - - /** - * Called when there is an irrecoverable error during I/O. - */ - public void onIoError() { - } - } - - private class IoParams { - private final int index; - private final long positionUs; - private final SampleHolder sample; - private final ConditionVariable conditionVariable; - private final ConcurrentLinkedQueue<SampleHolder> readSampleBuffer; - - private IoParams(int index, long positionUs, SampleHolder sample, - ConditionVariable conditionVariable, - ConcurrentLinkedQueue<SampleHolder> readSampleBuffer) { - this.index = index; - this.positionUs = positionUs; - this.sample = sample; - this.conditionVariable = conditionVariable; - this.readSampleBuffer = readSampleBuffer; - } - } - - /** - * Creates {@link SampleChunk} I/O handler. - * - * @param ids track names - * @param mediaFormats {@link android.media.MediaFormat} for each track - * @param bufferReason reason to be buffered - * @param bufferManager manager of {@link SampleChunk} collections - * @param samplePool allocator for a sample - * @param ioCallback listeners for I/O events - */ - public SampleChunkIoHelper(List<String> ids, List<MediaFormat> mediaFormats, - @BufferReason int bufferReason, BufferManager bufferManager, SamplePool samplePool, - IoCallback ioCallback) { - mTrackCount = ids.size(); - mIds = ids; - mMediaFormats = mediaFormats; - mBufferReason = bufferReason; - mBufferManager = bufferManager; - mSamplePool = samplePool; - mIoCallback = ioCallback; - - mReadSampleBuffers = new ConcurrentLinkedQueue[mTrackCount]; - mHandlerReadSampleBuffers = new ConcurrentLinkedQueue[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) { - mWriteIndexEndPositionUs[i] = RecordingSampleBuffer.MIN_SEEK_DURATION_US; - mWriteChunkEndPositionUs[i] = mSampleChunkDurationUs; - mReadIoStates[i] = new SampleChunk.IoState(); - mWriteIoStates[i] = new SampleChunk.IoState(); - } - } - - /** - * Prepares and initializes for I/O operations. - * - * @throws IOException - */ - public void init() throws IOException { - HandlerThread handlerThread = new HandlerThread(TAG); - handlerThread.start(); - mIoHandler = new Handler(handlerThread.getLooper(), this); - if (mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDED_PLAYBACK) { - for (int i = 0; i < mTrackCount; ++i) { - mBufferManager.loadTrackFromStorage(mIds.get(i), mSamplePool); - } - mWriteEnded = true; - } else { - for (int i = 0; i < mTrackCount; ++i) { - mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_OPEN_WRITE, i)); - } - } - } - - /** - * Reads a sample if it is available. - * - * @param index track index - * @return {@code null} if a sample is not available, otherwise returns a sample - */ - public SampleHolder readSample(int index) { - SampleHolder sample = mReadSampleBuffers[index].poll(); - mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_READ, index)); - return sample; - } - - /** - * Writes a sample. - * - * @param index track index - * @param sample to write - * @param conditionVariable which will be wait until the write is finished - * @throws IOException - */ - public void writeSample(int index, SampleHolder sample, - ConditionVariable conditionVariable) throws IOException { - if (mErrorNotified) { - throw new IOException("Storage I/O error happened"); - } - conditionVariable.close(); - IoParams params = new IoParams(index, 0, sample, conditionVariable, null); - mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_WRITE, params)); - } - - /** - * Starts read from the specified position. - * - * @param index track index - * @param positionUs the specified position - */ - public void openRead(int index, long positionUs) { - // Old mReadSampleBuffers may have a pending read. - mReadSampleBuffers[index] = new ConcurrentLinkedQueue<>(); - IoParams params = new IoParams(index, positionUs, null, null, mReadSampleBuffers[index]); - mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_OPEN_READ, params)); - } - - /** - * 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() { - mIoHandler.sendEmptyMessage(MSG_CLOSE_WRITE); - } - - /** - * Finishes I/O operations and releases all the resources. - * @throws IOException - */ - public void release() throws IOException { - if (mIoHandler == null) { - return; - } - // Finishes all I/O operations. - ConditionVariable conditionVariable = new ConditionVariable(); - mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_RELEASE, conditionVariable)); - conditionVariable.block(); - - for (int i = 0; i < mTrackCount; ++i) { - mBufferManager.unregisterChunkEvictedListener(mIds.get(i)); - } - try { - if (mBufferReason == RecordingSampleBuffer.BUFFER_REASON_RECORDING && mTrackCount > 0) { - // Saves meta information for recording. - 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 (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(audios, videos); - } - } finally { - mBufferManager.release(); - mIoHandler.getLooper().quitSafely(); - } - } - - @Override - public boolean handleMessage(Message message) { - if (mFinished) { - return true; - } - releaseEvictedChunks(); - try { - switch (message.what) { - case MSG_OPEN_READ: - doOpenRead((IoParams) message.obj); - return true; - 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; - case MSG_READ: - doRead((int) message.obj); - return true; - case MSG_WRITE: - doWrite((IoParams) message.obj); - // Since only write will increase storage, eviction will be handled here. - return true; - case MSG_RELEASE: - doRelease((ConditionVariable) message.obj); - return true; - } - } catch (IOException e) { - mIoCallback.onIoError(); - mErrorNotified = true; - Log.e(TAG, "IoException happened", e); - return true; - } - return false; - } - - private void doOpenRead(IoParams params) throws IOException { - int index = params.index; - mIoHandler.removeMessages(MSG_READ, index); - 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(readPosition, TAG, errorMessage); - throw new IOException(errorMessage); - } - mSelectedTracks.add(index); - mReadIoStates[index].openRead(readPosition.first, (long) readPosition.second); - if (mHandlerReadSampleBuffers[index] != null) { - SampleHolder sample; - while ((sample = mHandlerReadSampleBuffers[index].poll()) != null) { - mSamplePool.releaseSample(sample); - } - } - mHandlerReadSampleBuffers[index] = params.readSampleBuffer; - mIoHandler.sendMessage(mIoHandler.obtainMessage(MSG_READ, index)); - } - - private void doOpenWrite(int index) throws IOException { - 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) { - // If enough samples are buffered, try again few moments later hoping that - // buffered samples are consumed. - mIoHandler.sendMessageDelayed( - mIoHandler.obtainMessage(MSG_READ, index), READ_RESCHEDULING_DELAY_MS); - } else { - if (mReadIoStates[index].isReadFinished()) { - for (int i = 0; i < mTrackCount; ++i) { - if (!mReadIoStates[i].isReadFinished()) { - return; - } - } - mIoCallback.onIoReachedEos(); - return; - } - SampleHolder sample = mReadIoStates[index].read(); - if (sample != null) { - mHandlerReadSampleBuffers[index].offer(sample); - } else { - // Read reached write but write is not finished yet --- wait a few moments to - // see if another sample is written. - mIoHandler.sendMessageDelayed( - mIoHandler.obtainMessage(MSG_READ, index), - READ_RESCHEDULING_DELAY_MS); - } - } - } - - private void doWrite(IoParams params) throws IOException { - try { - if (mWriteEnded) { - SoftPreconditions.checkState(false); - return; - } - int index = params.index; - SampleHolder sample = params.sample; - SampleChunk nextChunk = null; - if ((sample.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { - if (sample.timeUs > mBufferDurationUs) { - mBufferDurationUs = sample.timeUs; - } - 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); - } finally { - params.conditionVariable.open(); - } - } - - private void doCloseWrite() throws IOException { - if (mWriteEnded) { - return; - } - mWriteEnded = true; - boolean readFinished = true; - for (int i = 0; i < mTrackCount; ++i) { - readFinished = readFinished && mReadIoStates[i].isReadFinished(); - mWriteIoStates[i].closeWrite(); - } - if (readFinished) { - mIoCallback.onIoReachedEos(); - } - } - - private void doRelease(ConditionVariable conditionVariable) { - mIoHandler.removeCallbacksAndMessages(null); - mFinished = true; - conditionVariable.open(); - mSelectedTracks.clear(); - } - - private void releaseEvictedChunks() { - 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)), - currentStartPositionUs); - mBufferManager.evictChunks(mIds.get(i), evictEndPositionUs); - } - } -}
\ No newline at end of file diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SamplePool.java b/src/com/android/tv/tuner/exoplayer/buffer/SamplePool.java deleted file mode 100644 index bb048e85..00000000 --- a/src/com/android/tv/tuner/exoplayer/buffer/SamplePool.java +++ /dev/null @@ -1,71 +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.exoplayer.buffer; - -import com.google.android.exoplayer.SampleHolder; - -import java.util.LinkedList; - -/** - * Pool of samples to recycle ByteBuffers as much as possible. - */ -public class SamplePool { - private final LinkedList<SampleHolder> mSamplePool = new LinkedList<>(); - - /** - * Acquires a sample with a buffer larger than size from the pool. Allocate new one or resize - * an existing buffer if necessary. - */ - public synchronized SampleHolder acquireSample(int size) { - if (mSamplePool.isEmpty()) { - SampleHolder sample = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); - sample.ensureSpaceForWrite(size); - return sample; - } - SampleHolder smallestSufficientSample = null; - SampleHolder maxSample = mSamplePool.getFirst(); - for (SampleHolder sample : mSamplePool) { - // Grab the smallest sufficient sample. - if (sample.data.capacity() >= size && (smallestSufficientSample == null - || smallestSufficientSample.data.capacity() > sample.data.capacity())) { - smallestSufficientSample = sample; - } - - // Grab the max size sample. - if (maxSample.data.capacity() < sample.data.capacity()) { - maxSample = sample; - } - } - SampleHolder sampleFromPool = smallestSufficientSample; - - // If there's no sufficient sample, grab the maximum sample and resize it to size. - if (sampleFromPool == null) { - sampleFromPool = maxSample; - sampleFromPool.ensureSpaceForWrite(size); - } - mSamplePool.remove(sampleFromPool); - return sampleFromPool; - } - - /** - * Releases the sample back to the pool. - */ - public synchronized void releaseSample(SampleHolder sample) { - sample.clearData(); - mSamplePool.offerLast(sample); - } -} diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java b/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java deleted file mode 100644 index 75eac5a2..00000000 --- a/src/com/android/tv/tuner/exoplayer/buffer/SampleQueue.java +++ /dev/null @@ -1,75 +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.exoplayer.buffer; - -import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.SampleSource; - -import java.util.LinkedList; - -/** - * A sample queue which reads from the buffer and passes to player pipeline. - */ -public class SampleQueue { - private final LinkedList<SampleHolder> mQueue = new LinkedList<>(); - private final SamplePool mSamplePool; - private Long mLastQueuedPositionUs = null; - - public SampleQueue(SamplePool samplePool) { - mSamplePool = samplePool; - } - - public void queueSample(SampleHolder sample) { - mQueue.offer(sample); - mLastQueuedPositionUs = sample.timeUs; - } - - public int dequeueSample(SampleHolder sample) { - SampleHolder sampleFromQueue = mQueue.poll(); - if (sampleFromQueue == null) { - return SampleSource.NOTHING_READ; - } - sample.ensureSpaceForWrite(sampleFromQueue.size); - sample.size = sampleFromQueue.size; - sample.flags = sampleFromQueue.flags; - sample.timeUs = sampleFromQueue.timeUs; - sample.clearData(); - sampleFromQueue.data.position(0).limit(sample.size); - sample.data.put(sampleFromQueue.data); - mSamplePool.releaseSample(sampleFromQueue); - return SampleSource.SAMPLE_READ; - } - - public void clear() { - while (!mQueue.isEmpty()) { - mSamplePool.releaseSample(mQueue.poll()); - } - mLastQueuedPositionUs = null; - } - - public Long getLastQueuedPositionUs() { - return mLastQueuedPositionUs; - } - - public boolean isDurationGreaterThan(long durationUs) { - return !mQueue.isEmpty() && mQueue.getLast().timeUs - mQueue.getFirst().timeUs > durationUs; - } - - public boolean isEmpty() { - return mQueue.isEmpty(); - } -} diff --git a/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java b/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java deleted file mode 100644 index 159fde18..00000000 --- a/src/com/android/tv/tuner/exoplayer/buffer/SimpleSampleBuffer.java +++ /dev/null @@ -1,180 +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.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; -import com.google.android.exoplayer.SampleSource; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.tuner.tvinput.PlaybackBufferListener; -import com.android.tv.tuner.exoplayer.SampleExtractor; - -import java.io.IOException; -import java.util.List; - -/** - * Handles I/O for {@link SampleExtractor} when - * physical storage based buffer is not used. Trickplay is disabled. - */ -public class SimpleSampleBuffer implements BufferManager.SampleBuffer { - private final SamplePool mSamplePool = new SamplePool(); - private SampleQueue[] mPlayingSampleQueues; - private long mLastBufferedPositionUs = C.UNKNOWN_TIME_US; - - private volatile boolean mEos; - - public SimpleSampleBuffer(PlaybackBufferListener bufferListener) { - if (bufferListener != null) { - // Disables trickplay. - bufferListener.onBufferStateChanged(false); - } - } - - @Override - public synchronized void init(@NonNull List<String> ids, - @NonNull List<MediaFormat> mediaFormats) { - int trackCount = ids.size(); - mPlayingSampleQueues = new SampleQueue[trackCount]; - for (int i = 0; i < trackCount; i++) { - mPlayingSampleQueues[i] = null; - } - } - - @Override - public void setEos() { - mEos = true; - } - - private boolean reachedEos() { - return mEos; - } - - @Override - public void selectTrack(int index) { - synchronized (this) { - if (mPlayingSampleQueues[index] == null) { - mPlayingSampleQueues[index] = new SampleQueue(mSamplePool); - } else { - mPlayingSampleQueues[index].clear(); - } - } - } - - @Override - public void deselectTrack(int index) { - synchronized (this) { - if (mPlayingSampleQueues[index] != null) { - mPlayingSampleQueues[index].clear(); - mPlayingSampleQueues[index] = null; - } - } - } - - @Override - public synchronized long getBufferedPositionUs() { - Long result = null; - for (SampleQueue queue : mPlayingSampleQueues) { - if (queue == null) { - continue; - } - Long lastQueuedSamplePositionUs = queue.getLastQueuedPositionUs(); - if (lastQueuedSamplePositionUs == null) { - // No sample has been queued. - result = mLastBufferedPositionUs; - continue; - } - if (result == null || result > lastQueuedSamplePositionUs) { - result = lastQueuedSamplePositionUs; - } - } - if (result == null) { - return mLastBufferedPositionUs; - } - return (mLastBufferedPositionUs = result); - } - - @Override - public synchronized int readSample(int track, SampleHolder sampleHolder) { - SampleQueue queue = mPlayingSampleQueues[track]; - SoftPreconditions.checkNotNull(queue); - int result = queue == null ? SampleSource.NOTHING_READ : queue.dequeueSample(sampleHolder); - if (result != SampleSource.SAMPLE_READ && reachedEos()) { - return SampleSource.END_OF_STREAM; - } - return result; - } - - @Override - public void writeSample(int index, SampleHolder sample, - ConditionVariable conditionVariable) throws IOException { - sample.data.position(0).limit(sample.size); - SampleHolder sampleToQueue = mSamplePool.acquireSample(sample.size); - sampleToQueue.size = sample.size; - sampleToQueue.clearData(); - sampleToQueue.data.put(sample.data); - sampleToQueue.timeUs = sample.timeUs; - sampleToQueue.flags = sample.flags; - - synchronized (this) { - if (mPlayingSampleQueues[index] != null) { - mPlayingSampleQueues[index].queueSample(sampleToQueue); - } - } - } - - @Override - public boolean isWriteSpeedSlow(int sampleSize, long durationNs) { - // Since SimpleSampleBuffer write samples only to memory (not to physical storage), - // write speed is always fine. - return false; - } - - @Override - public void handleWriteSpeedSlow() { - // no-op - } - - @Override - public synchronized boolean continueBuffering(long positionUs) { - for (SampleQueue queue : mPlayingSampleQueues) { - if (queue == null) { - continue; - } - if (queue.getLastQueuedPositionUs() == null - || positionUs > queue.getLastQueuedPositionUs()) { - // No more buffered data. - return false; - } - } - return true; - } - - @Override - public void seekTo(long positionUs) { - // Not used. - } - - @Override - public void release() { - // Not used. - } -} diff --git a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java b/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java deleted file mode 100644 index 9fe921b8..00000000 --- a/src/com/android/tv/tuner/exoplayer/buffer/TrickplayStorageManager.java +++ /dev/null @@ -1,147 +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.exoplayer.buffer; - -import android.content.Context; -import android.os.AsyncTask; -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) - private static final String - SYS_STORAGE_THRESHOLD_PERCENTAGE = "sys_storage_threshold_percentage"; - private static final String - SYS_STORAGE_THRESHOLD_MAX_BYTES = "sys_storage_threshold_max_bytes"; - - // Copied from android.os.StorageManager - private static final int DEFAULT_THRESHOLD_PERCENTAGE = 10; - private static final long DEFAULT_THRESHOLD_MAX_BYTES = 500L * 1024 * 1024; - - private static AsyncTask<Void, Void, Void> sLastCacheCleanUpTask; - private static File sBufferDir; - private static long sStorageBufferBytes; - - private final long mMaxBufferSize; - - 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 lowPercentageToBytes = path.getTotalSpace() * lowPercentage / 100; - long maxLowBytes = Settings.Global.getLong(context.getContentResolver(), - SYS_STORAGE_THRESHOLD_MAX_BYTES, DEFAULT_THRESHOLD_MAX_BYTES); - sStorageBufferBytes = Math.min(lowPercentageToBytes, maxLowBytes); - } - - public TrickplayStorageManager(Context context, @NonNull File baseDir, long maxBufferSize) { - initParamsIfNeeded(context, new File(baseDir, BUFFER_DIR)); - sBufferDir.mkdirs(); - mMaxBufferSize = maxBufferSize; - clearStorage(); - } - - private void clearStorage() { - long now = System.currentTimeMillis(); - if (sLastCacheCleanUpTask != null) { - sLastCacheCleanUpTask.cancel(true); - } - 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; - } - }; - sLastCacheCleanUpTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - @Override - public File getBufferDir() { - return sBufferDir; - } - - @Override - public boolean isPersistent() { - return false; - } - - @Override - public boolean reachedStorageMax(long bufferSize, long pendingDelete) { - return bufferSize - pendingDelete > mMaxBufferSize; - } - - @Override - public boolean hasEnoughBuffer(long pendingDelete) { - return sBufferDir.getUsableSpace() + pendingDelete >= sStorageBufferBytes; - } - - @Override - public List<BufferManager.TrackFormat> readTrackInfoFiles(boolean isAudio) { - return null; - } - - @Override - public ArrayList<BufferManager.PositionHolder> readIndexFile(String trackId) { - return null; - } - - @Override - public void writeTrackInfoFiles(List<BufferManager.TrackFormat> formatList, boolean isAudio) { - } - - @Override - public void writeIndexFile(String trackName, - SortedMap<Long, Pair<SampleChunk, Integer>> index) { - } - -} diff --git a/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderClient.java b/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderClient.java deleted file mode 100644 index 356636cc..00000000 --- a/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderClient.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * 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.exoplayer.ffmpeg; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.IBinder; -import android.os.RemoteException; - -import android.support.annotation.MainThread; -import android.support.annotation.WorkerThread; -import android.support.annotation.VisibleForTesting; -import com.google.android.exoplayer.SampleHolder; -import com.android.tv.Features; -import com.android.tv.tuner.exoplayer.audio.AudioDecoder; - -import java.nio.ByteBuffer; - -/** - * The class connects {@link FfmpegDecoderService} to decode audio samples. - * In order to sandbox ffmpeg based decoder, {@link FfmpegDecoderService} is an isolated process - * without any permission and connected by binder. - */ -public class FfmpegDecoderClient extends AudioDecoder { - private static FfmpegDecoderClient sInstance; - - private IFfmpegDecoder mService; - private Boolean mIsAvailable; - - private static final String FFMPEG_DECODER_SERVICE_FILTER = - "com.android.tv.tuner.exoplayer.ffmpeg.IFfmpegDecoder"; - private static final long FFMPEG_SERVICE_CONNECT_TIMEOUT_MS = 500; - - private final ServiceConnection mConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName className, IBinder service) { - mService = IFfmpegDecoder.Stub.asInterface(service); - synchronized (FfmpegDecoderClient.this) { - try { - mIsAvailable = mService.isAvailable(); - } catch (RemoteException e) { - } - FfmpegDecoderClient.this.notify(); - } - } - - @Override - public void onServiceDisconnected(ComponentName className) { - synchronized (FfmpegDecoderClient.this) { - sInstance.releaseLocked(); - mIsAvailable = false; - mService = null; - } - } - }; - - /** - * Connects to the decoder service for future uses. - * @param context - * @return {@code true} when decoder service is connected. - */ - @MainThread - public synchronized static boolean connect(Context context) { - if (Features.AC3_SOFTWARE_DECODE.isEnabled(context)) { - if (sInstance == null) { - sInstance = new FfmpegDecoderClient(); - Intent intent = - new Intent(FFMPEG_DECODER_SERVICE_FILTER) - .setComponent( - new ComponentName(context, FfmpegDecoderService.class)); - if (context.bindService(intent, sInstance.mConnection, Context.BIND_AUTO_CREATE)) { - return true; - } else { - sInstance = null; - } - } - } - return false; - } - - /** - * Disconnects from the decoder service and release resources. - * @param context - */ - @MainThread - public synchronized static void disconnect(Context context) { - if (sInstance != null) { - synchronized (sInstance) { - sInstance.releaseLocked(); - if (sInstance.mIsAvailable != null && sInstance.mIsAvailable) { - context.unbindService(sInstance.mConnection); - } - sInstance.mIsAvailable = false; - sInstance.mService = null; - } - sInstance = null; - } - } - - /** - * Returns whether service is available or not. - * Before using client, this should be used to check availability. - */ - @WorkerThread - public synchronized static boolean isAvailable() { - if (sInstance != null) { - return sInstance.available(); - } - return false; - } - - /** - * Returns an client instance. - */ - public synchronized static FfmpegDecoderClient getInstance() { - if (sInstance != null) { - sInstance.createDecoder(); - } - return sInstance; - } - - private FfmpegDecoderClient() { - } - - private synchronized boolean available() { - if (mIsAvailable == null) { - try { - this.wait(FFMPEG_SERVICE_CONNECT_TIMEOUT_MS); - } catch (InterruptedException e) { - } - } - return mIsAvailable != null && mIsAvailable == true; - } - - private synchronized void createDecoder() { - if (mIsAvailable == null || mIsAvailable == false) { - return; - } - try { - mService.create(); - } catch (RemoteException e) { - } - } - - private void releaseLocked() { - if (mIsAvailable == null || mIsAvailable == false) { - return; - } - try { - mService.release(); - } catch (RemoteException e) { - } - } - - @Override - public synchronized void release() { - releaseLocked(); - } - - @Override - public synchronized void decode(SampleHolder sampleHolder) { - if (mIsAvailable == null || mIsAvailable == false) { - return; - } - byte[] sampleBytes = new byte [sampleHolder.data.limit()]; - sampleHolder.data.get(sampleBytes, 0, sampleBytes.length); - try { - mService.decode(sampleHolder.timeUs, sampleBytes); - } catch (RemoteException e) { - } - } - - @Override - public synchronized void resetDecoderState(String mimeType) { - if (mIsAvailable == null || mIsAvailable == false) { - return; - } - try { - mService.resetDecoderState(mimeType); - } catch (RemoteException e) { - } - } - - @Override - public synchronized ByteBuffer getDecodedSample() { - if (mIsAvailable == null || mIsAvailable == false) { - return null; - } - try { - byte[] outputBytes = mService.getDecodedSample(); - if (outputBytes != null && outputBytes.length > 0) { - return ByteBuffer.wrap(outputBytes); - } - } catch (RemoteException e) { - } - return null; - } - - @Override - public synchronized long getDecodedTimeUs() { - if (mIsAvailable == null || mIsAvailable == false) { - return 0; - } - try { - return mService.getDecodedTimeUs(); - } catch (RemoteException e) { - } - return 0; - } - - @VisibleForTesting - public boolean testSandboxIsolatedProcess() { - // When testing isolated process, we will check the permission in FfmpegDecoderService. - // If the service have any permission, an exception will be thrown. - try { - mService.testSandboxIsolatedProcess(); - } catch (RemoteException e) { - return false; - } - return true; - } - - @VisibleForTesting - public void testSandboxMinijail() { - // When testing minijail, we will call a system call which is blocked by minijail. In that - // case, the FfmpegDecoderService will be disconnected, we can check the connection status - // to make sure if the minijail works or not. - try { - mService.testSandboxMinijail(); - } catch (RemoteException e) { - } - } -} diff --git a/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderService.java b/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderService.java deleted file mode 100644 index 3ebdd381..00000000 --- a/src/com/android/tv/tuner/exoplayer/ffmpeg/FfmpegDecoderService.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * 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.exoplayer.ffmpeg; - -import android.app.Service; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.res.AssetFileDescriptor; -import android.os.AsyncTask; -import android.os.IBinder; -import android.os.ParcelFileDescriptor; -import android.util.Log; - -import com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioDecoder; - -import java.io.FileInputStream; -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * Ffmpeg based audio decoder service. - * It should be isolatedProcess due to security reason. - */ -public class FfmpegDecoderService extends Service { - private static final String TAG = "FfmpegDecoderService"; - private static final boolean DEBUG = false; - - private static final String POLICY_FILE = "whitelist.policy"; - - private static final long MINIJAIL_SETUP_WAIT_TIMEOUT_MS = 5000; - - private static boolean sLibraryLoaded = true; - - static { - try { - System.loadLibrary("minijail_jni"); - } catch (Exception | Error e) { - Log.e(TAG, "Load minijail failed:", e); - sLibraryLoaded = false; - } - } - - private FfmpegDecoder mBinder = new FfmpegDecoder(); - private volatile Object mMinijailSetupMonitor = new Object(); - //@GuardedBy("mMinijailSetupMonitor") - private volatile Boolean mMinijailSetup; - - @Override - public void onCreate() { - if (sLibraryLoaded) { - new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - synchronized (mMinijailSetupMonitor) { - int pipeFd = getPolicyPipeFd(); - if (pipeFd <= 0) { - Log.e(TAG, "fail to open policy file"); - mMinijailSetup = false; - } else { - nativeSetupMinijail(pipeFd); - mMinijailSetup = true; - if (DEBUG) Log.d(TAG, "Minijail setup successfully"); - } - mMinijailSetupMonitor.notify(); - } - return null; - } - }.execute(); - } else { - synchronized (mMinijailSetupMonitor) { - mMinijailSetup = false; - mMinijailSetupMonitor.notify(); - } - } - super.onCreate(); - } - - @Override - public IBinder onBind(Intent intent) { - return mBinder; - } - - private int getPolicyPipeFd() { - try { - ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); - final ParcelFileDescriptor.AutoCloseOutputStream outputStream = - new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]); - final AssetFileDescriptor policyFile = getAssets().openFd("whitelist.policy"); - final byte[] buffer = new byte[2048]; - final FileInputStream policyStream = policyFile.createInputStream(); - while (true) { - int bytesRead = policyStream.read(buffer); - if (bytesRead == -1) break; - outputStream.write(buffer, 0, bytesRead); - } - policyStream.close(); - outputStream.close(); - return pipe[0].detachFd(); - } catch (IOException e) { - Log.e(TAG, "Policy file not found:" + e); - } - return -1; - } - - private final class FfmpegDecoder extends IFfmpegDecoder.Stub { - FfmpegAudioDecoder mDecoder; - @Override - public boolean isAvailable() { - return isMinijailSetupDone() && FfmpegAudioDecoder.isAvailable(); - } - - @Override - public void create() { - mDecoder = new FfmpegAudioDecoder(FfmpegDecoderService.this); - } - - @Override - public void release() { - if (mDecoder != null) { - mDecoder.release(); - mDecoder = null; - } - } - - @Override - public void decode(long timeUs, byte[] sample) { - if (!isMinijailSetupDone()) { - // If minijail is not setup, we don't run decode for better security. - return; - } - mDecoder.decode(timeUs, sample); - } - - @Override - public void resetDecoderState(String mimetype) { - mDecoder.resetDecoderState(mimetype); - } - - @Override - public byte[] getDecodedSample() { - ByteBuffer decodedBuffer = mDecoder.getDecodedSample(); - byte[] ret = new byte[decodedBuffer.limit()]; - decodedBuffer.get(ret, 0, ret.length); - return ret; - } - - @Override - public long getDecodedTimeUs() { - return mDecoder.getDecodedTimeUs(); - } - - private boolean isMinijailSetupDone() { - synchronized (mMinijailSetupMonitor) { - if (DEBUG) Log.d(TAG, "mMinijailSetup in isAvailable(): " + mMinijailSetup); - if (mMinijailSetup == null) { - try { - if (DEBUG) Log.d(TAG, "Wait till Minijail setup is done"); - mMinijailSetupMonitor.wait(MINIJAIL_SETUP_WAIT_TIMEOUT_MS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - return mMinijailSetup != null && mMinijailSetup; - } - } - - @Override - public void testSandboxIsolatedProcess() { - if (!isMinijailSetupDone()) { - // If minijail is not setup, we return directly to make the test fail. - return; - } - if (FfmpegDecoderService.this.checkSelfPermission("android.permission.INTERNET") - == PackageManager.PERMISSION_GRANTED) { - throw new SecurityException("Shouldn't have the permission of internet"); - } - } - - @Override - public void testSandboxMinijail() { - if (!isMinijailSetupDone()) { - // If minijail is not setup, we return directly to make the test fail. - return; - } - nativeTestMinijail(); - } - } - - private native void nativeSetupMinijail(int policyFd); - private native void nativeTestMinijail(); -} diff --git a/src/com/android/tv/tuner/exoplayer/ffmpeg/IFfmpegDecoder.aidl b/src/com/android/tv/tuner/exoplayer/ffmpeg/IFfmpegDecoder.aidl deleted file mode 100644 index ed053790..00000000 --- a/src/com/android/tv/tuner/exoplayer/ffmpeg/IFfmpegDecoder.aidl +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.exoplayer.ffmpeg; - -interface IFfmpegDecoder { - boolean isAvailable(); - void create(); - void release(); - void resetDecoderState(String mimetype); - void decode(long timeUs, in byte[] sample); - byte[] getDecodedSample(); - long getDecodedTimeUs(); - void testSandboxIsolatedProcess(); - void testSandboxMinijail(); -}
\ No newline at end of file diff --git a/src/com/android/tv/tuner/layout/ScaledLayout.java b/src/com/android/tv/tuner/layout/ScaledLayout.java deleted file mode 100644 index 379ea70e..00000000 --- a/src/com/android/tv/tuner/layout/ScaledLayout.java +++ /dev/null @@ -1,274 +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.layout; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Point; -import android.graphics.Rect; -import android.hardware.display.DisplayManager; -import android.util.AttributeSet; -import android.util.Log; -import android.view.Display; -import android.view.View; -import android.view.ViewGroup; - -import com.android.tv.tuner.R; - -import java.util.Arrays; -import java.util.Comparator; - -/** - * A layout that scales its children using the given percentage value. - */ -public class ScaledLayout extends ViewGroup { - private static final String TAG = "ScaledLayout"; - private static final boolean DEBUG = false; - private static final Comparator<Rect> mRectTopLeftSorter = new Comparator<Rect>() { - @Override - public int compare(Rect lhs, Rect rhs) { - if (lhs.top != rhs.top) { - return lhs.top - rhs.top; - } else { - return lhs.left - rhs.left; - } - } - }; - - private Rect[] mRectArray; - private final int mMaxWidth; - private final int mMaxHeight; - - public ScaledLayout(Context context) { - this(context, null); - } - - public ScaledLayout(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public ScaledLayout(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - Point size = new Point(); - DisplayManager displayManager = (DisplayManager) getContext() - .getSystemService(Context.DISPLAY_SERVICE); - Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY); - display.getRealSize(size); - mMaxWidth = size.x; - mMaxHeight = size.y; - } - - /** - * ScaledLayoutParams stores the four scale factors. - * <br> - * Vertical coordinate system: ({@code scaleStartRow} * 100) % ~ ({@code scaleEndRow} * 100) % - * Horizontal coordinate system: ({@code scaleStartCol} * 100) % ~ ({@code scaleEndCol} * 100) % - * <br> - * In XML, for example, - * <pre> - * {@code - * <View - * app:layout_scaleStartRow="0.1" - * app:layout_scaleEndRow="0.5" - * app:layout_scaleStartCol="0.4" - * app:layout_scaleEndCol="1" /> - * } - * </pre> - */ - public static class ScaledLayoutParams extends ViewGroup.LayoutParams { - public static final float SCALE_UNSPECIFIED = -1; - public final float scaleStartRow; - public final float scaleEndRow; - public final float scaleStartCol; - public final float scaleEndCol; - - public ScaledLayoutParams(float scaleStartRow, float scaleEndRow, - float scaleStartCol, float scaleEndCol) { - super(MATCH_PARENT, MATCH_PARENT); - this.scaleStartRow = scaleStartRow; - this.scaleEndRow = scaleEndRow; - this.scaleStartCol = scaleStartCol; - this.scaleEndCol = scaleEndCol; - } - - public ScaledLayoutParams(Context context, AttributeSet attrs) { - super(MATCH_PARENT, MATCH_PARENT); - TypedArray array = - context.obtainStyledAttributes(attrs, R.styleable.utScaledLayout); - scaleStartRow = - array.getFloat(R.styleable.utScaledLayout_layout_scaleStartRow, SCALE_UNSPECIFIED); - scaleEndRow = - array.getFloat(R.styleable.utScaledLayout_layout_scaleEndRow, SCALE_UNSPECIFIED); - scaleStartCol = - array.getFloat(R.styleable.utScaledLayout_layout_scaleStartCol, SCALE_UNSPECIFIED); - scaleEndCol = - array.getFloat(R.styleable.utScaledLayout_layout_scaleEndCol, SCALE_UNSPECIFIED); - array.recycle(); - } - } - - @Override - public LayoutParams generateLayoutParams(AttributeSet attrs) { - return new ScaledLayoutParams(getContext(), attrs); - } - - @Override - protected boolean checkLayoutParams(LayoutParams p) { - return (p instanceof ScaledLayoutParams); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); - int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); - int width = widthSpecSize - getPaddingLeft() - getPaddingRight(); - int height = heightSpecSize - getPaddingTop() - getPaddingBottom(); - if (DEBUG) { - Log.d(TAG, String.format("onMeasure width: %d, height: %d", width, height)); - } - int count = getChildCount(); - mRectArray = new Rect[count]; - for (int i = 0; i < count; ++i) { - View child = getChildAt(i); - ViewGroup.LayoutParams params = child.getLayoutParams(); - float scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol; - if (!(params instanceof ScaledLayoutParams)) { - throw new RuntimeException( - "A child of ScaledLayout cannot have the UNSPECIFIED scale factors"); - } - scaleStartRow = ((ScaledLayoutParams) params).scaleStartRow; - scaleEndRow = ((ScaledLayoutParams) params).scaleEndRow; - scaleStartCol = ((ScaledLayoutParams) params).scaleStartCol; - scaleEndCol = ((ScaledLayoutParams) params).scaleEndCol; - if (scaleStartRow < 0 || scaleStartRow > 1) { - throw new RuntimeException("A child of ScaledLayout should have a range of " - + "scaleStartRow between 0 and 1"); - } - if (scaleEndRow < scaleStartRow || scaleStartRow > 1) { - throw new RuntimeException("A child of ScaledLayout should have a range of " - + "scaleEndRow between scaleStartRow and 1"); - } - if (scaleEndCol < 0 || scaleEndCol > 1) { - throw new RuntimeException("A child of ScaledLayout should have a range of " - + "scaleStartCol between 0 and 1"); - } - if (scaleEndCol < scaleStartCol || scaleEndCol > 1) { - throw new RuntimeException("A child of ScaledLayout should have a range of " - + "scaleEndCol between scaleStartCol and 1"); - } - if (DEBUG) { - Log.d(TAG, String.format("onMeasure child scaleStartRow: %f scaleEndRow: %f " - + "scaleStartCol: %f scaleEndCol: %f", - scaleStartRow, scaleEndRow, scaleStartCol, scaleEndCol)); - } - mRectArray[i] = new Rect((int) (scaleStartCol * width), (int) (scaleStartRow * height), - (int) (scaleEndCol * width), (int) (scaleEndRow * height)); - int scaleWidth = (int) (width * (scaleEndCol - scaleStartCol)); - int childWidthSpec = MeasureSpec.makeMeasureSpec( - scaleWidth > mMaxWidth ? mMaxWidth : scaleWidth, MeasureSpec.EXACTLY); - int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - child.measure(childWidthSpec, childHeightSpec); - - // If the height of the measured child view is bigger than the height of the calculated - // region by the given ScaleLayoutParams, the height of the region should be increased - // to fit the size of the child view. - if (child.getMeasuredHeight() > mRectArray[i].height()) { - int overflowedHeight = child.getMeasuredHeight() - mRectArray[i].height(); - overflowedHeight = (overflowedHeight + 1) / 2; - mRectArray[i].bottom += overflowedHeight; - mRectArray[i].top -= overflowedHeight; - if (mRectArray[i].top < 0) { - mRectArray[i].bottom -= mRectArray[i].top; - mRectArray[i].top = 0; - } - if (mRectArray[i].bottom > height) { - mRectArray[i].top -= mRectArray[i].bottom - height; - mRectArray[i].bottom = height; - } - } - int scaleHeight = (int) (height * (scaleEndRow - scaleStartRow)); - childHeightSpec = MeasureSpec.makeMeasureSpec( - scaleHeight > mMaxHeight ? mMaxHeight : scaleHeight, MeasureSpec.EXACTLY); - child.measure(childWidthSpec, childHeightSpec); - } - - // Avoid overlapping rectangles. - // Step 1. Sort rectangles by position (top-left). - int visibleRectCount = 0; - int[] visibleRectGroup = new int[count]; - Rect[] visibleRectArray = new Rect[count]; - for (int i = 0; i < count; ++i) { - if (getChildAt(i).getVisibility() == View.VISIBLE) { - visibleRectGroup[visibleRectCount] = visibleRectCount; - visibleRectArray[visibleRectCount] = mRectArray[i]; - ++visibleRectCount; - } - } - Arrays.sort(visibleRectArray, 0, visibleRectCount, mRectTopLeftSorter); - - // Step 2. Move down if there are overlapping rectangles. - for (int i = 0; i < visibleRectCount - 1; ++i) { - for (int j = i + 1; j < visibleRectCount; ++j) { - if (Rect.intersects(visibleRectArray[i], visibleRectArray[j])) { - visibleRectGroup[j] = visibleRectGroup[i]; - visibleRectArray[j].set(visibleRectArray[j].left, - visibleRectArray[i].bottom, - visibleRectArray[j].right, - visibleRectArray[i].bottom + visibleRectArray[j].height()); - } - } - } - - // Step 3. Move up if there is any overflowed rectangle. - for (int i = visibleRectCount - 1; i >= 0; --i) { - if (visibleRectArray[i].bottom > height) { - int overflowedHeight = visibleRectArray[i].bottom - height; - for (int j = 0; j <= i; ++j) { - if (visibleRectGroup[i] == visibleRectGroup[j]) { - visibleRectArray[j].set(visibleRectArray[j].left, - visibleRectArray[j].top - overflowedHeight, - visibleRectArray[j].right, - visibleRectArray[j].bottom - overflowedHeight); - } - } - } - } - setMeasuredDimension(widthSpecSize, heightSpecSize); - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - int paddingLeft = getPaddingLeft(); - int paddingTop = getPaddingTop(); - int count = getChildCount(); - for (int i = 0; i < count; ++i) { - View child = getChildAt(i); - if (child.getVisibility() != GONE) { - int childLeft = paddingLeft + mRectArray[i].left; - int childTop = paddingTop + mRectArray[i].top; - int childBottom = paddingLeft + mRectArray[i].bottom; - int childRight = paddingTop + mRectArray[i].right; - if (DEBUG) { - Log.d(TAG, String.format("layoutChild bottom: %d left: %d right: %d top: %d", - childBottom, childLeft, - childRight, childTop)); - } - child.layout(childLeft, childTop, childRight, childBottom); - } - } - } -} diff --git a/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java b/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java deleted file mode 100644 index e0e21a20..00000000 --- a/src/com/android/tv/tuner/setup/ConnectionTypeFragment.java +++ /dev/null @@ -1,101 +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.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 com.android.tv.common.BuildConfig; -import com.android.tv.common.ui.setup.SetupGuidedStepFragment; -import com.android.tv.common.ui.setup.SetupMultiPaneFragment; -import com.android.tv.tuner.R; - -import java.util.List; -import java.util.TimeZone; - -/** - * A fragment for connection type selection. - */ -public class ConnectionTypeFragment extends SetupMultiPaneFragment { - public static final String ACTION_CATEGORY = - "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(); - } - - @Override - protected String getActionCategory() { - return ACTION_CATEGORY; - } - - @Override - protected boolean needsDoneButton() { - return false; - } - - public static class ContentFragment extends SetupGuidedStepFragment { - - @NonNull - @Override - public Guidance onCreateGuidance(Bundle savedInstanceState) { - return new Guidance(getString(R.string.ut_connection_title), - getString(R.string.ut_connection_description), - getString(R.string.ut_setup_breadcrumb), null); - } - - @Override - public void onCreateActions(@NonNull List<GuidedAction> actions, - Bundle savedInstanceState) { - String[] choices = getResources().getStringArray(R.array.ut_connection_choices); - int length = choices.length - 1; - int startOffset = 0; - for (int i = 0; i < length; ++i) { - actions.add(new GuidedAction.Builder(getActivity()) - .id(startOffset + i) - .title(choices[i]) - .build()); - } - } - - @Override - protected String getActionCategory() { - return ACTION_CATEGORY; - } - } -} diff --git a/src/com/android/tv/tuner/setup/PostalCodeFragment.java b/src/com/android/tv/tuner/setup/PostalCodeFragment.java deleted file mode 100644 index 025b9193..00000000 --- a/src/com/android/tv/tuner/setup/PostalCodeFragment.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * 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.InputFilter.AllCaps; -import android.view.View; -import android.widget.TextView; -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 com.android.tv.util.LocationUtils; -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(); - String region = LocationUtils.getCurrentCountry(getContext()); - if (postalCode != null && PostalCodeUtils.matches(postalCode, region)) { - 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) { - int maxLength = PostalCodeUtils.getRegionMaxLength(getContext()); - mEditedActionView = getView().findViewById(R.id.guidedactions_editable); - ((TextView) mEditedActionView.findViewById(R.id.guidedactions_item_title)) - .setFilters( - new InputFilter[] { - new InputFilter.LengthFilter(maxLength), new AllCaps() - }); - } - 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 deleted file mode 100644 index b6936e38..00000000 --- a/src/com/android/tv/tuner/setup/ScanFragment.java +++ /dev/null @@ -1,523 +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.setup; - -import android.animation.LayoutTransition; -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; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.BaseAdapter; -import android.widget.Button; -import android.widget.ListView; -import android.widget.ProgressBar; -import android.widget.TextView; - -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.R; -import com.android.tv.tuner.TunerHal; -import com.android.tv.tuner.TunerPreferences; -import com.android.tv.tuner.data.PsipData; -import com.android.tv.tuner.data.TunerChannel; -import com.android.tv.tuner.data.nano.Channel; -import com.android.tv.tuner.source.FileTsStreamer; -import com.android.tv.tuner.source.TsDataSource; -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 java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -/** - * A fragment for scanning channels. - */ -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; - - private static final String VCTLESS_CHANNEL_NAME_FORMAT = "RF%d-%d"; - - public static final String ACTION_CATEGORY = "com.android.tv.tuner.setup.ScanFragment"; - public static final int ACTION_CANCEL = 1; - public static final int ACTION_FINISH = 2; - - public static final String EXTRA_FOR_CHANNEL_SCAN_FILE = "scan_file_choice"; - - private static final long CHANNEL_SCAN_SHOW_DELAY_MS = 10000; - private static final long CHANNEL_SCAN_PERIOD_MS = 4000; - private static final long SHOW_PROGRESS_DIALOG_DELAY_MS = 300; - - // Build channels out of the locally stored TS streams. - private static final boolean SCAN_LOCAL_STREAMS = true; - - private ChannelDataManager mChannelDataManager; - private ChannelScanTask mChannelScanTask; - private ProgressBar mProgressBar; - private TextView mScanningMessage; - private View mChannelHolder; - private ChannelAdapter mAdapter; - private volatile boolean mChannelListVisible; - private Button mCancelButton; - - @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()); - mAdapter = new ChannelAdapter(); - mProgressBar = (ProgressBar) view.findViewById(R.id.tune_progress); - mScanningMessage = (TextView) view.findViewById(R.id.tune_description); - ListView channelList = (ListView) view.findViewById(R.id.channel_list); - channelList.setAdapter(mAdapter); - channelList.setOnItemClickListener(null); - ViewGroup progressHolder = (ViewGroup) view.findViewById(R.id.progress_holder); - LayoutTransition transition = new LayoutTransition(); - transition.enableTransitionType(LayoutTransition.CHANGING); - progressHolder.setLayoutTransition(transition); - mChannelHolder = view.findViewById(R.id.channel_holder); - mCancelButton = (Button) view.findViewById(R.id.tune_cancel); - mCancelButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - finishScan(false); - } - }); - 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); - 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; - } - - @Override - protected int getLayoutResourceId() { - return R.layout.ut_channel_scan; - } - - @Override - protected int[] getParentIdsForDelay() { - return new int[] {R.id.progress_holder}; - } - - private void startScan(int channelMapId) { - mChannelScanTask = new ChannelScanTask(channelMapId); - mChannelScanTask.execute(); - } - - @Override - 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.onPause(); - } - - /** - * Finishes the current scan thread. This fragment will be popped after the scan thread ends. - * - * @param cancel a flag which indicates the scan is canceled or not. - */ - public void finishScan(boolean cancel) { - if (mChannelScanTask != null) { - mChannelScanTask.cancelScan(cancel); - - // Notifies a user of waiting to finish the scanning process. - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - if (mChannelScanTask != null) { - mChannelScanTask.showFinishingProgressDialog(); - } - } - }, SHOW_PROGRESS_DIALOG_DELAY_MS); - - // Hides the cancel button. - mCancelButton.setEnabled(false); - } - } - - private class ChannelAdapter extends BaseAdapter { - private final ArrayList<TunerChannel> mChannels; - - public ChannelAdapter() { - mChannels = new ArrayList<>(); - } - - @Override - public boolean areAllItemsEnabled() { - return false; - } - - @Override - public boolean isEnabled(int pos) { - return false; - } - - @Override - public int getCount() { - return mChannels.size(); - } - - @Override - public Object getItem(int pos) { - return pos; - } - - @Override - public long getItemId(int pos) { - return pos; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - final Context context = parent.getContext(); - - if (convertView == null) { - LayoutInflater inflater = (LayoutInflater) context - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - convertView = inflater.inflate(R.layout.ut_channel_list, parent, false); - } - - TextView channelNum = (TextView) convertView.findViewById(R.id.channel_num); - channelNum.setText(mChannels.get(position).getDisplayNumber()); - - TextView channelName = (TextView) convertView.findViewById(R.id.channel_name); - channelName.setText(mChannels.get(position).getName()); - return convertView; - } - - public void add(TunerChannel channel) { - mChannels.add(channel); - notifyDataSetChanged(); - } - } - - private class ChannelScanTask extends AsyncTask<Void, Integer, Void> - implements EventDetector.EventListener, ChannelDataManager.ChannelScanListener { - private static final int MAX_PROGRESS = 100; - - private final Activity mActivity; - private final int mChannelMapId; - private final TsStreamer mScanTsStreamer; - private final TsStreamer mFileTsStreamer; - private final ConditionVariable mConditionStopped; - - private final List<ChannelScanFileParser.ScanChannel> mScanChannelList = new ArrayList<>(); - private boolean mIsCanceled; - private boolean mIsFinished; - private ProgressDialog mFinishingProgressDialog; - private CountDownLatch mLatch; - - public ChannelScanTask(int channelMapId) { - mActivity = getActivity(); - mChannelMapId = channelMapId; - if (FAKE_MODE) { - mScanTsStreamer = new FakeTsStreamer(this); - } else { - TunerHal hal = ((TunerSetupActivity) mActivity).getTunerHal(); - if (hal == null) { - throw new RuntimeException("Failed to open a DVB device"); - } - mScanTsStreamer = new TunerTsStreamer(hal, this); - } - mFileTsStreamer = SCAN_LOCAL_STREAMS ? new FileTsStreamer(this, mActivity) : null; - mConditionStopped = new ConditionVariable(); - mChannelDataManager.setChannelScanListener(this, new Handler()); - } - - private void maybeSetChannelListVisible() { - mActivity.runOnUiThread(new Runnable() { - @Override - public void run() { - int channelsFound = mAdapter.getCount(); - if (!mChannelListVisible && channelsFound > 0) { - String format = getResources().getQuantityString( - R.plurals.ut_channel_scan_message, channelsFound, channelsFound); - mScanningMessage.setText(String.format(format, channelsFound)); - mChannelHolder.setVisibility(View.VISIBLE); - mChannelListVisible = true; - } - } - }); - } - - private void addChannel(final TunerChannel channel) { - mActivity.runOnUiThread(new Runnable() { - @Override - public void run() { - mAdapter.add(channel); - if (mChannelListVisible) { - int channelsFound = mAdapter.getCount(); - String format = getResources().getQuantityString( - R.plurals.ut_channel_scan_message, channelsFound, channelsFound); - mScanningMessage.setText(String.format(format, channelsFound)); - } - } - }); - } - - @Override - protected Void doInBackground(Void... params) { - mScanChannelList.clear(); - if (SCAN_LOCAL_STREAMS) { - FileTsStreamer.addLocalStreamFiles(mScanChannelList); - } - mScanChannelList.addAll(ChannelScanFileParser.parseScanFile( - getResources().openRawResource(mChannelMapId))); - scanChannels(); - return null; - } - - @Override - protected void onCancelled() { - SoftPreconditions.checkState(false, TAG, "call cancelScan instead of cancel"); - } - - @Override - protected void onProgressUpdate(Integer... values) { - 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(); - } - - private void cancelScan(boolean cancel) { - mIsCanceled = cancel; - stopScan(); - } - - private void scanChannels() { - if (DEBUG) Log.i(TAG, "Channel scan starting"); - mChannelDataManager.notifyScanStarted(); - - long startMs = System.currentTimeMillis(); - int i = 1; - for (ChannelScanFileParser.ScanChannel scanChannel : mScanChannelList) { - int frequency = scanChannel.frequency; - String modulation = scanChannel.modulation; - Log.i(TAG, "Tuning to " + frequency + " " + modulation); - - TsStreamer streamer = getStreamer(scanChannel.type); - SoftPreconditions.checkNotNull(streamer); - if (streamer != null && streamer.startStream(scanChannel)) { - mLatch = new CountDownLatch(1); - try { - mLatch.await(CHANNEL_SCAN_PERIOD_MS, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - Log.e(TAG, "The current thread is interrupted during scanChannels(). " + - "The TS stream is stopped earlier than expected.", e); - } - streamer.stopStream(); - - addChannelsWithoutVct(scanChannel); - if (System.currentTimeMillis() > startMs + CHANNEL_SCAN_SHOW_DELAY_MS - && !mChannelListVisible) { - maybeSetChannelListVisible(); - } - } - if (mConditionStopped.block(-1)) { - break; - } - publishProgress(MAX_PROGRESS * i++ / mScanChannelList.size()); - } - mChannelDataManager.notifyScanCompleted(); - if (!mConditionStopped.block(-1)) { - publishProgress(MAX_PROGRESS); - } - if (DEBUG) Log.i(TAG, "Channel scan ended"); - } - - - private void addChannelsWithoutVct(ChannelScanFileParser.ScanChannel scanChannel) { - if (scanChannel.radioFrequencyNumber == null - || !(mScanTsStreamer instanceof TunerTsStreamer)) { - return; - } - for (TunerChannel tunerChannel - : ((TunerTsStreamer) mScanTsStreamer).getMalFormedChannels()) { - if ((tunerChannel.getVideoPid() != TunerChannel.INVALID_PID) - && (tunerChannel.getAudioPid() != TunerChannel.INVALID_PID)) { - tunerChannel.setFrequency(scanChannel.frequency); - tunerChannel.setModulation(scanChannel.modulation); - tunerChannel.setShortName(String.format(Locale.US, VCTLESS_CHANNEL_NAME_FORMAT, - scanChannel.radioFrequencyNumber, - tunerChannel.getProgramNumber())); - tunerChannel.setVirtualMajor(scanChannel.radioFrequencyNumber); - tunerChannel.setVirtualMinor(tunerChannel.getProgramNumber()); - onChannelDetected(tunerChannel, true); - } - } - } - - private TsStreamer getStreamer(int type) { - switch (type) { - case Channel.TYPE_TUNER: - return mScanTsStreamer; - case Channel.TYPE_FILE: - return mFileTsStreamer; - default: - return null; - } - } - - @Override - public void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items) { - mChannelDataManager.notifyEventDetected(channel, items); - } - - @Override - public void onChannelScanDone() { - if (mLatch != null) { - mLatch.countDown(); - } - } - - @Override - public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) { - if (channelArrivedAtFirstTime) { - Log.i(TAG, "Found channel " + channel); - } - if (channelArrivedAtFirstTime && channel.hasAudio()) { - // Playbacks with video-only stream have not been tested yet. - // No video-only channel has been found. - addChannel(channel); - mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime); - } - } - - public void showFinishingProgressDialog() { - // Show a progress dialog to wait for the scanning process if it's not done yet. - if (!mIsFinished && mFinishingProgressDialog == null) { - mFinishingProgressDialog = ProgressDialog.show(mActivity, "", - getString(R.string.ut_setup_cancel), true, false); - } - } - - @Override - public void onChannelHandlingDone() { - mChannelDataManager.setCurrentVersion(mActivity); - mChannelDataManager.releaseSafely(); - mIsFinished = true; - TunerPreferences.setScannedChannelCount(mActivity.getApplicationContext(), - mChannelDataManager.getScannedChannelCount()); - // Cancel a previously shown notification. - TunerSetupActivity.cancelNotification(mActivity.getApplicationContext()); - // Mark scan as done - TunerPreferences.setScanDone(mActivity.getApplicationContext()); - // finishing will be done manually. - if (mFinishingProgressDialog != null) { - mFinishingProgressDialog.dismiss(); - } - // 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; - } - } - - private static class FakeTsStreamer implements TsStreamer { - private final EventDetector.EventListener mEventListener; - private int mProgramNumber = 0; - - FakeTsStreamer(EventDetector.EventListener eventListener) { - mEventListener = eventListener; - } - - @Override - public boolean startStream(ChannelScanFileParser.ScanChannel channel) { - if (++mProgramNumber % 2 == 1) { - return true; - } - final String displayNumber = Integer.toString(mProgramNumber); - final String name = "Channel-" + mProgramNumber; - mEventListener.onChannelDetected(new TunerChannel(mProgramNumber, new ArrayList<>()) { - @Override - public String getDisplayNumber() { - return displayNumber; - } - - @Override - public String getName() { - return name; - } - }, true); - return true; - } - - @Override - public boolean startStream(TunerChannel channel) { - return false; - } - - @Override - public void stopStream() { - } - - @Override - public TsDataSource createDataSource() { - return null; - } - } -} diff --git a/src/com/android/tv/tuner/setup/ScanResultFragment.java b/src/com/android/tv/tuner/setup/ScanResultFragment.java deleted file mode 100644 index 3b8cd823..00000000 --- a/src/com/android/tv/tuner/setup/ScanResultFragment.java +++ /dev/null @@ -1,127 +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.setup; - -import android.content.Context; -import android.content.res.Resources; -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 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; - -import java.util.List; - -/** - * A fragment for initial screen. - */ -public class ScanResultFragment extends SetupMultiPaneFragment { - public static final String ACTION_CATEGORY = - "com.android.tv.tuner.setup.ScanResultFragment"; - - @Override - protected SetupGuidedStepFragment onCreateContentFragment() { - return new ContentFragment(); - } - - @Override - protected String getActionCategory() { - return ACTION_CATEGORY; - } - - @Override - protected boolean needsDoneButton() { - return false; - } - - public static class ContentFragment extends SetupGuidedStepFragment { - private int mChannelCountOnPreference; - - @Override - public void onAttach(Context context) { - super.onAttach(context); - mChannelCountOnPreference = TunerPreferences.getScannedChannelCount(context); - } - - @NonNull - @Override - public Guidance onCreateGuidance(Bundle savedInstanceState) { - String title; - String description; - String breadcrumb; - if (mChannelCountOnPreference > 0) { - Resources res = getResources(); - title = res.getQuantityString(R.plurals.ut_result_found_title, - mChannelCountOnPreference, mChannelCountOnPreference); - description = res.getQuantityString(R.plurals.ut_result_found_description, - 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); - 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); - } - return new Guidance(title, description, breadcrumb, null); - } - - @Override - public void onCreateActions(@NonNull List<GuidedAction> actions, - Bundle savedInstanceState) { - String[] choices; - int doneActionIndex; - if (mChannelCountOnPreference > 0) { - choices = getResources().getStringArray(R.array.ut_result_found_choices); - doneActionIndex = 0; - } else { - choices = getResources().getStringArray(R.array.ut_result_not_found_choices); - doneActionIndex = 1; - } - for (int i = 0; i < choices.length; ++i) { - if (i == doneActionIndex) { - actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_DONE) - .title(choices[i]).build()); - } else { - actions.add(new GuidedAction.Builder(getActivity()).id(i).title(choices[i]) - .build()); - } - } - } - - @Override - protected String getActionCategory() { - return ACTION_CATEGORY; - } - } -} diff --git a/src/com/android/tv/tuner/setup/TunerSetupActivity.java b/src/com/android/tv/tuner/setup/TunerSetupActivity.java deleted file mode 100644 index e9f3baa7..00000000 --- a/src/com/android/tv/tuner/setup/TunerSetupActivity.java +++ /dev/null @@ -1,543 +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.setup; - -import android.app.Fragment; -import android.app.FragmentManager; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -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.Build; -import android.os.Bundle; -import android.support.annotation.MainThread; -import android.support.annotation.NonNull; -import android.support.annotation.VisibleForTesting; -import android.support.annotation.WorkerThread; -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.Features; -import com.android.tv.TvApplication; -import com.android.tv.common.AutoCloseableUtils; -import com.android.tv.common.SoftPreconditions; -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.PostalCodeUtils; - -import java.util.concurrent.Executor; - -/** - * An activity that serves tuner setup process. - */ -public class TunerSetupActivity extends SetupActivity { - 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 notification. - private static final String TV_ACTIVITY_CLASS_NAME = "com.android.tv.TvActivity"; - private static final String TUNER_SET_UP_NOTIFICATION_CHANNEL_ID = "tuner_setup_channel"; - 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, - R.raw.ut_us_cable_standard_center_frequencies_qam256, - R.raw.ut_us_all, - R.raw.ut_kr_atsc_center_frequencies_8vsb, - R.raw.ut_kr_cable_standard_center_frequencies_qam256, - R.raw.ut_kr_all, - R.raw.ut_kr_dev_cj_cable_center_frequencies_qam256, - R.raw.ut_euro_dvbt_all, - R.raw.ut_euro_dvbt_all, - R.raw.ut_euro_dvbt_all - }; - - private ScanFragment mLastScanFragment; - private Integer mTunerType; - private TunerHalFactory mTunerHalFactory; - private boolean mNeedToShowPostalCodeFragment; - private String mPreviousPostalCode; - - @Override - protected void onCreate(Bundle savedInstanceState) { - if (DEBUG) Log.d(TAG, "onCreate"); - new AsyncTask<Void, Void, Integer>() { - @Override - protected Integer doInBackground(Void... arg0) { - return TunerHal.getTunerTypeAndCount(TunerSetupActivity.this).first; - } - - @Override - protected void onPostExecute(Integer result) { - if (!TunerSetupActivity.this.isDestroyed()) { - mTunerType = result; - if (result == null) { - finish(); - } else { - showInitialFragment(); - } - } - } - }.execute(); - TvApplication.setCurrentRunningProcess(this, false); - super.onCreate(savedInstanceState); - // TODO: check {@link shouldShowRequestPermissionRationale}. - if (checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) - != PackageManager.PERMISSION_GRANTED) { - // No need to check the request result. - requestPermissions(new String[] {android.Manifest.permission.ACCESS_COARSE_LOCATION}, - PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION); - } - mTunerHalFactory = new TunerHalFactory(getApplicationContext()); - try { - // Updating postal code takes time, therefore we called it here for "warm-up". - mPreviousPostalCode = PostalCodeUtils.getLastPostalCode(this); - 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() { - if (mTunerType != null) { - 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; - } else { - return null; - } - } - - @Override - protected boolean executeAction(String category, int actionId, Bundle params) { - switch (category) { - case WelcomeFragment.ACTION_CATEGORY: - switch (actionId) { - case SetupMultiPaneFragment.ACTION_DONE: - // If the scan was performed, then the result should be OK. - setResult(mLastScanFragment == null ? RESULT_CANCELED : RESULT_OK); - finish(); - break; - default: - if (mNeedToShowPostalCodeFragment - || Features.ENABLE_CLOUD_EPG_REGION.isEnabled( - 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: - if (mTunerHalFactory.getOrCreate() == null) { - finish(); - Toast.makeText(getApplicationContext(), - R.string.ut_channel_scan_tuner_unavailable,Toast.LENGTH_LONG).show(); - return true; - } - mLastScanFragment = new ScanFragment(); - Bundle args1 = new Bundle(); - args1.putInt(ScanFragment.EXTRA_FOR_CHANNEL_SCAN_FILE, - CHANNEL_MAP_SCAN_FILE[actionId]); - args1.putInt(KEY_TUNER_TYPE, mTunerType); - mLastScanFragment.setArguments(args1); - showFragment(mLastScanFragment, true); - return true; - case ScanFragment.ACTION_CATEGORY: - switch (actionId) { - case ScanFragment.ACTION_CANCEL: - 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); - return true; - } - break; - case ScanResultFragment.ACTION_CATEGORY: - switch (actionId) { - case SetupMultiPaneFragment.ACTION_DONE: - setResult(RESULT_OK); - finish(); - break; - default: - SetupFragment fragment = new ConnectionTypeFragment(); - fragment.setShortDistance(SetupFragment.FRAGMENT_ENTER_TRANSITION - | SetupFragment.FRAGMENT_RETURN_TRANSITION); - showFragment(fragment, true); - break; - } - return true; - } - return false; - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_BACK) { - FragmentManager manager = getFragmentManager(); - int count = manager.getBackStackEntryCount(); - if (count > 0) { - String lastTag = manager.getBackStackEntryAt(count - 1).getName(); - if (ScanResultFragment.class.getCanonicalName().equals(lastTag) && count >= 2) { - // Pops fragment including ScanFragment. - manager.popBackStack(manager.getBackStackEntryAt(count - 2).getName(), - FragmentManager.POP_BACK_STACK_INCLUSIVE); - return true; - } else if (ScanFragment.class.getCanonicalName().equals(lastTag)) { - mLastScanFragment.finishScan(true); - return true; - } - } - } - return super.onKeyUp(keyCode, event); - } - - @Override - public void onDestroy() { - if (mPreviousPostalCode != null && PostalCodeUtils.getLastPostalCode(this) == null) { - PostalCodeUtils.setLastPostalCode(this, mPreviousPostalCode); - } - super.onDestroy(); - } - - /** - * A callback to be invoked when the TvInputService is enabled or disabled. - * - * @param context a {@link Context} instance - * @param enabled {@code true} for the {@link TunerTvInputService} to be enabled; - * otherwise {@code false} - */ - public static void onTvInputEnabled(Context context, boolean enabled, Integer tunerType) { - // Send a notification for tuner setup if there's no channels and the tuner TV input - // setup has been not done. - boolean channelScanDoneOnPreference = TunerPreferences.isScanDone(context); - int channelCountOnPreference = TunerPreferences.getScannedChannelCount(context); - if (enabled && !channelScanDoneOnPreference && channelCountOnPreference == 0) { - TunerPreferences.setShouldShowSetupActivity(context, true); - sendNotification(context, tunerType); - } else { - TunerPreferences.setShouldShowSetupActivity(context, false); - cancelNotification(context); - } - } - - /** - * Returns a {@link Intent} to launch the tuner TV input service. - * - * @param context a {@link Context} instance - */ - public static Intent createSetupActivity(Context context) { - String inputId = TvContract.buildInputId(new ComponentName(context.getPackageName(), - TunerTvInputService.class.getName())); - - // 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); - Intent tvActivityIntent = new Intent(); - tvActivityIntent.setComponent(new ComponentName(context, TV_ACTIVITY_CLASS_NAME)); - intent.putExtra(TvCommonConstants.EXTRA_ACTIVITY_AFTER_COMPLETION, tvActivityIntent); - return intent; - } - - /** - * Gets the currently used tuner HAL. - */ - TunerHal getTunerHal() { - return mTunerHalFactory.getOrCreate(); - } - - /** - * 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 - */ - private static PendingIntent createPendingIntentForSetupActivity(Context context) { - return PendingIntent.getActivity(context, 0, createSetupActivity(context), - PendingIntent.FLAG_UPDATE_CURRENT); - } - - private static void sendNotification(Context context, Integer tunerType) { - SoftPreconditions.checkState(tunerType != null, TAG, - "tunerType is null when send notification"); - if (tunerType == null) { - return; - } - Resources resources = context.getResources(); - String contentTitle = resources.getString(R.string.ut_setup_notification_content_title); - int contentTextId = 0; - switch (tunerType) { - case TunerHal.TUNER_TYPE_BUILT_IN: - contentTextId = R.string.bt_setup_notification_content_text; - break; - case TunerHal.TUNER_TYPE_USB: - contentTextId = R.string.ut_setup_notification_content_text; - break; - case TunerHal.TUNER_TYPE_NETWORK: - contentTextId = R.string.nt_setup_notification_content_text; - break; - } - String contentText = resources.getString(contentTextId); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - sendNotificationInternal(context, contentTitle, contentText); - } else { - Bitmap largeIcon = BitmapFactory.decodeResource(resources, - R.drawable.recommendation_antenna); - sendRecommendationCard(context, contentTitle, contentText, largeIcon); - } - } - - /** - * Sends the recommendation card to start the tuner TV input setup activity. - * - * @param context a {@link Context} instance - */ - private static void sendRecommendationCard(Context context, String contentTitle, - String contentText, Bitmap largeIcon) { - // Build and send the notification. - Notification notification = new NotificationCompat.BigPictureStyle( - new NotificationCompat.Builder(context) - .setAutoCancel(false) - .setContentTitle(contentTitle) - .setContentText(contentText) - .setContentInfo(contentText) - .setCategory(Notification.CATEGORY_RECOMMENDATION) - .setLargeIcon(largeIcon) - .setSmallIcon(context.getResources().getIdentifier( - TAG_ICON, TAG_DRAWABLE, context.getPackageName())) - .setContentIntent(createPendingIntentForSetupActivity(context))) - .build(); - NotificationManager notificationManager = (NotificationManager) context - .getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(NOTIFY_TAG, NOTIFY_ID, notification); - } - - private static void sendNotificationInternal(Context context, String contentTitle, - String contentText) { - NotificationManager notificationManager = (NotificationManager) context.getSystemService( - Context.NOTIFICATION_SERVICE); - notificationManager.createNotificationChannel(new NotificationChannel( - TUNER_SET_UP_NOTIFICATION_CHANNEL_ID, - context.getResources().getString(R.string.ut_setup_notification_channel_name), - NotificationManager.IMPORTANCE_HIGH)); - Notification notification = new Notification.Builder( - context, TUNER_SET_UP_NOTIFICATION_CHANNEL_ID) - .setContentTitle(contentTitle) - .setContentText(contentText) - .setSmallIcon(context.getResources().getIdentifier( - TAG_ICON, TAG_DRAWABLE, context.getPackageName())) - .setContentIntent(createPendingIntentForSetupActivity(context)) - .setVisibility(Notification.VISIBILITY_PUBLIC) - .extend(new Notification.TvExtender()) - .build(); - 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 notification. - * - * @param context a {@link Context} instance - */ - public static void cancelNotification(Context context) { - NotificationManager notificationManager = (NotificationManager) context - .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. - */ - @WorkerThread - TunerHal getOrCreate() { - 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. - */ - @MainThread - void generate() { - if (mGenerateTunerHalTask == null && mTunerHal == null) { - mGenerateTunerHalTask = new GenerateTunerHalTask(); - mGenerateTunerHalTask.executeOnExecutor(mExecutor); - } - } - - /** - * Clears the currently used tuner hal. - */ - @MainThread - void clear() { - if (mGenerateTunerHalTask != null) { - mGenerateTunerHalTask.cancel(true); - mGenerateTunerHalTask = null; - } - if (mTunerHal != null) { - AutoCloseableUtils.closeQuietly(mTunerHal); - mTunerHal = null; - } - } - - @WorkerThread - 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 deleted file mode 100644 index feae1ec9..00000000 --- a/src/com/android/tv/tuner/setup/WelcomeFragment.java +++ /dev/null @@ -1,120 +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.setup; - -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v17.leanback.widget.GuidanceStylist.Guidance; -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 java.util.List; - -/** - * A fragment for initial screen. - */ -public class WelcomeFragment extends SetupMultiPaneFragment { - public static final String ACTION_CATEGORY = - "com.android.tv.tuner.setup.WelcomeFragment"; - - @Override - protected SetupGuidedStepFragment onCreateContentFragment() { - ContentFragment fragment = new ContentFragment(); - fragment.setArguments(getArguments()); - return fragment; - } - - @Override - protected String getActionCategory() { - return ACTION_CATEGORY; - } - - @Override - protected boolean needsDoneButton() { - return false; - } - - public static class ContentFragment extends SetupGuidedStepFragment { - private int mChannelCountOnPreference; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - mChannelCountOnPreference = - TunerPreferences.getScannedChannelCount(getActivity().getApplicationContext()); - super.onCreate(savedInstanceState); - } - - @NonNull - @Override - 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) { - 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); - 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); - } - - @Override - public void onCreateActions(@NonNull List<GuidedAction> actions, - Bundle savedInstanceState) { - String[] choices = getResources().getStringArray(mChannelCountOnPreference == 0 - ? R.array.ut_setup_new_choices : R.array.ut_setup_again_choices); - for (int i = 0; i < choices.length - 1; ++i) { - actions.add(new GuidedAction.Builder(getActivity()).id(i).title(choices[i]) - .build()); - } - actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_DONE) - .title(choices[choices.length - 1]).build()); - } - - @Override - protected String getActionCategory() { - return ACTION_CATEGORY; - } - } -} diff --git a/src/com/android/tv/tuner/source/FileTsStreamer.java b/src/com/android/tv/tuner/source/FileTsStreamer.java deleted file mode 100644 index f17dd46b..00000000 --- a/src/com/android/tv/tuner/source/FileTsStreamer.java +++ /dev/null @@ -1,484 +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.source; - -import android.content.Context; -import android.os.Environment; -import android.util.Log; -import android.util.SparseBooleanArray; - -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.upstream.DataSpec; -import com.android.tv.Features; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.tuner.ChannelScanFileParser.ScanChannel; -import com.android.tv.tuner.data.TunerChannel; -import com.android.tv.tuner.ts.TsParser; -import com.android.tv.tuner.tvinput.EventDetector; -import com.android.tv.tuner.tvinput.FileSourceEventDetector; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.List; -import java.util.concurrent.atomic.AtomicLong; - -/** - * Provides MPEG-2 TS stream sources for both channel scanning and channel playing from a local file - * generated by capturing TV signal. - */ -public class FileTsStreamer implements TsStreamer { - private static final String TAG = "FileTsStreamer"; - - private static final int TS_PACKET_SIZE = 188; - private static final int TS_SYNC_BYTE = 0x47; - private static final int MIN_READ_UNIT = TS_PACKET_SIZE * 10; - private static final int READ_BUFFER_SIZE = MIN_READ_UNIT * 10; // ~20KB - private static final int CIRCULAR_BUFFER_SIZE = MIN_READ_UNIT * 4000; // ~ 8MB - private static final int PADDING_SIZE = MIN_READ_UNIT * 1000; // ~2MB - private static final int READ_TIMEOUT_MS = 10000; // 10 secs. - private static final int BUFFER_UNDERRUN_SLEEP_MS = 10; - private static final String FILE_DIR = - new File(Environment.getExternalStorageDirectory(), "Streams").getAbsolutePath(); - - // Virtual frequency base used for file-based source - public static final int FREQ_BASE = 100; - - private final Object mCircularBufferMonitor = new Object(); - private final byte[] mCircularBuffer = new byte[CIRCULAR_BUFFER_SIZE]; - private final FileSourceEventDetector mEventDetector; - private final Context mContext; - - private long mBytesFetched; - private long mLastReadPosition; - private boolean mStreaming; - - private Thread mStreamingThread; - private StreamProvider mSource; - - public static class FileDataSource extends TsDataSource { - private final FileTsStreamer mTsStreamer; - private final AtomicLong mLastReadPosition = new AtomicLong(0); - private long mStartBufferedPosition; - - private FileDataSource(FileTsStreamer tsStreamer) { - mTsStreamer = tsStreamer; - mStartBufferedPosition = tsStreamer.getBufferedPosition(); - } - - @Override - public long getBufferedPosition() { - return mTsStreamer.getBufferedPosition() - mStartBufferedPosition; - } - - @Override - public long getLastReadPosition() { - return mLastReadPosition.get(); - } - - @Override - public void shiftStartPosition(long offset) { - SoftPreconditions.checkState(mLastReadPosition.get() == 0); - SoftPreconditions.checkArgument(0 <= offset && offset <= getBufferedPosition()); - mStartBufferedPosition += offset; - } - - @Override - public long open(DataSpec dataSpec) throws IOException { - mLastReadPosition.set(0); - return C.LENGTH_UNBOUNDED; - } - - @Override - public void close() { - } - - @Override - public int read(byte[] buffer, int offset, int readLength) throws IOException { - int ret = mTsStreamer.readAt(mStartBufferedPosition + mLastReadPosition.get(), buffer, - offset, readLength); - if (ret > 0) { - mLastReadPosition.addAndGet(ret); - } - return ret; - } - } - - /** - * Creates {@link TsStreamer} for scanning & playing MPEG-2 TS file. - * @param eventListener the listener for channel & program information - */ - public FileTsStreamer(EventDetector.EventListener eventListener, Context context) { - mEventDetector = - new FileSourceEventDetector( - eventListener, Features.ENABLE_FILE_DVB.isEnabled(context)); - mContext = context; - } - - @Override - public boolean startStream(ScanChannel channel) { - String filepath = new File(FILE_DIR, channel.filename).getAbsolutePath(); - mSource = new StreamProvider(filepath); - if (!mSource.isReady()) { - return false; - } - mEventDetector.start(mSource, FileSourceEventDetector.ALL_PROGRAM_NUMBERS); - mSource.addPidFilter(TsParser.PAT_PID); - mSource.addPidFilter(TsParser.ATSC_SI_BASE_PID); - if (Features.ENABLE_FILE_DVB.isEnabled(mContext)) { - mSource.addPidFilter(TsParser.DVB_EIT_PID); - mSource.addPidFilter(TsParser.DVB_SDT_PID); - } - synchronized (mCircularBufferMonitor) { - if (mStreaming) { - return true; - } - mStreaming = true; - } - - mStreamingThread = new StreamingThread(); - mStreamingThread.start(); - Log.i(TAG, "Streaming started"); - return true; - } - - @Override - public boolean startStream(TunerChannel channel) { - Log.i(TAG, "tuneToChannel with: " + channel.getFilepath()); - mSource = new StreamProvider(channel.getFilepath()); - if (!mSource.isReady()) { - return false; - } - mEventDetector.start(mSource, channel.getProgramNumber()); - mSource.addPidFilter(channel.getVideoPid()); - for (Integer i : channel.getAudioPids()) { - mSource.addPidFilter(i); - } - mSource.addPidFilter(channel.getPcrPid()); - mSource.addPidFilter(TsParser.PAT_PID); - mSource.addPidFilter(TsParser.ATSC_SI_BASE_PID); - if (Features.ENABLE_FILE_DVB.isEnabled(mContext)) { - mSource.addPidFilter(TsParser.DVB_EIT_PID); - mSource.addPidFilter(TsParser.DVB_SDT_PID); - } - synchronized (mCircularBufferMonitor) { - if (mStreaming) { - return true; - } - mStreaming = true; - } - - mStreamingThread = new StreamingThread(); - mStreamingThread.start(); - Log.i(TAG, "Streaming started"); - return true; - } - - /** - * Blocks the current thread until the streaming thread stops. In rare cases when the tuner - * device is overloaded this can take a while, but usually it returns pretty quickly. - */ - @Override - public void stopStream() { - synchronized (mCircularBufferMonitor) { - mStreaming = false; - mCircularBufferMonitor.notify(); - } - - try { - if (mStreamingThread != null) { - mStreamingThread.join(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - @Override - public TsDataSource createDataSource() { - return new FileDataSource(this); - } - - /** - * Returns the current buffered position from the file. - * @return the current buffered position - */ - public long getBufferedPosition() { - synchronized (mCircularBufferMonitor) { - return mBytesFetched; - } - } - - /** - * Provides MPEG-2 transport stream from a local file. Stream can be filtered by PID. - */ - public static class StreamProvider { - private final String mFilepath; - private final SparseBooleanArray mPids = new SparseBooleanArray(); - private final byte[] mPreBuffer = new byte[READ_BUFFER_SIZE]; - - private BufferedInputStream mInputStream; - - private StreamProvider(String filepath) { - mFilepath = filepath; - open(filepath); - } - - private void open(String filepath) { - try { - mInputStream = new BufferedInputStream(new FileInputStream(filepath)); - } catch (IOException e) { - Log.e(TAG, "Error opening input stream", e); - mInputStream = null; - } - } - - private boolean isReady() { - return mInputStream != null; - } - - /** - * Returns the file path of the MPEG-2 TS file. - */ - public String getFilepath() { - return mFilepath; - } - - /** - * Adds a pid for filtering from the MPEG-2 TS file. - */ - public void addPidFilter(int pid) { - mPids.put(pid, true); - } - - /** - * Returns whether the current pid filter is empty or not. - */ - public boolean isFilterEmpty() { - return mPids.size() == 0; - } - - /** - * Clears the current pid filter. - */ - public void clearPidFilter() { - mPids.clear(); - } - - /** - * Returns whether a pid is in the pid filter or not. - * @param pid the pid to check - */ - public boolean isInFilter(int pid) { - return mPids.get(pid); - } - - /** - * Reads from the MPEG-2 TS file to buffer. - * - * @param inputBuffer to read - * @return the number of read bytes - */ - private int read(byte[] inputBuffer) { - int readSize = readInternal(); - if (readSize <= 0) { - // Reached the end of stream. Restart from the beginning. - close(); - open(mFilepath); - if (mInputStream == null) { - return -1; - } - readSize = readInternal(); - } - - if (mPreBuffer[0] != TS_SYNC_BYTE) { - Log.e(TAG, "Error reading input stream - no TS sync found"); - return -1; - } - int filteredSize = 0; - for (int i = 0, destPos = 0; i < readSize; i += TS_PACKET_SIZE) { - if (mPreBuffer[i] == TS_SYNC_BYTE) { - int pid = ((mPreBuffer[i + 1] & 0x1f) << 8) + (mPreBuffer[i + 2] & 0xff); - if (mPids.get(pid)) { - System.arraycopy(mPreBuffer, i, inputBuffer, destPos, TS_PACKET_SIZE); - destPos += TS_PACKET_SIZE; - filteredSize += TS_PACKET_SIZE; - } - } - } - return filteredSize; - } - - private int readInternal() { - int readSize; - try { - readSize = mInputStream.read(mPreBuffer, 0, mPreBuffer.length); - } catch (IOException e) { - Log.e(TAG, "Error reading input stream", e); - return -1; - } - return readSize; - } - - private void close() { - try { - mInputStream.close(); - } catch (IOException e) { - Log.e(TAG, "Error closing input stream:", e); - } - mInputStream = null; - } - } - - /** - * Reads data from internal buffer. - * @param pos the position to read from - * @param buffer to read - * @param offset start position of the read buffer - * @param amount number of bytes to read - * @return number of read bytes when successful, {@code -1} otherwise - * @throws IOException - */ - public int readAt(long pos, byte[] buffer, int offset, int amount) throws IOException { - synchronized (mCircularBufferMonitor) { - long initialBytesFetched = mBytesFetched; - while (mBytesFetched < pos + amount && mStreaming) { - try { - mCircularBufferMonitor.wait(READ_TIMEOUT_MS); - } catch (InterruptedException e) { - // Wait again. - Thread.currentThread().interrupt(); - } - if (initialBytesFetched == mBytesFetched) { - Log.w(TAG, "No data update for " + READ_TIMEOUT_MS + "ms. returning -1."); - - // Returning -1 will make demux report EOS so that the input service can retry - // the playback. - return -1; - } - } - if (!mStreaming) { - Log.w(TAG, "Stream is already stopped."); - return -1; - } - if (mBytesFetched - CIRCULAR_BUFFER_SIZE > pos) { - Log.e(TAG, "Demux is requesting the data which is already overwritten."); - return -1; - } - int posInBuffer = (int) (pos % CIRCULAR_BUFFER_SIZE); - int bytesToCopyInFirstPass = amount; - if (posInBuffer + bytesToCopyInFirstPass > mCircularBuffer.length) { - bytesToCopyInFirstPass = mCircularBuffer.length - posInBuffer; - } - System.arraycopy(mCircularBuffer, posInBuffer, buffer, offset, bytesToCopyInFirstPass); - if (bytesToCopyInFirstPass < amount) { - System.arraycopy(mCircularBuffer, 0, buffer, offset + bytesToCopyInFirstPass, - amount - bytesToCopyInFirstPass); - } - mLastReadPosition = pos + amount; - mCircularBufferMonitor.notify(); - return amount; - } - } - - /** - * Adds {@link ScanChannel} instance for local files. - * - * @param output a list of channels where the results will be placed in - */ - public static void addLocalStreamFiles(List<ScanChannel> output) { - File dir = new File(FILE_DIR); - if (!dir.exists()) return; - - File[] tsFiles = dir.listFiles(); - if (tsFiles == null) return; - int freq = FileTsStreamer.FREQ_BASE; - for (File file : tsFiles) { - if (!file.isFile()) continue; - output.add(ScanChannel.forFile(freq, file.getName())); - freq += 100; - } - } - - /** - * A thread managing a circular buffer that holds stream data to be consumed by player. - * Keeps reading data in from a {@link StreamProvider} to hold enough amount for buffering. - * Started and stopped by {@link #startStream()} and {@link #stopStream()}, respectively. - */ - private class StreamingThread extends Thread { - @Override - public void run() { - byte[] dataBuffer = new byte[READ_BUFFER_SIZE]; - - synchronized (mCircularBufferMonitor) { - mBytesFetched = 0; - mLastReadPosition = 0; - } - - while (true) { - synchronized (mCircularBufferMonitor) { - while ((mBytesFetched - mLastReadPosition + PADDING_SIZE) > CIRCULAR_BUFFER_SIZE - && mStreaming) { - try { - mCircularBufferMonitor.wait(); - } catch (InterruptedException e) { - // Wait again. - Thread.currentThread().interrupt(); - } - } - if (!mStreaming) { - break; - } - } - - int bytesWritten = mSource.read(dataBuffer); - if (bytesWritten <= 0) { - try { - // When buffer is underrun, we sleep for short time to prevent - // unnecessary CPU draining. - sleep(BUFFER_UNDERRUN_SLEEP_MS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - continue; - } - - mEventDetector.feedTSStream(dataBuffer, 0, bytesWritten); - - synchronized (mCircularBufferMonitor) { - int posInBuffer = (int) (mBytesFetched % CIRCULAR_BUFFER_SIZE); - int bytesToCopyInFirstPass = bytesWritten; - if (posInBuffer + bytesToCopyInFirstPass > mCircularBuffer.length) { - bytesToCopyInFirstPass = mCircularBuffer.length - posInBuffer; - } - System.arraycopy(dataBuffer, 0, mCircularBuffer, posInBuffer, - bytesToCopyInFirstPass); - if (bytesToCopyInFirstPass < bytesWritten) { - System.arraycopy(dataBuffer, bytesToCopyInFirstPass, mCircularBuffer, 0, - bytesWritten - bytesToCopyInFirstPass); - } - mBytesFetched += bytesWritten; - mCircularBufferMonitor.notify(); - } - } - - Log.i(TAG, "Streaming stopped"); - mSource.close(); - } - } -} diff --git a/src/com/android/tv/tuner/source/TsDataSource.java b/src/com/android/tv/tuner/source/TsDataSource.java deleted file mode 100644 index 2ce3e670..00000000 --- a/src/com/android/tv/tuner/source/TsDataSource.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.source; - -import com.google.android.exoplayer.upstream.DataSource; - -/** - * {@link DataSource} for MPEG-TS stream, which will be used by {@link TsExtractor}. - */ -public abstract class TsDataSource implements DataSource { - - /** - * Returns the number of bytes being buffered by {@link TsStreamer} so far. - * - * @return the buffered position - */ - public long getBufferedPosition() { - return 0; - } - - /** - * Returns the offset position where the last {@link DataSource#read} read. - * - * @return the last read position - */ - public long getLastReadPosition() { - return 0; - } - - /** - * Shifts start position by the specified offset. - * Do not call this method when the class already provided MPEG-TS stream to the extractor. - * @param offset 0 <= offset <= buffered position - */ - public void shiftStartPosition(long offset) { } -}
\ No newline at end of file diff --git a/src/com/android/tv/tuner/source/TsDataSourceManager.java b/src/com/android/tv/tuner/source/TsDataSourceManager.java deleted file mode 100644 index 16be7582..00000000 --- a/src/com/android/tv/tuner/source/TsDataSourceManager.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.source; - -import android.content.Context; -import android.support.annotation.VisibleForTesting; - -import com.android.tv.tuner.TunerHal; -import com.android.tv.tuner.data.TunerChannel; -import com.android.tv.tuner.data.nano.Channel; -import com.android.tv.tuner.tvinput.EventDetector; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Manages {@link DataSource} for playback and recording. - * The class hides handling of {@link TunerHal} and {@link TsStreamer} from other classes. - * One TsDataSourceManager should be created for per session. - */ -public class TsDataSourceManager { - private static final Object sLock = new Object(); - private static final Map<TsDataSource, TsStreamer> sTsStreamers = - new ConcurrentHashMap<>(); - - private static int sSequenceId; - - private final int mId; - private final boolean mIsRecording; - private final TunerTsStreamerManager mTunerStreamerManager = - TunerTsStreamerManager.getInstance(); - - private boolean mKeepTuneStatus; - - /** - * Creates TsDataSourceManager to create and release {@link DataSource} which will be - * used for playing and recording. - * @param isRecording {@code true} when for recording, {@code false} otherwise - * @return {@link TsDataSourceManager} - */ - public static TsDataSourceManager createSourceManager(boolean isRecording) { - int id; - synchronized (sLock) { - id = ++sSequenceId; - } - return new TsDataSourceManager(id, isRecording); - } - - private TsDataSourceManager(int id, boolean isRecording) { - mId = id; - mIsRecording = isRecording; - mKeepTuneStatus = true; - } - - /** - * Creates or retrieves {@link TsDataSource} for playing or recording - * @param context a {@link Context} instance - * @param channel to play or record - * @param eventListener for program information which will be scanned from MPEG2-TS stream - * @return {@link TsDataSource} which will provide the specified channel stream - */ - public TsDataSource createDataSource(Context context, TunerChannel channel, - EventDetector.EventListener eventListener) { - if (channel.getType() == Channel.TYPE_FILE) { - // MPEG2 TS captured stream file recording is not supported. - if (mIsRecording) { - return null; - } - FileTsStreamer streamer = new FileTsStreamer(eventListener, context); - if (streamer.startStream(channel)) { - TsDataSource source = streamer.createDataSource(); - sTsStreamers.put(source, streamer); - return source; - } - return null; - } - return mTunerStreamerManager.createDataSource(context, channel, eventListener, - mId, !mIsRecording && mKeepTuneStatus); - } - - /** - * Releases the specified {@link TsDataSource} and underlying {@link TunerHal}. - * @param source to release - */ - public void releaseDataSource(TsDataSource source) { - if (source instanceof TunerTsStreamer.TunerDataSource) { - mTunerStreamerManager.releaseDataSource( - source, mId, !mIsRecording && mKeepTuneStatus); - } else if (source instanceof FileTsStreamer.FileDataSource) { - FileTsStreamer streamer = (FileTsStreamer) sTsStreamers.get(source); - if (streamer != null) { - sTsStreamers.remove(source); - streamer.stopStream(); - } - } - } - - /** - * Indicates that the current session has pending tunes. - */ - public void setHasPendingTune() { - mTunerStreamerManager.setHasPendingTune(mId); - } - - /** - * Indicates whether the underlying {@link TunerHal} should be kept or not when data source - * is being released. - * TODO: If b/30750953 is fixed, we can remove this function. - * @param keepTuneStatus underlying {@link TunerHal} will be reused when data source releasing. - */ - public void setKeepTuneStatus(boolean keepTuneStatus) { - mKeepTuneStatus = keepTuneStatus; - } - - /** - * Add tuner hal into TunerTsStreamerManager for test. - */ - @VisibleForTesting - public void addTunerHalForTest(TunerHal tunerHal) { - mTunerStreamerManager.addTunerHal(tunerHal, mId); - } - - /** - * Releases persistent resources. - */ - public void release() { - mTunerStreamerManager.release(mId); - } -} diff --git a/src/com/android/tv/tuner/source/TsStreamWriter.java b/src/com/android/tv/tuner/source/TsStreamWriter.java deleted file mode 100644 index 30650555..00000000 --- a/src/com/android/tv/tuner/source/TsStreamWriter.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.source; - -import android.content.Context; -import android.util.Log; -import com.android.tv.tuner.data.TunerChannel; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.HashSet; -import java.util.Set; - -/** - * Stores TS files to the disk for debugging. - */ -public class TsStreamWriter { - private static final String TAG = "TsStreamWriter"; - private static final boolean DEBUG = false; - - private static final long TIME_LIMIT_MS = 10000; // 10s - private static final int NO_INSTANCE_ID = 0; - private static final int MAX_GET_ID_RETRY_COUNT = 5; - private static final int MAX_INSTANCE_ID = 10000; - private static final String SEPARATOR = "_"; - - private FileOutputStream mFileOutputStream; - private long mFileStartTimeMs; - private String mFileName = null; - private final String mDirectoryPath; - private final File mDirectory; - private final int mInstanceId; - private TunerChannel mChannel; - - public TsStreamWriter(Context context) { - File externalFilesDir = context.getExternalFilesDir(null); - if (externalFilesDir == null || !externalFilesDir.isDirectory()) { - mDirectoryPath = null; - mDirectory = null; - mInstanceId = NO_INSTANCE_ID; - if (DEBUG) { - Log.w(TAG, "Fail to get external files dir!"); - } - } else { - mDirectoryPath = externalFilesDir.getPath() + "/EngTsStream"; - mDirectory = new File(mDirectoryPath); - if (!mDirectory.exists()) { - boolean madeDir = mDirectory.mkdir(); - if (!madeDir) { - Log.w(TAG, "Error. Fail to create folder!"); - } - } - mInstanceId = generateInstanceId(); - } - } - - /** - * Sets the current channel. - * - * @param channel curren channel of the stream - */ - public void setChannel(TunerChannel channel) { - mChannel = channel; - } - - /** - * Opens a file to store TS data. - */ - public void openFile() { - if (mChannel == null || mDirectoryPath == null) { - return; - } - mFileStartTimeMs = System.currentTimeMillis(); - mFileName = mChannel.getDisplayNumber() + SEPARATOR + mFileStartTimeMs + SEPARATOR - + mInstanceId + ".ts"; - String filePath = mDirectoryPath + "/" + mFileName; - try { - mFileOutputStream = new FileOutputStream(filePath, false); - } catch (FileNotFoundException e) { - Log.w(TAG, "Cannot open file: " + filePath, e); - } - } - - /** - * Closes the file and stops storing TS data. - * - * @param calledWhenStopStream {@code true} if this method is called when the stream is stopped - * {@code false} otherwise - */ - public void closeFile(boolean calledWhenStopStream) { - if (mFileOutputStream == null) { - return; - } - try { - mFileOutputStream.close(); - deleteOutdatedFiles(calledWhenStopStream); - mFileName = null; - mFileOutputStream = null; - } catch (IOException e) { - Log.w(TAG, "Error on closing file.", e); - } - } - - /** - * Writes the data to the file. - * - * @param buffer the data to be written - * @param bytesWritten number of bytes written - */ - public void writeToFile(byte[] buffer, int bytesWritten) { - if (mFileOutputStream == null) { - return; - } - if (System.currentTimeMillis() - mFileStartTimeMs > TIME_LIMIT_MS) { - closeFile(false); - openFile(); - } - try { - mFileOutputStream.write(buffer, 0, bytesWritten); - } catch (IOException e) { - Log.w(TAG, "Error on writing TS stream.", e); - } - } - - /** - * Deletes outdated files to save storage. - * - * @param deleteAll {@code true} if all the files with the relative ID should be deleted - * {@code false} if the most recent file should not be deleted - */ - private void deleteOutdatedFiles(boolean deleteAll) { - if (mFileName == null) { - return; - } - if (mDirectory == null || !mDirectory.isDirectory()) { - Log.e(TAG, "Error. The folder doesn't exist!"); - return; - } - if (mFileName == null) { - Log.e(TAG, "Error. The current file name is null!"); - return; - } - for (File file : mDirectory.listFiles()) { - if (file.isFile() && getFileId(file) == mInstanceId - && (deleteAll || !mFileName.equals(file.getName()))) { - boolean deleted = file.delete(); - if (DEBUG && !deleted) { - Log.w(TAG, "Failed to delete " + file.getName()); - } - } - } - } - - /** - * Generates a unique instance ID. - * - * @return a unique instance ID - */ - private int generateInstanceId() { - if (mDirectory == null) { - return NO_INSTANCE_ID; - } - Set<Integer> idSet = getExistingIds(); - if (idSet == null) { - return NO_INSTANCE_ID; - } - for (int i = 0; i < MAX_GET_ID_RETRY_COUNT; i++) { - // Range [1, MAX_INSTANCE_ID] - int id = (int)Math.floor(Math.random() * MAX_INSTANCE_ID) + 1; - if (!idSet.contains(id)) { - return id; - } - } - return NO_INSTANCE_ID; - } - - /** - * Gets all existing instance IDs. - * - * @return a set of all existing instance IDs - */ - private Set<Integer> getExistingIds() { - if (mDirectory == null || !mDirectory.isDirectory()) { - return null; - } - - Set<Integer> idSet = new HashSet<>(); - for (File file : mDirectory.listFiles()) { - int id = getFileId(file); - if(id != NO_INSTANCE_ID) { - idSet.add(id); - } - } - return idSet; - } - - /** - * Gets the instance ID of a given file. - * - * @param file the file whose TsStreamWriter ID is returned - * @return the TsStreamWriter ID of the file or NO_INSTANCE_ID if not available - */ - private static int getFileId(File file) { - if (file == null || !file.isFile()) { - return NO_INSTANCE_ID; - } - String fileName = file.getName(); - int lastSeparator = fileName.lastIndexOf(SEPARATOR); - if (!fileName.endsWith(".ts") || lastSeparator == -1) { - return NO_INSTANCE_ID; - } - try { - return Integer.parseInt(fileName.substring(lastSeparator + 1, fileName.length() - 3)); - } catch (NumberFormatException e) { - if (DEBUG) { - Log.e(TAG, fileName + " is not a valid file name."); - } - } - return NO_INSTANCE_ID; - } -} diff --git a/src/com/android/tv/tuner/source/TsStreamer.java b/src/com/android/tv/tuner/source/TsStreamer.java deleted file mode 100644 index 1ac950bb..00000000 --- a/src/com/android/tv/tuner/source/TsStreamer.java +++ /dev/null @@ -1,56 +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.source; - -import com.android.tv.tuner.ChannelScanFileParser; -import com.android.tv.tuner.data.TunerChannel; - -/** - * Interface definition for a stream generator. The interface will provide streams - * for scanning channels and/or playback. - */ -public interface TsStreamer { - /** - * Starts streaming the data for channel scanning process. - * - * @param channel {@link ChannelScanFileParser.ScanChannel} to be scanned - * @return {@code true} if ready to stream, otherwise {@code false} - */ - boolean startStream(ChannelScanFileParser.ScanChannel channel); - - /** - * Starts streaming the data for channel playing or recording. - * - * @param channel {@link TunerChannel} to tune - * @return {@code true} if ready to stream, otherwise {@code false} - */ - boolean startStream(TunerChannel channel); - - /** - * Stops streaming the data. - */ - void stopStream(); - - /** - * Creates {@link TsDataSource} which will provide MPEG-2 TS stream for - * {@link android.media.MediaExtractor}. The source will start from the position - * where it is created. - * - * @return {@link TsDataSource} - */ - TsDataSource createDataSource(); -} diff --git a/src/com/android/tv/tuner/source/TunerTsStreamer.java b/src/com/android/tv/tuner/source/TunerTsStreamer.java deleted file mode 100644 index 843cbdb7..00000000 --- a/src/com/android/tv/tuner/source/TunerTsStreamer.java +++ /dev/null @@ -1,408 +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.source; - -import android.content.Context; -import android.util.Log; -import android.util.Pair; - -import com.google.android.exoplayer.C; -import com.google.android.exoplayer.upstream.DataSpec; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.tuner.ChannelScanFileParser; -import com.android.tv.tuner.TunerHal; -import com.android.tv.tuner.TunerPreferences; -import com.android.tv.tuner.data.TunerChannel; -import com.android.tv.tuner.tvinput.EventDetector; -import com.android.tv.tuner.tvinput.EventDetector.EventListener; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicLong; - -/** - * Provides MPEG-2 TS stream sources for channel playing from an underlying tuner device. - */ -public class TunerTsStreamer implements TsStreamer { - private static final String TAG = "TunerTsStreamer"; - - 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 mStreaming; - - private final TunerHal mTunerHal; - private TunerChannel mChannel; - private Thread mStreamingThread; - private final EventDetector mEventDetector; - private final List<Pair<EventListener, Boolean>> mEventListenerActions = new ArrayList<>(); - - private final TsStreamWriter mTsStreamWriter; - private String mChannelNumber; - - public static class TunerDataSource extends TsDataSource { - private final TunerTsStreamer mTsStreamer; - private final AtomicLong mLastReadPosition = new AtomicLong(0); - private long mStartBufferedPosition; - - private TunerDataSource(TunerTsStreamer tsStreamer) { - mTsStreamer = tsStreamer; - mStartBufferedPosition = tsStreamer.getBufferedPosition(); - } - - @Override - public long getBufferedPosition() { - return mTsStreamer.getBufferedPosition() - mStartBufferedPosition; - } - - @Override - public long getLastReadPosition() { - return mLastReadPosition.get(); - } - - @Override - public void shiftStartPosition(long offset) { - SoftPreconditions.checkState(mLastReadPosition.get() == 0); - SoftPreconditions.checkArgument(0 <= offset && offset <= getBufferedPosition()); - mStartBufferedPosition += offset; - } - - @Override - public long open(DataSpec dataSpec) throws IOException { - mLastReadPosition.set(0); - return C.LENGTH_UNBOUNDED; - } - - @Override - public void close() { - } - - @Override - public int read(byte[] buffer, int offset, int readLength) throws IOException { - int ret = mTsStreamer.readAt(mStartBufferedPosition + mLastReadPosition.get(), buffer, - 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; - } - } - /** - * Creates {@link TsStreamer} for playing or recording the specified channel. - * @param tunerHal the HAL for tuner device - * @param eventListener the listener for channel & program information - */ - public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener, Context context) { - mTunerHal = tunerHal; - mEventDetector = new EventDetector(mTunerHal); - if (eventListener != null) { - mEventDetector.registerListener(eventListener); - } - mTsStreamWriter = context != null && TunerPreferences.getStoreTsStream(context) ? - new TsStreamWriter(context) : null; - } - - public TunerTsStreamer(TunerHal tunerHal, EventListener eventListener) { - this(tunerHal, eventListener, null); - } - - @Override - public boolean startStream(TunerChannel channel) { - if (mTunerHal.tune(channel.getFrequency(), channel.getModulation(), - channel.getDisplayNumber(false))) { - if (channel.hasVideo()) { - mTunerHal.addPidFilter(channel.getVideoPid(), - TunerHal.FILTER_TYPE_VIDEO); - } - boolean audioFilterSet = false; - for (Integer audioPid : channel.getAudioPids()) { - if (!audioFilterSet) { - mTunerHal.addPidFilter(audioPid, TunerHal.FILTER_TYPE_AUDIO); - audioFilterSet = true; - } else { - // FILTER_TYPE_AUDIO overrides the previous filter for audio. We use - // FILTER_TYPE_OTHER from the secondary one to get the all audio tracks. - mTunerHal.addPidFilter(audioPid, TunerHal.FILTER_TYPE_OTHER); - } - } - mTunerHal.addPidFilter(channel.getPcrPid(), - TunerHal.FILTER_TYPE_PCR); - if (mEventDetector != null) { - mEventDetector.startDetecting(channel.getFrequency(), channel.getModulation(), - channel.getProgramNumber()); - } - mChannel = channel; - mChannelNumber = channel.getDisplayNumber(); - synchronized (mCircularBufferMonitor) { - if (mStreaming) { - Log.w(TAG, "Streaming should be stopped before start streaming"); - return true; - } - mStreaming = true; - mBytesFetched = 0; - mLastReadPosition.set(0L); - } - if (mTsStreamWriter != null) { - mTsStreamWriter.setChannel(mChannel); - mTsStreamWriter.openFile(); - } - mStreamingThread = new StreamingThread(); - mStreamingThread.start(); - Log.i(TAG, "Streaming started"); - return true; - } - return false; - } - - @Override - public boolean startStream(ChannelScanFileParser.ScanChannel channel) { - if (mTunerHal.tune(channel.frequency, channel.modulation, null)) { - mEventDetector.startDetecting( - channel.frequency, channel.modulation, EventDetector.ALL_PROGRAM_NUMBERS); - synchronized (mCircularBufferMonitor) { - if (mStreaming) { - Log.w(TAG, "Streaming should be stopped before start streaming"); - return true; - } - mStreaming = true; - mBytesFetched = 0; - mLastReadPosition.set(0L); - } - mStreamingThread = new StreamingThread(); - mStreamingThread.start(); - Log.i(TAG, "Streaming started"); - return true; - } - return false; - } - - /** - * Blocks the current thread until the streaming thread stops. In rare cases when the tuner - * device is overloaded this can take a while, but usually it returns pretty quickly. - */ - @Override - public void stopStream() { - mChannel = null; - synchronized (mCircularBufferMonitor) { - mStreaming = false; - mCircularBufferMonitor.notifyAll(); - } - - try { - if (mStreamingThread != null) { - mStreamingThread.join(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - if (mTsStreamWriter != null) { - mTsStreamWriter.closeFile(true); - mTsStreamWriter.setChannel(null); - } - } - - @Override - public TsDataSource createDataSource() { - return new TunerDataSource(this); - } - - /** - * Returns incomplete channel lists which was scanned so far. Incomplete channel means - * the channel whose channel information is not complete or is not well-formed. - * @return {@link List} of {@link TunerChannel} - */ - public List<TunerChannel> getMalFormedChannels() { - return mEventDetector.getMalFormedChannels(); - } - - /** - * Returns the current {@link TunerHal} which provides MPEG-TS stream for TunerTsStreamer. - * @return {@link TunerHal} - */ - public TunerHal getTunerHal() { - return mTunerHal; - } - - /** - * Returns the current tuned channel for TunerTsStreamer. - * @return {@link TunerChannel} - */ - public TunerChannel getChannel() { - return mChannel; - } - - /** - * Returns the current buffered position from tuner. - * @return the current buffered position - */ - public long getBufferedPosition() { - synchronized (mCircularBufferMonitor) { - return mBytesFetched; - } - } - - public String getStreamerInfo() { - return "Channel: " + mChannelNumber + ", Streaming: " + mStreaming; - } - - public void registerListener(EventListener listener) { - if (mEventDetector != null && listener != null) { - synchronized (mEventListenerActions) { - mEventListenerActions.add(new Pair<>(listener, true)); - } - } - } - - public void unregisterListener(EventListener listener) { - if (mEventDetector != null) { - synchronized (mEventListenerActions) { - mEventListenerActions.add(new Pair(listener, false)); - } - } - } - - private class StreamingThread extends Thread { - @Override - public void run() { - // Buffers for streaming data from the tuner and the internal buffer. - byte[] dataBuffer = new byte[READ_BUFFER_SIZE]; - - while (true) { - synchronized (mCircularBufferMonitor) { - if (!mStreaming) { - break; - } - } - - if (mEventDetector != null) { - synchronized (mEventListenerActions) { - for (Pair listenerAction : mEventListenerActions) { - EventListener listener = (EventListener) listenerAction.first; - if ((boolean) listenerAction.second) { - mEventDetector.registerListener(listener); - } else { - mEventDetector.unregisterListener(listener); - } - } - mEventListenerActions.clear(); - } - } - - int bytesWritten = mTunerHal.readTsStream(dataBuffer, dataBuffer.length); - if (bytesWritten <= 0) { - try { - // When buffer is underrun, we sleep for short time to prevent - // unnecessary CPU draining. - sleep(BUFFER_UNDERRUN_SLEEP_MS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - continue; - } - - if (mTsStreamWriter != null) { - mTsStreamWriter.writeToFile(dataBuffer, bytesWritten); - } - - if (mEventDetector != null) { - mEventDetector.feedTSStream(dataBuffer, 0, bytesWritten); - } - synchronized (mCircularBufferMonitor) { - int posInBuffer = (int) (mBytesFetched % CIRCULAR_BUFFER_SIZE); - int bytesToCopyInFirstPass = bytesWritten; - if (posInBuffer + bytesToCopyInFirstPass > mCircularBuffer.length) { - bytesToCopyInFirstPass = mCircularBuffer.length - posInBuffer; - } - System.arraycopy(dataBuffer, 0, mCircularBuffer, posInBuffer, - bytesToCopyInFirstPass); - if (bytesToCopyInFirstPass < bytesWritten) { - System.arraycopy(dataBuffer, bytesToCopyInFirstPass, mCircularBuffer, 0, - bytesWritten - bytesToCopyInFirstPass); - } - mBytesFetched += bytesWritten; - mCircularBufferMonitor.notifyAll(); - } - } - - Log.i(TAG, "Streaming stopped"); - } - } - - /** - * Reads data from internal buffer. - * @param pos the position to read from - * @param buffer to read - * @param offset start position of the read buffer - * @param amount number of bytes to read - * @return number of read bytes when successful, {@code -1} otherwise - * @throws IOException - */ - public int readAt(long pos, byte[] buffer, int offset, int amount) throws IOException { - while (true) { - synchronized (mCircularBufferMonitor) { - if (!mStreaming) { - return READ_ERROR_STREAMING_ENDED; - } - if (mBytesFetched - CIRCULAR_BUFFER_SIZE > pos) { - Log.w(TAG, "Demux is requesting the data which is already overwritten."); - return READ_ERROR_BUFFER_OVERWRITTEN; - } - if (mBytesFetched < pos + amount) { - try { - mCircularBufferMonitor.wait(READ_TIMEOUT_MS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - // Try again to prevent starvation. - // Give chances to read from other threads. - continue; - } - int startPos = (int) (pos % CIRCULAR_BUFFER_SIZE); - int endPos = (int) ((pos + amount) % CIRCULAR_BUFFER_SIZE); - int firstLength = (startPos > endPos ? CIRCULAR_BUFFER_SIZE : endPos) - startPos; - System.arraycopy(mCircularBuffer, startPos, buffer, offset, firstLength); - if (firstLength < amount) { - System.arraycopy(mCircularBuffer, 0, buffer, offset + firstLength, - amount - firstLength); - } - mCircularBufferMonitor.notifyAll(); - return amount; - } - } - } -} diff --git a/src/com/android/tv/tuner/source/TunerTsStreamerManager.java b/src/com/android/tv/tuner/source/TunerTsStreamerManager.java deleted file mode 100644 index 258a4d86..00000000 --- a/src/com/android/tv/tuner/source/TunerTsStreamerManager.java +++ /dev/null @@ -1,304 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.source; - -import android.content.Context; - -import com.android.tv.common.AutoCloseableUtils; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.tuner.TunerHal; -import com.android.tv.tuner.data.TunerChannel; -import com.android.tv.tuner.tvinput.EventDetector; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; - -/** - * Manages {@link TunerTsStreamer} for playback and recording. - * The class hides handling of {@link TunerHal} from other classes. - * This class is used by {@link TsDataSourceManager}. Don't use this class directly. - */ -class TunerTsStreamerManager { - // The lock will protect mStreamerFinder, mSourceToStreamerMap and some part of TsStreamCreator - // to support timely {@link TunerTsStreamer} cancellation due to a new tune request from - // the same session. - 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; - - /** - * Returns the singleton instance for the class - * @return TunerTsStreamerManager - */ - static synchronized TunerTsStreamerManager getInstance() { - if (sInstance == null) { - sInstance = new TunerTsStreamerManager(); - } - return sInstance; - } - - private TunerTsStreamerManager() { } - - synchronized TsDataSource createDataSource( - Context context, TunerChannel channel, EventDetector.EventListener listener, - int sessionId, boolean reuse) { - TsStreamerCreator creator; - synchronized (mCancelLock) { - if (mStreamerFinder.containsLocked(channel)) { - 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; - } - creator = new TsStreamerCreator(context, channel, listener); - mCreators.put(sessionId, creator); - } - TunerTsStreamer streamer = creator.create(sessionId, reuse); - synchronized (mCancelLock) { - mCreators.remove(sessionId); - if (streamer == null) { - return null; - } - if (!creator.isCancelledLocked()) { - mStreamerFinder.putLocked(channel, sessionId, streamer); - TsDataSource source = streamer.createDataSource(); - mListeners.put(sessionId, listener); - mSourceToStreamerMap.put(source, streamer); - return source; - } - } - // Created streamer was cancelled by a new tune request. - streamer.stopStream(); - TunerHal hal = streamer.getTunerHal(); - hal.setHasPendingTune(false); - mTunerHalManager.releaseTunerHal(hal, sessionId, reuse); - return null; - } - - synchronized void releaseDataSource(TsDataSource source, int sessionId, - boolean reuse) { - TunerTsStreamer streamer; - synchronized (mCancelLock) { - streamer = mSourceToStreamerMap.get(source); - mSourceToStreamerMap.remove(source); - 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); - if (mStreamerFinder.containsLocked(channel)) { - return; - } - } - streamer.stopStream(); - TunerHal hal = streamer.getTunerHal(); - hal.setHasPendingTune(false); - mTunerHalManager.releaseTunerHal(hal, sessionId, reuse); - } - - void setHasPendingTune(int sessionId) { - synchronized (mCancelLock) { - if (mCreators.containsKey(sessionId)) { - mCreators.get(sessionId).cancelLocked(); - } - } - } - - /** - * 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); - } - - private class StreamerFinder { - private final Map<TunerChannel, Set<Integer>> mSessions = new HashMap<>(); - private final Map<TunerChannel, TunerTsStreamer> mStreamers = new HashMap<>(); - - // @GuardedBy("mCancelLock") - private void putLocked(TunerChannel channel, int sessionId, TunerTsStreamer streamer) { - Set<Integer> sessions = new HashSet<>(); - sessions.add(sessionId); - mSessions.put(channel, sessions); - mStreamers.put(channel, streamer); - } - - // @GuardedBy("mCancelLock") - private void appendSessionLocked(TunerChannel channel, int sessionId) { - if (mSessions.containsKey(channel)) { - mSessions.get(channel).add(sessionId); - } - } - - // @GuardedBy("mCancelLock") - private void removeSessionLocked(TunerChannel channel, int sessionId) { - Set<Integer> sessions = mSessions.get(channel); - sessions.remove(sessionId); - if (sessions.size() == 0) { - mSessions.remove(channel); - mStreamers.remove(channel); - } - } - - // @GuardedBy("mCancelLock") - private boolean containsLocked(TunerChannel channel) { - return mSessions.containsKey(channel); - } - - // @GuardedBy("mCancelLock") - private TunerTsStreamer getStreamerLocked(TunerChannel channel) { - return mStreamers.containsKey(channel) ? mStreamers.get(channel) : null; - } - } - - /** - * {@link TunerTsStreamer} creation can be cancelled by a new tune request for the same - * session. The class supports the cancellation in creating new {@link TunerTsStreamer}. - */ - private class TsStreamerCreator { - private final Context mContext; - private final TunerChannel mChannel; - private final EventDetector.EventListener mEventListener; - // mCancelled will be {@code true} if a new tune request for the same session - // cancels create(). - private boolean mCancelled; - private TunerHal mTunerHal; - - private TsStreamerCreator(Context context, TunerChannel channel, - EventDetector.EventListener listener) { - mContext = context; - mChannel = channel; - mEventListener = listener; - } - - private TunerTsStreamer create(int sessionId, boolean reuse) { - TunerHal hal = mTunerHalManager.getOrCreateTunerHal(mContext, sessionId); - if (hal == null) { - return null; - } - boolean canceled = false; - synchronized (mCancelLock) { - if (!mCancelled) { - mTunerHal = hal; - } else { - canceled = true; - } - } - if (!canceled) { - TunerTsStreamer tsStreamer = new TunerTsStreamer(hal, mEventListener, mContext); - if (tsStreamer.startStream(mChannel)) { - return tsStreamer; - } - synchronized (mCancelLock) { - mTunerHal = null; - } - } - hal.setHasPendingTune(false); - // Since TunerTsStreamer is not properly created, closes TunerHal. - // And do not re-use TunerHal when it is not cancelled. - mTunerHalManager.releaseTunerHal(hal, sessionId, mCancelled && reuse); - return null; - } - - // @GuardedBy("mCancelLock") - private void cancelLocked() { - if (mCancelled) { - return; - } - mCancelled = true; - if (mTunerHal != null) { - mTunerHal.setHasPendingTune(true); - } - } - - // @GuardedBy("mCancelLock") - private boolean isCancelledLocked() { - return mCancelled; - } - } - - /** - * Supports sharing {@link TunerHal} among multiple sessions. - * The class also supports session affinity for {@link TunerHal} allocation. - */ - private class TunerHalManager { - private final Map<Integer, TunerHal> mTunerHals = new HashMap<>(); - - private TunerHal getOrCreateTunerHal(Context context, int sessionId) { - // Handles session affinity. - TunerHal hal = mTunerHals.get(sessionId); - if (hal != null) { - mTunerHals.remove(sessionId); - return hal; - } - // Finds a TunerHal which is cached for other sessions. - Iterator it = mTunerHals.keySet().iterator(); - if (it.hasNext()) { - Integer key = (Integer) it.next(); - hal = mTunerHals.get(key); - mTunerHals.remove(key); - return hal; - } - return TunerHal.createInstance(context); - } - - private void releaseTunerHal(TunerHal hal, int sessionId, boolean reuse) { - if (!reuse || !hal.isReusable()) { - AutoCloseableUtils.closeQuietly(hal); - return; - } - TunerHal cachedHal = mTunerHals.get(sessionId); - if (cachedHal != hal) { - mTunerHals.put(sessionId, hal); - if (cachedHal != null) { - AutoCloseableUtils.closeQuietly(cachedHal); - } - } - } - - private void releaseCachedHal(int sessionId) { - TunerHal hal = mTunerHals.get(sessionId); - if (hal != null) { - mTunerHals.remove(sessionId); - } - if (hal != null) { - 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 deleted file mode 100644 index e1f890f3..00000000 --- a/src/com/android/tv/tuner/ts/SectionParser.java +++ /dev/null @@ -1,1759 +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.ts; - -import android.media.tv.TvContentRating; -import android.media.tv.TvContract.Programs.Genres; -import android.support.annotation.VisibleForTesting; -import android.text.TextUtils; -import android.util.ArraySet; -import android.util.Log; -import android.util.SparseArray; - -import com.android.tv.tuner.data.PsiData.PatItem; -import com.android.tv.tuner.data.PsiData.PmtItem; -import com.android.tv.tuner.data.PsipData.Ac3AudioDescriptor; -import com.android.tv.tuner.data.PsipData.CaptionServiceDescriptor; -import com.android.tv.tuner.data.PsipData.ContentAdvisoryDescriptor; -import com.android.tv.tuner.data.PsipData.EitItem; -import com.android.tv.tuner.data.PsipData.EttItem; -import com.android.tv.tuner.data.PsipData.ExtendedChannelNameDescriptor; -import com.android.tv.tuner.data.PsipData.GenreDescriptor; -import com.android.tv.tuner.data.PsipData.Iso639LanguageDescriptor; -import com.android.tv.tuner.data.PsipData.MgtItem; -import com.android.tv.tuner.data.PsipData.ParentalRatingDescriptor; -import com.android.tv.tuner.data.PsipData.PsipSection; -import com.android.tv.tuner.data.PsipData.RatingRegion; -import com.android.tv.tuner.data.PsipData.RegionalRating; -import com.android.tv.tuner.data.PsipData.SdtItem; -import com.android.tv.tuner.data.PsipData.ServiceDescriptor; -import com.android.tv.tuner.data.PsipData.ShortEventDescriptor; -import com.android.tv.tuner.data.PsipData.TsDescriptor; -import com.android.tv.tuner.data.PsipData.VctItem; -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.util.ByteArrayBuffer; - -import com.android.tv.tuner.util.ConvertUtils; -import com.ibm.icu.text.UnicodeDecompressor; - -import java.io.UnsupportedEncodingException; -import java.nio.charset.Charset; -import java.util.Calendar; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * Parses ATSC PSIP sections. - */ -public class SectionParser { - private static final String TAG = "SectionParser"; - private static final boolean DEBUG = false; - - private static final byte TABLE_ID_PAT = (byte) 0x00; - private static final byte TABLE_ID_PMT = (byte) 0x02; - private static final byte TABLE_ID_MGT = (byte) 0xc7; - private static final byte TABLE_ID_TVCT = (byte) 0xc8; - private static final byte TABLE_ID_CVCT = (byte) 0xc9; - private static final byte TABLE_ID_EIT = (byte) 0xcb; - private static final byte TABLE_ID_ETT = (byte) 0xcc; - - // Table id for DVB - private static final byte TABLE_ID_SDT = (byte) 0x42; - private static final byte TABLE_ID_DVB_ACTUAL_P_F_EIT = (byte) 0x4e; - private static final byte TABLE_ID_DVB_OTHER_P_F_EIT = (byte) 0x4f; - private static final byte TABLE_ID_DVB_ACTUAL_SCHEDULE_EIT = (byte) 0x50; - private static final byte TABLE_ID_DVB_OTHER_SCHEDULE_EIT = (byte) 0x60; - - // For details of the structure for the tags of descriptors, see ATSC A/65 Table 6.25. - public static final int DESCRIPTOR_TAG_ISO639LANGUAGE = 0x0a; - public static final int DESCRIPTOR_TAG_CAPTION_SERVICE = 0x86; - public static final int DESCRIPTOR_TAG_CONTENT_ADVISORY = 0x87; - public static final int DESCRIPTOR_TAG_AC3_AUDIO_STREAM = 0x81; - public static final int DESCRIPTOR_TAG_EXTENDED_CHANNEL_NAME = 0xa0; - public static final int DESCRIPTOR_TAG_GENRE = 0xab; - - // For details of the structure for the tags of DVB descriptors, see DVB Document A038 Table 12. - public static final int DVB_DESCRIPTOR_TAG_SERVICE = 0x48; - public static final int DVB_DESCRIPTOR_TAG_SHORT_EVENT = 0X4d; - public static final int DVB_DESCRIPTOR_TAG_CONTENT = 0x54; - public static final int DVB_DESCRIPTOR_TAG_PARENTAL_RATING = 0x55; - - private static final byte COMPRESSION_TYPE_NO_COMPRESSION = (byte) 0x00; - private static final byte MODE_SELECTED_UNICODE_RANGE_1 = (byte) 0x00; // 0x0000 - 0x00ff - private static final byte MODE_UTF16 = (byte) 0x3f; - private static final byte MODE_SCSU = (byte) 0x3e; - private static final int MAX_SHORT_NAME_BYTES = 14; - - // See ANSI/CEA-766-C. - private static final int RATING_REGION_US_TV = 1; - private static final int RATING_REGION_KR_TV = 4; - - // The following values are defined in the live channels app. - // See https://developer.android.com/reference/android/media/tv/TvContentRating.html. - private static final String RATING_DOMAIN = "com.android.tv"; - private static final String RATING_REGION_RATING_SYSTEM_US_TV = "US_TV"; - private static final String RATING_REGION_RATING_SYSTEM_US_MV = "US_MV"; - private static final String RATING_REGION_RATING_SYSTEM_KR_TV = "KR_TV"; - - private static final String[] RATING_REGION_TABLE_US_TV = { - "US_TV_Y", "US_TV_Y7", "US_TV_G", "US_TV_PG", "US_TV_14", "US_TV_MA" - }; - - private static final String[] RATING_REGION_TABLE_US_MV = { - "US_MV_G", "US_MV_PG", "US_MV_PG13", "US_MV_R", "US_MV_NC17" - }; - - private static final String[] RATING_REGION_TABLE_KR_TV = { - "KR_TV_ALL", "KR_TV_7", "KR_TV_12", "KR_TV_15", "KR_TV_19" - }; - - private static final String[] RATING_REGION_TABLE_US_TV_SUBRATING = { - "US_TV_D", "US_TV_L", "US_TV_S", "US_TV_V", "US_TV_FV" - }; - - // According to ANSI-CEA-766-D - private static final int VALUE_US_TV_Y = 1; - private static final int VALUE_US_TV_Y7 = 2; - private static final int VALUE_US_TV_NONE = 1; - private static final int VALUE_US_TV_G = 2; - private static final int VALUE_US_TV_PG = 3; - private static final int VALUE_US_TV_14 = 4; - private static final int VALUE_US_TV_MA = 5; - - private static final int DIMENSION_US_TV_RATING = 0; - private static final int DIMENSION_US_TV_D = 1; - private static final int DIMENSION_US_TV_L = 2; - private static final int DIMENSION_US_TV_S = 3; - private static final int DIMENSION_US_TV_V = 4; - private static final int DIMENSION_US_TV_Y = 5; - private static final int DIMENSION_US_TV_FV = 6; - private static final int DIMENSION_US_MV_RATING = 7; - - private static final int VALUE_US_MV_G = 2; - private static final int VALUE_US_MV_PG = 3; - private static final int VALUE_US_MV_PG13 = 4; - private static final int VALUE_US_MV_R = 5; - private static final int VALUE_US_MV_NC17 = 6; - private static final int VALUE_US_MV_X = 7; - - private static final String STRING_US_TV_Y = "US_TV_Y"; - private static final String STRING_US_TV_Y7 = "US_TV_Y7"; - private static final String STRING_US_TV_FV = "US_TV_FV"; - - - /* - * The following CRC table is from the code generated by the following command. - * $ python pycrc.py --model crc-32-mpeg --algorithm table-driven --generate c - * To see the details of pycrc, visit http://www.tty1.net/pycrc/index_en.html - */ - public static final int[] CRC_TABLE = { - 0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, - 0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005, - 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61, - 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd, - 0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9, - 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75, - 0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, - 0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd, - 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039, - 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5, - 0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81, - 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d, - 0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, - 0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95, - 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1, - 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d, - 0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae, - 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072, - 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, - 0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca, - 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde, - 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02, - 0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066, - 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba, - 0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, - 0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692, - 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6, - 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a, - 0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e, - 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2, - 0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, - 0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a, - 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637, - 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb, - 0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f, - 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53, - 0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, - 0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b, - 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff, - 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623, - 0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7, - 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b, - 0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, - 0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3, - 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7, - 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b, - 0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f, - 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3, - 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, - 0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c, - 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8, - 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24, - 0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30, - 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec, - 0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, - 0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654, - 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0, - 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c, - 0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18, - 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4, - 0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, - 0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c, - 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668, - 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4 - }; - - // A table which maps ATSC genres to TIF genres. - // See ATSC/65 Table 6.20. - private static final String[] CANONICAL_GENRES_TABLE = { - null, null, null, null, - null, null, null, null, - null, null, null, null, - null, null, null, null, - null, null, null, null, - null, null, null, null, - null, null, null, null, - null, null, null, null, - Genres.EDUCATION, Genres.ENTERTAINMENT, Genres.MOVIES, Genres.NEWS, - Genres.LIFE_STYLE, Genres.SPORTS, null, Genres.MOVIES, - null, - Genres.FAMILY_KIDS, Genres.DRAMA, null, Genres.ENTERTAINMENT, Genres.SPORTS, - Genres.SPORTS, - null, null, - Genres.MUSIC, Genres.EDUCATION, - null, - Genres.COMEDY, - null, - Genres.MUSIC, - null, null, - Genres.MOVIES, Genres.ENTERTAINMENT, Genres.NEWS, Genres.DRAMA, - Genres.EDUCATION, Genres.MOVIES, Genres.SPORTS, Genres.MOVIES, - null, - Genres.LIFE_STYLE, Genres.ARTS, Genres.LIFE_STYLE, Genres.SPORTS, - null, null, - Genres.GAMING, Genres.LIFE_STYLE, Genres.SPORTS, - null, - Genres.LIFE_STYLE, Genres.EDUCATION, Genres.EDUCATION, Genres.LIFE_STYLE, - Genres.SPORTS, Genres.LIFE_STYLE, Genres.MOVIES, Genres.NEWS, - null, null, null, - Genres.EDUCATION, - null, null, null, - Genres.EDUCATION, - null, null, null, - Genres.DRAMA, Genres.MUSIC, Genres.MOVIES, - null, - Genres.ANIMAL_WILDLIFE, - null, null, - Genres.PREMIER, - null, null, null, null, - Genres.SPORTS, Genres.ARTS, - null, null, null, - Genres.MOVIES, Genres.TECH_SCIENCE, Genres.DRAMA, - null, - Genres.SHOPPING, Genres.DRAMA, - null, - Genres.MOVIES, Genres.ENTERTAINMENT, Genres.TECH_SCIENCE, Genres.SPORTS, - Genres.TRAVEL, Genres.ENTERTAINMENT, Genres.ARTS, Genres.NEWS, - null, - Genres.ARTS, Genres.SPORTS, Genres.SPORTS, Genres.NEWS, - Genres.SPORTS, Genres.SPORTS, Genres.SPORTS, Genres.FAMILY_KIDS, - Genres.FAMILY_KIDS, Genres.MOVIES, - null, - Genres.TECH_SCIENCE, Genres.MUSIC, - null, - Genres.SPORTS, Genres.FAMILY_KIDS, Genres.NEWS, Genres.SPORTS, - Genres.NEWS, Genres.SPORTS, Genres.ANIMAL_WILDLIFE, - null, - Genres.MUSIC, Genres.NEWS, Genres.SPORTS, - null, - Genres.NEWS, Genres.NEWS, Genres.NEWS, Genres.NEWS, - Genres.SPORTS, Genres.MOVIES, Genres.ARTS, Genres.ANIMAL_WILDLIFE, - Genres.MUSIC, Genres.MUSIC, Genres.MOVIES, Genres.EDUCATION, - Genres.DRAMA, Genres.SPORTS, Genres.SPORTS, Genres.SPORTS, - Genres.SPORTS, - null, - Genres.SPORTS, Genres.SPORTS, - }; - - // A table which contains ATSC categorical genre code assignments. - // See ATSC/65 Table 6.20. - private static final String[] BROADCAST_GENRES_TABLE = new String[] { - null, null, null, null, - null, null, null, null, - null, null, null, null, - null, null, null, null, - null, null, null, null, - null, null, null, null, - null, null, null, null, - null, null, null, null, - "Education", "Entertainment", "Movie", "News", - "Religious", "Sports", "Other", "Action", - "Advertisement", "Animated", "Anthology", "Automobile", - "Awards", "Baseball", "Basketball", "Bulletin", - "Business", "Classical", "College", "Combat", - "Comedy", "Commentary", "Concert", "Consumer", - "Contemporary", "Crime", "Dance", "Documentary", - "Drama", "Elementary", "Erotica", "Exercise", - "Fantasy", "Farm", "Fashion", "Fiction", - "Food", "Football", "Foreign", "Fund Raiser", - "Game/Quiz", "Garden", "Golf", "Government", - "Health", "High School", "History", "Hobby", - "Hockey", "Home", "Horror", "Information", - "Instruction", "International", "Interview", "Language", - "Legal", "Live", "Local", "Math", - "Medical", "Meeting", "Military", "Miniseries", - "Music", "Mystery", "National", "Nature", - "Police", "Politics", "Premier", "Prerecorded", - "Product", "Professional", "Public", "Racing", - "Reading", "Repair", "Repeat", "Review", - "Romance", "Science", "Series", "Service", - "Shopping", "Soap Opera", "Special", "Suspense", - "Talk", "Technical", "Tennis", "Travel", - "Variety", "Video", "Weather", "Western", - "Art", "Auto Racing", "Aviation", "Biography", - "Boating", "Bowling", "Boxing", "Cartoon", - "Children", "Classic Film", "Community", "Computers", - "Country Music", "Court", "Extreme Sports", "Family", - "Financial", "Gymnastics", "Headlines", "Horse Racing", - "Hunting/Fishing/Outdoors", "Independent", "Jazz", "Magazine", - "Motorcycle Racing", "Music/Film/Books", "News-International", "News-Local", - "News-National", "News-Regional", "Olympics", "Original", - "Performing Arts", "Pets/Animals", "Pop", "Rock & Roll", - "Sci-Fi", "Self Improvement", "Sitcom", "Skating", - "Skiing", "Soccer", "Track/Field", "True", - "Volleyball", "Wrestling", - }; - - // Audio language code map from ISO 639-2/B to 639-2/T, in order to show correct audio language. - private static final HashMap<String, String> ISO_LANGUAGE_CODE_MAP; - static { - ISO_LANGUAGE_CODE_MAP = new HashMap<>(); - ISO_LANGUAGE_CODE_MAP.put("alb", "sqi"); - ISO_LANGUAGE_CODE_MAP.put("arm", "hye"); - ISO_LANGUAGE_CODE_MAP.put("baq", "eus"); - ISO_LANGUAGE_CODE_MAP.put("bur", "mya"); - ISO_LANGUAGE_CODE_MAP.put("chi", "zho"); - ISO_LANGUAGE_CODE_MAP.put("cze", "ces"); - ISO_LANGUAGE_CODE_MAP.put("dut", "nld"); - ISO_LANGUAGE_CODE_MAP.put("fre", "fra"); - ISO_LANGUAGE_CODE_MAP.put("geo", "kat"); - ISO_LANGUAGE_CODE_MAP.put("ger", "deu"); - ISO_LANGUAGE_CODE_MAP.put("gre", "ell"); - ISO_LANGUAGE_CODE_MAP.put("ice", "isl"); - ISO_LANGUAGE_CODE_MAP.put("mac", "mkd"); - ISO_LANGUAGE_CODE_MAP.put("mao", "mri"); - ISO_LANGUAGE_CODE_MAP.put("may", "msa"); - ISO_LANGUAGE_CODE_MAP.put("per", "fas"); - ISO_LANGUAGE_CODE_MAP.put("rum", "ron"); - ISO_LANGUAGE_CODE_MAP.put("slo", "slk"); - ISO_LANGUAGE_CODE_MAP.put("tib", "bod"); - ISO_LANGUAGE_CODE_MAP.put("wel", "cym"); - ISO_LANGUAGE_CODE_MAP.put("esl", "spa"); // Special entry for channel 9-1 KQED in bay area. - } - - // Containers to store the last version numbers of the PSIP sections. - private final HashMap<PsipSection, Integer> mSectionVersionMap = new HashMap<>(); - private final SparseArray<List<EttItem>> mParsedEttItems = new SparseArray<>(); - - public interface OutputListener { - void onPatParsed(List<PatItem> items); - void onPmtParsed(int programNumber, List<PmtItem> items); - void onMgtParsed(List<MgtItem> items); - void onVctParsed(List<VctItem> items, int sectionNumber, int lastSectionNumber); - void onEitParsed(int sourceId, List<EitItem> items); - void onEttParsed(int sourceId, List<EttItem> descriptions); - void onSdtParsed(List<SdtItem> items); - } - - private final OutputListener mListener; - - public SectionParser(OutputListener listener) { - mListener = listener; - } - - public void parseSections(ByteArrayBuffer data) { - int pos = 0; - while (pos + 3 <= data.length()) { - if ((data.byteAt(pos) & 0xff) == 0xff) { - // Clear stuffing bytes according to H222.0 section 2.4.4. - data.setLength(0); - break; - } - int sectionLength = - (((data.byteAt(pos + 1) & 0x0f) << 8) | (data.byteAt(pos + 2) & 0xff)) + 3; - if (pos + sectionLength > data.length()) { - break; - } - if (DEBUG) { - Log.d(TAG, "parseSections 0x" + Integer.toHexString(data.byteAt(pos) & 0xff)); - } - parseSection(Arrays.copyOfRange(data.buffer(), pos, pos + sectionLength)); - pos += sectionLength; - } - if (mListener != null) { - for (int i = 0; i < mParsedEttItems.size(); ++i) { - int sourceId = mParsedEttItems.keyAt(i); - List<EttItem> descriptions = mParsedEttItems.valueAt(i); - mListener.onEttParsed(sourceId, descriptions); - } - } - mParsedEttItems.clear(); - } - - public void resetVersionNumbers() { - mSectionVersionMap.clear(); - } - - private void parseSection(byte[] data) { - if (!checkSanity(data)) { - Log.d(TAG, "Bad CRC!"); - return; - } - PsipSection section = PsipSection.create(data); - if (section == null) { - return; - } - - // The currentNextIndicator indicates that the section sent is currently applicable. - if (!section.getCurrentNextIndicator()) { - return; - } - int versionNumber = (data[5] & 0x3e) >> 1; - Integer oldVersionNumber = mSectionVersionMap.get(section); - - // The versionNumber shall be incremented when a change in the information carried within - // the section occurs. - if (oldVersionNumber != null && versionNumber == oldVersionNumber) { - return; - } - boolean result = false; - switch (data[0]) { - case TABLE_ID_PAT: - result = parsePAT(data); - break; - case TABLE_ID_PMT: - result = parsePMT(data); - break; - case TABLE_ID_MGT: - result = parseMGT(data); - break; - case TABLE_ID_TVCT: - case TABLE_ID_CVCT: - result = parseVCT(data); - break; - case TABLE_ID_EIT: - result = parseEIT(data); - break; - case TABLE_ID_ETT: - result = parseETT(data); - break; - case TABLE_ID_SDT: - result = parseSDT(data); - break; - case TABLE_ID_DVB_ACTUAL_P_F_EIT: - case TABLE_ID_DVB_ACTUAL_SCHEDULE_EIT: - result = parseDVBEIT(data); - break; - default: - break; - } - if (result) { - mSectionVersionMap.put(section, versionNumber); - } - } - - private boolean parsePAT(byte[] data) { - if (DEBUG) { - Log.d(TAG, "PAT is discovered."); - } - int pos = 8; - - List<PatItem> results = new ArrayList<>(); - for (; pos < data.length - 4; pos = pos + 4) { - if (pos > data.length - 4 - 4) { - Log.e(TAG, "Broken PAT."); - return false; - } - int programNo = ((data[pos] & 0xff) << 8) | (data[pos + 1] & 0xff); - int pmtPid = ((data[pos + 2] & 0x1f) << 8) | (data[pos + 3] & 0xff); - results.add(new PatItem(programNo, pmtPid)); - } - if (mListener != null) { - mListener.onPatParsed(results); - } - return true; - } - - private boolean parsePMT(byte[] data) { - int table_id_ext = ((data[3] & 0xff) << 8) | (data[4] & 0xff); - if (DEBUG) { - Log.d(TAG, "PMT is discovered. programNo = " + table_id_ext); - } - if (data.length <= 11) { - Log.e(TAG, "Broken PMT."); - return false; - } - int pcrPid = (data[8] & 0x1f) << 8 | data[9]; - int programInfoLen = (data[10] & 0x0f) << 8 | data[11]; - int pos = 12; - List<TsDescriptor> descriptors = parseDescriptors(data, pos, pos + programInfoLen); - pos += programInfoLen; - if (DEBUG) { - Log.d(TAG, "PMT descriptors size: " + descriptors.size()); - } - List<PmtItem> results = new ArrayList<>(); - for (; pos < data.length - 4;) { - if (pos < 0) { - Log.e(TAG, "Broken PMT."); - return false; - } - int streamType = data[pos] & 0xff; - int esPid = (data[pos + 1] & 0x1f) << 8 | (data[pos + 2] & 0xff); - int esInfoLen = (data[pos + 3] & 0xf) << 8 | (data[pos + 4] & 0xff); - if (data.length < pos + esInfoLen + 5) { - Log.e(TAG, "Broken PMT."); - return false; - } - descriptors = parseDescriptors(data, pos + 5, pos + 5 + esInfoLen); - List<AtscAudioTrack> audioTracks = generateAudioTracks(descriptors); - List<AtscCaptionTrack> captionTracks = generateCaptionTracks(descriptors); - PmtItem pmtItem = new PmtItem(streamType, esPid, audioTracks, captionTracks); - if (DEBUG) { - Log.d(TAG, "PMT " + pmtItem + " descriptors size: " + descriptors.size()); - } - results.add(pmtItem); - pos = pos + esInfoLen + 5; - } - results.add(new PmtItem(PmtItem.ES_PID_PCR, pcrPid, null, null)); - if (mListener != null) { - mListener.onPmtParsed(table_id_ext, results); - } - return true; - } - - private boolean parseMGT(byte[] data) { - // For details of the structure for MGT, see ATSC A/65 Table 6.2. - if (DEBUG) { - Log.d(TAG, "MGT is discovered."); - } - if (data.length <= 10) { - Log.e(TAG, "Broken MGT."); - return false; - } - int tablesDefined = ((data[9] & 0xff) << 8) | (data[10] & 0xff); - int pos = 11; - List<MgtItem> results = new ArrayList<>(); - for (int i = 0; i < tablesDefined; ++i) { - if (data.length <= pos + 10) { - Log.e(TAG, "Broken MGT."); - return false; - } - int tableType = ((data[pos] & 0xff) << 8) | (data[pos + 1] & 0xff); - int tableTypePid = ((data[pos + 2] & 0x1f) << 8) | (data[pos + 3] & 0xff); - int descriptorsLength = ((data[pos + 9] & 0x0f) << 8) | (data[pos + 10] & 0xff); - pos += 11 + descriptorsLength; - results.add(new MgtItem(tableType, tableTypePid)); - } - // Skip the remaining descriptor part which we don't use. - - if (mListener != null) { - mListener.onMgtParsed(results); - } - return true; - } - - private boolean parseVCT(byte[] data) { - // For details of the structure for VCT, see ATSC A/65 Table 6.4 and 6.8. - if (DEBUG) { - Log.d(TAG, "VCT is discovered."); - } - if (data.length <= 9) { - Log.e(TAG, "Broken VCT."); - return false; - } - int numChannelsInSection = (data[9] & 0xff); - int sectionNumber = (data[6] & 0xff); - int lastSectionNumber = (data[7] & 0xff); - if (sectionNumber > lastSectionNumber) { - // According to section 6.3.1 of the spec ATSC A/65, - // last section number is the largest section number. - Log.w(TAG, "Invalid VCT. Section Number " + sectionNumber + " > Last Section Number " - + lastSectionNumber); - return false; - } - int pos = 10; - List<VctItem> results = new ArrayList<>(); - for (int i = 0; i < numChannelsInSection; ++i) { - if (data.length <= pos + 31) { - Log.e(TAG, "Broken VCT."); - return false; - } - String shortName = ""; - int shortNameSize = getShortNameSize(data, pos); - try { - shortName = new String( - Arrays.copyOfRange(data, pos, pos + shortNameSize), "UTF-16"); - } catch (UnsupportedEncodingException e) { - Log.e(TAG, "Broken VCT.", e); - return false; - } - if ((data[pos + 14] & 0xf0) != 0xf0) { - Log.e(TAG, "Broken VCT."); - return false; - } - int majorNumber = ((data[pos + 14] & 0x0f) << 6) | ((data[pos + 15] & 0xff) >> 2); - int minorNumber = ((data[pos + 15] & 0x03) << 8) | (data[pos + 16] & 0xff); - if ((majorNumber & 0x3f0) == 0x3f0) { - // If the six MSBs are 111111, these indicate that there is only one-part channel - // number. To see details, refer A/65 Section 6.3.2. - majorNumber = ((majorNumber & 0xf) << 10) + minorNumber; - minorNumber = 0; - } - int channelTsid = ((data[pos + 22] & 0xff) << 8) | (data[pos + 23] & 0xff); - int programNumber = ((data[pos + 24] & 0xff) << 8) | (data[pos + 25] & 0xff); - boolean accessControlled = (data[pos + 26] & 0x20) != 0; - boolean hidden = (data[pos + 26] & 0x10) != 0; - int serviceType = (data[pos + 27] & 0x3f); - int sourceId = ((data[pos + 28] & 0xff) << 8) | (data[pos + 29] & 0xff); - int descriptorsPos = pos + 32; - int descriptorsLength = ((data[pos + 30] & 0x03) << 8) | (data[pos + 31] & 0xff); - pos += 32 + descriptorsLength; - if (data.length < pos) { - Log.e(TAG, "Broken VCT."); - return false; - } - List<TsDescriptor> descriptors = parseDescriptors( - data, descriptorsPos, descriptorsPos + descriptorsLength); - String longName = null; - for (TsDescriptor descriptor : descriptors) { - if (descriptor instanceof ExtendedChannelNameDescriptor) { - ExtendedChannelNameDescriptor extendedChannelNameDescriptor = - (ExtendedChannelNameDescriptor) descriptor; - longName = extendedChannelNameDescriptor.getLongChannelName(); - break; - } - } - if (DEBUG) { - Log.d(TAG, String.format( - "Found channel [%s] %s - serviceType: %d tsid: 0x%x program: %d " - + "channel: %d-%d encrypted: %b hidden: %b, descriptors: %d", - shortName, longName, serviceType, channelTsid, programNumber, majorNumber, - minorNumber, accessControlled, hidden, descriptors.size())); - } - if (!accessControlled && !hidden && (serviceType == Channel.SERVICE_TYPE_ATSC_AUDIO || - serviceType == Channel.SERVICE_TYPE_ATSC_DIGITAL_TELEVISION || - serviceType == Channel.SERVICE_TYPE_UNASSOCIATED_SMALL_SCREEN_SERVICE)) { - // Hide hidden, encrypted, or unsupported ATSC service type channels - results.add(new VctItem(shortName, longName, serviceType, channelTsid, - programNumber, majorNumber, minorNumber, sourceId)); - } - } - // Skip the remaining descriptor part which we don't use. - - if (mListener != null) { - mListener.onVctParsed(results, sectionNumber, lastSectionNumber); - } - return true; - } - - private boolean parseEIT(byte[] data) { - // For details of the structure for EIT, see ATSC A/65 Table 6.11. - if (DEBUG) { - Log.d(TAG, "EIT is discovered."); - } - if (data.length <= 9) { - Log.e(TAG, "Broken EIT."); - return false; - } - int sourceId = ((data[3] & 0xff) << 8) | (data[4] & 0xff); - int numEventsInSection = (data[9] & 0xff); - - int pos = 10; - List<EitItem> results = new ArrayList<>(); - for (int i = 0; i < numEventsInSection; ++i) { - if (data.length <= pos + 9) { - Log.e(TAG, "Broken EIT."); - return false; - } - if ((data[pos] & 0xc0) != 0xc0) { - Log.e(TAG, "Broken EIT."); - return false; - } - int eventId = ((data[pos] & 0x3f) << 8) + (data[pos + 1] & 0xff); - long startTime = ((data[pos + 2] & (long) 0xff) << 24) | ((data[pos + 3] & 0xff) << 16) - | ((data[pos + 4] & 0xff) << 8) | (data[pos + 5] & 0xff); - int lengthInSecond = ((data[pos + 6] & 0x0f) << 16) - | ((data[pos + 7] & 0xff) << 8) | (data[pos + 8] & 0xff); - int titleLength = (data[pos + 9] & 0xff); - if (data.length <= pos + 10 + titleLength + 1) { - Log.e(TAG, "Broken EIT."); - return false; - } - String titleText = ""; - if (titleLength > 0) { - titleText = extractText(data, pos + 10); - } - if ((data[pos + 10 + titleLength] & 0xf0) != 0xf0) { - Log.e(TAG, "Broken EIT."); - return false; - } - int descriptorsLength = ((data[pos + 10 + titleLength] & 0x0f) << 8) - | (data[pos + 10 + titleLength + 1] & 0xff); - int descriptorsPos = pos + 10 + titleLength + 2; - if (data.length < descriptorsPos + descriptorsLength) { - Log.e(TAG, "Broken EIT."); - return false; - } - List<TsDescriptor> descriptors = parseDescriptors( - data, descriptorsPos, descriptorsPos + descriptorsLength); - if (DEBUG) { - Log.d(TAG, String.format("EIT descriptors size: %d", descriptors.size())); - } - String contentRating = generateContentRating(descriptors); - String broadcastGenre = generateBroadcastGenre(descriptors); - String canonicalGenre = generateCanonicalGenre(descriptors); - List<AtscAudioTrack> audioTracks = generateAudioTracks(descriptors); - List<AtscCaptionTrack> captionTracks = generateCaptionTracks(descriptors); - pos += 10 + titleLength + 2 + descriptorsLength; - results.add(new EitItem(EitItem.INVALID_PROGRAM_ID, eventId, titleText, - startTime, lengthInSecond, contentRating, audioTracks, captionTracks, - broadcastGenre, canonicalGenre, null)); - } - if (mListener != null) { - mListener.onEitParsed(sourceId, results); - } - return true; - } - - private boolean parseETT(byte[] data) { - // For details of the structure for ETT, see ATSC A/65 Table 6.13. - if (DEBUG) { - Log.d(TAG, "ETT is discovered."); - } - if (data.length <= 12) { - Log.e(TAG, "Broken ETT."); - return false; - } - int sourceId = ((data[9] & 0xff) << 8) | (data[10] & 0xff); - int eventId = (((data[11] & 0xff) << 8) | (data[12] & 0xff)) >> 2; - String text = extractText(data, 13); - List<EttItem> ettItems = mParsedEttItems.get(sourceId); - if (ettItems == null) { - ettItems = new ArrayList<>(); - mParsedEttItems.put(sourceId, ettItems); - } - ettItems.add(new EttItem(eventId, text)); - return true; - } - - private boolean parseSDT(byte[] data) { - // For details of the structure for SDT, see DVB Document A038 Table 5. - if (DEBUG) { - Log.d(TAG, "SDT id discovered"); - } - if (data.length <= 11) { - Log.e(TAG, "Broken SDT."); - return false; - } - if ((data[1] & 0x80) >> 7 != 1) { - Log.e(TAG, "Broken SDT, section syntax indicator error."); - return false; - } - int sectionLength = ((data[1] & 0x0f) << 8) | (data[2] & 0xff); - int transportStreamId = ((data[3] & 0xff) << 8) | (data[4] & 0xff); - int originalNetworkId = ((data[8] & 0xff) << 8) | (data[9] & 0xff); - int pos = 11; - if (sectionLength + 3 > data.length) { - Log.e(TAG, "Broken SDT."); - } - List<SdtItem> sdtItems = new ArrayList<>(); - while (pos + 9 < data.length) { - int serviceId = ((data[pos] & 0xff) << 8) | (data[pos + 1] & 0xff); - int descriptorsLength = ((data[pos + 3] & 0x0f) << 8) | (data[pos + 4] & 0xff); - pos += 5; - List<TsDescriptor> descriptors = parseDescriptors(data, pos, pos + descriptorsLength); - List<ServiceDescriptor> serviceDescriptors = generateServiceDescriptors(descriptors); - String serviceName = ""; - String serviceProviderName = ""; - int serviceType = 0; - for (ServiceDescriptor serviceDescriptor : serviceDescriptors) { - serviceName = serviceDescriptor.getServiceName(); - serviceProviderName = serviceDescriptor.getServiceProviderName(); - serviceType = serviceDescriptor.getServiceType(); - } - if (serviceDescriptors.size() > 0) { - sdtItems.add(new SdtItem(serviceName, serviceProviderName, serviceType, serviceId, - originalNetworkId)); - } - pos += descriptorsLength; - } - if (mListener != null) { - mListener.onSdtParsed(sdtItems); - } - return true; - } - - private boolean parseDVBEIT(byte[] data) { - // For details of the structure for DVB ETT, see DVB Document A038 Table 7. - if (DEBUG) { - Log.d(TAG, "DVB EIT is discovered."); - } - if (data.length < 18) { - Log.e(TAG, "Broken DVB EIT."); - return false; - } - int sectionLength = ((data[1] & 0x0f) << 8) | (data[2] & 0xff); - int sourceId = ((data[3] & 0xff) << 8) | (data[4] & 0xff); - int transportStreamId = ((data[8] & 0xff) << 8) | (data[9] & 0xff); - int originalNetworkId = ((data[10] & 0xff) << 8) | (data[11] & 0xff); - - int pos = 14; - List<EitItem> results = new ArrayList<>(); - while (pos + 12 < data.length) { - int eventId = ((data[pos] & 0xff) << 8) + (data[pos + 1] & 0xff); - float modifiedJulianDate = ((data[pos + 2] & 0xff) << 8) | (data[pos + 3] & 0xff); - int startYear = (int) ((modifiedJulianDate - 15078.2f) / 365.25f); - int mjdMonth = (int) ((modifiedJulianDate - 14956.1f - - (int) (startYear * 365.25f)) / 30.6001f); - int startDay = (int) modifiedJulianDate - 14956 - (int) (startYear * 365.25f) - - (int) (mjdMonth * 30.6001f); - int startMonth = mjdMonth - 1; - if (mjdMonth == 14 || mjdMonth == 15) { - startYear += 1; - startMonth -= 12; - } - int startHour = ((data[pos + 4] & 0xf0) >> 4) * 10 + (data[pos + 4] & 0x0f); - int startMinute = ((data[pos + 5] & 0xf0) >> 4) * 10 + (data[pos + 5] & 0x0f); - int startSecond = ((data[pos + 6] & 0xf0) >> 4) * 10 + (data[pos + 6] & 0x0f); - Calendar calendar = Calendar.getInstance(); - startYear += 1900; - calendar.set(startYear, startMonth, startDay, startHour, startMinute, startSecond); - long startTime = ConvertUtils.convertUnixEpochToGPSTime( - calendar.getTimeInMillis() / 1000); - int durationInSecond = (((data[pos + 7] & 0xf0) >> 4) * 10 - + (data[pos + 7] & 0x0f)) * 3600 - + (((data[pos + 8] & 0xf0) >> 4) * 10 + (data[pos + 8] & 0x0f)) * 60 - + (((data[pos + 9] & 0xf0) >> 4) * 10 + (data[pos + 9] & 0x0f)); - int descriptorsLength = ((data[pos + 10] & 0x0f) << 8) - | (data[pos + 10 + 1] & 0xff); - int descriptorsPos = pos + 10 + 2; - if (data.length < descriptorsPos + descriptorsLength) { - Log.e(TAG, "Broken EIT."); - return false; - } - List<TsDescriptor> descriptors = parseDescriptors( - data, descriptorsPos, descriptorsPos + descriptorsLength); - if (DEBUG) { - Log.d(TAG, String.format("DVB EIT descriptors size: %d", descriptors.size())); - } - // TODO: Add logic to generating content rating for dvb. See DVB document 6.2.28 for - // details. Content rating here will be null - String contentRating = generateContentRating(descriptors); - // TODO: Add logic for generating genre for dvb. See DVB document 6.2.9 for details. - // Genre here will be null here. - String broadcastGenre = generateBroadcastGenre(descriptors); - String canonicalGenre = generateCanonicalGenre(descriptors); - String titleText = generateShortEventName(descriptors); - List<AtscAudioTrack> audioTracks = generateAudioTracks(descriptors); - List<AtscCaptionTrack> captionTracks = generateCaptionTracks(descriptors); - pos += 12 + descriptorsLength; - results.add(new EitItem(EitItem.INVALID_PROGRAM_ID, eventId, titleText, - startTime, durationInSecond, contentRating, audioTracks, captionTracks, - broadcastGenre, canonicalGenre, null)); - } - if (mListener != null) { - mListener.onEitParsed(sourceId, results); - } - return true; - } - - private static List<AtscAudioTrack> generateAudioTracks(List<TsDescriptor> descriptors) { - // The list of audio tracks sent is located at both AC3 Audio descriptor and ISO 639 - // Language descriptor. - List<AtscAudioTrack> ac3Tracks = new ArrayList<>(); - List<AtscAudioTrack> iso639LanguageTracks = new ArrayList<>(); - for (TsDescriptor descriptor : descriptors) { - if (descriptor instanceof Ac3AudioDescriptor) { - Ac3AudioDescriptor audioDescriptor = - (Ac3AudioDescriptor) descriptor; - AtscAudioTrack audioTrack = new AtscAudioTrack(); - 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(); - ac3Tracks.add(audioTrack); - } - } - for (TsDescriptor descriptor : descriptors) { - if (descriptor instanceof Iso639LanguageDescriptor) { - Iso639LanguageDescriptor iso639LanguageDescriptor = - (Iso639LanguageDescriptor) descriptor; - iso639LanguageTracks.addAll(iso639LanguageDescriptor.getAudioTracks()); - } - } - - // An AC3 audio stream descriptor only has a audio channel count and a audio sample rate - // while a ISO 639 Language descriptor only has a audio type, which describes a main use - // case of its audio track. - // Some channels contain only AC3 audio stream descriptors with valid language values. - // Other channels contain both an AC3 audio stream descriptor and a ISO 639 Language - // descriptor per audio track, and those AC3 audio stream descriptors often have a null - // value of language field. - // Combines two descriptors into one in order to gather more audio track specific - // information as much as possible. - List<AtscAudioTrack> tracks = new ArrayList<>(); - if (!ac3Tracks.isEmpty() && !iso639LanguageTracks.isEmpty() - && ac3Tracks.size() != iso639LanguageTracks.size()) { - // This shouldn't be happen. In here, it handles two cases. The first case is that the - // only one type of descriptors arrives. The second case is that the two types of - // descriptors have the same number of tracks. - Log.e(TAG, "AC3 audio stream descriptors size != ISO 639 Language descriptors size"); - return tracks; - } - int size = Math.max(ac3Tracks.size(), iso639LanguageTracks.size()); - for (int i = 0; i < size; ++i) { - AtscAudioTrack audioTrack = null; - if (i < ac3Tracks.size()) { - audioTrack = ac3Tracks.get(i); - } - if (i < iso639LanguageTracks.size()) { - if (audioTrack == null) { - audioTrack = iso639LanguageTracks.get(i); - } else { - AtscAudioTrack iso639LanguageTrack = iso639LanguageTracks.get(i); - if (audioTrack.language == null || TextUtils.equals(audioTrack.language, "")) { - audioTrack.language = iso639LanguageTrack.language; - } - audioTrack.audioType = iso639LanguageTrack.audioType; - } - } - String language = ISO_LANGUAGE_CODE_MAP.get(audioTrack.language); - if (language != null) { - audioTrack.language = language; - } - tracks.add(audioTrack); - } - return tracks; - } - - private static List<AtscCaptionTrack> generateCaptionTracks(List<TsDescriptor> descriptors) { - List<AtscCaptionTrack> services = new ArrayList<>(); - for (TsDescriptor descriptor : descriptors) { - if (descriptor instanceof CaptionServiceDescriptor) { - CaptionServiceDescriptor captionServiceDescriptor = - (CaptionServiceDescriptor) descriptor; - services.addAll(captionServiceDescriptor.getCaptionTracks()); - } - } - return services; - } - - @VisibleForTesting - static String generateContentRating(List<TsDescriptor> descriptors) { - Set<String> contentRatings = new ArraySet<>(); - List<RatingRegion> usRatingRegions = getRatingRegions(descriptors, RATING_REGION_US_TV); - List<RatingRegion> krRatingRegions = getRatingRegions(descriptors, RATING_REGION_KR_TV); - for (RatingRegion region : usRatingRegions) { - String contentRating = getUsRating(region); - if (contentRating != null) { - contentRatings.add(contentRating); - } - } - for (RatingRegion region : krRatingRegions) { - String contentRating = getKrRating(region); - if (contentRating != null) { - contentRatings.add(contentRating); - } - } - return TextUtils.join(",", contentRatings); - } - - /** - * Gets a list of {@link RatingRegion} in the specific region. - * - * @param descriptors {@link TsDescriptor} list which may contains rating information - * @param region the specific region - * @return a list of {@link RatingRegion} in the specific region - */ - private static List<RatingRegion> getRatingRegions(List<TsDescriptor> descriptors, int region) { - List<RatingRegion> ratingRegions = new ArrayList<>(); - for (TsDescriptor descriptor : descriptors) { - if (!(descriptor instanceof ContentAdvisoryDescriptor)) { - continue; - } - ContentAdvisoryDescriptor contentAdvisoryDescriptor = - (ContentAdvisoryDescriptor) descriptor; - for (RatingRegion ratingRegion : contentAdvisoryDescriptor.getRatingRegions()) { - if (ratingRegion.getName() == region) { - ratingRegions.add(ratingRegion); - } - } - } - return ratingRegions; - } - - /** - * Gets US content rating and subratings (if any). - * - * @param ratingRegion a {@link RatingRegion} instance which may contain rating information. - * @return A string representing the US content rating and subratings. The format of the string - * is defined in {@link TvContentRating}. null, if no such a string exists. - */ - private static String getUsRating(RatingRegion ratingRegion) { - if (ratingRegion.getName() != RATING_REGION_US_TV) { - return null; - } - List<RegionalRating> regionalRatings = ratingRegion.getRegionalRatings(); - String rating = null; - int ratingIndex = VALUE_US_TV_NONE; - List<String> subratings = new ArrayList<>(); - for (RegionalRating index : regionalRatings) { - // See Table 3 of ANSI-CEA-766-D - int dimension = index.getDimension(); - int value = index.getRating(); - switch (dimension) { - // According to Table 6.27 of ATSC A65, - // the dimensions shall be in increasing order. - // Therefore, rating and ratingIndex are assigned before any corresponding - // subrating. - case DIMENSION_US_TV_RATING: - if (value >= VALUE_US_TV_G && value < RATING_REGION_TABLE_US_TV.length) { - rating = RATING_REGION_TABLE_US_TV[value]; - ratingIndex = value; - } - break; - case DIMENSION_US_TV_D: - if (value == 1 - && (ratingIndex == VALUE_US_TV_PG || ratingIndex == VALUE_US_TV_14)) { - // US_TV_D is applicable to US_TV_PG and US_TV_14 - subratings.add(RATING_REGION_TABLE_US_TV_SUBRATING[dimension - 1]); - } - break; - case DIMENSION_US_TV_L: - case DIMENSION_US_TV_S: - case DIMENSION_US_TV_V: - if (value == 1 - && ratingIndex >= VALUE_US_TV_PG - && ratingIndex <= VALUE_US_TV_MA) { - // US_TV_L, US_TV_S, and US_TV_V are applicable to - // US_TV_PG, US_TV_14 and US_TV_MA - subratings.add(RATING_REGION_TABLE_US_TV_SUBRATING[dimension - 1]); - } - break; - case DIMENSION_US_TV_Y: - if (rating == null) { - if (value == VALUE_US_TV_Y) { - rating = STRING_US_TV_Y; - } else if (value == VALUE_US_TV_Y7) { - rating = STRING_US_TV_Y7; - } - } - break; - case DIMENSION_US_TV_FV: - if (STRING_US_TV_Y7.equals(rating) && value == 1) { - // US_TV_FV is applicable to US_TV_Y7 - subratings.add(STRING_US_TV_FV); - } - break; - case DIMENSION_US_MV_RATING: - if (value >= VALUE_US_MV_G && value <= VALUE_US_MV_X) { - if (value == VALUE_US_MV_X) { - // US_MV_X was replaced by US_MV_NC17 in 1990, - // and it's not supported by TvContentRating - value = VALUE_US_MV_NC17; - } - if (rating != null) { - // According to Table 3 of ANSI-CEA-766-D, - // DIMENSION_US_TV_RATING and DIMENSION_US_MV_RATING shall not be - // present in the same descriptor. - Log.w( - TAG, - "DIMENSION_US_TV_RATING and DIMENSION_US_MV_RATING are " - + "present in the same descriptor"); - } else { - return TvContentRating.createRating( - RATING_DOMAIN, - RATING_REGION_RATING_SYSTEM_US_MV, - RATING_REGION_TABLE_US_MV[value - 2]) - .flattenToString(); - } - } - break; - - default: - break; - } - } - if (rating == null) { - return null; - } - - String[] subratingArray = subratings.toArray(new String[subratings.size()]); - return TvContentRating.createRating( - RATING_DOMAIN, RATING_REGION_RATING_SYSTEM_US_TV, rating, subratingArray) - .flattenToString(); - } - - /** - * Gets KR(South Korea) content rating. - * - * @param ratingRegion a {@link RatingRegion} instance which may contain rating information. - * @return A string representing the KR content rating. The format of the string is defined in - * {@link TvContentRating}. null, if no such a string exists. - */ - private static String getKrRating(RatingRegion ratingRegion) { - if (ratingRegion.getName() != RATING_REGION_KR_TV) { - return null; - } - List<RegionalRating> regionalRatings = ratingRegion.getRegionalRatings(); - String rating = null; - for (RegionalRating index : regionalRatings) { - if (index.getDimension() == 0 - && index.getRating() >= 0 - && index.getRating() < RATING_REGION_TABLE_KR_TV.length) { - rating = RATING_REGION_TABLE_KR_TV[index.getRating()]; - break; - } - } - if (rating == null) { - return null; - } - return TvContentRating.createRating( - RATING_DOMAIN, RATING_REGION_RATING_SYSTEM_KR_TV, rating) - .flattenToString(); - } - - private static String generateBroadcastGenre(List<TsDescriptor> descriptors) { - for (TsDescriptor descriptor : descriptors) { - if (descriptor instanceof GenreDescriptor) { - GenreDescriptor genreDescriptor = - (GenreDescriptor) descriptor; - return TextUtils.join(",", genreDescriptor.getBroadcastGenres()); - } - } - return null; - } - - private static String generateCanonicalGenre(List<TsDescriptor> descriptors) { - for (TsDescriptor descriptor : descriptors) { - if (descriptor instanceof GenreDescriptor) { - GenreDescriptor genreDescriptor = - (GenreDescriptor) descriptor; - return Genres.encode(genreDescriptor.getCanonicalGenres()); - } - } - return null; - } - - private static List<ServiceDescriptor> generateServiceDescriptors( - List<TsDescriptor> descriptors) { - List<ServiceDescriptor> serviceDescriptors = new ArrayList<>(); - for (TsDescriptor descriptor : descriptors) { - if (descriptor instanceof ServiceDescriptor) { - ServiceDescriptor serviceDescriptor = (ServiceDescriptor) descriptor; - serviceDescriptors.add(serviceDescriptor); - } - } - return serviceDescriptors; - } - - private static String generateShortEventName(List<TsDescriptor> descriptors) { - for (TsDescriptor descriptor : descriptors) { - if (descriptor instanceof ShortEventDescriptor) { - ShortEventDescriptor shortEventDescriptor = (ShortEventDescriptor) descriptor; - return shortEventDescriptor.getEventName(); - } - } - return ""; - } - - private static List<TsDescriptor> parseDescriptors(byte[] data, int offset, int limit) { - // For details of the structure for descriptors, see ATSC A/65 Section 6.9. - List<TsDescriptor> descriptors = new ArrayList<>(); - if (data.length < limit) { - return descriptors; - } - int pos = offset; - while (pos + 1 < limit) { - int tag = data[pos] & 0xff; - int length = data[pos + 1] & 0xff; - if (length <= 0) { - break; - } - if (limit < pos + length + 2) { - break; - } - if (DEBUG) { - Log.d(TAG, String.format("Descriptor tag: %02x", tag)); - } - TsDescriptor descriptor = null; - switch (tag) { - case DESCRIPTOR_TAG_CONTENT_ADVISORY: - descriptor = parseContentAdvisory(data, pos, pos + length + 2); - break; - - case DESCRIPTOR_TAG_CAPTION_SERVICE: - descriptor = parseCaptionService(data, pos, pos + length + 2); - break; - - case DESCRIPTOR_TAG_EXTENDED_CHANNEL_NAME: - descriptor = parseLongChannelName(data, pos, pos + length + 2); - break; - - case DESCRIPTOR_TAG_GENRE: - descriptor = parseGenre(data, pos, pos + length + 2); - break; - - case DESCRIPTOR_TAG_AC3_AUDIO_STREAM: - descriptor = parseAc3AudioStream(data, pos, pos + length + 2); - break; - - case DESCRIPTOR_TAG_ISO639LANGUAGE: - descriptor = parseIso639Language(data, pos, pos + length + 2); - break; - - case DVB_DESCRIPTOR_TAG_SERVICE: - descriptor = parseDvbService(data, pos, pos + length + 2); - break; - - case DVB_DESCRIPTOR_TAG_SHORT_EVENT: - descriptor = parseDvbShortEvent(data, pos, pos + length + 2); - break; - - case DVB_DESCRIPTOR_TAG_CONTENT: - descriptor = parseDvbContent(data, pos, pos + length + 2); - break; - - case DVB_DESCRIPTOR_TAG_PARENTAL_RATING: - descriptor = parseDvbParentalRating(data, pos, pos + length + 2); - break; - - default: - } - if (descriptor != null) { - if (DEBUG) { - Log.d(TAG, "Descriptor parsed: " + descriptor); - } - descriptors.add(descriptor); - } - pos += length + 2; - } - return descriptors; - } - - private static Iso639LanguageDescriptor parseIso639Language(byte[] data, int pos, int limit) { - // For the details of the structure of ISO 639 language descriptor, - // see ISO13818-1 second edition Section 2.6.18. - pos += 2; - List<AtscAudioTrack> audioTracks = new ArrayList<>(); - while (pos + 4 <= limit) { - if (limit <= pos + 3) { - Log.e(TAG, "Broken Iso639Language."); - return null; - } - String language = new String(data, pos, 3); - int audioType = data[pos + 3] & 0xff; - AtscAudioTrack audioTrack = new AtscAudioTrack(); - audioTrack.language = language; - audioTrack.audioType = audioType; - audioTracks.add(audioTrack); - pos += 4; - } - return new Iso639LanguageDescriptor(audioTracks); - } - - private static CaptionServiceDescriptor parseCaptionService(byte[] data, int pos, int limit) { - // For the details of the structure of caption service descriptor, - // see ATSC A/65 Section 6.9.2. - if (limit <= pos + 2) { - Log.e(TAG, "Broken CaptionServiceDescriptor."); - return null; - } - List<AtscCaptionTrack> services = new ArrayList<>(); - pos += 2; - int numberServices = data[pos] & 0x1f; - ++pos; - if (limit < pos + numberServices * 6) { - Log.e(TAG, "Broken CaptionServiceDescriptor."); - return null; - } - for (int i = 0; i < numberServices; ++i) { - String language = new String(Arrays.copyOfRange(data, pos, pos + 3)); - pos += 3; - boolean ccType = (data[pos] & 0x80) != 0; - if (!ccType) { - pos +=3; - continue; - } - int captionServiceNumber = data[pos] & 0x3f; - ++pos; - boolean easyReader = (data[pos] & 0x80) != 0; - boolean wideAspectRatio = (data[pos] & 0x40) != 0; - byte[] reserved = new byte[2]; - reserved[0] = (byte) (data[pos] << 2); - reserved[0] |= (byte) ((data[pos + 1] & 0xc0) >>> 6); - reserved[1] = (byte) ((data[pos + 1] & 0x3f) << 2); - pos += 2; - AtscCaptionTrack captionTrack = new AtscCaptionTrack(); - captionTrack.language = language; - captionTrack.serviceNumber = captionServiceNumber; - captionTrack.easyReader = easyReader; - captionTrack.wideAspectRatio = wideAspectRatio; - services.add(captionTrack); - } - return new CaptionServiceDescriptor(services); - } - - private static ContentAdvisoryDescriptor parseContentAdvisory(byte[] data, int pos, int limit) { - // For details of the structure for content advisory descriptor, see A/65 Table 6.27. - if (limit <= pos + 2) { - Log.e(TAG, "Broken ContentAdvisory"); - return null; - } - int count = data[pos + 2] & 0x3f; - pos += 3; - List<RatingRegion> ratingRegions = new ArrayList<>(); - for (int i = 0; i < count; ++i) { - if (limit <= pos + 1) { - Log.e(TAG, "Broken ContentAdvisory"); - return null; - } - List<RegionalRating> indices = new ArrayList<>(); - int ratingRegion = data[pos] & 0xff; - int dimensionCount = data[pos + 1] & 0xff; - pos += 2; - int previousDimension = -1; - for (int j = 0; j < dimensionCount; ++j) { - if (limit <= pos + 1) { - Log.e(TAG, "Broken ContentAdvisory"); - return null; - } - int dimensionIndex = data[pos] & 0xff; - int ratingValue = data[pos + 1] & 0x0f; - if (dimensionIndex <= previousDimension) { - // According to Table 6.27 of ATSC A65, - // the indices shall be in increasing order. - Log.e(TAG, "Broken ContentAdvisory"); - return null; - } - previousDimension = dimensionIndex; - pos += 2; - indices.add(new RegionalRating(dimensionIndex, ratingValue)); - } - if (limit <= pos) { - Log.e(TAG, "Broken ContentAdvisory"); - return null; - } - int ratingDescriptionLength = data[pos] & 0xff; - ++pos; - if (limit < pos + ratingDescriptionLength) { - Log.e(TAG, "Broken ContentAdvisory"); - return null; - } - String ratingDescription = extractText(data, pos); - pos += ratingDescriptionLength; - ratingRegions.add(new RatingRegion(ratingRegion, ratingDescription, indices)); - } - return new ContentAdvisoryDescriptor(ratingRegions); - } - - private static ExtendedChannelNameDescriptor parseLongChannelName(byte[] data, int pos, - int limit) { - if (limit <= pos + 2) { - Log.e(TAG, "Broken ExtendedChannelName."); - return null; - } - pos += 2; - String text = extractText(data, pos); - if (text == null) { - Log.e(TAG, "Broken ExtendedChannelName."); - return null; - } - return new ExtendedChannelNameDescriptor(text); - } - - private static GenreDescriptor parseGenre(byte[] data, int pos, int limit) { - pos += 2; - int attributeCount = data[pos] & 0x1f; - if (limit <= pos + attributeCount) { - Log.e(TAG, "Broken Genre."); - return null; - } - HashSet<String> broadcastGenreSet = new HashSet<>(); - HashSet<String> canonicalGenreSet = new HashSet<>(); - for (int i = 0; i < attributeCount; ++i) { - ++pos; - int genreCode = data[pos] & 0xff; - if (genreCode < BROADCAST_GENRES_TABLE.length) { - String broadcastGenre = BROADCAST_GENRES_TABLE[genreCode]; - if (broadcastGenre != null && !broadcastGenreSet.contains(broadcastGenre)) { - broadcastGenreSet.add(broadcastGenre); - } - } - if (genreCode < CANONICAL_GENRES_TABLE.length) { - String canonicalGenre = CANONICAL_GENRES_TABLE[genreCode]; - if (canonicalGenre != null && !canonicalGenreSet.contains(canonicalGenre)) { - canonicalGenreSet.add(canonicalGenre); - } - } - } - return new GenreDescriptor(broadcastGenreSet.toArray(new String[broadcastGenreSet.size()]), - canonicalGenreSet.toArray(new String[canonicalGenreSet.size()])); - } - - private static TsDescriptor parseAc3AudioStream(byte[] data, int pos, int limit) { - // For details of the AC3 audio stream descriptor, see A/52 Table A4.1. - if (limit <= pos + 5) { - Log.e(TAG, "Broken AC3 audio stream descriptor."); - return null; - } - pos += 2; - byte sampleRateCode = (byte) ((data[pos] & 0xe0) >> 5); - byte bsid = (byte) (data[pos] & 0x1f); - ++pos; - byte bitRateCode = (byte) ((data[pos] & 0xfc) >> 2); - byte surroundMode = (byte) (data[pos] & 0x03); - ++pos; - byte bsmod = (byte) ((data[pos] & 0xe0) >> 5); - int numChannels = (data[pos] & 0x1e) >> 1; - boolean fullSvc = (data[pos] & 0x01) != 0; - ++pos; - byte langCod = data[pos]; - byte langCod2 = 0; - if (numChannels == 0) { - if (limit <= pos) { - Log.e(TAG, "Broken AC3 audio stream descriptor."); - return null; - } - ++pos; - langCod2 = data[pos]; - } - if (limit <= pos + 1) { - Log.e(TAG, "Broken AC3 audio stream descriptor."); - return null; - } - byte mainId = 0; - byte priority = 0; - byte asvcflags = 0; - ++pos; - if (bsmod < 2) { - mainId = (byte) ((data[pos] & 0xe0) >> 5); - priority = (byte) ((data[pos] & 0x18) >> 3); - if ((data[pos] & 0x07) != 0x07) { - Log.e(TAG, "Broken AC3 audio stream descriptor reserved failed"); - return null; - } - } else { - asvcflags = data[pos]; - } - - // See A/52B Table A3.6 num_channels. - int numEncodedChannels; - switch (numChannels) { - case 1: - case 8: - numEncodedChannels = 1; - break; - case 2: - case 9: - numEncodedChannels = 2; - break; - case 3: - case 4: - case 10: - numEncodedChannels = 3; - break; - case 5: - case 6: - case 11: - numEncodedChannels = 4; - break; - case 7: - case 12: - numEncodedChannels = 5; - break; - case 13: - numEncodedChannels = 6; - break; - default: - numEncodedChannels = 0; - break; - } - - if (limit <= pos + 1) { - Log.w(TAG, "Missing text and language fields on AC3 audio stream descriptor."); - return new Ac3AudioDescriptor(sampleRateCode, bsid, bitRateCode, surroundMode, bsmod, - numEncodedChannels, fullSvc, langCod, langCod2, mainId, priority, asvcflags, - null, null, null); - } - ++pos; - int textLen = (data[pos] & 0xfe) >> 1; - boolean textCode = (data[pos] & 0x01) != 0; - ++pos; - String text = ""; - if (textLen > 0) { - if (limit < pos + textLen) { - Log.e(TAG, "Broken AC3 audio stream descriptor"); - return null; - } - if (textCode) { - text = new String(data, pos, textLen); - } else { - text = new String(data, pos, textLen, Charset.forName("UTF-16")); - } - pos += textLen; - } - String language = null; - String language2 = null; - if (pos < limit) { - // Many AC3 audio stream descriptors skip the language fields. - boolean languageFlag1 = (data[pos] & 0x80) != 0; - boolean languageFlag2 = (data[pos] & 0x40) != 0; - if ((data[pos] & 0x3f) != 0x3f) { - Log.e(TAG, "Broken AC3 audio stream descriptor"); - return null; - } - if (pos + (languageFlag1 ? 3 : 0) + (languageFlag2 ? 3 : 0) > limit) { - Log.e(TAG, "Broken AC3 audio stream descriptor"); - return null; - } - ++pos; - if (languageFlag1) { - language = new String(data, pos, 3); - pos += 3; - } - if (languageFlag2) { - language2 = new String(data, pos, 3); - } - } - - return new Ac3AudioDescriptor(sampleRateCode, bsid, bitRateCode, surroundMode, bsmod, - numEncodedChannels, fullSvc, langCod, langCod2, mainId, priority, asvcflags, text, - language, language2); - } - - private static TsDescriptor parseDvbService(byte[] data, int pos, int limit) { - // For details of DVB service descriptors, see DVB Document A038 Table 86. - if (limit < pos + 5) { - Log.e(TAG, "Broken service descriptor."); - return null; - } - pos += 2; - int serviceType = data[pos] & 0xff; - pos++; - int serviceProviderNameLength = data[pos] & 0xff; - pos++; - String serviceProviderName = extractTextFromDvb(data, pos, serviceProviderNameLength); - pos += serviceProviderNameLength; - int serviceNameLength = data[pos] & 0xff; - pos++; - String serviceName = extractTextFromDvb(data, pos, serviceNameLength); - return new ServiceDescriptor(serviceType, serviceProviderName, serviceName); - } - - private static TsDescriptor parseDvbShortEvent(byte[] data, int pos, int limit) { - // For details of DVB service descriptors, see DVB Document A038 Table 91. - if (limit < pos + 7) { - Log.e(TAG, "Broken short event descriptor."); - return null; - } - pos += 2; - String language = new String(data, pos, 3); - int eventNameLength = data[pos + 3] & 0xff; - pos += 4; - if (pos + eventNameLength > limit) { - Log.e(TAG, "Broken short event descriptor."); - return null; - } - String eventName = new String(data, pos, eventNameLength); - pos += eventNameLength; - int textLength = data[pos] & 0xff; - if (pos + textLength > limit) { - Log.e(TAG, "Broken short event descriptor."); - return null; - } - pos++; - String text = new String(data, pos, textLength); - return new ShortEventDescriptor(language, eventName, text); - } - - private static TsDescriptor parseDvbContent(byte[] data, int pos, int limit) { - // TODO: According to DVB Document A038 Table 27 to add a parser for content descriptor to - // get content genre. - return null; - } - - private static TsDescriptor parseDvbParentalRating(byte[] data, int pos, int limit) { - // For details of DVB service descriptors, see DVB Document A038 Table 81. - HashMap<String, Integer> ratings = new HashMap<>(); - pos += 2; - while (pos + 4 <= limit) { - String countryCode = new String(data, pos, 3); - int rating = data[pos + 3] & 0xff; - pos += 4; - if (rating > 15) { - // Rating > 15 means that the ratings is defined by broadcaster. - continue; - } - ratings.put(countryCode, rating + 3); - } - return new ParentalRatingDescriptor(ratings); - } - - private static int getShortNameSize(byte[] data, int offset) { - for (int i = 0; i < MAX_SHORT_NAME_BYTES; i += 2) { - if (data[offset + i] == 0 && data[offset + i + 1] == 0) { - return i; - } - } - return MAX_SHORT_NAME_BYTES; - } - - private static String extractText(byte[] data, int pos) { - if (data.length < pos) { - return null; - } - int numStrings = data[pos] & 0xff; - pos++; - for (int i = 0; i < numStrings; ++i) { - if (data.length <= pos + 3) { - Log.e(TAG, "Broken text."); - return null; - } - int numSegments = data[pos + 3] & 0xff; - pos += 4; - for (int j = 0; j < numSegments; ++j) { - if (data.length <= pos + 2) { - Log.e(TAG, "Broken text."); - return null; - } - int compressionType = data[pos] & 0xff; - int mode = data[pos + 1] & 0xff; - int numBytes = data[pos + 2] & 0xff; - if (data.length < pos + 3 + numBytes) { - Log.e(TAG, "Broken text."); - return null; - } - byte[] bytes = Arrays.copyOfRange(data, pos + 3, pos + 3 + numBytes); - if (compressionType == COMPRESSION_TYPE_NO_COMPRESSION) { - try { - switch (mode) { - case MODE_SELECTED_UNICODE_RANGE_1: - return new String(bytes, "ISO-8859-1"); - case MODE_SCSU: - return UnicodeDecompressor.decompress(bytes); - case MODE_UTF16: - return new String(bytes, "UTF-16"); - } - } catch (UnsupportedEncodingException e) { - Log.e(TAG, "Unsupported text format.", e); - } - } - pos += 3 + numBytes; - } - } - return null; - } - - private static String extractTextFromDvb(byte[] data, int pos, int length) { - // For details of DVB character set selection, see DVB Document A038 Annex A. - if (data.length < pos + length) { - return null; - } - try { - String charsetPrefix = "ISO-8859-"; - switch (data[0]) { - case 0x01: - case 0x02: - case 0x03: - case 0x04: - case 0x05: - case 0x06: - case 0x07: - case 0x09: - case 0x0A: - case 0x0B: - String charset = charsetPrefix + String.valueOf(data[0] & 0xff + 4); - return new String(data, pos, length, charset); - case 0x10: - if (length < 3) { - Log.e(TAG, "Broken DVB text"); - return null; - } - int codeTable = data[pos + 2] & 0xff; - if (data[pos + 1] == 0 && codeTable > 0 && codeTable < 15) { - return new String( - data, pos, length, charsetPrefix + String.valueOf(codeTable)); - } else { - return new String(data, pos, length, "ISO-8859-1"); - } - case 0x11: - case 0x14: - case 0x15: - return new String(data, pos, length, "UTF-16BE"); - case 0x12: - return new String(data, pos, length, "EUC-KR"); - case 0x13: - return new String(data, pos, length, "GB2312"); - default: - return new String(data, pos, length, "ISO-8859-1"); - } - } catch (UnsupportedEncodingException e) { - Log.e(TAG, "Unsupported text format.", e); - } - return new String(data, pos, length); - } - - private static boolean checkSanity(byte[] data) { - if (data.length <= 1) { - return false; - } - boolean hasCRC = (data[1] & 0x80) != 0; // section_syntax_indicator - if (hasCRC) { - int crc = 0xffffffff; - for(byte b : data) { - int index = ((crc >> 24) ^ (b & 0xff)) & 0xff; - crc = CRC_TABLE[index] ^ (crc << 8); - } - if(crc != 0){ - return false; - } - } - return true; - } -} diff --git a/src/com/android/tv/tuner/ts/TsParser.java b/src/com/android/tv/tuner/ts/TsParser.java deleted file mode 100644 index 7cdb534e..00000000 --- a/src/com/android/tv/tuner/ts/TsParser.java +++ /dev/null @@ -1,520 +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.ts; - -import android.util.Log; -import android.util.SparseArray; -import android.util.SparseBooleanArray; - -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.EttItem; -import com.android.tv.tuner.data.PsipData.MgtItem; -import com.android.tv.tuner.data.PsipData.SdtItem; -import com.android.tv.tuner.data.PsipData.VctItem; -import com.android.tv.tuner.data.TunerChannel; -import com.android.tv.tuner.ts.SectionParser.OutputListener; -import com.android.tv.tuner.util.ByteArrayBuffer; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.TreeSet; - -/** - * Parses MPEG-2 TS packets. - */ -public class TsParser { - private static final String TAG = "TsParser"; - private static final boolean DEBUG = false; - - public static final int ATSC_SI_BASE_PID = 0x1ffb; - public static final int PAT_PID = 0x0000; - public static final int DVB_SDT_PID = 0x0011; - public static final int DVB_EIT_PID = 0x0012; - private static final int TS_PACKET_START_CODE = 0x47; - private static final int TS_PACKET_TEI_MASK = 0x80; - private static final int TS_PACKET_SIZE = 188; - - /* - * Using a SparseArray removes the need to auto box the int key for mStreamMap - * in feedTdPacket which is called 100 times a second. This greatly reduces the - * number of objects created and the frequency of garbage collection. - * Other maps might be suitable for a SparseArray, but the performance - * trade offs must be considered carefully. - * mStreamMap is the only one called at such a high rate. - */ - private final SparseArray<Stream> mStreamMap = new SparseArray<>(); - private final Map<Integer, VctItem> mSourceIdToVctItemMap = new HashMap<>(); - private final Map<Integer, String> mSourceIdToVctItemDescriptionMap = new HashMap<>(); - private final Map<Integer, VctItem> mProgramNumberToVctItemMap = new HashMap<>(); - private final Map<Integer, List<PmtItem>> mProgramNumberToPMTMap = new HashMap<>(); - private final Map<Integer, List<EitItem>> mSourceIdToEitMap = new HashMap<>(); - private final Map<Integer, SdtItem> mProgramNumberToSdtItemMap = new HashMap<>(); - private final Map<EventSourceEntry, List<EitItem>> mEitMap = new HashMap<>(); - private final Map<EventSourceEntry, List<EttItem>> mETTMap = new HashMap<>(); - private final TreeSet<Integer> mEITPids = new TreeSet<>(); - private final TreeSet<Integer> mETTPids = new TreeSet<>(); - private final SparseBooleanArray mProgramNumberHandledStatus = new SparseBooleanArray(); - private final SparseBooleanArray mVctItemHandledStatus = new SparseBooleanArray(); - private final TsOutputListener mListener; - private final boolean mIsDvbSignal; - - private int mVctItemCount; - private int mHandledVctItemCount; - private int mVctSectionParsedCount; - private boolean[] mVctSectionParsed; - - public interface TsOutputListener { - void onPatDetected(List<PatItem> items); - void onEitPidDetected(int pid); - void onVctItemParsed(VctItem channel, List<PmtItem> pmtItems); - void onEitItemParsed(VctItem channel, List<EitItem> items); - void onEttPidDetected(int pid); - void onAllVctItemsParsed(); - void onSdtItemParsed(SdtItem channel, List<PmtItem> pmtItems); - } - - private abstract class Stream { - private static final int INVALID_CONTINUITY_COUNTER = -1; - private static final int NUM_CONTINUITY_COUNTER = 16; - - protected int mContinuityCounter = INVALID_CONTINUITY_COUNTER; - protected final ByteArrayBuffer mPacket = new ByteArrayBuffer(TS_PACKET_SIZE); - - public void feedData(byte[] data, int continuityCounter, boolean startIndicator) { - if ((mContinuityCounter + 1) % NUM_CONTINUITY_COUNTER != continuityCounter) { - mPacket.setLength(0); - } - mContinuityCounter = continuityCounter; - handleData(data, startIndicator); - } - - protected abstract void handleData(byte[] data, boolean startIndicator); - protected abstract void resetDataVersions(); - } - - private class SectionStream extends Stream { - private final SectionParser mSectionParser; - private final int mPid; - - public SectionStream(int pid) { - mPid = pid; - mSectionParser = new SectionParser(mSectionListener); - } - - @Override - protected void handleData(byte[] data, boolean startIndicator) { - int startPos = 0; - if (mPacket.length() == 0) { - if (startIndicator) { - startPos = (data[0] & 0xff) + 1; - } else { - // Don't know where the section starts yet. Wait until start indicator is on. - return; - } - } else { - if (startIndicator) { - startPos = 1; - } - } - - // When a broken packet is encountered, parsing will stop and return right away. - if (startPos >= data.length) { - mPacket.setLength(0); - return; - } - mPacket.append(data, startPos, data.length - startPos); - mSectionParser.parseSections(mPacket); - } - - @Override - protected void resetDataVersions() { - mSectionParser.resetVersionNumbers(); - } - - private final OutputListener mSectionListener = new OutputListener() { - @Override - public void onPatParsed(List<PatItem> items) { - for (PatItem i : items) { - startListening(i.getPmtPid()); - } - if (mListener != null) { - mListener.onPatDetected(items); - } - } - - @Override - public void onPmtParsed(int programNumber, List<PmtItem> items) { - mProgramNumberToPMTMap.put(programNumber, items); - if (DEBUG) { - Log.d(TAG, "onPMTParsed, programNo " + programNumber + " handledStatus is " - + mProgramNumberHandledStatus.get(programNumber, false)); - } - int statusIndex = mProgramNumberHandledStatus.indexOfKey(programNumber); - if (statusIndex < 0) { - mProgramNumberHandledStatus.put(programNumber, false); - } - if (!mProgramNumberHandledStatus.get(programNumber)) { - VctItem vctItem = mProgramNumberToVctItemMap.get(programNumber); - if (vctItem != null) { - // When PMT is parsed later than VCT. - mProgramNumberHandledStatus.put(programNumber, true); - handleVctItem(vctItem, items); - mHandledVctItemCount++; - if (mHandledVctItemCount >= mVctItemCount - && mVctSectionParsedCount >= mVctSectionParsed.length - && mListener != null) { - mListener.onAllVctItemsParsed(); - } - } - SdtItem sdtItem = mProgramNumberToSdtItemMap.get(programNumber); - if (sdtItem != null) { - // When PMT is parsed later than SDT. - mProgramNumberHandledStatus.put(programNumber, true); - handleSdtItem(sdtItem, items); - } - } - } - - @Override - public void onMgtParsed(List<MgtItem> items) { - for (MgtItem i : items) { - if (mStreamMap.get(i.getTableTypePid()) != null) { - continue; - } - if (i.getTableType() >= MgtItem.TABLE_TYPE_EIT_RANGE_START - && i.getTableType() <= MgtItem.TABLE_TYPE_EIT_RANGE_END) { - startListening(i.getTableTypePid()); - mEITPids.add(i.getTableTypePid()); - if (mListener != null) { - mListener.onEitPidDetected(i.getTableTypePid()); - } - } else if (i.getTableType() == MgtItem.TABLE_TYPE_CHANNEL_ETT || - (i.getTableType() >= MgtItem.TABLE_TYPE_ETT_RANGE_START - && i.getTableType() <= MgtItem.TABLE_TYPE_ETT_RANGE_END)) { - startListening(i.getTableTypePid()); - mETTPids.add(i.getTableTypePid()); - if (mListener != null) { - mListener.onEttPidDetected(i.getTableTypePid()); - } - } - } - } - - @Override - public void onVctParsed(List<VctItem> items, int sectionNumber, int lastSectionNumber) { - if (mVctSectionParsed == null) { - mVctSectionParsed = new boolean[lastSectionNumber + 1]; - } else if (mVctSectionParsed[sectionNumber]) { - // The current section was handled before. - if (DEBUG) { - Log.d(TAG, "Duplicate VCT section found."); - } - return; - } - mVctSectionParsed[sectionNumber] = true; - mVctSectionParsedCount++; - mVctItemCount += items.size(); - for (VctItem i : items) { - if (DEBUG) Log.d(TAG, "onVCTParsed " + i); - if (i.getSourceId() != 0) { - mSourceIdToVctItemMap.put(i.getSourceId(), i); - i.setDescription(mSourceIdToVctItemDescriptionMap.get(i.getSourceId())); - } - int programNumber = i.getProgramNumber(); - mProgramNumberToVctItemMap.put(programNumber, i); - List<PmtItem> pmtList = mProgramNumberToPMTMap.get(programNumber); - if (pmtList != null) { - mProgramNumberHandledStatus.put(programNumber, true); - handleVctItem(i, pmtList); - mHandledVctItemCount++; - if (mHandledVctItemCount >= mVctItemCount - && mVctSectionParsedCount >= mVctSectionParsed.length - && mListener != null) { - mListener.onAllVctItemsParsed(); - } - } else { - mProgramNumberHandledStatus.put(programNumber, false); - Log.i(TAG, "onVCTParsed, but PMT for programNo " + programNumber - + " is not found yet."); - } - } - } - - @Override - public void onEitParsed(int sourceId, List<EitItem> items) { - if (DEBUG) Log.d(TAG, "onEITParsed " + sourceId); - EventSourceEntry entry = new EventSourceEntry(mPid, sourceId); - mEitMap.put(entry, items); - handleEvents(sourceId); - } - - @Override - public void onEttParsed(int sourceId, List<EttItem> descriptions) { - if (DEBUG) { - Log.d(TAG, String.format("onETTParsed sourceId: %d, descriptions.size(): %d", - sourceId, descriptions.size())); - } - for (EttItem item : descriptions) { - if (item.eventId == 0) { - // Channel description - mSourceIdToVctItemDescriptionMap.put(sourceId, item.text); - VctItem vctItem = mSourceIdToVctItemMap.get(sourceId); - if (vctItem != null) { - vctItem.setDescription(item.text); - List<PmtItem> pmtItems = - mProgramNumberToPMTMap.get(vctItem.getProgramNumber()); - if (pmtItems != null) { - handleVctItem(vctItem, pmtItems); - } - } - } - } - - // Event Information description - EventSourceEntry entry = new EventSourceEntry(mPid, sourceId); - mETTMap.put(entry, descriptions); - handleEvents(sourceId); - } - - @Override - public void onSdtParsed(List<SdtItem> sdtItems) { - for (SdtItem sdtItem : sdtItems) { - if (DEBUG) Log.d(TAG, "onSdtParsed " + sdtItem); - int programNumber = sdtItem.getServiceId(); - mProgramNumberToSdtItemMap.put(programNumber, sdtItem); - List<PmtItem> pmtList = mProgramNumberToPMTMap.get(programNumber); - if (pmtList != null) { - mProgramNumberHandledStatus.put(programNumber, true); - handleSdtItem(sdtItem, pmtList); - } else { - mProgramNumberHandledStatus.put(programNumber, false); - Log.i(TAG, "onSdtParsed, but PMT for programNo " + programNumber - + " is not found yet."); - } - } - } - }; - } - - private static class EventSourceEntry { - public final int pid; - public final int sourceId; - - public EventSourceEntry(int pid, int sourceId) { - this.pid = pid; - this.sourceId = sourceId; - } - - @Override - public int hashCode() { - int result = 17; - result = 31 * result + pid; - result = 31 * result + sourceId; - return result; - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof EventSourceEntry) { - EventSourceEntry another = (EventSourceEntry) obj; - return pid == another.pid && sourceId == another.sourceId; - } - return false; - } - } - - private void handleVctItem(VctItem channel, List<PmtItem> pmtItems) { - if (DEBUG) { - Log.d(TAG, "handleVctItem " + channel); - } - if (mListener != null) { - mListener.onVctItemParsed(channel, pmtItems); - } - int sourceId = channel.getSourceId(); - int statusIndex = mVctItemHandledStatus.indexOfKey(sourceId); - if (statusIndex < 0) { - mVctItemHandledStatus.put(sourceId, false); - return; - } - if (!mVctItemHandledStatus.valueAt(statusIndex)) { - List<EitItem> eitItems = mSourceIdToEitMap.get(sourceId); - if (eitItems != null) { - // When VCT is parsed later than EIT. - mVctItemHandledStatus.put(sourceId, true); - handleEitItems(channel, eitItems); - } - } - } - - private void handleEitItems(VctItem channel, List<EitItem> items) { - if (mListener != null) { - mListener.onEitItemParsed(channel, items); - } - } - - private void handleSdtItem(SdtItem channel, List<PmtItem> pmtItems) { - if (DEBUG) { - Log.d(TAG, "handleSdtItem " + channel); - } - if (mListener != null) { - mListener.onSdtItemParsed(channel, pmtItems); - } - } - - private void handleEvents(int sourceId) { - Map<Integer, EitItem> itemSet = new HashMap<>(); - for (int pid : mEITPids) { - List<EitItem> eitItems = mEitMap.get(new EventSourceEntry(pid, sourceId)); - if (eitItems != null) { - for (EitItem item : eitItems) { - item.setDescription(null); - itemSet.put(item.getEventId(), item); - } - } - } - for (int pid : mETTPids) { - List<EttItem> ettItems = mETTMap.get(new EventSourceEntry(pid, sourceId)); - if (ettItems != null) { - for (EttItem ettItem : ettItems) { - if (ettItem.eventId != 0) { - EitItem item = itemSet.get(ettItem.eventId); - if (item != null) { - item.setDescription(ettItem.text); - } - } - } - } - } - List<EitItem> items = new ArrayList<>(itemSet.values()); - mSourceIdToEitMap.put(sourceId, items); - VctItem channel = mSourceIdToVctItemMap.get(sourceId); - if (channel != null && mProgramNumberHandledStatus.get(channel.getProgramNumber())) { - mVctItemHandledStatus.put(sourceId, true); - handleEitItems(channel, items); - } else { - mVctItemHandledStatus.put(sourceId, false); - if (!mIsDvbSignal) { - // Log only when zapping to non-DVB channels, since there is not VCT in DVB signal. - Log.i(TAG, "onEITParsed, but VCT for sourceId " + sourceId + " is not found yet."); - } - } - } - - /** - * Creates MPEG-2 TS parser. - * - * @param listener TsOutputListener - */ - public TsParser(TsOutputListener listener, boolean isDvbSignal) { - startListening(PAT_PID); - startListening(ATSC_SI_BASE_PID); - mIsDvbSignal = isDvbSignal; - if (isDvbSignal) { - startListening(DVB_EIT_PID); - startListening(DVB_SDT_PID); - } - mListener = listener; - } - - private void startListening(int pid) { - mStreamMap.put(pid, new SectionStream(pid)); - } - - private boolean feedTSPacket(byte[] tsData, int pos) { - if (tsData.length < pos + TS_PACKET_SIZE) { - if (DEBUG) Log.d(TAG, "Data should include a single TS packet."); - return false; - } - if (tsData[pos] != TS_PACKET_START_CODE) { - if (DEBUG) Log.d(TAG, "Invalid ts packet."); - return false; - } - if ((tsData[pos + 1] & TS_PACKET_TEI_MASK) != 0) { - if (DEBUG) Log.d(TAG, "Erroneous ts packet."); - return false; - } - - // For details for the structure of TS packet, see H.222.0 Table 2-2. - int pid = ((tsData[pos + 1] & 0x1f) << 8) | (tsData[pos + 2] & 0xff); - boolean hasAdaptation = (tsData[pos + 3] & 0x20) != 0; - boolean hasPayload = (tsData[pos + 3] & 0x10) != 0; - boolean payloadStartIndicator = (tsData[pos + 1] & 0x40) != 0; - int continuityCounter = tsData[pos + 3] & 0x0f; - Stream stream = mStreamMap.get(pid); - int payloadPos = pos; - payloadPos += hasAdaptation ? 5 + (tsData[pos + 4] & 0xff) : 4; - if (!hasPayload || stream == null) { - // We are not interested in this packet. - return false; - } - if (payloadPos >= pos + TS_PACKET_SIZE) { - if (DEBUG) Log.d(TAG, "Payload should be included in a single TS packet."); - return false; - } - stream.feedData(Arrays.copyOfRange(tsData, payloadPos, pos + TS_PACKET_SIZE), - continuityCounter, payloadStartIndicator); - return true; - } - - /** - * Feeds MPEG-2 TS data to parse. - * @param tsData buffer for ATSC TS stream - * @param pos the offset where buffer starts - * @param length The length of available data - */ - public void feedTSData(byte[] tsData, int pos, int length) { - for (; pos <= length - TS_PACKET_SIZE; pos += TS_PACKET_SIZE) { - feedTSPacket(tsData, pos); - } - } - - /** - * Retrieves the channel information regardless of being well-formed. - * @return {@link List} of {@link TunerChannel} - */ - public List<TunerChannel> getMalFormedChannels() { - List<TunerChannel> incompleteChannels = new ArrayList<>(); - for (int i = 0; i < mProgramNumberHandledStatus.size(); i++) { - if (!mProgramNumberHandledStatus.valueAt(i)) { - int programNumber = mProgramNumberHandledStatus.keyAt(i); - List<PmtItem> pmtList = mProgramNumberToPMTMap.get(programNumber); - if (pmtList != null) { - TunerChannel tunerChannel = new TunerChannel(programNumber, pmtList); - incompleteChannels.add(tunerChannel); - } - } - } - 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 deleted file mode 100644 index d2b4998a..00000000 --- a/src/com/android/tv/tuner/tvinput/ChannelDataManager.java +++ /dev/null @@ -1,734 +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.tvinput; - -import android.content.ComponentName; -import android.content.ContentProviderOperation; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.OperationApplicationException; -import android.database.Cursor; -import android.media.tv.TvContract; -import android.net.Uri; -import android.os.Build; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Message; -import android.os.RemoteException; -import android.support.annotation.Nullable; -import android.text.format.DateUtils; -import android.util.Log; - -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; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentSkipListMap; -import java.util.concurrent.ConcurrentSkipListSet; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Manages the channel info and EPG data through {@link TvInputManager}. - */ -public class ChannelDataManager implements Handler.Callback { - private static final String TAG = "ChannelDataManager"; - - private static final String[] ALL_PROGRAMS_SELECTION_ARGS = new String[] { - TvContract.Programs._ID, - TvContract.Programs.COLUMN_TITLE, - TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, - TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, - TvContract.Programs.COLUMN_CONTENT_RATING, - TvContract.Programs.COLUMN_BROADCAST_GENRE, - TvContract.Programs.COLUMN_CANONICAL_GENRE, - TvContract.Programs.COLUMN_SHORT_DESCRIPTION, - TvContract.Programs.COLUMN_VERSION_NUMBER }; - private static final String[] CHANNEL_DATA_SELECTION_ARGS = new String[] { - TvContract.Channels._ID, - TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA, - TvContract.Channels.COLUMN_INTERNAL_PROVIDER_FLAG1}; - - private static final int MSG_HANDLE_EVENTS = 1; - private static final int MSG_HANDLE_CHANNEL = 2; - private static final int MSG_BUILD_CHANNEL_MAP = 3; - private static final int MSG_REQUEST_PROGRAMS = 4; - private static final int MSG_CLEAR_CHANNELS = 6; - private static final int MSG_CHECK_VERSION = 7; - - // Throttle the batch operations to avoid TransactionTooLargeException. - private static final int BATCH_OPERATION_COUNT = 100; - // At most 16 days of program information is delivered through an EIT, - // according to the Chapter 6.4 of ATSC Recommended Practice A/69. - private static final long PROGRAM_QUERY_DURATION = TimeUnit.DAYS.toMillis(16); - - /** - * A version number to enforce consistency of the channel data. - * - * WARNING: If a change in the database serialization lead to breaking the backward - * compatibility, you must increment this value so that the old data are purged, - * and the user is requested to perform the auto-scan again to generate the new data set. - */ - private static final int VERSION = 6; - - private final Context mContext; - private final String mInputId; - private ProgramInfoListener mListener; - private ChannelScanListener mChannelScanListener; - private Handler mChannelScanHandler; - private final HandlerThread mHandlerThread; - private final Handler mHandler; - private final ConcurrentHashMap<Long, TunerChannel> mTunerChannelMap; - private final ConcurrentSkipListMap<TunerChannel, Long> mTunerChannelIdMap; - private final Uri mChannelsUri; - - // Used for scanning - private final ConcurrentSkipListSet<TunerChannel> mScannedChannels; - private final ConcurrentSkipListSet<TunerChannel> mPreviousScannedChannels; - private final AtomicBoolean mIsScanning; - private final AtomicBoolean scanCompleted = new AtomicBoolean(); - - public interface ProgramInfoListener { - - /** - * Invoked when a request for getting programs of a channel has been processed and passes - * the requested channel and the programs retrieved from database to the listener. - */ - void onRequestProgramsResponse(TunerChannel channel, List<EitItem> programs); - - /** - * Invoked when programs of a channel have been arrived and passes the arrived channel and - * programs to the listener. - */ - void onProgramsArrived(TunerChannel channel, List<EitItem> programs); - - /** - * Invoked when a channel has been arrived and passes the arrived channel to the listener. - */ - void onChannelArrived(TunerChannel channel); - - /** - * Invoked when the database schema has been changed and the old-format channels have been - * deleted. A receiver should notify to a user that re-scanning channels is necessary. - */ - void onRescanNeeded(); - } - - public interface ChannelScanListener { - /** - * Invoked when all pending channels have been handled. - */ - void onChannelHandlingDone(); - } - - public ChannelDataManager(Context context) { - mContext = context; - mInputId = TvContract.buildInputId(new ComponentName(mContext.getPackageName(), - TunerTvInputService.class.getName())); - mChannelsUri = TvContract.buildChannelsUriForInput(mInputId); - mTunerChannelMap = new ConcurrentHashMap<>(); - mTunerChannelIdMap = new ConcurrentSkipListMap<>(); - mHandlerThread = new HandlerThread("TvInputServiceBackgroundThread"); - mHandlerThread.start(); - mHandler = new Handler(mHandlerThread.getLooper(), this); - mIsScanning = new AtomicBoolean(); - mScannedChannels = new ConcurrentSkipListSet<>(); - mPreviousScannedChannels = new ConcurrentSkipListSet<>(); - } - - // Public methods - public void checkDataVersion(Context context) { - int version = TunerPreferences.getChannelDataVersion(context); - Log.d(TAG, "ChannelDataManager.VERSION=" + VERSION + " (current=" + version + ")"); - if (version == VERSION) { - // Everything is awesome. Return and continue. - return; - } - setCurrentVersion(context); - - if (version == TunerPreferences.CHANNEL_DATA_VERSION_NOT_SET) { - mHandler.sendEmptyMessage(MSG_CHECK_VERSION); - } else { - // The stored channel data seem outdated. Delete them all. - mHandler.sendEmptyMessage(MSG_CLEAR_CHANNELS); - } - } - - public void setCurrentVersion(Context context) { - TunerPreferences.setChannelDataVersion(context, VERSION); - } - - public void setListener(ProgramInfoListener listener) { - mListener = listener; - } - - public void setChannelScanListener(ChannelScanListener listener, Handler handler) { - mChannelScanListener = listener; - mChannelScanHandler = handler; - } - - public void release() { - mHandler.removeCallbacksAndMessages(null); - releaseSafely(); - } - - public void releaseSafely() { - mHandlerThread.quitSafely(); - mListener = null; - mChannelScanListener = null; - mChannelScanHandler = null; - } - - public TunerChannel getChannel(long channelId) { - TunerChannel channel = mTunerChannelMap.get(channelId); - if (channel != null) { - return channel; - } - mHandler.sendEmptyMessage(MSG_BUILD_CHANNEL_MAP); - byte[] data = null; - try (Cursor cursor = mContext.getContentResolver().query(TvContract.buildChannelUri( - channelId), CHANNEL_DATA_SELECTION_ARGS, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - data = cursor.getBlob(1); - } - } - if (data == null) { - return null; - } - channel = TunerChannel.parseFrom(data); - if (channel == null) { - return null; - } - channel.setChannelId(channelId); - return channel; - } - - public void requestProgramsData(TunerChannel channel) { - mHandler.removeMessages(MSG_REQUEST_PROGRAMS); - mHandler.obtainMessage(MSG_REQUEST_PROGRAMS, channel).sendToTarget(); - } - - public void notifyEventDetected(TunerChannel channel, List<EitItem> items) { - mHandler.obtainMessage(MSG_HANDLE_EVENTS, new ChannelEvent(channel, items)).sendToTarget(); - } - - public void notifyChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) { - if (mIsScanning.get()) { - // During scanning, channels should be handle first to improve scan time. - // EIT items can be handled in background after channel scan. - mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage(MSG_HANDLE_CHANNEL, channel)); - } else { - mHandler.obtainMessage(MSG_HANDLE_CHANNEL, channel).sendToTarget(); - } - } - - // For scanning process - /** - * Invoked when starting a scanning mode. This method gets the previous channels to detect the - * obsolete channels after scanning and initializes the variables used for scanning. - */ - public void notifyScanStarted() { - mScannedChannels.clear(); - mPreviousScannedChannels.clear(); - try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri, - CHANNEL_DATA_SELECTION_ARGS, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - do { - long channelId = cursor.getLong(0); - byte[] data = cursor.getBlob(1); - TunerChannel channel = TunerChannel.parseFrom(data); - if (channel != null) { - channel.setChannelId(channelId); - mPreviousScannedChannels.add(channel); - } - } while (cursor.moveToNext()); - } - } - mIsScanning.set(true); - } - - /** - * Invoked when completing the scanning mode. Passes {@code MSG_SCAN_COMPLETED} to the handler - * in order to wait for finishing the remaining messages in the handler queue. Then removes the - * obsolete channels, which are previously scanned but are not in the current scanned result. - */ - public void notifyScanCompleted() { - // Send a dummy message to check whether there is any MSG_HANDLE_CHANNEL in queue - // and avoid race conditions. - scanCompleted.set(true); - mHandler.sendMessageAtFrontOfQueue(mHandler.obtainMessage(MSG_HANDLE_CHANNEL, null)); - } - - public void scannedChannelHandlingCompleted() { - mIsScanning.set(false); - if (!mPreviousScannedChannels.isEmpty()) { - ArrayList<ContentProviderOperation> ops = new ArrayList<>(); - for (TunerChannel channel : mPreviousScannedChannels) { - ops.add(ContentProviderOperation.newDelete( - TvContract.buildChannelUri(channel.getChannelId())).build()); - } - try { - mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, ops); - } catch (RemoteException | OperationApplicationException e) { - Log.e(TAG, "Error deleting obsolete channels", e); - } - } - if (mChannelScanListener != null && mChannelScanHandler != null) { - mChannelScanHandler.post(new Runnable() { - @Override - public void run() { - mChannelScanListener.onChannelHandlingDone(); - } - }); - } else { - Log.e(TAG, "Error. mChannelScanListener is null."); - } - } - - /** - * Returns the number of scanned channels in the scanning mode. - */ - public int getScannedChannelCount() { - return mScannedChannels.size(); - } - - /** - * Removes all callbacks and messages in handler to avoid previous messages from last channel. - */ - public void removeAllCallbacksAndMessages() { - mHandler.removeCallbacksAndMessages(null); - } - - @Override - public boolean handleMessage(Message msg) { - switch (msg.what) { - case MSG_HANDLE_EVENTS: { - ChannelEvent event = (ChannelEvent) msg.obj; - handleEvents(event.channel, event.eitItems); - return true; - } - case MSG_HANDLE_CHANNEL: { - TunerChannel channel = (TunerChannel) msg.obj; - if (channel != null) { - handleChannel(channel); - } - if (scanCompleted.get() && mIsScanning.get() - && !mHandler.hasMessages(MSG_HANDLE_CHANNEL)) { - // Complete the scan when all found channels have already been handled. - scannedChannelHandlingCompleted(); - } - return true; - } - case MSG_BUILD_CHANNEL_MAP: { - mHandler.removeMessages(MSG_BUILD_CHANNEL_MAP); - buildChannelMap(); - return true; - } - case MSG_REQUEST_PROGRAMS: { - if (mHandler.hasMessages(MSG_REQUEST_PROGRAMS)) { - return true; - } - TunerChannel channel = (TunerChannel) msg.obj; - if (mListener != null) { - mListener.onRequestProgramsResponse(channel, getAllProgramsForChannel(channel)); - } - return true; - } - case MSG_CLEAR_CHANNELS: { - clearChannels(); - return true; - } - case MSG_CHECK_VERSION: { - checkVersion(); - return true; - } - } - return false; - } - - // Private methods - private void handleEvents(TunerChannel channel, List<EitItem> items) { - long channelId = getChannelId(channel); - if (channelId <= 0) { - return; - } - channel.setChannelId(channelId); - - // Schedule the audio and caption tracks of the current program and the programs being - // listed after the current one into TIS. - if (mListener != null) { - mListener.onProgramsArrived(channel, items); - } - - long currentTime = System.currentTimeMillis(); - List<EitItem> oldItems = getAllProgramsForChannel(channel, currentTime, - currentTime + PROGRAM_QUERY_DURATION); - ArrayList<ContentProviderOperation> ops = new ArrayList<>(); - // TODO: Find a right way to check if the programs are added outside. - boolean addedOutside = false; - for (EitItem item : oldItems) { - if (item.getEventId() == 0) { - // The event has been added outside TV tuner. - addedOutside = true; - break; - } - } - - // Inserting programs only when there is no overlapping with existing data assuming that: - // 1. external EPG is more accurate and rich and - // 2. the data we add here will be updated when we apply external EPG. - if (addedOutside) { - // oldItemCount cannot be 0 if addedOutside is true. - int oldItemCount = oldItems.size(); - for (EitItem newItem : items) { - if (newItem.getEndTimeUtcMillis() < currentTime) { - continue; - } - long newItemStartTime = newItem.getStartTimeUtcMillis(); - long newItemEndTime = newItem.getEndTimeUtcMillis(); - if (newItemStartTime < oldItems.get(0).getStartTimeUtcMillis()) { - // Start time smaller than that of any old items. Insert if no overlap. - if (newItemEndTime > oldItems.get(0).getStartTimeUtcMillis()) continue; - } else if (newItemStartTime - > oldItems.get(oldItemCount - 1).getStartTimeUtcMillis()) { - // Start time larger than that of any old item. Insert if no overlap. - if (newItemStartTime - < oldItems.get(oldItemCount - 1).getEndTimeUtcMillis()) continue; - } else { - int pos = Collections.binarySearch(oldItems, newItem, - new Comparator<EitItem>() { - @Override - public int compare(EitItem lhs, EitItem rhs) { - return Long.compare(lhs.getStartTimeUtcMillis(), - rhs.getStartTimeUtcMillis()); - } - }); - if (pos >= 0) { - // Same start Time found. Overlapped. - continue; - } - int insertPoint = -1 - pos; - // Check the two adjacent items. - if (newItemStartTime < oldItems.get(insertPoint - 1).getEndTimeUtcMillis() - || newItemEndTime > oldItems.get(insertPoint).getStartTimeUtcMillis()) { - continue; - } - } - ops.add(buildContentProviderOperation(ContentProviderOperation.newInsert( - TvContract.Programs.CONTENT_URI), newItem, channel)); - if (ops.size() >= BATCH_OPERATION_COUNT) { - applyBatch(channel.getName(), ops); - ops.clear(); - } - } - applyBatch(channel.getName(), ops); - return; - } - - List<EitItem> outdatedOldItems = new ArrayList<>(); - Map<Integer, EitItem> newEitItemMap = new HashMap<>(); - for (EitItem item : items) { - newEitItemMap.put(item.getEventId(), item); - } - for (EitItem oldItem : oldItems) { - EitItem item = newEitItemMap.get(oldItem.getEventId()); - if (item == null) { - outdatedOldItems.add(oldItem); - continue; - } - - // Since program descriptions arrive at different time, the older one may have the - // correct program description while the newer one has no clue what value is. - if (oldItem.getDescription() != null && item.getDescription() == null - && oldItem.getEventId() == item.getEventId() - && oldItem.getStartTime() == item.getStartTime() - && oldItem.getLengthInSecond() == item.getLengthInSecond() - && Objects.equals(oldItem.getContentRating(), item.getContentRating()) - && Objects.equals(oldItem.getBroadcastGenre(), item.getBroadcastGenre()) - && Objects.equals(oldItem.getCanonicalGenre(), item.getCanonicalGenre())) { - item.setDescription(oldItem.getDescription()); - } - if (item.compareTo(oldItem) != 0) { - ops.add(buildContentProviderOperation(ContentProviderOperation.newUpdate( - TvContract.buildProgramUri(oldItem.getProgramId())), item, null)); - if (ops.size() >= BATCH_OPERATION_COUNT) { - applyBatch(channel.getName(), ops); - ops.clear(); - } - } - newEitItemMap.remove(item.getEventId()); - } - for (EitItem unverifiedOldItems : outdatedOldItems) { - if (unverifiedOldItems.getStartTimeUtcMillis() > currentTime) { - // The given new EIT item list covers partial time span of EPG. Here, we delete old - // item only when it has an overlapping with the new EIT item list. - long startTime = unverifiedOldItems.getStartTimeUtcMillis(); - long endTime = unverifiedOldItems.getEndTimeUtcMillis(); - for (EitItem item : newEitItemMap.values()) { - long newItemStartTime = item.getStartTimeUtcMillis(); - long newItemEndTime = item.getEndTimeUtcMillis(); - if ((startTime >= newItemStartTime && startTime < newItemEndTime) - || (endTime > newItemStartTime && endTime <= newItemEndTime)) { - ops.add(ContentProviderOperation.newDelete(TvContract.buildProgramUri( - unverifiedOldItems.getProgramId())).build()); - if (ops.size() >= BATCH_OPERATION_COUNT) { - applyBatch(channel.getName(), ops); - ops.clear(); - } - break; - } - } - } - } - for (EitItem item : newEitItemMap.values()) { - if (item.getEndTimeUtcMillis() < currentTime) { - continue; - } - ops.add(buildContentProviderOperation(ContentProviderOperation.newInsert( - TvContract.Programs.CONTENT_URI), item, channel)); - if (ops.size() >= BATCH_OPERATION_COUNT) { - applyBatch(channel.getName(), ops); - ops.clear(); - } - } - - applyBatch(channel.getName(), ops); - } - - private ContentProviderOperation buildContentProviderOperation( - ContentProviderOperation.Builder builder, EitItem item, TunerChannel channel) { - if (channel != null) { - builder.withValue(TvContract.Programs.COLUMN_CHANNEL_ID, channel.getChannelId()); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - builder.withValue(TvContract.Programs.COLUMN_RECORDING_PROHIBITED, - channel.isRecordingProhibited() ? 1 : 0); - } - } - if (item != null) { - builder.withValue(TvContract.Programs.COLUMN_TITLE, item.getTitleText()) - .withValue(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, - item.getStartTimeUtcMillis()) - .withValue(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, - item.getEndTimeUtcMillis()) - .withValue(TvContract.Programs.COLUMN_CONTENT_RATING, - item.getContentRating()) - .withValue(TvContract.Programs.COLUMN_AUDIO_LANGUAGE, - item.getAudioLanguage()) - .withValue(TvContract.Programs.COLUMN_SHORT_DESCRIPTION, - item.getDescription()) - .withValue(TvContract.Programs.COLUMN_VERSION_NUMBER, - item.getEventId()); - } - return builder.build(); - } - - private void applyBatch(String channelName, ArrayList<ContentProviderOperation> operations) { - try { - mContext.getContentResolver().applyBatch(TvContract.AUTHORITY, operations); - } catch (RemoteException | OperationApplicationException e) { - Log.e(TAG, "Error updating EPG " + channelName, e); - } - } - - private void handleChannel(TunerChannel channel) { - long channelId = getChannelId(channel); - ContentValues values = new ContentValues(); - values.put(TvContract.Channels.COLUMN_NETWORK_AFFILIATION, channel.getShortName()); - values.put(TvContract.Channels.COLUMN_SERVICE_TYPE, channel.getServiceTypeName()); - values.put(TvContract.Channels.COLUMN_TRANSPORT_STREAM_ID, channel.getTsid()); - values.put(TvContract.Channels.COLUMN_DISPLAY_NUMBER, channel.getDisplayNumber()); - 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); - values.put(TvContract.Channels.COLUMN_TYPE, "QAM256".equals(channel.getModulation()) - ? TvContract.Channels.TYPE_ATSC_C : TvContract.Channels.TYPE_ATSC_T); - values.put(TvContract.Channels.COLUMN_SERVICE_ID, channel.getProgramNumber()); - - // ATSC doesn't have original_network_id - values.put(TvContract.Channels.COLUMN_ORIGINAL_NETWORK_ID, channel.getFrequency()); - - Uri channelUri = mContext.getContentResolver().insert(TvContract.Channels.CONTENT_URI, - values); - channelId = ContentUris.parseId(channelUri); - } else { - mContext.getContentResolver().update( - TvContract.buildChannelUri(channelId), values, null, null); - } - channel.setChannelId(channelId); - mTunerChannelMap.put(channelId, channel); - mTunerChannelIdMap.put(channel, channelId); - if (mIsScanning.get()) { - mScannedChannels.add(channel); - mPreviousScannedChannels.remove(channel); - } - if (mListener != null) { - mListener.onChannelArrived(channel); - } - } - - private void clearChannels() { - int count = mContext.getContentResolver().delete(mChannelsUri, null, null); - if (count > 0) { - // We have just deleted obsolete data. Now tell the user that he or she needs - // to perform the auto-scan again. - if (mListener != null) { - mListener.onRescanNeeded(); - } - } - } - - private void checkVersion() { - 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; - } - } - } - } - } - } - - private long getChannelId(TunerChannel channel) { - Long channelId = mTunerChannelIdMap.get(channel); - if (channelId != null) { - return channelId; - } - mHandler.sendEmptyMessage(MSG_BUILD_CHANNEL_MAP); - try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri, - CHANNEL_DATA_SELECTION_ARGS, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - do { - channelId = cursor.getLong(0); - byte[] providerData = cursor.getBlob(1); - TunerChannel tunerChannel = TunerChannel.parseFrom(providerData); - if (tunerChannel != null && tunerChannel.compareTo(channel) == 0) { - channel.setChannelId(channelId); - mTunerChannelIdMap.put(channel, channelId); - mTunerChannelMap.put(channelId, channel); - return channelId; - } - } while (cursor.moveToNext()); - } - } - return -1; - } - - private List<EitItem> getAllProgramsForChannel(TunerChannel channel) { - return getAllProgramsForChannel(channel, null, null); - } - - private List<EitItem> getAllProgramsForChannel(TunerChannel channel, @Nullable Long startTimeMs, - @Nullable Long endTimeMs) { - List<EitItem> items = new ArrayList<>(); - long channelId = channel.getChannelId(); - Uri programsUri = (startTimeMs == null || endTimeMs == null) ? - TvContract.buildProgramsUriForChannel(channelId) : - TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs); - try (Cursor cursor = mContext.getContentResolver().query(programsUri, - ALL_PROGRAMS_SELECTION_ARGS, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - do { - long id = cursor.getLong(0); - String titleText = cursor.getString(1); - long startTime = ConvertUtils.convertUnixEpochToGPSTime( - cursor.getLong(2) / DateUtils.SECOND_IN_MILLIS); - long endTime = ConvertUtils.convertUnixEpochToGPSTime( - cursor.getLong(3) / DateUtils.SECOND_IN_MILLIS); - int lengthInSecond = (int) (endTime - startTime); - String contentRating = cursor.getString(4); - String broadcastGenre = cursor.getString(5); - String canonicalGenre = cursor.getString(6); - String description = cursor.getString(7); - int eventId = cursor.getInt(8); - EitItem eitItem = new EitItem(id, eventId, titleText, startTime, lengthInSecond, - contentRating, null, null, broadcastGenre, canonicalGenre, description); - items.add(eitItem); - } while (cursor.moveToNext()); - } - } - return items; - } - - private void buildChannelMap() { - ArrayList<TunerChannel> channels = new ArrayList<>(); - try (Cursor cursor = mContext.getContentResolver().query(mChannelsUri, - CHANNEL_DATA_SELECTION_ARGS, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - do { - long channelId = cursor.getLong(0); - byte[] data = cursor.getBlob(1); - TunerChannel channel = TunerChannel.parseFrom(data); - if (channel != null) { - channel.setChannelId(channelId); - channels.add(channel); - } - } while (cursor.moveToNext()); - } - } - mTunerChannelMap.clear(); - mTunerChannelIdMap.clear(); - for (TunerChannel channel : channels) { - mTunerChannelMap.put(channel.getChannelId(), channel); - mTunerChannelIdMap.put(channel, channel.getChannelId()); - } - } - - private static class ChannelEvent { - public final TunerChannel channel; - public final List<EitItem> eitItems; - - public ChannelEvent(TunerChannel channel, List<EitItem> eitItems) { - this.channel = channel; - this.eitItems = eitItems; - } - } -} diff --git a/src/com/android/tv/tuner/tvinput/EventDetector.java b/src/com/android/tv/tuner/tvinput/EventDetector.java deleted file mode 100644 index dc99118a..00000000 --- a/src/com/android/tv/tuner/tvinput/EventDetector.java +++ /dev/null @@ -1,334 +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.tvinput; - -import android.util.Log; -import android.util.SparseArray; -import android.util.SparseBooleanArray; - -import com.android.tv.tuner.TunerHal; -import com.android.tv.tuner.ts.TsParser; -import com.android.tv.tuner.data.PsiData; -import com.android.tv.tuner.data.PsipData; -import com.android.tv.tuner.data.TunerChannel; -import com.android.tv.tuner.data.nano.Track.AtscAudioTrack; -import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * Detects channels and programs that are emerged or changed while parsing ATSC PSIP information. - */ -public class EventDetector { - private static final String TAG = "EventDetector"; - private static final boolean DEBUG = false; - public static final int ALL_PROGRAM_NUMBERS = -1; - - private final TunerHal mTunerHal; - - private TsParser mTsParser; - private final Set<Integer> mPidSet = new HashSet<>(); - - // To prevent channel duplication - private final Set<Integer> mVctProgramNumberSet = new HashSet<>(); - private final Set<Integer> mSdtProgramNumberSet = new HashSet<>(); - private final SparseArray<TunerChannel> mChannelMap = new SparseArray<>(); - private final SparseBooleanArray mVctCaptionTracksFound = new SparseBooleanArray(); - private final SparseBooleanArray mEitCaptionTracksFound = new SparseBooleanArray(); - private final List<EventListener> mEventListeners = new ArrayList<>(); - private int mFrequency; - private String mModulation; - private int mProgramNumber = ALL_PROGRAM_NUMBERS; - - private final TsParser.TsOutputListener mTsOutputListener = new TsParser.TsOutputListener() { - @Override - public void onPatDetected(List<PsiData.PatItem> items) { - for (PsiData.PatItem i : items) { - if (mProgramNumber == ALL_PROGRAM_NUMBERS || mProgramNumber == i.getProgramNo()) { - mTunerHal.addPidFilter(i.getPmtPid(), TunerHal.FILTER_TYPE_OTHER); - } - } - } - - @Override - public void onEitPidDetected(int pid) { - startListening(pid); - } - - @Override - public void onEitItemParsed(PsipData.VctItem channel, List<PsipData.EitItem> items) { - TunerChannel tunerChannel = mChannelMap.get(channel.getProgramNumber()); - if (DEBUG) { - Log.d(TAG, "onEitItemParsed tunerChannel:" + tunerChannel + " " - + channel.getProgramNumber()); - } - int channelSourceId = channel.getSourceId(); - - // Source id 0 is useful for cases where a cable operator wishes to define a channel for - // which no EPG data is currently available. - // We don't handle such a case. - if (channelSourceId == 0) { - return; - } - - // If at least a one caption track have been found in EIT items for the given channel, - // we starts to interpret the zero tracks as a clearance of the caption tracks. - boolean captionTracksFound = mEitCaptionTracksFound.get(channelSourceId); - for (PsipData.EitItem item : items) { - if (captionTracksFound) { - break; - } - List<AtscCaptionTrack> captionTracks = item.getCaptionTracks(); - if (captionTracks != null && !captionTracks.isEmpty()) { - captionTracksFound = true; - } - } - mEitCaptionTracksFound.put(channelSourceId, captionTracksFound); - if (captionTracksFound) { - for (PsipData.EitItem item : items) { - item.setHasCaptionTrack(); - } - } - if (tunerChannel != null && !mEventListeners.isEmpty()) { - for (EventListener eventListener : mEventListeners) { - eventListener.onEventDetected(tunerChannel, items); - } - } - } - - @Override - public void onEttPidDetected(int pid) { - startListening(pid); - } - - @Override - public void onAllVctItemsParsed() { - if (!mEventListeners.isEmpty()) { - for (EventListener eventListener : mEventListeners) { - eventListener.onChannelScanDone(); - } - } - } - - @Override - public void onVctItemParsed(PsipData.VctItem channel, List<PsiData.PmtItem> pmtItems) { - if (DEBUG) { - Log.d(TAG, "onVctItemParsed VCT " + channel); - Log.d(TAG, " PMT " + pmtItems); - } - - // Merges the audio and caption tracks located in PMT items into the tracks of the given - // tuner channel. - TunerChannel tunerChannel = new TunerChannel(channel, pmtItems); - List<AtscAudioTrack> audioTracks = new ArrayList<>(); - List<AtscCaptionTrack> captionTracks = new ArrayList<>(); - for (PsiData.PmtItem pmtItem : pmtItems) { - if (pmtItem.getAudioTracks() != null) { - audioTracks.addAll(pmtItem.getAudioTracks()); - } - if (pmtItem.getCaptionTracks() != null) { - captionTracks.addAll(pmtItem.getCaptionTracks()); - } - } - int channelProgramNumber = channel.getProgramNumber(); - - // If at least a one caption track have been found in VCT items for the given channel, - // we starts to interpret the zero tracks as a clearance of the caption tracks. - boolean captionTracksFound = mVctCaptionTracksFound.get(channelProgramNumber) - || !captionTracks.isEmpty(); - mVctCaptionTracksFound.put(channelProgramNumber, captionTracksFound); - if (captionTracksFound) { - tunerChannel.setHasCaptionTrack(); - } - tunerChannel.setAudioTracks(audioTracks); - tunerChannel.setCaptionTracks(captionTracks); - tunerChannel.setFrequency(mFrequency); - tunerChannel.setModulation(mModulation); - mChannelMap.put(tunerChannel.getProgramNumber(), tunerChannel); - boolean found = mVctProgramNumberSet.contains(channelProgramNumber); - if (!found) { - mVctProgramNumberSet.add(channelProgramNumber); - } - if (!mEventListeners.isEmpty()) { - for (EventListener eventListener : mEventListeners) { - eventListener.onChannelDetected(tunerChannel, !found); - } - } - } - - @Override - public void onSdtItemParsed(PsipData.SdtItem channel, List<PsiData.PmtItem> pmtItems) { - if (DEBUG) { - Log.d(TAG, "onSdtItemParsed SDT " + channel); - Log.d(TAG, " PMT " + pmtItems); - } - - // Merges the audio and caption tracks located in PMT items into the tracks of the given - // tuner channel. - TunerChannel tunerChannel = new TunerChannel(channel, pmtItems); - List<AtscAudioTrack> audioTracks = new ArrayList<>(); - List<AtscCaptionTrack> captionTracks = new ArrayList<>(); - for (PsiData.PmtItem pmtItem : pmtItems) { - if (pmtItem.getAudioTracks() != null) { - audioTracks.addAll(pmtItem.getAudioTracks()); - } - if (pmtItem.getCaptionTracks() != null) { - captionTracks.addAll(pmtItem.getCaptionTracks()); - } - } - int channelProgramNumber = channel.getServiceId(); - tunerChannel.setAudioTracks(audioTracks); - tunerChannel.setCaptionTracks(captionTracks); - tunerChannel.setFrequency(mFrequency); - tunerChannel.setModulation(mModulation); - mChannelMap.put(tunerChannel.getProgramNumber(), tunerChannel); - boolean found = mSdtProgramNumberSet.contains(channelProgramNumber); - if (!found) { - mSdtProgramNumberSet.add(channelProgramNumber); - } - if (!mEventListeners.isEmpty()) { - for (EventListener eventListener : mEventListeners) { - eventListener.onChannelDetected(tunerChannel, !found); - } - } - } - }; - - /** - * Listener for detecting ATSC TV channels and receiving EPG data. - */ - public interface EventListener { - - /** - * Fired when new information of an ATSC TV channel arrived. - * - * @param channel an ATSC TV channel - * @param channelArrivedAtFirstTime tells whether this channel arrived at first time - */ - void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime); - - /** - * Fired when new program events of an ATSC TV channel arrived. - * - * @param channel an ATSC TV channel - * @param items a list of EIT items that were received - */ - void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items); - - /** - * Fired when information of all detectable ATSC TV channels in current frequency arrived. - */ - void onChannelScanDone(); - } - - /** - * Creates a detector for ATSC TV channles and program information. - * - * @param usbTunerInteface {@link TunerHal} - */ - public EventDetector(TunerHal usbTunerInteface) { - mTunerHal = usbTunerInteface; - } - - private void reset() { - // TODO: Use TsParser.reset() - int deliverySystemType = mTunerHal.getDeliverySystemType(); - mTsParser = - new TsParser( - mTsOutputListener, - TunerHal.isDvbDeliverySystem(mTunerHal.getDeliverySystemType())); - mPidSet.clear(); - mVctProgramNumberSet.clear(); - mSdtProgramNumberSet.clear(); - mVctCaptionTracksFound.clear(); - mEitCaptionTracksFound.clear(); - mChannelMap.clear(); - } - - /** - * Starts detecting channel and program information. - * - * @param frequency The frequency to listen to. - * @param modulation The modulation type. - * @param programNumber The program number if this is for handling tune request. For scanning - * purpose, supply {@link #ALL_PROGRAM_NUMBERS}. - */ - public void startDetecting(int frequency, String modulation, int programNumber) { - reset(); - mFrequency = frequency; - mModulation = modulation; - mProgramNumber = programNumber; - } - - private void startListening(int pid) { - if (mPidSet.contains(pid)) { - return; - } - mPidSet.add(pid); - mTunerHal.addPidFilter(pid, TunerHal.FILTER_TYPE_OTHER); - } - - /** - * Feeds ATSC TS stream to detect channel and program information. - * @param data buffer for ATSC TS stream - * @param startOffset the offset where buffer starts - * @param length The length of available data - */ - public void feedTSStream(byte[] data, int startOffset, int length) { - if (mPidSet.isEmpty()) { - startListening(TsParser.ATSC_SI_BASE_PID); - } - if (mTsParser != null) { - mTsParser.feedTSData(data, startOffset, length); - } - } - - /** - * Retrieves the channel information regardless of being well-formed. - * @return {@link List} of {@link TunerChannel} - */ - 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 deleted file mode 100644 index 99222bf8..00000000 --- a/src/com/android/tv/tuner/tvinput/FileSourceEventDetector.java +++ /dev/null @@ -1,249 +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.tvinput; - -import android.util.Log; -import android.util.SparseArray; -import android.util.SparseBooleanArray; - -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.SdtItem; -import com.android.tv.tuner.data.PsipData.VctItem; -import com.android.tv.tuner.data.TunerChannel; -import com.android.tv.tuner.data.nano.Track.AtscAudioTrack; -import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; -import com.android.tv.tuner.source.FileTsStreamer; -import com.android.tv.tuner.ts.TsParser; -import com.android.tv.tuner.tvinput.EventDetector.EventListener; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * PSIP event detector for a file source. - * - * <p>Uses {@link TsParser} to analyze input MPEG-2 transport stream, detects and reports - * various PSIP-related events via {@link TsParser.TsOutputListener}. - */ -public class FileSourceEventDetector { - private static final String TAG = "FileSourceEventDetector"; - private static final boolean DEBUG = true; - public static final int ALL_PROGRAM_NUMBERS = 0; - - private TsParser mTsParser; - private final Set<Integer> mVctProgramNumberSet = new HashSet<>(); - private final Set<Integer> mSdtProgramNumberSet = new HashSet<>(); - 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 boolean mEnableDvbSignal; - private FileTsStreamer.StreamProvider mStreamProvider; - private int mProgramNumber = ALL_PROGRAM_NUMBERS; - - public FileSourceEventDetector(EventDetector.EventListener listener, boolean enableDvbSignal) { - mEventListener = listener; - mEnableDvbSignal = enableDvbSignal; - } - - /** - * Starts detecting channel and program information. - * - * @param provider MPEG-2 transport stream source. - * @param programNumber The program number if this is for handling tune request. For scanning - * purpose, supply {@link #ALL_PROGRAM_NUMBERS}. - */ - public void start(FileTsStreamer.StreamProvider provider, int programNumber) { - mStreamProvider = provider; - mProgramNumber = programNumber; - reset(); - } - - private void reset() { - mTsParser = new TsParser(mTsOutputListener, mEnableDvbSignal); // TODO: Use TsParser.reset() - mStreamProvider.clearPidFilter(); - mVctProgramNumberSet.clear(); - mSdtProgramNumberSet.clear(); - mVctCaptionTracksFound.clear(); - mEitCaptionTracksFound.clear(); - mChannelMap.clear(); - } - - public void feedTSStream(byte[] data, int startOffset, int length) { - if (mStreamProvider.isFilterEmpty()) { - startListening(TsParser.ATSC_SI_BASE_PID); - startListening(TsParser.PAT_PID); - } - if (mTsParser != null) { - mTsParser.feedTSData(data, startOffset, length); - } - } - - private void startListening(int pid) { - if (mStreamProvider.isInFilter(pid)) { - return; - } - mStreamProvider.addPidFilter(pid); - } - - private final TsParser.TsOutputListener mTsOutputListener = new TsParser.TsOutputListener() { - @Override - public void onPatDetected(List<PatItem> items) { - for (PatItem i : items) { - if (mProgramNumber == ALL_PROGRAM_NUMBERS || mProgramNumber == i.getProgramNo()) { - mStreamProvider.addPidFilter(i.getPmtPid()); - } - } - } - - @Override - public void onEitPidDetected(int pid) { - startListening(pid); - } - - @Override - public void onEitItemParsed(VctItem channel, List<EitItem> items) { - TunerChannel tunerChannel = mChannelMap.get(channel.getProgramNumber()); - if (DEBUG) { - Log.d(TAG, "onEitItemParsed tunerChannel:" + tunerChannel + " " - + channel.getProgramNumber()); - } - int channelSourceId = channel.getSourceId(); - - // Source id 0 is useful for cases where a cable operator wishes to define a channel for - // which no EPG data is currently available. - // We don't handle such a case. - if (channelSourceId == 0) { - return; - } - - // If at least a one caption track have been found in EIT items for the given channel, - // we starts to interpret the zero tracks as a clearance of the caption tracks. - boolean captionTracksFound = mEitCaptionTracksFound.get(channelSourceId); - for (EitItem item : items) { - if (captionTracksFound) { - break; - } - List<AtscCaptionTrack> captionTracks = item.getCaptionTracks(); - if (captionTracks != null && !captionTracks.isEmpty()) { - captionTracksFound = true; - } - } - mEitCaptionTracksFound.put(channelSourceId, captionTracksFound); - if (captionTracksFound) { - for (EitItem item : items) { - item.setHasCaptionTrack(); - } - } - if (tunerChannel != null && mEventListener != null) { - mEventListener.onEventDetected(tunerChannel, items); - } - } - - @Override - public void onEttPidDetected(int pid) { - startListening(pid); - } - - @Override - public void onAllVctItemsParsed() { - // do nothing. - } - - @Override - public void onVctItemParsed(VctItem channel, List<PmtItem> pmtItems) { - if (DEBUG) { - Log.d(TAG, "onVctItemParsed VCT " + channel); - Log.d(TAG, " PMT " + pmtItems); - } - - // Merges the audio and caption tracks located in PMT items into the tracks of the given - // tuner channel. - TunerChannel tunerChannel = TunerChannel.forFile(channel, pmtItems); - List<AtscAudioTrack> audioTracks = new ArrayList<>(); - List<AtscCaptionTrack> captionTracks = new ArrayList<>(); - for (PmtItem pmtItem : pmtItems) { - if (pmtItem.getAudioTracks() != null) { - audioTracks.addAll(pmtItem.getAudioTracks()); - } - if (pmtItem.getCaptionTracks() != null) { - captionTracks.addAll(pmtItem.getCaptionTracks()); - } - } - int channelProgramNumber = channel.getProgramNumber(); - - // If at least a one caption track have been found in VCT items for the given channel, - // we starts to interpret the zero tracks as a clearance of the caption tracks. - boolean captionTracksFound = mVctCaptionTracksFound.get(channelProgramNumber) - || !captionTracks.isEmpty(); - mVctCaptionTracksFound.put(channelProgramNumber, captionTracksFound); - if (captionTracksFound) { - tunerChannel.setHasCaptionTrack(); - } - tunerChannel.setFilepath(mStreamProvider.getFilepath()); - tunerChannel.setAudioTracks(audioTracks); - tunerChannel.setCaptionTracks(captionTracks); - - mChannelMap.put(tunerChannel.getProgramNumber(), tunerChannel); - boolean found = mVctProgramNumberSet.contains(channelProgramNumber); - if (!found) { - mVctProgramNumberSet.add(channelProgramNumber); - } - if (mEventListener != null) { - mEventListener.onChannelDetected(tunerChannel, !found); - } - } - - @Override - public void onSdtItemParsed(SdtItem channel, List<PmtItem> pmtItems) { - if (DEBUG) { - Log.d(TAG, "onSdtItemParsed SDT " + channel); - Log.d(TAG, " PMT " + pmtItems); - } - - // Merges the audio and caption tracks located in PMT items into the tracks of the given - // tuner channel. - TunerChannel tunerChannel = TunerChannel.forDvbFile(channel, pmtItems); - List<AtscAudioTrack> audioTracks = new ArrayList<>(); - List<AtscCaptionTrack> captionTracks = new ArrayList<>(); - for (PmtItem pmtItem : pmtItems) { - if (pmtItem.getAudioTracks() != null) { - audioTracks.addAll(pmtItem.getAudioTracks()); - } - if (pmtItem.getCaptionTracks() != null) { - captionTracks.addAll(pmtItem.getCaptionTracks()); - } - } - int channelProgramNumber = channel.getServiceId(); - tunerChannel.setFilepath(mStreamProvider.getFilepath()); - tunerChannel.setAudioTracks(audioTracks); - tunerChannel.setCaptionTracks(captionTracks); - mChannelMap.put(tunerChannel.getProgramNumber(), tunerChannel); - boolean found = mSdtProgramNumberSet.contains(channelProgramNumber); - if (!found) { - mSdtProgramNumberSet.add(channelProgramNumber); - } - if (mEventListener != null) { - mEventListener.onChannelDetected(tunerChannel, !found); - } - } - }; -} diff --git a/src/com/android/tv/tuner/tvinput/PlaybackBufferListener.java b/src/com/android/tv/tuner/tvinput/PlaybackBufferListener.java deleted file mode 100644 index 3908fe6c..00000000 --- a/src/com/android/tv/tuner/tvinput/PlaybackBufferListener.java +++ /dev/null @@ -1,42 +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.tvinput; - -/** - * The listener for buffer events occurred during playback. - */ -public interface PlaybackBufferListener { - - /** - * Invoked when the start position of the buffer has been changed. - * - * @param startTimeMs the new start time of the buffer in millisecond - */ - void onBufferStartTimeChanged(long startTimeMs); - - /** - * Invoked when the state of the buffer has been changed. - * - * @param available whether the buffer is available or not - */ - void onBufferStateChanged(boolean available); - - /** - * Invoked when the disk speed is too slow to write the buffers. - */ - void onDiskTooSlow(); -} diff --git a/src/com/android/tv/tuner/tvinput/TunerDebug.java b/src/com/android/tv/tuner/tvinput/TunerDebug.java deleted file mode 100644 index 2ddc946a..00000000 --- a/src/com/android/tv/tuner/tvinput/TunerDebug.java +++ /dev/null @@ -1,150 +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.tvinput; - -import android.os.SystemClock; -import android.util.Log; - -/** - * A class to maintain various debugging information. - */ -public class TunerDebug { - private static final String TAG = "TunerDebug"; - public static final boolean ENABLED = false; - - private int mVideoFrameDrop; - private int mBytesInQueue; - - private long mAudioPositionUs; - private long mAudioPtsUs; - private long mVideoPtsUs; - - private long mLastAudioPositionUs; - private long mLastAudioPtsUs; - private long mLastVideoPtsUs; - private long mLastCheckTimestampMs; - - private long mAudioPositionUsRate; - private long mAudioPtsUsRate; - private long mVideoPtsUsRate; - - private TunerDebug() { - mVideoFrameDrop = 0; - mLastCheckTimestampMs = SystemClock.elapsedRealtime(); - } - - private static class LazyHolder { - private static final TunerDebug INSTANCE = new TunerDebug(); - } - - public static TunerDebug getInstance() { - return LazyHolder.INSTANCE; - } - - public static void notifyVideoFrameDrop(int count, long delta) { - // TODO: provide timestamp mismatch information using delta - TunerDebug sTunerDebug = getInstance(); - sTunerDebug.mVideoFrameDrop += count; - } - - public static int getVideoFrameDrop() { - TunerDebug sTunerDebug = getInstance(); - int videoFrameDrop = sTunerDebug.mVideoFrameDrop; - if (videoFrameDrop > 0) { - Log.d(TAG, "Dropped video frame: " + videoFrameDrop); - } - sTunerDebug.mVideoFrameDrop = 0; - return videoFrameDrop; - } - - public static void setBytesInQueue(int bytesInQueue) { - TunerDebug sTunerDebug = getInstance(); - sTunerDebug.mBytesInQueue = bytesInQueue; - } - - public static int getBytesInQueue() { - TunerDebug sTunerDebug = getInstance(); - return sTunerDebug.mBytesInQueue; - } - - public static void setAudioPositionUs(long audioPositionUs) { - TunerDebug sTunerDebug = getInstance(); - sTunerDebug.mAudioPositionUs = audioPositionUs; - } - - public static long getAudioPositionUs() { - TunerDebug sTunerDebug = getInstance(); - return sTunerDebug.mAudioPositionUs; - } - - public static void setAudioPtsUs(long audioPtsUs) { - TunerDebug sTunerDebug = getInstance(); - sTunerDebug.mAudioPtsUs = audioPtsUs; - } - - public static long getAudioPtsUs() { - TunerDebug sTunerDebug = getInstance(); - return sTunerDebug.mAudioPtsUs; - } - - public static void setVideoPtsUs(long videoPtsUs) { - TunerDebug sTunerDebug = getInstance(); - sTunerDebug.mVideoPtsUs = videoPtsUs; - } - - public static long getVideoPtsUs() { - TunerDebug sTunerDebug = getInstance(); - return sTunerDebug.mVideoPtsUs; - } - - public static void calculateDiff() { - TunerDebug sTunerDebug = getInstance(); - long currentTime = SystemClock.elapsedRealtime(); - long duration = currentTime - sTunerDebug.mLastCheckTimestampMs; - if (duration != 0) { - sTunerDebug.mAudioPositionUsRate = - (sTunerDebug.mAudioPositionUs - sTunerDebug.mLastAudioPositionUs) * 1000 - / duration; - sTunerDebug.mAudioPtsUsRate = - (sTunerDebug.mAudioPtsUs - sTunerDebug.mLastAudioPtsUs) * 1000 - / duration; - sTunerDebug.mVideoPtsUsRate = - (sTunerDebug.mVideoPtsUs - sTunerDebug.mLastVideoPtsUs) * 1000 - / duration; - } - - sTunerDebug.mLastAudioPositionUs = sTunerDebug.mAudioPositionUs; - sTunerDebug.mLastAudioPtsUs = sTunerDebug.mAudioPtsUs; - sTunerDebug.mLastVideoPtsUs = sTunerDebug.mVideoPtsUs; - sTunerDebug.mLastCheckTimestampMs = currentTime; - } - - public static long getAudioPositionUsRate() { - TunerDebug sTunerDebug = getInstance(); - return sTunerDebug.mAudioPositionUsRate; - } - - public static long getAudioPtsUsRate() { - TunerDebug sTunerDebug = getInstance(); - return sTunerDebug.mAudioPtsUsRate; - } - - public static long getVideoPtsUsRate() { - TunerDebug sTunerDebug = getInstance(); - return sTunerDebug.mVideoPtsUsRate; - } -} diff --git a/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java b/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java deleted file mode 100644 index acdd149f..00000000 --- a/src/com/android/tv/tuner/tvinput/TunerRecordingSession.java +++ /dev/null @@ -1,104 +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.tvinput; - -import android.content.Context; -import android.media.tv.TvInputManager; -import android.media.tv.TvInputService; -import android.net.Uri; -import android.support.annotation.MainThread; -import android.support.annotation.Nullable; -import android.support.annotation.WorkerThread; -import android.util.Log; - -/** - * Processes DVR recordings, and deletes the previously recorded contents. - */ -public class TunerRecordingSession extends TvInputService.RecordingSession { - private static final String TAG = "TunerRecordingSession"; - private static final boolean DEBUG = false; - - private final TunerRecordingSessionWorker mSessionWorker; - - public TunerRecordingSession(Context context, String inputId, - ChannelDataManager channelDataManager) { - super(context); - mSessionWorker = new TunerRecordingSessionWorker(context, inputId, channelDataManager, - this); - } - - // RecordingSession - @MainThread - @Override - public void onTune(Uri channelUri) { - // TODO(dvr): support calling more than once, http://b/27171225 - if (DEBUG) { - Log.d(TAG, "Requesting recording session tune: " + channelUri); - } - mSessionWorker.tune(channelUri); - } - - @MainThread - @Override - public void onRelease() { - if (DEBUG) { - Log.d(TAG, "Requesting recording session release."); - } - mSessionWorker.release(); - } - - @MainThread - @Override - public void onStartRecording(@Nullable Uri programUri) { - if (DEBUG) { - Log.d(TAG, "Requesting start recording."); - } - mSessionWorker.startRecording(programUri); - } - - @MainThread - @Override - public void onStopRecording() { - if (DEBUG) { - Log.d(TAG, "Requesting stop recording."); - } - mSessionWorker.stopRecording(); - } - - // Called from TunerRecordingSessionImpl in a worker thread. - @WorkerThread - public void onTuned(Uri channelUri) { - if (DEBUG) { - Log.d(TAG, "Notifying recording session tuned."); - } - notifyTuned(channelUri); - } - - @WorkerThread - public void onRecordFinished(final Uri recordedProgramUri) { - if (DEBUG) { - Log.d(TAG, "Notifying record successfully finished."); - } - notifyRecordingStopped(recordedProgramUri); - } - - @WorkerThread - public void onError(int reason) { - Log.w(TAG, "Notifying recording error: " + reason); - notifyError(reason); - } -} diff --git a/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java b/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java deleted file mode 100644 index 34013bf1..00000000 --- a/src/com/android/tv/tuner/tvinput/TunerRecordingSessionWorker.java +++ /dev/null @@ -1,662 +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.tvinput; - -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.media.tv.TvContract; -import android.media.tv.TvInputManager; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Message; -import android.support.annotation.IntDef; -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.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.TunerChannel; -import com.android.tv.tuner.data.nano.Track.AtscCaptionTrack; -import com.android.tv.tuner.exoplayer.ExoPlayerSampleExtractor; -import com.android.tv.tuner.exoplayer.SampleExtractor; -import com.android.tv.tuner.exoplayer.buffer.BufferManager; -import com.android.tv.tuner.exoplayer.buffer.DvrStorageManager; -import com.android.tv.tuner.source.TsDataSource; -import com.android.tv.tuner.source.TsDataSourceManager; -import com.android.tv.util.Utils; - -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.TimeUnit; - -/** - * Implements a DVR feature. - */ -public class TunerRecordingSessionWorker implements PlaybackBufferListener, - EventDetector.EventListener, SampleExtractor.OnCompletionListener, - Handler.Callback { - private static final String TAG = "TunerRecordingSessionW"; - private static final boolean DEBUG = false; - - private static final String SORT_BY_TIME = TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS - + ", " + TvContract.Programs.COLUMN_CHANNEL_ID + ", " - + TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS; - private static final 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; - private static final int MSG_TUNE = 1; - private static final int MSG_START_RECORDING = 2; - private static final int MSG_PREPARE_RECODER = 3; - 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_TUNING, STATE_TUNED, STATE_RECORDING}) - @Retention(RetentionPolicy.SOURCE) - public @interface DvrSessionState {} - private static final int STATE_IDLE = 1; - 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; - private final DvrStorageStatusManager mDvrStorageStatusManager; - private final Handler mHandler; - private final TsDataSourceManager mSourceManager; - private final Random mRandom = new Random(); - - private TsDataSource mTunerSource; - private TunerChannel mChannel; - private File mStorageDir; - private long mRecordStartTime; - private long mRecordEndTime; - private boolean mRecorderRunning; - 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()); - mContext = context; - HandlerThread handlerThread = new HandlerThread(TAG); - handlerThread.start(); - mHandler = new Handler(handlerThread.getLooper(), this); - mDvrStorageStatusManager = - TvApplication.getSingletons(context).getDvrStorageStatusManager(); - mChannelDataManager = dataManager; - mChannelDataManager.checkDataVersion(context); - mSourceManager = TsDataSourceManager.createSourceManager(true); - mCapabilities = new DvbDeviceAccessor(context).getRecordingCapability(inputId); - mInputId = inputId; - if (DEBUG) Log.d(TAG, mCapabilities.toString()); - mSession = session; - } - - // PlaybackBufferListener - @Override - public void onBufferStartTimeChanged(long startTimeMs) { } - - @Override - public void onBufferStateChanged(boolean available) { } - - @Override - public void onDiskTooSlow() { } - - // EventDetector.EventListener - @Override - public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) { - if (mChannel == null || mChannel.compareTo(channel) != 0) { - return; - } - mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime); - } - - @Override - public void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items) { - if (mChannel == null || mChannel.compareTo(channel) != 0) { - return; - } - mHandler.obtainMessage(MSG_UPDATE_CC_INFO, new Pair<>(channel, items)).sendToTarget(); - mChannelDataManager.notifyEventDetected(channel, items); - } - - @Override - public void onChannelScanDone() { - // do nothing. - } - - // SampleExtractor.OnCompletionListener - @Override - public void onCompletion(boolean success, long lastExtractedPositionUs) { - onRecordingResult(success, lastExtractedPositionUs); - reset(); - } - - /** - * Tunes to {@code channelUri}. - */ - @MainThread - public void tune(Uri channelUri) { - mHandler.removeCallbacksAndMessages(null); - mHandler.obtainMessage(MSG_TUNE, 0, 0, channelUri).sendToTarget(); - } - - /** - * Starts recording. - */ - @MainThread - public void startRecording(@Nullable Uri programUri) { - mHandler.obtainMessage(MSG_START_RECORDING, programUri).sendToTarget(); - } - - /** - * Stops recording. - */ - @MainThread - public void stopRecording() { - mHandler.sendEmptyMessage(MSG_STOP_RECORDING); - } - - /** - * Releases all resources. - */ - @MainThread - public void release() { - mHandler.removeCallbacksAndMessages(null); - mHandler.sendEmptyMessage(MSG_RELEASE); - } - - @Override - public boolean handleMessage(Message msg) { - 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)) { - 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; - } - case MSG_START_RECORDING: { - if (DEBUG) Log.d(TAG, "Start recording"); - if (!doStartRecording((Uri) msg.obj)) { - reset(); - } - return true; - } - case MSG_PREPARE_RECODER: { - if (DEBUG) Log.d(TAG, "Preparing recorder"); - if (!mRecorderRunning) { - return true; - } - try { - if (!mRecorder.prepare()) { - mHandler.sendEmptyMessageDelayed(MSG_PREPARE_RECODER, - PREPARE_RECORDER_POLL_MS); - } - } catch (IOException e) { - Log.w(TAG, "Failed to start recording. Couldn't prepare an extractor"); - mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); - reset(); - } - return true; - } - case MSG_STOP_RECORDING: { - if (DEBUG) Log.d(TAG, "Stop recording"); - if (mSessionState != STATE_RECORDING) { - mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); - reset(); - return true; - } - if (mRecorderRunning) { - stopRecorder(); - } - return true; - } - case MSG_MONITOR_STORAGE_STATUS: { - if (mSessionState != STATE_RECORDING) { - return true; - } - if (!mDvrStorageStatusManager.isStorageSufficient()) { - if (mRecorderRunning) { - stopRecorder(); - } - new DeleteRecordingTask().execute(mStorageDir); - mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); - reset(); - } else { - mHandler.sendEmptyMessageDelayed(MSG_MONITOR_STORAGE_STATUS, - STORAGE_MONITOR_INTERVAL_MS); - } - return true; - } - case MSG_RELEASE: { - // Since release was requested, current recording will be cancelled - // without notification. - reset(); - mSourceManager.release(); - mHandler.removeCallbacksAndMessages(null); - 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; - } - - @Nullable - private TunerChannel getChannel(Uri channelUri) { - if (channelUri == null) { - return null; - } - long channelId; - try { - channelId = ContentUris.parseId(channelUri); - } catch (UnsupportedOperationException | NumberFormatException e) { - channelId = CHANNEL_ID_NONE; - } - return (channelId == CHANNEL_ID_NONE) ? null : mChannelDataManager.getChannel(channelId); - } - - private String getStorageKey() { - long prefix = System.currentTimeMillis(); - int suffix = mRandom.nextInt(); - return String.format(Locale.ENGLISH, "%016x_%016x", prefix, suffix); - } - - private void reset() { - if (mRecorder != null) { - mRecorder.release(); - mRecorder = 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 && mSessionState != STATE_TUNING) { - mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); - Log.e(TAG, "Tuning was requested from wrong status."); - return false; - } - mChannel = getChannel(channelUri); - if (mChannel == null) { - 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); - Log.w(TAG, "Tuning failed due to insufficient storage."); - return false; - } - mTunerSource = mSourceManager.createDataSource(mContext, mChannel, this); - if (mTunerSource == null) { - // Retry tuning in this case. - mSessionState = STATE_TUNING; - return true; - } - mSessionState = STATE_TUNED; - return true; - } - - private boolean doStartRecording(@Nullable Uri programUri) { - if (mSessionState != STATE_TUNED) { - mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); - Log.e(TAG, "Recording session status abnormal"); - return false; - } - mStorageDir = mDvrStorageStatusManager.isStorageSufficient() ? - new File(mDvrStorageStatusManager.getRecordingRootDataDirectory(), - getStorageKey()) : null; - if (mStorageDir == null) { - mSession.onError(TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE); - Log.w(TAG, "Failed to start recording due to insufficient storage."); - return false; - } - // Since tuning might be happened a while ago, shifts the start position of tuned source. - mTunerSource.shiftStartPosition(mTunerSource.getBufferedPosition()); - mRecordStartTime = System.currentTimeMillis(); - 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; - mRecorderRunning = true; - mHandler.sendEmptyMessage(MSG_PREPARE_RECODER); - mHandler.removeMessages(MSG_MONITOR_STORAGE_STATUS); - mHandler.sendEmptyMessageDelayed(MSG_MONITOR_STORAGE_STATUS, - STORAGE_MONITOR_INTERVAL_MS); - return true; - } - - private void stopRecorder() { - // Do not change session status. - if (mRecorder != null) { - mRecorder.release(); - mRecordEndTime = System.currentTimeMillis(); - mRecorder = null; - } - mRecorderRunning = false; - mHandler.removeMessages(MSG_MONITOR_STORAGE_STATUS); - 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; - private String mSeriesId; - private final String mSeasonTitle; - private final String mEpisodeTitle; - private final String mSeasonNumber; - private final String mEpisodeNumber; - private final String mDescription; - private final String mPosterArtUri; - private final String mThumbnailUri; - private final String mCanonicalGenres; - private final String mContentRatings; - private final long mStartTimeUtcMillis; - private final long mEndTimeUtcMillis; - private final int mVideoWidth; - private final int mVideoHeight; - private final byte[] mInternalProviderData; - - private static final String[] PROJECTION = { - TvContract.Programs.COLUMN_CHANNEL_ID, - TvContract.Programs.COLUMN_TITLE, - TvContract.Programs.COLUMN_SEASON_TITLE, - TvContract.Programs.COLUMN_EPISODE_TITLE, - TvContract.Programs.COLUMN_SEASON_DISPLAY_NUMBER, - TvContract.Programs.COLUMN_EPISODE_DISPLAY_NUMBER, - TvContract.Programs.COLUMN_SHORT_DESCRIPTION, - TvContract.Programs.COLUMN_POSTER_ART_URI, - TvContract.Programs.COLUMN_THUMBNAIL_URI, - TvContract.Programs.COLUMN_CANONICAL_GENRE, - TvContract.Programs.COLUMN_CONTENT_RATING, - TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, - TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, - TvContract.Programs.COLUMN_VIDEO_WIDTH, - TvContract.Programs.COLUMN_VIDEO_HEIGHT, - TvContract.Programs.COLUMN_INTERNAL_PROVIDER_DATA - }; - - public Program(Cursor cursor) { - int index = 0; - mChannelId = cursor.getLong(index++); - mTitle = cursor.getString(index++); - mSeasonTitle = cursor.getString(index++); - mEpisodeTitle = cursor.getString(index++); - mSeasonNumber = cursor.getString(index++); - mEpisodeNumber = cursor.getString(index++); - mDescription = cursor.getString(index++); - mPosterArtUri = cursor.getString(index++); - mThumbnailUri = cursor.getString(index++); - mCanonicalGenres = cursor.getString(index++); - mContentRatings = cursor.getString(index++); - mStartTimeUtcMillis = cursor.getLong(index++); - mEndTimeUtcMillis = cursor.getLong(index++); - mVideoWidth = cursor.getInt(index++); - mVideoHeight = cursor.getInt(index++); - mInternalProviderData = cursor.getBlob(index++); - SoftPreconditions.checkArgument(index == PROJECTION.length); - } - - public Program(long channelId) { - mChannelId = channelId; - mTitle = "Unknown"; - mSeasonTitle = ""; - mEpisodeTitle = ""; - mSeasonNumber = ""; - mEpisodeNumber = ""; - mDescription = "Unknown"; - mPosterArtUri = null; - mThumbnailUri = null; - mCanonicalGenres = null; - mContentRatings = null; - mStartTimeUtcMillis = 0; - mEndTimeUtcMillis = 0; - mVideoWidth = 0; - mVideoHeight = 0; - mInternalProviderData = null; - } - - public static Program onQuery(Cursor c) { - Program program = null; - if (c != null && c.moveToNext()) { - program = new Program(c); - } - return program; - } - - public ContentValues buildValues() { - ContentValues values = new ContentValues(); - int index = 0; - values.put(PROJECTION[index++], mChannelId); - values.put(PROJECTION[index++], mTitle); - values.put(PROJECTION[index++], mSeasonTitle); - values.put(PROJECTION[index++], mEpisodeTitle); - values.put(PROJECTION[index++], mSeasonNumber); - values.put(PROJECTION[index++], mEpisodeNumber); - values.put(PROJECTION[index++], mDescription); - values.put(PROJECTION[index++], mPosterArtUri); - values.put(PROJECTION[index++], mThumbnailUri); - values.put(PROJECTION[index++], mCanonicalGenres); - values.put(PROJECTION[index++], mContentRatings); - values.put(PROJECTION[index++], mStartTimeUtcMillis); - values.put(PROJECTION[index++], mEndTimeUtcMillis); - values.put(PROJECTION[index++], mVideoWidth); - values.put(PROJECTION[index++], mVideoHeight); - values.put(PROJECTION[index++], mInternalProviderData); - SoftPreconditions.checkArgument(index == PROJECTION.length); - return values; - } - } - - private Program getRecordedProgram() { - ContentResolver resolver = mContext.getContentResolver(); - Uri programUri = mProgramUri; - if (mProgramUri == null) { - long avg = mRecordStartTime / 2 + mRecordEndTime / 2; - programUri = TvContract.buildProgramsUriForChannel(mChannel.getChannelId(), avg, avg); - } - try (Cursor c = resolver.query(programUri, Program.PROJECTION, null, null, SORT_BY_TIME)) { - if (c != null) { - Program result = Program.onQuery(c); - if (DEBUG) { - Log.v(TAG, "Finished query for " + this); - } - return result; - } else { - if (c == null) { - Log.e(TAG, "Unknown query error for " + this); - } else { - if (DEBUG) Log.d(TAG, "Canceled query for " + this); - } - return null; - } - } - } - - private Uri insertRecordedProgram(Program program, long channelId, String storageUri, - long totalBytes, long startTime, long endTime) { - // TODO: Set title even though program is null. - RecordedProgram recordedProgram = RecordedProgram.builder() - .setInputId(mInputId) - .setChannelId(channelId) - .setDataUri(storageUri) - .setDurationMillis(endTime - startTime) - .setDataBytes(totalBytes) - // startTime and endTime could be overridden by program's start and end value. - .setStartTimeUtcMillis(startTime) - .setEndTimeUtcMillis(endTime) - .build(); - ContentValues values = RecordedProgram.toValues(recordedProgram); - if (program != null) { - values.putAll(program.buildValues()); - } - return mContext.getContentResolver().insert(TvContract.RecordedPrograms.CONTENT_URI, - values); - } - - private void onRecordingResult(boolean success, long lastExtractedPositionUs) { - if (mSessionState != STATE_RECORDING) { - // Error notification is not needed. - Log.e(TAG, "Recording session status abnormal"); - return; - } - if (mRecorderRunning) { - // In case of recorder not being stopped, because of premature termination of recording. - stopRecorder(); - } - if (!success && lastExtractedPositionUs < - TimeUnit.MILLISECONDS.toMicros(MIN_PARTIAL_RECORDING_DURATION_MS)) { - new DeleteRecordingTask().execute(mStorageDir); - mSession.onError(TvInputManager.RECORDING_ERROR_UNKNOWN); - Log.w(TAG, "Recording failed during recording"); - return; - } - Log.i(TAG, "recording finished " + (success ? "completely" : "partially")); - 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); - } - - private static class DeleteRecordingTask extends AsyncTask<File, Void, Void> { - - @Override - public Void doInBackground(File... files) { - if (files == null || files.length == 0) { - return null; - } - for(File file : files) { - Utils.deleteDirOrFile(file); - } - return null; - } - } -} diff --git a/src/com/android/tv/tuner/tvinput/TunerSession.java b/src/com/android/tv/tuner/tvinput/TunerSession.java deleted file mode 100644 index 44bae908..00000000 --- a/src/com/android/tv/tuner/tvinput/TunerSession.java +++ /dev/null @@ -1,324 +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.tvinput; - -import android.annotation.TargetApi; -import android.content.Context; -import android.media.PlaybackParams; -import android.media.tv.TvContentRating; -import android.media.tv.TvInputManager; -import android.media.tv.TvInputService; -import android.net.Uri; -import android.os.Build; -import android.os.Handler; -import android.os.Message; -import android.os.SystemClock; -import android.text.Html; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.Surface; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import android.widget.Toast; - -import com.google.android.exoplayer.audio.AudioCapabilities; -import com.android.tv.tuner.R; -import com.android.tv.tuner.TunerPreferences; -import com.android.tv.tuner.TunerPreferences.TunerPreferencesChangedListener; -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.util.GlobalSettingsUtils; -import com.android.tv.tuner.util.StatusTextUtils; -import com.android.tv.tuner.util.SystemPropertiesProxy; - -/** - * Provides a tuner TV input session. It handles Overlay UI works. Main tuner input functions - * are implemented in {@link TunerSessionWorker}. - */ -public class TunerSession extends TvInputService.Session implements - Handler.Callback, TunerPreferencesChangedListener { - private static final String TAG = "TunerSession"; - private static final boolean DEBUG = false; - private static final String USBTUNER_SHOW_DEBUG = "persist.tv.tuner.show_debug"; - - public static final int MSG_UI_SHOW_MESSAGE = 1; - public static final int MSG_UI_HIDE_MESSAGE = 2; - public static final int MSG_UI_SHOW_AUDIO_UNPLAYABLE = 3; - public static final int MSG_UI_HIDE_AUDIO_UNPLAYABLE = 4; - public static final int MSG_UI_PROCESS_CAPTION_TRACK = 5; - public static final int MSG_UI_START_CAPTION_TRACK = 6; - public static final int MSG_UI_STOP_CAPTION_TRACK = 7; - public static final int MSG_UI_RESET_CAPTION_TRACK = 8; - public static final int MSG_UI_CLEAR_CAPTION_RENDERER = 9; - public static final int MSG_UI_SET_STATUS_TEXT = 10; - public static final int MSG_UI_TOAST_RESCAN_NEEDED = 11; - - private final Context mContext; - private final Handler mUiHandler; - private final View mOverlayView; - private final TextView mMessageView; - private final TextView mStatusView; - private final TextView mAudioStatusView; - private final ViewGroup mMessageLayout; - private final CaptionTrackRenderer mCaptionTrackRenderer; - private final TunerSessionWorker mSessionWorker; - private boolean mReleased = false; - private boolean mPlayPaused; - private long mTuneStartTimestamp; - - public TunerSession(Context context, ChannelDataManager channelDataManager) { - super(context); - mContext = context; - mUiHandler = new Handler(this); - LayoutInflater inflater = (LayoutInflater) - context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - mOverlayView = inflater.inflate(R.layout.ut_overlay_view, null); - mMessageLayout = (ViewGroup) mOverlayView.findViewById(R.id.message_layout); - mMessageLayout.setVisibility(View.INVISIBLE); - mMessageView = (TextView) mOverlayView.findViewById(R.id.message); - mStatusView = (TextView) mOverlayView.findViewById(R.id.tuner_status); - boolean showDebug = SystemPropertiesProxy.getBoolean(USBTUNER_SHOW_DEBUG, false); - mStatusView.setVisibility(showDebug ? View.VISIBLE : View.INVISIBLE); - mAudioStatusView = (TextView) mOverlayView.findViewById(R.id.audio_status); - mAudioStatusView.setVisibility(View.INVISIBLE); - CaptionLayout captionLayout = (CaptionLayout) mOverlayView.findViewById(R.id.caption); - mCaptionTrackRenderer = new CaptionTrackRenderer(captionLayout); - mSessionWorker = new TunerSessionWorker(context, channelDataManager, this); - TunerPreferences.setTunerPreferencesChangedListener(this); - } - - public boolean isReleased() { - return mReleased; - } - - @Override - public View onCreateOverlayView() { - return mOverlayView; - } - - @Override - public boolean onSelectTrack(int type, String trackId) { - mSessionWorker.sendMessage(TunerSessionWorker.MSG_SELECT_TRACK, type, 0, trackId); - return false; - } - - @Override - public void onSetCaptionEnabled(boolean enabled) { - mSessionWorker.setCaptionEnabled(enabled); - } - - @Override - public void onSetStreamVolume(float volume) { - mSessionWorker.setStreamVolume(volume); - } - - @Override - public boolean onSetSurface(Surface surface) { - mSessionWorker.setSurface(surface); - return true; - } - - @Override - public void onTimeShiftPause() { - mSessionWorker.sendMessage(TunerSessionWorker.MSG_TIMESHIFT_PAUSE); - mPlayPaused = true; - } - - @Override - public void onTimeShiftResume() { - mSessionWorker.sendMessage(TunerSessionWorker.MSG_TIMESHIFT_RESUME); - mPlayPaused = false; - } - - @Override - public void onTimeShiftSeekTo(long timeMs) { - if (DEBUG) Log.d(TAG, "Timeshift seekTo requested position: " + timeMs / 1000); - mSessionWorker.sendMessage(TunerSessionWorker.MSG_TIMESHIFT_SEEK_TO, - mPlayPaused ? 1 : 0, 0, timeMs); - } - - @Override - public void onTimeShiftSetPlaybackParams(PlaybackParams params) { - mSessionWorker.sendMessage( - TunerSessionWorker.MSG_TIMESHIFT_SET_PLAYBACKPARAMS, params); - } - - @Override - public long onTimeShiftGetStartPosition() { - return mSessionWorker.getStartPosition(); - } - - @Override - public long onTimeShiftGetCurrentPosition() { - return mSessionWorker.getCurrentPosition(); - } - - @Override - public boolean onTune(Uri channelUri) { - if (DEBUG) { - Log.d(TAG, "onTune to " + channelUri != null ? channelUri.toString() : ""); - } - if (channelUri == null) { - Log.w(TAG, "onTune() is failed due to null channelUri."); - mSessionWorker.stopTune(); - return false; - } - mTuneStartTimestamp = SystemClock.elapsedRealtime(); - mSessionWorker.tune(channelUri); - mPlayPaused = false; - return true; - } - - @TargetApi(Build.VERSION_CODES.N) - @Override - public void onTimeShiftPlay(Uri recordUri) { - if (recordUri == null) { - Log.w(TAG, "onTimeShiftPlay() is failed due to null channelUri."); - mSessionWorker.stopTune(); - return; - } - mTuneStartTimestamp = SystemClock.elapsedRealtime(); - mSessionWorker.tune(recordUri); - mPlayPaused = false; - } - - @Override - public void onUnblockContent(TvContentRating unblockedRating) { - mSessionWorker.sendMessage(TunerSessionWorker.MSG_UNBLOCKED_RATING, - unblockedRating); - } - - @Override - public void onRelease() { - if (DEBUG) { - Log.d(TAG, "onRelease"); - } - mReleased = true; - mSessionWorker.release(); - mUiHandler.removeCallbacksAndMessages(null); - TunerPreferences.setTunerPreferencesChangedListener(null); - } - - /** - * Sets {@link AudioCapabilities}. - */ - public void setAudioCapabilities(AudioCapabilities audioCapabilities) { - mSessionWorker.sendMessage(TunerSessionWorker.MSG_AUDIO_CAPABILITIES_CHANGED, - audioCapabilities); - } - - @Override - public void notifyVideoAvailable() { - super.notifyVideoAvailable(); - if (mTuneStartTimestamp != 0) { - Log.i(TAG, "[Profiler] Video available in " - + (SystemClock.elapsedRealtime() - mTuneStartTimestamp) + " ms"); - mTuneStartTimestamp = 0; - } - } - - @Override - public void notifyVideoUnavailable(int reason) { - super.notifyVideoUnavailable(reason); - if (reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING - && reason != TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL) { - notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE); - } - } - - public void sendUiMessage(int message) { - mUiHandler.sendEmptyMessage(message); - } - - public void sendUiMessage(int message, Object object) { - mUiHandler.obtainMessage(message, object).sendToTarget(); - } - - public void sendUiMessage(int message, int arg1, int arg2, Object object) { - mUiHandler.obtainMessage(message, arg1, arg2, object).sendToTarget(); - } - - @Override - public boolean handleMessage(Message msg) { - switch (msg.what) { - case MSG_UI_SHOW_MESSAGE: { - mMessageView.setText((String) msg.obj); - mMessageLayout.setVisibility(View.VISIBLE); - return true; - } - case MSG_UI_HIDE_MESSAGE: { - mMessageLayout.setVisibility(View.INVISIBLE); - return true; - } - case MSG_UI_SHOW_AUDIO_UNPLAYABLE: { - // Showing message of enabling surround sound only when global surround sound - // setting is "never". - final int value = GlobalSettingsUtils.getEncodedSurroundOutputSettings(mContext); - if (value == GlobalSettingsUtils.ENCODED_SURROUND_OUTPUT_NEVER) { - mAudioStatusView.setText(Html.fromHtml(StatusTextUtils.getAudioWarningInHTML( - mContext.getString(R.string.ut_surround_sound_disabled)))); - } else { - 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: { - mAudioStatusView.setVisibility(View.INVISIBLE); - return true; - } - case MSG_UI_PROCESS_CAPTION_TRACK: { - mCaptionTrackRenderer.processCaptionEvent((CaptionEvent) msg.obj); - return true; - } - case MSG_UI_START_CAPTION_TRACK: { - mCaptionTrackRenderer.start((AtscCaptionTrack) msg.obj); - return true; - } - case MSG_UI_STOP_CAPTION_TRACK: { - mCaptionTrackRenderer.stop(); - return true; - } - case MSG_UI_RESET_CAPTION_TRACK: { - mCaptionTrackRenderer.reset(); - return true; - } - case MSG_UI_CLEAR_CAPTION_RENDERER: { - mCaptionTrackRenderer.clear(); - return true; - } - case MSG_UI_SET_STATUS_TEXT: { - mStatusView.setText((CharSequence) msg.obj); - return true; - } - case MSG_UI_TOAST_RESCAN_NEEDED: { - Toast.makeText(mContext, R.string.ut_rescan_needed, Toast.LENGTH_LONG).show(); - return true; - } - } - return false; - } - - @Override - public void onTunerPreferencesChanged() { - mSessionWorker.sendMessage(TunerSessionWorker.MSG_TUNER_PREFERENCES_CHANGED); - } -} diff --git a/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java b/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java deleted file mode 100644 index e7eb017e..00000000 --- a/src/com/android/tv/tuner/tvinput/TunerSessionWorker.java +++ /dev/null @@ -1,1754 +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.tvinput; - -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.Context; -import android.database.Cursor; -import android.media.MediaFormat; -import android.media.PlaybackParams; -import android.media.tv.TvContentRating; -import android.media.tv.TvContract; -import android.media.tv.TvInputManager; -import android.media.tv.TvTrackInfo; -import android.net.Uri; -import android.os.Environment; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Message; -import android.os.SystemClock; -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; -import android.view.Surface; -import android.view.accessibility.CaptioningManager; - -import com.google.android.exoplayer.audio.AudioCapabilities; -import com.google.android.exoplayer.ExoPlayer; -import com.android.tv.common.SoftPreconditions; -import com.android.tv.common.TvContentRatingCache; -import com.android.tv.customization.TvCustomizationManager; -import com.android.tv.customization.TvCustomizationManager.TRICKPLAY_MODE; -import com.android.tv.tuner.TunerPreferences; -import com.android.tv.tuner.TunerPreferences.TrickplaySetting; -import com.android.tv.tuner.data.Cea708Data; -import com.android.tv.tuner.data.PsipData.EitItem; -import com.android.tv.tuner.data.PsipData.TvTracksInterface; -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.exoplayer.ffmpeg.FfmpegDecoderClient; -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.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; - -/** - * {@link TunerSessionWorker} implements a handler thread which processes TV input jobs - * such as handling {@link ExoPlayer}, managing a tuner device, trickplay, and so on. - */ -@WorkerThread -public class TunerSessionWorker implements PlaybackBufferListener, - MpegTsPlayer.VideoEventListener, MpegTsPlayer.Listener, EventDetector.EventListener, - ChannelDataManager.ProgramInfoListener, Handler.Callback { - private static final String TAG = "TunerSessionWorker"; - private static final boolean DEBUG = false; - private static final boolean ENABLE_PROFILER = true; - private static final String PLAY_FROM_CHANNEL = "channel"; - private static final String 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; - public static final int MSG_UPDATE_CAPTION_TRACK = 2; - public static final int MSG_SET_STREAM_VOLUME = 3; - public static final int MSG_TIMESHIFT_PAUSE = 4; - public static final int MSG_TIMESHIFT_RESUME = 5; - public static final int MSG_TIMESHIFT_SEEK_TO = 6; - public static final int MSG_TIMESHIFT_SET_PLAYBACKPARAMS = 7; - public static final int MSG_AUDIO_CAPABILITIES_CHANGED = 8; - public static final int MSG_UNBLOCKED_RATING = 9; - public static final int MSG_TUNER_PREFERENCES_CHANGED = 10; - - // Private messages - private static final int MSG_TUNE = 1000; - private static final int MSG_RELEASE = 1001; - private static final int MSG_RETRY_PLAYBACK = 1002; - private static final int MSG_START_PLAYBACK = 1003; - private static final int MSG_UPDATE_PROGRAM = 1008; - private static final int MSG_SCHEDULE_OF_PROGRAMS = 1009; - private static final int MSG_UPDATE_CHANNEL_INFO = 1010; - private static final int MSG_TRICKPLAY_BY_SEEK = 1011; - private static final int MSG_SMOOTH_TRICKPLAY_MONITOR = 1012; - private static final int MSG_PARENTAL_CONTROLS = 1015; - private static final int MSG_RESCHEDULE_PROGRAMS = 1016; - private static final int MSG_BUFFER_START_TIME_CHANGED = 1017; - private static final int MSG_CHECK_SIGNAL = 1018; - private static final int MSG_DISCOVER_CAPTION_SERVICE_NUMBER = 1019; - private static final int MSG_RESET_PLAYBACK = 1020; - private static final int MSG_BUFFER_STATE_CHANGED = 1021; - private static final int MSG_PROGRAM_DATA_RESULT = 1022; - private static final int MSG_STOP_TUNE = 1023; - private static final int MSG_SET_SURFACE = 1024; - private static final int MSG_NOTIFY_AUDIO_TRACK_UPDATED = 1025; - - private static final int TS_PACKET_SIZE = 188; - private static final int CHECK_NO_SIGNAL_INITIAL_DELAY_MS = 4000; - private static final int CHECK_NO_SIGNAL_PERIOD_MS = 500; - private static final int RECOVER_STOPPED_PLAYBACK_PERIOD_MS = 2500; - private static final int PARENTAL_CONTROLS_INTERVAL_MS = 5000; - private static final int RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS = 4000; - private static final int RESCHEDULE_PROGRAMS_INTERVAL_MS = 10000; - private static final int RESCHEDULE_PROGRAMS_TOLERANCE_MS = 2000; - // The following 3s is defined empirically. This should be larger than 2s considering video - // key frame interval in the TS stream. - private static final int PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS = 3000; - private static final int PLAYBACK_RETRY_DELAY_MS = 5000; - private static final int MAX_IMMEDIATE_RETRY_COUNT = 5; - private static final long INVALID_TIME = -1; - - // Some examples of the track ids of the audio tracks, "a0", "a1", "a2". - // The number after prefix is being used for indicating a index of the given audio track. - private static final String AUDIO_TRACK_PREFIX = "a"; - - // Some examples of the tracks id of the caption tracks, "s1", "s2", "s3". - // The number after prefix is being used for indicating a index of a caption service number - // of the given caption track. - private static final String SUBTITLE_TRACK_PREFIX = "s"; - private static final int TRACK_PREFIX_SIZE = 1; - private static final String VIDEO_TRACK_ID = "v"; - private static final long BUFFER_UNDERFLOW_BUFFER_MS = 5000; - - // Actual interval would be divided by the speed. - 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; - private static final long TRICKPLAY_OFF_DURATION_MS = TimeUnit.DAYS.toMillis(14); - - // 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 final @TRICKPLAY_MODE int mTrickplayModeCustomization; - private volatile Surface mSurface; - private volatile float mVolume = 1.0f; - private volatile boolean mCaptionEnabled; - private volatile MpegTsPlayer mPlayer; - private volatile TunerChannel mChannel; - private volatile Long mRecordingDuration; - private volatile long mRecordStartTimeMs; - private volatile long mBufferStartTimeMs; - private volatile boolean mTrickplayDisabledByStorageIssue; - private @TrickplaySetting int mTrickplaySetting; - private long mTrickplayExpiredMs; - private String mRecordingId; - private final Handler mHandler; - private int mRetryCount; - private final ArrayList<TvTrackInfo> mTvTracks; - private final SparseArray<AtscAudioTrack> mAudioTrackMap; - private final SparseArray<AtscCaptionTrack> mCaptionTrackMap; - private AtscCaptionTrack mCaptionTrack; - private PlaybackParams mPlaybackParams = new PlaybackParams(); - private boolean mPlayerStarted = false; - private boolean mReportedDrawnToSurface = false; - private boolean mReportedWeakSignal = false; - private EitItem mProgram; - private List<EitItem> mPrograms; - private final TvInputManager mTvInputManager; - private boolean mChannelBlocked; - private TvContentRating mUnblockedContentRating; - private long mLastPositionMs; - private AudioCapabilities mAudioCapabilities; - private long mLastLimitInBytes; - private final TvContentRatingCache mTvContentRatingCache = TvContentRatingCache.getInstance(); - private final TunerSession mSession; - private final boolean mHasSoftwareAudioDecoder; - 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, - TunerSession tunerSession) { - if (DEBUG) Log.d(TAG, "TunerSessionWorker created"); - mContext = context; - - // HandlerThread should be set up before it is registered as a listener in the all other - // components. - HandlerThread handlerThread = new HandlerThread(TAG); - handlerThread.start(); - mHandler = new Handler(handlerThread.getLooper(), this); - mSession = tunerSession; - mChannelDataManager = channelDataManager; - mChannelDataManager.setListener(this); - mChannelDataManager.checkDataVersion(mContext); - mSourceManager = TsDataSourceManager.createSourceManager(false); - mTvInputManager = (TvInputManager) context.getSystemService(Context.TV_INPUT_SERVICE); - mTvTracks = new ArrayList<>(); - mAudioTrackMap = new SparseArray<>(); - mCaptionTrackMap = new SparseArray<>(); - CaptioningManager captioningManager = - (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE); - mCaptionEnabled = captioningManager.isEnabled(); - mPlaybackParams.setSpeed(1.0f); - mMaxTrickplayBufferSizeMb = - SystemPropertiesProxy.getInt(MAX_BUFFER_SIZE_KEY, MAX_BUFFER_SIZE_DEF); - mTrickplayModeCustomization = TvCustomizationManager.getTrickplayMode(context); - if (mTrickplayModeCustomization == - TvCustomizationManager.TRICKPLAY_MODE_USE_EXTERNAL_STORAGE) { - boolean useExternalStorage = - Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) && - Environment.isExternalStorageRemovable(); - mTrickplayBufferDir = useExternalStorage ? context.getExternalCacheDir() : null; - } else if (mTrickplayModeCustomization == TvCustomizationManager.TRICKPLAY_MODE_ENABLED) { - mTrickplayBufferDir = context.getCacheDir(); - } else { - mTrickplayBufferDir = null; - } - mTrickplayDisabledByStorageIssue = mTrickplayBufferDir == null; - mTrickplaySetting = TunerPreferences.getTrickplaySetting(context); - if (mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_NOT_SET - && mTrickplayModeCustomization - == TvCustomizationManager.TRICKPLAY_MODE_USE_EXTERNAL_STORAGE) { - // Consider the case of Customization package updates the value of trickplay mode - // to TRICKPLAY_MODE_USE_EXTERNAL_STORAGE after install. - mTrickplaySetting = TunerPreferences.TRICKPLAY_SETTING_NOT_SET; - TunerPreferences.setTrickplaySetting(context, mTrickplaySetting); - TunerPreferences.setTrickplayExpiredMs(context, 0); - } - mTrickplayExpiredMs = TunerPreferences.getTrickplayExpiredMs(context); - mPreparingStartTimeMs = INVALID_TIME; - mBufferingStartTimeMs = INVALID_TIME; - mReadyStartTimeMs = INVALID_TIME; - // NOTE: We assume that TunerSessionWorker instance will be at most one. - // Only one TunerSessionWorker can be connected to FfmpegDecoderClient at any given time. - // connect() will return false, if there is a connected TunerSessionWorker already. - mHasSoftwareAudioDecoder = FfmpegDecoderClient.connect(context); - } - - // Public methods - @MainThread - public void tune(Uri channelUri) { - mHandler.removeCallbacksAndMessages(null); - mSourceManager.setHasPendingTune(); - sendMessage(MSG_TUNE, channelUri); - } - - @MainThread - public void stopTune() { - mHandler.removeCallbacksAndMessages(null); - sendMessage(MSG_STOP_TUNE); - } - - /** - * Sets {@link Surface}. - */ - @MainThread - public void setSurface(Surface surface) { - if (surface != null && !surface.isValid()) { - Log.w(TAG, "Ignoring invalid surface."); - return; - } - // mSurface is kept even when tune is called right after. But, messages can be deleted by - // tune or updateChannelBlockStatus. So mSurface should be stored here, not through message. - mSurface = surface; - mHandler.sendEmptyMessage(MSG_SET_SURFACE); - } - - /** - * Sets volume. - */ - @MainThread - public void setStreamVolume(float volume) { - // mVolume is kept even when tune is called right after. But, messages can be deleted by - // tune or updateChannelBlockStatus. So mVolume is stored here and mPlayer.setVolume will be - // called in MSG_SET_STREAM_VOLUME. - mVolume = volume; - mHandler.sendEmptyMessage(MSG_SET_STREAM_VOLUME); - } - - /** - * Sets if caption is enabled or disabled. - */ - @MainThread - public void setCaptionEnabled(boolean captionEnabled) { - // mCaptionEnabled is kept even when tune is called right after. But, messages can be - // deleted by tune or updateChannelBlockStatus. So mCaptionEnabled is stored here and - // start/stopCaptionTrack will be called in MSG_UPDATE_CAPTION_STATUS. - mCaptionEnabled = captionEnabled; - mHandler.sendEmptyMessage(MSG_UPDATE_CAPTION_TRACK); - } - - public TunerChannel getCurrentChannel() { - return mChannel; - } - - @MainThread - public long getStartPosition() { - return mBufferStartTimeMs; - } - - - private String getRecordingPath() { - return Uri.parse(mRecordingId).getPath(); - } - - private Long getDurationForRecording(String recordingId) { - DvrStorageManager storageManager = - new DvrStorageManager(new File(getRecordingPath()), false); - 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; - } - Log.e(TAG, "meta file for recording was not found: " + recordingId); - return null; - } - - @MainThread - public long getCurrentPosition() { - // TODO: More precise time may be necessary. - MpegTsPlayer mpegTsPlayer = mPlayer; - long currentTime = mpegTsPlayer != null - ? mRecordStartTimeMs + mpegTsPlayer.getCurrentPosition() : mRecordStartTimeMs; - if (mChannel == null && mPlayerState == ExoPlayer.STATE_ENDED) { - currentTime = mRecordingDuration + mRecordStartTimeMs; - } - if (DEBUG) { - long systemCurrentTime = System.currentTimeMillis(); - Log.d(TAG, "currentTime = " + currentTime - + " ; System.currentTimeMillis() = " + systemCurrentTime - + " ; diff = " + (currentTime - systemCurrentTime)); - } - return currentTime; - } - - @AnyThread - public void sendMessage(int messageType) { - mHandler.sendEmptyMessage(messageType); - } - - @AnyThread - public void sendMessage(int messageType, Object object) { - mHandler.obtainMessage(messageType, object).sendToTarget(); - } - - @AnyThread - public void sendMessage(int messageType, int arg1, int arg2, Object object) { - mHandler.obtainMessage(messageType, arg1, arg2, object).sendToTarget(); - } - - @MainThread - public void release() { - if (DEBUG) Log.d(TAG, "release()"); - synchronized (mReleaseLock) { - mReleaseRequested = true; - } - if (mHasSoftwareAudioDecoder) { - FfmpegDecoderClient.disconnect(mContext); - } - mChannelDataManager.setListener(null); - mHandler.removeCallbacksAndMessages(null); - mHandler.sendEmptyMessage(MSG_RELEASE); - } - - // MpegTsPlayer.Listener - // Called in the same thread as mHandler. - @Override - public void onStateChanged(boolean playWhenReady, int playbackState) { - if (DEBUG) Log.d(TAG, "ExoPlayer state change: " + playbackState + " " + playWhenReady); - if (playbackState == mPlayerState) { - return; - } - mReadyStartTimeMs = INVALID_TIME; - mPreparingStartTimeMs = INVALID_TIME; - mBufferingStartTimeMs = INVALID_TIME; - if (playbackState == ExoPlayer.STATE_READY) { - if (DEBUG) Log.d(TAG, "ExoPlayer ready"); - if (!mPlayerStarted) { - sendMessage(MSG_START_PLAYBACK, System.identityHashCode(mPlayer)); - } - mReadyStartTimeMs = SystemClock.elapsedRealtime(); - } else if (playbackState == ExoPlayer.STATE_PREPARING) { - mPreparingStartTimeMs = SystemClock.elapsedRealtime(); - } else if (playbackState == ExoPlayer.STATE_BUFFERING) { - mBufferingStartTimeMs = SystemClock.elapsedRealtime(); - } else if (playbackState == ExoPlayer.STATE_ENDED) { - // Final status - // 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, System.identityHashCode(mPlayer)); - } - } - mPlayerState = playbackState; - } - - @Override - public void onError(Exception e) { - if (TunerPreferences.getStoreTsStream(mContext)) { - // Crash intentionally to capture the error causing TS file. - Log.e(TAG, "Crash intentionally to capture the error causing TS file. " - + e.getMessage()); - SoftPreconditions.checkState(false); - } - // There maybe some errors that finally raise ExoPlaybackException and will be handled here. - // 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, System.identityHashCode(mPlayer)) - .sendToTarget(); - } - } - - @Override - public void onVideoSizeChanged(int width, int height, float pixelWidthHeight) { - if (mChannel != null && mChannel.hasVideo()) { - updateVideoTrack(width, height); - } - if (mRecordingId != null) { - updateVideoTrack(width, height); - } - } - - @Override - public void onDrawnToSurface(MpegTsPlayer player, Surface surface) { - if (mSurface != null && mPlayerStarted) { - if (DEBUG) Log.d(TAG, "MSG_DRAWN_TO_SURFACE"); - 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; - - // If surface is drawn successfully, it means that the playback was brought back - // to normal and therefore, the playback recovery status will be reset through - // setting a zero value to the retry count. - // TODO: Consider audio only channels for detecting playback status changes to - // be normal. - mRetryCount = 0; - if (mCaptionEnabled && mCaptionTrack != null) { - startCaptionTrack(); - } else { - stopCaptionTrack(); - } - mHandler.sendEmptyMessage(MSG_NOTIFY_AUDIO_TRACK_UPDATED); - } - } - - @Override - public void onSmoothTrickplayForceStopped() { - if (mPlayer == null || !mHandler.hasMessages(MSG_SMOOTH_TRICKPLAY_MONITOR)) { - return; - } - mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR); - doTrickplayBySeek((int) mPlayer.getCurrentPosition()); - } - - @Override - public void onAudioUnplayable() { - if (mPlayer == null) { - return; - } - Log.i(TAG, "AC3 audio cannot be played due to device limitation"); - mSession.sendUiMessage( - TunerSession.MSG_UI_SHOW_AUDIO_UNPLAYABLE); - } - - // MpegTsPlayer.VideoEventListener - @Override - public void onEmitCaptionEvent(Cea708Data.CaptionEvent event) { - mSession.sendUiMessage(TunerSession.MSG_UI_PROCESS_CAPTION_TRACK, event); - } - - @Override - public void onClearCaptionEvent() { - mSession.sendUiMessage(TunerSession.MSG_UI_CLEAR_CAPTION_RENDERER); - } - - @Override - public void onDiscoverCaptionServiceNumber(int serviceNumber) { - sendMessage(MSG_DISCOVER_CAPTION_SERVICE_NUMBER, serviceNumber); - } - - // ChannelDataManager.ProgramInfoListener - @Override - public void onProgramsArrived(TunerChannel channel, List<EitItem> programs) { - sendMessage(MSG_SCHEDULE_OF_PROGRAMS, new Pair<>(channel, programs)); - } - - @Override - public void onChannelArrived(TunerChannel channel) { - sendMessage(MSG_UPDATE_CHANNEL_INFO, channel); - } - - @Override - public void onRescanNeeded() { - mSession.sendUiMessage(TunerSession.MSG_UI_TOAST_RESCAN_NEEDED); - } - - @Override - public void onRequestProgramsResponse(TunerChannel channel, List<EitItem> programs) { - sendMessage(MSG_PROGRAM_DATA_RESULT, new Pair<>(channel, programs)); - } - - // PlaybackBufferListener - @Override - public void onBufferStartTimeChanged(long startTimeMs) { - sendMessage(MSG_BUFFER_START_TIME_CHANGED, startTimeMs); - } - - @Override - public void onBufferStateChanged(boolean available) { - sendMessage(MSG_BUFFER_STATE_CHANGED, available); - } - - @Override - public void onDiskTooSlow() { - mTrickplayDisabledByStorageIssue = true; - sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)); - } - - // EventDetector.EventListener - @Override - public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) { - mChannelDataManager.notifyChannelDetected(channel, channelArrivedAtFirstTime); - } - - @Override - public void onEventDetected(TunerChannel channel, List<EitItem> items) { - mChannelDataManager.notifyEventDetected(channel, items); - } - - @Override - public void onChannelScanDone() { - // do nothing. - } - - private long parseChannel(Uri uri) { - try { - List<String> paths = uri.getPathSegments(); - if (paths.size() > 1 && paths.get(0).equals(PLAY_FROM_CHANNEL)) { - return ContentUris.parseId(uri); - } - } catch (UnsupportedOperationException | NumberFormatException e) { - } - return -1; - } - - private static class RecordedProgram { - private final long mChannelId; - private final String mDataUri; - - private static final String[] PROJECTION = { - TvContract.Programs.COLUMN_CHANNEL_ID, - TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI, - }; - - public RecordedProgram(Cursor cursor) { - int index = 0; - mChannelId = cursor.getLong(index++); - mDataUri = cursor.getString(index++); - } - - public RecordedProgram(long channelId, String dataUri) { - mChannelId = channelId; - mDataUri = dataUri; - } - - public static RecordedProgram onQuery(Cursor c) { - RecordedProgram recording = null; - if (c != null && c.moveToNext()) { - recording = new RecordedProgram(c); - } - return recording; - } - - public String getDataUri() { - return mDataUri; - } - } - - private RecordedProgram getRecordedProgram(Uri recordedUri) { - ContentResolver resolver = mContext.getContentResolver(); - try(Cursor c = resolver.query(recordedUri, RecordedProgram.PROJECTION, null, null, null)) { - if (c != null) { - RecordedProgram result = RecordedProgram.onQuery(c); - if (DEBUG) { - Log.d(TAG, "Finished query for " + this); - } - return result; - } else { - if (c == null) { - Log.e(TAG, "Unknown query error for " + this); - } else { - if (DEBUG) Log.d(TAG, "Canceled query for " + this); - } - return null; - } - } - } - - private String parseRecording(Uri uri) { - RecordedProgram recording = getRecordedProgram(uri); - if (recording != null) { - return recording.getDataUri(); - } - return null; - } - - @Override - public boolean handleMessage(Message msg) { - switch (msg.what) { - case MSG_TUNE: { - if (DEBUG) Log.d(TAG, "MSG_TUNE"); - - // When sequential tuning messages arrived, it skips middle tuning messages in order - // to change to the last requested channel quickly. - if (mHandler.hasMessages(MSG_TUNE)) { - 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); - TunerChannel channel = (channelId == -1) ? null - : mChannelDataManager.getChannel(channelId); - if (channelId == -1) { - recording = parseRecording(channelUri); - } - if (channel == null && recording == null) { - Log.w(TAG, "onTune() is failed. Can't find channel for " + channelUri); - stopTune(); - notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); - return true; - } - clearCallbacksAndMessagesSafely(); - mChannelDataManager.removeAllCallbacksAndMessages(); - if (channel != null) { - mChannelDataManager.requestProgramsData(channel); - } - prepareTune(channel, recording); - // TODO: Need to refactor. notifyContentAllowed() should not be called if parental - // control is turned on. - mSession.notifyContentAllowed(); - resetTvTracks(); - resetPlayback(); - mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS, - RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS); - return true; - } - case MSG_STOP_TUNE: { - if (DEBUG) Log.d(TAG, "MSG_STOP_TUNE"); - mChannel = null; - stopPlayback(true); - stopCaptionTrack(); - resetTvTracks(); - notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); - return true; - } - case MSG_RELEASE: { - if (DEBUG) Log.d(TAG, "MSG_RELEASE"); - mHandler.removeCallbacksAndMessages(null); - stopPlayback(true); - stopCaptionTrack(); - mSourceManager.release(); - mHandler.getLooper().quitSafely(); - if (mIsActiveSession) { - sActiveSessionSemaphore.release(); - } - return true; - } - case MSG_RETRY_PLAYBACK: { - 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. - mSourceManager.setKeepTuneStatus(false); - mRetryCount++; - 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(false); - stopCaptionTrack(); - - notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); - Log.i(TAG, "Notify weak signal since fail to retry playback"); - - // After MAX_IMMEDIATE_RETRY_COUNT, give some delay of an empirically chosen - // value before recovering the playback. - mHandler.sendEmptyMessageDelayed(MSG_RESET_PLAYBACK, - RECOVER_STOPPED_PLAYBACK_PERIOD_MS); - } - } - return true; - } - 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((int) msg.obj); - } - return true; - } - case MSG_UPDATE_PROGRAM: { - if (mChannel != null) { - EitItem program = (EitItem) msg.obj; - updateTvTracks(program, false); - mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS); - } - return true; - } - case MSG_SCHEDULE_OF_PROGRAMS: { - mHandler.removeMessages(MSG_UPDATE_PROGRAM); - Pair<TunerChannel, List<EitItem>> pair = - (Pair<TunerChannel, List<EitItem>>) msg.obj; - TunerChannel channel = pair.first; - if (mChannel == null) { - return true; - } - if (mChannel != null && mChannel.compareTo(channel) != 0) { - return true; - } - mPrograms = pair.second; - EitItem currentProgram = getCurrentProgram(); - if (currentProgram == null) { - mProgram = null; - } - long currentTimeMs = getCurrentPosition(); - if (mPrograms != null) { - for (EitItem item : mPrograms) { - if (currentProgram != null && currentProgram.compareTo(item) == 0) { - if (DEBUG) { - Log.d(TAG, "Update current TvTracks " + item); - } - if (mProgram != null && mProgram.compareTo(item) == 0) { - continue; - } - mProgram = item; - updateTvTracks(item, false); - } else if (item.getStartTimeUtcMillis() > currentTimeMs) { - if (DEBUG) { - Log.d(TAG, "Update next TvTracks " + item + " " - + (item.getStartTimeUtcMillis() - currentTimeMs)); - } - mHandler.sendMessageDelayed( - mHandler.obtainMessage(MSG_UPDATE_PROGRAM, item), - item.getStartTimeUtcMillis() - currentTimeMs); - } - } - } - mHandler.sendEmptyMessage(MSG_PARENTAL_CONTROLS); - return true; - } - case MSG_UPDATE_CHANNEL_INFO: { - TunerChannel channel = (TunerChannel) msg.obj; - if (mChannel != null && mChannel.compareTo(channel) == 0) { - updateChannelInfo(channel); - } - return true; - } - case MSG_PROGRAM_DATA_RESULT: { - TunerChannel channel = (TunerChannel) ((Pair) msg.obj).first; - - // If there already exists, skip it since real-time data is a top priority, - if (mChannel != null && mChannel.compareTo(channel) == 0 - && mPrograms == null && mProgram == null) { - sendMessage(MSG_SCHEDULE_OF_PROGRAMS, msg.obj); - } - return true; - } - case MSG_TRICKPLAY_BY_SEEK: { - if (mPlayer == null) { - return true; - } - doTrickplayBySeek(msg.arg1); - return true; - } - case MSG_SMOOTH_TRICKPLAY_MONITOR: { - if (mPlayer == null) { - return true; - } - long systemCurrentTime = System.currentTimeMillis(); - long position = getCurrentPosition(); - if (mRecordingId == null) { - // Checks if the position exceeds the upper bound when forwarding, - // or exceed the lower bound when rewinding. - // If the direction is not checked, there can be some issues. - // (See b/29939781 for more details.) - if ((position > systemCurrentTime && mPlaybackParams.getSpeed() > 0L) - || (position < mBufferStartTimeMs && mPlaybackParams.getSpeed() < 0L)) { - doTimeShiftResume(); - return true; - } - } else { - if (position > mRecordingDuration || position < 0) { - doTimeShiftPause(); - return true; - } - } - mHandler.sendEmptyMessageDelayed(MSG_SMOOTH_TRICKPLAY_MONITOR, - TRICKPLAY_MONITOR_INTERVAL_MS); - return true; - } - case MSG_RESCHEDULE_PROGRAMS: { - if (mHandler.hasMessages(MSG_SCHEDULE_OF_PROGRAMS)) { - mHandler.sendEmptyMessage(MSG_RESCHEDULE_PROGRAMS); - } else { - doReschedulePrograms(); - } - return true; - } - case MSG_PARENTAL_CONTROLS: { - doParentalControls(); - mHandler.removeMessages(MSG_PARENTAL_CONTROLS); - mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, - PARENTAL_CONTROLS_INTERVAL_MS); - return true; - } - case MSG_UNBLOCKED_RATING: { - mUnblockedContentRating = (TvContentRating) msg.obj; - doParentalControls(); - mHandler.removeMessages(MSG_PARENTAL_CONTROLS); - mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, - PARENTAL_CONTROLS_INTERVAL_MS); - return true; - } - case MSG_DISCOVER_CAPTION_SERVICE_NUMBER: { - int serviceNumber = (int) msg.obj; - doDiscoverCaptionServiceNumber(serviceNumber); - return true; - } - case MSG_SELECT_TRACK: { - if (mChannel != null || mRecordingId != null) { - doSelectTrack(msg.arg1, (String) msg.obj); - } - return true; - } - case MSG_UPDATE_CAPTION_TRACK: { - if (mCaptionEnabled) { - startCaptionTrack(); - } else { - stopCaptionTrack(); - } - return true; - } - case MSG_TIMESHIFT_PAUSE: { - if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_PAUSE"); - if (mPlayer == null) { - return true; - } - setTrickplayEnabledIfNeeded(); - doTimeShiftPause(); - return true; - } - case MSG_TIMESHIFT_RESUME: { - if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_RESUME"); - if (mPlayer == null) { - return true; - } - setTrickplayEnabledIfNeeded(); - doTimeShiftResume(); - return true; - } - case MSG_TIMESHIFT_SEEK_TO: { - long position = (long) msg.obj; - if (DEBUG) Log.d(TAG, "MSG_TIMESHIFT_SEEK_TO (position=" + position + ")"); - if (mPlayer == null) { - return true; - } - setTrickplayEnabledIfNeeded(); - doTimeShiftSeekTo(position); - return true; - } - case MSG_TIMESHIFT_SET_PLAYBACKPARAMS: { - if (mPlayer == null) { - return true; - } - setTrickplayEnabledIfNeeded(); - doTimeShiftSetPlaybackParams((PlaybackParams) msg.obj); - return true; - } - case MSG_AUDIO_CAPABILITIES_CHANGED: { - AudioCapabilities capabilities = (AudioCapabilities) msg.obj; - if (DEBUG) { - Log.d(TAG, "MSG_AUDIO_CAPABILITIES_CHANGED " + capabilities); - } - if (capabilities == null) { - return true; - } - if (!capabilities.equals(mAudioCapabilities)) { - // HDMI supported encodings are changed. restart player. - mAudioCapabilities = capabilities; - resetPlayback(); - } - return true; - } - case MSG_SET_STREAM_VOLUME: { - if (mPlayer != null && mPlayer.isPlaying()) { - mPlayer.setVolume(mVolume); - } - return true; - } - case MSG_TUNER_PREFERENCES_CHANGED: { - mHandler.removeMessages(MSG_TUNER_PREFERENCES_CHANGED); - @TrickplaySetting int trickplaySetting = - TunerPreferences.getTrickplaySetting(mContext); - if (trickplaySetting != mTrickplaySetting) { - boolean wasTrcikplayEnabled = - mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED; - boolean isTrickplayEnabled = - trickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED; - mTrickplaySetting = trickplaySetting; - if (isTrickplayEnabled != wasTrcikplayEnabled) { - sendMessage(MSG_RESET_PLAYBACK, System.identityHashCode(mPlayer)); - } - } - return true; - } - case MSG_BUFFER_START_TIME_CHANGED: { - if (mPlayer == null) { - return true; - } - mBufferStartTimeMs = (long) msg.obj; - if (!hasEnoughBackwardBuffer() - && (!mPlayer.isPlaying() || mPlaybackParams.getSpeed() < 1.0f)) { - mPlayer.setPlayWhenReady(true); - mPlayer.setAudioTrackAndClosedCaption(true); - mPlaybackParams.setSpeed(1.0f); - } - return true; - } - case MSG_BUFFER_STATE_CHANGED: { - boolean available = (boolean) msg.obj; - mSession.notifyTimeShiftStatusChanged(available - ? TvInputManager.TIME_SHIFT_STATUS_AVAILABLE - : TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE); - return true; - } - case MSG_CHECK_SIGNAL: { - if (mChannel == null || mPlayer == null) { - return true; - } - TsDataSource source = mPlayer.getDataSource(); - long limitInBytes = source != null ? source.getBufferedPosition() : 0L; - if (TunerDebug.ENABLED) { - TunerDebug.calculateDiff(); - mSession.sendUiMessage(TunerSession.MSG_UI_SET_STATUS_TEXT, - Html.fromHtml( - StatusTextUtils.getStatusWarningInHTML( - (limitInBytes - mLastLimitInBytes) - / TS_PACKET_SIZE, - TunerDebug.getVideoFrameDrop(), - TunerDebug.getBytesInQueue(), - TunerDebug.getAudioPositionUs(), - TunerDebug.getAudioPositionUsRate(), - TunerDebug.getAudioPtsUs(), - TunerDebug.getAudioPtsUsRate(), - TunerDebug.getVideoPtsUs(), - TunerDebug.getVideoPtsUsRate() - ))); - } - mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE); - long currentTime = SystemClock.elapsedRealtime(); - long bufferingTimeMs = mBufferingStartTimeMs != INVALID_TIME - ? currentTime - mBufferingStartTimeMs : mBufferingStartTimeMs; - long preparingTimeMs = mPreparingStartTimeMs != INVALID_TIME - ? currentTime - mPreparingStartTimeMs : mPreparingStartTimeMs; - boolean isBufferingTooLong = - bufferingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS; - boolean isPreparingTooLong = - preparingTimeMs > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS; - boolean isWeakSignal = source != null - && mChannel.getType() != Channel.TYPE_FILE - && (isBufferingTooLong || isPreparingTooLong); - if (isWeakSignal && !mReportedWeakSignal) { - if (!mHandler.hasMessages(MSG_RETRY_PLAYBACK)) { - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY_PLAYBACK, - System.identityHashCode(mPlayer)), PLAYBACK_RETRY_DELAY_MS); - } - if (mPlayer != null) { - mPlayer.setAudioTrackAndClosedCaption(false); - } - notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); - Log.i(TAG, "Notify weak signal due to signal check, " + String.format( - "packetsPerSec:%d, bufferingTimeMs:%d, preparingTimeMs:%d, " + - "videoFrameDrop:%d", - (limitInBytes - mLastLimitInBytes) / TS_PACKET_SIZE, - bufferingTimeMs, - preparingTimeMs, - TunerDebug.getVideoFrameDrop() - )); - } else if (!isWeakSignal && mReportedWeakSignal) { - boolean isPlaybackStable = mReadyStartTimeMs != INVALID_TIME - && currentTime - mReadyStartTimeMs - > PLAYBACK_STATE_CHANGED_WAITING_THRESHOLD_MS; - if (!isPlaybackStable) { - // Wait until playback becomes stable. - } else if (mReportedDrawnToSurface) { - mHandler.removeMessages(MSG_RETRY_PLAYBACK); - notifyVideoAvailable(); - mPlayer.setAudioTrackAndClosedCaption(true); - } - } - mLastLimitInBytes = limitInBytes; - mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_PERIOD_MS); - return true; - } - case MSG_SET_SURFACE: { - if (mPlayer != null) { - mPlayer.setSurface(mSurface); - } else { - // TODO: Since surface is dynamically set, we can remove the dependency of - // playback start on mSurface nullity. - resetPlayback(); - } - return true; - } - case MSG_NOTIFY_AUDIO_TRACK_UPDATED: { - notifyAudioTracksUpdated(); - return true; - } - default: { - Log.w(TAG, "Unhandled message code: " + msg.what); - return false; - } - } - } - - // Private methods - private void doSelectTrack(int type, String trackId) { - int numTrackId = trackId != null - ? Integer.parseInt(trackId.substring(TRACK_PREFIX_SIZE)) : -1; - if (type == TvTrackInfo.TYPE_AUDIO) { - if (trackId == null) { - return; - } - 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) { - if (trackId == null) { - mSession.notifyTrackSelected(type, null); - mCaptionTrack = null; - stopCaptionTrack(); - return; - } - for (TvTrackInfo track : mTvTracks) { - if (track.getId().equals(trackId)) { - // The service number of the caption service is used for track id of a - // subtitle track. Passes the following track id on to TsParser. - mSession.notifyTrackSelected(type, trackId); - mCaptionTrack = mCaptionTrackMap.get(numTrackId); - startCaptionTrack(); - return; - } - } - } - } - - private void setTrickplayEnabledIfNeeded() { - if (mChannel == null || - mTrickplayModeCustomization != TvCustomizationManager.TRICKPLAY_MODE_ENABLED) { - return; - } - if (mTrickplaySetting == TunerPreferences.TRICKPLAY_SETTING_NOT_SET) { - mTrickplaySetting = TunerPreferences.TRICKPLAY_SETTING_ENABLED; - TunerPreferences.setTrickplaySetting( - mContext, mTrickplaySetting); - } - } - - private MpegTsPlayer createPlayer(AudioCapabilities capabilities) { - if (capabilities == null) { - Log.w(TAG, "No Audio Capabilities"); - } - long now = System.currentTimeMillis(); - if (mTrickplayModeCustomization == TvCustomizationManager.TRICKPLAY_MODE_ENABLED - && mTrickplaySetting == TunerPreferences.TRICKPLAY_SETTING_NOT_SET) { - if (mTrickplayExpiredMs == 0) { - mTrickplayExpiredMs = now + TRICKPLAY_OFF_DURATION_MS; - TunerPreferences.setTrickplayExpiredMs(mContext, mTrickplayExpiredMs); - } else { - if (mTrickplayExpiredMs < now) { - mTrickplaySetting = TunerPreferences.TRICKPLAY_SETTING_DISABLED; - TunerPreferences.setTrickplaySetting(mContext, mTrickplaySetting); - } - } - } - BufferManager bufferManager = null; - if (mRecordingId != null) { - StorageManager storageManager = - new DvrStorageManager(new File(getRecordingPath()), false); - bufferManager = new BufferManager(storageManager); - updateCaptionTracks(((DvrStorageManager)storageManager).readCaptionInfoFiles()); - } else if (!mTrickplayDisabledByStorageIssue - && mTrickplaySetting != TunerPreferences.TRICKPLAY_SETTING_DISABLED - && 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); - Log.i(TAG, "Passthrough AC3 renderer"); - if (DEBUG) Log.d(TAG, "ExoPlayer created"); - return player; - } - - private void startCaptionTrack() { - if (mCaptionEnabled && mCaptionTrack != null) { - mSession.sendUiMessage( - TunerSession.MSG_UI_START_CAPTION_TRACK, mCaptionTrack); - if (mPlayer != null) { - mPlayer.setCaptionServiceNumber(mCaptionTrack.serviceNumber); - } - } - } - - private void stopCaptionTrack() { - if (mPlayer != null) { - mPlayer.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER); - } - mSession.sendUiMessage(TunerSession.MSG_UI_STOP_CAPTION_TRACK); - } - - private void resetTvTracks() { - mTvTracks.clear(); - mAudioTrackMap.clear(); - mCaptionTrackMap.clear(); - mSession.sendUiMessage(TunerSession.MSG_UI_RESET_CAPTION_TRACK); - mSession.notifyTracksChanged(mTvTracks); - } - - private void updateTvTracks(TvTracksInterface tvTracksInterface, boolean fromPmt) { - 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); - } - } - } - - private void removeTvTracks(int trackType) { - Iterator<TvTrackInfo> iterator = mTvTracks.iterator(); - while (iterator.hasNext()) { - TvTrackInfo tvTrackInfo = iterator.next(); - if (tvTrackInfo.getType() == trackType) { - iterator.remove(); - } - } - } - - private void updateVideoTrack(int width, int height) { - removeTvTracks(TvTrackInfo.TYPE_VIDEO); - mTvTracks.add(new TvTrackInfo.Builder(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID) - .setVideoWidth(width).setVideoHeight(height).build()); - mSession.notifyTracksChanged(mTvTracks); - mSession.notifyTrackSelected(TvTrackInfo.TYPE_VIDEO, VIDEO_TRACK_ID); - } - - private void updateAudioTracks(List<AtscAudioTrack> audioTracks) { - if (DEBUG) { - Log.d(TAG, "Update AudioTracks " + audioTracks); - } - mAudioTrackMap.clear(); - if (audioTracks != null) { - int index = 0; - for (AtscAudioTrack audioTrack : audioTracks) { - audioTrack.index = index; - mAudioTrackMap.put(index, audioTrack); - ++index; - } - } - mHandler.sendEmptyMessage(MSG_NOTIFY_AUDIO_TRACK_UPDATED); - } - - private void notifyAudioTracksUpdated() { - if (mPlayer == null) { - // Audio tracks will be updated later once player initialization is done. - return; - } - int audioTrackCount = mPlayer.getTrackCount(MpegTsPlayer.TRACK_TYPE_AUDIO); - removeTvTracks(TvTrackInfo.TYPE_AUDIO); - for (int i = 0; i < audioTrackCount; 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(infoFromPlayer.channelCount); - builder.setAudioSampleRate(infoFromPlayer.sampleRate); - TvTrackInfo track = builder.build(); - mTvTracks.add(track); - } - mSession.notifyTracksChanged(mTvTracks); - } - - private void updateCaptionTracks(List<AtscCaptionTrack> captionTracks) { - if (DEBUG) { - Log.d(TAG, "Update CaptionTrack " + captionTracks); - } - removeTvTracks(TvTrackInfo.TYPE_SUBTITLE); - mCaptionTrackMap.clear(); - if (captionTracks != null) { - for (AtscCaptionTrack captionTrack : captionTracks) { - if (mCaptionTrackMap.indexOfKey(captionTrack.serviceNumber) >= 0) { - continue; - } - String language = captionTrack.language; - - // The service number of the caption service is used for track id of a subtitle. - // Later, when a subtitle is chosen, track id will be passed on to TsParser. - TvTrackInfo.Builder builder = - new TvTrackInfo.Builder(TvTrackInfo.TYPE_SUBTITLE, - SUBTITLE_TRACK_PREFIX + captionTrack.serviceNumber); - builder.setLanguage(language); - mTvTracks.add(builder.build()); - mCaptionTrackMap.put(captionTrack.serviceNumber, captionTrack); - } - } - mSession.notifyTracksChanged(mTvTracks); - } - - private void updateChannelInfo(TunerChannel channel) { - if (DEBUG) { - Log.d(TAG, String.format("Channel Info (old) videoPid: %d audioPid: %d " + - "audioSize: %d", mChannel.getVideoPid(), mChannel.getAudioPid(), - mChannel.getAudioPids().size())); - } - - // The list of the audio tracks resided in a channel is often changed depending on a - // program being on the air. So, we should update the streaming PIDs and types of the - // tuned channel according to the newly received channel data. - int oldVideoPid = mChannel.getVideoPid(); - int oldAudioPid = mChannel.getAudioPid(); - List<Integer> audioPids = channel.getAudioPids(); - List<Integer> audioStreamTypes = channel.getAudioStreamTypes(); - int size = audioPids.size(); - mChannel.setVideoPid(channel.getVideoPid()); - mChannel.setAudioPids(audioPids); - mChannel.setAudioStreamTypes(audioStreamTypes); - updateTvTracks(channel, true); - int index = audioPids.isEmpty() ? -1 : 0; - for (int i = 0; i < size; ++i) { - if (audioPids.get(i) == oldAudioPid) { - index = i; - break; - } - } - mChannel.selectAudioTrack(index); - mSession.notifyTrackSelected(TvTrackInfo.TYPE_AUDIO, - index == -1 ? null : AUDIO_TRACK_PREFIX + index); - - // Reset playback if there is a change in the listening streaming PIDs. - if (oldVideoPid != mChannel.getVideoPid() - || oldAudioPid != mChannel.getAudioPid()) { - // TODO: Implement a switching between tracks more smoothly. - resetPlayback(); - } - if (DEBUG) { - Log.d(TAG, String.format("Channel Info (new) videoPid: %d audioPid: %d " + - " audioSize: %d", mChannel.getVideoPid(), mChannel.getAudioPid(), - mChannel.getAudioPids().size())); - } - } - - private void stopPlayback(boolean removeChannelDataCallbacks) { - if (removeChannelDataCallbacks) { - mChannelDataManager.removeAllCallbacksAndMessages(); - } - if (mPlayer != null) { - mPlayer.setPlayWhenReady(false); - mPlayer.release(); - mPlayer = null; - mPlayerState = ExoPlayer.STATE_IDLE; - mPlaybackParams.setSpeed(1.0f); - mPlayerStarted = false; - mReportedDrawnToSurface = false; - mPreparingStartTimeMs = INVALID_TIME; - mBufferingStartTimeMs = INVALID_TIME; - mReadyStartTimeMs = INVALID_TIME; - mLastLimitInBytes = 0L; - mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_AUDIO_UNPLAYABLE); - mSession.notifyTimeShiftStatusChanged(TvInputManager.TIME_SHIFT_STATUS_UNAVAILABLE); - } - } - - private void startPlayback(int playerHashCode) { - // TODO: provide hasAudio()/hasVideo() for play recordings. - if (mPlayer == null || System.identityHashCode(mPlayer) != playerHashCode) { - return; - } - if (mChannel != null && !mChannel.hasAudio()) { - if (DEBUG) Log.d(TAG, "Channel " + mChannel + " does not have audio."); - // Playbacks with video-only stream have not been tested yet. - // No video-only channel has been found. - notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN); - return; - } - if (mChannel != null && ((mChannel.hasAudio() && !mPlayer.hasAudio()) - || (mChannel.hasVideo() && !mPlayer.hasVideo())) - && mChannel.getType() != Channel.TYPE_NETWORK) { - // If the channel is from network, skip this part since the video and audio tracks - // information for channels from network are more reliable in the extractor. Otherwise, - // tracks haven't been detected in the extractor. Try again. - sendMessage(MSG_RETRY_PLAYBACK, System.identityHashCode(mPlayer)); - return; - } - // Since mSurface is volatile, we define a local variable surface to keep the same value - // inside this method. - Surface surface = mSurface; - if (surface != null && !mPlayerStarted) { - mPlayer.setSurface(surface); - mPlayer.setPlayWhenReady(true); - mPlayer.setVolume(mVolume); - if (mChannel != null && mPlayer.hasAudio() && !mPlayer.hasVideo()) { - notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_AUDIO_ONLY); - } else if (!mReportedWeakSignal) { - // Doesn't show buffering during weak signal. - notifyVideoUnavailable(TvInputManager.VIDEO_UNAVAILABLE_REASON_BUFFERING); - } - mSession.sendUiMessage(TunerSession.MSG_UI_HIDE_MESSAGE); - mPlayerStarted = true; - } - } - - private void preparePlayback() { - SoftPreconditions.checkState(mPlayer == null); - if (mChannel == null && mRecordingId == null) { - return; - } - mSourceManager.setKeepTuneStatus(true); - MpegTsPlayer player = createPlayer(mAudioCapabilities); - player.setCaptionServiceNumber(Cea708Data.EMPTY_SERVICE_NUMBER); - player.setVideoEventListener(this); - player.setCaptionServiceNumber(mCaptionTrack != null ? - mCaptionTrack.serviceNumber : Cea708Data.EMPTY_SERVICE_NUMBER); - if (!player.prepare(mContext, mChannel, mHasSoftwareAudioDecoder, this)) { - mSourceManager.setKeepTuneStatus(false); - player.release(); - if (!mHandler.hasMessages(MSG_TUNE)) { - // 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); - Log.i(TAG, "Notify weak signal due to player preparation failure"); - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_RETRY_PLAYBACK, - System.identityHashCode(mPlayer)), PLAYBACK_RETRY_DELAY_MS); - } - } else { - mPlayer = player; - mPlayerStarted = false; - mHandler.removeMessages(MSG_CHECK_SIGNAL); - mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_INITIAL_DELAY_MS); - } - } - - private void resetPlayback() { - long timestamp, oldTimestamp; - timestamp = SystemClock.elapsedRealtime(); - stopPlayback(false); - stopCaptionTrack(); - if (ENABLE_PROFILER) { - oldTimestamp = timestamp; - timestamp = SystemClock.elapsedRealtime(); - Log.i(TAG, "[Profiler] stopPlayback() takes " + (timestamp - oldTimestamp) + " ms"); - } - if (mChannelBlocked || mSurface == null) { - return; - } - preparePlayback(); - } - - private void prepareTune(TunerChannel channel, String recording) { - mChannelBlocked = false; - mUnblockedContentRating = null; - mRetryCount = 0; - mChannel = channel; - mRecordingId = recording; - mRecordingDuration = recording != null ? getDurationForRecording(recording) : null; - mProgram = null; - mPrograms = null; - 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); - } - - private void doReschedulePrograms() { - long currentPositionMs = getCurrentPosition(); - long forwardDifference = Math.abs(currentPositionMs - mLastPositionMs - - RESCHEDULE_PROGRAMS_INTERVAL_MS); - mLastPositionMs = currentPositionMs; - - // A gap is measured as the time difference between previous and next current position - // periodically. If the gap has a significant difference with an interval of a period, - // this means that there is a change of playback status and the programs of the current - // channel should be rescheduled to new playback timeline. - if (forwardDifference > RESCHEDULE_PROGRAMS_TOLERANCE_MS) { - if (DEBUG) { - Log.d(TAG, "reschedule programs size:" - + (mPrograms != null ? mPrograms.size() : 0) + " current program: " - + getCurrentProgram()); - } - mHandler.obtainMessage(MSG_SCHEDULE_OF_PROGRAMS, new Pair<>(mChannel, mPrograms)) - .sendToTarget(); - } - mHandler.removeMessages(MSG_RESCHEDULE_PROGRAMS); - mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS, - RESCHEDULE_PROGRAMS_INTERVAL_MS); - } - - private int getTrickPlaySeekIntervalMs() { - return Math.max(EXPECTED_KEY_FRAME_INTERVAL_MS / (int) Math.abs(mPlaybackParams.getSpeed()), - MIN_TRICKPLAY_SEEK_INTERVAL_MS); - } - - private void doTrickplayBySeek(int seekPositionMs) { - mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK); - if (mPlaybackParams.getSpeed() == 1.0f || !mPlayer.isPrepared()) { - return; - } - if (seekPositionMs < mBufferStartTimeMs - mRecordStartTimeMs) { - if (mPlaybackParams.getSpeed() > 1.0f) { - // If fast forwarding, the seekPositionMs can be out of the buffered range - // because of chuck evictions. - seekPositionMs = (int) (mBufferStartTimeMs - mRecordStartTimeMs); - } else { - mPlayer.seekTo(mBufferStartTimeMs - mRecordStartTimeMs); - mPlaybackParams.setSpeed(1.0f); - mPlayer.setAudioTrackAndClosedCaption(true); - return; - } - } else if (seekPositionMs > System.currentTimeMillis() - mRecordStartTimeMs) { - // Stops trickplay when FF requested the position later than current position. - // If RW trickplay requested the position later than current position, - // continue trickplay. - if (mPlaybackParams.getSpeed() > 0.0f) { - mPlayer.seekTo(System.currentTimeMillis() - mRecordStartTimeMs); - mPlaybackParams.setSpeed(1.0f); - mPlayer.setAudioTrackAndClosedCaption(true); - return; - } - } - - long delayForNextSeek = getTrickPlaySeekIntervalMs(); - if (!mPlayer.isBuffering()) { - mPlayer.seekTo(seekPositionMs); - } else { - delayForNextSeek = MIN_TRICKPLAY_SEEK_INTERVAL_MS; - } - seekPositionMs += mPlaybackParams.getSpeed() * delayForNextSeek; - mHandler.sendMessageDelayed(mHandler.obtainMessage( - MSG_TRICKPLAY_BY_SEEK, seekPositionMs, 0), delayForNextSeek); - } - - private void doTimeShiftPause() { - mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR); - mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK); - if (!hasEnoughBackwardBuffer()) { - return; - } - mPlaybackParams.setSpeed(1.0f); - mPlayer.setPlayWhenReady(false); - mPlayer.setAudioTrackAndClosedCaption(true); - } - - private void doTimeShiftResume() { - mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR); - mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK); - mPlaybackParams.setSpeed(1.0f); - mPlayer.setPlayWhenReady(true); - mPlayer.setAudioTrackAndClosedCaption(true); - } - - private void doTimeShiftSeekTo(long timeMs) { - mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR); - mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK); - mPlayer.seekTo((int) (timeMs - mRecordStartTimeMs)); - } - - private void doTimeShiftSetPlaybackParams(PlaybackParams params) { - if (!hasEnoughBackwardBuffer() && params.getSpeed() < 1.0f) { - return; - } - mPlaybackParams = params; - float speed = mPlaybackParams.getSpeed(); - if (speed == 1.0f) { - mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR); - mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK); - doTimeShiftResume(); - } else if (mPlayer.supportSmoothTrickPlay(speed)) { - mHandler.removeMessages(MSG_TRICKPLAY_BY_SEEK); - mPlayer.setAudioTrackAndClosedCaption(false); - mPlayer.startSmoothTrickplay(mPlaybackParams); - mHandler.sendEmptyMessageDelayed(MSG_SMOOTH_TRICKPLAY_MONITOR, - TRICKPLAY_MONITOR_INTERVAL_MS); - } else { - mHandler.removeMessages(MSG_SMOOTH_TRICKPLAY_MONITOR); - if (!mHandler.hasMessages(MSG_TRICKPLAY_BY_SEEK)) { - mPlayer.setAudioTrackAndClosedCaption(false); - mPlayer.setPlayWhenReady(false); - // Initiate trickplay - mHandler.sendMessage(mHandler.obtainMessage(MSG_TRICKPLAY_BY_SEEK, - (int) (mPlayer.getCurrentPosition() - + speed * getTrickPlaySeekIntervalMs()), 0)); - } - } - } - - private EitItem getCurrentProgram() { - if (mPrograms == null || mPrograms.isEmpty()) { - return null; - } - if (mChannel.getType() == Channel.TYPE_FILE) { - // For the playback from the local file, we use the first one from the given program. - EitItem first = mPrograms.get(0); - if (first != null && (mProgram == null - || first.getStartTimeUtcMillis() < mProgram.getStartTimeUtcMillis())) { - return first; - } - return null; - } - long currentTimeMs = getCurrentPosition(); - for (EitItem item : mPrograms) { - if (item.getStartTimeUtcMillis() <= currentTimeMs - && item.getEndTimeUtcMillis() >= currentTimeMs) { - return item; - } - } - return null; - } - - private void doParentalControls() { - boolean isParentalControlsEnabled = mTvInputManager.isParentalControlsEnabled(); - if (isParentalControlsEnabled) { - TvContentRating blockContentRating = getContentRatingOfCurrentProgramBlocked(); - if (DEBUG) { - if (blockContentRating != null) { - Log.d(TAG, "Check parental controls: blocked by content rating - " - + blockContentRating); - } else { - Log.d(TAG, "Check parental controls: available"); - } - } - updateChannelBlockStatus(blockContentRating != null, blockContentRating); - } else { - if (DEBUG) { - Log.d(TAG, "Check parental controls: available"); - } - updateChannelBlockStatus(false, null); - } - } - - private void doDiscoverCaptionServiceNumber(int serviceNumber) { - int index = mCaptionTrackMap.indexOfKey(serviceNumber); - if (index < 0) { - AtscCaptionTrack captionTrack = new AtscCaptionTrack(); - captionTrack.serviceNumber = serviceNumber; - captionTrack.wideAspectRatio = false; - captionTrack.easyReader = false; - mCaptionTrackMap.put(serviceNumber, captionTrack); - mTvTracks.add(new TvTrackInfo.Builder(TvTrackInfo.TYPE_SUBTITLE, - SUBTITLE_TRACK_PREFIX + serviceNumber).build()); - mSession.notifyTracksChanged(mTvTracks); - } - } - - private TvContentRating getContentRatingOfCurrentProgramBlocked() { - EitItem currentProgram = getCurrentProgram(); - if (currentProgram == null) { - return null; - } - TvContentRating[] ratings = mTvContentRatingCache - .getRatings(currentProgram.getContentRating()); - if (ratings == null || ratings.length == 0) { - ratings = new TvContentRating[] {TvContentRating.UNRATED}; - } - for (TvContentRating rating : ratings) { - if (!Objects.equals(mUnblockedContentRating, rating) && mTvInputManager - .isRatingBlocked(rating)) { - return rating; - } - } - return null; - } - - private void updateChannelBlockStatus(boolean channelBlocked, - TvContentRating contentRating) { - if (mChannelBlocked == channelBlocked) { - return; - } - mChannelBlocked = channelBlocked; - if (mChannelBlocked) { - clearCallbacksAndMessagesSafely(); - stopPlayback(true); - resetTvTracks(); - if (contentRating != null) { - mSession.notifyContentBlocked(contentRating); - } - mHandler.sendEmptyMessageDelayed(MSG_PARENTAL_CONTROLS, PARENTAL_CONTROLS_INTERVAL_MS); - } else { - clearCallbacksAndMessagesSafely(); - resetPlayback(); - mSession.notifyContentAllowed(); - mHandler.sendEmptyMessageDelayed(MSG_RESCHEDULE_PROGRAMS, - RESCHEDULE_PROGRAMS_INITIAL_DELAY_MS); - mHandler.removeMessages(MSG_CHECK_SIGNAL); - mHandler.sendEmptyMessageDelayed(MSG_CHECK_SIGNAL, CHECK_NO_SIGNAL_INITIAL_DELAY_MS); - } - } - - @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; - } - - private void notifyVideoUnavailable(final int reason) { - mReportedWeakSignal = (reason == TvInputManager.VIDEO_UNAVAILABLE_REASON_WEAK_SIGNAL); - if (mSession != null) { - mSession.notifyVideoUnavailable(reason); - } - } - - private void notifyVideoAvailable() { - mReportedWeakSignal = false; - if (mSession != null) { - mSession.notifyVideoAvailable(); - } - } -} diff --git a/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java b/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java deleted file mode 100644 index 6ad00daa..00000000 --- a/src/com/android/tv/tuner/tvinput/TunerStorageCleanUpService.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.tv.tuner.tvinput; - -import android.app.job.JobParameters; -import android.app.job.JobService; -import android.content.ContentResolver; -import android.content.Context; -import android.database.Cursor; -import android.media.tv.TvContract; -import android.net.Uri; -import android.os.AsyncTask; -import android.util.Log; - -import com.android.tv.TvApplication; -import com.android.tv.dvr.DvrStorageStatusManager; -import com.android.tv.util.Utils; - -import java.io.File; -import java.io.IOException; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -/** - * Creates {@link JobService} to clean up recorded program files which are not referenced - * from database. - */ -public class TunerStorageCleanUpService extends JobService { - private static final String TAG = "TunerStorageCleanUpService"; - - private CleanUpStorageTask mTask; - - @Override - public void onCreate() { - if (!TvApplication.getSingletons(this).getTvInputManagerHelper().hasTvInputManager()) { - Log.wtf(TAG, "Stopping because device does not have a TvInputManager"); - this.stopSelf(); - return; - } - TvApplication.setCurrentRunningProcess(this, false); - super.onCreate(); - mTask = new CleanUpStorageTask(this, this); - } - - @Override - public boolean onStartJob(JobParameters params) { - mTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params); - return true; - } - - @Override - public boolean onStopJob(JobParameters params) { - return false; - } - - /** - * Cleans up recorded program files which are not referenced from database. - * Cleaning up will be done periodically. - */ - public static class CleanUpStorageTask extends AsyncTask<JobParameters, Void, JobParameters[]> { - private final static String[] mProjection = { - TvContract.RecordedPrograms.COLUMN_PACKAGE_NAME, - TvContract.RecordedPrograms.COLUMN_RECORDING_DATA_URI - }; - private final static long ELAPSED_MILLIS_TO_DELETE = TimeUnit.DAYS.toMillis(1); - - private final Context mContext; - private final DvrStorageStatusManager mDvrStorageStatusManager; - private final JobService mJobService; - private final ContentResolver mContentResolver; - - /** - * Creates a recurring storage cleaning task. - * - * @param context {@link Context} - * @param jobService {@link JobService} - */ - public CleanUpStorageTask(Context context, JobService jobService) { - mContext = context; - mDvrStorageStatusManager = - TvApplication.getSingletons(mContext).getDvrStorageStatusManager(); - mJobService = jobService; - mContentResolver = mContext.getContentResolver(); - } - - private Set<String> getRecordedProgramsDirs() { - try (Cursor c = mContentResolver.query( - TvContract.RecordedPrograms.CONTENT_URI, mProjection, null, null, null)) { - if (c == null) { - return null; - } - Set<String> recordedProgramDirs = new HashSet<>(); - while (c.moveToNext()) { - String packageName = c.getString(0); - String dataUriString = c.getString(1); - if (dataUriString == null) { - continue; - } - Uri dataUri = Uri.parse(dataUriString); - if (!Utils.isInBundledPackageSet(packageName) - || dataUri == null || dataUri.getPath() == null - || !ContentResolver.SCHEME_FILE.equals(dataUri.getScheme())) { - continue; - } - File recordedProgramDir = new File(dataUri.getPath()); - try { - recordedProgramDirs.add(recordedProgramDir.getCanonicalPath()); - } catch (IOException | SecurityException e) { - } - } - return recordedProgramDirs; - } - } - - @Override - protected JobParameters[] doInBackground(JobParameters... params) { - if (mDvrStorageStatusManager.getDvrStorageStatus() - == DvrStorageStatusManager.STORAGE_STATUS_MISSING) { - return params; - } - File dvrRecordingDir = mDvrStorageStatusManager.getRecordingRootDataDirectory(); - if (dvrRecordingDir == null || !dvrRecordingDir.isDirectory()) { - return params; - } - Set<String> recordedProgramDirs = getRecordedProgramsDirs(); - if (recordedProgramDirs == null) { - return params; - } - File[] files = dvrRecordingDir.listFiles(); - if (files == null || files.length == 0) { - return params; - } - for (File recordingDir : files) { - try { - if (!recordedProgramDirs.contains(recordingDir.getCanonicalPath())) { - long lastModified = recordingDir.lastModified(); - long now = System.currentTimeMillis(); - if (lastModified != 0 - && lastModified < now - ELAPSED_MILLIS_TO_DELETE) { - // To prevent current recordings from being deleted, - // deletes recordings which was not modified for long enough time. - Utils.deleteDirOrFile(recordingDir); - } - } - } catch (IOException | SecurityException e) { - // would not happen - } - } - return params; - } - - @Override - protected void onPostExecute(JobParameters[] params) { - for (JobParameters param : params) { - mJobService.jobFinished(param, false); - } - } - } -} diff --git a/src/com/android/tv/tuner/tvinput/TunerTvInputService.java b/src/com/android/tv/tuner/tvinput/TunerTvInputService.java deleted file mode 100644 index 2725ddfc..00000000 --- a/src/com/android/tv/tuner/tvinput/TunerTvInputService.java +++ /dev/null @@ -1,123 +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.tvinput; - -import android.app.job.JobInfo; -import android.app.job.JobScheduler; -import android.content.ComponentName; -import android.content.Context; -import android.media.tv.TvContract; -import android.media.tv.TvInputService; -import android.util.Log; - -import com.google.android.exoplayer.audio.AudioCapabilities; -import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver; -import com.android.tv.TvApplication; -import com.android.tv.common.feature.CommonFeatures; - -import java.util.Collections; -import java.util.Set; -import java.util.WeakHashMap; -import java.util.concurrent.TimeUnit; - -/** - * {@link TunerTvInputService} serves TV channels coming from a tuner device. - */ -public class TunerTvInputService extends TvInputService - implements AudioCapabilitiesReceiver.Listener{ - private static final String TAG = "TunerTvInputService"; - private static final boolean DEBUG = false; - - private static final int DVR_STORAGE_CLEANUP_JOB_ID = 100; - - // WeakContainer for {@link TvInputSessionImpl} - private final Set<TunerSession> mTunerSessions = Collections.newSetFromMap(new WeakHashMap<>()); - private ChannelDataManager mChannelDataManager; - private AudioCapabilitiesReceiver mAudioCapabilitiesReceiver; - private AudioCapabilities mAudioCapabilities; - - @Override - public void onCreate() { - if (!TvApplication.getSingletons(this).getTvInputManagerHelper().hasTvInputManager()) { - Log.wtf(TAG, "Stopping because device does not have a TvInputManager"); - this.stopSelf(); - return; - } - TvApplication.setCurrentRunningProcess(this, false); - super.onCreate(); - if (DEBUG) Log.d(TAG, "onCreate"); - mChannelDataManager = new ChannelDataManager(getApplicationContext()); - mAudioCapabilitiesReceiver = new AudioCapabilitiesReceiver(getApplicationContext(), this); - mAudioCapabilitiesReceiver.register(); - if (CommonFeatures.DVR.isEnabled(this)) { - JobScheduler jobScheduler = - (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE); - JobInfo pendingJob = jobScheduler.getPendingJob(DVR_STORAGE_CLEANUP_JOB_ID); - if (pendingJob != null) { - // storage cleaning job is already scheduled. - } else { - JobInfo job = new JobInfo.Builder(DVR_STORAGE_CLEANUP_JOB_ID, - new ComponentName(this, TunerStorageCleanUpService.class)) - .setPersisted(true).setPeriodic(TimeUnit.DAYS.toMillis(1)).build(); - jobScheduler.schedule(job); - } - } - } - - @Override - public void onDestroy() { - if (DEBUG) Log.d(TAG, "onDestroy"); - super.onDestroy(); - mChannelDataManager.release(); - mAudioCapabilitiesReceiver.unregister(); - } - - @Override - public RecordingSession onCreateRecordingSession(String inputId) { - return new TunerRecordingSession(this, inputId, mChannelDataManager); - } - - @Override - public Session onCreateSession(String inputId) { - if (DEBUG) Log.d(TAG, "onCreateSession"); - try { - final TunerSession session = new TunerSession(this, mChannelDataManager); - mTunerSessions.add(session); - session.setAudioCapabilities(mAudioCapabilities); - session.setOverlayViewEnabled(true); - return session; - } catch (RuntimeException e) { - // There are no available DVB devices. - Log.e(TAG, "Creating a session for " + inputId + " failed.", e); - return null; - } - } - - @Override - public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) { - mAudioCapabilities = audioCapabilities; - for (TunerSession session : mTunerSessions) { - if (!session.isReleased()) { - session.setAudioCapabilities(audioCapabilities); - } - } - } - - public static String getInputId(Context context) { - return TvContract.buildInputId(new ComponentName(context, TunerTvInputService.class)); - } -} diff --git a/src/com/android/tv/tuner/util/ByteArrayBuffer.java b/src/com/android/tv/tuner/util/ByteArrayBuffer.java deleted file mode 100644 index da887e7d..00000000 --- a/src/com/android/tv/tuner/util/ByteArrayBuffer.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpcore/trunk/module-main/src/main/java/org/apache/http/util/ByteArrayBuffer.java $ - * $Revision: 496070 $ - * $Date: 2007-01-14 04:18:34 -0800 (Sun, 14 Jan 2007) $ - * - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * <http://www.apache.org/>. - * - */ - -package com.android.tv.tuner.util; - -/** - * An expandable byte buffer built on byte array. - */ -public final class ByteArrayBuffer { - - private byte[] buffer; - private int len; - - public ByteArrayBuffer(int capacity) { - super(); - if (capacity < 0) { - throw new IllegalArgumentException("Buffer capacity may not be negative"); - } - this.buffer = new byte[capacity]; - } - - private void expand(int newlen) { - byte newbuffer[] = new byte[Math.max(this.buffer.length << 1, newlen)]; - System.arraycopy(this.buffer, 0, newbuffer, 0, this.len); - this.buffer = newbuffer; - } - - public void append(final byte[] b, int off, int len) { - if (b == null) { - return; - } - if ((off < 0) || (off > b.length) || (len < 0) || - ((off + len) < 0) || ((off + len) > b.length)) { - throw new IndexOutOfBoundsException(); - } - if (len == 0) { - return; - } - int newlen = this.len + len; - if (newlen > this.buffer.length) { - expand(newlen); - } - System.arraycopy(b, off, this.buffer, this.len, len); - this.len = newlen; - } - - public void append(int b) { - int newlen = this.len + 1; - if (newlen > this.buffer.length) { - expand(newlen); - } - this.buffer[this.len] = (byte) b; - this.len = newlen; - } - - public void append(final char[] b, int off, int len) { - if (b == null) { - return; - } - if ((off < 0) || (off > b.length) || (len < 0) || - ((off + len) < 0) || ((off + len) > b.length)) { - throw new IndexOutOfBoundsException(); - } - if (len == 0) { - return; - } - int oldlen = this.len; - int newlen = oldlen + len; - if (newlen > this.buffer.length) { - expand(newlen); - } - for (int i1 = off, i2 = oldlen; i2 < newlen; i1++, i2++) { - this.buffer[i2] = (byte) b[i1]; - } - this.len = newlen; - } - - public void clear() { - this.len = 0; - } - - public byte[] toByteArray() { - byte[] b = new byte[this.len]; - if (this.len > 0) { - System.arraycopy(this.buffer, 0, b, 0, this.len); - } - return b; - } - - public int byteAt(int i) { - return this.buffer[i]; - } - - public int capacity() { - return this.buffer.length; - } - - public int length() { - return this.len; - } - - public byte[] buffer() { - return this.buffer; - } - - public void setLength(int len) { - if (len < 0 || len > this.buffer.length) { - throw new IndexOutOfBoundsException(); - } - this.len = len; - } - - public boolean isEmpty() { - return this.len == 0; - } - - public boolean isFull() { - return this.len == this.buffer.length; - } - -} diff --git a/src/com/android/tv/tuner/util/ConvertUtils.java b/src/com/android/tv/tuner/util/ConvertUtils.java deleted file mode 100644 index abf18d8c..00000000 --- a/src/com/android/tv/tuner/util/ConvertUtils.java +++ /dev/null @@ -1,35 +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 converting date and time. - */ -public class ConvertUtils { - // Time diff between 1.1.1970 00:00:00 and 6.1.1980 00:00:00 - private static final long DIFF_BETWEEN_UNIX_EPOCH_AND_GPS = 315964800; - - private ConvertUtils() { } - - public static long convertGPSTimeToUnixEpoch(long gpsTime) { - return gpsTime + DIFF_BETWEEN_UNIX_EPOCH_AND_GPS; - } - - public static long convertUnixEpochToGPSTime(long epochTime) { - return epochTime - DIFF_BETWEEN_UNIX_EPOCH_AND_GPS; - } -} diff --git a/src/com/android/tv/tuner/util/GlobalSettingsUtils.java b/src/com/android/tv/tuner/util/GlobalSettingsUtils.java deleted file mode 100644 index 0cefcbed..00000000 --- a/src/com/android/tv/tuner/util/GlobalSettingsUtils.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.util; - -import android.content.Context; -import android.provider.Settings; - -/** - * Utility class that get information of global settings. - */ -public class GlobalSettingsUtils { - // Since global surround setting is hided, add the related variable here for checking surround - // sound setting when the audio is unavailable. Remove this workaround after b/31254857 fixed. - private static final String ENCODED_SURROUND_OUTPUT = "encoded_surround_output"; - public static final int ENCODED_SURROUND_OUTPUT_NEVER = 1; - - private GlobalSettingsUtils () { } - - public static int getEncodedSurroundOutputSettings(Context context) { - return Settings.Global.getInt(context.getContentResolver(), ENCODED_SURROUND_OUTPUT, 0); - } -} diff --git a/src/com/android/tv/tuner/util/Ints.java b/src/com/android/tv/tuner/util/Ints.java deleted file mode 100644 index 0b1be426..00000000 --- a/src/com/android/tv/tuner/util/Ints.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.android.tv.tuner.util; - -import java.util.ArrayList; -import java.util.List; - -/** - * Static utility methods pertaining to int primitives. (Referred Guava's Ints class) - */ -public class Ints { - private Ints() {} - - public static int[] toArray(List<Integer> integerList) { - int[] intArray = new int[integerList.size()]; - int i = 0; - for (Integer data : integerList) { - intArray[i++] = data; - } - return intArray; - } - - public static List<Integer> asList(int[] intArray) { - List<Integer> integerList = new ArrayList<>(intArray.length); - for (int data : intArray) { - integerList.add(data); - } - return integerList; - } -} diff --git a/src/com/android/tv/tuner/util/PostalCodeUtils.java b/src/com/android/tv/tuner/util/PostalCodeUtils.java deleted file mode 100644 index 9eb689a7..00000000 --- a/src/com/android/tv/tuner/util/PostalCodeUtils.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * 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.NonNull; -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.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.regex.Pattern; - -/** - * A utility class to update, get, and set the last known postal or zip code. - */ -public class PostalCodeUtils { - private static final String TAG = "PostalCodeUtils"; - - // Postcode formats, where A signifies a letter and 9 a digit: - // US zip code format: 99999 - private static final String POSTCODE_REGEX_US = "^(\\d{5})"; - // UK postcode district formats: A9, A99, AA9, AA99 - // Full UK postcode format: Postcode District + space + 9AA - // Should be able to handle both postcode district and full postcode - private static final String POSTCODE_REGEX_GB = - "^([A-Z][A-Z]?[0-9][0-9A-Z]?)( ?[0-9][A-Z]{2})?$"; - private static final String POSTCODE_REGEX_GB_GIR = "^GIR( ?0AA)?$"; // special UK postcode - - private static final Map<String, Pattern> REGION_PATTERN = new HashMap<>(); - private static final Map<String, Integer> REGION_MAX_LENGTH = new HashMap<>(); - - static { - REGION_PATTERN.put(Locale.US.getCountry(), Pattern.compile(POSTCODE_REGEX_US)); - REGION_PATTERN.put( - Locale.UK.getCountry(), - Pattern.compile(POSTCODE_REGEX_GB + "|" + POSTCODE_REGEX_GB_GIR)); - REGION_MAX_LENGTH.put(Locale.US.getCountry(), 5); - REGION_MAX_LENGTH.put(Locale.UK.getCountry(), 8); - } - - // The longest postcode number is 10-character-long. - // Use a larger number to accommodate future changes. - private static final int DEFAULT_MAX_LENGTH = 16; - - /** 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()); - 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() { - } - } - - /** - * Checks whether a postcode matches the format of the specific region. - * - * @return {@code false} if the region is supported and the postcode doesn't match; {@code true} - * otherwise - */ - public static boolean matches(@NonNull CharSequence postcode, @NonNull String region) { - Pattern pattern = REGION_PATTERN.get(region.toUpperCase()); - return pattern == null || pattern.matcher(postcode).matches(); - } - - /** - * Gets the largest possible postcode length in the region. - * - * @return maximum postcode length if the region is supported; {@link #DEFAULT_MAX_LENGTH} - * otherwise - */ - public static int getRegionMaxLength(Context context) { - Integer maxLength = - REGION_MAX_LENGTH.get(LocationUtils.getCurrentCountry(context).toUpperCase()); - return maxLength == null ? DEFAULT_MAX_LENGTH : maxLength; - } -}
\ No newline at end of file diff --git a/src/com/android/tv/tuner/util/StatusTextUtils.java b/src/com/android/tv/tuner/util/StatusTextUtils.java deleted file mode 100644 index 2633834b..00000000 --- a/src/com/android/tv/tuner/util/StatusTextUtils.java +++ /dev/null @@ -1,119 +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; - -import java.util.Locale; - -/** - * Utility class for tuner status messages. - */ -public class StatusTextUtils { - private static final int PACKETS_PER_SEC_YELLOW = 1500; - private static final int PACKETS_PER_SEC_RED = 1000; - private static final int AUDIO_POSITION_MS_RATE_DIFF_YELLOW = 100; - private static final int AUDIO_POSITION_MS_RATE_DIFF_RED = 200; - private static final String COLOR_RED = "red"; - private static final String COLOR_YELLOW = "yellow"; - private static final String COLOR_GREEN = "green"; - private static final String COLOR_GRAY = "gray"; - - private StatusTextUtils() { } - - /** - * Returns tuner status warning message in HTML. - * - * <p>This is only called for debuging and always shown in english.</p> - */ - public static String getStatusWarningInHTML(long packetsPerSec, - int videoFrameDrop, int bytesInQueue, - long audioPositionUs, long audioPositionUsRate, - long audioPtsUs, long audioPtsUsRate, - long videoPtsUs, long videoPtsUsRate) { - StringBuffer buffer = new StringBuffer(); - - // audioPosition should go in rate of 1000ms. - long audioPositionMsRate = audioPositionUsRate / 1000; - String audioPositionColor; - if (Math.abs(audioPositionMsRate - 1000) > AUDIO_POSITION_MS_RATE_DIFF_RED) { - audioPositionColor = COLOR_RED; - } else if (Math.abs(audioPositionMsRate - 1000) > AUDIO_POSITION_MS_RATE_DIFF_YELLOW) { - audioPositionColor = COLOR_YELLOW; - } else { - audioPositionColor = COLOR_GRAY; - } - buffer.append(String.format(Locale.US, "<font color=%s>", audioPositionColor)); - buffer.append( - String.format(Locale.US, "audioPositionMs: %d (%d)<br>", audioPositionUs / 1000, - audioPositionMsRate)); - buffer.append("</font>\n"); - buffer.append("<font color=" + COLOR_GRAY + ">"); - buffer.append(String.format(Locale.US, "audioPtsMs: %d (%d, %d)<br>", audioPtsUs / 1000, - audioPtsUsRate / 1000, (audioPtsUs - audioPositionUs) / 1000)); - buffer.append(String.format(Locale.US, "videoPtsMs: %d (%d, %d)<br>", videoPtsUs / 1000, - videoPtsUsRate / 1000, (videoPtsUs - audioPositionUs) / 1000)); - buffer.append("</font>\n"); - - appendStatusLine(buffer, "KbytesInQueue", bytesInQueue / 1000, 1, 10); - buffer.append("<br/>"); - appendErrorStatusLine(buffer, "videoFrameDrop", videoFrameDrop, 0, 2); - buffer.append("<br/>"); - appendStatusLine(buffer, "packetsPerSec", packetsPerSec, PACKETS_PER_SEC_RED, - PACKETS_PER_SEC_YELLOW); - return buffer.toString(); - } - - /** - * Returns audio unavailable warning message in HTML. - */ - public static String getAudioWarningInHTML(String msg) { - return String.format("<font color=%s>%s</font>\n", COLOR_YELLOW, msg); - } - - private static void appendStatusLine(StringBuffer buffer, String factorName, long value, - int minRed, int minYellow) { - buffer.append("<font color="); - if (value <= minRed) { - buffer.append(COLOR_RED); - } else if (value <= minYellow) { - buffer.append(COLOR_YELLOW); - } else { - buffer.append(COLOR_GREEN); - } - buffer.append(">"); - buffer.append(factorName); - buffer.append(" : "); - buffer.append(value); - buffer.append("</font>"); - } - - private static void appendErrorStatusLine(StringBuffer buffer, String factorName, int value, - int minGreen, int minYellow) { - buffer.append("<font color="); - if (value <= minGreen) { - buffer.append(COLOR_GREEN); - } else if (value <= minYellow) { - buffer.append(COLOR_YELLOW); - } else { - buffer.append(COLOR_RED); - } - buffer.append(">"); - buffer.append(factorName); - buffer.append(" : "); - buffer.append(value); - buffer.append("</font>"); - } -} diff --git a/src/com/android/tv/tuner/util/SystemPropertiesProxy.java b/src/com/android/tv/tuner/util/SystemPropertiesProxy.java deleted file mode 100644 index 2817ccbf..00000000 --- a/src/com/android/tv/tuner/util/SystemPropertiesProxy.java +++ /dev/null @@ -1,77 +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; - -import android.util.Log; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; - -/** - * Proxy class that gives an access to a hidden API {@link android.os.SystemProperties#getBoolean}. - */ -public class SystemPropertiesProxy { - private static final String TAG = "SystemPropertiesProxy"; - - private SystemPropertiesProxy() { } - - public static boolean getBoolean(String key, boolean def) - throws IllegalArgumentException { - try { - Class SystemPropertiesClass = Class.forName("android.os.SystemProperties"); - Method getBooleanMethod = SystemPropertiesClass.getDeclaredMethod("getBoolean", - String.class, boolean.class); - getBooleanMethod.setAccessible(true); - return (boolean) getBooleanMethod.invoke(SystemPropertiesClass, key, def); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException - | ClassNotFoundException e) { - Log.e(TAG, "Failed to invoke SystemProperties.getBoolean()", e); - } - return def; - } - - public static int getInt(String key, int def) - throws IllegalArgumentException { - try { - Class SystemPropertiesClass = Class.forName("android.os.SystemProperties"); - Method getIntMethod = SystemPropertiesClass.getDeclaredMethod("getInt", - String.class, int.class); - getIntMethod.setAccessible(true); - return (int) getIntMethod.invoke(SystemPropertiesClass, key, def); - } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException - | ClassNotFoundException e) { - Log.e(TAG, "Failed to invoke SystemProperties.getInt()", e); - } - 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/TisConfiguration.java b/src/com/android/tv/tuner/util/TisConfiguration.java deleted file mode 100644 index ca861d67..00000000 --- a/src/com/android/tv/tuner/util/TisConfiguration.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.android.tv.tuner.util; - -import android.content.Context; - -/** - * A helper class of tuner configuration. - */ -public class TisConfiguration { - private static final String LC_PACKAGE_NAME = "com.android.tv"; - - public static boolean isPackagedWithLiveChannels(Context context) { - return (LC_PACKAGE_NAME.equals(context.getPackageName())); - } - - public static boolean isInternalTunerTvInput(Context context) { - return (!LC_PACKAGE_NAME.equals(context.getPackageName())); - } - - public static int getTunerHwDeviceId(Context context) { - return 0; // FIXME: Make it OEM configurable - } -} diff --git a/src/com/android/tv/tuner/util/TunerInputInfoUtils.java b/src/com/android/tv/tuner/util/TunerInputInfoUtils.java deleted file mode 100644 index f421bf1a..00000000 --- a/src/com/android/tv/tuner/util/TunerInputInfoUtils.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.tv.tuner.util; - -import android.annotation.TargetApi; -import android.content.ComponentName; -import android.content.Context; -import android.media.tv.TvInputInfo; -import android.media.tv.TvInputManager; -import android.os.AsyncTask; -import android.os.Build; -import android.support.annotation.Nullable; -import android.util.Log; -import android.util.Pair; - -import com.android.tv.common.feature.CommonFeatures; -import com.android.tv.tuner.R; -import com.android.tv.tuner.TunerHal; -import com.android.tv.tuner.tvinput.TunerTvInputService; - -/** - * Utility class for providing tuner input info. - */ -public class TunerInputInfoUtils { - private static final String TAG = "TunerInputInfoUtils"; - private static final boolean DEBUG = false; - - /** - * Builds tuner input's info. - */ - @Nullable - @TargetApi(Build.VERSION_CODES.N) - public static TvInputInfo buildTunerInputInfo(Context context) { - Pair<Integer, Integer> tunerTypeAndCount = TunerHal.getTunerTypeAndCount(context); - if (tunerTypeAndCount.first == null || tunerTypeAndCount.second == 0) { - return null; - } - 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 { - 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 (IllegalArgumentException | NullPointerException e) { - // TunerTvInputService is not enabled. - return null; - } - } - - /** - * Updates tuner input's info. - * - * @param context {@link Context} instance - */ - public static void updateTunerInputInfo(Context context) { - final Context appContext = context.getApplicationContext(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - new AsyncTask<Void, Void, TvInputInfo>() { - @Override - protected TvInputInfo doInBackground(Void... params) { - if (DEBUG) Log.d(TAG, "updateTunerInputInfo()"); - return buildTunerInputInfo(appContext); - } - - @Override - @TargetApi(Build.VERSION_CODES.N) - protected void onPostExecute(TvInputInfo info) { - if (info != null) { - ((TvInputManager) appContext.getSystemService(Context.TV_INPUT_SERVICE)) - .updateTvInputInfo(info); - if (DEBUG) { - Log.d( - TAG, - "TvInputInfo [" - + info.loadLabel(appContext) - + "] updated: " - + info.toString()); - } - } else { - if (DEBUG) { - Log.d(TAG, "Updating tuner input info failed. Input is not ready yet."); - } - } - } - }.execute(); - } - } -}
\ No newline at end of file |