diff options
Diffstat (limited to 'tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputService.java')
-rw-r--r-- | tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputService.java | 450 |
1 files changed, 258 insertions, 192 deletions
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; + } + } } |