diff options
Diffstat (limited to 'tuner/sampletunertvinput')
5 files changed, 422 insertions, 24 deletions
diff --git a/tuner/sampletunertvinput/Android.bp b/tuner/sampletunertvinput/Android.bp index 9d737c84..4e5900bb 100644 --- a/tuner/sampletunertvinput/Android.bp +++ b/tuner/sampletunertvinput/Android.bp @@ -14,6 +14,10 @@ // limitations under the License. // +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + android_app { name: "sampletunertvinput", srcs: ["src/**/*.java"], @@ -25,7 +29,6 @@ android_app { platform_apis: true, system_ext_specific: true, - privileged: true, certificate: "platform", // product_specific: true, @@ -53,10 +56,19 @@ android_app { "tv-lib-dagger-android", "tv-test-common", ], + optional_uses_libs: ["com.android.libraries.tv.tvsystem"], aaptflags: ["-0 .ts"], plugins: [ "tv-auto-value", "tv-auto-factory", ], + required: ["com.android.tv.samples.sampletunertvinput.xml"], // min_sdk_version: "29", } + +prebuilt_etc { + name: "com.android.tv.samples.sampletunertvinput.xml", + sub_dir: "permissions", + src: "com.android.tv.samples.sampletunertvinput.xml", + system_ext_specific: true, +} diff --git a/tuner/sampletunertvinput/AndroidManifest.xml b/tuner/sampletunertvinput/AndroidManifest.xml index d282889a..8b25d0bf 100644 --- a/tuner/sampletunertvinput/AndroidManifest.xml +++ b/tuner/sampletunertvinput/AndroidManifest.xml @@ -43,7 +43,8 @@ android:theme="@android:style/Theme.Holo.Light.NoActionBar" android:appComponentFactory="android.support.v4.app.CoreComponentFactory" > <uses-library android:name="com.android.libraries.tv.tvsystem" android:required="false" /> - <activity android:name=".SampleTunerTvInputSetupActivity" > + <activity android:name=".SampleTunerTvInputSetupActivity" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> </intent-filter> @@ -51,7 +52,8 @@ <service android:name=".SampleTunerTvInputService" android:permission="android.permission.BIND_TV_INPUT" android:label="@string/sample_tuner_tv_input" - android:process="com.android.tv.samples.sampletunertvinput"> + android:process="com.android.tv.samples.sampletunertvinput" + android:exported="true"> <intent-filter> <action android:name="android.media.tv.TvInputService" /> </intent-filter> diff --git a/tuner/sampletunertvinput/com.android.tv.samples.sampletunertvinput.xml b/tuner/sampletunertvinput/com.android.tv.samples.sampletunertvinput.xml new file mode 100644 index 00000000..d98e36ce --- /dev/null +++ b/tuner/sampletunertvinput/com.android.tv.samples.sampletunertvinput.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<permissions> + <privapp-permissions package="com.android.tv.samples.sampletunertvinput"> + <permission name="android.permission.ACCESS_TV_DESCRAMBLER"/> + <permission name="android.permission.ACCESS_TV_TUNER"/> + <permission name="android.permission.TUNER_RESOURCE_ACCESS"/> + </privapp-permissions> +</permissions> diff --git a/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/AndroidManifest.xml b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/AndroidManifest.xml index 8fc96b2a..909e2431 100644 --- a/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/AndroidManifest.xml +++ b/tuner/sampletunertvinput/src/com/android/tv/samples/sampletunertvinput/AndroidManifest.xml @@ -39,7 +39,8 @@ android:icon="@mipmap/ic_launcher" android:theme="@android:style/Theme.Holo.Light.NoActionBar" android:appComponentFactory="android.support.v4.app.CoreComponentFactory" > - <activity android:name=".SampleTunerTvInputSetupActivity" > + <activity android:name=".SampleTunerTvInputSetupActivity" + android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> </intent-filter> @@ -47,7 +48,8 @@ <service android:name=".SampleTunerTvInputService" android:permission="android.permission.BIND_TV_INPUT" android:label="@string/sample_tuner_tv_input" - android:process="com.android.tv.samples.sampletunertvinput"> + android:process="com.android.tv.samples.sampletunertvinput" + android:exported="true"> <intent-filter> <action android:name="android.media.tv.TvInputService" /> </intent-filter> 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 +} |