aboutsummaryrefslogtreecommitdiff
path: root/tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java
diff options
context:
space:
mode:
Diffstat (limited to 'tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java')
-rw-r--r--tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java413
1 files changed, 413 insertions, 0 deletions
diff --git a/tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java b/tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java
new file mode 100644
index 00000000..0e9bd357
--- /dev/null
+++ b/tuner/tests/unittests/javatests/com/android/tv/tuner/ZappingTimeTest.java
@@ -0,0 +1,413 @@
+/*
+ * Copyright (C) 2016 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.tuner;
+
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.support.test.filters.LargeTest;
+import android.test.InstrumentationTestCase;
+import android.util.Log;
+import android.view.Surface;
+import com.android.tv.tuner.data.Cea708Data;
+import com.android.tv.tuner.data.PsiData;
+import com.android.tv.tuner.data.PsipData;
+import com.android.tv.tuner.data.TunerChannel;
+import com.android.tv.tuner.data.nano.Channel;
+import com.android.tv.tuner.exoplayer.MpegTsPlayer;
+import com.android.tv.tuner.exoplayer.MpegTsRendererBuilder;
+import com.android.tv.tuner.exoplayer.buffer.BufferManager;
+import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager;
+import com.android.tv.tuner.source.TsDataSourceManager;
+import com.android.tv.tuner.tvinput.EventDetector;
+import com.android.tv.tuner.tvinput.PlaybackBufferListener;
+import com.google.android.exoplayer.ExoPlayer;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import org.junit.Ignore;
+
+/** This class use {@link FileTunerHal} to simulate tunerhal's actions to test zapping time. */
+@LargeTest
+public class ZappingTimeTest extends InstrumentationTestCase {
+ private static final String TAG = "ZappingTimeTest";
+ private static final boolean DEBUG = false;
+ private static final int TS_COPY_BUFFER_SIZE = 1024 * 512;
+ private static final int PROGRAM_NUMBER = 1;
+ private static final int VIDEO_PID = 49;
+ private static final int PCR_PID = 49;
+ private static final List<Integer> AUDIO_PIDS = Arrays.asList(51, 52, 53);
+ private static final long BUFFER_SIZE_DEF = 2 * 1024;
+ private static final int FREQUENCY = -1;
+ private static final String MODULATION = "";
+ private static final long ZAPPING_TIME_OUT_MS = 10000;
+ private static final long MAX_AVERAGE_ZAPPING_TIME_MS = 4000;
+ private static final int TEST_ITERATION_COUNT = 10;
+ private static final int STRESS_ZAPPING_TEST_COUNT = 50;
+ private static final long SKIP_DURATION_MS_TO_ADD = 200;
+ private static final String TEST_TS_FILE_PATH = "capture_kqed.ts";
+
+ private static final int MSG_START_PLAYBACK = 1;
+
+ private TunerChannel mChannel;
+ private FileTunerHal mTunerHal;
+ private MpegTsPlayer mPlayer;
+ private TsDataSourceManager mSourceManager;
+ private Handler mHandler;
+ private Context mTargetContext;
+ private File mTrickplayBufferDir;
+ private Surface mSurface;
+ private CountDownLatch mErrorLatch;
+ private CountDownLatch mDrawnToSurfaceLatch;
+ private CountDownLatch mWaitTuneExecuteLatch;
+ private AtomicLong mOnDrawnToSurfaceTimeMs = new AtomicLong(0);
+ private MockMpegTsPlayerListener mMpegTsPlayerListener = new MockMpegTsPlayerListener();
+ private MockPlaybackBufferListener mPlaybackBufferListener = new MockPlaybackBufferListener();
+ private MockEventListener mEventListener = new MockEventListener();
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mTargetContext = getInstrumentation().getTargetContext();
+ mTrickplayBufferDir = mTargetContext.getCacheDir();
+ HandlerThread handlerThread = new HandlerThread(TAG);
+ handlerThread.start();
+ List<PsiData.PmtItem> pmtItems = new ArrayList<>();
+ pmtItems.add(new PsiData.PmtItem(Channel.VideoStreamType.MPEG2, VIDEO_PID, null, null));
+ for (int audioPid : AUDIO_PIDS) {
+ pmtItems.add(
+ new PsiData.PmtItem(Channel.AudioStreamType.A52AC3AUDIO, audioPid, null, null));
+ }
+
+ Context context = getInstrumentation().getContext();
+ // Since assets and resource files are compressed, random access to the specified offset
+ // in assets or resource files will add some delay which is proportional to the offset.
+ // So the TS stream asset file are copied to a cache file, and the starting stream position
+ // in the file will be accessed by underlying {@link RandomAccessFile}.
+ File tsCacheFile = createCacheFile(context, mTargetContext, TEST_TS_FILE_PATH);
+ pmtItems.add(new PsiData.PmtItem(0x100, PCR_PID, null, null));
+ mChannel = new TunerChannel(PROGRAM_NUMBER, pmtItems);
+ mChannel.setFrequency(FREQUENCY);
+ mChannel.setModulation(MODULATION);
+ mTunerHal = new FileTunerHal(context, tsCacheFile);
+ mTunerHal.openFirstAvailable();
+ mSourceManager = TsDataSourceManager.createSourceManager(false);
+ mSourceManager.addTunerHalForTest(mTunerHal);
+ mHandler =
+ new Handler(
+ handlerThread.getLooper(),
+ new Handler.Callback() {
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_START_PLAYBACK:
+ {
+ mHandler.removeCallbacksAndMessages(null);
+ stopPlayback();
+ mOnDrawnToSurfaceTimeMs.set(0);
+ mDrawnToSurfaceLatch = new CountDownLatch(1);
+ if (mWaitTuneExecuteLatch != null) {
+ mWaitTuneExecuteLatch.countDown();
+ }
+ int frequency = msg.arg1;
+ boolean useSimpleSampleBuffer = (msg.arg2 == 1);
+ BufferManager bufferManager = null;
+ if (!useSimpleSampleBuffer) {
+ bufferManager =
+ new BufferManager(
+ new TrickplayStorageManager(
+ mTargetContext,
+ mTrickplayBufferDir,
+ 1024L
+ * 1024L
+ * BUFFER_SIZE_DEF));
+ }
+ mChannel.setFrequency(frequency);
+ mSourceManager.setKeepTuneStatus(true);
+ mPlayer =
+ new MpegTsPlayer(
+ new MpegTsRendererBuilder(
+ mTargetContext,
+ bufferManager,
+ mPlaybackBufferListener),
+ mHandler,
+ mSourceManager,
+ null,
+ mMpegTsPlayerListener);
+ mPlayer.setCaptionServiceNumber(
+ Cea708Data.EMPTY_SERVICE_NUMBER);
+ mPlayer.prepare(
+ mTargetContext,
+ mChannel,
+ false,
+ mEventListener);
+ return true;
+ }
+ default:
+ {
+ Log.i(TAG, "Unhandled message code: " + msg.what);
+ return true;
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ if (mPlayer != null) {
+ mPlayer.release();
+ }
+ if (mSurface != null) {
+ mSurface.release();
+ }
+ mHandler.getLooper().quitSafely();
+ super.tearDown();
+ }
+
+ public void testZappingTime() {
+ zappingTimeTest(false, TEST_ITERATION_COUNT, true);
+ }
+
+ public void testZappingTimeWithSimpleSampleBuffer() {
+ zappingTimeTest(true, TEST_ITERATION_COUNT, true);
+ }
+
+ @Ignore("b/69978026")
+ @SuppressWarnings("JUnit4ClassUsedInJUnit3")
+ public void testStressZapping() {
+ zappingTimeTest(false, STRESS_ZAPPING_TEST_COUNT, false);
+ }
+
+ @Ignore("b/69978093")
+ @SuppressWarnings("JUnit4ClassUsedInJUnit3")
+ public void testZappingWithPacketMissing() {
+ mTunerHal.setEnablePacketMissing(true);
+ mTunerHal.setEnableArtificialDelay(true);
+ SurfaceTexture surfaceTexture = new SurfaceTexture(0);
+ mSurface = new Surface(surfaceTexture);
+ long zappingStartTimeMs = System.currentTimeMillis();
+ mErrorLatch = new CountDownLatch(1);
+ mHandler.obtainMessage(MSG_START_PLAYBACK, FREQUENCY, 0).sendToTarget();
+ boolean errorAppeared = false;
+ while (System.currentTimeMillis() - zappingStartTimeMs < ZAPPING_TIME_OUT_MS) {
+ try {
+ errorAppeared = mErrorLatch.await(100, TimeUnit.MILLISECONDS);
+ if (errorAppeared) {
+ break;
+ }
+ } catch (InterruptedException e) {
+ }
+ }
+ assertFalse("Error happened when packet lost", errorAppeared);
+ }
+
+ private static File createCacheFile(Context context, Context targetContext, String filename)
+ throws IOException {
+ File cacheFile = new File(targetContext.getCacheDir(), filename);
+
+ if (cacheFile.createNewFile() == false) {
+ cacheFile.delete();
+ cacheFile.createNewFile();
+ }
+
+ InputStream inputStream = context.getResources().getAssets().open(filename);
+ FileOutputStream fileOutputStream = new FileOutputStream(cacheFile);
+
+ byte[] buffer = new byte[TS_COPY_BUFFER_SIZE];
+ while (inputStream.read(buffer, 0, TS_COPY_BUFFER_SIZE) != -1) {
+ fileOutputStream.write(buffer);
+ }
+
+ fileOutputStream.close();
+ inputStream.close();
+
+ return cacheFile;
+ }
+
+ private void zappingTimeTest(
+ boolean useSimpleSampleBuffer, int testIterationCount, boolean enableArtificialDelay) {
+ String bufferManagerLogString =
+ !enableArtificialDelay
+ ? "for stress test"
+ : useSimpleSampleBuffer ? "with simple sample buffer" : "";
+ SurfaceTexture surfaceTexture = new SurfaceTexture(0);
+ mSurface = new Surface(surfaceTexture);
+ mTunerHal.setEnablePacketMissing(false);
+ mTunerHal.setEnableArtificialDelay(enableArtificialDelay);
+ double totalZappingTime = 0.0;
+ for (int i = 0; i < testIterationCount; i++) {
+ mWaitTuneExecuteLatch = new CountDownLatch(1);
+ long zappingStartTimeMs = System.currentTimeMillis();
+ mTunerHal.setInitialSkipMs(SKIP_DURATION_MS_TO_ADD * (i % TEST_ITERATION_COUNT));
+ mHandler.obtainMessage(MSG_START_PLAYBACK, FREQUENCY + i, useSimpleSampleBuffer ? 1 : 0)
+ .sendToTarget();
+ try {
+ mWaitTuneExecuteLatch.await();
+ } catch (InterruptedException e) {
+ }
+ boolean drawnToSurface = false;
+ while (System.currentTimeMillis() - zappingStartTimeMs < ZAPPING_TIME_OUT_MS) {
+ try {
+ drawnToSurface = mDrawnToSurfaceLatch.await(100, TimeUnit.MILLISECONDS);
+ if (drawnToSurface) {
+ break;
+ }
+ } catch (InterruptedException e) {
+ }
+ }
+ if (i == 0) {
+ continue;
+ // Get rid of the first result, which shows outlier often.
+ }
+ // In 10s, all zapping request will finish. Set the maximum zapping time as 10s could be
+ // reasonable.
+ totalZappingTime +=
+ (mOnDrawnToSurfaceTimeMs.get() > 0
+ ? mOnDrawnToSurfaceTimeMs.get() - zappingStartTimeMs
+ : ZAPPING_TIME_OUT_MS);
+ }
+ double averageZappingTime = totalZappingTime / (testIterationCount - 1);
+ Log.i(TAG, "Average zapping time " + bufferManagerLogString + ":" + averageZappingTime);
+ assertTrue(
+ "Average Zapping time "
+ + bufferManagerLogString
+ + " is too large:"
+ + averageZappingTime,
+ averageZappingTime < MAX_AVERAGE_ZAPPING_TIME_MS);
+ }
+
+ private void stopPlayback() {
+ if (mPlayer != null) {
+ mPlayer.setPlayWhenReady(false);
+ mPlayer.release();
+ mPlayer = null;
+ }
+ }
+
+ private class MockMpegTsPlayerListener implements MpegTsPlayer.Listener {
+
+ @Override
+ public void onStateChanged(boolean playWhenReady, int playbackState) {
+ if (DEBUG) {
+ Log.d(TAG, "ExoPlayer state change: " + playbackState + " " + playWhenReady);
+ }
+ if (playbackState == ExoPlayer.STATE_READY) {
+ mPlayer.setSurface(mSurface);
+ mPlayer.setPlayWhenReady(true);
+ mPlayer.setVolume(1.0f);
+ }
+ }
+
+ @Override
+ public void onError(Exception e) {
+ if (DEBUG) {
+ Log.d(TAG, "onError");
+ }
+ if (mErrorLatch != null) {
+ mErrorLatch.countDown();
+ }
+ }
+
+ @Override
+ public void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) {
+ if (DEBUG) {
+ Log.d(TAG, "onVideoSizeChanged");
+ }
+ }
+
+ @Override
+ public void onDrawnToSurface(MpegTsPlayer player, Surface surface) {
+ if (DEBUG) {
+ Log.d(TAG, "onDrawnToSurface");
+ }
+ mOnDrawnToSurfaceTimeMs.set(System.currentTimeMillis());
+ if (mDrawnToSurfaceLatch != null) {
+ mDrawnToSurfaceLatch.countDown();
+ }
+ }
+
+ @Override
+ public void onAudioUnplayable() {
+ if (DEBUG) {
+ Log.d(TAG, "onAudioUnplayable");
+ }
+ }
+
+ @Override
+ public void onSmoothTrickplayForceStopped() {
+ if (DEBUG) {
+ Log.d(TAG, "onSmoothTrickplayForceStopped");
+ }
+ }
+ }
+
+ private static class MockPlaybackBufferListener implements PlaybackBufferListener {
+ @Override
+ public void onBufferStartTimeChanged(long startTimeMs) {
+ if (DEBUG) {
+ Log.d(TAG, "onBufferStartTimeChanged");
+ }
+ }
+
+ @Override
+ public void onBufferStateChanged(boolean available) {
+ if (DEBUG) {
+ Log.d(TAG, "onBufferStateChanged");
+ }
+ }
+
+ @Override
+ public void onDiskTooSlow() {
+ if (DEBUG) {
+ Log.d(TAG, "onDiskTooSlow");
+ }
+ }
+ }
+
+ private static class MockEventListener implements EventDetector.EventListener {
+ @Override
+ public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) {
+ if (DEBUG) {
+ Log.d(TAG, "onChannelDetected");
+ }
+ }
+
+ @Override
+ public void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items) {
+ if (DEBUG) {
+ Log.d(TAG, "onEventDetected");
+ }
+ }
+
+ @Override
+ public void onChannelScanDone() {
+ if (DEBUG) {
+ Log.d(TAG, "onChannelScanDone");
+ }
+ }
+ }
+}