1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tv.tuner; 18 19 import android.content.Context; 20 import android.graphics.SurfaceTexture; 21 import android.os.Handler; 22 import android.os.HandlerThread; 23 import android.os.Message; 24 import android.support.test.filters.LargeTest; 25 import android.test.InstrumentationTestCase; 26 import android.util.Log; 27 import android.view.Surface; 28 import com.android.tv.tuner.data.Cea708Data; 29 import com.android.tv.tuner.data.PsiData; 30 import com.android.tv.tuner.data.PsipData; 31 import com.android.tv.tuner.data.TunerChannel; 32 import com.android.tv.tuner.data.nano.Channel; 33 import com.android.tv.tuner.exoplayer.MpegTsPlayer; 34 import com.android.tv.tuner.exoplayer.MpegTsRendererBuilder; 35 import com.android.tv.tuner.exoplayer.buffer.BufferManager; 36 import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager; 37 import com.android.tv.tuner.source.TsDataSourceManager; 38 import com.android.tv.tuner.tvinput.EventDetector; 39 import com.android.tv.tuner.tvinput.PlaybackBufferListener; 40 import com.google.android.exoplayer.ExoPlayer; 41 import java.io.File; 42 import java.io.FileOutputStream; 43 import java.io.IOException; 44 import java.io.InputStream; 45 import java.util.ArrayList; 46 import java.util.Arrays; 47 import java.util.List; 48 import java.util.concurrent.CountDownLatch; 49 import java.util.concurrent.TimeUnit; 50 import java.util.concurrent.atomic.AtomicLong; 51 import org.junit.Ignore; 52 53 /** This class use {@link FileTunerHal} to simulate tunerhal's actions to test zapping time. */ 54 @LargeTest 55 public class ZappingTimeTest extends InstrumentationTestCase { 56 private static final String TAG = "ZappingTimeTest"; 57 private static final boolean DEBUG = false; 58 private static final int TS_COPY_BUFFER_SIZE = 1024 * 512; 59 private static final int PROGRAM_NUMBER = 1; 60 private static final int VIDEO_PID = 49; 61 private static final int PCR_PID = 49; 62 private static final List<Integer> AUDIO_PIDS = Arrays.asList(51, 52, 53); 63 private static final long BUFFER_SIZE_DEF = 2 * 1024; 64 private static final int FREQUENCY = -1; 65 private static final String MODULATION = ""; 66 private static final long ZAPPING_TIME_OUT_MS = 10000; 67 private static final long MAX_AVERAGE_ZAPPING_TIME_MS = 4000; 68 private static final int TEST_ITERATION_COUNT = 10; 69 private static final int STRESS_ZAPPING_TEST_COUNT = 50; 70 private static final long SKIP_DURATION_MS_TO_ADD = 200; 71 private static final String TEST_TS_FILE_PATH = "capture_kqed.ts"; 72 73 private static final int MSG_START_PLAYBACK = 1; 74 75 private TunerChannel mChannel; 76 private FileTunerHal mTunerHal; 77 private MpegTsPlayer mPlayer; 78 private TsDataSourceManager mSourceManager; 79 private Handler mHandler; 80 private Context mTargetContext; 81 private File mTrickplayBufferDir; 82 private Surface mSurface; 83 private CountDownLatch mErrorLatch; 84 private CountDownLatch mDrawnToSurfaceLatch; 85 private CountDownLatch mWaitTuneExecuteLatch; 86 private AtomicLong mOnDrawnToSurfaceTimeMs = new AtomicLong(0); 87 private MockMpegTsPlayerListener mMpegTsPlayerListener = new MockMpegTsPlayerListener(); 88 private MockPlaybackBufferListener mPlaybackBufferListener = new MockPlaybackBufferListener(); 89 private MockEventListener mEventListener = new MockEventListener(); 90 91 @Override 92 protected void setUp() throws Exception { 93 super.setUp(); 94 mTargetContext = getInstrumentation().getTargetContext(); 95 mTrickplayBufferDir = mTargetContext.getCacheDir(); 96 HandlerThread handlerThread = new HandlerThread(TAG); 97 handlerThread.start(); 98 List<PsiData.PmtItem> pmtItems = new ArrayList<>(); 99 pmtItems.add(new PsiData.PmtItem(Channel.VideoStreamType.MPEG2, VIDEO_PID, null, null)); 100 for (int audioPid : AUDIO_PIDS) { 101 pmtItems.add( 102 new PsiData.PmtItem(Channel.AudioStreamType.A52AC3AUDIO, audioPid, null, null)); 103 } 104 105 Context context = getInstrumentation().getContext(); 106 // Since assets and resource files are compressed, random access to the specified offset 107 // in assets or resource files will add some delay which is proportional to the offset. 108 // So the TS stream asset file are copied to a cache file, and the starting stream position 109 // in the file will be accessed by underlying {@link RandomAccessFile}. 110 File tsCacheFile = createCacheFile(context, mTargetContext, TEST_TS_FILE_PATH); 111 pmtItems.add(new PsiData.PmtItem(0x100, PCR_PID, null, null)); 112 mChannel = new TunerChannel(PROGRAM_NUMBER, pmtItems); 113 mChannel.setFrequency(FREQUENCY); 114 mChannel.setModulation(MODULATION); 115 mTunerHal = new FileTunerHal(context, tsCacheFile); 116 mTunerHal.openFirstAvailable(); 117 mSourceManager = TsDataSourceManager.createSourceManager(false); 118 mSourceManager.addTunerHalForTest(mTunerHal); 119 mHandler = 120 new Handler( 121 handlerThread.getLooper(), 122 new Handler.Callback() { 123 @Override 124 public boolean handleMessage(Message msg) { 125 switch (msg.what) { 126 case MSG_START_PLAYBACK: 127 { 128 mHandler.removeCallbacksAndMessages(null); 129 stopPlayback(); 130 mOnDrawnToSurfaceTimeMs.set(0); 131 mDrawnToSurfaceLatch = new CountDownLatch(1); 132 if (mWaitTuneExecuteLatch != null) { 133 mWaitTuneExecuteLatch.countDown(); 134 } 135 int frequency = msg.arg1; 136 boolean useSimpleSampleBuffer = (msg.arg2 == 1); 137 BufferManager bufferManager = null; 138 if (!useSimpleSampleBuffer) { 139 bufferManager = 140 new BufferManager( 141 new TrickplayStorageManager( 142 mTargetContext, 143 mTrickplayBufferDir, 144 1024L 145 * 1024L 146 * BUFFER_SIZE_DEF)); 147 } 148 mChannel.setFrequency(frequency); 149 mSourceManager.setKeepTuneStatus(true); 150 mPlayer = 151 new MpegTsPlayer( 152 new MpegTsRendererBuilder( 153 mTargetContext, 154 bufferManager, 155 mPlaybackBufferListener), 156 mHandler, 157 mSourceManager, 158 null, 159 mMpegTsPlayerListener); 160 mPlayer.setCaptionServiceNumber( 161 Cea708Data.EMPTY_SERVICE_NUMBER); 162 mPlayer.prepare( 163 mTargetContext, 164 mChannel, 165 false, 166 mEventListener); 167 return true; 168 } 169 default: 170 { 171 Log.i(TAG, "Unhandled message code: " + msg.what); 172 return true; 173 } 174 } 175 } 176 }); 177 } 178 179 @Override 180 protected void tearDown() throws Exception { 181 if (mPlayer != null) { 182 mPlayer.release(); 183 } 184 if (mSurface != null) { 185 mSurface.release(); 186 } 187 mHandler.getLooper().quitSafely(); 188 super.tearDown(); 189 } 190 191 public void testZappingTime() { 192 zappingTimeTest(false, TEST_ITERATION_COUNT, true); 193 } 194 195 public void testZappingTimeWithSimpleSampleBuffer() { 196 zappingTimeTest(true, TEST_ITERATION_COUNT, true); 197 } 198 199 @Ignore("b/69978026") 200 @SuppressWarnings("JUnit4ClassUsedInJUnit3") 201 public void testStressZapping() { 202 zappingTimeTest(false, STRESS_ZAPPING_TEST_COUNT, false); 203 } 204 205 @Ignore("b/69978093") 206 @SuppressWarnings("JUnit4ClassUsedInJUnit3") 207 public void testZappingWithPacketMissing() { 208 mTunerHal.setEnablePacketMissing(true); 209 mTunerHal.setEnableArtificialDelay(true); 210 SurfaceTexture surfaceTexture = new SurfaceTexture(0); 211 mSurface = new Surface(surfaceTexture); 212 long zappingStartTimeMs = System.currentTimeMillis(); 213 mErrorLatch = new CountDownLatch(1); 214 mHandler.obtainMessage(MSG_START_PLAYBACK, FREQUENCY, 0).sendToTarget(); 215 boolean errorAppeared = false; 216 while (System.currentTimeMillis() - zappingStartTimeMs < ZAPPING_TIME_OUT_MS) { 217 try { 218 errorAppeared = mErrorLatch.await(100, TimeUnit.MILLISECONDS); 219 if (errorAppeared) { 220 break; 221 } 222 } catch (InterruptedException e) { 223 } 224 } 225 assertFalse("Error happened when packet lost", errorAppeared); 226 } 227 228 private static File createCacheFile(Context context, Context targetContext, String filename) 229 throws IOException { 230 File cacheFile = new File(targetContext.getCacheDir(), filename); 231 232 if (cacheFile.createNewFile() == false) { 233 cacheFile.delete(); 234 cacheFile.createNewFile(); 235 } 236 237 InputStream inputStream = context.getResources().getAssets().open(filename); 238 FileOutputStream fileOutputStream = new FileOutputStream(cacheFile); 239 240 byte[] buffer = new byte[TS_COPY_BUFFER_SIZE]; 241 while (inputStream.read(buffer, 0, TS_COPY_BUFFER_SIZE) != -1) { 242 fileOutputStream.write(buffer); 243 } 244 245 fileOutputStream.close(); 246 inputStream.close(); 247 248 return cacheFile; 249 } 250 251 private void zappingTimeTest( 252 boolean useSimpleSampleBuffer, int testIterationCount, boolean enableArtificialDelay) { 253 String bufferManagerLogString = 254 !enableArtificialDelay 255 ? "for stress test" 256 : useSimpleSampleBuffer ? "with simple sample buffer" : ""; 257 SurfaceTexture surfaceTexture = new SurfaceTexture(0); 258 mSurface = new Surface(surfaceTexture); 259 mTunerHal.setEnablePacketMissing(false); 260 mTunerHal.setEnableArtificialDelay(enableArtificialDelay); 261 double totalZappingTime = 0.0; 262 for (int i = 0; i < testIterationCount; i++) { 263 mWaitTuneExecuteLatch = new CountDownLatch(1); 264 long zappingStartTimeMs = System.currentTimeMillis(); 265 mTunerHal.setInitialSkipMs(SKIP_DURATION_MS_TO_ADD * (i % TEST_ITERATION_COUNT)); 266 mHandler.obtainMessage(MSG_START_PLAYBACK, FREQUENCY + i, useSimpleSampleBuffer ? 1 : 0) 267 .sendToTarget(); 268 try { 269 mWaitTuneExecuteLatch.await(); 270 } catch (InterruptedException e) { 271 } 272 boolean drawnToSurface = false; 273 while (System.currentTimeMillis() - zappingStartTimeMs < ZAPPING_TIME_OUT_MS) { 274 try { 275 drawnToSurface = mDrawnToSurfaceLatch.await(100, TimeUnit.MILLISECONDS); 276 if (drawnToSurface) { 277 break; 278 } 279 } catch (InterruptedException e) { 280 } 281 } 282 if (i == 0) { 283 continue; 284 // Get rid of the first result, which shows outlier often. 285 } 286 // In 10s, all zapping request will finish. Set the maximum zapping time as 10s could be 287 // reasonable. 288 totalZappingTime += 289 (mOnDrawnToSurfaceTimeMs.get() > 0 290 ? mOnDrawnToSurfaceTimeMs.get() - zappingStartTimeMs 291 : ZAPPING_TIME_OUT_MS); 292 } 293 double averageZappingTime = totalZappingTime / (testIterationCount - 1); 294 Log.i(TAG, "Average zapping time " + bufferManagerLogString + ":" + averageZappingTime); 295 assertTrue( 296 "Average Zapping time " 297 + bufferManagerLogString 298 + " is too large:" 299 + averageZappingTime, 300 averageZappingTime < MAX_AVERAGE_ZAPPING_TIME_MS); 301 } 302 303 private void stopPlayback() { 304 if (mPlayer != null) { 305 mPlayer.setPlayWhenReady(false); 306 mPlayer.release(); 307 mPlayer = null; 308 } 309 } 310 311 private class MockMpegTsPlayerListener implements MpegTsPlayer.Listener { 312 313 @Override 314 public void onStateChanged(boolean playWhenReady, int playbackState) { 315 if (DEBUG) { 316 Log.d(TAG, "ExoPlayer state change: " + playbackState + " " + playWhenReady); 317 } 318 if (playbackState == ExoPlayer.STATE_READY) { 319 mPlayer.setSurface(mSurface); 320 mPlayer.setPlayWhenReady(true); 321 mPlayer.setVolume(1.0f); 322 } 323 } 324 325 @Override 326 public void onError(Exception e) { 327 if (DEBUG) { 328 Log.d(TAG, "onError"); 329 } 330 if (mErrorLatch != null) { 331 mErrorLatch.countDown(); 332 } 333 } 334 335 @Override 336 public void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio) { 337 if (DEBUG) { 338 Log.d(TAG, "onVideoSizeChanged"); 339 } 340 } 341 342 @Override 343 public void onDrawnToSurface(MpegTsPlayer player, Surface surface) { 344 if (DEBUG) { 345 Log.d(TAG, "onDrawnToSurface"); 346 } 347 mOnDrawnToSurfaceTimeMs.set(System.currentTimeMillis()); 348 if (mDrawnToSurfaceLatch != null) { 349 mDrawnToSurfaceLatch.countDown(); 350 } 351 } 352 353 @Override 354 public void onAudioUnplayable() { 355 if (DEBUG) { 356 Log.d(TAG, "onAudioUnplayable"); 357 } 358 } 359 360 @Override 361 public void onSmoothTrickplayForceStopped() { 362 if (DEBUG) { 363 Log.d(TAG, "onSmoothTrickplayForceStopped"); 364 } 365 } 366 } 367 368 private static class MockPlaybackBufferListener implements PlaybackBufferListener { 369 @Override 370 public void onBufferStartTimeChanged(long startTimeMs) { 371 if (DEBUG) { 372 Log.d(TAG, "onBufferStartTimeChanged"); 373 } 374 } 375 376 @Override 377 public void onBufferStateChanged(boolean available) { 378 if (DEBUG) { 379 Log.d(TAG, "onBufferStateChanged"); 380 } 381 } 382 383 @Override 384 public void onDiskTooSlow() { 385 if (DEBUG) { 386 Log.d(TAG, "onDiskTooSlow"); 387 } 388 } 389 } 390 391 private static class MockEventListener implements EventDetector.EventListener { 392 @Override 393 public void onChannelDetected(TunerChannel channel, boolean channelArrivedAtFirstTime) { 394 if (DEBUG) { 395 Log.d(TAG, "onChannelDetected"); 396 } 397 } 398 399 @Override 400 public void onEventDetected(TunerChannel channel, List<PsipData.EitItem> items) { 401 if (DEBUG) { 402 Log.d(TAG, "onEventDetected"); 403 } 404 } 405 406 @Override 407 public void onChannelScanDone() { 408 if (DEBUG) { 409 Log.d(TAG, "onChannelScanDone"); 410 } 411 } 412 } 413 } 414