diff options
Diffstat (limited to 'tuner/sampletunertvinput/src/com/android')
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); + } + } + }; + } +} |