1 /* 2 * Copyright (C) 2011 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 package android.media.player.cts; 17 18 import static org.junit.Assert.assertFalse; 19 import static org.junit.Assert.assertTrue; 20 import static org.junit.Assert.fail; 21 22 import android.media.MediaFormat; 23 import android.media.MediaPlayer; 24 import android.media.MediaPlayer.TrackInfo; 25 import android.media.TimedMetaData; 26 import android.media.cts.MediaPlayerTestBase; 27 import android.net.Uri; 28 import android.os.Looper; 29 import android.os.PowerManager; 30 import android.os.SystemClock; 31 import android.platform.test.annotations.AppModeFull; 32 import android.util.Log; 33 import android.webkit.cts.CtsTestServer; 34 35 import androidx.test.ext.junit.runners.AndroidJUnit4; 36 37 import com.android.compatibility.common.util.FrameworkSpecificTest; 38 import com.android.compatibility.common.util.MediaUtils; 39 import com.android.compatibility.common.util.NonMainlineTest; 40 import com.android.compatibility.common.util.Preconditions; 41 42 import org.junit.After; 43 import org.junit.Before; 44 import org.junit.Test; 45 import org.junit.runner.RunWith; 46 47 import java.net.HttpCookie; 48 import java.util.HashMap; 49 import java.util.List; 50 import java.util.Map; 51 import java.util.concurrent.atomic.AtomicInteger; 52 53 /** 54 * Tests of MediaPlayer streaming capabilities. 55 */ 56 @FrameworkSpecificTest 57 @NonMainlineTest 58 @AppModeFull(reason = "TODO: evaluate and port to instant") 59 @RunWith(AndroidJUnit4.class) 60 public class StreamingMediaPlayerTest extends MediaPlayerTestBase { 61 62 private static final String TAG = "StreamingMediaPlayerTest"; 63 static final String mInpPrefix = WorkDir.getMediaDirString() + "assets/"; 64 65 private static final int HLS_PLAYBACK_TIME_MS = 20 * 1000; 66 private CtsTestServer mServer; 67 68 @Before 69 @Override setUp()70 public void setUp() throws Throwable { 71 super.setUp(); 72 mServer = new CtsTestServer(mContext); 73 } 74 75 @After 76 @Override tearDown()77 public void tearDown() { 78 mServer.shutdown(); 79 super.tearDown(); 80 } 81 82 /* RTSP tests are more flaky and vulnerable to network condition. 83 Disable until better solution is available 84 // Streaming RTSP video from YouTube 85 public void testRTSP_H263_AMR_Video1() throws Exception { 86 playVideoTest("rtsp://v2.cache7.c.youtube.com/video.3gp?cid=0x271de9756065677e" 87 + "&fmt=13&user=android-device-test", 176, 144); 88 } 89 public void testRTSP_H263_AMR_Video2() throws Exception { 90 playVideoTest("rtsp://v2.cache7.c.youtube.com/video.3gp?cid=0xc80658495af60617" 91 + "&fmt=13&user=android-device-test", 176, 144); 92 } 93 94 public void testRTSP_MPEG4SP_AAC_Video1() throws Exception { 95 playVideoTest("rtsp://v2.cache7.c.youtube.com/video.3gp?cid=0x271de9756065677e" 96 + "&fmt=17&user=android-device-test", 176, 144); 97 } 98 public void testRTSP_MPEG4SP_AAC_Video2() throws Exception { 99 playVideoTest("rtsp://v2.cache7.c.youtube.com/video.3gp?cid=0xc80658495af60617" 100 + "&fmt=17&user=android-device-test", 176, 144); 101 } 102 103 public void testRTSP_H264Base_AAC_Video1() throws Exception { 104 playVideoTest("rtsp://v2.cache7.c.youtube.com/video.3gp?cid=0x271de9756065677e" 105 + "&fmt=18&user=android-device-test", 480, 270); 106 } 107 public void testRTSP_H264Base_AAC_Video2() throws Exception { 108 playVideoTest("rtsp://v2.cache7.c.youtube.com/video.3gp?cid=0xc80658495af60617" 109 + "&fmt=18&user=android-device-test", 480, 270); 110 } 111 */ 112 113 @Test testHTTP_H263_AMR_Video1()114 public void testHTTP_H263_AMR_Video1() throws Exception { 115 if (!MediaUtils.checkDecoder(MediaFormat.MIMETYPE_VIDEO_H263, MediaFormat.MIMETYPE_AUDIO_AMR_NB)) { 116 return; // skip 117 } 118 119 localStreamingTest("streaming_media_player_test_http_h263_amr_video1.mp4", 176, 144); 120 } 121 122 @Test testHTTP_H263_AMR_Video2()123 public void testHTTP_H263_AMR_Video2() throws Exception { 124 if (!MediaUtils.checkDecoder(MediaFormat.MIMETYPE_VIDEO_H263, MediaFormat.MIMETYPE_AUDIO_AMR_NB)) { 125 return; // skip 126 } 127 128 localStreamingTest("streaming_media_player_test_http_h263_amr_video2.mp4", 176, 144); 129 } 130 131 @Test testHTTP_MPEG4SP_AAC_Video1()132 public void testHTTP_MPEG4SP_AAC_Video1() throws Exception { 133 if (!MediaUtils.checkDecoder(MediaFormat.MIMETYPE_VIDEO_MPEG4)) { 134 return; // skip 135 } 136 137 localStreamingTest("streaming_media_player_test_http_mpeg4_sp_aac_video1.mp4", 176, 144); 138 } 139 140 @Test testHTTP_MPEG4SP_AAC_Video2()141 public void testHTTP_MPEG4SP_AAC_Video2() throws Exception { 142 if (!MediaUtils.checkDecoder(MediaFormat.MIMETYPE_VIDEO_MPEG4)) { 143 return; // skip 144 } 145 146 localStreamingTest("streaming_media_player_test_http_mpeg4_sp_aac_video2.mp4", 176, 144); 147 } 148 149 @Test testHTTP_H264Base_AAC_Video1()150 public void testHTTP_H264Base_AAC_Video1() throws Exception { 151 if (!MediaUtils.checkDecoder(MediaFormat.MIMETYPE_VIDEO_AVC)) { 152 return; // skip 153 } 154 155 localStreamingTest("streaming_media_player_test_http_h264_base_aac_video1.mp4", 640, 360); 156 } 157 158 @Test testHTTP_H264Base_AAC_Video2()159 public void testHTTP_H264Base_AAC_Video2() throws Exception { 160 if (!MediaUtils.checkDecoder(MediaFormat.MIMETYPE_VIDEO_AVC)) { 161 return; // skip 162 } 163 164 localStreamingTest("streaming_media_player_test_http_h264_base_aac_video2.mp4", 640, 360); 165 } 166 167 // Streaming HLS video downloaded from YouTube 168 @Test testHLS()169 public void testHLS() throws Exception { 170 if (!MediaUtils.checkDecoder(MediaFormat.MIMETYPE_VIDEO_AVC)) { 171 return; // skip 172 } 173 174 localHlsTest("hls_variant/index.m3u8", false /*isAudioOnly*/); 175 } 176 177 @Test testHlsWithHeadersCookies()178 public void testHlsWithHeadersCookies() throws Exception { 179 if (!MediaUtils.checkDecoder(MediaFormat.MIMETYPE_VIDEO_AVC)) { 180 return; // skip 181 } 182 183 // TODO: fake values for headers/cookies till we find a server that actually needs them 184 HashMap<String, String> headers = new HashMap<>(); 185 headers.put("header0", "value0"); 186 headers.put("header1", "value1"); 187 188 String cookieName = "auth_1234567"; 189 String cookieValue = "0123456789ABCDEF0123456789ABCDEF"; 190 HttpCookie cookie = new HttpCookie(cookieName, cookieValue); 191 cookie.setHttpOnly(true); 192 cookie.setDomain("www.youtube.com"); 193 cookie.setPath("/"); // all paths 194 cookie.setSecure(false); 195 cookie.setDiscard(false); 196 cookie.setMaxAge(24 * 3600); // 24hrs 197 198 java.util.Vector<HttpCookie> cookies = new java.util.Vector<HttpCookie>(); 199 cookies.add(cookie); 200 201 localHlsTest("hls_variant/index.m3u8", false /*isAudioOnly*/); 202 } 203 204 @Test testHlsSampleAes_bbb_audio_only_overridable()205 public void testHlsSampleAes_bbb_audio_only_overridable() throws Exception { 206 if (!MediaUtils.checkDecoder(MediaFormat.MIMETYPE_VIDEO_AVC)) { 207 return; // skip 208 } 209 localHlsTest("audio_only/index.m3u8", true /*isAudioOnly*/); 210 } 211 212 @Test testHlsSampleAes_bbb_unmuxed_1500k()213 public void testHlsSampleAes_bbb_unmuxed_1500k() throws Exception { 214 if (!MediaUtils.checkDecoder(MediaFormat.MIMETYPE_VIDEO_AVC)) { 215 return; // skip 216 } 217 MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 1920, 1080); 218 String[] decoderNames = MediaUtils.getDecoderNames(false, format); 219 220 if (decoderNames.length == 0) { 221 MediaUtils.skipTest("No decoders for " + format); 222 } else { 223 localHlsTest("unmuxed_1500k/index.m3u8", false /*isAudioOnly*/); 224 } 225 } 226 227 228 // Streaming audio from local HTTP server 229 @Test testPlayMp3Stream1()230 public void testPlayMp3Stream1() throws Throwable { 231 localHttpAudioStreamTest("ringer.mp3", false, false); 232 } 233 @Test testPlayMp3Stream2()234 public void testPlayMp3Stream2() throws Throwable { 235 localHttpAudioStreamTest("ringer.mp3", false, false); 236 } 237 @Test testPlayMp3StreamRedirect()238 public void testPlayMp3StreamRedirect() throws Throwable { 239 localHttpAudioStreamTest("ringer.mp3", true, false); 240 } 241 @Test testPlayMp3StreamNoLength()242 public void testPlayMp3StreamNoLength() throws Throwable { 243 localHttpAudioStreamTest("noiseandchirps.mp3", false, true); 244 } 245 @Test testPlayOggStream()246 public void testPlayOggStream() throws Throwable { 247 localHttpAudioStreamTest("noiseandchirps.ogg", false, false); 248 } 249 @Test testPlayOggStreamRedirect()250 public void testPlayOggStreamRedirect() throws Throwable { 251 localHttpAudioStreamTest("noiseandchirps.ogg", true, false); 252 } 253 @Test testPlayOggStreamNoLength()254 public void testPlayOggStreamNoLength() throws Throwable { 255 localHttpAudioStreamTest("noiseandchirps.ogg", false, true); 256 } 257 @Test testPlayMp3Stream1Ssl()258 public void testPlayMp3Stream1Ssl() throws Throwable { 259 localHttpsAudioStreamTest("ringer.mp3", false, false); 260 } 261 localHttpAudioStreamTest(final String name, boolean redirect, boolean nolength)262 private void localHttpAudioStreamTest(final String name, boolean redirect, boolean nolength) 263 throws Throwable { 264 Preconditions.assertTestFileExists(mInpPrefix + name); 265 String stream_url = null; 266 if (redirect) { 267 // Stagefright doesn't have a limit, but we can't test support of infinite redirects 268 // Up to 4 redirects seems reasonable though. 269 stream_url = mServer.getRedirectingAssetUrl(mInpPrefix + name, 4); 270 } else { 271 stream_url = mServer.getAssetUrl(mInpPrefix + name); 272 } 273 if (nolength) { 274 stream_url = stream_url + "?" + CtsTestServer.NOLENGTH_POSTFIX; 275 } 276 277 if (!MediaUtils.checkCodecsForPath(mContext, stream_url)) { 278 return; // skip 279 } 280 281 mMediaPlayer.setDataSource(stream_url); 282 283 mMediaPlayer.setDisplay(getActivity().getSurfaceHolder()); 284 mMediaPlayer.setScreenOnWhilePlaying(true); 285 286 mOnBufferingUpdateCalled.reset(); 287 mMediaPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() { 288 @Override 289 public void onBufferingUpdate(MediaPlayer mp, int percent) { 290 mOnBufferingUpdateCalled.signal(); 291 } 292 }); 293 mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { 294 @Override 295 public boolean onError(MediaPlayer mp, int what, int extra) { 296 fail("Media player had error " + what + " playing " + name); 297 return true; 298 } 299 }); 300 301 assertFalse(mOnBufferingUpdateCalled.isSignalled()); 302 mMediaPlayer.prepare(); 303 304 if (nolength) { 305 mMediaPlayer.start(); 306 Thread.sleep(LONG_SLEEP_TIME); 307 assertFalse(mMediaPlayer.isPlaying()); 308 } else { 309 mOnBufferingUpdateCalled.waitForSignal(); 310 mMediaPlayer.start(); 311 Thread.sleep(SLEEP_TIME); 312 } 313 mMediaPlayer.stop(); 314 mMediaPlayer.reset(); 315 } localHttpsAudioStreamTest(final String name, boolean redirect, boolean nolength)316 private void localHttpsAudioStreamTest(final String name, boolean redirect, boolean nolength) 317 throws Throwable { 318 Preconditions.assertTestFileExists(mInpPrefix + name); 319 CtsTestServer server = new CtsTestServer(mContext, /* ssl */ true); 320 try { 321 String stream_url = null; 322 if (redirect) { 323 // Stagefright doesn't have a limit, but we can't test support of infinite redirects 324 // Up to 4 redirects seems reasonable though. 325 stream_url = server.getRedirectingAssetUrl(mInpPrefix + name, 4); 326 } else { 327 stream_url = server.getAssetUrl(mInpPrefix + name); 328 } 329 if (nolength) { 330 stream_url = stream_url + "?" + CtsTestServer.NOLENGTH_POSTFIX; 331 } 332 333 mMediaPlayer.setDataSource(stream_url); 334 335 mMediaPlayer.setDisplay(getActivity().getSurfaceHolder()); 336 mMediaPlayer.setScreenOnWhilePlaying(true); 337 338 mOnBufferingUpdateCalled.reset(); 339 mMediaPlayer.setOnBufferingUpdateListener( 340 (mp, percent) -> mOnBufferingUpdateCalled.signal()); 341 mMediaPlayer.setOnErrorListener((mp, what, extra) -> { 342 fail("Media player had error " + what + " playing " + name); 343 return true; 344 }); 345 346 assertFalse(mOnBufferingUpdateCalled.isSignalled()); 347 try { 348 mMediaPlayer.prepare(); 349 } catch (Exception ex) { 350 return; 351 } 352 fail("https playback should have failed"); 353 } finally { 354 server.shutdown(); 355 } 356 } 357 358 @Test testPlayHlsStream()359 public void testPlayHlsStream() throws Throwable { 360 if (!MediaUtils.checkDecoder(MediaFormat.MIMETYPE_VIDEO_AVC)) { 361 return; // skip 362 } 363 localHlsTest("hls.m3u8", false, false, false /*isAudioOnly*/); 364 } 365 366 @Test testPlayHlsStreamWithQueryString()367 public void testPlayHlsStreamWithQueryString() throws Throwable { 368 if (!MediaUtils.checkDecoder(MediaFormat.MIMETYPE_VIDEO_AVC)) { 369 return; // skip 370 } 371 localHlsTest("hls.m3u8", true, false, false /*isAudioOnly*/); 372 } 373 374 @Test testPlayHlsStreamWithRedirect()375 public void testPlayHlsStreamWithRedirect() throws Throwable { 376 if (!MediaUtils.checkDecoder(MediaFormat.MIMETYPE_VIDEO_AVC)) { 377 return; // skip 378 } 379 localHlsTest("hls.m3u8", false, true, false /*isAudioOnly*/); 380 } 381 382 @Test testPlayHlsStreamWithTimedId3()383 public void testPlayHlsStreamWithTimedId3() throws Throwable { 384 if (!MediaUtils.checkDecoder(MediaFormat.MIMETYPE_VIDEO_AVC)) { 385 Log.d(TAG, "Device doesn't have video codec, skipping test"); 386 return; 387 } 388 Preconditions.assertTestFileExists(mInpPrefix + "prog_index.m3u8"); 389 390 // counter must be final if we want to access it inside onTimedMetaData; 391 // use AtomicInteger so we can have a final counter object with mutable integer value. 392 final AtomicInteger counter = new AtomicInteger(); 393 String stream_url = mServer.getAssetUrl(mInpPrefix + "prog_index.m3u8"); 394 mMediaPlayer.setDataSource(stream_url); 395 mMediaPlayer.setDisplay(getActivity().getSurfaceHolder()); 396 mMediaPlayer.setScreenOnWhilePlaying(true); 397 mMediaPlayer.setWakeMode(mContext, PowerManager.PARTIAL_WAKE_LOCK); 398 mMediaPlayer.setOnTimedMetaDataAvailableListener( 399 new MediaPlayer.OnTimedMetaDataAvailableListener() { 400 @Override 401 public void onTimedMetaDataAvailable(MediaPlayer mp, TimedMetaData md) { 402 counter.incrementAndGet(); 403 int pos = mp.getCurrentPosition(); 404 long timeUs = md.getTimestamp(); 405 byte[] rawData = md.getMetaData(); 406 // Raw data contains an id3 tag holding the decimal string representation of 407 // the associated time stamp rounded to the closest half second. 408 409 int offset = 0; 410 offset += 3; // "ID3" 411 offset += 2; // version 412 offset += 1; // flags 413 offset += 4; // size 414 offset += 4; // "TXXX" 415 offset += 4; // frame size 416 offset += 2; // frame flags 417 offset += 1; // "\x03" : UTF-8 encoded Unicode 418 offset += 1; // "\x00" : null-terminated empty description 419 420 int length = rawData.length; 421 length -= offset; 422 length -= 1; // "\x00" : terminating null 423 424 String data = new String(rawData, offset, length); 425 int dataTimeUs = Integer.parseInt(data); 426 assertTrue("Timed ID3 timestamp does not match content", 427 Math.abs(dataTimeUs - timeUs) < 500000); 428 assertTrue("Timed ID3 arrives after timestamp", pos * 1000 < timeUs); 429 } 430 }); 431 432 final Object completion = new Object(); 433 mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { 434 int mRun; 435 @Override 436 public void onCompletion(MediaPlayer mp) { 437 if (mRun++ == 0) { 438 mMediaPlayer.seekTo(0); 439 mMediaPlayer.start(); 440 } else { 441 mMediaPlayer.stop(); 442 synchronized (completion) { 443 completion.notify(); 444 } 445 } 446 } 447 }); 448 449 mMediaPlayer.prepare(); 450 451 // Select the ID3 track before calling start() to ensure all the timed 452 // ID3s are received. 453 int i = -1; 454 TrackInfo[] trackInfos = mMediaPlayer.getTrackInfo(); 455 for (i = 0; i < trackInfos.length; i++) { 456 TrackInfo trackInfo = trackInfos[i]; 457 if (trackInfo.getTrackType() == TrackInfo.MEDIA_TRACK_TYPE_METADATA) { 458 break; 459 } 460 } 461 assertTrue("Stream has no timed ID3 track", i >= 0); 462 mMediaPlayer.selectTrack(i); 463 464 mMediaPlayer.start(); 465 assertTrue("MediaPlayer not playing", mMediaPlayer.isPlaying()); 466 467 synchronized (completion) { 468 completion.wait(); 469 } 470 471 // There are a total of 19 metadata access units in the test stream; every one of them 472 // should be received twice: once before the seek and once after. 473 assertTrue("Incorrect number of timed ID3s received", counter.get() == 38); 474 } 475 476 private static class WorkerWithPlayer implements Runnable { 477 private final Object mLock = new Object(); 478 private Looper mLooper; 479 private MediaPlayer mMediaPlayer; 480 481 /** 482 * Creates a worker thread with the given name. The thread 483 * then runs a {@link android.os.Looper}. 484 * @param name A name for the new thread 485 */ WorkerWithPlayer(String name)486 WorkerWithPlayer(String name) { 487 Thread t = new Thread(null, this, name); 488 t.setPriority(Thread.MIN_PRIORITY); 489 t.start(); 490 synchronized (mLock) { 491 while (mLooper == null) { 492 try { 493 mLock.wait(); 494 } catch (InterruptedException ex) { 495 } 496 } 497 } 498 } 499 getPlayer()500 public MediaPlayer getPlayer() { 501 return mMediaPlayer; 502 } 503 504 @Override run()505 public void run() { 506 synchronized (mLock) { 507 Looper.prepare(); 508 mLooper = Looper.myLooper(); 509 mMediaPlayer = new MediaPlayer(); 510 mLock.notifyAll(); 511 } 512 Looper.loop(); 513 } 514 quit()515 public void quit() { 516 mLooper.quit(); 517 mMediaPlayer.release(); 518 } 519 } 520 521 @Test testBlockingReadRelease()522 public void testBlockingReadRelease() throws Throwable { 523 WorkerWithPlayer worker = new WorkerWithPlayer("player"); 524 final MediaPlayer mp = worker.getPlayer(); 525 526 Preconditions.assertTestFileExists(mInpPrefix + "noiseandchirps.ogg"); 527 String path = mServer.getDelayedAssetUrl(mInpPrefix + "noiseandchirps.ogg", 15000); 528 mp.setDataSource(path); 529 mp.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { 530 @Override 531 public void onPrepared(MediaPlayer mp) { 532 fail("prepare should not succeed"); 533 } 534 }); 535 mp.prepareAsync(); 536 Thread.sleep(1000); 537 long start = SystemClock.elapsedRealtime(); 538 mp.release(); 539 long end = SystemClock.elapsedRealtime(); 540 long releaseDuration = (end - start); 541 assertTrue("release took too long: " + releaseDuration, releaseDuration < 1000); 542 543 // give the worker a bit of time to start processing the message before shutting it down 544 Thread.sleep(5000); 545 worker.quit(); 546 } 547 localStreamingTest(String name, int width, int height)548 private void localStreamingTest(String name, int width, int height) throws Exception { 549 Preconditions.assertTestFileExists(mInpPrefix + name); 550 String streamUrl = mServer.getAssetUrl(mInpPrefix + name); 551 playVideoTest(streamUrl, width, height); 552 } 553 localHlsTest(final String name, boolean appendQueryString, boolean redirect, boolean isAudioOnly)554 private void localHlsTest(final String name, boolean appendQueryString, 555 boolean redirect, boolean isAudioOnly) throws Exception { 556 localHlsTest(name, null, null, appendQueryString, redirect, isAudioOnly); 557 } 558 localHlsTest(final String name, boolean isAudioOnly)559 private void localHlsTest(final String name, boolean isAudioOnly) 560 throws Exception { 561 localHlsTest(name, null, null, false, false, isAudioOnly); 562 } 563 localHlsTest(String name, Map<String, String> headers, List<HttpCookie> cookies, boolean appendQueryString, boolean redirect, boolean isAudioOnly)564 private void localHlsTest(String name, Map<String, String> headers, List<HttpCookie> cookies, 565 boolean appendQueryString, boolean redirect, boolean isAudioOnly) 566 throws Exception { 567 Preconditions.assertTestFileExists(mInpPrefix + name); 568 569 String stream_url = null; 570 if (redirect) { 571 stream_url = mServer.getQueryRedirectingAssetUrl(mInpPrefix + name); 572 } else { 573 stream_url = mServer.getAssetUrl(mInpPrefix + name); 574 } 575 if (appendQueryString) { 576 stream_url += "?foo=bar/baz"; 577 } 578 if (isAudioOnly) { 579 playLiveAudioOnlyTest(Uri.parse(stream_url), headers, cookies, HLS_PLAYBACK_TIME_MS); 580 } else { 581 playLiveVideoTest(Uri.parse(stream_url), headers, cookies, HLS_PLAYBACK_TIME_MS); 582 } 583 } 584 } 585