aboutsummaryrefslogtreecommitdiff
path: root/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputService.java
diff options
context:
space:
mode:
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.java412
1 files changed, 393 insertions, 19 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 6ac95353..03e79650 100644
--- a/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputService.java
+++ b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/SampleTunerTvInputService.java
@@ -1,13 +1,40 @@
package com.android.tv.samples.sampletunertvinput;
+import static android.media.tv.TvInputManager.VIDEO_UNAVAILABLE_REASON_UNKNOWN;
+
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.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.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 java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
/** SampleTunerTvInputService */
@@ -15,8 +42,39 @@ 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 MediaFormat VIDEO_FORMAT;
+
+ static {
+ // format extracted for the specific input file
+ VIDEO_FORMAT = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 320, 240);
+ VIDEO_FORMAT.setInteger(MediaFormat.KEY_TRACK_ID, 1);
+ VIDEO_FORMAT.setLong(MediaFormat.KEY_DURATION, 9933333);
+ VIDEO_FORMAT.setInteger(MediaFormat.KEY_LEVEL, 32);
+ 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});
+ VIDEO_FORMAT.setByteBuffer("csd-0", csd);
+ csd = ByteBuffer.wrap(new byte[] {0, 0, 0, 1, 104, -50, 60, -128});
+ VIDEO_FORMAT.setByteBuffer("csd-1", csd);
+ }
+
public static final String INPUT_ID =
- "com.android.tv.samples.sampletunertvinput/.SampleTunerTvInputService";
+ "com.android.tv.samples.sampletunertvinput/.SampleTunerTvInputService";
private String mSessionId;
@Override
@@ -36,9 +94,19 @@ public class SampleTunerTvInputService extends TvInputService {
class TvInputSessionImpl extends Session {
- private Surface surface;
private final Context mContext;
- Tuner tuner;
+ private Handler mHandler;
+
+ private Surface mSurface;
+ private Filter mAudioFilter;
+ private Filter mVideoFilter;
+ private DvrPlayback mDvr;
+ private Tuner mTuner;
+ private MediaCodec mMediaCodec;
+ private Thread mDecoderThread;
+ private Deque<MediaEvent> mDataQueue;
+ private List<MediaEvent> mSavedData;
+ private boolean mDataReady = false;
public TvInputSessionImpl(Context context) {
@@ -51,6 +119,30 @@ public class SampleTunerTvInputService extends TvInputService {
if (DEBUG) {
Log.d(TAG, "onRelease");
}
+ if (mDecoderThread != null) {
+ mDecoderThread.interrupt();
+ mDecoderThread = null;
+ }
+ if (mMediaCodec != null) {
+ mMediaCodec.release();
+ mMediaCodec = null;
+ }
+ if (mAudioFilter != null) {
+ mAudioFilter.close();
+ }
+ if (mVideoFilter != null) {
+ mVideoFilter.close();
+ }
+ if (mDvr != null) {
+ mDvr.close();
+ mDvr = null;
+ }
+ if (mTuner != null) {
+ mTuner.close();
+ mTuner = null;
+ }
+ mDataQueue = null;
+ mSavedData = null;
}
@Override
@@ -58,7 +150,7 @@ public class SampleTunerTvInputService extends TvInputService {
if (DEBUG) {
Log.d(TAG, "onSetSurface");
}
- this.surface = surface;
+ this.mSurface = surface;
return true;
}
@@ -74,20 +166,16 @@ public class SampleTunerTvInputService extends TvInputService {
if (DEBUG) {
Log.d(TAG, "onTune " + uri);
}
- tuner = new Tuner(mContext, mSessionId,
- TvInputService.PRIORITY_HINT_USE_CASE_TYPE_LIVE);
-
- int feCount = tuner.getFrontendIds().size();
- if (feCount <= 0) return false;
-
- AtscFrontendSettings settings =
- AtscFrontendSettings
- .builder()
- .setFrequency(2000)
- .setModulation(AtscFrontendSettings.MODULATION_AUTO)
- .build();
- tuner.tune(settings);
-
+ if (!initCodec()) {
+ Log.e(TAG, "null codec!");
+ return false;
+ }
+ mHandler = new Handler();
+ mDecoderThread =
+ new Thread(
+ this::decodeInternal,
+ "sample-tuner-tis-decoder-thread");
+ mDecoderThread.start();
return true;
}
@@ -97,5 +185,291 @@ public class SampleTunerTvInputService extends TvInputService {
Log.d(TAG, "onSetCaptionEnabled " + b);
}
}
+
+ 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());
+ }
+ }
+ }
+
+ @Override
+ public void onFilterStatusChanged(Filter filter, int status) {
+ if (DEBUG) {
+ Log.d(TAG, "onFilterEvent audio, status=" + status);
+ }
+ }
+ });
+ AvSettings settings =
+ AvSettings.builder(Filter.TYPE_TS, true).setPassthrough(false).build();
+ audioFilter.configure(
+ TsFilterConfiguration.builder().setTpid(AUDIO_TPID)
+ .setSettings(settings).build());
+ return audioFilter;
+ }
+
+ 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);
+ }
+ }
+ }
+ }
+
+ @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;
+ }
+ }
+ });
+ 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);
+ }
+ });
+ 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);
+ if (DEBUG) {
+ Log.d(TAG, "read=" + read);
+ }
+ }
+ });
+ mTuner.tune(feSettings);
+ }
+
+ private boolean initCodec() {
+ if (mMediaCodec != null) {
+ mMediaCodec.release();
+ mMediaCodec = null;
+ }
+ try {
+ mMediaCodec = MediaCodec.createDecoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
+ mMediaCodec.configure(VIDEO_FORMAT, mSurface, null, 0);
+ } catch (IOException e) {
+ Log.e(TAG, "Error in initCodec: " + e.getMessage());
+ }
+
+ if (mMediaCodec == null) {
+ Log.e(TAG, "null codec!");
+ notifyVideoUnavailable(VIDEO_UNAVAILABLE_REASON_UNKNOWN);
+ return false;
+ }
+ return true;
+ }
+
+ private void decodeInternal() {
+ mDataQueue = new ArrayDeque<>();
+ mSavedData = new ArrayList<>();
+ mTuner = new Tuner(mContext, mSessionId,
+ TvInputService.PRIORITY_HINT_USE_CASE_TYPE_LIVE);
+
+ mAudioFilter = audioFilter();
+ mVideoFilter = videoFilter();
+ mAudioFilter.start();
+ mVideoFilter.start();
+ // use dvr playback to feed the data on platform without physical tuner
+ mDvr = dvrPlayback();
+ tune();
+ mDvr.start();
+ mMediaCodec.start();
+
+ try {
+ while (!Thread.interrupted()) {
+ if (!mDataReady) {
+ Thread.sleep(100);
+ continue;
+ }
+ if (!mDataQueue.isEmpty()) {
+ if (handleDataBuffer(mDataQueue.getFirst())) {
+ // data consumed, remove.
+ mDataQueue.pollFirst();
+ }
+ }
+ if (SAVE_DATA) {
+ mDataQueue.addAll(mSavedData);
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error in decodeInternal: " + e.getMessage());
+ }
+ }
+
+ private boolean handleDataBuffer(MediaEvent mediaEvent) {
+ if (mediaEvent.getLinearBlock() == null) {
+ if (DEBUG) Log.d(TAG, "getLinearBlock() == null");
+ return true;
+ }
+ boolean success = false;
+ LinearBlock block = mediaEvent.getLinearBlock();
+ if (queueCodecInputBuffer(block, mediaEvent.getDataLength(), mediaEvent.getOffset(),
+ mediaEvent.getPts())) {
+ releaseCodecOutputBuffer();
+ success = true;
+ }
+ mediaEvent.release();
+ return success;
+ }
+
+ private boolean queueCodecInputBuffer(LinearBlock block, long sampleSize,
+ long offset, long pts) {
+ int res = mMediaCodec.dequeueInputBuffer(TIMEOUT_US);
+ if (res >= 0) {
+ ByteBuffer buffer = mMediaCodec.getInputBuffer(res);
+ if (buffer == null) {
+ 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()));
+ }
+ // 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);
+ }
+
+ mMediaCodec.queueInputBuffer(res, 0, (int) sampleSize, pts, 0);
+ } else {
+ if (DEBUG) Log.d(TAG, "queueCodecInputBuffer res=" + res);
+ return false;
+ }
+ return true;
+ }
+
+ private void releaseCodecOutputBuffer() {
+ // play frames
+ BufferInfo bufferInfo = new BufferInfo();
+ int res = mMediaCodec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US);
+ if (res >= 0) {
+ mMediaCodec.releaseOutputBuffer(res, true);
+ notifyVideoAvailable();
+ if (DEBUG) {
+ Log.d(TAG, "notifyVideoAvailable");
+ }
+ } else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
+ MediaFormat format = mMediaCodec.getOutputFormat();
+ if (DEBUG) {
+ Log.d(TAG, "releaseCodecOutputBuffer: Output format changed:" + format);
+ }
+ } else if (res == MediaCodec.INFO_TRY_AGAIN_LATER) {
+ if (DEBUG) {
+ Log.d(TAG, "releaseCodecOutputBuffer: timeout");
+ }
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "Return value of releaseCodecOutputBuffer:" + res);
+ }
+ }
+ }
+
}
-} \ No newline at end of file
+}