blob: ef653f86532e34984c2d324d4adadd4d7afff0a2 [file] [log] [blame]
/*
* 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.test.InstrumentationTestCase;
import android.util.Log;
import android.view.Surface;
import androidx.test.filters.LargeTest;
import com.android.tv.common.flags.impl.DefaultConcurrentDvrPlaybackFlags;
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.PlaybackBufferListener;
import com.android.tv.tuner.exoplayer.buffer.TrickplayStorageManager;
import com.android.tv.tuner.source.TsDataSourceManager;
import com.android.tv.tuner.ts.EventDetector.EventListener;
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 MockChannelScanListener mEventListener = new MockChannelScanListener();
private DefaultConcurrentDvrPlaybackFlags mConcurrentDvrPlaybackFlags =
new DefaultConcurrentDvrPlaybackFlags();
@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,
mConcurrentDvrPlaybackFlags),
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 MockChannelScanListener implements 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");
}
}
}
}