aboutsummaryrefslogtreecommitdiff
path: root/tuner/sampletunertvinput/src/com/android
diff options
context:
space:
mode:
Diffstat (limited to 'tuner/sampletunertvinput/src/com/android')
-rw-r--r--tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSectionParser.java341
-rw-r--r--tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputService.java450
-rw-r--r--tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSetupActivity.java158
-rw-r--r--tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputUtils.java174
4 files changed, 907 insertions, 216 deletions
diff --git a/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSectionParser.java b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSectionParser.java
new file mode 100644
index 00000000..20c73de4
--- /dev/null
+++ b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSectionParser.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2022 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.samples.sampletunertvinput;
+
+import android.util.Log;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/** Parser for ATSC PSIP sections */
+public class SampleTunerTvInputSectionParser {
+ private static final String TAG = "SampleTunerTvInput";
+ private static final boolean DEBUG = true;
+
+ public static final byte DESCRIPTOR_TAG_EXTENDED_CHANNEL_NAME = (byte) 0xa0;
+ public static final byte COMPRESSION_TYPE_NO_COMPRESSION = (byte) 0x00;
+ public static final byte MODE_UTF16 = (byte) 0x3f;
+
+ /**
+ * Parses a single TVCT section, as defined in A/65 6.4
+ * @param data, a ByteBuffer containing a single TVCT section which describes only one channel
+ * @return null if there is an error while parsing, the channel with parsed data otherwise
+ */
+ public static TvctChannelInfo parseTvctSection(byte[] data) {
+ if (!checkValidPsipSection(data)) {
+ return null;
+ }
+ int numChannels = data[9] & 0xff;
+ if(numChannels != 1) {
+ Log.e(TAG, "parseTVCTSection expected 1 channel, found " + numChannels);
+ return null;
+ }
+ // TVCT Sections are a minimum of 16 bytes, with a minimum of 32 bytes per channel
+ if(data.length < 48) {
+ Log.e(TAG, "parseTVCTSection found section under minimum length");
+ return null;
+ }
+
+ // shortName begins at data[10] and ends at either the first stuffing
+ // UTF-16 character of value 0x0000, or at a length of 14 Bytes
+ int shortNameLength = 14;
+ for(int i = 0; i < 14; i += 2) {
+ int charValue = ((data[10 + i] & 0xff) << 8) | (data[10 + (i + 1)] & 0xff);
+ if (charValue == 0x0000) {
+ shortNameLength = i;
+ break;
+ }
+ }
+ // Data field positions are as defined by A/65 Section 6.4 for one channel
+ String name = new String(Arrays.copyOfRange(data, 10, 10 + shortNameLength),
+ StandardCharsets.UTF_16);
+ int majorNumber = ((data[24] & 0x0f) << 6) | ((data[25] & 0xff) >> 2);
+ int minorNumber = ((data[25] & 0x03) << 8) | (data[26] & 0xff);
+ if (DEBUG) {
+ Log.d(TAG, "parseTVCTSection found shortName: " + name
+ + " channel number: " + majorNumber + "-" + minorNumber);
+ }
+ int descriptorsLength = ((data[40] & 0x03) << 8) | (data[41] & 0xff);
+ List<TsDescriptor> descriptors = parseDescriptors(data, 42, 42 + descriptorsLength);
+ for (TsDescriptor descriptor : descriptors) {
+ if (descriptor instanceof ExtendedChannelNameDescriptor) {
+ ExtendedChannelNameDescriptor longNameDescriptor =
+ (ExtendedChannelNameDescriptor)descriptor;
+ name = longNameDescriptor.getLongChannelName();
+ if (DEBUG) {
+ Log.d(TAG, "parseTVCTSection found longName: " + name);
+ }
+ }
+ }
+
+ return new TvctChannelInfo(name, majorNumber, minorNumber);
+ }
+
+ /**
+ * Parses a single EIT section, as defined in ATSC A/65 Section 6.5
+ * @param data, a byte array containing a single EIT section which describes only one event
+ * @return {@code null} if there is an error while parsing, the event with parsed data otherwise
+ */
+ public static EitEventInfo parseEitSection(byte[] data) {
+ if (!checkValidPsipSection(data)) {
+ return null;
+ }
+ int numEvents = data[9] & 0xff;
+ if(numEvents != 1) {
+ Log.e(TAG, "parseEitSection expected 1 event, found " + numEvents);
+ return null;
+ }
+ // EIT Sections are a minimum of 14 bytes, with a minimum of 12 bytes per event
+ if(data.length < 26) {
+ Log.e(TAG, "parseEitSection found section under minimum length");
+ return null;
+ }
+
+ // Data field positions are as defined by A/65 Section 6.5 for one event
+ int lengthInSeconds = ((data[16] & 0x0f) << 16) | ((data[17] & 0xff) << 8)
+ | (data[18] & 0xff);
+ int titleLength = data[19] & 0xff;
+ String titleText = parseMultipleStringStructure(data, 20, 20 + titleLength);
+
+ if (DEBUG) {
+ Log.d(TAG, "parseEitSection found titleText: " + titleText
+ + " lengthInSeconds: " + lengthInSeconds);
+ }
+ return new EitEventInfo(titleText, lengthInSeconds);
+ }
+
+
+ // Descriptor data structure defined in ISO/IEC 13818-1 Section 2.6
+ // Returns an empty list on parsing failures
+ private static List<TsDescriptor> parseDescriptors(byte[] data, int offset, int limit) {
+ List<TsDescriptor> descriptors = new ArrayList<>();
+ if (data.length < limit) {
+ Log.e(TAG, "parseDescriptors given limit larger than data");
+ return descriptors;
+ }
+ int pos = offset;
+ while (pos + 1 < limit) {
+ int tag = data[pos] & 0xff;
+ int length = data[pos + 1] & 0xff;
+ if (length <= 0) {
+ continue;
+ }
+ pos += 2;
+
+ if (limit < pos + length) {
+ Log.e(TAG, "parseDescriptors found descriptor with length longer than limit");
+ break;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "parseDescriptors found descriptor with tag: " + tag);
+ }
+ TsDescriptor descriptor = null;
+ switch ((byte) tag) {
+ case DESCRIPTOR_TAG_EXTENDED_CHANNEL_NAME:
+ descriptor = parseExtendedChannelNameDescriptor(data, pos, pos + length);
+ break;
+ default:
+ break;
+ }
+ if (descriptor != null) {
+ descriptors.add(descriptor);
+ }
+ pos += length;
+ }
+ return descriptors;
+ }
+
+ // ExtendedChannelNameDescriptor is defined in ATSC A/65 Section 6.9.4 as containing only
+ // a single MultipleStringStructure after its tag and length.
+ // @return {@code null} if parsing MultipleStringStructure fails
+ private static ExtendedChannelNameDescriptor parseExtendedChannelNameDescriptor(byte[] data,
+ int offset, int limit) {
+ String channelName = parseMultipleStringStructure(data, offset, limit);
+ return channelName == null ? null : new ExtendedChannelNameDescriptor(channelName);
+ }
+
+ // MultipleStringStructure is defined in ATSC A/65 Section 6.10
+ // Returns first string segment with supported compression and mode
+ // @return {@code null} on invalid data or no supported string segments
+ private static String parseMultipleStringStructure(byte[] data, int offset, int limit) {
+ if (limit < offset + 8) {
+ Log.e(TAG, "parseMultipleStringStructure given too little data");
+ return null;
+ }
+
+ int numStrings = data[offset] & 0xff;
+ if (numStrings <= 0) {
+ Log.e(TAG, "parseMultipleStringStructure found no strings");
+ return null;
+ }
+ int pos = offset + 1;
+ for (int i = 0; i < numStrings; i++) {
+ if (limit < pos + 4) {
+ Log.e(TAG, "parseMultipleStringStructure ran out of data");
+ return null;
+ }
+ int numSegments = data[pos + 3] & 0xff;
+ pos += 4;
+ for (int j = 0; j < numSegments; j++) {
+ if (limit < pos + 3) {
+ Log.e(TAG, "parseMultipleStringStructure ran out of data");
+ return null;
+ }
+ int compressionType = data[pos] & 0xff;
+ int mode = data[pos + 1] & 0xff;
+ int numBytes = data[pos + 2] & 0xff;
+ pos += 3;
+ if (data.length < pos + numBytes) {
+ Log.e(TAG, "parseMultipleStringStructure ran out of data");
+ return null;
+ }
+ if (compressionType == COMPRESSION_TYPE_NO_COMPRESSION && mode == MODE_UTF16) {
+ return new String(data, pos, numBytes, StandardCharsets.UTF_16);
+ }
+ pos += numBytes;
+ }
+ }
+
+ Log.e(TAG, "parseMultipleStringStructure found no supported segments");
+ return null;
+ }
+
+ private static boolean checkValidPsipSection(byte[] data) {
+ if (data.length < 13) {
+ Log.e(TAG, "Section was too small");
+ return false;
+ }
+ if ((data[0] & 0xff) == 0xff) {
+ // Should clear stuffing bytes as detailed by H222.0 section 2.4.4.
+ Log.e(TAG, "Unexpected stuffing bytes while parsing section");
+ return false;
+ }
+ int sectionLength = (((data[1] & 0x0f) << 8) | (data[2] & 0xff)) + 3;
+ if (sectionLength != data.length) {
+ Log.e(TAG, "Length mismatch while parsing section");
+ return false;
+ }
+ int sectionNumber = data[6] & 0xff;
+ int lastSectionNumber = data[7] & 0xff;
+ if(sectionNumber > lastSectionNumber) {
+ Log.e(TAG, "Found sectionNumber > lastSectionNumber while parsing section");
+ return false;
+ }
+ // TODO: Check CRC 32/MPEG for validity
+ return true;
+ }
+
+ // Contains the portion of the data contained in the TVCT used by
+ // our SampleTunerTvInputSetupActivity
+ public static class TvctChannelInfo {
+ private final String mChannelName;
+ private final int mMajorChannelNumber;
+ private final int mMinorChannelNumber;
+
+ public TvctChannelInfo(
+ String channelName,
+ int majorChannelNumber,
+ int minorChannelNumber) {
+ mChannelName = channelName;
+ mMajorChannelNumber = majorChannelNumber;
+ mMinorChannelNumber = minorChannelNumber;
+ }
+
+ public String getChannelName() {
+ return mChannelName;
+ }
+
+ public int getMajorChannelNumber() {
+ return mMajorChannelNumber;
+ }
+
+ public int getMinorChannelNumber() {
+ return mMinorChannelNumber;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.US,
+ "ChannelName: %s ChannelNumber: %d-%d",
+ mChannelName,
+ mMajorChannelNumber,
+ mMinorChannelNumber);
+ }
+ }
+
+ /**
+ * Contains the portion of the data contained in the EIT used by
+ * our SampleTunerTvInputService
+ */
+ public static class EitEventInfo {
+ private final String mEventTitle;
+ private final int mLengthSeconds;
+
+ public EitEventInfo(
+ String eventTitle,
+ int lengthSeconds) {
+ mEventTitle = eventTitle;
+ mLengthSeconds = lengthSeconds;
+ }
+
+ public String getEventTitle() {
+ return mEventTitle;
+ }
+
+ public int getLengthSeconds() {
+ return mLengthSeconds;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.US,
+ "Event Title: %s Length in Seconds: %d",
+ mEventTitle,
+ mLengthSeconds);
+ }
+ }
+
+ /**
+ * A base class for TS descriptors
+ * For details of their structure, see ATSC A/65 Section 6.9
+ */
+ public abstract static class TsDescriptor {
+ public abstract int getTag();
+ }
+
+ public static class ExtendedChannelNameDescriptor extends TsDescriptor {
+ private final String mLongChannelName;
+
+ public ExtendedChannelNameDescriptor(String longChannelName) {
+ mLongChannelName = longChannelName;
+ }
+
+ @Override
+ public int getTag() {
+ return DESCRIPTOR_TAG_EXTENDED_CHANNEL_NAME;
+ }
+
+ public String getLongChannelName() {
+ return mLongChannelName;
+ }
+ }
+}
diff --git a/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputService.java b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputService.java
index 03e79650..d59ccd9d 100644
--- a/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputService.java
+++ b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputService.java
@@ -1,34 +1,31 @@
package com.android.tv.samples.sampletunertvinput;
+import static android.media.tv.TvInputManager.VIDEO_UNAVAILABLE_REASON_TUNING;
import static android.media.tv.TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN;
+import android.content.ContentUris;
+import android.content.ContentValues;
import android.content.Context;
import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo;
-import android.media.MediaCodec.LinearBlock;
import android.media.MediaFormat;
+import android.media.tv.TvContract;
import android.media.tv.tuner.dvr.DvrPlayback;
import android.media.tv.tuner.dvr.DvrSettings;
-import android.media.tv.tuner.filter.AvSettings;
import android.media.tv.tuner.filter.Filter;
import android.media.tv.tuner.filter.FilterCallback;
import android.media.tv.tuner.filter.FilterEvent;
import android.media.tv.tuner.filter.MediaEvent;
-import android.media.tv.tuner.filter.TsFilterConfiguration;
-import android.media.tv.tuner.frontend.AtscFrontendSettings;
-import android.media.tv.tuner.frontend.DvbtFrontendSettings;
-import android.media.tv.tuner.frontend.FrontendSettings;
-import android.media.tv.tuner.frontend.OnTuneEventListener;
import android.media.tv.tuner.Tuner;
import android.media.tv.TvInputService;
+import android.media.tv.tuner.filter.SectionEvent;
import android.net.Uri;
import android.os.Handler;
-import android.os.HandlerExecutor;
-import android.os.ParcelFileDescriptor;
import android.util.Log;
import android.view.Surface;
-import java.io.File;
-import java.io.FileNotFoundException;
+
+import com.android.tv.common.util.Clock;
+
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
@@ -42,40 +39,31 @@ public class SampleTunerTvInputService extends TvInputService {
private static final String TAG = "SampleTunerTvInput";
private static final boolean DEBUG = true;
- private static final int AUDIO_TPID = 257;
- private static final int VIDEO_TPID = 256;
- private static final int STATUS_MASK = 0xf;
- private static final int LOW_THRESHOLD = 0x1000;
- private static final int HIGH_THRESHOLD = 0x07fff;
- private static final int FREQUENCY = 578000;
- private static final int FILTER_BUFFER_SIZE = 16000000;
- private static final int DVR_BUFFER_SIZE = 4000000;
- private static final int INPUT_FILE_MAX_SIZE = 700000;
- private static final int PACKET_SIZE = 188;
-
private static final int TIMEOUT_US = 100000;
private static final boolean SAVE_DATA = false;
- private static final String ES_FILE_NAME = "test.es";
+ private static final boolean USE_DVR = true;
+ private static final String MEDIA_INPUT_FILE_NAME = "media.ts";
private static final MediaFormat VIDEO_FORMAT;
static {
// format extracted for the specific input file
- VIDEO_FORMAT = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 320, 240);
+ VIDEO_FORMAT = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 480, 360);
VIDEO_FORMAT.setInteger(MediaFormat.KEY_TRACK_ID, 1);
- VIDEO_FORMAT.setLong(MediaFormat.KEY_DURATION, 9933333);
- VIDEO_FORMAT.setInteger(MediaFormat.KEY_LEVEL, 32);
+ VIDEO_FORMAT.setLong(MediaFormat.KEY_DURATION, 10000000);
+ VIDEO_FORMAT.setInteger(MediaFormat.KEY_LEVEL, 256);
VIDEO_FORMAT.setInteger(MediaFormat.KEY_PROFILE, 65536);
ByteBuffer csd = ByteBuffer.wrap(
- new byte[] {0, 0, 0, 1, 103, 66, -64, 20, -38, 5, 7, -24, 64, 0, 0, 3, 0, 64, 0,
- 0, 15, 35, -59, 10, -88});
+ new byte[] {0, 0, 0, 1, 103, 66, -64, 30, -39, 1, -32, -65, -27, -64, 68, 0, 0, 3,
+ 0, 4, 0, 0, 3, 0, -16, 60, 88, -71, 32});
VIDEO_FORMAT.setByteBuffer("csd-0", csd);
- csd = ByteBuffer.wrap(new byte[] {0, 0, 0, 1, 104, -50, 60, -128});
+ csd = ByteBuffer.wrap(new byte[] {0, 0, 0, 1, 104, -53, -125, -53, 32});
VIDEO_FORMAT.setByteBuffer("csd-1", csd);
}
public static final String INPUT_ID =
"com.android.tv.samples.sampletunertvinput/.SampleTunerTvInputService";
private String mSessionId;
+ private Uri mChannelUri;
@Override
public TvInputSessionImpl onCreateSession(String inputId, String sessionId) {
@@ -89,6 +77,9 @@ public class SampleTunerTvInputService extends TvInputService {
@Override
public TvInputSessionImpl onCreateSession(String inputId) {
+ if (DEBUG) {
+ Log.d(TAG, "onCreateSession(inputId=" + inputId + ")");
+ }
return new TvInputSessionImpl(this);
}
@@ -100,12 +91,16 @@ public class SampleTunerTvInputService extends TvInputService {
private Surface mSurface;
private Filter mAudioFilter;
private Filter mVideoFilter;
+ private Filter mSectionFilter;
private DvrPlayback mDvr;
private Tuner mTuner;
private MediaCodec mMediaCodec;
private Thread mDecoderThread;
- private Deque<MediaEvent> mDataQueue;
- private List<MediaEvent> mSavedData;
+ private Deque<MediaEventData> mDataQueue;
+ private List<MediaEventData> mSavedData;
+ private long mCurrentLoopStartTimeUs = 0;
+ private long mLastFramePtsUs = 0;
+ private boolean mVideoAvailable;
private boolean mDataReady = false;
@@ -133,6 +128,9 @@ public class SampleTunerTvInputService extends TvInputService {
if (mVideoFilter != null) {
mVideoFilter.close();
}
+ if (mSectionFilter != null) {
+ mSectionFilter.close();
+ }
if (mDvr != null) {
mDvr.close();
mDvr = null;
@@ -170,7 +168,11 @@ public class SampleTunerTvInputService extends TvInputService {
Log.e(TAG, "null codec!");
return false;
}
+ mChannelUri = uri;
mHandler = new Handler();
+ mVideoAvailable = false;
+ notifyVideoUnavailable(VIDEO_UNAVAILABLE_REASON_TUNING);
+
mDecoderThread =
new Thread(
this::decodeInternal,
@@ -186,139 +188,79 @@ public class SampleTunerTvInputService extends TvInputService {
}
}
- private Filter audioFilter() {
- Filter audioFilter = mTuner.openFilter(Filter.TYPE_TS, Filter.SUBTYPE_AUDIO,
- FILTER_BUFFER_SIZE, new HandlerExecutor(mHandler),
- new FilterCallback() {
- @Override
- public void onFilterEvent(Filter filter, FilterEvent[] events) {
- if (DEBUG) {
- Log.d(TAG, "onFilterEvent audio, size=" + events.length);
- }
- for (int i = 0; i < events.length; i++) {
- if (DEBUG) {
- Log.d(TAG, "events[" + i + "] is "
- + events[i].getClass().getSimpleName());
- }
- }
+ private FilterCallback videoFilterCallback() {
+ return new FilterCallback() {
+ @Override
+ public void onFilterEvent(Filter filter, FilterEvent[] events) {
+ if (DEBUG) {
+ Log.d(TAG, "onFilterEvent video, size=" + events.length);
+ }
+ for (int i = 0; i < events.length; i++) {
+ if (DEBUG) {
+ Log.d(TAG, "events[" + i + "] is "
+ + events[i].getClass().getSimpleName());
}
+ if (events[i] instanceof MediaEvent) {
+ MediaEvent me = (MediaEvent) events[i];
- @Override
- public void onFilterStatusChanged(Filter filter, int status) {
- if (DEBUG) {
- Log.d(TAG, "onFilterEvent audio, status=" + status);
+ MediaEventData storedEvent = MediaEventData.generateEventData(me);
+ if (storedEvent == null) {
+ continue;
+ }
+ mDataQueue.add(storedEvent);
+ if (SAVE_DATA) {
+ mSavedData.add(storedEvent);
}
}
- });
- AvSettings settings =
- AvSettings.builder(Filter.TYPE_TS, true).setPassthrough(false).build();
- audioFilter.configure(
- TsFilterConfiguration.builder().setTpid(AUDIO_TPID)
- .setSettings(settings).build());
- return audioFilter;
+ }
+ }
+
+ @Override
+ public void onFilterStatusChanged(Filter filter, int status) {
+ if (DEBUG) {
+ Log.d(TAG, "onFilterEvent video, status=" + status);
+ }
+ if (status == Filter.STATUS_DATA_READY) {
+ mDataReady = true;
+ }
+ }
+ };
}
- private Filter videoFilter() {
- Filter videoFilter = mTuner.openFilter(Filter.TYPE_TS, Filter.SUBTYPE_VIDEO,
- FILTER_BUFFER_SIZE, new HandlerExecutor(mHandler),
- new FilterCallback() {
- @Override
- public void onFilterEvent(Filter filter, FilterEvent[] events) {
- if (DEBUG) {
- Log.d(TAG, "onFilterEvent video, size=" + events.length);
- }
- for (int i = 0; i < events.length; i++) {
- if (DEBUG) {
- Log.d(TAG, "events[" + i + "] is "
- + events[i].getClass().getSimpleName());
- }
- if (events[i] instanceof MediaEvent) {
- MediaEvent me = (MediaEvent) events[i];
- mDataQueue.add(me);
- if (SAVE_DATA) {
- mSavedData.add(me);
- }
- }
- }
+ private FilterCallback sectionFilterCallback() {
+ return new FilterCallback() {
+ @Override
+ public void onFilterEvent(Filter filter, FilterEvent[] events) {
+ if (DEBUG) {
+ Log.d(TAG, "onFilterEvent section, size=" + events.length);
+ }
+ for (int i = 0; i < events.length; i++) {
+ if (DEBUG) {
+ Log.d(TAG, "events[" + i + "] is "
+ + events[i].getClass().getSimpleName());
}
-
- @Override
- public void onFilterStatusChanged(Filter filter, int status) {
+ if (events[i] instanceof SectionEvent) {
+ SectionEvent sectionEvent = (SectionEvent) events[i];
+ int dataSize = (int)sectionEvent.getDataLengthLong();
if (DEBUG) {
- Log.d(TAG, "onFilterEvent video, status=" + status);
- }
- if (status == Filter.STATUS_DATA_READY) {
- mDataReady = true;
+ Log.d(TAG, "section dataSize:" + dataSize);
}
- }
- });
- AvSettings settings =
- AvSettings.builder(Filter.TYPE_TS, false).setPassthrough(false).build();
- videoFilter.configure(
- TsFilterConfiguration.builder().setTpid(VIDEO_TPID)
- .setSettings(settings).build());
- return videoFilter;
- }
- private DvrPlayback dvrPlayback() {
- DvrPlayback dvr = mTuner.openDvrPlayback(DVR_BUFFER_SIZE, new HandlerExecutor(mHandler),
- status -> {
- if (DEBUG) {
- Log.d(TAG, "onPlaybackStatusChanged status=" + status);
+ byte[] data = new byte[dataSize];
+ filter.read(data, 0, dataSize);
+
+ handleSection(data);
}
- });
- int res = dvr.configure(
- DvrSettings.builder()
- .setStatusMask(STATUS_MASK)
- .setLowThreshold(LOW_THRESHOLD)
- .setHighThreshold(HIGH_THRESHOLD)
- .setDataFormat(DvrSettings.DATA_FORMAT_ES)
- .setPacketSize(PACKET_SIZE)
- .build());
- if (DEBUG) {
- Log.d(TAG, "config res=" + res);
- }
- String testFile = mContext.getFilesDir().getAbsolutePath() + "/" + ES_FILE_NAME;
- File file = new File(testFile);
- if (file.exists()) {
- try {
- dvr.setFileDescriptor(
- ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE));
- } catch (FileNotFoundException e) {
- Log.e(TAG, "Failed to create FD");
+ }
}
- } else {
- Log.w(TAG, "File not existing");
- }
- return dvr;
- }
- private void tune() {
- DvbtFrontendSettings feSettings = DvbtFrontendSettings.builder()
- .setFrequency(FREQUENCY)
- .setTransmissionMode(DvbtFrontendSettings.TRANSMISSION_MODE_AUTO)
- .setBandwidth(DvbtFrontendSettings.BANDWIDTH_8MHZ)
- .setConstellation(DvbtFrontendSettings.CONSTELLATION_AUTO)
- .setHierarchy(DvbtFrontendSettings.HIERARCHY_AUTO)
- .setHighPriorityCodeRate(DvbtFrontendSettings.CODERATE_AUTO)
- .setLowPriorityCodeRate(DvbtFrontendSettings.CODERATE_AUTO)
- .setGuardInterval(DvbtFrontendSettings.GUARD_INTERVAL_AUTO)
- .setHighPriority(true)
- .setStandard(DvbtFrontendSettings.STANDARD_T)
- .build();
- mTuner.setOnTuneEventListener(new HandlerExecutor(mHandler), new OnTuneEventListener() {
@Override
- public void onTuneEvent(int tuneEvent) {
- if (DEBUG) {
- Log.d(TAG, "onTuneEvent " + tuneEvent);
- }
- long read = mDvr.read(INPUT_FILE_MAX_SIZE);
+ public void onFilterStatusChanged(Filter filter, int status) {
if (DEBUG) {
- Log.d(TAG, "read=" + read);
+ Log.d(TAG, "onFilterStatusChanged section, status=" + status);
}
}
- });
- mTuner.tune(feSettings);
+ };
}
private boolean initCodec() {
@@ -335,6 +277,7 @@ public class SampleTunerTvInputService extends TvInputService {
if (mMediaCodec == null) {
Log.e(TAG, "null codec!");
+ mVideoAvailable = false;
notifyVideoUnavailable(VIDEO_UNAVAILABLE_REASON_UNKNOWN);
return false;
}
@@ -347,14 +290,26 @@ public class SampleTunerTvInputService extends TvInputService {
mTuner = new Tuner(mContext, mSessionId,
TvInputService.PRIORITY_HINT_USE_CASE_TYPE_LIVE);
- mAudioFilter = audioFilter();
- mVideoFilter = videoFilter();
+ mAudioFilter = SampleTunerTvInputUtils.createAvFilter(mTuner, mHandler,
+ SampleTunerTvInputUtils.createDefaultLoggingFilterCallback("audio"), true);
+ mVideoFilter = SampleTunerTvInputUtils.createAvFilter(mTuner, mHandler,
+ videoFilterCallback(), false);
+ mSectionFilter = SampleTunerTvInputUtils.createSectionFilter(mTuner, mHandler,
+ sectionFilterCallback());
mAudioFilter.start();
mVideoFilter.start();
- // use dvr playback to feed the data on platform without physical tuner
- mDvr = dvrPlayback();
- tune();
- mDvr.start();
+ mSectionFilter.start();
+
+ // Dvr Playback can be used to read a file instead of relying on physical tuner
+ if (USE_DVR) {
+ mDvr = SampleTunerTvInputUtils.configureDvrPlayback(mTuner, mHandler,
+ DvrSettings.DATA_FORMAT_TS);
+ SampleTunerTvInputUtils.readFilePlaybackInput(getApplicationContext(), mDvr,
+ MEDIA_INPUT_FILE_NAME);
+ mDvr.start();
+ } else {
+ SampleTunerTvInputUtils.tune(mTuner, mHandler);
+ }
mMediaCodec.start();
try {
@@ -369,7 +324,10 @@ public class SampleTunerTvInputService extends TvInputService {
mDataQueue.pollFirst();
}
}
- if (SAVE_DATA) {
+ else if (SAVE_DATA) {
+ if (DEBUG) {
+ Log.d(TAG, "Adding saved data to data queue");
+ }
mDataQueue.addAll(mSavedData);
}
}
@@ -378,24 +336,50 @@ public class SampleTunerTvInputService extends TvInputService {
}
}
- private boolean handleDataBuffer(MediaEvent mediaEvent) {
- if (mediaEvent.getLinearBlock() == null) {
- if (DEBUG) Log.d(TAG, "getLinearBlock() == null");
- return true;
+ private void handleSection(byte[] data) {
+ SampleTunerTvInputSectionParser.EitEventInfo eventInfo =
+ SampleTunerTvInputSectionParser.parseEitSection(data);
+ if (eventInfo == null) {
+ Log.e(TAG, "Did not receive event info from parser");
+ return;
+ }
+
+ // We assume that our program starts at the current time
+ long startTimeMs = Clock.SYSTEM.currentTimeMillis();
+ long endTimeMs = startTimeMs + ((long)eventInfo.getLengthSeconds() * 1000);
+
+ // Remove any other programs which conflict with our start and end time
+ Uri conflictsUri =
+ TvContract.buildProgramsUriForChannel(mChannelUri, startTimeMs, endTimeMs);
+ int programsDeleted = mContext.getContentResolver().delete(conflictsUri, null, null);
+ if (DEBUG) {
+ Log.d(TAG, "Deleted " + programsDeleted + " conflicting program(s)");
+ }
+
+ // Insert our new program into the newly opened time slot
+ ContentValues values = new ContentValues();
+ values.put(TvContract.Programs.COLUMN_CHANNEL_ID, ContentUris.parseId(mChannelUri));
+ values.put(TvContract.Programs.COLUMN_START_TIME_UTC_MILLIS, startTimeMs);
+ values.put(TvContract.Programs.COLUMN_END_TIME_UTC_MILLIS, endTimeMs);
+ values.put(TvContract.Programs.COLUMN_TITLE, eventInfo.getEventTitle());
+ values.put(TvContract.Programs.COLUMN_SHORT_DESCRIPTION, "");
+ if (DEBUG) {
+ Log.d(TAG, "Inserting program with values: " + values);
}
+ mContext.getContentResolver().insert(TvContract.Programs.CONTENT_URI, values);
+ }
+
+ private boolean handleDataBuffer(MediaEventData mediaEventData) {
boolean success = false;
- LinearBlock block = mediaEvent.getLinearBlock();
- if (queueCodecInputBuffer(block, mediaEvent.getDataLength(), mediaEvent.getOffset(),
- mediaEvent.getPts())) {
+ if (queueCodecInputBuffer(mediaEventData.getData(), mediaEventData.getDataSize(),
+ mediaEventData.getPts())) {
releaseCodecOutputBuffer();
success = true;
}
- mediaEvent.release();
return success;
}
- private boolean queueCodecInputBuffer(LinearBlock block, long sampleSize,
- long offset, long pts) {
+ private boolean queueCodecInputBuffer(byte[] data, int size, long pts) {
int res = mMediaCodec.dequeueInputBuffer(TIMEOUT_US);
if (res >= 0) {
ByteBuffer buffer = mMediaCodec.getInputBuffer(res);
@@ -403,41 +387,19 @@ public class SampleTunerTvInputService extends TvInputService {
throw new RuntimeException("Null decoder input buffer");
}
- ByteBuffer data = block.map();
- if (offset > 0 && offset < data.limit()) {
- data.position((int) offset);
- } else {
- data.position(0);
- }
-
if (DEBUG) {
Log.d(
TAG,
"Decoder: Send data to decoder."
- + " Sample size="
- + sampleSize
+ " pts="
+ pts
- + " limit="
- + data.limit()
- + " pos="
- + data.position()
+ " size="
- + (data.limit() - data.position()));
+ + size);
}
// fill codec input buffer
- int size = sampleSize > data.limit() ? data.limit() : (int) sampleSize;
- if (DEBUG) Log.d(TAG, "limit " + data.limit() + " sampleSize " + sampleSize);
- if (data.hasArray()) {
- Log.d(TAG, "hasArray");
- buffer.put(data.array(), 0, size);
- } else {
- byte[] array = new byte[size];
- data.get(array, 0, size);
- buffer.put(array, 0, size);
- }
+ buffer.put(data, 0, size);
- mMediaCodec.queueInputBuffer(res, 0, (int) sampleSize, pts, 0);
+ mMediaCodec.queueInputBuffer(res, 0, size, pts, 0);
} else {
if (DEBUG) Log.d(TAG, "queueCodecInputBuffer res=" + res);
return false;
@@ -450,10 +412,43 @@ public class SampleTunerTvInputService extends TvInputService {
BufferInfo bufferInfo = new BufferInfo();
int res = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US);
if (res >= 0) {
- mMediaCodec.releaseOutputBuffer(res, true);
- notifyVideoAvailable();
+ long currentFramePtsUs = bufferInfo.presentationTimeUs;
+
+ // We know we are starting a new loop if the loop time is not set or if
+ // the current frame is before the last frame
+ if (mCurrentLoopStartTimeUs == 0 || currentFramePtsUs < mLastFramePtsUs) {
+ mCurrentLoopStartTimeUs = System.nanoTime() / 1000;
+ }
+ mLastFramePtsUs = currentFramePtsUs;
+
+ long desiredUs = mCurrentLoopStartTimeUs + currentFramePtsUs;
+ long nowUs = System.nanoTime() / 1000;
+ long sleepTimeUs = desiredUs - nowUs;
+
if (DEBUG) {
- Log.d(TAG, "notifyVideoAvailable");
+ Log.d(TAG, "currentFramePts: " + currentFramePtsUs
+ + " sleeping for: " + sleepTimeUs);
+ }
+ if (sleepTimeUs > 0) {
+ try {
+ Thread.sleep(
+ /* millis */ sleepTimeUs / 1000,
+ /* nanos */ (int) (sleepTimeUs % 1000) * 1000);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ if (DEBUG) {
+ Log.d(TAG, "InterruptedException:\n" + Log.getStackTraceString(e));
+ }
+ return;
+ }
+ }
+ mMediaCodec.releaseOutputBuffer(res, true);
+ if (!mVideoAvailable) {
+ mVideoAvailable = true;
+ notifyVideoAvailable();
+ if (DEBUG) {
+ Log.d(TAG, "notifyVideoAvailable");
+ }
}
} else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat format = mMediaCodec.getOutputFormat();
@@ -472,4 +467,75 @@ public class SampleTunerTvInputService extends TvInputService {
}
}
+
+ /**
+ * MediaEventData is a helper class which is used to hold the data within MediaEvents
+ * locally in our Java code, instead of in the position allocated by our native code
+ */
+ public static class MediaEventData {
+ private final long mPts;
+ private final int mDataSize;
+ private final byte[] mData;
+
+ public MediaEventData(long pts, int dataSize, byte[] data) {
+ mPts = pts;
+ mDataSize = dataSize;
+ mData = data;
+ }
+
+ /**
+ * Parses a MediaEvent, including copying its data and freeing the underlying LinearBlock
+ * @return {@code null} if the event has no LinearBlock
+ */
+ public static MediaEventData generateEventData(MediaEvent event) {
+ if(event.getLinearBlock() == null) {
+ if (DEBUG) {
+ Log.d(TAG, "MediaEvent had null LinearBlock");
+ }
+ return null;
+ }
+
+ ByteBuffer memoryBlock = event.getLinearBlock().map();
+ int eventOffset = (int)event.getOffset();
+ int eventDataLength = (int)event.getDataLength();
+ if (DEBUG) {
+ Log.d(TAG, "MediaEvent has length=" + eventDataLength
+ + " offset=" + eventOffset
+ + " capacity=" + memoryBlock.capacity()
+ + " limit=" + memoryBlock.limit());
+ }
+ if (eventOffset < 0 || eventDataLength < 0 || eventOffset >= memoryBlock.limit()) {
+ if (DEBUG) {
+ Log.e(TAG, "MediaEvent length or offset was invalid");
+ }
+ event.getLinearBlock().recycle();
+ event.release();
+ return null;
+ }
+ // We allow the case of eventOffset + eventDataLength > memoryBlock.limit()
+ // When it occurs, we read until memoryBlock.limit
+ int dataSize = Math.min(eventDataLength, memoryBlock.limit() - eventOffset);
+ memoryBlock.position(eventOffset);
+
+ byte[] memoryData = new byte[dataSize];
+ memoryBlock.get(memoryData, 0, dataSize);
+ MediaEventData eventData = new MediaEventData(event.getPts(), dataSize, memoryData);
+
+ event.getLinearBlock().recycle();
+ event.release();
+ return eventData;
+ }
+
+ public long getPts() {
+ return mPts;
+ }
+
+ public int getDataSize() {
+ return mDataSize;
+ }
+
+ public byte[] getData() {
+ return mData;
+ }
+ }
}
diff --git a/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSetupActivity.java b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSetupActivity.java
index b932b605..4774243e 100644
--- a/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSetupActivity.java
+++ b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputSetupActivity.java
@@ -3,48 +3,158 @@ package com.android.tv.samples.sampletunertvinput;
import android.app.Activity;
import android.content.Intent;
import android.media.tv.TvInputInfo;
+import android.media.tv.TvInputService;
+import android.media.tv.tuner.Tuner;
+import android.media.tv.tuner.dvr.DvrPlayback;
+import android.media.tv.tuner.dvr.DvrSettings;
+import android.media.tv.tuner.filter.Filter;
+import android.media.tv.tuner.filter.FilterCallback;
+import android.media.tv.tuner.filter.FilterEvent;
+import android.media.tv.tuner.filter.SectionEvent;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import com.android.tv.common.util.Clock;
import com.android.tv.testing.data.ChannelInfo;
import com.android.tv.testing.data.ChannelUtils;
import com.android.tv.testing.data.ProgramInfo;
+import com.android.tv.testing.data.ProgramUtils;
+
import java.util.Collections;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
/** Setup activity for SampleTunerTvInput */
public class SampleTunerTvInputSetupActivity extends Activity {
+ private static final String TAG = "SampleTunerTvInput";
+ private static final boolean DEBUG = true;
+
+ private static final boolean USE_DVR = true;
+ private static final String SETUP_INPUT_FILE_NAME = "setup.ts";
+
+ private Tuner mTuner;
+ private DvrPlayback mDvr;
+ private Filter mSectionFilter;
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ initTuner();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (mTuner != null) {
+ mTuner.close();
+ mTuner = null;
+ }
+ if (mDvr != null) {
+ mDvr.close();
+ mDvr = null;
+ }
+ if (mSectionFilter != null) {
+ mSectionFilter.close();
+ mSectionFilter = null;
+ }
+ }
+
+ private void setChannel(byte[] sectionData) {
+ SampleTunerTvInputSectionParser.TvctChannelInfo channelInfo =
+ SampleTunerTvInputSectionParser.parseTvctSection(sectionData);
+
+ String channelNumber = "";
+ String channelName = "";
+
+ if(channelInfo == null) {
+ Log.e(TAG, "Did not receive channel description from parser");
+ } else {
+ channelNumber = String.format(Locale.US, "%d-%d", channelInfo.getMajorChannelNumber(),
+ channelInfo.getMinorChannelNumber());
+ channelName = channelInfo.getChannelName();
+ }
+
ChannelInfo channel =
- new ChannelInfo.Builder()
- .setNumber("1-1")
- .setName("Sample Channel")
- .setLogoUrl(
- ChannelInfo.getUriStringForChannelLogo(this, 100))
- .setOriginalNetworkId(1)
- .setVideoWidth(640)
- .setVideoHeight(480)
- .setAudioChannel(2)
- .setAudioLanguageCount(1)
- .setHasClosedCaption(false)
- .setProgram(
- new ProgramInfo(
- "Sample Program",
- "",
- 0,
- 0,
- ProgramInfo.GEN_POSTER,
- "Sample description",
- ProgramInfo.GEN_DURATION,
- null,
- ProgramInfo.GEN_GENRE,
- null))
- .build();
+ new ChannelInfo.Builder()
+ .setNumber(channelNumber)
+ .setName(channelName)
+ .setLogoUrl(
+ ChannelInfo.getUriStringForChannelLogo(this, 100))
+ .setOriginalNetworkId(1)
+ .setVideoWidth(640)
+ .setVideoHeight(480)
+ .setAudioChannel(2)
+ .setAudioLanguageCount(1)
+ .setHasClosedCaption(false)
+ .build();
Intent intent = getIntent();
String inputId = intent.getStringExtra(TvInputInfo.EXTRA_INPUT_ID);
ChannelUtils.updateChannels(this, inputId, Collections.singletonList(channel));
+ ProgramUtils.updateProgramForAllChannelsOf(this, inputId, Clock.SYSTEM,
+ TimeUnit.DAYS.toMillis(1));
+
setResult(Activity.RESULT_OK);
finish();
}
+ private FilterCallback sectionFilterCallback() {
+ return new FilterCallback() {
+ @Override
+ public void onFilterEvent(Filter filter, FilterEvent[] events) {
+ if (DEBUG) {
+ Log.d(TAG, "onFilterEvent setup section, size=" + events.length);
+ }
+ for (int i = 0; i < events.length; i++) {
+ if (DEBUG) {
+ Log.d(TAG, "events[" + i + "] is "
+ + events[i].getClass().getSimpleName());
+ }
+ if (events[i] instanceof SectionEvent) {
+ SectionEvent sectionEvent = (SectionEvent) events[i];
+ int dataSize = (int)sectionEvent.getDataLengthLong();
+ if (DEBUG) {
+ Log.d(TAG, "section dataSize:" + dataSize);
+ }
+
+ byte[] data = new byte[dataSize];
+ filter.read(data, 0, dataSize);
+
+ setChannel(data);
+ }
+ }
+ }
+
+ @Override
+ public void onFilterStatusChanged(Filter filter, int status) {
+ if (DEBUG) {
+ Log.d(TAG, "onFilterStatusChanged setup section, status=" + status);
+ }
+ }
+ };
+ }
+
+ private void initTuner() {
+ mTuner = new Tuner(getApplicationContext(), null,
+ TvInputService.PRIORITY_HINT_USE_CASE_TYPE_LIVE);
+ Handler handler = new Handler(Looper.myLooper());
+
+ mSectionFilter = SampleTunerTvInputUtils.createSectionFilter(mTuner, handler,
+ sectionFilterCallback());
+ mSectionFilter.start();
+
+ // Dvr Playback can be used to read a file instead of relying on physical tuner
+ if (USE_DVR) {
+ mDvr = SampleTunerTvInputUtils.configureDvrPlayback(mTuner, handler,
+ DvrSettings.DATA_FORMAT_TS);
+ SampleTunerTvInputUtils.readFilePlaybackInput(getApplicationContext(), mDvr,
+ SETUP_INPUT_FILE_NAME);
+ mDvr.start();
+ } else {
+ SampleTunerTvInputUtils.tune(mTuner, handler);
+ }
+ }
+
}
diff --git a/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputUtils.java b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputUtils.java
new file mode 100644
index 00000000..9638f33a
--- /dev/null
+++ b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputUtils.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2022 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.samples.sampletunertvinput;
+
+import android.content.Context;
+import android.media.tv.tuner.Tuner;
+import android.media.tv.tuner.dvr.DvrPlayback;
+import android.media.tv.tuner.dvr.DvrSettings;
+import android.media.tv.tuner.filter.AvSettings;
+import android.media.tv.tuner.filter.Filter;
+import android.media.tv.tuner.filter.FilterCallback;
+import android.media.tv.tuner.filter.FilterEvent;
+import android.media.tv.tuner.filter.SectionSettingsWithSectionBits;
+import android.media.tv.tuner.filter.TsFilterConfiguration;
+import android.media.tv.tuner.frontend.DvbtFrontendSettings;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+
+public class SampleTunerTvInputUtils {
+ private static final String TAG = "SampleTunerTvInput";
+ private static final boolean DEBUG = true;
+
+ private static final int AUDIO_TPID = 257;
+ private static final int VIDEO_TPID = 256;
+ private static final int SECTION_TPID = 255;
+ private static final int FILTER_BUFFER_SIZE = 16000000;
+
+ private static final int STATUS_MASK = 0xf;
+ private static final int LOW_THRESHOLD = 0x1000;
+ private static final int HIGH_THRESHOLD = 0x07fff;
+ private static final int DVR_BUFFER_SIZE = 4000000;
+ private static final int PACKET_SIZE = 188;
+ private static final long FREQUENCY = 578000;
+ private static final int INPUT_FILE_MAX_SIZE = 1000000;
+
+ public static DvrPlayback configureDvrPlayback(Tuner tuner, Handler handler, int dataFormat) {
+ DvrPlayback dvr = tuner.openDvrPlayback(DVR_BUFFER_SIZE, new HandlerExecutor(handler),
+ status -> {
+ if (DEBUG) {
+ Log.d(TAG, "onPlaybackStatusChanged status=" + status);
+ }
+ });
+ int res = dvr.configure(
+ DvrSettings.builder()
+ .setStatusMask(STATUS_MASK)
+ .setLowThreshold(LOW_THRESHOLD)
+ .setHighThreshold(HIGH_THRESHOLD)
+ .setDataFormat(dataFormat)
+ .setPacketSize(PACKET_SIZE)
+ .build());
+ if (DEBUG) {
+ Log.d(TAG, "config res=" + res);
+ }
+ return dvr;
+ }
+
+ public static void readFilePlaybackInput(Context context, DvrPlayback dvr, String fileName) {
+ String testFile = context.getFilesDir().getAbsolutePath() + "/" + fileName;
+ File file = new File(testFile);
+ if (file.exists()) {
+ try {
+ dvr.setFileDescriptor(
+ ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE));
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "Failed to create FD");
+ }
+ } else {
+ Log.w(TAG, "File not existing");
+ }
+
+ long read = dvr.read(INPUT_FILE_MAX_SIZE);
+ if (DEBUG) {
+ Log.d(TAG, "read=" + read);
+ }
+ }
+
+ public static void tune(Tuner tuner, Handler handler) {
+ DvbtFrontendSettings feSettings = DvbtFrontendSettings.builder()
+ .setFrequencyLong(FREQUENCY)
+ .setTransmissionMode(DvbtFrontendSettings.TRANSMISSION_MODE_AUTO)
+ .setBandwidth(DvbtFrontendSettings.BANDWIDTH_8MHZ)
+ .setConstellation(DvbtFrontendSettings.CONSTELLATION_AUTO)
+ .setHierarchy(DvbtFrontendSettings.HIERARCHY_AUTO)
+ .setHighPriorityCodeRate(DvbtFrontendSettings.CODERATE_AUTO)
+ .setLowPriorityCodeRate(DvbtFrontendSettings.CODERATE_AUTO)
+ .setGuardInterval(DvbtFrontendSettings.GUARD_INTERVAL_AUTO)
+ .setHighPriority(true)
+ .setStandard(DvbtFrontendSettings.STANDARD_T)
+ .build();
+
+ tuner.setOnTuneEventListener(new HandlerExecutor(handler), tuneEvent -> {
+ if (DEBUG) {
+ Log.d(TAG, "onTuneEvent " + tuneEvent);
+ }
+ });
+
+ tuner.tune(feSettings);
+ }
+
+ public static Filter createSectionFilter(Tuner tuner, Handler handler,
+ FilterCallback callback) {
+ Filter sectionFilter = tuner.openFilter(Filter.TYPE_TS, Filter.SUBTYPE_SECTION,
+ FILTER_BUFFER_SIZE, new HandlerExecutor(handler), callback);
+
+ SectionSettingsWithSectionBits settings = SectionSettingsWithSectionBits
+ .builder(Filter.TYPE_TS).build();
+
+ sectionFilter.configure(
+ TsFilterConfiguration.builder().setTpid(SECTION_TPID)
+ .setSettings(settings).build());
+
+ return sectionFilter;
+ }
+
+ public static Filter createAvFilter(Tuner tuner, Handler handler,
+ FilterCallback callback, boolean isAudio) {
+ Filter avFilter = tuner.openFilter(Filter.TYPE_TS,
+ isAudio ? Filter.SUBTYPE_AUDIO : Filter.SUBTYPE_VIDEO,
+ FILTER_BUFFER_SIZE,
+ new HandlerExecutor(handler),
+ callback);
+
+ AvSettings settings =
+ AvSettings.builder(Filter.TYPE_TS, isAudio).setPassthrough(false).build();
+ avFilter.configure(
+ TsFilterConfiguration.builder().
+ setTpid(isAudio ? AUDIO_TPID : VIDEO_TPID)
+ .setSettings(settings).build());
+ return avFilter;
+ }
+
+ public static FilterCallback createDefaultLoggingFilterCallback(String filterType) {
+ return new FilterCallback() {
+ @Override
+ public void onFilterEvent(Filter filter, FilterEvent[] events) {
+ if (DEBUG) {
+ Log.d(TAG, "onFilterEvent " + filterType + ", size=" + events.length);
+ }
+ for (int i = 0; i < events.length; i++) {
+ if (DEBUG) {
+ Log.d(TAG, "events[" + i + "] is "
+ + events[i].getClass().getSimpleName());
+ }
+ }
+ }
+
+ @Override
+ public void onFilterStatusChanged(Filter filter, int status) {
+ if (DEBUG) {
+ Log.d(TAG, "onFilterStatusChanged " + filterType + ", status=" + status);
+ }
+ }
+ };
+ }
+}