Quellcode durchsuchen

Adding some tests for iZpl-A and izpl plugin for transfer of iZpl-data

LH vor 10 Jahren
Ursprung
Commit
b56f78d3ce
100 geänderte Dateien mit 18127 neuen und 19 gelöschten Zeilen
  1. 2 1
      IZPL-A/IZPL/app/build.gradle
  2. 2 5
      IZPL-A/IZPL/app/src/main/java/de/nplusc/izc/izpl/Enqueue.java
  3. 58 5
      IZPL-A/IZPL/app/src/main/java/de/nplusc/izc/izpl/MainMenu.java
  4. 155 0
      IZPL-A/IZPL/app/src/main/java/de/nplusc/izc/izpl/PlayBackService.java
  5. 27 7
      IZPL-A/IZPL/app/src/main/java/de/nplusc/izc/izpl/PlayListCore.java
  6. 1 1
      IZPL-A/IZPL/build.gradle
  7. 202 0
      IZPL-A/IZPL/exoplayer-LICENSE
  8. 60 0
      IZPL-A/IZPL/exoplayer/build.gradle
  9. 104 0
      IZPL-A/IZPL/exoplayer/doc/images/exoplayer_diagrams.svg
  10. BIN
      IZPL-A/IZPL/exoplayer/doc/images/exoplayer_playbackstate.png
  11. BIN
      IZPL-A/IZPL/exoplayer/doc/images/exoplayer_state.png
  12. BIN
      IZPL-A/IZPL/exoplayer/doc/images/exoplayer_threading_model.png
  13. BIN
      IZPL-A/IZPL/exoplayer/doc/images/trackrenderer_state.png
  14. 9 0
      IZPL-A/IZPL/exoplayer/src/main/.classpath
  15. 33 0
      IZPL-A/IZPL/exoplayer/src/main/.project
  16. 4 0
      IZPL-A/IZPL/exoplayer/src/main/.settings/org.eclipse.jdt.core.prefs
  17. 32 0
      IZPL-A/IZPL/exoplayer/src/main/AndroidManifest.xml
  18. 33 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/BehindLiveWindowException.java
  19. 53 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/C.java
  20. 58 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/CodecCounters.java
  21. 116 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/CryptoInfo.java
  22. 48 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/DecoderInfo.java
  23. 284 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java
  24. 67 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java
  25. 37 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/ExoPlaybackException.java
  26. 391 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/ExoPlayer.java
  27. 211 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/ExoPlayerImpl.java
  28. 598 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java
  29. 51 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java
  30. 77 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/LoadControl.java
  31. 79 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/MediaClock.java
  32. 304 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java
  33. 810 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java
  34. 381 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java
  35. 560 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java
  36. 288 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/MediaFormat.java
  37. 36 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/MediaFormatHolder.java
  38. 37 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/ParserException.java
  39. 108 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/SampleHolder.java
  40. 160 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/SampleSource.java
  41. 180 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/SmoothFrameReleaseTimeHelper.java
  42. 43 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/TrackInfo.java
  43. 351 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/TrackRenderer.java
  44. 77 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/VideoSurfaceView.java
  45. 97 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/audio/AudioCapabilities.java
  46. 101 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/audio/AudioCapabilitiesReceiver.java
  47. 799 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java
  48. 185 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/Chunk.java
  49. 34 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/ChunkOperationHolder.java
  50. 832 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java
  51. 114 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java
  52. 152 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/ContainerMediaChunk.java
  53. 170 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/Format.java
  54. 301 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java
  55. 140 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java
  56. 120 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/MultiTrackChunkSource.java
  57. 110 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/SingleSampleChunkSource.java
  58. 139 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java
  59. 121 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/parser/Extractor.java
  60. 70 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/parser/SegmentIndex.java
  61. 32 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/DefaultSampleValues.java
  62. 841 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java
  63. 50 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/TrackEncryptionBox.java
  64. 155 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/TrackFragment.java
  65. 550 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/parser/webm/DefaultEbmlReader.java
  66. 125 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/parser/webm/EbmlEventHandler.java
  67. 109 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/parser/webm/EbmlReader.java
  68. 645 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/parser/webm/WebmExtractor.java
  69. 599 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java
  70. 100 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/DashSegmentIndex.java
  71. 75 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/DashSingleSegmentIndex.java
  72. 82 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/DashWrappingSegmentIndex.java
  73. 58 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/mpd/AdaptationSet.java
  74. 83 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/mpd/ContentProtection.java
  75. 55 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java
  76. 691 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java
  77. 76 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java
  78. 127 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java
  79. 293 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/mpd/Representation.java
  80. 384 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java
  81. 164 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/mpd/UrlTemplate.java
  82. 31 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/mpd/UtcTimingElement.java
  83. 109 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/drm/DrmSessionManager.java
  84. 49 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/drm/MediaDrmCallback.java
  85. 405 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java
  86. 117 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/DataChunk.java
  87. 51 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/HlsChunk.java
  88. 589 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java
  89. 34 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java
  90. 90 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylist.java
  91. 57 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/HlsParserUtil.java
  92. 36 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/HlsPlaylist.java
  93. 282 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java
  94. 448 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java
  95. 134 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/TsChunk.java
  96. 56 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/Variant.java
  97. 126 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/parser/AdtsExtractor.java
  98. 182 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java
  99. 47 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/parser/ElementaryStreamReader.java
  100. 378 0
      IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java

+ 2 - 1
IZPL-A/IZPL/app/build.gradle

@@ -29,5 +29,6 @@ dependencies {
     compile files('libs/izpl-shared.jar')
     compile 'com.android.support:appcompat-v7:22.1.1'
     compile project(':filechooser')
-    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.1'
+    compile 'org.apache.logging.log4j:log4j-api:2.1'
+    compile project(':exoplayer')
 }

+ 2 - 5
IZPL-A/IZPL/app/src/main/java/de/nplusc/izc/izpl/Enqueue.java

@@ -3,14 +3,11 @@ package de.nplusc.izc.izpl;
 import android.app.ActionBar;
 import android.support.v7.app.ActionBarActivity;
 import android.os.Bundle;
-import android.util.Log;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 import android.widget.AdapterView;
-import android.widget.ArrayAdapter;
 import android.widget.ListView;
-import android.widget.Toast;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -21,7 +18,7 @@ import de.nplusc.izc.iZpl.API.shared.PlayListItem;
 
 public class Enqueue extends ActionBarActivity {
 
-    PlaybackCore c=null;
+    PlayListCore c=null;
     PlayListAdapter adp;
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -30,7 +27,7 @@ public class Enqueue extends ActionBarActivity {
         ActionBar ab = getActionBar();
         if(ab !=null)ab.hide();
         ListView poolList = (ListView) findViewById(R.id.lstTracks);
-        c = PlaybackCore.openSession();
+        c = PlayListCore.openSession();
         List<PlayListItemWrapper> datat = new ArrayList<>();
 
         List<PlayListItem> data = c.getPool();

+ 58 - 5
IZPL-A/IZPL/app/src/main/java/de/nplusc/izc/izpl/MainMenu.java

@@ -2,8 +2,11 @@ package de.nplusc.izc.izpl;
 
 import android.app.ActionBar;
 import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
-import android.net.Uri;
+import android.content.ServiceConnection;
+import android.os.IBinder;
 import android.support.v7.app.ActionBarActivity;
 import android.os.Bundle;
 import android.util.Log;
@@ -18,18 +21,44 @@ import br.com.thinkti.android.filechooser.FileChooser;
 
 public class MainMenu extends ActionBarActivity {
 
+    @Override
+    protected void onStart() {
+        super.onStart();
+        // Bind to LocalService
+
+
+
+
+        Intent intent = new Intent(this, PlayBackService.class);
+        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+    }
 
+    @Override
+    protected void onStop() {
+        super.onStop();
+        plservice=null;
+        // Unbind from the service
+        if (mBound) {
+            unbindService(mConnection);
+            mBound = false;
+        }
+    }
 
-    PlaybackCore c;
+    PlayListCore c;
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_menu);
         ActionBar ab = getActionBar();
         if(ab !=null)ab.hide();
-        c=PlaybackCore.openSession();
+        c= PlayListCore.openSession();
+
+
+
+
         if(!c.isLoaded())
         {
+            startService(new Intent(this, PlayBackService.class));
             performFileSearch();
         }
         findViewById(R.id.btnList).setOnClickListener(new View.OnClickListener() {
@@ -39,6 +68,15 @@ public class MainMenu extends ActionBarActivity {
                 MainMenu.this.startActivity(myIntent);
             }
         });
+
+        findViewById(R.id.btnPlay).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                Intent myIntent = new Intent(MainMenu.this, Enqueue.class);
+                plservice.startstop();
+            }
+        });
+
     }
 
 
@@ -95,8 +133,23 @@ public class MainMenu extends ActionBarActivity {
             }
         }
     }
+    private PlayBackService plservice;
+    private boolean mBound;
+    private ServiceConnection mConnection = new ServiceConnection() {
+
+        @Override
+        public void onServiceConnected(ComponentName className,
+                                       IBinder service) {
+            // We've bound to LocalService, cast the IBinder and get LocalService instance
+            PlayBackService.PLBinder binder = (PlayBackService.PLBinder) service;
+            plservice = binder.getService();
+            mBound = true;
+        }
 
-
-
+        @Override
+        public void onServiceDisconnected(ComponentName arg0) {
+            mBound = false;
+        }
+    };
 
 }

+ 155 - 0
IZPL-A/IZPL/app/src/main/java/de/nplusc/izc/izpl/PlayBackService.java

@@ -0,0 +1,155 @@
+package de.nplusc.izc.izpl;
+
+import android.app.Service;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.IBinder;
+
+import com.google.android.exoplayer.ExoPlaybackException;
+import com.google.android.exoplayer.ExoPlayer;
+import com.google.android.exoplayer.MediaCodecAudioTrackRenderer;
+import com.google.android.exoplayer.source.DefaultSampleSource;
+import com.google.android.exoplayer.source.FrameworkSampleExtractor;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.util.HashMap;
+
+import de.nplusc.izc.iZpl.API.shared.PlayListItem;
+import de.nplusc.izc.iZpl.API.shared.SinglePlayListItem;
+
+/**
+ * Created by LH on 04.05.2015.
+ */
+public class PlayBackService extends Service implements ExoPlayer.Listener {
+    private boolean playing=false;
+    private ExoPlayer player;
+    private PlayListCore c=null;
+
+    private boolean istbooted=false;
+
+    private IBinder b = new PLBinder();
+
+    private MainMenu listeningUI = null;
+
+    public void registerMainMenu(MainMenu m)
+    {
+        listeningUI=m;
+    }
+
+
+    public void unregisterUI()
+    {
+        listeningUI=null;
+    }
+
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        c=PlayListCore.openSession();
+        player = ExoPlayer.Factory.newInstance(1);
+        player.addListener(this);
+    }
+
+    public PlayListCore getPlaylistCore()
+    {
+        return c;
+    }
+
+    public void setPlaylistCore(PlayListCore co)
+    {
+        c=co;
+    }
+
+    @Override
+    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
+        if(playbackState==ExoPlayer.STATE_ENDED)
+        {
+            pullTrackFromRandomizer();
+        }
+    }
+
+    @Override
+    public void onPlayWhenReadyCommitted() {
+
+    }
+
+    @Override
+    public void onPlayerError(ExoPlaybackException error) {
+
+    }
+
+    public class PLBinder extends Binder
+    {
+        PlayBackService getService() {
+            // Return this instance of PlaybackService so clients can call public methods
+            return PlayBackService.this;
+        }
+    }
+
+    public void startstop()
+    {
+        if(istbooted)
+        {
+            playing = !playing;
+            player.setPlayWhenReady(playing);
+        }
+        else
+        {
+            pullTrackFromRandomizer();
+        }
+
+    }
+
+    public void startTrack(String path)
+    {
+        player.stop();
+        player.seekTo(0L);
+        MediaCodecAudioTrackRenderer audioRenderer = new MediaCodecAudioTrackRenderer(new DefaultSampleSource
+                (new FrameworkSampleExtractor(this, Uri.fromFile(new File(path)) ,new HashMap<String,String>()),1));
+        player.prepare(audioRenderer);
+        player.setPlayWhenReady(true);
+        playing=true;
+
+    }
+    private String currentTrack="Nothing at the moment";
+    public void pullTrackFromRandomizer()
+    {
+        if(c!=null&&c.isLoaded())
+        {
+            SinglePlayListItem itm = (SinglePlayListItem) c.getNextItem();
+            currentTrack=itm.getTitle();
+            startTrack(itm.getPath());
+        }
+    }
+    public String getCurrentTitle()
+    {
+        return currentTrack;
+    }
+
+
+    public int getTrackLengthInSeconds()
+    {
+        return (int)player.getDuration();
+    }
+
+    public int getCurrentPosition()
+    {
+        return (int)player.getCurrentPosition();
+    }
+    public void seek(int secs)
+    {
+        player.seekTo(secs*1000);
+    }
+
+
+
+
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return b;
+    }
+}

+ 27 - 7
IZPL-A/IZPL/app/src/main/java/de/nplusc/izc/izpl/PlaybackCore.java → IZPL-A/IZPL/app/src/main/java/de/nplusc/izc/izpl/PlayListCore.java

@@ -1,29 +1,29 @@
 package de.nplusc.izc.izpl;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 
 import de.nplusc.izc.iZpl.API.shared.InvalidPlayListFileException;
+import de.nplusc.izc.iZpl.API.shared.MultiPlayListItem;
 import de.nplusc.izc.iZpl.API.shared.PlayListItem;
+import de.nplusc.izc.iZpl.API.shared.SinglePlayListItem;
 import de.nplusc.izc.iZpl.Utils.shared.PLFileIO;
 
 /**
  * Created by LH on 02.05.2015.
  */
-public class PlaybackCore
+public class PlayListCore
 {
     private List<PlayListItem> currentPool;
-    private static PlaybackCore session = null;
+    private static PlayListCore session = null;
     private List<PlayListItem> cache = new ArrayList<>();
     private List<PlayListItem> forceQueue=new ArrayList<>();
     private boolean loaded=false;
-    public static synchronized PlaybackCore openSession()
+    public static synchronized PlayListCore openSession()
     {
         if(session==null)
         {
-            session=new PlaybackCore();
+            session=new PlayListCore();
         }
         return session;
     }
@@ -56,7 +56,27 @@ public class PlaybackCore
         forceQueue.add(currentPool.get(idx));
     }
 
-    public PlayListItem getNextItem()
+    private List<PlayListItem> multiitemQueue = new ArrayList<>();
+
+    public SinglePlayListItem getNextItem()
+    {
+        if(!multiitemQueue.isEmpty())
+        {
+            return (SinglePlayListItem)multiitemQueue.remove(0);
+        }
+        PlayListItem tempo = getNextItemAux();
+        if(tempo instanceof MultiPlayListItem)
+        {
+            multiitemQueue.addAll(((MultiPlayListItem)tempo).getItems());
+            return (SinglePlayListItem)multiitemQueue.remove(0);
+        }
+        else
+        {
+            return (SinglePlayListItem)tempo;
+        }
+    }
+
+    private PlayListItem getNextItemAux()
     {
         if(forceQueue.isEmpty())
         {

+ 1 - 1
IZPL-A/IZPL/build.gradle

@@ -6,7 +6,7 @@ buildscript {
     }
     dependencies {
         classpath 'com.android.tools.build:gradle:1.0.0-rc1'
-
+        classpath 'com.novoda:bintray-release:0.2.7'
         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files
     }

+ 202 - 0
IZPL-A/IZPL/exoplayer-LICENSE

@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.

+ 60 - 0
IZPL-A/IZPL/exoplayer/build.gradle

@@ -0,0 +1,60 @@
+// Copyright (C) 2014 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.
+apply plugin: 'com.android.library'
+apply plugin: 'bintray-release'
+
+android {
+    compileSdkVersion 21
+    buildToolsVersion "21.1.2"
+
+    defaultConfig {
+        minSdkVersion 9
+        targetSdkVersion 21
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
+        }
+    }
+
+    lintOptions {
+        abortOnError false
+    }
+}
+
+dependencies {
+}
+
+android.libraryVariants.all { variant ->
+    def name = variant.buildType.name
+    if (name.equals(com.android.builder.core.BuilderConstants.DEBUG)) {
+        return; // Skip debug builds.
+    }
+    def task = project.tasks.create "jar${name.capitalize()}", Jar
+    task.dependsOn variant.javaCompile
+    task.from variant.javaCompile.destinationDir
+    artifacts.add('archives', task);
+}
+
+publish {
+    repoName = 'exoplayer'
+    userOrg = 'google'
+    groupId = 'com.google.android.exoplayer'
+    artifactId = 'exoplayer'
+    version = 'r1.2.4'
+    description = 'The ExoPlayer library.'
+    website = 'https://github.com/google/ExoPlayer'
+}

Datei-Diff unterdrückt, da er zu groß ist
+ 104 - 0
IZPL-A/IZPL/exoplayer/doc/images/exoplayer_diagrams.svg


BIN
IZPL-A/IZPL/exoplayer/doc/images/exoplayer_playbackstate.png


BIN
IZPL-A/IZPL/exoplayer/doc/images/exoplayer_state.png


BIN
IZPL-A/IZPL/exoplayer/doc/images/exoplayer_threading_model.png


BIN
IZPL-A/IZPL/exoplayer/doc/images/trackrenderer_state.png


+ 9 - 0
IZPL-A/IZPL/exoplayer/src/main/.classpath

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+	<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
+	<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
+	<classpathentry kind="src" path="java"/>
+	<classpathentry kind="src" path="gen"/>
+	<classpathentry kind="output" path="bin/classes"/>
+</classpath>

+ 33 - 0
IZPL-A/IZPL/exoplayer/src/main/.project

@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>ExoPlayerLib</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>com.android.ide.eclipse.adt.ApkBuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>

+ 4 - 0
IZPL-A/IZPL/exoplayer/src/main/.settings/org.eclipse.jdt.core.prefs

@@ -0,0 +1,4 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.source=1.6

+ 32 - 0
IZPL-A/IZPL/exoplayer/src/main/AndroidManifest.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.google.android.exoplayer">
+
+  <uses-permission android:name="android.permission.INTERNET"/>
+  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+  <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+
+  <!--
+    Important: ExoPlayerLib specifies a minSdkVersion of 9 because various components provided by
+    the library may be of use on older devices. However, please note that the core video playback
+    functionality provided by the library requires API level 16 or greater.
+  -->
+  <uses-sdk android:minSdkVersion="9" android:targetSdkVersion="21"/>
+
+</manifest>

+ 33 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/BehindLiveWindowException.java

@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+import java.io.IOException;
+
+/**
+ * Thrown when a live playback falls behind the available media window.
+ */
+public class BehindLiveWindowException extends IOException {
+
+  public BehindLiveWindowException() {
+    super();
+  }
+
+  public BehindLiveWindowException(String message) {
+    super(message);
+  }
+
+}

+ 53 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/C.java

@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+import android.media.MediaExtractor;
+
+/**
+ * Defines constants that are generally useful throughout the library.
+ */
+public final class C {
+
+  /**
+   * Represents an unknown microsecond time or duration.
+   */
+  public static final long UNKNOWN_TIME_US = -1L;
+
+  /**
+   * The number of microseconds in one second.
+   */
+  public static final long MICROS_PER_SECOND = 1000000L;
+
+  /**
+   * Represents an unbounded length of data.
+   */
+  public static final int LENGTH_UNBOUNDED = -1;
+
+  /**
+   * The name of the UTF-8 charset.
+   */
+  public static final String UTF8_NAME = "UTF-8";
+
+  /**
+   * Sample flag that indicates the sample is a synchronization sample.
+   */
+  @SuppressWarnings("InlinedApi")
+  public static final int SAMPLE_FLAG_SYNC = MediaExtractor.SAMPLE_FLAG_SYNC;
+
+  private C() {}
+
+}

+ 58 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/CodecCounters.java

@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+/**
+ * Maintains codec event counts, for debugging purposes only.
+ * <p>
+ * Counters should be written from the playback thread only. Counters may be read from any thread.
+ * To ensure that the counter values are correctly reflected between threads, users of this class
+ * should invoke {@link #ensureUpdated()} prior to reading and after writing.
+ */
+public final class CodecCounters {
+
+  public int codecInitCount;
+  public int codecReleaseCount;
+  public int outputFormatChangedCount;
+  public int outputBuffersChangedCount;
+  public int renderedOutputBufferCount;
+  public int skippedOutputBufferCount;
+  public int droppedOutputBufferCount;
+
+  /**
+   * Should be invoked from the playback thread after the counters have been updated. Should also
+   * be invoked from any other thread that wishes to read the counters, before reading. These calls
+   * ensure that counter updates are made visible to the reading threads.
+   */
+  public synchronized void ensureUpdated() {
+    // Do nothing. The use of synchronized ensures a memory barrier should another thread also
+    // call this method.
+  }
+
+  public String getDebugString() {
+    ensureUpdated();
+    StringBuilder builder = new StringBuilder();
+    builder.append("cic(").append(codecInitCount).append(")");
+    builder.append("crc(").append(codecReleaseCount).append(")");
+    builder.append("ofc(").append(outputFormatChangedCount).append(")");
+    builder.append("obc(").append(outputBuffersChangedCount).append(")");
+    builder.append("ren(").append(renderedOutputBufferCount).append(")");
+    builder.append("sob(").append(skippedOutputBufferCount).append(")");
+    builder.append("dob(").append(droppedOutputBufferCount).append(")");
+    return builder.toString();
+  }
+
+}

+ 116 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/CryptoInfo.java

@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+import com.google.android.exoplayer.util.Util;
+
+import android.annotation.TargetApi;
+import android.media.MediaExtractor;
+
+/**
+ * Compatibility wrapper around {@link android.media.MediaCodec.CryptoInfo}.
+ */
+public class CryptoInfo {
+
+  /**
+   * @see android.media.MediaCodec.CryptoInfo#iv
+   */
+  public byte[] iv;
+  /**
+   * @see android.media.MediaCodec.CryptoInfo#key
+   */
+  public byte[] key;
+  /**
+   * @see android.media.MediaCodec.CryptoInfo#mode
+   */
+  public int mode;
+  /**
+   * @see android.media.MediaCodec.CryptoInfo#numBytesOfClearData
+   */
+  public int[] numBytesOfClearData;
+  /**
+   * @see android.media.MediaCodec.CryptoInfo#numBytesOfEncryptedData
+   */
+  public int[] numBytesOfEncryptedData;
+  /**
+   * @see android.media.MediaCodec.CryptoInfo#numSubSamples
+   */
+  public int numSubSamples;
+
+  private final android.media.MediaCodec.CryptoInfo frameworkCryptoInfo;
+
+  public CryptoInfo() {
+    frameworkCryptoInfo = Util.SDK_INT >= 16 ? newFrameworkCryptoInfoV16() : null;
+  }
+
+  /**
+   * @see android.media.MediaCodec.CryptoInfo#set(int, int[], int[], byte[], byte[], int)
+   */
+  public void set(int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData,
+      byte[] key, byte[] iv, int mode) {
+    this.numSubSamples = numSubSamples;
+    this.numBytesOfClearData = numBytesOfClearData;
+    this.numBytesOfEncryptedData = numBytesOfEncryptedData;
+    this.key = key;
+    this.iv = iv;
+    this.mode = mode;
+    if (Util.SDK_INT >= 16) {
+      updateFrameworkCryptoInfoV16();
+    }
+  }
+
+  /**
+   * Equivalent to {@link MediaExtractor#getSampleCryptoInfo(android.media.MediaCodec.CryptoInfo)}.
+   *
+   * @param extractor The extractor from which to retrieve the crypto information.
+   */
+  @TargetApi(16)
+  public void setFromExtractorV16(MediaExtractor extractor) {
+    extractor.getSampleCryptoInfo(frameworkCryptoInfo);
+    numSubSamples = frameworkCryptoInfo.numSubSamples;
+    numBytesOfClearData = frameworkCryptoInfo.numBytesOfClearData;
+    numBytesOfEncryptedData = frameworkCryptoInfo.numBytesOfEncryptedData;
+    key = frameworkCryptoInfo.key;
+    iv = frameworkCryptoInfo.iv;
+    mode = frameworkCryptoInfo.mode;
+  }
+
+  /**
+   * Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance.
+   * <p>
+   * Successive calls to this method on a single {@link CryptoInfo} will return the same instance.
+   * Changes to the {@link CryptoInfo} will be reflected in the returned object. The return object
+   * should not be modified directly.
+   *
+   * @return The equivalent {@link android.media.MediaCodec.CryptoInfo} instance.
+   */
+  @TargetApi(16)
+  public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfoV16() {
+    return frameworkCryptoInfo;
+  }
+
+  @TargetApi(16)
+  private android.media.MediaCodec.CryptoInfo newFrameworkCryptoInfoV16() {
+    return new android.media.MediaCodec.CryptoInfo();
+  }
+
+  @TargetApi(16)
+  private void updateFrameworkCryptoInfoV16() {
+    frameworkCryptoInfo.set(numSubSamples, numBytesOfClearData, numBytesOfEncryptedData, key, iv,
+        mode);
+  }
+
+}

+ 48 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/DecoderInfo.java

@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+/**
+ * Contains information about a media decoder.
+ */
+public final class DecoderInfo {
+
+  /**
+   * The name of the decoder.
+   * <p>
+   * May be passed to {@link android.media.MediaCodec#createByCodecName(String)} to create an
+   * instance of the decoder.
+   */
+  public final String name;
+
+  /**
+   * Whether the decoder supports seamless resolution switches.
+   *
+   * @see android.media.MediaCodecInfo.CodecCapabilities#isFeatureSupported(String)
+   * @see android.media.MediaCodecInfo.CodecCapabilities#FEATURE_AdaptivePlayback
+   */
+  public final boolean adaptive;
+
+  /**
+   * @param name The name of the decoder.
+   * @param adaptive Whether the decoder is adaptive.
+   */
+  /* package */ DecoderInfo(String name, boolean adaptive) {
+    this.name = name;
+    this.adaptive = adaptive;
+  }
+
+}

+ 284 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/DefaultLoadControl.java

@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+import com.google.android.exoplayer.upstream.Allocator;
+import com.google.android.exoplayer.upstream.NetworkLock;
+
+import android.os.Handler;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * A {@link LoadControl} implementation that allows loads to continue in a sequence that prevents
+ * any loader from getting too far ahead or behind any of the other loaders.
+ * <p>
+ * Loads are scheduled so as to fill the available buffer space as rapidly as possible. Once the
+ * duration of buffered media and the buffer utilization both exceed respective thresholds, the
+ * control switches to a draining state during which no loads are permitted to start. During
+ * draining periods, resources such as the device radio have an opportunity to switch into low
+ * power modes. The control reverts back to the loading state when either the duration of buffered
+ * media or the buffer utilization fall below respective thresholds.
+ * <p>
+ * This implementation of {@link LoadControl} integrates with {@link NetworkLock}, by registering
+ * itself as a task with priority {@link NetworkLock#STREAMING_PRIORITY} during loading periods,
+ * and unregistering itself during draining periods.
+ */
+public class DefaultLoadControl implements LoadControl {
+
+  /**
+   * Interface definition for a callback to be notified of {@link DefaultLoadControl} events.
+   */
+  public interface EventListener {
+
+    /**
+     * Invoked when the control transitions from a loading to a draining state, or vice versa.
+     *
+     * @param loading Whether the control is now in a loading state.
+     */
+    void onLoadingChanged(boolean loading);
+
+  }
+
+  public static final int DEFAULT_LOW_WATERMARK_MS = 15000;
+  public static final int DEFAULT_HIGH_WATERMARK_MS = 30000;
+  public static final float DEFAULT_LOW_POOL_LOAD = 0.2f;
+  public static final float DEFAULT_HIGH_POOL_LOAD = 0.8f;
+
+  private static final int ABOVE_HIGH_WATERMARK = 0;
+  private static final int BETWEEN_WATERMARKS = 1;
+  private static final int BELOW_LOW_WATERMARK = 2;
+
+  private final Allocator allocator;
+  private final List<Object> loaders;
+  private final HashMap<Object, LoaderState> loaderStates;
+  private final Handler eventHandler;
+  private final EventListener eventListener;
+
+  private final long lowWatermarkUs;
+  private final long highWatermarkUs;
+  private final float lowPoolLoad;
+  private final float highPoolLoad;
+
+  private int targetBufferSize;
+  private long maxLoadStartPositionUs;
+  private int bufferPoolState;
+  private boolean fillingBuffers;
+  private boolean streamingPrioritySet;
+
+  /**
+   * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class.
+   *
+   * @param allocator The {@link Allocator} used by the loader.
+   */
+  public DefaultLoadControl(Allocator allocator) {
+    this(allocator, null, null);
+  }
+
+  /**
+   * Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class.
+   *
+   * @param allocator The {@link Allocator} used by the loader.
+   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+   *     null if delivery of events is not required.
+   * @param eventListener A listener of events. May be null if delivery of events is not required.
+   */
+  public DefaultLoadControl(Allocator allocator, Handler eventHandler,
+      EventListener eventListener) {
+    this(allocator, eventHandler, eventListener, DEFAULT_LOW_WATERMARK_MS,
+        DEFAULT_HIGH_WATERMARK_MS, DEFAULT_LOW_POOL_LOAD, DEFAULT_HIGH_POOL_LOAD);
+  }
+
+  /**
+   * Constructs a new instance.
+   *
+   * @param allocator The {@link Allocator} used by the loader.
+   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+   *     null if delivery of events is not required.
+   * @param eventListener A listener of events. May be null if delivery of events is not required.
+   * @param lowWatermarkMs The minimum duration of media that can be buffered for the control to
+   *     be in the draining state. If less media is buffered, then the control will transition to
+   *     the filling state.
+   * @param highWatermarkMs The minimum duration of media that can be buffered for the control to
+   *     transition from filling to draining.
+   * @param lowPoolLoad The minimum fraction of the buffer that must be utilized for the control
+   *     to be in the draining state. If the utilization is lower, then the control will transition
+   *     to the filling state.
+   * @param highPoolLoad The minimum fraction of the buffer that must be utilized for the control
+   *     to transition from the loading state to the draining state.
+   */
+  public DefaultLoadControl(Allocator allocator, Handler eventHandler, EventListener eventListener,
+      int lowWatermarkMs, int highWatermarkMs, float lowPoolLoad, float highPoolLoad) {
+    this.allocator = allocator;
+    this.eventHandler = eventHandler;
+    this.eventListener = eventListener;
+    this.loaders = new ArrayList<Object>();
+    this.loaderStates = new HashMap<Object, LoaderState>();
+    this.lowWatermarkUs = lowWatermarkMs * 1000L;
+    this.highWatermarkUs = highWatermarkMs * 1000L;
+    this.lowPoolLoad = lowPoolLoad;
+    this.highPoolLoad = highPoolLoad;
+  }
+
+  @Override
+  public void register(Object loader, int bufferSizeContribution) {
+    loaders.add(loader);
+    loaderStates.put(loader, new LoaderState(bufferSizeContribution));
+    targetBufferSize += bufferSizeContribution;
+  }
+
+  @Override
+  public void unregister(Object loader) {
+    loaders.remove(loader);
+    LoaderState state = loaderStates.remove(loader);
+    targetBufferSize -= state.bufferSizeContribution;
+    updateControlState();
+  }
+
+  @Override
+  public void trimAllocator() {
+    allocator.trim(targetBufferSize);
+  }
+
+  @Override
+  public Allocator getAllocator() {
+    return allocator;
+  }
+
+  @Override
+  public boolean update(Object loader, long playbackPositionUs, long nextLoadPositionUs,
+      boolean loading, boolean failed) {
+    // Update the loader state.
+    int loaderBufferState = getLoaderBufferState(playbackPositionUs, nextLoadPositionUs);
+    LoaderState loaderState = loaderStates.get(loader);
+    boolean loaderStateChanged = loaderState.bufferState != loaderBufferState
+        || loaderState.nextLoadPositionUs != nextLoadPositionUs || loaderState.loading != loading
+        || loaderState.failed != failed;
+    if (loaderStateChanged) {
+      loaderState.bufferState = loaderBufferState;
+      loaderState.nextLoadPositionUs = nextLoadPositionUs;
+      loaderState.loading = loading;
+      loaderState.failed = failed;
+    }
+
+    // Update the buffer pool state.
+    int allocatedSize = allocator.getAllocatedSize();
+    int bufferPoolState = getBufferPoolState(allocatedSize);
+    boolean bufferPoolStateChanged = this.bufferPoolState != bufferPoolState;
+    if (bufferPoolStateChanged) {
+      this.bufferPoolState = bufferPoolState;
+    }
+
+    // If either of the individual states have changed, update the shared control state.
+    if (loaderStateChanged || bufferPoolStateChanged) {
+      updateControlState();
+    }
+
+    return allocatedSize < targetBufferSize && nextLoadPositionUs != -1
+        && nextLoadPositionUs <= maxLoadStartPositionUs;
+  }
+
+  private int getLoaderBufferState(long playbackPositionUs, long nextLoadPositionUs) {
+    if (nextLoadPositionUs == -1) {
+      return ABOVE_HIGH_WATERMARK;
+    } else {
+      long timeUntilNextLoadPosition = nextLoadPositionUs - playbackPositionUs;
+      return timeUntilNextLoadPosition > highWatermarkUs ? ABOVE_HIGH_WATERMARK :
+          timeUntilNextLoadPosition < lowWatermarkUs ? BELOW_LOW_WATERMARK :
+          BETWEEN_WATERMARKS;
+    }
+  }
+
+  private int getBufferPoolState(int allocatedSize) {
+    float bufferPoolLoad = (float) allocatedSize / targetBufferSize;
+    return bufferPoolLoad > highPoolLoad ? ABOVE_HIGH_WATERMARK :
+        bufferPoolLoad < lowPoolLoad ? BELOW_LOW_WATERMARK :
+        BETWEEN_WATERMARKS;
+  }
+
+  private void updateControlState() {
+    boolean loading = false;
+    boolean failed = false;
+    boolean haveNextLoadPosition = false;
+    int highestState = bufferPoolState;
+    for (int i = 0; i < loaders.size(); i++) {
+      LoaderState loaderState = loaderStates.get(loaders.get(i));
+      loading |= loaderState.loading;
+      failed |= loaderState.failed;
+      haveNextLoadPosition |= loaderState.nextLoadPositionUs != -1;
+      highestState = Math.max(highestState, loaderState.bufferState);
+    }
+
+    fillingBuffers = !loaders.isEmpty() && !failed && (loading || haveNextLoadPosition)
+        && (highestState == BELOW_LOW_WATERMARK
+        || (highestState == BETWEEN_WATERMARKS && fillingBuffers));
+    if (fillingBuffers && !streamingPrioritySet) {
+      NetworkLock.instance.add(NetworkLock.STREAMING_PRIORITY);
+      streamingPrioritySet = true;
+      notifyLoadingChanged(true);
+    } else if (!fillingBuffers && streamingPrioritySet && !loading) {
+      NetworkLock.instance.remove(NetworkLock.STREAMING_PRIORITY);
+      streamingPrioritySet = false;
+      notifyLoadingChanged(false);
+    }
+
+    maxLoadStartPositionUs = -1;
+    if (fillingBuffers) {
+      for (int i = 0; i < loaders.size(); i++) {
+        Object loader = loaders.get(i);
+        LoaderState loaderState = loaderStates.get(loader);
+        long loaderTime = loaderState.nextLoadPositionUs;
+        if (loaderTime != -1
+            && (maxLoadStartPositionUs == -1 || loaderTime < maxLoadStartPositionUs)) {
+          maxLoadStartPositionUs = loaderTime;
+        }
+      }
+    }
+  }
+
+  private void notifyLoadingChanged(final boolean loading) {
+    if (eventHandler != null && eventListener != null) {
+      eventHandler.post(new Runnable()  {
+        @Override
+        public void run() {
+          eventListener.onLoadingChanged(loading);
+        }
+      });
+    }
+  }
+
+  private static class LoaderState {
+
+    public final int bufferSizeContribution;
+
+    public int bufferState;
+    public boolean loading;
+    public boolean failed;
+    public long nextLoadPositionUs;
+
+    public LoaderState(int bufferSizeContribution) {
+      this.bufferSizeContribution = bufferSizeContribution;
+      bufferState = ABOVE_HIGH_WATERMARK;
+      loading = false;
+      failed = false;
+      nextLoadPositionUs = -1;
+    }
+
+  }
+
+}

+ 67 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/DummyTrackRenderer.java

@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+/**
+ * A {@link TrackRenderer} that does nothing.
+ * <p>
+ * This renderer returns {@link TrackRenderer#STATE_IGNORE} from {@link #doPrepare()} in order to
+ * request that it should be ignored. {@link IllegalStateException} is thrown from all methods that
+ * are documented to indicate that they should not be invoked unless the renderer is prepared.
+ */
+public class DummyTrackRenderer extends TrackRenderer {
+
+  @Override
+  protected int doPrepare() throws ExoPlaybackException {
+    return STATE_IGNORE;
+  }
+
+  @Override
+  protected boolean isEnded() {
+    throw new IllegalStateException();
+  }
+
+  @Override
+  protected boolean isReady() {
+    throw new IllegalStateException();
+  }
+
+  @Override
+  protected void seekTo(long positionUs) {
+    throw new IllegalStateException();
+  }
+
+  @Override
+  protected void doSomeWork(long positionUs, long elapsedRealtimeUs) {
+    throw new IllegalStateException();
+  }
+
+  @Override
+  protected long getDurationUs() {
+    throw new IllegalStateException();
+  }
+
+  @Override
+  protected long getBufferedPositionUs() {
+    throw new IllegalStateException();
+  }
+
+  @Override
+  protected long getCurrentPositionUs() {
+    throw new IllegalStateException();
+  }
+
+}

+ 37 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/ExoPlaybackException.java

@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+/**
+ * Thrown when a non-recoverable playback failure occurs.
+ * <p>
+ * Where possible, the cause returned by {@link #getCause()} will indicate the reason for failure.
+ */
+public class ExoPlaybackException extends Exception {
+
+  public ExoPlaybackException(String message) {
+    super(message);
+  }
+
+  public ExoPlaybackException(Throwable cause) {
+    super(cause);
+  }
+
+  public ExoPlaybackException(String message, Throwable cause) {
+    super(message, cause);
+  }
+
+}

+ 391 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/ExoPlayer.java

@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+import android.os.Looper;
+
+/**
+ * An extensible media player exposing traditional high-level media player functionality, such as
+ * the ability to prepare, play, pause and seek.
+ *
+ * <p>Topics covered here are:
+ * <ol>
+ * <li><a href="#Assumptions">Assumptions and player composition</a>
+ * <li><a href="#Threading">Threading model</a>
+ * <li><a href="#State">Player state</a>
+ * </ol>
+ *
+ * <a name="Assumptions"></a>
+ * <h3>Assumptions and player construction</h3>
+ *
+ * <p>The implementation is designed make no assumptions about (and hence impose no restrictions
+ * on) the type of the media being played, how and where it is stored, or how it is rendered.
+ * Rather than implementing the loading and rendering of media directly, {@link ExoPlayer} instead
+ * delegates this work to one or more {@link TrackRenderer}s, which are injected when the player
+ * is prepared. Hence {@link ExoPlayer} is capable of loading and playing any media for which a
+ * {@link TrackRenderer} implementation can be provided.
+ *
+ * <p>{@link MediaCodecAudioTrackRenderer} and {@link MediaCodecVideoTrackRenderer} can be used for
+ * the common cases of rendering audio and video. These components in turn require an
+ * <i>upstream</i> {@link SampleSource} to be injected through their constructors, where upstream
+ * is defined to denote a component that is closer to the source of the media. This pattern of
+ * upstream dependency injection is actively encouraged, since it means that the functionality of
+ * the player is built up through the composition of components that can easily be exchanged for
+ * alternate implementations. For example a {@link SampleSource} implementation may require a
+ * further upstream data loading component to be injected through its constructor, with different
+ * implementations enabling the loading of data from various sources.
+ *
+ * <a name="Threading"></a>
+ * <h3>Threading model</h3>
+ *
+ * <p>The figure below shows the {@link ExoPlayer} threading model.</p>
+ * <p align="center"><img src="../../../../../doc_src/images/exoplayer_threading_model.png"
+ *     alt="MediaPlayer state diagram"
+ *     border="0"/></p>
+ *
+ * <ul>
+ * <li>It is recommended that instances are created and accessed from a single application thread.
+ * An application's main thread is ideal. Accessing an instance from multiple threads is
+ * discouraged, however if an application does wish to do this then it may do so provided that it
+ * ensures accesses are synchronized.
+ * </li>
+ * <li>Registered {@link Listener}s are invoked on the thread that created the {@link ExoPlayer}
+ * instance.</li>
+ * <li>An internal playback thread is responsible for managing playback and invoking the
+ * {@link TrackRenderer}s in order to load and play the media.</li>
+ * <li>{@link TrackRenderer} implementations (or any upstream components that they depend on) may
+ * use additional background threads (e.g. to load data). These are implementation specific.</li>
+ * </ul>
+ *
+ * <a name="State"></a>
+ * <h3>Player state</h3>
+ *
+ * <p>The components of an {@link ExoPlayer}'s state can be divided into two distinct groups. State
+ * accessed by {@link #getRendererEnabled(int)} and {@link #getPlayWhenReady()} are only ever
+ * changed by invoking the player's methods, and are never changed as a result of operations that
+ * have been performed asynchronously by the playback thread. In contrast, the playback state
+ * accessed by {@link #getPlaybackState()} is only ever changed as a result of operations
+ * completing on the playback thread, as illustrated below.</p>
+ * <p align="center"><img src="../../../../../doc_src/images/exoplayer_state.png"
+ *     alt="ExoPlayer state"
+ *     border="0"/></p>
+ *
+ * <p>The possible playback state transitions are shown below. Transitions can be triggered either
+ * by changes in the state of the {@link TrackRenderer}s being used, or as a result of
+ * {@link #prepare(TrackRenderer[])}, {@link #stop()} or {@link #release()} being invoked.</p>
+ * <p align="center"><img src="../../../../../doc_src/images/exoplayer_playbackstate.png"
+ *     alt="ExoPlayer playback state transitions"
+ *     border="0"/></p>
+ */
+public interface ExoPlayer {
+
+  /**
+   * A factory for instantiating ExoPlayer instances.
+   */
+  public static final class Factory {
+
+    /**
+     * The default minimum duration of data that must be buffered for playback to start or resume
+     * following a user action such as a seek.
+     */
+    public static final int DEFAULT_MIN_BUFFER_MS = 500;
+
+    /**
+     * The default minimum duration of data that must be buffered for playback to resume
+     * after a player invoked rebuffer (i.e. a rebuffer that occurs due to buffer depletion, and
+     * not due to a user action such as starting playback or seeking).
+     */
+    public static final int DEFAULT_MIN_REBUFFER_MS = 5000;
+
+    private Factory() {}
+
+    /**
+     * Obtains an {@link ExoPlayer} instance.
+     * <p>
+     * Must be invoked from a thread that has an associated {@link Looper}.
+     *
+     * @param rendererCount The number of {@link TrackRenderer}s that will be passed to
+     *     {@link #prepare(TrackRenderer[])}.
+     * @param minBufferMs A minimum duration of data that must be buffered for playback to start
+     *     or resume following a user action such as a seek.
+     * @param minRebufferMs A minimum duration of data that must be buffered for playback to resume
+     *     after a player invoked rebuffer (i.e. a rebuffer that occurs due to buffer depletion, and
+     *     not due to a user action such as starting playback or seeking).
+     */
+    public static ExoPlayer newInstance(int rendererCount, int minBufferMs, int minRebufferMs) {
+      return new ExoPlayerImpl(rendererCount, minBufferMs, minRebufferMs);
+    }
+
+    /**
+     * Obtains an {@link ExoPlayer} instance.
+     * <p>
+     * Must be invoked from a thread that has an associated {@link Looper}.
+     *
+     * @param rendererCount The number of {@link TrackRenderer}s that will be passed to
+     *     {@link #prepare(TrackRenderer[])}.
+     */
+    public static ExoPlayer newInstance(int rendererCount) {
+      return new ExoPlayerImpl(rendererCount, DEFAULT_MIN_BUFFER_MS, DEFAULT_MIN_REBUFFER_MS);
+    }
+
+    /**
+     * @deprecated Please use {@link #newInstance(int, int, int)}.
+     */
+    @Deprecated
+    public static ExoPlayer newInstance(int rendererCount, int minRebufferMs) {
+      return new ExoPlayerImpl(rendererCount, DEFAULT_MIN_BUFFER_MS, minRebufferMs);
+    }
+
+  }
+
+  /**
+   * Interface definition for a callback to be notified of changes in player state.
+   */
+  public interface Listener {
+    /**
+     * Invoked when the value returned from either {@link ExoPlayer#getPlayWhenReady()} or
+     * {@link ExoPlayer#getPlaybackState()} changes.
+     *
+     * @param playWhenReady Whether playback will proceed when ready.
+     * @param playbackState One of the {@code STATE} constants defined in this class.
+     */
+    void onPlayerStateChanged(boolean playWhenReady, int playbackState);
+    /**
+     * Invoked when the current value of {@link ExoPlayer#getPlayWhenReady()} has been reflected
+     * by the internal playback thread.
+     * <p>
+     * An invocation of this method will shortly follow any call to
+     * {@link ExoPlayer#setPlayWhenReady(boolean)} that changes the state. If multiple calls are
+     * made in rapid succession, then this method will be invoked only once, after the final state
+     * has been reflected.
+     */
+    void onPlayWhenReadyCommitted();
+    /**
+     * Invoked when an error occurs. The playback state will transition to
+     * {@link ExoPlayer#STATE_IDLE} immediately after this method is invoked. The player instance
+     * can still be used, and {@link ExoPlayer#release()} must still be called on the player should
+     * it no longer be required.
+     *
+     * @param error The error.
+     */
+    void onPlayerError(ExoPlaybackException error);
+  }
+
+  /**
+   * A component of an {@link ExoPlayer} that can receive messages on the playback thread.
+   * <p>
+   * Messages can be delivered to a component via {@link ExoPlayer#sendMessage} and
+   * {@link ExoPlayer#blockingSendMessage}.
+   */
+  public interface ExoPlayerComponent {
+
+    /**
+     * Handles a message delivered to the component. Invoked on the playback thread.
+     *
+     * @param messageType An integer identifying the type of message.
+     * @param message The message object.
+     * @throws ExoPlaybackException If an error occurred whilst handling the message.
+     */
+    void handleMessage(int messageType, Object message) throws ExoPlaybackException;
+
+  }
+
+  /**
+   * The player is neither prepared or being prepared.
+   */
+  public static final int STATE_IDLE = 1;
+  /**
+   * The player is being prepared.
+   */
+  public static final int STATE_PREPARING = 2;
+  /**
+   * The player is prepared but not able to immediately play from the current position. The cause
+   * is {@link TrackRenderer} specific, but this state typically occurs when more data needs
+   * to be buffered for playback to start.
+   */
+  public static final int STATE_BUFFERING = 3;
+  /**
+   * The player is prepared and able to immediately play from the current position. The player will
+   * be playing if {@link #setPlayWhenReady(boolean)} returns true, and paused otherwise.
+   */
+  public static final int STATE_READY = 4;
+  /**
+   * The player has finished playing the media.
+   */
+  public static final int STATE_ENDED = 5;
+  /**
+   * Represents an unknown time or duration.
+   */
+  public static final long UNKNOWN_TIME = -1;
+
+  /**
+   * Gets the {@link Looper} associated with the playback thread.
+   *
+   * @return The {@link Looper} associated with the playback thread.
+   */
+  public Looper getPlaybackLooper();
+
+  /**
+   * Register a listener to receive events from the player. The listener's methods will be invoked
+   * on the thread that was used to construct the player.
+   *
+   * @param listener The listener to register.
+   */
+  public void addListener(Listener listener);
+
+  /**
+   * Unregister a listener. The listener will no longer receive events from the player.
+   *
+   * @param listener The listener to unregister.
+   */
+  public void removeListener(Listener listener);
+
+  /**
+   * Returns the current state of the player.
+   *
+   * @return One of the {@code STATE} constants defined in this class.
+   */
+  public int getPlaybackState();
+
+  /**
+   * Prepares the player for playback.
+   *
+   * @param renderers The {@link TrackRenderer}s to use. The number of renderers must match the
+   *     value that was passed to the {@link ExoPlayer.Factory#newInstance} method.
+   */
+  public void prepare(TrackRenderer... renderers);
+
+  /**
+   * Sets whether the renderer at the given index is enabled.
+   *
+   * @param index The index of the renderer.
+   * @param enabled Whether the renderer at the given index should be enabled.
+   */
+  public void setRendererEnabled(int index, boolean enabled);
+
+  /**
+   * Whether the renderer at the given index is enabled.
+   *
+   * @param index The index of the renderer.
+   * @return Whether the renderer is enabled.
+   */
+  public boolean getRendererEnabled(int index);
+
+  /**
+   * Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
+   * If the player is already in this state, then this method can be used to pause and resume
+   * playback.
+   *
+   * @param playWhenReady Whether playback should proceed when ready.
+   */
+  public void setPlayWhenReady(boolean playWhenReady);
+
+  /**
+   * Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
+   *
+   * @return Whether playback will proceed when ready.
+   */
+  public boolean getPlayWhenReady();
+
+  /**
+   * Whether the current value of {@link ExoPlayer#getPlayWhenReady()} has been reflected by the
+   * internal playback thread.
+   *
+   * @return True if the current value has been reflected. False otherwise.
+   */
+  public boolean isPlayWhenReadyCommitted();
+
+  /**
+   * Seeks to a position specified in milliseconds.
+   *
+   * @param positionMs The seek position.
+   */
+  public void seekTo(long positionMs);
+
+  /**
+   * Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention
+   * is to pause playback.
+   * <p>
+   * Calling this method will cause the playback state to transition to
+   * {@link ExoPlayer#STATE_IDLE}. The player instance can still be used, and
+   * {@link ExoPlayer#release()} must still be called on the player if it's no longer required.
+   * <p>
+   * Calling this method does not reset the playback position. If this player instance will be used
+   * to play another video from its start, then {@code seekTo(0)} should be called after stopping
+   * the player and before preparing it for the next video.
+   */
+  public void stop();
+
+  /**
+   * Releases the player. This method must be called when the player is no longer required.
+   * <p>
+   * The player must not be used after calling this method.
+   */
+  public void release();
+
+  /**
+   * Sends a message to a specified component. The message is delivered to the component on the
+   * playback thread. If the component throws a {@link ExoPlaybackException}, then it is
+   * propagated out of the player as an error.
+   *
+   * @param target The target to which the message should be delivered.
+   * @param messageType An integer that can be used to identify the type of the message.
+   * @param message The message object.
+   */
+  public void sendMessage(ExoPlayerComponent target, int messageType, Object message);
+
+  /**
+   * Blocking variant of {@link #sendMessage(ExoPlayerComponent, int, Object)} that does not return
+   * until after the message has been delivered.
+   *
+   * @param target The target to which the message should be delivered.
+   * @param messageType An integer that can be used to identify the type of the message.
+   * @param message The message object.
+   */
+  public void blockingSendMessage(ExoPlayerComponent target, int messageType, Object message);
+
+  /**
+   * Gets the duration of the track in milliseconds.
+   *
+   * @return The duration of the track in milliseconds, or {@link ExoPlayer#UNKNOWN_TIME} if the
+   *     duration is not known.
+   */
+  public long getDuration();
+
+  /**
+   * Gets the current playback position in milliseconds.
+   *
+   * @return The current playback position in milliseconds.
+   */
+  public long getCurrentPosition();
+
+  /**
+   * Gets an estimate of the absolute position in milliseconds up to which data is buffered.
+   *
+   * @return An estimate of the absolute position in milliseconds up to which data is buffered,
+   *     or {@link ExoPlayer#UNKNOWN_TIME} if no estimate is available.
+   */
+  public long getBufferedPosition();
+
+  /**
+   * Gets an estimate of the percentage into the media up to which data is buffered.
+   *
+   * @return An estimate of the percentage into the media up to which data is buffered. 0 if the
+   *     duration of the media is not known or if no estimate is available.
+   */
+  public int getBufferedPercentage();
+
+}

+ 211 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/ExoPlayerImpl.java

@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+import android.annotation.SuppressLint;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * Concrete implementation of {@link ExoPlayer}.
+ */
+/* package */ final class ExoPlayerImpl implements ExoPlayer {
+
+  private static final String TAG = "ExoPlayerImpl";
+
+  private final Handler eventHandler;
+  private final ExoPlayerImplInternal internalPlayer;
+  private final CopyOnWriteArraySet<Listener> listeners;
+  private final boolean[] rendererEnabledFlags;
+
+  private boolean playWhenReady;
+  private int playbackState;
+  private int pendingPlayWhenReadyAcks;
+
+  /**
+   * Constructs an instance. Must be invoked from a thread that has an associated {@link Looper}.
+   *
+   * @param rendererCount The number of {@link TrackRenderer}s that will be passed to
+   *     {@link #prepare(TrackRenderer[])}.
+   * @param minBufferMs A minimum duration of data that must be buffered for playback to start
+   *     or resume following a user action such as a seek.
+   * @param minRebufferMs A minimum duration of data that must be buffered for playback to resume
+   *     after a player invoked rebuffer (i.e. a rebuffer that occurs due to buffer depletion, and
+   *     not due to a user action such as starting playback or seeking).
+   */
+  @SuppressLint("HandlerLeak")
+  public ExoPlayerImpl(int rendererCount, int minBufferMs, int minRebufferMs) {
+    Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION);
+    this.playWhenReady = false;
+    this.playbackState = STATE_IDLE;
+    this.listeners = new CopyOnWriteArraySet<Listener>();
+    this.rendererEnabledFlags = new boolean[rendererCount];
+    for (int i = 0; i < rendererEnabledFlags.length; i++) {
+      rendererEnabledFlags[i] = true;
+    }
+    eventHandler = new Handler() {
+      @Override
+      public void handleMessage(Message msg) {
+        ExoPlayerImpl.this.handleEvent(msg);
+      }
+    };
+    internalPlayer = new ExoPlayerImplInternal(eventHandler, playWhenReady, rendererEnabledFlags,
+        minBufferMs, minRebufferMs);
+  }
+
+  @Override
+  public Looper getPlaybackLooper() {
+    return internalPlayer.getPlaybackLooper();
+  }
+
+  @Override
+  public void addListener(Listener listener) {
+    listeners.add(listener);
+  }
+
+  @Override
+  public void removeListener(Listener listener) {
+    listeners.remove(listener);
+  }
+
+  @Override
+  public int getPlaybackState() {
+    return playbackState;
+  }
+
+  @Override
+  public void prepare(TrackRenderer... renderers) {
+    internalPlayer.prepare(renderers);
+  }
+
+  @Override
+  public void setRendererEnabled(int index, boolean enabled) {
+    if (rendererEnabledFlags[index] != enabled) {
+      rendererEnabledFlags[index] = enabled;
+      internalPlayer.setRendererEnabled(index, enabled);
+    }
+  }
+
+  @Override
+  public boolean getRendererEnabled(int index) {
+    return rendererEnabledFlags[index];
+  }
+
+  @Override
+  public void setPlayWhenReady(boolean playWhenReady) {
+    if (this.playWhenReady != playWhenReady) {
+      this.playWhenReady = playWhenReady;
+      pendingPlayWhenReadyAcks++;
+      internalPlayer.setPlayWhenReady(playWhenReady);
+      for (Listener listener : listeners) {
+        listener.onPlayerStateChanged(playWhenReady, playbackState);
+      }
+    }
+  }
+
+  @Override
+  public boolean getPlayWhenReady() {
+    return playWhenReady;
+  }
+
+  @Override
+  public boolean isPlayWhenReadyCommitted() {
+    return pendingPlayWhenReadyAcks == 0;
+  }
+
+  @Override
+  public void seekTo(long positionMs) {
+    internalPlayer.seekTo(positionMs);
+  }
+
+  @Override
+  public void stop() {
+    internalPlayer.stop();
+  }
+
+  @Override
+  public void release() {
+    internalPlayer.release();
+    eventHandler.removeCallbacksAndMessages(null);
+  }
+
+  @Override
+  public void sendMessage(ExoPlayerComponent target, int messageType, Object message) {
+    internalPlayer.sendMessage(target, messageType, message);
+  }
+
+  @Override
+  public void blockingSendMessage(ExoPlayerComponent target, int messageType, Object message) {
+    internalPlayer.blockingSendMessage(target, messageType, message);
+  }
+
+  @Override
+  public long getDuration() {
+    return internalPlayer.getDuration();
+  }
+
+  @Override
+  public long getCurrentPosition() {
+    return internalPlayer.getCurrentPosition();
+  }
+
+  @Override
+  public long getBufferedPosition() {
+    return internalPlayer.getBufferedPosition();
+  }
+
+  @Override
+  public int getBufferedPercentage() {
+    long bufferedPosition = getBufferedPosition();
+    long duration = getDuration();
+    return bufferedPosition == ExoPlayer.UNKNOWN_TIME || duration == ExoPlayer.UNKNOWN_TIME ? 0
+        : (int) (duration == 0 ? 100 : (bufferedPosition * 100) / duration);
+  }
+
+  // Not private so it can be called from an inner class without going through a thunk method.
+  /* package */ void handleEvent(Message msg) {
+    switch (msg.what) {
+      case ExoPlayerImplInternal.MSG_STATE_CHANGED: {
+        playbackState = msg.arg1;
+        for (Listener listener : listeners) {
+          listener.onPlayerStateChanged(playWhenReady, playbackState);
+        }
+        break;
+      }
+      case ExoPlayerImplInternal.MSG_SET_PLAY_WHEN_READY_ACK: {
+        pendingPlayWhenReadyAcks--;
+        if (pendingPlayWhenReadyAcks == 0) {
+          for (Listener listener : listeners) {
+            listener.onPlayWhenReadyCommitted();
+          }
+        }
+        break;
+      }
+      case ExoPlayerImplInternal.MSG_ERROR: {
+        ExoPlaybackException exception = (ExoPlaybackException) msg.obj;
+        for (Listener listener : listeners) {
+          listener.onPlayerError(exception);
+        }
+        break;
+      }
+    }
+  }
+
+}

+ 598 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java

@@ -0,0 +1,598 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent;
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.PriorityHandlerThread;
+import com.google.android.exoplayer.util.TraceUtil;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.Pair;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Implements the internal behavior of {@link ExoPlayerImpl}.
+ */
+/* package */ final class ExoPlayerImplInternal implements Handler.Callback {
+
+  private static final String TAG = "ExoPlayerImplInternal";
+
+  // External messages
+  public static final int MSG_STATE_CHANGED = 1;
+  public static final int MSG_SET_PLAY_WHEN_READY_ACK = 2;
+  public static final int MSG_ERROR = 3;
+
+  // Internal messages
+  private static final int MSG_PREPARE = 1;
+  private static final int MSG_INCREMENTAL_PREPARE = 2;
+  private static final int MSG_SET_PLAY_WHEN_READY = 3;
+  private static final int MSG_STOP = 4;
+  private static final int MSG_RELEASE = 5;
+  private static final int MSG_SEEK_TO = 6;
+  private static final int MSG_DO_SOME_WORK = 7;
+  private static final int MSG_SET_RENDERER_ENABLED = 8;
+  private static final int MSG_CUSTOM = 9;
+
+  private static final int PREPARE_INTERVAL_MS = 10;
+  private static final int RENDERING_INTERVAL_MS = 10;
+  private static final int IDLE_INTERVAL_MS = 1000;
+
+  private final Handler handler;
+  private final HandlerThread internalPlaybackThread;
+  private final Handler eventHandler;
+  private final MediaClock mediaClock;
+  private final boolean[] rendererEnabledFlags;
+  private final long minBufferUs;
+  private final long minRebufferUs;
+
+  private final List<TrackRenderer> enabledRenderers;
+  private TrackRenderer[] renderers;
+  private TrackRenderer timeSourceTrackRenderer;
+
+  private boolean released;
+  private boolean playWhenReady;
+  private boolean rebuffering;
+  private int state;
+  private int customMessagesSent = 0;
+  private int customMessagesProcessed = 0;
+  private long elapsedRealtimeUs;
+
+  private volatile long durationUs;
+  private volatile long positionUs;
+  private volatile long bufferedPositionUs;
+
+  public ExoPlayerImplInternal(Handler eventHandler, boolean playWhenReady,
+      boolean[] rendererEnabledFlags, int minBufferMs, int minRebufferMs) {
+    this.eventHandler = eventHandler;
+    this.playWhenReady = playWhenReady;
+    this.rendererEnabledFlags = new boolean[rendererEnabledFlags.length];
+    this.minBufferUs = minBufferMs * 1000L;
+    this.minRebufferUs = minRebufferMs * 1000L;
+    for (int i = 0; i < rendererEnabledFlags.length; i++) {
+      this.rendererEnabledFlags[i] = rendererEnabledFlags[i];
+    }
+
+    this.state = ExoPlayer.STATE_IDLE;
+    this.durationUs = TrackRenderer.UNKNOWN_TIME_US;
+    this.bufferedPositionUs = TrackRenderer.UNKNOWN_TIME_US;
+
+    mediaClock = new MediaClock();
+    enabledRenderers = new ArrayList<TrackRenderer>(rendererEnabledFlags.length);
+    // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
+    // not normally change to this priority" is incorrect.
+    internalPlaybackThread = new PriorityHandlerThread(getClass().getSimpleName() + ":Handler",
+        Process.THREAD_PRIORITY_AUDIO);
+    internalPlaybackThread.start();
+    handler = new Handler(internalPlaybackThread.getLooper(), this);
+  }
+
+  public Looper getPlaybackLooper() {
+    return internalPlaybackThread.getLooper();
+  }
+
+  public long getCurrentPosition() {
+    return positionUs / 1000;
+  }
+
+  public long getBufferedPosition() {
+    return bufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME
+        : bufferedPositionUs / 1000;
+  }
+
+  public long getDuration() {
+    return durationUs == TrackRenderer.UNKNOWN_TIME_US ? ExoPlayer.UNKNOWN_TIME
+        : durationUs / 1000;
+  }
+
+  public void prepare(TrackRenderer... renderers) {
+    handler.obtainMessage(MSG_PREPARE, renderers).sendToTarget();
+  }
+
+  public void setPlayWhenReady(boolean playWhenReady) {
+    handler.obtainMessage(MSG_SET_PLAY_WHEN_READY, playWhenReady ? 1 : 0, 0).sendToTarget();
+  }
+
+  public void seekTo(long positionMs) {
+    handler.obtainMessage(MSG_SEEK_TO, positionMs).sendToTarget();
+  }
+
+  public void stop() {
+    handler.sendEmptyMessage(MSG_STOP);
+  }
+
+  public void setRendererEnabled(int index, boolean enabled) {
+    handler.obtainMessage(MSG_SET_RENDERER_ENABLED, index, enabled ? 1 : 0).sendToTarget();
+  }
+
+  public void sendMessage(ExoPlayerComponent target, int messageType, Object message) {
+    customMessagesSent++;
+    handler.obtainMessage(MSG_CUSTOM, messageType, 0, Pair.create(target, message)).sendToTarget();
+  }
+
+  public synchronized void blockingSendMessage(ExoPlayerComponent target, int messageType,
+      Object message) {
+    if (released) {
+      Log.w(TAG, "Sent message(" + messageType + ") after release. Message ignored.");
+      return;
+    }
+    int messageNumber = customMessagesSent++;
+    handler.obtainMessage(MSG_CUSTOM, messageType, 0, Pair.create(target, message)).sendToTarget();
+    while (customMessagesProcessed <= messageNumber) {
+      try {
+        wait();
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+      }
+    }
+  }
+
+  public synchronized void release() {
+    if (released) {
+      return;
+    }
+    handler.sendEmptyMessage(MSG_RELEASE);
+    while (!released) {
+      try {
+        wait();
+      } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+      }
+    }
+    internalPlaybackThread.quit();
+  }
+
+  @Override
+  public boolean handleMessage(Message msg) {
+    try {
+      switch (msg.what) {
+        case MSG_PREPARE: {
+          prepareInternal((TrackRenderer[]) msg.obj);
+          return true;
+        }
+        case MSG_INCREMENTAL_PREPARE: {
+          incrementalPrepareInternal();
+          return true;
+        }
+        case MSG_SET_PLAY_WHEN_READY: {
+          setPlayWhenReadyInternal(msg.arg1 != 0);
+          return true;
+        }
+        case MSG_DO_SOME_WORK: {
+          doSomeWork();
+          return true;
+        }
+        case MSG_SEEK_TO: {
+          seekToInternal((Long) msg.obj);
+          return true;
+        }
+        case MSG_STOP: {
+          stopInternal();
+          return true;
+        }
+        case MSG_RELEASE: {
+          releaseInternal();
+          return true;
+        }
+        case MSG_CUSTOM: {
+          sendMessageInternal(msg.arg1, msg.obj);
+          return true;
+        }
+        case MSG_SET_RENDERER_ENABLED: {
+          setRendererEnabledInternal(msg.arg1, msg.arg2 != 0);
+          return true;
+        }
+        default:
+          return false;
+      }
+    } catch (ExoPlaybackException e) {
+      Log.e(TAG, "Internal track renderer error.", e);
+      eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
+      stopInternal();
+      return true;
+    } catch (RuntimeException e) {
+      Log.e(TAG, "Internal runtime error.", e);
+      eventHandler.obtainMessage(MSG_ERROR, new ExoPlaybackException(e)).sendToTarget();
+      stopInternal();
+      return true;
+    }
+  }
+
+  private void setState(int state) {
+    if (this.state != state) {
+      this.state = state;
+      eventHandler.obtainMessage(MSG_STATE_CHANGED, state, 0).sendToTarget();
+    }
+  }
+
+  private void prepareInternal(TrackRenderer[] renderers) {
+    rebuffering = false;
+    this.renderers = renderers;
+    for (int i = 0; i < renderers.length; i++) {
+      if (renderers[i].isTimeSource()) {
+        Assertions.checkState(timeSourceTrackRenderer == null);
+        timeSourceTrackRenderer = renderers[i];
+      }
+    }
+    setState(ExoPlayer.STATE_PREPARING);
+    handler.sendEmptyMessage(MSG_INCREMENTAL_PREPARE);
+  }
+
+  private void incrementalPrepareInternal() throws ExoPlaybackException {
+    long operationStartTimeMs = SystemClock.elapsedRealtime();
+    boolean prepared = true;
+    for (int i = 0; i < renderers.length; i++) {
+      if (renderers[i].getState() == TrackRenderer.STATE_UNPREPARED) {
+        int state = renderers[i].prepare();
+        if (state == TrackRenderer.STATE_UNPREPARED) {
+          prepared = false;
+        }
+      }
+    }
+
+    if (!prepared) {
+      // We're still waiting for some sources to be prepared.
+      scheduleNextOperation(MSG_INCREMENTAL_PREPARE, operationStartTimeMs, PREPARE_INTERVAL_MS);
+      return;
+    }
+
+    long durationUs = 0;
+    boolean isEnded = true;
+    boolean allRenderersReadyOrEnded = true;
+    for (int i = 0; i < renderers.length; i++) {
+      TrackRenderer renderer = renderers[i];
+      if (rendererEnabledFlags[i] && renderer.getState() == TrackRenderer.STATE_PREPARED) {
+        renderer.enable(positionUs, false);
+        enabledRenderers.add(renderer);
+        isEnded = isEnded && renderer.isEnded();
+        allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer);
+        if (durationUs == TrackRenderer.UNKNOWN_TIME_US) {
+          // We've already encountered a track for which the duration is unknown, so the media
+          // duration is unknown regardless of the duration of this track.
+        } else {
+          long trackDurationUs = renderer.getDurationUs();
+          if (trackDurationUs == TrackRenderer.UNKNOWN_TIME_US) {
+            durationUs = TrackRenderer.UNKNOWN_TIME_US;
+          } else if (trackDurationUs == TrackRenderer.MATCH_LONGEST_US) {
+            // Do nothing.
+          } else {
+            durationUs = Math.max(durationUs, trackDurationUs);
+          }
+        }
+      }
+    }
+    this.durationUs = durationUs;
+
+    if (isEnded) {
+      // We don't expect this case, but handle it anyway.
+      setState(ExoPlayer.STATE_ENDED);
+    } else {
+      setState(allRenderersReadyOrEnded ? ExoPlayer.STATE_READY : ExoPlayer.STATE_BUFFERING);
+      if (playWhenReady && state == ExoPlayer.STATE_READY) {
+        startRenderers();
+      }
+    }
+
+    handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+  }
+
+  private boolean rendererReadyOrEnded(TrackRenderer renderer) {
+    if (renderer.isEnded()) {
+      return true;
+    }
+    if (!renderer.isReady()) {
+      return false;
+    }
+    if (state == ExoPlayer.STATE_READY) {
+      return true;
+    }
+    long rendererDurationUs = renderer.getDurationUs();
+    long rendererBufferedPositionUs = renderer.getBufferedPositionUs();
+    long minBufferDurationUs = rebuffering ? minRebufferUs : minBufferUs;
+    return minBufferDurationUs <= 0
+        || rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US
+        || rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK_US
+        || rendererBufferedPositionUs >= positionUs + minBufferDurationUs
+        || (rendererDurationUs != TrackRenderer.UNKNOWN_TIME_US
+            && rendererDurationUs != TrackRenderer.MATCH_LONGEST_US
+            && rendererBufferedPositionUs >= rendererDurationUs);
+  }
+
+  private void setPlayWhenReadyInternal(boolean playWhenReady) throws ExoPlaybackException {
+    try {
+      rebuffering = false;
+      this.playWhenReady = playWhenReady;
+      if (!playWhenReady) {
+        stopRenderers();
+        updatePositionUs();
+      } else {
+        if (state == ExoPlayer.STATE_READY) {
+          startRenderers();
+          handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+        } else if (state == ExoPlayer.STATE_BUFFERING) {
+          handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+        }
+      }
+    } finally {
+      eventHandler.obtainMessage(MSG_SET_PLAY_WHEN_READY_ACK).sendToTarget();
+    }
+  }
+
+  private void startRenderers() throws ExoPlaybackException {
+    rebuffering = false;
+    mediaClock.start();
+    for (int i = 0; i < enabledRenderers.size(); i++) {
+      enabledRenderers.get(i).start();
+    }
+  }
+
+  private void stopRenderers() throws ExoPlaybackException {
+    mediaClock.stop();
+    for (int i = 0; i < enabledRenderers.size(); i++) {
+      ensureStopped(enabledRenderers.get(i));
+    }
+  }
+
+  private void updatePositionUs() {
+    if (timeSourceTrackRenderer != null && enabledRenderers.contains(timeSourceTrackRenderer)
+        && !timeSourceTrackRenderer.isEnded()) {
+      positionUs = timeSourceTrackRenderer.getCurrentPositionUs();
+      mediaClock.setPositionUs(positionUs);
+    } else {
+      positionUs = mediaClock.getPositionUs();
+    }
+    elapsedRealtimeUs = SystemClock.elapsedRealtime() * 1000;
+  }
+
+  private void doSomeWork() throws ExoPlaybackException {
+    TraceUtil.beginSection("doSomeWork");
+    long operationStartTimeMs = SystemClock.elapsedRealtime();
+    long bufferedPositionUs = durationUs != TrackRenderer.UNKNOWN_TIME_US ? durationUs
+        : Long.MAX_VALUE;
+    boolean isEnded = true;
+    boolean allRenderersReadyOrEnded = true;
+    updatePositionUs();
+    for (int i = 0; i < enabledRenderers.size(); i++) {
+      TrackRenderer renderer = enabledRenderers.get(i);
+      // TODO: Each renderer should return the maximum delay before which it wishes to be
+      // invoked again. The minimum of these values should then be used as the delay before the next
+      // invocation of this method.
+      renderer.doSomeWork(positionUs, elapsedRealtimeUs);
+      isEnded = isEnded && renderer.isEnded();
+      allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded(renderer);
+
+      if (bufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US) {
+        // We've already encountered a track for which the buffered position is unknown. Hence the
+        // media buffer position unknown regardless of the buffered position of this track.
+      } else {
+        long rendererDurationUs = renderer.getDurationUs();
+        long rendererBufferedPositionUs = renderer.getBufferedPositionUs();
+        if (rendererBufferedPositionUs == TrackRenderer.UNKNOWN_TIME_US) {
+          bufferedPositionUs = TrackRenderer.UNKNOWN_TIME_US;
+        } else if (rendererBufferedPositionUs == TrackRenderer.END_OF_TRACK_US
+            || (rendererDurationUs != TrackRenderer.UNKNOWN_TIME_US
+                && rendererDurationUs != TrackRenderer.MATCH_LONGEST_US
+                && rendererBufferedPositionUs >= rendererDurationUs)) {
+          // This track is fully buffered.
+        } else {
+          bufferedPositionUs = Math.min(bufferedPositionUs, rendererBufferedPositionUs);
+        }
+      }
+    }
+    this.bufferedPositionUs = bufferedPositionUs;
+
+    if (isEnded) {
+      setState(ExoPlayer.STATE_ENDED);
+      stopRenderers();
+    } else if (state == ExoPlayer.STATE_BUFFERING && allRenderersReadyOrEnded) {
+      setState(ExoPlayer.STATE_READY);
+      if (playWhenReady) {
+        startRenderers();
+      }
+    } else if (state == ExoPlayer.STATE_READY && !allRenderersReadyOrEnded) {
+      rebuffering = playWhenReady;
+      setState(ExoPlayer.STATE_BUFFERING);
+      stopRenderers();
+    }
+
+    handler.removeMessages(MSG_DO_SOME_WORK);
+    if ((playWhenReady && state == ExoPlayer.STATE_READY) || state == ExoPlayer.STATE_BUFFERING) {
+      scheduleNextOperation(MSG_DO_SOME_WORK, operationStartTimeMs, RENDERING_INTERVAL_MS);
+    } else if (!enabledRenderers.isEmpty()) {
+      scheduleNextOperation(MSG_DO_SOME_WORK, operationStartTimeMs, IDLE_INTERVAL_MS);
+    }
+
+    TraceUtil.endSection();
+  }
+
+  private void scheduleNextOperation(int operationType, long thisOperationStartTimeMs,
+      long intervalMs) {
+    long nextOperationStartTimeMs = thisOperationStartTimeMs + intervalMs;
+    long nextOperationDelayMs = nextOperationStartTimeMs - SystemClock.elapsedRealtime();
+    if (nextOperationDelayMs <= 0) {
+      handler.sendEmptyMessage(operationType);
+    } else {
+      handler.sendEmptyMessageDelayed(operationType, nextOperationDelayMs);
+    }
+  }
+
+  private void seekToInternal(long positionMs) throws ExoPlaybackException {
+    rebuffering = false;
+    positionUs = positionMs * 1000L;
+    mediaClock.stop();
+    mediaClock.setPositionUs(positionUs);
+    if (state == ExoPlayer.STATE_IDLE || state == ExoPlayer.STATE_PREPARING) {
+      return;
+    }
+    for (int i = 0; i < enabledRenderers.size(); i++) {
+      TrackRenderer renderer = enabledRenderers.get(i);
+      ensureStopped(renderer);
+      renderer.seekTo(positionUs);
+    }
+    setState(ExoPlayer.STATE_BUFFERING);
+    handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+  }
+
+  private void stopInternal() {
+    rebuffering = false;
+    resetInternal();
+  }
+
+  private void releaseInternal() {
+    resetInternal();
+    synchronized (this) {
+      released = true;
+      notifyAll();
+    }
+  }
+
+  private void resetInternal() {
+    handler.removeMessages(MSG_DO_SOME_WORK);
+    handler.removeMessages(MSG_INCREMENTAL_PREPARE);
+    mediaClock.stop();
+    if (renderers == null) {
+      return;
+    }
+    for (int i = 0; i < renderers.length; i++) {
+      TrackRenderer renderer = renderers[i];
+      stopAndDisable(renderer);
+      release(renderer);
+    }
+    renderers = null;
+    timeSourceTrackRenderer = null;
+    enabledRenderers.clear();
+    setState(ExoPlayer.STATE_IDLE);
+  }
+
+  private void stopAndDisable(TrackRenderer renderer) {
+    try {
+      ensureStopped(renderer);
+      if (renderer.getState() == TrackRenderer.STATE_ENABLED) {
+        renderer.disable();
+      }
+    } catch (ExoPlaybackException e) {
+      // There's nothing we can do.
+      Log.e(TAG, "Stop failed.", e);
+    } catch (RuntimeException e) {
+      // Ditto.
+      Log.e(TAG, "Stop failed.", e);
+    }
+  }
+
+  private void release(TrackRenderer renderer) {
+    try {
+      renderer.release();
+    } catch (ExoPlaybackException e) {
+      // There's nothing we can do.
+      Log.e(TAG, "Release failed.", e);
+    } catch (RuntimeException e) {
+      // Ditto.
+      Log.e(TAG, "Release failed.", e);
+    }
+  }
+
+  private <T> void sendMessageInternal(int what, Object obj)
+      throws ExoPlaybackException {
+    try {
+      @SuppressWarnings("unchecked")
+      Pair<ExoPlayerComponent, Object> targetAndMessage = (Pair<ExoPlayerComponent, Object>) obj;
+      targetAndMessage.first.handleMessage(what, targetAndMessage.second);
+    } finally {
+      synchronized (this) {
+        customMessagesProcessed++;
+        notifyAll();
+      }
+    }
+    if (state != ExoPlayer.STATE_IDLE && state != ExoPlayer.STATE_PREPARING) {
+      // The message may have caused something to change that now requires us to do work.
+      handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+    }
+  }
+
+  private void setRendererEnabledInternal(int index, boolean enabled)
+      throws ExoPlaybackException {
+    if (rendererEnabledFlags[index] == enabled) {
+      return;
+    }
+
+    rendererEnabledFlags[index] = enabled;
+    if (state == ExoPlayer.STATE_IDLE || state == ExoPlayer.STATE_PREPARING) {
+      return;
+    }
+
+    TrackRenderer renderer = renderers[index];
+    int rendererState = renderer.getState();
+    if (rendererState != TrackRenderer.STATE_PREPARED &&
+        rendererState != TrackRenderer.STATE_ENABLED &&
+        rendererState != TrackRenderer.STATE_STARTED) {
+      return;
+    }
+
+    if (enabled) {
+      boolean playing = playWhenReady && state == ExoPlayer.STATE_READY;
+      renderer.enable(positionUs, playing);
+      enabledRenderers.add(renderer);
+      if (playing) {
+        renderer.start();
+      }
+      handler.sendEmptyMessage(MSG_DO_SOME_WORK);
+    } else {
+      if (renderer == timeSourceTrackRenderer) {
+        // We've been using timeSourceTrackRenderer to advance the current position, but it's
+        // being disabled. Sync mediaClock so that it can take over timing responsibilities.
+        mediaClock.setPositionUs(renderer.getCurrentPositionUs());
+      }
+      ensureStopped(renderer);
+      enabledRenderers.remove(renderer);
+      renderer.disable();
+    }
+  }
+
+  private void ensureStopped(TrackRenderer renderer) throws ExoPlaybackException {
+    if (renderer.getState() == TrackRenderer.STATE_STARTED) {
+      renderer.stop();
+    }
+  }
+
+}

+ 51 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/ExoPlayerLibraryInfo.java

@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+/**
+ * Information about the ExoPlayer library.
+ */
+// TODO: This file should be automatically generated by the build system.
+public class ExoPlayerLibraryInfo {
+
+  private ExoPlayerLibraryInfo() {}
+
+  /**
+   * The version of the library, expressed as a string.
+   */
+  public static final String VERSION = "1.2.4";
+
+  /**
+   * The version of the library, expressed as an integer.
+   * <p>
+   * Three digits are used for each component of {@link #VERSION}. For example "1.2.3" has the
+   * corresponding integer version 001002003.
+   */
+  public static final int VERSION_INT = 001002004;
+
+  /**
+   * Whether the library was compiled with {@link com.google.android.exoplayer.util.Assertions}
+   * checks enabled.
+   */
+  public static final boolean ASSERTIONS_ENABLED = true;
+
+  /**
+   * Whether the library was compiled with {@link com.google.android.exoplayer.util.TraceUtil}
+   * trace enabled.
+   */
+  public static final boolean TRACE_ENABLED = true;
+
+}

+ 77 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/LoadControl.java

@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+import com.google.android.exoplayer.upstream.Allocator;
+
+/**
+ * Coordinates multiple loaders of time series data.
+ */
+public interface LoadControl {
+
+  /**
+   * Registers a loader.
+   *
+   * @param loader The loader being registered.
+   * @param bufferSizeContribution For controls whose {@link Allocator}s maintain a pool of memory
+   *     for the purpose of satisfying allocation requests, this is a hint indicating the loader's
+   *     desired contribution to the size of the pool, in bytes.
+   */
+  void register(Object loader, int bufferSizeContribution);
+
+  /**
+   * Unregisters a loader.
+   *
+   * @param loader The loader being unregistered.
+   */
+  void unregister(Object loader);
+
+  /**
+   * Gets the {@link Allocator} that loaders should use to obtain memory allocations into which
+   * data can be loaded.
+   *
+   * @return The {@link Allocator} to use.
+   */
+  Allocator getAllocator();
+
+  /**
+   * Hints to the control that it should consider trimming any unused memory being held in order
+   * to satisfy allocation requests.
+   * <p>
+   * This method is typically invoked by a recently unregistered loader, once it has released all
+   * of its allocations back to the {@link Allocator}.
+   */
+  void trimAllocator();
+
+  /**
+   * Invoked by a loader to update the control with its current state.
+   * <p>
+   * This method must be called by a registered loader whenever its state changes. This is true
+   * even if the registered loader does not itself wish to start its next load (since the state of
+   * the loader will still affect whether other registered loaders are allowed to proceed).
+   *
+   * @param loader The loader invoking the update.
+   * @param playbackPositionUs The loader's playback position.
+   * @param nextLoadPositionUs The loader's next load position. -1 if finished, failed, or if the
+   *     next load position is not yet known.
+   * @param loading Whether the loader is currently loading data.
+   * @param failed Whether the loader has failed.
+   * @return True if the loader is allowed to start its next load. False otherwise.
+   */
+  boolean update(Object loader, long playbackPositionUs, long nextLoadPositionUs,
+      boolean loading, boolean failed);
+
+}

+ 79 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/MediaClock.java

@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+import android.os.SystemClock;
+
+/**
+ * A simple clock for tracking the progression of media time. The clock can be started, stopped and
+ * its time can be set and retrieved. When started, this clock is based on
+ * {@link SystemClock#elapsedRealtime()}.
+ */
+/* package */ class MediaClock {
+
+  private boolean started;
+
+  /**
+   * The media time when the clock was last set or stopped.
+   */
+  private long positionUs;
+
+  /**
+   * The difference between {@link SystemClock#elapsedRealtime()} and {@link #positionUs}
+   * when the clock was last set or started.
+   */
+  private long deltaUs;
+
+  /**
+   * Starts the clock. Does nothing if the clock is already started.
+   */
+  public void start() {
+    if (!started) {
+      started = true;
+      deltaUs = elapsedRealtimeMinus(positionUs);
+    }
+  }
+
+  /**
+   * Stops the clock. Does nothing if the clock is already stopped.
+   */
+  public void stop() {
+    if (started) {
+      positionUs = elapsedRealtimeMinus(deltaUs);
+      started = false;
+    }
+  }
+
+  /**
+   * @param timeUs The position to set in microseconds.
+   */
+  public void setPositionUs(long timeUs) {
+    this.positionUs = timeUs;
+    deltaUs = elapsedRealtimeMinus(timeUs);
+  }
+
+  /**
+   * @return The current position in microseconds.
+   */
+  public long getPositionUs() {
+    return started ? elapsedRealtimeMinus(deltaUs) : positionUs;
+  }
+
+  private long elapsedRealtimeMinus(long toSubtractUs) {
+    return SystemClock.elapsedRealtime() * 1000 - toSubtractUs;
+  }
+
+}

+ 304 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java

@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+import com.google.android.exoplayer.audio.AudioTrack;
+import com.google.android.exoplayer.drm.DrmSessionManager;
+import com.google.android.exoplayer.util.MimeTypes;
+
+import android.annotation.TargetApi;
+import android.media.MediaCodec;
+import android.media.MediaFormat;
+import android.media.audiofx.Virtualizer;
+import android.os.Handler;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Decodes and renders audio using {@link MediaCodec} and {@link android.media.AudioTrack}.
+ */
+@TargetApi(16)
+public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer {
+
+  /**
+   * Interface definition for a callback to be notified of {@link MediaCodecAudioTrackRenderer}
+   * events.
+   */
+  public interface EventListener extends MediaCodecTrackRenderer.EventListener {
+
+    /**
+     * Invoked when an {@link AudioTrack} fails to initialize.
+     *
+     * @param e The corresponding exception.
+     */
+    void onAudioTrackInitializationError(AudioTrack.InitializationException e);
+
+    /**
+     * Invoked when an {@link AudioTrack} write fails.
+     *
+     * @param e The corresponding exception.
+     */
+    void onAudioTrackWriteError(AudioTrack.WriteException e);
+
+  }
+
+  /**
+   * The type of a message that can be passed to an instance of this class via
+   * {@link ExoPlayer#sendMessage} or {@link ExoPlayer#blockingSendMessage}. The message object
+   * should be a {@link Float} with 0 being silence and 1 being unity gain.
+   */
+  public static final int MSG_SET_VOLUME = 1;
+
+  private final EventListener eventListener;
+  private final AudioTrack audioTrack;
+
+  private int audioSessionId;
+  private long currentPositionUs;
+
+  /**
+   * @param source The upstream source from which the renderer obtains samples.
+   */
+  public MediaCodecAudioTrackRenderer(SampleSource source) {
+    this(source, null, true);
+  }
+
+  /**
+   * @param source The upstream source from which the renderer obtains samples.
+   * @param drmSessionManager For use with encrypted content. May be null if support for encrypted
+   *     content is not required.
+   * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
+   *     For example a media file may start with a short clear region so as to allow playback to
+   *     begin in parallel with key acquisision. This parameter specifies whether the renderer is
+   *     permitted to play clear regions of encrypted media files before {@code drmSessionManager}
+   *     has obtained the keys necessary to decrypt encrypted regions of the media.
+   */
+  public MediaCodecAudioTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
+      boolean playClearSamplesWithoutKeys) {
+    this(source, drmSessionManager, playClearSamplesWithoutKeys, null, null);
+  }
+
+  /**
+   * @param source The upstream source from which the renderer obtains samples.
+   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+   *     null if delivery of events is not required.
+   * @param eventListener A listener of events. May be null if delivery of events is not required.
+   */
+  public MediaCodecAudioTrackRenderer(SampleSource source, Handler eventHandler,
+      EventListener eventListener) {
+    this(source, null, true, eventHandler, eventListener);
+  }
+
+  /**
+   * @param source The upstream source from which the renderer obtains samples.
+   * @param drmSessionManager For use with encrypted content. May be null if support for encrypted
+   *     content is not required.
+   * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
+   *     For example a media file may start with a short clear region so as to allow playback to
+   *     begin in parallel with key acquisision. This parameter specifies whether the renderer is
+   *     permitted to play clear regions of encrypted media files before {@code drmSessionManager}
+   *     has obtained the keys necessary to decrypt encrypted regions of the media.
+   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+   *     null if delivery of events is not required.
+   * @param eventListener A listener of events. May be null if delivery of events is not required.
+   */
+  public MediaCodecAudioTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
+      boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener) {
+    super(source, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener);
+    this.eventListener = eventListener;
+    this.audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
+    this.audioTrack = new AudioTrack();
+  }
+
+  @Override
+  protected boolean isTimeSource() {
+    return true;
+  }
+
+  @Override
+  protected boolean handlesMimeType(String mimeType) {
+    return MimeTypes.isAudio(mimeType) && super.handlesMimeType(mimeType);
+  }
+
+  @Override
+  protected void onEnabled(long positionUs, boolean joining) {
+    super.onEnabled(positionUs, joining);
+    currentPositionUs = Long.MIN_VALUE;
+  }
+
+  @Override
+  protected void onOutputFormatChanged(MediaFormat format) {
+    audioTrack.reconfigure(format);
+  }
+
+  /**
+   * Invoked when the audio session id becomes known. Once the id is known it will not change
+   * (and hence this method will not be invoked again) unless the renderer is disabled and then
+   * subsequently re-enabled.
+   * <p>
+   * The default implementation is a no-op. One reason for overriding this method would be to
+   * instantiate and enable a {@link Virtualizer} in order to spatialize the audio channels. For
+   * this use case, any {@link Virtualizer} instances should be released in {@link #onDisabled()}
+   * (if not before).
+   *
+   * @param audioSessionId The audio session id.
+   */
+  protected void onAudioSessionId(int audioSessionId) {
+    // Do nothing.
+  }
+
+  @Override
+  protected void onStarted() {
+    super.onStarted();
+    audioTrack.play();
+  }
+
+  @Override
+  protected void onStopped() {
+    audioTrack.pause();
+    super.onStopped();
+  }
+
+  @Override
+  protected boolean isEnded() {
+    // We've exhausted the output stream, and the AudioTrack has either played all of the data
+    // submitted, or has been fed insufficient data to begin playback.
+    return super.isEnded() && (!audioTrack.hasPendingData()
+        || !audioTrack.hasEnoughDataToBeginPlayback());
+  }
+
+  @Override
+  protected boolean isReady() {
+    return audioTrack.hasPendingData()
+        || (super.isReady() && getSourceState() == SOURCE_STATE_READY_READ_MAY_FAIL);
+  }
+
+  @Override
+  protected long getCurrentPositionUs() {
+    long audioTrackCurrentPositionUs = audioTrack.getCurrentPositionUs(isEnded());
+    if (audioTrackCurrentPositionUs == AudioTrack.CURRENT_POSITION_NOT_SET) {
+      // Use the super class position before audio playback starts.
+      currentPositionUs = Math.max(currentPositionUs, super.getCurrentPositionUs());
+    } else {
+      // Make sure we don't ever report time moving backwards.
+      currentPositionUs = Math.max(currentPositionUs, audioTrackCurrentPositionUs);
+    }
+    return currentPositionUs;
+  }
+
+  @Override
+  protected void onDisabled() {
+    audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
+    try {
+      audioTrack.reset();
+    } finally {
+      super.onDisabled();
+    }
+  }
+
+  @Override
+  protected void seekTo(long positionUs) throws ExoPlaybackException {
+    super.seekTo(positionUs);
+    // TODO: Try and re-use the same AudioTrack instance once [Internal: b/7941810] is fixed.
+    audioTrack.reset();
+    currentPositionUs = Long.MIN_VALUE;
+  }
+
+  @Override
+  protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec,
+      ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip)
+      throws ExoPlaybackException {
+    if (shouldSkip) {
+      codec.releaseOutputBuffer(bufferIndex, false);
+      codecCounters.skippedOutputBufferCount++;
+      audioTrack.handleDiscontinuity();
+      return true;
+    }
+
+    // Initialize and start the audio track now.
+    if (!audioTrack.isInitialized()) {
+      try {
+        if (audioSessionId != AudioTrack.SESSION_ID_NOT_SET) {
+          audioTrack.initialize(audioSessionId);
+        } else {
+          audioSessionId = audioTrack.initialize();
+          onAudioSessionId(audioSessionId);
+        }
+      } catch (AudioTrack.InitializationException e) {
+        notifyAudioTrackInitializationError(e);
+        throw new ExoPlaybackException(e);
+      }
+
+      if (getState() == TrackRenderer.STATE_STARTED) {
+        audioTrack.play();
+      }
+    }
+
+    int handleBufferResult;
+    try {
+      handleBufferResult = audioTrack.handleBuffer(
+          buffer, bufferInfo.offset, bufferInfo.size, bufferInfo.presentationTimeUs);
+    } catch (AudioTrack.WriteException e) {
+      notifyAudioTrackWriteError(e);
+      throw new ExoPlaybackException(e);
+    }
+
+    // If we are out of sync, allow currentPositionUs to jump backwards.
+    if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) {
+      currentPositionUs = Long.MIN_VALUE;
+    }
+
+    // Release the buffer if it was consumed.
+    if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) {
+      codec.releaseOutputBuffer(bufferIndex, false);
+      codecCounters.renderedOutputBufferCount++;
+      return true;
+    }
+
+    return false;
+  }
+
+  @Override
+  public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
+    if (messageType == MSG_SET_VOLUME) {
+      audioTrack.setVolume((Float) message);
+    } else {
+      super.handleMessage(messageType, message);
+    }
+  }
+
+  private void notifyAudioTrackInitializationError(final AudioTrack.InitializationException e) {
+    if (eventHandler != null && eventListener != null) {
+      eventHandler.post(new Runnable()  {
+        @Override
+        public void run() {
+          eventListener.onAudioTrackInitializationError(e);
+        }
+      });
+    }
+  }
+
+  private void notifyAudioTrackWriteError(final AudioTrack.WriteException e) {
+    if (eventHandler != null && eventListener != null) {
+      eventHandler.post(new Runnable()  {
+        @Override
+        public void run() {
+          eventListener.onAudioTrackWriteError(e);
+        }
+      });
+    }
+  }
+
+}

+ 810 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/MediaCodecTrackRenderer.java

@@ -0,0 +1,810 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException;
+import com.google.android.exoplayer.drm.DrmSessionManager;
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.Util;
+
+import android.annotation.TargetApi;
+import android.media.MediaCodec;
+import android.media.MediaCodec.CodecException;
+import android.media.MediaCodec.CryptoException;
+import android.media.MediaCrypto;
+import android.media.MediaExtractor;
+import android.os.Handler;
+import android.os.SystemClock;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * An abstract {@link TrackRenderer} that uses {@link MediaCodec} to decode samples for rendering.
+ */
+@TargetApi(16)
+public abstract class MediaCodecTrackRenderer extends TrackRenderer {
+
+  /**
+   * Interface definition for a callback to be notified of {@link MediaCodecTrackRenderer} events.
+   */
+  public interface EventListener {
+
+    /**
+     * Invoked when a decoder fails to initialize.
+     *
+     * @param e The corresponding exception.
+     */
+    void onDecoderInitializationError(DecoderInitializationException e);
+
+    /**
+     * Invoked when a decoder operation raises a {@link CryptoException}.
+     *
+     * @param e The corresponding exception.
+     */
+    void onCryptoError(CryptoException e);
+
+  }
+
+  /**
+   * Thrown when a failure occurs instantiating a decoder.
+   */
+  public static class DecoderInitializationException extends Exception {
+
+    private static final int CUSTOM_ERROR_CODE_BASE = -50000;
+    private static final int NO_SUITABLE_DECODER_ERROR = CUSTOM_ERROR_CODE_BASE + 1;
+    private static final int DECODER_QUERY_ERROR = CUSTOM_ERROR_CODE_BASE + 2;
+
+    /**
+     * The name of the decoder that failed to initialize. Null if no suitable decoder was found.
+     */
+    public final String decoderName;
+
+    /**
+     * An optional developer-readable diagnostic information string. May be null.
+     */
+    public final String diagnosticInfo;
+
+    public DecoderInitializationException(MediaFormat mediaFormat, Throwable cause, int errorCode) {
+      super("Decoder init failed: [" + errorCode + "], " + mediaFormat, cause);
+      this.decoderName = null;
+      this.diagnosticInfo = buildCustomDiagnosticInfo(errorCode);
+    }
+
+    public DecoderInitializationException(MediaFormat mediaFormat, Throwable cause,
+        String decoderName) {
+      super("Decoder init failed: " + decoderName + ", " + mediaFormat, cause);
+      this.decoderName = decoderName;
+      this.diagnosticInfo = Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null;
+    }
+
+    @TargetApi(21)
+    private static String getDiagnosticInfoV21(Throwable cause) {
+      if (cause instanceof CodecException) {
+        return ((CodecException) cause).getDiagnosticInfo();
+      }
+      return null;
+    }
+
+    private static String buildCustomDiagnosticInfo(int errorCode) {
+      String sign = errorCode < 0 ? "neg_" : "";
+      return "com.google.android.exoplayer.MediaCodecTrackRenderer_" + sign + Math.abs(errorCode);
+    }
+
+  }
+
+  /**
+   * Value returned by {@link #getSourceState()} when the source is not ready.
+   */
+  protected static final int SOURCE_STATE_NOT_READY = 0;
+  /**
+   * Value returned by {@link #getSourceState()} when the source is ready and we're able to read
+   * from it.
+   */
+  protected static final int SOURCE_STATE_READY = 1;
+  /**
+   * Value returned by {@link #getSourceState()} when the source is ready but we might not be able
+   * to read from it. We transition to this state when an attempt to read a sample fails despite the
+   * source reporting that samples are available. This can occur when the next sample to be provided
+   * by the source is for another renderer.
+   */
+  protected static final int SOURCE_STATE_READY_READ_MAY_FAIL = 2;
+
+  /**
+   * If the {@link MediaCodec} is hotswapped (i.e. replaced during playback), this is the period of
+   * time during which {@link #isReady()} will report true regardless of whether the new codec has
+   * output frames that are ready to be rendered.
+   * <p>
+   * This allows codec hotswapping to be performed seamlessly, without interrupting the playback of
+   * other renderers, provided the new codec is able to decode some frames within this time period.
+   */
+  private static final long MAX_CODEC_HOTSWAP_TIME_MS = 1000;
+
+  /**
+   * There is no pending adaptive reconfiguration work.
+   */
+  private static final int RECONFIGURATION_STATE_NONE = 0;
+  /**
+   * Codec configuration data needs to be written into the next buffer.
+   */
+  private static final int RECONFIGURATION_STATE_WRITE_PENDING = 1;
+  /**
+   * Codec configuration data has been written into the next buffer, but that buffer still needs to
+   * be returned to the codec.
+   */
+  private static final int RECONFIGURATION_STATE_QUEUE_PENDING = 2;
+
+  public final CodecCounters codecCounters;
+
+  private final DrmSessionManager drmSessionManager;
+  private final boolean playClearSamplesWithoutKeys;
+  private final SampleSource source;
+  private final SampleHolder sampleHolder;
+  private final MediaFormatHolder formatHolder;
+  private final List<Long> decodeOnlyPresentationTimestamps;
+  private final MediaCodec.BufferInfo outputBufferInfo;
+  private final EventListener eventListener;
+  protected final Handler eventHandler;
+
+  private MediaFormat format;
+  private Map<UUID, byte[]> drmInitData;
+  private MediaCodec codec;
+  private boolean codecIsAdaptive;
+  private ByteBuffer[] inputBuffers;
+  private ByteBuffer[] outputBuffers;
+  private long codecHotswapTimeMs;
+  private int inputIndex;
+  private int outputIndex;
+  private boolean openedDrmSession;
+  private boolean codecReconfigured;
+  private int codecReconfigurationState;
+
+  private int trackIndex;
+  private int sourceState;
+  private boolean inputStreamEnded;
+  private boolean outputStreamEnded;
+  private boolean waitingForKeys;
+  private boolean waitingForFirstSyncFrame;
+  private long currentPositionUs;
+
+  /**
+   * @param source The upstream source from which the renderer obtains samples.
+   * @param drmSessionManager For use with encrypted media. May be null if support for encrypted
+   *     media is not required.
+   * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
+   *     For example a media file may start with a short clear region so as to allow playback to
+   *     begin in parallel with key acquisision. This parameter specifies whether the renderer is
+   *     permitted to play clear regions of encrypted media files before {@code drmSessionManager}
+   *     has obtained the keys necessary to decrypt encrypted regions of the media.
+   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+   *     null if delivery of events is not required.
+   * @param eventListener A listener of events. May be null if delivery of events is not required.
+   */
+  public MediaCodecTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
+      boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener) {
+    Assertions.checkState(Util.SDK_INT >= 16);
+    this.source = source;
+    this.drmSessionManager = drmSessionManager;
+    this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
+    this.eventHandler = eventHandler;
+    this.eventListener = eventListener;
+    codecCounters = new CodecCounters();
+    sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED);
+    formatHolder = new MediaFormatHolder();
+    decodeOnlyPresentationTimestamps = new ArrayList<Long>();
+    outputBufferInfo = new MediaCodec.BufferInfo();
+  }
+
+  @Override
+  protected int doPrepare() throws ExoPlaybackException {
+    try {
+      boolean sourcePrepared = source.prepare();
+      if (!sourcePrepared) {
+        return TrackRenderer.STATE_UNPREPARED;
+      }
+    } catch (IOException e) {
+      throw new ExoPlaybackException(e);
+    }
+
+    for (int i = 0; i < source.getTrackCount(); i++) {
+      // TODO: Right now this is getting the mime types of the container format
+      // (e.g. audio/mp4 and video/mp4 for fragmented mp4). It needs to be getting the mime types
+      // of the actual samples (e.g. audio/mp4a-latm and video/avc).
+      if (handlesMimeType(source.getTrackInfo(i).mimeType)) {
+        trackIndex = i;
+        return TrackRenderer.STATE_PREPARED;
+      }
+    }
+
+    return TrackRenderer.STATE_IGNORE;
+  }
+
+  /**
+   * Determines whether a mime type is handled by the renderer.
+   *
+   * @param mimeType The mime type to test.
+   * @return True if the renderer can handle the mime type. False otherwise.
+   */
+  protected boolean handlesMimeType(String mimeType) {
+    return true;
+    // TODO: Uncomment once the TODO above is fixed.
+    // DecoderInfoUtil.getDecoder(mimeType) != null;
+  }
+
+  @Override
+  protected void onEnabled(long positionUs, boolean joining) {
+    source.enable(trackIndex, positionUs);
+    sourceState = SOURCE_STATE_NOT_READY;
+    inputStreamEnded = false;
+    outputStreamEnded = false;
+    waitingForKeys = false;
+    currentPositionUs = positionUs;
+  }
+
+  /**
+   * Configures a newly created {@link MediaCodec}. Sub-classes should
+   * override this method if they wish to configure the codec with a
+   * non-null surface.
+   **/
+  protected void configureCodec(MediaCodec codec, android.media.MediaFormat x, MediaCrypto crypto) {
+    codec.configure(x, null, crypto, 0);
+  }
+
+  @SuppressWarnings("deprecation")
+  protected final void maybeInitCodec() throws ExoPlaybackException {
+    if (!shouldInitCodec()) {
+      return;
+    }
+
+    String mimeType = format.mimeType;
+    MediaCrypto mediaCrypto = null;
+    boolean requiresSecureDecoder = false;
+    if (drmInitData != null) {
+      if (drmSessionManager == null) {
+        throw new ExoPlaybackException("Media requires a DrmSessionManager");
+      }
+      if (!openedDrmSession) {
+        drmSessionManager.open(drmInitData, mimeType);
+        openedDrmSession = true;
+      }
+      int drmSessionState = drmSessionManager.getState();
+      if (drmSessionState == DrmSessionManager.STATE_ERROR) {
+        throw new ExoPlaybackException(drmSessionManager.getError());
+      } else if (drmSessionState == DrmSessionManager.STATE_OPENED
+          || drmSessionState == DrmSessionManager.STATE_OPENED_WITH_KEYS) {
+        mediaCrypto = drmSessionManager.getMediaCrypto();
+        requiresSecureDecoder = drmSessionManager.requiresSecureDecoderComponent(mimeType);
+      } else {
+        // The drm session isn't open yet.
+        return;
+      }
+    }
+
+    DecoderInfo decoderInfo = null;
+    try {
+      decoderInfo = MediaCodecUtil.getDecoderInfo(mimeType, requiresSecureDecoder);
+    } catch (DecoderQueryException e) {
+      notifyAndThrowDecoderInitError(new DecoderInitializationException(format, e,
+          DecoderInitializationException.DECODER_QUERY_ERROR));
+    }
+
+    if (decoderInfo == null) {
+      notifyAndThrowDecoderInitError(new DecoderInitializationException(format, null,
+          DecoderInitializationException.NO_SUITABLE_DECODER_ERROR));
+    }
+
+    String decoderName = decoderInfo.name;
+    codecIsAdaptive = decoderInfo.adaptive;
+    try {
+      codec = MediaCodec.createByCodecName(decoderName);
+      configureCodec(codec, format.getFrameworkMediaFormatV16(), mediaCrypto);
+      codec.start();
+      inputBuffers = codec.getInputBuffers();
+      outputBuffers = codec.getOutputBuffers();
+    } catch (Exception e) {
+      notifyAndThrowDecoderInitError(new DecoderInitializationException(format, e, decoderName));
+    }
+    codecHotswapTimeMs = getState() == TrackRenderer.STATE_STARTED ?
+        SystemClock.elapsedRealtime() : -1;
+    inputIndex = -1;
+    outputIndex = -1;
+    waitingForFirstSyncFrame = true;
+    codecCounters.codecInitCount++;
+  }
+
+  private void notifyAndThrowDecoderInitError(DecoderInitializationException e)
+      throws ExoPlaybackException {
+    notifyDecoderInitializationError(e);
+    throw new ExoPlaybackException(e);
+  }
+
+  protected boolean shouldInitCodec() {
+    return codec == null && format != null;
+  }
+
+  protected final boolean codecInitialized() {
+    return codec != null;
+  }
+
+  protected final boolean haveFormat() {
+    return format != null;
+  }
+
+  @Override
+  protected void onDisabled() {
+    format = null;
+    drmInitData = null;
+    try {
+      releaseCodec();
+    } finally {
+      try {
+        if (openedDrmSession) {
+          drmSessionManager.close();
+          openedDrmSession = false;
+        }
+      } finally {
+        source.disable(trackIndex);
+      }
+    }
+  }
+
+  protected void releaseCodec() {
+    if (codec != null) {
+      codecHotswapTimeMs = -1;
+      inputIndex = -1;
+      outputIndex = -1;
+      decodeOnlyPresentationTimestamps.clear();
+      inputBuffers = null;
+      outputBuffers = null;
+      codecReconfigured = false;
+      codecIsAdaptive = false;
+      codecReconfigurationState = RECONFIGURATION_STATE_NONE;
+      codecCounters.codecReleaseCount++;
+      try {
+        codec.stop();
+      } finally {
+        try {
+          codec.release();
+        } finally {
+          codec = null;
+        }
+      }
+    }
+  }
+
+  @Override
+  protected void onReleased() {
+    source.release();
+  }
+
+  @Override
+  protected long getCurrentPositionUs() {
+    return currentPositionUs;
+  }
+
+  @Override
+  protected long getDurationUs() {
+    return source.getTrackInfo(trackIndex).durationUs;
+  }
+
+  @Override
+  protected long getBufferedPositionUs() {
+    long sourceBufferedPosition = source.getBufferedPositionUs();
+    return sourceBufferedPosition == UNKNOWN_TIME_US || sourceBufferedPosition == END_OF_TRACK_US
+        ? sourceBufferedPosition : Math.max(sourceBufferedPosition, getCurrentPositionUs());
+  }
+
+  @Override
+  protected void seekTo(long positionUs) throws ExoPlaybackException {
+    currentPositionUs = positionUs;
+    source.seekToUs(positionUs);
+    sourceState = SOURCE_STATE_NOT_READY;
+    inputStreamEnded = false;
+    outputStreamEnded = false;
+    waitingForKeys = false;
+  }
+
+  @Override
+  protected void onStarted() {
+    // Do nothing. Overridden to remove throws clause.
+  }
+
+  @Override
+  protected void onStopped() {
+    // Do nothing. Overridden to remove throws clause.
+  }
+
+  @Override
+  protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
+    try {
+      sourceState = source.continueBuffering(positionUs)
+          ? (sourceState == SOURCE_STATE_NOT_READY ? SOURCE_STATE_READY : sourceState)
+          : SOURCE_STATE_NOT_READY;
+      checkForDiscontinuity();
+      if (format == null) {
+        readFormat();
+      }
+      if (codec == null && shouldInitCodec()) {
+        maybeInitCodec();
+      }
+      if (codec != null) {
+        while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {}
+        if (feedInputBuffer(true)) {
+          while (feedInputBuffer(false)) {}
+        }
+      }
+      codecCounters.ensureUpdated();
+    } catch (IOException e) {
+      throw new ExoPlaybackException(e);
+    }
+  }
+
+  private void readFormat() throws IOException, ExoPlaybackException {
+    int result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
+    if (result == SampleSource.FORMAT_READ) {
+      onInputFormatChanged(formatHolder);
+    }
+  }
+
+  private void checkForDiscontinuity() throws IOException, ExoPlaybackException {
+    if (codec == null) {
+      return;
+    }
+    int result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, true);
+    if (result == SampleSource.DISCONTINUITY_READ) {
+      flushCodec();
+    }
+  }
+
+  private void flushCodec() throws ExoPlaybackException {
+    codecHotswapTimeMs = -1;
+    inputIndex = -1;
+    outputIndex = -1;
+    waitingForFirstSyncFrame = true;
+    decodeOnlyPresentationTimestamps.clear();
+    // Workaround for framework bugs.
+    // See [Internal: b/8347958], [Internal: b/8578467], [Internal: b/8543366].
+    if (Util.SDK_INT >= 18) {
+      codec.flush();
+    } else {
+      releaseCodec();
+      maybeInitCodec();
+    }
+    if (codecReconfigured && format != null) {
+      // Any reconfiguration data that we send shortly before the flush may be discarded. We
+      // avoid this issue by sending reconfiguration data following every flush.
+      codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
+    }
+  }
+
+  /**
+   * @param firstFeed True if this is the first call to this method from the current invocation of
+   *     {@link #doSomeWork(long, long)}. False otherwise.
+   * @return True if it may be possible to feed more input data. False otherwise.
+   * @throws IOException If an error occurs reading data from the upstream source.
+   * @throws ExoPlaybackException If an error occurs feeding the input buffer.
+   */
+  private boolean feedInputBuffer(boolean firstFeed) throws IOException, ExoPlaybackException {
+    if (inputStreamEnded) {
+      return false;
+    }
+    if (inputIndex < 0) {
+      inputIndex = codec.dequeueInputBuffer(0);
+      if (inputIndex < 0) {
+        return false;
+      }
+      sampleHolder.data = inputBuffers[inputIndex];
+      sampleHolder.data.clear();
+    }
+
+    int result;
+    if (waitingForKeys) {
+      // We've already read an encrypted sample into sampleHolder, and are waiting for keys.
+      result = SampleSource.SAMPLE_READ;
+    } else {
+      // For adaptive reconfiguration OMX decoders expect all reconfiguration data to be supplied
+      // at the start of the buffer that also contains the first frame in the new format.
+      if (codecReconfigurationState == RECONFIGURATION_STATE_WRITE_PENDING) {
+        for (int i = 0; i < format.initializationData.size(); i++) {
+          byte[] data = format.initializationData.get(i);
+          sampleHolder.data.put(data);
+        }
+        codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING;
+      }
+      result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
+      if (firstFeed && sourceState == SOURCE_STATE_READY && result == SampleSource.NOTHING_READ) {
+        sourceState = SOURCE_STATE_READY_READ_MAY_FAIL;
+      }
+    }
+
+    if (result == SampleSource.NOTHING_READ) {
+      return false;
+    }
+    if (result == SampleSource.DISCONTINUITY_READ) {
+      flushCodec();
+      return true;
+    }
+    if (result == SampleSource.FORMAT_READ) {
+      if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
+        // We received two formats in a row. Clear the current buffer of any reconfiguration data
+        // associated with the first format.
+        sampleHolder.data.clear();
+        codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
+      }
+      onInputFormatChanged(formatHolder);
+      return true;
+    }
+    if (result == SampleSource.END_OF_STREAM) {
+      if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
+        // We received a new format immediately before the end of the stream. We need to clear
+        // the corresponding reconfiguration data from the current buffer, but re-write it into
+        // a subsequent buffer if there are any (e.g. if the user seeks backwards).
+        sampleHolder.data.clear();
+        codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
+      }
+      inputStreamEnded = true;
+      try {
+        codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+        inputIndex = -1;
+      } catch (CryptoException e) {
+        notifyCryptoError(e);
+        throw new ExoPlaybackException(e);
+      }
+      return false;
+    }
+    if (waitingForFirstSyncFrame) {
+      // TODO: Find out if it's possible to supply samples prior to the first sync
+      // frame for HE-AAC.
+      if ((sampleHolder.flags & C.SAMPLE_FLAG_SYNC) == 0) {
+        sampleHolder.data.clear();
+        if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
+          // The buffer we just cleared contained reconfiguration data. We need to re-write this
+          // data into a subsequent buffer (if there is one).
+          codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
+        }
+        return true;
+      }
+      waitingForFirstSyncFrame = false;
+    }
+    boolean sampleEncrypted = (sampleHolder.flags & MediaExtractor.SAMPLE_FLAG_ENCRYPTED) != 0;
+    waitingForKeys = shouldWaitForKeys(sampleEncrypted);
+    if (waitingForKeys) {
+      return false;
+    }
+    try {
+      int bufferSize = sampleHolder.data.position();
+      int adaptiveReconfigurationBytes = bufferSize - sampleHolder.size;
+      long presentationTimeUs = sampleHolder.timeUs;
+      if (sampleHolder.decodeOnly) {
+        decodeOnlyPresentationTimestamps.add(presentationTimeUs);
+      }
+      if (sampleEncrypted) {
+        MediaCodec.CryptoInfo cryptoInfo = getFrameworkCryptoInfo(sampleHolder,
+            adaptiveReconfigurationBytes);
+        codec.queueSecureInputBuffer(inputIndex, 0, cryptoInfo, presentationTimeUs, 0);
+      } else {
+        codec.queueInputBuffer(inputIndex, 0 , bufferSize, presentationTimeUs, 0);
+      }
+      inputIndex = -1;
+      codecReconfigurationState = RECONFIGURATION_STATE_NONE;
+    } catch (CryptoException e) {
+      notifyCryptoError(e);
+      throw new ExoPlaybackException(e);
+    }
+    return true;
+  }
+
+  private static MediaCodec.CryptoInfo getFrameworkCryptoInfo(SampleHolder sampleHolder,
+      int adaptiveReconfigurationBytes) {
+    MediaCodec.CryptoInfo cryptoInfo = sampleHolder.cryptoInfo.getFrameworkCryptoInfoV16();
+    if (adaptiveReconfigurationBytes == 0) {
+      return cryptoInfo;
+    }
+    // There must be at least one sub-sample, although numBytesOfClearData is permitted to be
+    // null if it contains no clear data. Instantiate it if needed, and add the reconfiguration
+    // bytes to the clear byte count of the first sub-sample.
+    if (cryptoInfo.numBytesOfClearData == null) {
+      cryptoInfo.numBytesOfClearData = new int[1];
+    }
+    cryptoInfo.numBytesOfClearData[0] += adaptiveReconfigurationBytes;
+    return cryptoInfo;
+  }
+
+  private boolean shouldWaitForKeys(boolean sampleEncrypted) throws ExoPlaybackException {
+    if (!openedDrmSession) {
+      return false;
+    }
+    int drmManagerState = drmSessionManager.getState();
+    if (drmManagerState == DrmSessionManager.STATE_ERROR) {
+      throw new ExoPlaybackException(drmSessionManager.getError());
+    }
+    if (drmManagerState != DrmSessionManager.STATE_OPENED_WITH_KEYS &&
+        (sampleEncrypted || !playClearSamplesWithoutKeys)) {
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Invoked when a new format is read from the upstream {@link SampleSource}.
+   *
+   * @param formatHolder Holds the new format.
+   * @throws ExoPlaybackException If an error occurs reinitializing the {@link MediaCodec}.
+   */
+  protected void onInputFormatChanged(MediaFormatHolder formatHolder) throws ExoPlaybackException {
+    MediaFormat oldFormat = format;
+    format = formatHolder.format;
+    drmInitData = formatHolder.drmInitData;
+    if (codec != null && canReconfigureCodec(codec, codecIsAdaptive, oldFormat, format)) {
+      codecReconfigured = true;
+      codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
+    } else {
+      releaseCodec();
+      maybeInitCodec();
+    }
+  }
+
+  /**
+   * Invoked when the output format of the {@link MediaCodec} changes.
+   * <p>
+   * The default implementation is a no-op.
+   *
+   * @param format The new output format.
+   */
+  protected void onOutputFormatChanged(android.media.MediaFormat format) {
+    // Do nothing.
+  }
+
+  /**
+   * Determines whether the existing {@link MediaCodec} should be reconfigured for a new format by
+   * sending codec specific initialization data at the start of the next input buffer. If true is
+   * returned then the {@link MediaCodec} instance will be reconfigured in this way. If false is
+   * returned then the instance will be released, and a new instance will be created for the new
+   * format.
+   * <p>
+   * The default implementation returns false.
+   *
+   * @param codec The existing {@link MediaCodec} instance.
+   * @param codecIsAdaptive Whether the codec is adaptive.
+   * @param oldFormat The format for which the existing instance is configured.
+   * @param newFormat The new format.
+   * @return True if the existing instance can be reconfigured. False otherwise.
+   */
+  protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive,
+      MediaFormat oldFormat, MediaFormat newFormat) {
+    return false;
+  }
+
+  @Override
+  protected boolean isEnded() {
+    return outputStreamEnded;
+  }
+
+  @Override
+  protected boolean isReady() {
+    return format != null && !waitingForKeys
+        && (sourceState != SOURCE_STATE_NOT_READY || outputIndex >= 0 || isWithinHotswapPeriod());
+  }
+
+  /**
+   * Gets the source state.
+   *
+   * @return One of {@link #SOURCE_STATE_NOT_READY}, {@link #SOURCE_STATE_READY} and
+   *     {@link #SOURCE_STATE_READY_READ_MAY_FAIL}.
+   */
+  protected final int getSourceState() {
+    return sourceState;
+  }
+
+  private boolean isWithinHotswapPeriod() {
+    return SystemClock.elapsedRealtime() < codecHotswapTimeMs + MAX_CODEC_HOTSWAP_TIME_MS;
+  }
+
+  /**
+   * @return True if it may be possible to drain more output data. False otherwise.
+   * @throws ExoPlaybackException If an error occurs draining the output buffer.
+   */
+  @SuppressWarnings("deprecation")
+  private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs)
+      throws ExoPlaybackException {
+    if (outputStreamEnded) {
+      return false;
+    }
+
+    if (outputIndex < 0) {
+      outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, 0);
+    }
+
+    if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
+      onOutputFormatChanged(codec.getOutputFormat());
+      codecCounters.outputFormatChangedCount++;
+      return true;
+    } else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
+      outputBuffers = codec.getOutputBuffers();
+      codecCounters.outputBuffersChangedCount++;
+      return true;
+    } else if (outputIndex < 0) {
+      return false;
+    }
+
+    if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+      outputStreamEnded = true;
+      return false;
+    }
+
+    int decodeOnlyIndex = getDecodeOnlyIndex(outputBufferInfo.presentationTimeUs);
+    if (processOutputBuffer(positionUs, elapsedRealtimeUs, codec, outputBuffers[outputIndex],
+        outputBufferInfo, outputIndex, decodeOnlyIndex != -1)) {
+      if (decodeOnlyIndex != -1) {
+        decodeOnlyPresentationTimestamps.remove(decodeOnlyIndex);
+      } else {
+        currentPositionUs = outputBufferInfo.presentationTimeUs;
+      }
+      outputIndex = -1;
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Processes the provided output buffer.
+   *
+   * @return True if the output buffer was processed (e.g. rendered or discarded) and hence is no
+   *     longer required. False otherwise.
+   * @throws ExoPlaybackException If an error occurs processing the output buffer.
+   */
+  protected abstract boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs,
+      MediaCodec codec, ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex,
+      boolean shouldSkip) throws ExoPlaybackException;
+
+  private void notifyDecoderInitializationError(final DecoderInitializationException e) {
+    if (eventHandler != null && eventListener != null) {
+      eventHandler.post(new Runnable()  {
+        @Override
+        public void run() {
+          eventListener.onDecoderInitializationError(e);
+        }
+      });
+    }
+  }
+
+  private void notifyCryptoError(final CryptoException e) {
+    if (eventHandler != null && eventListener != null) {
+      eventHandler.post(new Runnable()  {
+        @Override
+        public void run() {
+          eventListener.onCryptoError(e);
+        }
+      });
+    }
+  }
+
+  private int getDecodeOnlyIndex(long presentationTimeUs) {
+    final int size = decodeOnlyPresentationTimestamps.size();
+    for (int i = 0; i < size; i++) {
+      if (decodeOnlyPresentationTimestamps.get(i).longValue() == presentationTimeUs) {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+}

+ 381 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/MediaCodecUtil.java

@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+import com.google.android.exoplayer.util.MimeTypes;
+import com.google.android.exoplayer.util.Util;
+
+import android.annotation.TargetApi;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecInfo.CodecProfileLevel;
+import android.media.MediaCodecList;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import java.util.HashMap;
+
+/**
+ * A utility class for querying the available codecs.
+ */
+@TargetApi(16)
+public class MediaCodecUtil {
+
+  /**
+   * Thrown when an error occurs querying the device for its underlying media capabilities.
+   * <p>
+   * Such failures are not expected in normal operation and are normally temporary (e.g. if the
+   * mediaserver process has crashed and is yet to restart).
+   */
+  public static class DecoderQueryException extends Exception {
+
+    private DecoderQueryException(Throwable cause) {
+      super("Failed to query underlying media codecs", cause);
+    }
+
+  }
+
+  private static final String TAG = "MediaCodecUtil";
+
+  private static final HashMap<CodecKey, Pair<String, CodecCapabilities>> codecs =
+      new HashMap<CodecKey, Pair<String, CodecCapabilities>>();
+
+  /**
+   * Get information about the decoder that will be used for a given mime type.
+   *
+   * @param mimeType The mime type.
+   * @param secure Whether the decoder is required to support secure decryption. Always pass false
+   *     unless secure decryption really is required.
+   * @return Information about the decoder that will be used, or null if no decoder exists.
+   */
+  public static DecoderInfo getDecoderInfo(String mimeType, boolean secure)
+      throws DecoderQueryException {
+    Pair<String, CodecCapabilities> info = getMediaCodecInfo(mimeType, secure);
+    if (info == null) {
+      return null;
+    }
+    return new DecoderInfo(info.first, isAdaptive(info.second));
+  }
+
+  /**
+   * Optional call to warm the codec cache for a given mime type.
+   * <p>
+   * Calling this method may speed up subsequent calls to {@link #getDecoderInfo(String, boolean)}.
+   *
+   * @param mimeType The mime type.
+   * @param secure Whether the decoder is required to support secure decryption. Always pass false
+   *     unless secure decryption really is required.
+   */
+  public static synchronized void warmCodec(String mimeType, boolean secure) {
+    try {
+      getMediaCodecInfo(mimeType, secure);
+    } catch (DecoderQueryException e) {
+      // Codec warming is best effort, so we can swallow the exception.
+      Log.e(TAG, "Codec warming failed", e);
+    }
+  }
+
+  /**
+   * Returns the name of the best decoder and its capabilities for the given mimeType.
+   */
+  private static synchronized Pair<String, CodecCapabilities> getMediaCodecInfo(
+      String mimeType, boolean secure) throws DecoderQueryException {
+    CodecKey key = new CodecKey(mimeType, secure);
+    if (codecs.containsKey(key)) {
+      return codecs.get(key);
+    }
+    MediaCodecListCompat mediaCodecList = Util.SDK_INT >= 21
+        ? new MediaCodecListCompatV21(secure) : new MediaCodecListCompatV16();
+    Pair<String, CodecCapabilities> codecInfo = getMediaCodecInfo(key, mediaCodecList);
+    // TODO: Verify this cannot occur on v22, and change >= to == [Internal: b/18678462].
+    if (secure && codecInfo == null && Util.SDK_INT >= 21) {
+      // Some devices don't list secure decoders on API level 21. Try the legacy path.
+      mediaCodecList = new MediaCodecListCompatV16();
+      codecInfo = getMediaCodecInfo(key, mediaCodecList);
+      if (codecInfo != null) {
+        Log.w(TAG, "MediaCodecList API didn't list secure decoder for: " + mimeType
+            + ". Assuming: " + codecInfo.first);
+      }
+    }
+    return codecInfo;
+  }
+
+  private static Pair<String, CodecCapabilities> getMediaCodecInfo(CodecKey key,
+      MediaCodecListCompat mediaCodecList) throws DecoderQueryException {
+    try {
+      return getMediaCodecInfoInternal(key, mediaCodecList);
+    } catch (Exception e) {
+      // If the underlying mediaserver is in a bad state, we may catch an IllegalStateException
+      // or an IllegalArgumentException here.
+      throw new DecoderQueryException(e);
+    }
+  }
+
+  private static Pair<String, CodecCapabilities> getMediaCodecInfoInternal(CodecKey key,
+      MediaCodecListCompat mediaCodecList) {
+    String mimeType = key.mimeType;
+    int numberOfCodecs = mediaCodecList.getCodecCount();
+    boolean secureDecodersExplicit = mediaCodecList.secureDecodersExplicit();
+    // Note: MediaCodecList is sorted by the framework such that the best decoders come first.
+    for (int i = 0; i < numberOfCodecs; i++) {
+      MediaCodecInfo info = mediaCodecList.getCodecInfoAt(i);
+      String codecName = info.getName();
+      if (!info.isEncoder() && codecName.startsWith("OMX.")
+          && (secureDecodersExplicit || !codecName.endsWith(".secure"))) {
+        String[] supportedTypes = info.getSupportedTypes();
+        for (int j = 0; j < supportedTypes.length; j++) {
+          String supportedType = supportedTypes[j];
+          if (supportedType.equalsIgnoreCase(mimeType)) {
+            CodecCapabilities capabilities = info.getCapabilitiesForType(supportedType);
+            boolean secure = mediaCodecList.isSecurePlaybackSupported(key.mimeType, capabilities);
+            if (!secureDecodersExplicit) {
+              // Cache variants for both insecure and (if we think it's supported) secure playback.
+              codecs.put(key.secure ? new CodecKey(mimeType, false) : key,
+                  Pair.create(codecName, capabilities));
+              if (secure) {
+                codecs.put(key.secure ? key : new CodecKey(mimeType, true),
+                    Pair.create(codecName + ".secure", capabilities));
+              }
+            } else {
+              // Only cache this variant. If both insecure and secure decoders are available, they
+              // should both be listed separately.
+              codecs.put(key.secure == secure ? key : new CodecKey(mimeType, secure),
+                  Pair.create(codecName, capabilities));
+            }
+            if (codecs.containsKey(key)) {
+              return codecs.get(key);
+            }
+          }
+        }
+      }
+    }
+    return null;
+  }
+
+  private static boolean isAdaptive(CodecCapabilities capabilities) {
+    if (Util.SDK_INT >= 19) {
+      return isAdaptiveV19(capabilities);
+    } else {
+      return false;
+    }
+  }
+
+  @TargetApi(19)
+  private static boolean isAdaptiveV19(CodecCapabilities capabilities) {
+    return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback);
+  }
+
+  /**
+   * @param profile An AVC profile constant from {@link CodecProfileLevel}.
+   * @param level An AVC profile level from {@link CodecProfileLevel}.
+   * @return Whether the specified profile is supported at the specified level.
+   */
+  public static boolean isH264ProfileSupported(int profile, int level)
+      throws DecoderQueryException {
+    Pair<String, CodecCapabilities> info = getMediaCodecInfo(MimeTypes.VIDEO_H264, false);
+    if (info == null) {
+      return false;
+    }
+
+    CodecCapabilities capabilities = info.second;
+    for (int i = 0; i < capabilities.profileLevels.length; i++) {
+      CodecProfileLevel profileLevel = capabilities.profileLevels[i];
+      if (profileLevel.profile == profile && profileLevel.level >= level) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   * @return the maximum frame size for an H264 stream that can be decoded on the device.
+   */
+  public static int maxH264DecodableFrameSize() throws DecoderQueryException {
+    Pair<String, CodecCapabilities> info = getMediaCodecInfo(MimeTypes.VIDEO_H264, false);
+    if (info == null) {
+      return 0;
+    }
+
+    int maxH264DecodableFrameSize = 0;
+    CodecCapabilities capabilities = info.second;
+    for (int i = 0; i < capabilities.profileLevels.length; i++) {
+      CodecProfileLevel profileLevel = capabilities.profileLevels[i];
+      maxH264DecodableFrameSize = Math.max(
+          avcLevelToMaxFrameSize(profileLevel.level), maxH264DecodableFrameSize);
+    }
+
+    return maxH264DecodableFrameSize;
+  }
+
+  /**
+   * Conversion values taken from: https://en.wikipedia.org/wiki/H.264/MPEG-4_AVC.
+   *
+   * @param avcLevel one of CodecProfileLevel.AVCLevel* constants.
+   * @return maximum frame size that can be decoded by a decoder with the specified avc level
+   *      (or {@code -1} if the level is not recognized)
+   */
+  private static int avcLevelToMaxFrameSize(int avcLevel) {
+    switch (avcLevel) {
+      case CodecProfileLevel.AVCLevel1: return 25344;
+      case CodecProfileLevel.AVCLevel1b: return 25344;
+      case CodecProfileLevel.AVCLevel12: return 101376;
+      case CodecProfileLevel.AVCLevel13: return 101376;
+      case CodecProfileLevel.AVCLevel2: return 101376;
+      case CodecProfileLevel.AVCLevel21: return 202752;
+      case CodecProfileLevel.AVCLevel22: return 414720;
+      case CodecProfileLevel.AVCLevel3: return 414720;
+      case CodecProfileLevel.AVCLevel31: return 921600;
+      case CodecProfileLevel.AVCLevel32: return 1310720;
+      case CodecProfileLevel.AVCLevel4: return 2097152;
+      case CodecProfileLevel.AVCLevel41: return 2097152;
+      case CodecProfileLevel.AVCLevel42: return 2228224;
+      case CodecProfileLevel.AVCLevel5: return 5652480;
+      case CodecProfileLevel.AVCLevel51: return 9437184;
+      default: return -1;
+    }
+  }
+
+  private interface MediaCodecListCompat {
+
+    /**
+     * The number of codecs in the list.
+     */
+    public int getCodecCount();
+
+    /**
+     * The info at the specified index in the list.
+     *
+     * @param index The index.
+     */
+    public MediaCodecInfo getCodecInfoAt(int index);
+
+    /**
+     * @return Returns whether secure decoders are explicitly listed, if present.
+     */
+    public boolean secureDecodersExplicit();
+
+    /**
+     * Whether secure playback is supported for the given {@link CodecCapabilities}, which should
+     * have been obtained from a {@link MediaCodecInfo} obtained from this list.
+     */
+    public boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities);
+
+  }
+
+  @TargetApi(21)
+  private static final class MediaCodecListCompatV21 implements MediaCodecListCompat {
+
+    private final int codecKind;
+
+    private MediaCodecInfo[] mediaCodecInfos;
+
+    public MediaCodecListCompatV21(boolean includeSecure) {
+      codecKind = includeSecure ? MediaCodecList.ALL_CODECS : MediaCodecList.REGULAR_CODECS;
+    }
+
+    @Override
+    public int getCodecCount() {
+      ensureMediaCodecInfosInitialized();
+      return mediaCodecInfos.length;
+    }
+
+    @Override
+    public MediaCodecInfo getCodecInfoAt(int index) {
+      ensureMediaCodecInfosInitialized();
+      return mediaCodecInfos[index];
+    }
+
+    @Override
+    public boolean secureDecodersExplicit() {
+      return true;
+    }
+
+    @Override
+    public boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities) {
+      return capabilities.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback);
+    }
+
+    private void ensureMediaCodecInfosInitialized() {
+      if (mediaCodecInfos == null) {
+        mediaCodecInfos = new MediaCodecList(codecKind).getCodecInfos();
+      }
+    }
+
+  }
+
+  @SuppressWarnings("deprecation")
+  private static final class MediaCodecListCompatV16 implements MediaCodecListCompat {
+
+    @Override
+    public int getCodecCount() {
+      return MediaCodecList.getCodecCount();
+    }
+
+    @Override
+    public MediaCodecInfo getCodecInfoAt(int index) {
+      return MediaCodecList.getCodecInfoAt(index);
+    }
+
+    @Override
+    public boolean secureDecodersExplicit() {
+      return false;
+    }
+
+    @Override
+    public boolean isSecurePlaybackSupported(String mimeType, CodecCapabilities capabilities) {
+      // Secure decoders weren't explicitly listed prior to API level 21. We assume that a secure
+      // H264 decoder exists.
+      return MimeTypes.VIDEO_H264.equals(mimeType);
+    }
+
+  }
+
+  private static final class CodecKey {
+
+    public final String mimeType;
+    public final boolean secure;
+
+    public CodecKey(String mimeType, boolean secure) {
+      this.mimeType = mimeType;
+      this.secure = secure;
+    }
+
+    @Override
+    public int hashCode() {
+      final int prime = 31;
+      int result = 1;
+      result = prime * result + ((mimeType == null) ? 0 : mimeType.hashCode());
+      result = prime * result + (secure ? 1231 : 1237);
+      return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+      if (obj == null || obj.getClass() != CodecKey.class) {
+        return false;
+      }
+      CodecKey other = (CodecKey) obj;
+      return TextUtils.equals(mimeType, other.mimeType) && secure == other.secure;
+    }
+
+  }
+
+}

+ 560 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/MediaCodecVideoTrackRenderer.java

@@ -0,0 +1,560 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+import com.google.android.exoplayer.drm.DrmSessionManager;
+import com.google.android.exoplayer.util.MimeTypes;
+import com.google.android.exoplayer.util.TraceUtil;
+import com.google.android.exoplayer.util.Util;
+
+import android.annotation.TargetApi;
+import android.media.MediaCodec;
+import android.media.MediaCrypto;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.view.Surface;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Decodes and renders video using {@link MediaCodec}.
+ */
+@TargetApi(16)
+public class MediaCodecVideoTrackRenderer extends MediaCodecTrackRenderer {
+
+  /**
+   * Interface definition for a callback to be notified of {@link MediaCodecVideoTrackRenderer}
+   * events.
+   */
+  public interface EventListener extends MediaCodecTrackRenderer.EventListener {
+
+    /**
+     * Invoked to report the number of frames dropped by the renderer. Dropped frames are reported
+     * whenever the renderer is stopped having dropped frames, and optionally, whenever the count
+     * reaches a specified threshold whilst the renderer is started.
+     *
+     * @param count The number of dropped frames.
+     * @param elapsed The duration in milliseconds over which the frames were dropped. This
+     *     duration is timed from when the renderer was started or from when dropped frames were
+     *     last reported (whichever was more recent), and not from when the first of the reported
+     *     drops occurred.
+     */
+    void onDroppedFrames(int count, long elapsed);
+
+    /**
+     * Invoked each time there's a change in the size of the video being rendered.
+     *
+     * @param width The video width in pixels.
+     * @param height The video height in pixels.
+     * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case
+     *     of square pixels this will be equal to 1.0. Different values are indicative of anamorphic
+     *     content.
+     */
+    void onVideoSizeChanged(int width, int height, float pixelWidthHeightRatio);
+
+    /**
+     * Invoked when a frame is rendered to a surface for the first time following that surface
+     * having been set as the target for the renderer.
+     *
+     * @param surface The surface to which a first frame has been rendered.
+     */
+    void onDrawnToSurface(Surface surface);
+
+  }
+
+  /**
+   * An interface for fine-grained adjustment of frame release times.
+   */
+  public interface FrameReleaseTimeHelper {
+
+    /**
+     * Enables the helper.
+     */
+    void enable();
+
+    /**
+     * Disables the helper.
+     */
+    void disable();
+
+    /**
+     * Called to make a fine-grained adjustment to a frame release time.
+     *
+     * @param framePresentationTimeUs The frame's media presentation time, in microseconds.
+     * @param unadjustedReleaseTimeNs The frame's unadjusted release time, in nanoseconds and in
+     *     the same time base as {@link System#nanoTime()}.
+     * @return An adjusted release time for the frame, in nanoseconds and in the same time base as
+     *     {@link System#nanoTime()}.
+     */
+    public long adjustReleaseTime(long framePresentationTimeUs, long unadjustedReleaseTimeNs);
+
+  }
+
+  // TODO: Use MediaFormat constants if these get exposed through the API. See [Internal: b/14127601].
+  private static final String KEY_CROP_LEFT = "crop-left";
+  private static final String KEY_CROP_RIGHT = "crop-right";
+  private static final String KEY_CROP_BOTTOM = "crop-bottom";
+  private static final String KEY_CROP_TOP = "crop-top";
+
+  /**
+   * The type of a message that can be passed to an instance of this class via
+   * {@link ExoPlayer#sendMessage} or {@link ExoPlayer#blockingSendMessage}. The message object
+   * should be the target {@link Surface}, or null.
+   */
+  public static final int MSG_SET_SURFACE = 1;
+
+  private final FrameReleaseTimeHelper frameReleaseTimeHelper;
+  private final EventListener eventListener;
+  private final long allowedJoiningTimeUs;
+  private final int videoScalingMode;
+  private final int maxDroppedFrameCountToNotify;
+
+  private Surface surface;
+  private boolean reportedDrawnToSurface;
+  private boolean renderedFirstFrame;
+  private long joiningDeadlineUs;
+  private long droppedFrameAccumulationStartTimeMs;
+  private int droppedFrameCount;
+
+  private int currentWidth;
+  private int currentHeight;
+  private float currentPixelWidthHeightRatio;
+  private int lastReportedWidth;
+  private int lastReportedHeight;
+  private float lastReportedPixelWidthHeightRatio;
+
+  /**
+   * @param source The upstream source from which the renderer obtains samples.
+   * @param videoScalingMode The scaling mode to pass to
+   *     {@link MediaCodec#setVideoScalingMode(int)}.
+   */
+  public MediaCodecVideoTrackRenderer(SampleSource source, int videoScalingMode) {
+    this(source, null, true, videoScalingMode);
+  }
+
+  /**
+   * @param source The upstream source from which the renderer obtains samples.
+   * @param drmSessionManager For use with encrypted content. May be null if support for encrypted
+   *     content is not required.
+   * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
+   *     For example a media file may start with a short clear region so as to allow playback to
+   *     begin in parallel with key acquisision. This parameter specifies whether the renderer is
+   *     permitted to play clear regions of encrypted media files before {@code drmSessionManager}
+   *     has obtained the keys necessary to decrypt encrypted regions of the media.
+   * @param videoScalingMode The scaling mode to pass to
+   *     {@link MediaCodec#setVideoScalingMode(int)}.
+   */
+  public MediaCodecVideoTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
+      boolean playClearSamplesWithoutKeys, int videoScalingMode) {
+    this(source, drmSessionManager, playClearSamplesWithoutKeys, videoScalingMode, 0);
+  }
+
+  /**
+   * @param source The upstream source from which the renderer obtains samples.
+   * @param videoScalingMode The scaling mode to pass to
+   *     {@link MediaCodec#setVideoScalingMode(int)}.
+   * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
+   *     can attempt to seamlessly join an ongoing playback.
+   */
+  public MediaCodecVideoTrackRenderer(SampleSource source, int videoScalingMode,
+      long allowedJoiningTimeMs) {
+    this(source, null, true, videoScalingMode, allowedJoiningTimeMs);
+  }
+
+  /**
+   * @param source The upstream source from which the renderer obtains samples.
+   * @param drmSessionManager For use with encrypted content. May be null if support for encrypted
+   *     content is not required.
+   * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
+   *     For example a media file may start with a short clear region so as to allow playback to
+   *     begin in parallel with key acquisision. This parameter specifies whether the renderer is
+   *     permitted to play clear regions of encrypted media files before {@code drmSessionManager}
+   *     has obtained the keys necessary to decrypt encrypted regions of the media.
+   * @param videoScalingMode The scaling mode to pass to
+   *     {@link MediaCodec#setVideoScalingMode(int)}.
+   * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
+   *     can attempt to seamlessly join an ongoing playback.
+   */
+  public MediaCodecVideoTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
+      boolean playClearSamplesWithoutKeys, int videoScalingMode, long allowedJoiningTimeMs) {
+    this(source, drmSessionManager, playClearSamplesWithoutKeys, videoScalingMode,
+        allowedJoiningTimeMs, null, null, null, -1);
+  }
+
+  /**
+   * @param source The upstream source from which the renderer obtains samples.
+   * @param videoScalingMode The scaling mode to pass to
+   *     {@link MediaCodec#setVideoScalingMode(int)}.
+   * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
+   *     can attempt to seamlessly join an ongoing playback.
+   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+   *     null if delivery of events is not required.
+   * @param eventListener A listener of events. May be null if delivery of events is not required.
+   * @param maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between
+   *     invocations of {@link EventListener#onDroppedFrames(int, long)}.
+   */
+  public MediaCodecVideoTrackRenderer(SampleSource source, int videoScalingMode,
+      long allowedJoiningTimeMs, Handler eventHandler, EventListener eventListener,
+      int maxDroppedFrameCountToNotify) {
+    this(source, null, true, videoScalingMode, allowedJoiningTimeMs, null, eventHandler,
+        eventListener, maxDroppedFrameCountToNotify);
+  }
+
+  /**
+   * @param source The upstream source from which the renderer obtains samples.
+   * @param drmSessionManager For use with encrypted content. May be null if support for encrypted
+   *     content is not required.
+   * @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
+   *     For example a media file may start with a short clear region so as to allow playback to
+   *     begin in parallel with key acquisision. This parameter specifies whether the renderer is
+   *     permitted to play clear regions of encrypted media files before {@code drmSessionManager}
+   *     has obtained the keys necessary to decrypt encrypted regions of the media.
+   * @param videoScalingMode The scaling mode to pass to
+   *     {@link MediaCodec#setVideoScalingMode(int)}.
+   * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
+   *     can attempt to seamlessly join an ongoing playback.
+   * @param frameReleaseTimeHelper An optional helper to make fine-grained adjustments to frame
+   *     release times. May be null.
+   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+   *     null if delivery of events is not required.
+   * @param eventListener A listener of events. May be null if delivery of events is not required.
+   * @param maxDroppedFrameCountToNotify The maximum number of frames that can be dropped between
+   *     invocations of {@link EventListener#onDroppedFrames(int, long)}.
+   */
+  public MediaCodecVideoTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
+      boolean playClearSamplesWithoutKeys, int videoScalingMode, long allowedJoiningTimeMs,
+      FrameReleaseTimeHelper frameReleaseTimeHelper, Handler eventHandler,
+      EventListener eventListener, int maxDroppedFrameCountToNotify) {
+    super(source, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, eventListener);
+    this.videoScalingMode = videoScalingMode;
+    this.allowedJoiningTimeUs = allowedJoiningTimeMs * 1000;
+    this.frameReleaseTimeHelper = frameReleaseTimeHelper;
+    this.eventListener = eventListener;
+    this.maxDroppedFrameCountToNotify = maxDroppedFrameCountToNotify;
+    joiningDeadlineUs = -1;
+    currentWidth = -1;
+    currentHeight = -1;
+    currentPixelWidthHeightRatio = -1;
+    lastReportedWidth = -1;
+    lastReportedHeight = -1;
+    lastReportedPixelWidthHeightRatio = -1;
+  }
+
+  @Override
+  protected boolean handlesMimeType(String mimeType) {
+    return MimeTypes.isVideo(mimeType) && super.handlesMimeType(mimeType);
+  }
+
+  @Override
+  protected void onEnabled(long positionUs, boolean joining) {
+    super.onEnabled(positionUs, joining);
+    renderedFirstFrame = false;
+    if (joining && allowedJoiningTimeUs > 0) {
+      joiningDeadlineUs = SystemClock.elapsedRealtime() * 1000L + allowedJoiningTimeUs;
+    }
+    if (frameReleaseTimeHelper != null) {
+      frameReleaseTimeHelper.enable();
+    }
+  }
+
+  @Override
+  protected void seekTo(long positionUs) throws ExoPlaybackException {
+    super.seekTo(positionUs);
+    renderedFirstFrame = false;
+    joiningDeadlineUs = -1;
+  }
+
+  @Override
+  protected boolean isReady() {
+    if (super.isReady() && (renderedFirstFrame || !codecInitialized()
+        || getSourceState() == SOURCE_STATE_READY_READ_MAY_FAIL)) {
+      // Ready. If we were joining then we've now joined, so clear the joining deadline.
+      joiningDeadlineUs = -1;
+      return true;
+    } else if (joiningDeadlineUs == -1) {
+      // Not joining.
+      return false;
+    } else if (SystemClock.elapsedRealtime() * 1000 < joiningDeadlineUs) {
+      // Joining and still within the joining deadline.
+      return true;
+    } else {
+      // The joining deadline has been exceeded. Give up and clear the deadline.
+      joiningDeadlineUs = -1;
+      return false;
+    }
+  }
+
+  @Override
+  protected void onStarted() {
+    super.onStarted();
+    droppedFrameCount = 0;
+    droppedFrameAccumulationStartTimeMs = SystemClock.elapsedRealtime();
+  }
+
+  @Override
+  protected void onStopped() {
+    joiningDeadlineUs = -1;
+    maybeNotifyDroppedFrameCount();
+    super.onStopped();
+  }
+
+  @Override
+  public void onDisabled() {
+    currentWidth = -1;
+    currentHeight = -1;
+    currentPixelWidthHeightRatio = -1;
+    lastReportedWidth = -1;
+    lastReportedHeight = -1;
+    lastReportedPixelWidthHeightRatio = -1;
+    if (frameReleaseTimeHelper != null) {
+      frameReleaseTimeHelper.disable();
+    }
+    super.onDisabled();
+  }
+
+  @Override
+  public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
+    if (messageType == MSG_SET_SURFACE) {
+      setSurface((Surface) message);
+    } else {
+      super.handleMessage(messageType, message);
+    }
+  }
+
+  /**
+   * @param surface The surface to set.
+   * @throws ExoPlaybackException
+   */
+  private void setSurface(Surface surface) throws ExoPlaybackException {
+    if (this.surface == surface) {
+      return;
+    }
+    this.surface = surface;
+    this.reportedDrawnToSurface = false;
+    int state = getState();
+    if (state == TrackRenderer.STATE_ENABLED || state == TrackRenderer.STATE_STARTED) {
+      releaseCodec();
+      maybeInitCodec();
+    }
+  }
+
+  @Override
+  protected boolean shouldInitCodec() {
+    return super.shouldInitCodec() && surface != null && surface.isValid();
+  }
+
+  // Override configureCodec to provide the surface.
+  @Override
+  protected void configureCodec(MediaCodec codec, android.media.MediaFormat format,
+      MediaCrypto crypto) {
+    codec.configure(format, surface, crypto, 0);
+    codec.setVideoScalingMode(videoScalingMode);
+  }
+
+  @Override
+  protected void onInputFormatChanged(MediaFormatHolder holder) throws ExoPlaybackException {
+    super.onInputFormatChanged(holder);
+    // TODO: Ideally this would be read in onOutputFormatChanged, but there doesn't seem
+    // to be a way to pass a custom key/value pair value through to the output format.
+    currentPixelWidthHeightRatio = holder.format.pixelWidthHeightRatio == MediaFormat.NO_VALUE ? 1
+        : holder.format.pixelWidthHeightRatio;
+  }
+
+  @Override
+  protected void onOutputFormatChanged(android.media.MediaFormat format) {
+    boolean hasCrop = format.containsKey(KEY_CROP_RIGHT) && format.containsKey(KEY_CROP_LEFT)
+        && format.containsKey(KEY_CROP_BOTTOM) && format.containsKey(KEY_CROP_TOP);
+    currentWidth = hasCrop
+        ? format.getInteger(KEY_CROP_RIGHT) - format.getInteger(KEY_CROP_LEFT) + 1
+        : format.getInteger(android.media.MediaFormat.KEY_WIDTH);
+    currentHeight = hasCrop
+        ? format.getInteger(KEY_CROP_BOTTOM) - format.getInteger(KEY_CROP_TOP) + 1
+        : format.getInteger(android.media.MediaFormat.KEY_HEIGHT);
+  }
+
+  @Override
+  protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive,
+      MediaFormat oldFormat, MediaFormat newFormat) {
+    return newFormat.mimeType.equals(oldFormat.mimeType)
+        && (codecIsAdaptive
+            || (oldFormat.width == newFormat.width && oldFormat.height == newFormat.height));
+  }
+
+  @Override
+  protected boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs, MediaCodec codec,
+      ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex, boolean shouldSkip) {
+    if (shouldSkip) {
+      skipOutputBuffer(codec, bufferIndex);
+      return true;
+    }
+
+    // Compute how many microseconds it is until the buffer's presentation time.
+    long elapsedSinceStartOfLoopUs = (SystemClock.elapsedRealtime() * 1000) - elapsedRealtimeUs;
+    long earlyUs = bufferInfo.presentationTimeUs - positionUs - elapsedSinceStartOfLoopUs;
+
+    // Compute the buffer's desired release time in nanoseconds.
+    long systemTimeNs = System.nanoTime();
+    long unadjustedFrameReleaseTimeNs = systemTimeNs + (earlyUs * 1000);
+
+    // Apply a timestamp adjustment, if there is one.
+    long adjustedReleaseTimeNs;
+    if (frameReleaseTimeHelper != null) {
+      adjustedReleaseTimeNs = frameReleaseTimeHelper.adjustReleaseTime(
+          bufferInfo.presentationTimeUs, unadjustedFrameReleaseTimeNs);
+      earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000;
+    } else {
+      adjustedReleaseTimeNs = unadjustedFrameReleaseTimeNs;
+    }
+
+    if (earlyUs < -30000) {
+      // We're more than 30ms late rendering the frame.
+      dropOutputBuffer(codec, bufferIndex);
+      return true;
+    }
+
+    if (!renderedFirstFrame) {
+      renderOutputBufferImmediate(codec, bufferIndex);
+      renderedFirstFrame = true;
+      return true;
+    }
+
+    if (getState() != TrackRenderer.STATE_STARTED) {
+      return false;
+    }
+
+    if (Util.SDK_INT >= 21) {
+      // Let the underlying framework time the release.
+      if (earlyUs < 50000) {
+        renderOutputBufferTimedV21(codec, bufferIndex, adjustedReleaseTimeNs);
+        return true;
+      }
+    } else {
+      // We need to time the release ourselves.
+      if (earlyUs < 30000) {
+        if (earlyUs > 11000) {
+          // We're a little too early to render the frame. Sleep until the frame can be rendered.
+          // Note: The 11ms threshold was chosen fairly arbitrarily.
+          try {
+            // Subtracting 10000 rather than 11000 ensures the sleep time will be at least 1ms.
+            Thread.sleep((earlyUs - 10000) / 1000);
+          } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+          }
+        }
+        renderOutputBufferImmediate(codec, bufferIndex);
+        return true;
+      }
+    }
+
+    // We're either not playing, or it's not time to render the frame yet.
+    return false;
+  }
+
+  private void skipOutputBuffer(MediaCodec codec, int bufferIndex) {
+    TraceUtil.beginSection("skipVideoBuffer");
+    codec.releaseOutputBuffer(bufferIndex, false);
+    TraceUtil.endSection();
+    codecCounters.skippedOutputBufferCount++;
+  }
+
+  private void dropOutputBuffer(MediaCodec codec, int bufferIndex) {
+    TraceUtil.beginSection("dropVideoBuffer");
+    codec.releaseOutputBuffer(bufferIndex, false);
+    TraceUtil.endSection();
+    codecCounters.droppedOutputBufferCount++;
+    droppedFrameCount++;
+    if (droppedFrameCount == maxDroppedFrameCountToNotify) {
+      maybeNotifyDroppedFrameCount();
+    }
+  }
+
+  private void renderOutputBufferImmediate(MediaCodec codec, int bufferIndex) {
+    maybeNotifyVideoSizeChanged();
+    TraceUtil.beginSection("renderVideoBufferImmediate");
+    codec.releaseOutputBuffer(bufferIndex, true);
+    TraceUtil.endSection();
+    codecCounters.renderedOutputBufferCount++;
+    maybeNotifyDrawnToSurface();
+  }
+
+  @TargetApi(21)
+  private void renderOutputBufferTimedV21(MediaCodec codec, int bufferIndex, long releaseTimeNs) {
+    maybeNotifyVideoSizeChanged();
+    TraceUtil.beginSection("releaseOutputBufferTimed");
+    codec.releaseOutputBuffer(bufferIndex, releaseTimeNs);
+    TraceUtil.endSection();
+    codecCounters.renderedOutputBufferCount++;
+    maybeNotifyDrawnToSurface();
+  }
+
+  private void maybeNotifyVideoSizeChanged() {
+    if (eventHandler == null || eventListener == null
+        || (lastReportedWidth == currentWidth && lastReportedHeight == currentHeight
+        && lastReportedPixelWidthHeightRatio == currentPixelWidthHeightRatio)) {
+      return;
+    }
+    // Make final copies to ensure the runnable reports the correct values.
+    final int currentWidth = this.currentWidth;
+    final int currentHeight = this.currentHeight;
+    final float currentPixelWidthHeightRatio = this.currentPixelWidthHeightRatio;
+    eventHandler.post(new Runnable()  {
+      @Override
+      public void run() {
+        eventListener.onVideoSizeChanged(currentWidth, currentHeight, currentPixelWidthHeightRatio);
+      }
+    });
+    // Update the last reported values.
+    lastReportedWidth = currentWidth;
+    lastReportedHeight = currentHeight;
+    lastReportedPixelWidthHeightRatio = currentPixelWidthHeightRatio;
+  }
+
+  private void maybeNotifyDrawnToSurface() {
+    if (eventHandler == null || eventListener == null || reportedDrawnToSurface) {
+      return;
+    }
+    // Make a final copy to ensure the runnable reports the correct surface.
+    final Surface surface = this.surface;
+    eventHandler.post(new Runnable()  {
+      @Override
+      public void run() {
+        eventListener.onDrawnToSurface(surface);
+      }
+    });
+    // Record that we have reported that the surface has been drawn to.
+    reportedDrawnToSurface = true;
+  }
+
+  private void maybeNotifyDroppedFrameCount() {
+    if (eventHandler == null || eventListener == null || droppedFrameCount == 0) {
+      return;
+    }
+    long now = SystemClock.elapsedRealtime();
+    // Make final copies to ensure the runnable reports the correct values.
+    final int countToNotify = droppedFrameCount;
+    final long elapsedToNotify = now - droppedFrameAccumulationStartTimeMs;
+    eventHandler.post(new Runnable()  {
+      @Override
+      public void run() {
+        eventListener.onDroppedFrames(countToNotify, elapsedToNotify);
+      }
+    });
+    // Reset the dropped frame tracking.
+    droppedFrameCount = 0;
+    droppedFrameAccumulationStartTimeMs = now;
+  }
+
+}

+ 288 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/MediaFormat.java

@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+import com.google.android.exoplayer.util.MimeTypes;
+import com.google.android.exoplayer.util.Util;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Defines the format of an elementary media stream.
+ */
+public class MediaFormat {
+
+  private static final String KEY_PIXEL_WIDTH_HEIGHT_RATIO =
+      "com.google.android.videos.pixelWidthHeightRatio";
+
+  public static final int NO_VALUE = -1;
+
+  public final String mimeType;
+  public final int maxInputSize;
+
+  public final int width;
+  public final int height;
+  public final float pixelWidthHeightRatio;
+
+  public final int channelCount;
+  public final int sampleRate;
+
+  public final int bitrate;
+
+  private int maxWidth;
+  private int maxHeight;
+
+  public final List<byte[]> initializationData;
+
+  // Lazy-initialized hashcode.
+  private int hashCode;
+  // Possibly-lazy-initialized framework media format.
+  private android.media.MediaFormat frameworkMediaFormat;
+
+  @TargetApi(16)
+  public static MediaFormat createFromFrameworkMediaFormatV16(android.media.MediaFormat format) {
+    return new MediaFormat(format);
+  }
+
+  public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width,
+      int height, List<byte[]> initializationData) {
+    return createVideoFormat(mimeType, maxInputSize, width, height, 1, initializationData);
+  }
+
+  public static MediaFormat createVideoFormat(String mimeType, int maxInputSize, int width,
+      int height, float pixelWidthHeightRatio, List<byte[]> initializationData) {
+    return new MediaFormat(mimeType, maxInputSize, width, height, pixelWidthHeightRatio, NO_VALUE,
+        NO_VALUE, NO_VALUE, initializationData);
+  }
+
+  public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount,
+      int sampleRate, List<byte[]> initializationData) {
+    return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, channelCount,
+        sampleRate, NO_VALUE, initializationData);
+  }
+
+  public static MediaFormat createAudioFormat(String mimeType, int maxInputSize, int channelCount,
+      int sampleRate, int bitrate, List<byte[]> initializationData) {
+    return new MediaFormat(mimeType, maxInputSize, NO_VALUE, NO_VALUE, NO_VALUE, channelCount,
+        sampleRate, bitrate, initializationData);
+  }
+
+  public static MediaFormat createId3Format() {
+    return createFormatForMimeType(MimeTypes.APPLICATION_ID3);
+  }
+
+  public static MediaFormat createEia608Format() {
+    return createFormatForMimeType(MimeTypes.APPLICATION_EIA608);
+  }
+
+  public static MediaFormat createTtmlFormat() {
+    return createFormatForMimeType(MimeTypes.APPLICATION_TTML);
+  }
+
+  public static MediaFormat createFormatForMimeType(String mimeType) {
+    return new MediaFormat(mimeType, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
+        NO_VALUE, null);
+  }
+
+  @TargetApi(16)
+  private MediaFormat(android.media.MediaFormat format) {
+    this.frameworkMediaFormat = format;
+    mimeType = format.getString(android.media.MediaFormat.KEY_MIME);
+    maxInputSize = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_MAX_INPUT_SIZE);
+    width = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_WIDTH);
+    height = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT);
+    channelCount = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT);
+    sampleRate = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE);
+    bitrate = getOptionalIntegerV16(format, android.media.MediaFormat.KEY_BIT_RATE);
+    pixelWidthHeightRatio = getOptionalFloatV16(format, KEY_PIXEL_WIDTH_HEIGHT_RATIO);
+    initializationData = new ArrayList<byte[]>();
+    for (int i = 0; format.containsKey("csd-" + i); i++) {
+      ByteBuffer buffer = format.getByteBuffer("csd-" + i);
+      byte[] data = new byte[buffer.limit()];
+      buffer.get(data);
+      initializationData.add(data);
+      buffer.flip();
+    }
+    maxWidth = NO_VALUE;
+    maxHeight = NO_VALUE;
+  }
+
+  private MediaFormat(String mimeType, int maxInputSize, int width, int height,
+      float pixelWidthHeightRatio, int channelCount, int sampleRate, int bitrate,
+      List<byte[]> initializationData) {
+    this.mimeType = mimeType;
+    this.maxInputSize = maxInputSize;
+    this.width = width;
+    this.height = height;
+    this.pixelWidthHeightRatio = pixelWidthHeightRatio;
+    this.channelCount = channelCount;
+    this.sampleRate = sampleRate;
+    this.bitrate = bitrate;
+    this.initializationData = initializationData == null ? Collections.<byte[]>emptyList()
+        : initializationData;
+    maxWidth = NO_VALUE;
+    maxHeight = NO_VALUE;
+  }
+
+  public void setMaxVideoDimensions(int maxWidth, int maxHeight) {
+    this.maxWidth = maxWidth;
+    this.maxHeight = maxHeight;
+    if (frameworkMediaFormat != null) {
+      maybeSetMaxDimensionsV16(frameworkMediaFormat);
+    }
+  }
+
+  public int getMaxVideoWidth() {
+    return maxWidth;
+  }
+
+  public int getMaxVideoHeight() {
+    return maxHeight;
+  }
+
+  @Override
+  public int hashCode() {
+    if (hashCode == 0) {
+      int result = 17;
+      result = 31 * result + (mimeType == null ? 0 : mimeType.hashCode());
+      result = 31 * result + maxInputSize;
+      result = 31 * result + width;
+      result = 31 * result + height;
+      result = 31 * result + Float.floatToRawIntBits(pixelWidthHeightRatio);
+      result = 31 * result + maxWidth;
+      result = 31 * result + maxHeight;
+      result = 31 * result + channelCount;
+      result = 31 * result + sampleRate;
+      result = 31 * result + bitrate;
+      for (int i = 0; i < initializationData.size(); i++) {
+        result = 31 * result + Arrays.hashCode(initializationData.get(i));
+      }
+      hashCode = result;
+    }
+    return hashCode;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (obj == null || getClass() != obj.getClass()) {
+      return false;
+    }
+    return equalsInternal((MediaFormat) obj, false);
+  }
+
+  public boolean equals(MediaFormat other, boolean ignoreMaxDimensions) {
+    if (this == other) {
+      return true;
+    }
+    if (other == null) {
+      return false;
+    }
+    return equalsInternal(other, ignoreMaxDimensions);
+  }
+
+  private boolean equalsInternal(MediaFormat other, boolean ignoreMaxDimensions) {
+    if (maxInputSize != other.maxInputSize || width != other.width || height != other.height
+        || pixelWidthHeightRatio != other.pixelWidthHeightRatio
+        || (!ignoreMaxDimensions && (maxWidth != other.maxWidth || maxHeight != other.maxHeight))
+        || channelCount != other.channelCount || sampleRate != other.sampleRate
+        || !Util.areEqual(mimeType, other.mimeType)
+        || bitrate != other.bitrate
+        || initializationData.size() != other.initializationData.size()) {
+      return false;
+    }
+    for (int i = 0; i < initializationData.size(); i++) {
+      if (!Arrays.equals(initializationData.get(i), other.initializationData.get(i))) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public String toString() {
+    return "MediaFormat(" + mimeType + ", " + maxInputSize + ", " + width + ", " + height + ", "
+        + pixelWidthHeightRatio + ", " + channelCount + ", " + sampleRate + ", " + bitrate + ", "
+        + maxWidth + ", " + maxHeight + ")";
+  }
+
+  /**
+   * @return A {@link MediaFormat} representation of this format.
+   */
+  @TargetApi(16)
+  public final android.media.MediaFormat getFrameworkMediaFormatV16() {
+    if (frameworkMediaFormat == null) {
+      android.media.MediaFormat format = new android.media.MediaFormat();
+      format.setString(android.media.MediaFormat.KEY_MIME, mimeType);
+      maybeSetIntegerV16(format, android.media.MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
+      maybeSetIntegerV16(format, android.media.MediaFormat.KEY_WIDTH, width);
+      maybeSetIntegerV16(format, android.media.MediaFormat.KEY_HEIGHT, height);
+      maybeSetIntegerV16(format, android.media.MediaFormat.KEY_CHANNEL_COUNT, channelCount);
+      maybeSetIntegerV16(format, android.media.MediaFormat.KEY_SAMPLE_RATE, sampleRate);
+      maybeSetIntegerV16(format, android.media.MediaFormat.KEY_BIT_RATE, bitrate);
+      maybeSetFloatV16(format, KEY_PIXEL_WIDTH_HEIGHT_RATIO, pixelWidthHeightRatio);
+      for (int i = 0; i < initializationData.size(); i++) {
+        format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
+      }
+      maybeSetMaxDimensionsV16(format);
+      frameworkMediaFormat = format;
+    }
+    return frameworkMediaFormat;
+  }
+
+  @SuppressLint("InlinedApi")
+  @TargetApi(16)
+  private final void maybeSetMaxDimensionsV16(android.media.MediaFormat format) {
+    maybeSetIntegerV16(format, android.media.MediaFormat.KEY_MAX_WIDTH, maxWidth);
+    maybeSetIntegerV16(format, android.media.MediaFormat.KEY_MAX_HEIGHT, maxHeight);
+  }
+
+  @TargetApi(16)
+  private static final void maybeSetIntegerV16(android.media.MediaFormat format, String key,
+      int value) {
+    if (value != NO_VALUE) {
+      format.setInteger(key, value);
+    }
+  }
+
+  @TargetApi(16)
+  private static final void maybeSetFloatV16(android.media.MediaFormat format, String key,
+      float value) {
+    if (value != NO_VALUE) {
+      format.setFloat(key, value);
+    }
+  }
+
+  @TargetApi(16)
+  private static final int getOptionalIntegerV16(android.media.MediaFormat format, String key) {
+    return format.containsKey(key) ? format.getInteger(key) : NO_VALUE;
+  }
+
+  @TargetApi(16)
+  private static final float getOptionalFloatV16(android.media.MediaFormat format, String key) {
+    return format.containsKey(key) ? format.getFloat(key) : NO_VALUE;
+  }
+
+}

+ 36 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/MediaFormatHolder.java

@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * Holds a {@link MediaFormat} and corresponding drm scheme initialization data.
+ */
+public final class MediaFormatHolder {
+
+  /**
+   * The format of the media.
+   */
+  public MediaFormat format;
+  /**
+   * Initialization data for each of the drm schemes supported by the media, keyed by scheme UUID.
+   * Null if the media is not encrypted.
+   */
+  public Map<UUID, byte[]> drmInitData;
+
+}

+ 37 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/ParserException.java

@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+import java.io.IOException;
+
+/**
+ * Thrown when an error occurs parsing media data.
+ */
+public class ParserException extends IOException {
+
+  public ParserException(String message) {
+    super(message);
+  }
+
+  public ParserException(Throwable cause) {
+    super(cause);
+  }
+
+  public ParserException(String message, Throwable cause) {
+    super(message, cause);
+  }
+
+}

+ 108 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/SampleHolder.java

@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Holds sample data and corresponding metadata.
+ */
+public final class SampleHolder {
+
+  /**
+   * Disallows buffer replacement.
+   */
+  public static final int BUFFER_REPLACEMENT_MODE_DISABLED = 0;
+
+  /**
+   * Allows buffer replacement using {@link ByteBuffer#allocate(int)}.
+   */
+  public static final int BUFFER_REPLACEMENT_MODE_NORMAL = 1;
+
+  /**
+   * Allows buffer replacement using {@link ByteBuffer#allocateDirect(int)}.
+   */
+  public static final int BUFFER_REPLACEMENT_MODE_DIRECT = 2;
+
+  public final CryptoInfo cryptoInfo;
+
+  /**
+   * A buffer holding the sample data.
+   */
+  public ByteBuffer data;
+
+  /**
+   * The size of the sample in bytes.
+   */
+  public int size;
+
+  /**
+   * Flags that accompany the sample. A combination of
+   * {@link android.media.MediaExtractor#SAMPLE_FLAG_SYNC} and
+   * {@link android.media.MediaExtractor#SAMPLE_FLAG_ENCRYPTED}
+   */
+  public int flags;
+
+  /**
+   * The time at which the sample should be presented.
+   */
+  public long timeUs;
+
+  /**
+   * If true then the sample should be decoded, but should not be presented.
+   */
+  public boolean decodeOnly;
+
+  private final int bufferReplacementMode;
+
+  /**
+   * @param bufferReplacementMode Determines the behavior of {@link #replaceBuffer(int)}. One of
+   *     {@link #BUFFER_REPLACEMENT_MODE_DISABLED}, {@link #BUFFER_REPLACEMENT_MODE_NORMAL} and
+   *     {@link #BUFFER_REPLACEMENT_MODE_DIRECT}.
+   */
+  public SampleHolder(int bufferReplacementMode) {
+    this.cryptoInfo = new CryptoInfo();
+    this.bufferReplacementMode = bufferReplacementMode;
+  }
+
+  /**
+   * Attempts to replace {@link #data} with a {@link ByteBuffer} of the specified capacity.
+   *
+   * @param capacity The capacity of the replacement buffer, in bytes.
+   * @return True if the buffer was replaced. False otherwise.
+   */
+  public boolean replaceBuffer(int capacity) {
+    switch (bufferReplacementMode) {
+      case BUFFER_REPLACEMENT_MODE_NORMAL:
+        data = ByteBuffer.allocate(capacity);
+        return true;
+      case BUFFER_REPLACEMENT_MODE_DIRECT:
+        data = ByteBuffer.allocateDirect(capacity);
+        return true;
+    }
+    return false;
+  }
+
+  /**
+   * Clears {@link #data}. Does nothing if {@link #data} is null.
+   */
+  public void clearData() {
+    if (data != null) {
+      data.clear();
+    }
+  }
+
+}

+ 160 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/SampleSource.java

@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+import java.io.IOException;
+
+/**
+ * A source of media samples.
+ * <p>
+ * A {@link SampleSource} may expose one or multiple tracks. The number of tracks and information
+ * about each can be queried using {@link #getTrackCount()}  and {@link #getTrackInfo(int)}
+ * respectively.
+ */
+public interface SampleSource {
+
+  /**
+   * The end of stream has been reached.
+   */
+  public static final int END_OF_STREAM = -1;
+  /**
+   * Neither a sample nor a format was read in full. This may be because insufficient data is
+   * buffered upstream. If multiple tracks are enabled, this return value may indicate that the
+   * next piece of data to be returned from the {@link SampleSource} corresponds to a different
+   * track than the one for which data was requested.
+   */
+  public static final int NOTHING_READ = -2;
+  /**
+   * A sample was read.
+   */
+  public static final int SAMPLE_READ = -3;
+  /**
+   * A format was read.
+   */
+  public static final int FORMAT_READ = -4;
+  /**
+   * A discontinuity in the sample stream.
+   */
+  public static final int DISCONTINUITY_READ = -5;
+
+  /**
+   * Prepares the source.
+   * <p>
+   * Preparation may require reading from the data source (e.g. to determine the available tracks
+   * and formats). If insufficient data is available then the call will return {@code false} rather
+   * than block. The method can be called repeatedly until the return value indicates success.
+   *
+   * @return True if the source was prepared successfully, false otherwise.
+   * @throws IOException If an error occurred preparing the source.
+   */
+  public boolean prepare() throws IOException;
+
+  /**
+   * Returns the number of tracks exposed by the source.
+   *
+   * @return The number of tracks.
+   */
+  public int getTrackCount();
+
+  /**
+   * Returns information about the specified track.
+   * <p>
+   * This method should not be called until after the source has been successfully prepared.
+   *
+   * @return Information about the specified track.
+   */
+  public TrackInfo getTrackInfo(int track);
+
+  /**
+   * Enable the specified track. This allows the track's format and samples to be read from
+   * {@link #readData(int, long, MediaFormatHolder, SampleHolder, boolean)}.
+   * <p>
+   * This method should not be called until after the source has been successfully prepared.
+   *
+   * @param track The track to enable.
+   * @param positionUs The player's current playback position.
+   */
+  public void enable(int track, long positionUs);
+
+  /**
+   * Disable the specified track.
+   * <p>
+   * This method should not be called until after the source has been successfully prepared.
+   *
+   * @param track The track to disable.
+   */
+  public void disable(int track);
+
+  /**
+   * Indicates to the source that it should still be buffering data.
+   *
+   * @param positionUs The current playback position.
+   * @return True if the source has available samples, or if the end of the stream has been reached.
+   *     False if more data needs to be buffered for samples to become available.
+   * @throws IOException If an error occurred reading from the source.
+   */
+  public boolean continueBuffering(long positionUs) throws IOException;
+
+  /**
+   * Attempts to read either a sample, a new format or or a discontinuity from the source.
+   * <p>
+   * This method should not be called until after the source has been successfully prepared.
+   * <p>
+   * Note that where multiple tracks are enabled, {@link #NOTHING_READ} may be returned if the
+   * next piece of data to be read from the {@link SampleSource} corresponds to a different track
+   * than the one for which data was requested.
+   *
+   * @param track The track from which to read.
+   * @param positionUs The current playback position.
+   * @param formatHolder A {@link MediaFormatHolder} object to populate in the case of a new format.
+   * @param sampleHolder A {@link SampleHolder} object to populate in the case of a new sample. If
+   *     the caller requires the sample data then it must ensure that {@link SampleHolder#data}
+   *     references a valid output buffer.
+   * @param onlyReadDiscontinuity Whether to only read a discontinuity. If true, only
+   *     {@link #DISCONTINUITY_READ} or {@link #NOTHING_READ} can be returned.
+   * @return The result, which can be {@link #SAMPLE_READ}, {@link #FORMAT_READ},
+   *     {@link #DISCONTINUITY_READ}, {@link #NOTHING_READ} or {@link #END_OF_STREAM}.
+   * @throws IOException If an error occurred reading from the source.
+   */
+  public int readData(int track, long positionUs, MediaFormatHolder formatHolder,
+      SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException;
+
+  /**
+   * Seeks to the specified time in microseconds.
+   * <p>
+   * This method should not be called until after the source has been successfully prepared.
+   *
+   * @param positionUs The seek position in microseconds.
+   */
+  public void seekToUs(long positionUs);
+
+  /**
+   * Returns an estimate of the position up to which data is buffered.
+   * <p>
+   * This method should not be called until after the source has been successfully prepared.
+   *
+   * @return An estimate of the absolute position in microseconds up to which data is buffered,
+   *     or {@link TrackRenderer#END_OF_TRACK_US} if data is buffered to the end of the stream, or
+   *     {@link TrackRenderer#UNKNOWN_TIME_US} if no estimate is available.
+   */
+  public long getBufferedPositionUs();
+
+  /**
+   * Releases the {@link SampleSource}.
+   */
+  public void release();
+
+}

+ 180 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/SmoothFrameReleaseTimeHelper.java

@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+import com.google.android.exoplayer.MediaCodecVideoTrackRenderer.FrameReleaseTimeHelper;
+
+import android.annotation.TargetApi;
+import android.view.Choreographer;
+import android.view.Choreographer.FrameCallback;
+
+/**
+ * Makes a best effort to adjust frame release timestamps for a smoother visual result.
+ */
+@TargetApi(16)
+public class SmoothFrameReleaseTimeHelper implements FrameReleaseTimeHelper, FrameCallback {
+
+  private static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500;
+  private static final long MAX_ALLOWED_DRIFT_NS = 20000000;
+
+  private static final long VSYNC_OFFSET_PERCENTAGE = 80;
+  private static final int MIN_FRAMES_FOR_ADJUSTMENT = 6;
+
+  private final boolean usePrimaryDisplayVsync;
+  private final long vsyncDurationNs;
+  private final long vsyncOffsetNs;
+
+  private Choreographer choreographer;
+  private long sampledVsyncTimeNs;
+
+  private long lastUnadjustedFrameTimeUs;
+  private long adjustedLastFrameTimeNs;
+  private long pendingAdjustedFrameTimeNs;
+
+  private boolean haveSync;
+  private long syncReleaseTimeNs;
+  private long syncFrameTimeNs;
+  private int frameCount;
+
+  /**
+   * @param primaryDisplayRefreshRate The refresh rate of the default display.
+   * @param usePrimaryDisplayVsync Whether to snap to the primary display vsync. May not be
+   *     suitable when rendering to secondary displays.
+   */
+  public SmoothFrameReleaseTimeHelper(
+      float primaryDisplayRefreshRate, boolean usePrimaryDisplayVsync) {
+    this.usePrimaryDisplayVsync = usePrimaryDisplayVsync;
+    if (usePrimaryDisplayVsync) {
+      vsyncDurationNs = (long) (1000000000d / primaryDisplayRefreshRate);
+      vsyncOffsetNs = (vsyncDurationNs * VSYNC_OFFSET_PERCENTAGE) / 100;
+    } else {
+      vsyncDurationNs = -1;
+      vsyncOffsetNs = -1;
+    }
+  }
+
+  @Override
+  public void enable() {
+    haveSync = false;
+    if (usePrimaryDisplayVsync) {
+      sampledVsyncTimeNs = 0;
+      choreographer = Choreographer.getInstance();
+      choreographer.postFrameCallback(this);
+    }
+  }
+
+  @Override
+  public void disable() {
+    if (usePrimaryDisplayVsync) {
+      choreographer.removeFrameCallback(this);
+      choreographer = null;
+    }
+  }
+
+  @Override
+  public void doFrame(long vsyncTimeNs) {
+    sampledVsyncTimeNs = vsyncTimeNs;
+    choreographer.postFrameCallbackDelayed(this, CHOREOGRAPHER_SAMPLE_DELAY_MILLIS);
+  }
+
+  @Override
+  public long adjustReleaseTime(long unadjustedFrameTimeUs, long unadjustedReleaseTimeNs) {
+    long unadjustedFrameTimeNs = unadjustedFrameTimeUs * 1000;
+
+    // Until we know better, the adjustment will be a no-op.
+    long adjustedFrameTimeNs = unadjustedFrameTimeNs;
+    long adjustedReleaseTimeNs = unadjustedReleaseTimeNs;
+
+    if (haveSync) {
+      // See if we've advanced to the next frame.
+      if (unadjustedFrameTimeUs != lastUnadjustedFrameTimeUs) {
+        frameCount++;
+        adjustedLastFrameTimeNs = pendingAdjustedFrameTimeNs;
+      }
+      if (frameCount >= MIN_FRAMES_FOR_ADJUSTMENT) {
+        // We're synced and have waited the required number of frames to apply an adjustment.
+        // Calculate the average frame time across all the frames we've seen since the last sync.
+        // This will typically give us a framerate at a finer granularity than the frame times
+        // themselves (which often only have millisecond granularity).
+        long averageFrameTimeNs = (unadjustedFrameTimeNs - syncFrameTimeNs) / frameCount;
+        // Project the adjusted frame time forward using the average.
+        long candidateAdjustedFrameTimeNs = adjustedLastFrameTimeNs + averageFrameTimeNs;
+
+        if (isDriftTooLarge(candidateAdjustedFrameTimeNs, unadjustedReleaseTimeNs)) {
+          haveSync = false;
+        } else {
+          adjustedFrameTimeNs = candidateAdjustedFrameTimeNs;
+          adjustedReleaseTimeNs = syncReleaseTimeNs + adjustedFrameTimeNs - syncFrameTimeNs;
+        }
+      } else {
+        // We're synced but haven't waited the required number of frames to apply an adjustment.
+        // Check drift anyway.
+        if (isDriftTooLarge(unadjustedFrameTimeNs, unadjustedReleaseTimeNs)) {
+          haveSync = false;
+        }
+      }
+    }
+
+    // If we need to sync, do so now.
+    if (!haveSync) {
+      syncFrameTimeNs = unadjustedFrameTimeNs;
+      syncReleaseTimeNs = unadjustedReleaseTimeNs;
+      frameCount = 0;
+      haveSync = true;
+      onSynced();
+    }
+
+    lastUnadjustedFrameTimeUs = unadjustedFrameTimeUs;
+    pendingAdjustedFrameTimeNs = adjustedFrameTimeNs;
+
+    if (sampledVsyncTimeNs == 0) {
+      return adjustedReleaseTimeNs;
+    }
+
+    // Find the timestamp of the closest vsync. This is the vsync that we're targeting.
+    long snappedTimeNs = closestVsync(adjustedReleaseTimeNs, sampledVsyncTimeNs, vsyncDurationNs);
+    // Apply an offset so that we release before the target vsync, but after the previous one.
+    return snappedTimeNs - vsyncOffsetNs;
+  }
+
+  protected void onSynced() {
+    // Do nothing.
+  }
+
+  private boolean isDriftTooLarge(long frameTimeNs, long releaseTimeNs) {
+    long elapsedFrameTimeNs = frameTimeNs - syncFrameTimeNs;
+    long elapsedReleaseTimeNs = releaseTimeNs - syncReleaseTimeNs;
+    return Math.abs(elapsedReleaseTimeNs - elapsedFrameTimeNs) > MAX_ALLOWED_DRIFT_NS;
+  }
+
+  private static long closestVsync(long releaseTime, long sampledVsyncTime, long vsyncDuration) {
+    long vsyncCount = (releaseTime - sampledVsyncTime) / vsyncDuration;
+    long snappedTimeNs = sampledVsyncTime + (vsyncDuration * vsyncCount);
+    long snappedBeforeNs;
+    long snappedAfterNs;
+    if (releaseTime <= snappedTimeNs) {
+      snappedBeforeNs = snappedTimeNs - vsyncDuration;
+      snappedAfterNs = snappedTimeNs;
+    } else {
+      snappedBeforeNs = snappedTimeNs;
+      snappedAfterNs = snappedTimeNs + vsyncDuration;
+    }
+    long snappedAfterDiff = snappedAfterNs - releaseTime;
+    long snappedBeforeDiff = releaseTime - snappedBeforeNs;
+    return snappedAfterDiff < snappedBeforeDiff ? snappedAfterNs : snappedBeforeNs;
+  }
+
+}

+ 43 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/TrackInfo.java

@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+/**
+ * Holds high level information about a media track.
+ */
+public final class TrackInfo {
+
+  /**
+   * The mime type.
+   */
+  public final String mimeType;
+
+  /**
+   * The duration in microseconds, or {@link C#UNKNOWN_TIME_US} if the duration is unknown.
+   */
+  public final long durationUs;
+
+  /**
+   * @param mimeType The mime type.
+   * @param durationUs The duration in microseconds, or {@link C#UNKNOWN_TIME_US} if the duration
+   *     is unknown.
+   */
+  public TrackInfo(String mimeType, long durationUs) {
+    this.mimeType = mimeType;
+    this.durationUs = durationUs;
+  }
+
+}

+ 351 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/TrackRenderer.java

@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent;
+import com.google.android.exoplayer.util.Assertions;
+
+/**
+ * Renders a single component of media.
+ *
+ * <p>Internally, a renderer's lifecycle is managed by the owning {@link ExoPlayer}. The player
+ * will transition its renderers through various states as the overall playback state changes. The
+ * valid state transitions are shown below, annotated with the methods that are invoked during each
+ * transition.
+ * <p align="center"><img src="../../../../../doc_src/images/trackrenderer_state.png"
+ *     alt="TrackRenderer state transitions"
+ *     border="0"/></p>
+ */
+public abstract class TrackRenderer implements ExoPlayerComponent {
+
+  /**
+   * The renderer has been released and should not be used.
+   */
+  protected static final int STATE_RELEASED = -2;
+  /**
+   * The renderer should be ignored by the player.
+   */
+  protected static final int STATE_IGNORE = -1;
+  /**
+   * The renderer has not yet been prepared.
+   */
+  protected static final int STATE_UNPREPARED = 0;
+  /**
+   * The renderer has completed necessary preparation. Preparation may include, for example,
+   * reading the header of a media file to determine the track format and duration.
+   * <p>
+   * The renderer should not hold scarce or expensive system resources (e.g. media decoders) and
+   * should not be actively buffering media data when in this state.
+   */
+  protected static final int STATE_PREPARED = 1;
+  /**
+   * The renderer is enabled. It should either be ready to be started, or be actively working
+   * towards this state (e.g. a renderer in this state will typically hold any resources that it
+   * requires, such as media decoders, and will have buffered or be buffering any media data that
+   * is required to start playback).
+   */
+  protected static final int STATE_ENABLED = 2;
+  /**
+   * The renderer is started. Calls to {@link #doSomeWork(long, long)} should cause the media to be
+   * rendered.
+   */
+  protected static final int STATE_STARTED = 3;
+
+  /**
+   * Represents an unknown time or duration. Equal to {@link C#UNKNOWN_TIME_US}.
+   */
+  public static final long UNKNOWN_TIME_US = C.UNKNOWN_TIME_US; // -1
+  /**
+   * Represents a time or duration that should match the duration of the longest track whose
+   * duration is known.
+   */
+  public static final long MATCH_LONGEST_US = -2;
+  /**
+   * Represents the time of the end of the track.
+   */
+  public static final long END_OF_TRACK_US = -3;
+
+  private int state;
+
+  /**
+   * A time source renderer is a renderer that, when started, advances its own playback position.
+   * This means that {@link #getCurrentPositionUs()} will return increasing positions independently
+   * to increasing values being passed to {@link #doSomeWork(long, long)}. A player may have at most
+   * one time source renderer. If provided, the player will use such a renderer as its source of
+   * time during playback.
+   * <p>
+   * This method may be called when the renderer is in any state.
+   *
+   * @return True if the renderer should be considered a time source. False otherwise.
+   */
+  protected boolean isTimeSource() {
+    return false;
+  }
+
+  /**
+   * Returns the current state of the renderer.
+   *
+   * @return The current state (one of the STATE_* constants).
+   */
+  protected final int getState() {
+    return state;
+  }
+
+  /**
+   * Prepares the renderer. This method is non-blocking, and hence it may be necessary to call it
+   * more than once in order to transition the renderer into the prepared state.
+   *
+   * @return The current state (one of the STATE_* constants), for convenience.
+   */
+  /* package */ final int prepare() throws ExoPlaybackException {
+    Assertions.checkState(state == TrackRenderer.STATE_UNPREPARED);
+    state = doPrepare();
+    Assertions.checkState(state == TrackRenderer.STATE_UNPREPARED ||
+        state == TrackRenderer.STATE_PREPARED ||
+        state == TrackRenderer.STATE_IGNORE);
+    return state;
+  }
+
+  /**
+   * Invoked to make progress when the renderer is in the {@link #STATE_UNPREPARED} state. This
+   * method will be called repeatedly until a value other than {@link #STATE_UNPREPARED} is
+   * returned.
+   * <p>
+   * This method should return quickly, and should not block if the renderer is currently unable to
+   * make any useful progress.
+   *
+   * @return The new state of the renderer. One of {@link #STATE_UNPREPARED},
+   *     {@link #STATE_PREPARED} and {@link #STATE_IGNORE}.
+   * @throws ExoPlaybackException If an error occurs.
+   */
+  protected abstract int doPrepare() throws ExoPlaybackException;
+
+  /**
+   * Enable the renderer.
+   *
+   * @param positionUs The player's current position.
+   * @param joining Whether this renderer is being enabled to join an ongoing playback. If true
+   *     then {@link #start} must be called immediately after this method returns (unless a
+   *     {@link ExoPlaybackException} is thrown).
+   */
+  /* package */ final void enable(long positionUs, boolean joining) throws ExoPlaybackException {
+    Assertions.checkState(state == TrackRenderer.STATE_PREPARED);
+    state = TrackRenderer.STATE_ENABLED;
+    onEnabled(positionUs, joining);
+  }
+
+  /**
+   * Called when the renderer is enabled.
+   * <p>
+   * The default implementation is a no-op.
+   *
+   * @param positionUs The player's current position.
+   * @param joining Whether this renderer is being enabled to join an ongoing playback. If true
+   *     then {@link #onStarted} is guaranteed to be called immediately after this method returns
+   *     (unless a {@link ExoPlaybackException} is thrown).
+   * @throws ExoPlaybackException If an error occurs.
+   */
+  protected void onEnabled(long positionUs, boolean joining) throws ExoPlaybackException {
+    // Do nothing.
+  }
+
+  /**
+   * Starts the renderer, meaning that calls to {@link #doSomeWork(long, long)} will cause the
+   * track to be rendered.
+   */
+  /* package */ final void start() throws ExoPlaybackException {
+    Assertions.checkState(state == TrackRenderer.STATE_ENABLED);
+    state = TrackRenderer.STATE_STARTED;
+    onStarted();
+  }
+
+  /**
+   * Called when the renderer is started.
+   * <p>
+   * The default implementation is a no-op.
+   *
+   * @throws ExoPlaybackException If an error occurs.
+   */
+  protected void onStarted() throws ExoPlaybackException {
+    // Do nothing.
+  }
+
+  /**
+   * Stops the renderer.
+   */
+  /* package */ final void stop() throws ExoPlaybackException {
+    Assertions.checkState(state == TrackRenderer.STATE_STARTED);
+    state = TrackRenderer.STATE_ENABLED;
+    onStopped();
+  }
+
+  /**
+   * Called when the renderer is stopped.
+   * <p>
+   * The default implementation is a no-op.
+   *
+   * @throws ExoPlaybackException If an error occurs.
+   */
+  protected void onStopped() throws ExoPlaybackException {
+    // Do nothing.
+  }
+
+  /**
+   * Disable the renderer.
+   */
+  /* package */ final void disable() throws ExoPlaybackException {
+    Assertions.checkState(state == TrackRenderer.STATE_ENABLED);
+    state = TrackRenderer.STATE_PREPARED;
+    onDisabled();
+  }
+
+  /**
+   * Called when the renderer is disabled.
+   * <p>
+   * The default implementation is a no-op.
+   *
+   * @throws ExoPlaybackException If an error occurs.
+   */
+  protected void onDisabled() throws ExoPlaybackException {
+    // Do nothing.
+  }
+
+  /**
+   * Releases the renderer.
+   */
+  /* package */ final void release() throws ExoPlaybackException {
+    Assertions.checkState(state != TrackRenderer.STATE_ENABLED
+        && state != TrackRenderer.STATE_STARTED
+        && state != TrackRenderer.STATE_RELEASED);
+    state = TrackRenderer.STATE_RELEASED;
+    onReleased();
+  }
+
+  /**
+   * Called when the renderer is released.
+   * <p>
+   * The default implementation is a no-op.
+   *
+   * @throws ExoPlaybackException If an error occurs.
+   */
+  protected void onReleased() throws ExoPlaybackException {
+    // Do nothing.
+  }
+
+  /**
+   * Whether the renderer is ready for the {@link ExoPlayer} instance to transition to
+   * {@link ExoPlayer#STATE_ENDED}. The player will make this transition as soon as {@code true} is
+   * returned by all of its {@link TrackRenderer}s.
+   * <p>
+   * This method may be called when the renderer is in the following states:
+   * {@link #STATE_ENABLED}, {@link #STATE_STARTED}
+   *
+   * @return Whether the renderer is ready for the player to transition to the ended state.
+   */
+  protected abstract boolean isEnded();
+
+  /**
+   * Whether the renderer is able to immediately render media from the current position.
+   * <p>
+   * If the renderer is in the {@link #STATE_STARTED} state then returning true indicates that the
+   * renderer has everything that it needs to continue playback. Returning false indicates that
+   * the player should pause until the renderer is ready.
+   * <p>
+   * If the renderer is in the {@link #STATE_ENABLED} state then returning true indicates that the
+   * renderer is ready for playback to be started. Returning false indicates that it is not.
+   * <p>
+   * This method may be called when the renderer is in the following states:
+   * {@link #STATE_ENABLED}, {@link #STATE_STARTED}
+   *
+   * @return True if the renderer is ready to render media. False otherwise.
+   */
+  protected abstract boolean isReady();
+
+  /**
+   * Invoked to make progress when the renderer is in the {@link #STATE_ENABLED} or
+   * {@link #STATE_STARTED} states.
+   * <p>
+   * If the renderer's state is {@link #STATE_STARTED}, then repeated calls to this method should
+   * cause the media track to be rendered. If the state is {@link #STATE_ENABLED}, then repeated
+   * calls should make progress towards getting the renderer into a position where it is ready to
+   * render the track.
+   * <p>
+   * This method should return quickly, and should not block if the renderer is currently unable to
+   * make any useful progress.
+   * <p>
+   * This method may be called when the renderer is in the following states:
+   * {@link #STATE_ENABLED}, {@link #STATE_STARTED}
+   *
+   * @param positionUs The current media time in microseconds, measured at the start of the
+   *     current iteration of the rendering loop.
+   * @param elapsedRealtimeUs {@link android.os.SystemClock#elapsedRealtime()} in microseconds,
+   *     measured at the start of the current iteration of the rendering loop.
+   * @throws ExoPlaybackException If an error occurs.
+   */
+  protected abstract void doSomeWork(long positionUs, long elapsedRealtimeUs)
+      throws ExoPlaybackException;
+
+  /**
+   * Returns the duration of the media being rendered.
+   * <p>
+   * This method may be called when the renderer is in the following states:
+   * {@link #STATE_PREPARED}, {@link #STATE_ENABLED}, {@link #STATE_STARTED}
+   *
+   * @return The duration of the track in microseconds, or {@link #MATCH_LONGEST_US} if
+   *     the track's duration should match that of the longest track whose duration is known, or
+   *     or {@link #UNKNOWN_TIME_US} if the duration is not known.
+   */
+  protected abstract long getDurationUs();
+
+  /**
+   * Returns the current playback position.
+   * <p>
+   * This method may be called when the renderer is in the following states:
+   * {@link #STATE_ENABLED}, {@link #STATE_STARTED}
+   *
+   * @return The current playback position in microseconds.
+   */
+  protected abstract long getCurrentPositionUs();
+
+  /**
+   * Returns an estimate of the absolute position in microseconds up to which data is buffered.
+   * <p>
+   * This method may be called when the renderer is in the following states:
+   * {@link #STATE_ENABLED}, {@link #STATE_STARTED}
+   *
+   * @return An estimate of the absolute position in microseconds up to which data is buffered,
+   *     or {@link #END_OF_TRACK_US} if the track is fully buffered, or {@link #UNKNOWN_TIME_US} if
+   *     no estimate is available.
+   */
+  protected abstract long getBufferedPositionUs();
+
+  /**
+   * Seeks to a specified time in the track.
+   * <p>
+   * This method may be called when the renderer is in the following states:
+   * {@link #STATE_ENABLED}
+   *
+   * @param positionUs The desired playback position in microseconds.
+   * @throws ExoPlaybackException If an error occurs.
+   */
+  protected abstract void seekTo(long positionUs) throws ExoPlaybackException;
+
+  @Override
+  public void handleMessage(int what, Object object) throws ExoPlaybackException {
+    // Do nothing.
+  }
+
+}

+ 77 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/VideoSurfaceView.java

@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.SurfaceView;
+
+/**
+ * A SurfaceView that resizes itself to match a specified aspect ratio.
+ */
+public class VideoSurfaceView extends SurfaceView {
+
+  /**
+   * The surface view will not resize itself if the fractional difference between its default
+   * aspect ratio and the aspect ratio of the video falls below this threshold.
+   * <p>
+   * This tolerance is useful for fullscreen playbacks, since it ensures that the surface will
+   * occupy the whole of the screen when playing content that has the same (or virtually the same)
+   * aspect ratio as the device. This typically reduces the number of view layers that need to be
+   * composited by the underlying system, which can help to reduce power consumption.
+   */
+  private static final float MAX_ASPECT_RATIO_DEFORMATION_PERCENT = 0.01f;
+
+  private float videoAspectRatio;
+
+  public VideoSurfaceView(Context context) {
+    super(context);
+  }
+
+  public VideoSurfaceView(Context context, AttributeSet attrs) {
+    super(context, attrs);
+  }
+
+  /**
+   * Set the aspect ratio that this {@link VideoSurfaceView} should satisfy.
+   *
+   * @param widthHeightRatio The width to height ratio.
+   */
+  public void setVideoWidthHeightRatio(float widthHeightRatio) {
+    if (this.videoAspectRatio != widthHeightRatio) {
+      this.videoAspectRatio = widthHeightRatio;
+      requestLayout();
+    }
+  }
+
+  @Override
+  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+    int width = getMeasuredWidth();
+    int height = getMeasuredHeight();
+    if (videoAspectRatio != 0) {
+      float viewAspectRatio = (float) width / height;
+      float aspectDeformation = videoAspectRatio / viewAspectRatio - 1;
+      if (aspectDeformation > MAX_ASPECT_RATIO_DEFORMATION_PERCENT) {
+        height = (int) (width / videoAspectRatio);
+      } else if (aspectDeformation < -MAX_ASPECT_RATIO_DEFORMATION_PERCENT) {
+        width = (int) (height * videoAspectRatio);
+      }
+    }
+    setMeasuredDimension(width, height);
+  }
+
+}

+ 97 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/audio/AudioCapabilities.java

@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.audio;
+
+import com.google.android.exoplayer.util.Util;
+
+import android.annotation.TargetApi;
+import android.media.AudioFormat;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Represents the set of audio formats a device is capable of playing back.
+ */
+@TargetApi(21)
+public final class AudioCapabilities {
+
+  private final Set<Integer> supportedEncodings;
+  private final int maxChannelCount;
+
+  /**
+   * Constructs new audio capabilities based on a set of supported encodings and a maximum channel
+   * count.
+   *
+   * @param supportedEncodings Supported audio encodings from {@link android.media.AudioFormat}'s
+   *     {@code ENCODING_*} constants.
+   * @param maxChannelCount The maximum number of audio channels that can be played simultaneously.
+   */
+  public AudioCapabilities(int[] supportedEncodings, int maxChannelCount) {
+    this.supportedEncodings = new HashSet<Integer>();
+    if (supportedEncodings != null) {
+      for (int i : supportedEncodings) {
+        this.supportedEncodings.add(i);
+      }
+    }
+    this.maxChannelCount = maxChannelCount;
+  }
+
+  /** Returns whether the device supports playback of AC-3. */
+  public boolean supportsAc3() {
+    return Util.SDK_INT >= 21 && supportedEncodings.contains(AudioFormat.ENCODING_AC3);
+  }
+
+  /** Returns whether the device supports playback of enhanced AC-3. */
+  public boolean supportsEAc3() {
+    return Util.SDK_INT >= 21 && supportedEncodings.contains(AudioFormat.ENCODING_E_AC3);
+  }
+
+  /** Returns whether the device supports playback of 16-bit PCM. */
+  public boolean supportsPcm() {
+    return supportedEncodings.contains(AudioFormat.ENCODING_PCM_16BIT);
+  }
+
+  /** Returns the maximum number of channels the device can play at the same time. */
+  public int getMaxChannelCount() {
+    return maxChannelCount;
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (this == other) {
+      return true;
+    }
+    if (!(other instanceof AudioCapabilities)) {
+      return false;
+    }
+    AudioCapabilities audioCapabilities = (AudioCapabilities) other;
+    return supportedEncodings.equals(audioCapabilities.supportedEncodings)
+        && maxChannelCount == audioCapabilities.maxChannelCount;
+  }
+
+  @Override
+  public int hashCode() {
+    return maxChannelCount + 31 * supportedEncodings.hashCode();
+  }
+
+  @Override
+  public String toString() {
+    return "AudioCapabilities[maxChannelCount=" + maxChannelCount
+        + ", supportedEncodings=" + supportedEncodings + "]";
+  }
+
+}

+ 101 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/audio/AudioCapabilitiesReceiver.java

@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.audio;
+
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.Util;
+
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+
+/**
+ * Notifies a listener when the audio playback capabilities change. Call {@link #register} to start
+ * receiving notifications, and {@link #unregister} to stop.
+ */
+public final class AudioCapabilitiesReceiver {
+
+  /** Listener notified when audio capabilities change. */
+  public interface Listener {
+
+    /** Called when the audio capabilities change. */
+    void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities);
+
+  }
+
+  /** Default to stereo PCM on SDK < 21 and when HDMI is unplugged. */
+  private static final AudioCapabilities DEFAULT_AUDIO_CAPABILITIES =
+      new AudioCapabilities(new int[] {AudioFormat.ENCODING_PCM_16BIT}, 2);
+
+  private final Context context;
+  private final Listener listener;
+  private final BroadcastReceiver receiver;
+
+  /**
+   * Constructs a new audio capabilities receiver.
+   *
+   * @param context Application context for registering to receive broadcasts.
+   * @param listener Listener to notify when audio capabilities change.
+   */
+  public AudioCapabilitiesReceiver(Context context, Listener listener) {
+    this.context = Assertions.checkNotNull(context);
+    this.listener = Assertions.checkNotNull(listener);
+    this.receiver = Util.SDK_INT >= 21 ? new HdmiAudioPlugBroadcastReceiver() : null;
+  }
+
+  /**
+   * Registers to notify the listener when audio capabilities change. The listener will immediately
+   * receive the current audio capabilities. It is important to call {@link #unregister} so that
+   * the listener can be garbage collected.
+   */
+  @TargetApi(21)
+  public void register() {
+    if (receiver != null) {
+      context.registerReceiver(receiver, new IntentFilter(AudioManager.ACTION_HDMI_AUDIO_PLUG));
+    }
+
+    listener.onAudioCapabilitiesChanged(DEFAULT_AUDIO_CAPABILITIES);
+  }
+
+  /** Unregisters to stop notifying the listener when audio capabilities change. */
+  public void unregister() {
+    if (receiver != null) {
+      context.unregisterReceiver(receiver);
+    }
+  }
+
+  @TargetApi(21)
+  private final class HdmiAudioPlugBroadcastReceiver extends BroadcastReceiver {
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+      String action = intent.getAction();
+      if (!action.equals(AudioManager.ACTION_HDMI_AUDIO_PLUG)) {
+        return;
+      }
+
+      listener.onAudioCapabilitiesChanged(
+          new AudioCapabilities(intent.getIntArrayExtra(AudioManager.EXTRA_ENCODINGS),
+              intent.getIntExtra(AudioManager.EXTRA_MAX_CHANNEL_COUNT, 0)));
+    }
+
+  }
+
+}

+ 799 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java

@@ -0,0 +1,799 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.audio;
+
+import com.google.android.exoplayer.C;
+import com.google.android.exoplayer.util.Util;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.media.AudioFormat;
+import android.media.AudioManager;
+import android.media.AudioTimestamp;
+import android.media.MediaFormat;
+import android.os.ConditionVariable;
+import android.util.Log;
+
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+
+/**
+ * Plays audio data. The implementation delegates to an {@link android.media.AudioTrack} and handles
+ * playback position smoothing, non-blocking writes and reconfiguration.
+ *
+ * <p>If {@link #isInitialized} returns {@code false}, the instance can be {@link #initialize}d.
+ * After initialization, start playback by calling {@link #play}.
+ *
+ * <p>Call {@link #handleBuffer} to write data for playback.
+ *
+ * <p>Call {@link #handleDiscontinuity} when a buffer is skipped.
+ *
+ * <p>Call {@link #reconfigure} when the output format changes.
+ *
+ * <p>Call {@link #reset} to free resources. It is safe to re-{@link #initialize} the instance.
+ */
+@TargetApi(16)
+public final class AudioTrack {
+
+  /**
+   * Thrown when a failure occurs instantiating an {@link android.media.AudioTrack}.
+   */
+  public static class InitializationException extends Exception {
+
+    /** The state as reported by {@link android.media.AudioTrack#getState()}. */
+    public final int audioTrackState;
+
+    public InitializationException(
+        int audioTrackState, int sampleRate, int channelConfig, int bufferSize) {
+      super("AudioTrack init failed: " + audioTrackState + ", Config(" + sampleRate + ", "
+          + channelConfig + ", " + bufferSize + ")");
+      this.audioTrackState = audioTrackState;
+    }
+
+  }
+
+  /**
+   * Thrown when a failure occurs writing to an {@link android.media.AudioTrack}.
+   */
+  public static class WriteException extends Exception {
+
+    /** The value returned from {@link android.media.AudioTrack#write(byte[], int, int)}. */
+    public final int errorCode;
+
+    public WriteException(int errorCode) {
+      super("AudioTrack write failed: " + errorCode);
+      this.errorCode = errorCode;
+    }
+
+  }
+
+  /** Returned in the result of {@link #handleBuffer} if the buffer was discontinuous. */
+  public static final int RESULT_POSITION_DISCONTINUITY = 1;
+  /** Returned in the result of {@link #handleBuffer} if the buffer can be released. */
+  public static final int RESULT_BUFFER_CONSUMED = 2;
+
+  /** Represents an unset {@link android.media.AudioTrack} session identifier. */
+  public static final int SESSION_ID_NOT_SET = 0;
+
+  /** Returned by {@link #getCurrentPositionUs} when the position is not set. */
+  public static final long CURRENT_POSITION_NOT_SET = Long.MIN_VALUE;
+
+  /** A minimum length for the {@link android.media.AudioTrack} buffer, in microseconds. */
+  private static final long MIN_BUFFER_DURATION_US = 250000;
+  /** A maximum length for the {@link android.media.AudioTrack} buffer, in microseconds. */
+  private static final long MAX_BUFFER_DURATION_US = 750000;
+  /**
+   * A multiplication factor to apply to the minimum buffer size requested by the underlying
+   * {@link android.media.AudioTrack}.
+   */
+  private static final int BUFFER_MULTIPLICATION_FACTOR = 4;
+
+  private static final String TAG = "AudioTrack";
+
+  /**
+   * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more
+   * than this amount.
+   *
+   * <p>This is a fail safe that should not be required on correctly functioning devices.
+   */
+  private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 5 * C.MICROS_PER_SECOND;
+
+  /**
+   * AudioTrack latencies are deemed impossibly large if they are greater than this amount.
+   *
+   * <p>This is a fail safe that should not be required on correctly functioning devices.
+   */
+  private static final long MAX_LATENCY_US = 5 * C.MICROS_PER_SECOND;
+
+  /** Value for ac3Bitrate before the bitrate has been calculated. */
+  private static final int UNKNOWN_AC3_BITRATE = 0;
+
+  private static final int START_NOT_SET = 0;
+  private static final int START_IN_SYNC = 1;
+  private static final int START_NEED_SYNC = 2;
+
+  private static final int MAX_PLAYHEAD_OFFSET_COUNT = 10;
+  private static final int MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US = 30000;
+  private static final int MIN_TIMESTAMP_SAMPLE_INTERVAL_US = 500000;
+
+  private final ConditionVariable releasingConditionVariable;
+  private final long[] playheadOffsets;
+
+  private android.media.AudioTrack audioTrack;
+  private AudioTrackUtil audioTrackUtil;
+  private int sampleRate;
+  private int channelConfig;
+  private int encoding;
+  private int frameSize;
+  private int minBufferSize;
+  private int bufferSize;
+
+  private int nextPlayheadOffsetIndex;
+  private int playheadOffsetCount;
+  private long smoothedPlayheadOffsetUs;
+  private long lastPlayheadSampleTimeUs;
+  private boolean audioTimestampSet;
+  private long lastTimestampSampleTimeUs;
+
+  private Method getLatencyMethod;
+  private long submittedBytes;
+  private int startMediaTimeState;
+  private long startMediaTimeUs;
+  private long resumeSystemTimeUs;
+  private long latencyUs;
+  private float volume;
+
+  private byte[] temporaryBuffer;
+  private int temporaryBufferOffset;
+  private int temporaryBufferSize;
+
+  private boolean isAc3;
+
+  /** Bitrate measured in kilobits per second, if {@link #isAc3} is true. */
+  private int ac3Bitrate;
+
+  public AudioTrack() {
+    releasingConditionVariable = new ConditionVariable(true);
+    if (Util.SDK_INT >= 18) {
+      try {
+        getLatencyMethod =
+            android.media.AudioTrack.class.getMethod("getLatency", (Class<?>[]) null);
+      } catch (NoSuchMethodException e) {
+        // There's no guarantee this method exists. Do nothing.
+      }
+    }
+    playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT];
+    volume = 1.0f;
+    startMediaTimeState = START_NOT_SET;
+  }
+
+  /**
+   * Returns whether the audio track has been successfully initialized via {@link #initialize} and
+   * not yet {@link #reset}.
+   */
+  public boolean isInitialized() {
+    return audioTrack != null;
+  }
+
+  /**
+   * Returns the playback position in the stream starting at zero, in microseconds, or
+   * {@link #CURRENT_POSITION_NOT_SET} if it is not yet available.
+   *
+   * <p>If the device supports it, the method uses the playback timestamp from
+   * {@link android.media.AudioTrack#getTimestamp}. Otherwise, it derives a smoothed position by
+   * sampling the {@link android.media.AudioTrack}'s frame position.
+   *
+   * @param sourceEnded Specify {@code true} if no more input buffers will be provided.
+   * @return The playback position relative to the start of playback, in microseconds.
+   */
+  public long getCurrentPositionUs(boolean sourceEnded) {
+    if (!hasCurrentPositionUs()) {
+      return CURRENT_POSITION_NOT_SET;
+    }
+
+    if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PLAYING) {
+      maybeSampleSyncParams();
+    }
+
+    long systemClockUs = System.nanoTime() / 1000;
+    long currentPositionUs;
+    if (audioTimestampSet) {
+      // How long ago in the past the audio timestamp is (negative if it's in the future).
+      long presentationDiff = systemClockUs - (audioTrackUtil.getTimestampNanoTime() / 1000);
+      long framesDiff = durationUsToFrames(presentationDiff);
+      // The position of the frame that's currently being presented.
+      long currentFramePosition = audioTrackUtil.getTimestampFramePosition() + framesDiff;
+      currentPositionUs = framesToDurationUs(currentFramePosition) + startMediaTimeUs;
+    } else {
+      if (playheadOffsetCount == 0) {
+        // The AudioTrack has started, but we don't have any samples to compute a smoothed position.
+        currentPositionUs = audioTrackUtil.getPlaybackHeadPositionUs() + startMediaTimeUs;
+      } else {
+        // getPlayheadPositionUs() only has a granularity of ~20ms, so we base the position off the
+        // system clock (and a smoothed offset between it and the playhead position) so as to
+        // prevent jitter in the reported positions.
+        currentPositionUs = systemClockUs + smoothedPlayheadOffsetUs + startMediaTimeUs;
+      }
+      if (!sourceEnded) {
+        currentPositionUs -= latencyUs;
+      }
+    }
+
+    return currentPositionUs;
+  }
+
+  /**
+   * Initializes the audio track for writing new buffers using {@link #handleBuffer}.
+   *
+   * @return The audio track session identifier.
+   */
+  public int initialize() throws InitializationException {
+    return initialize(SESSION_ID_NOT_SET);
+  }
+
+  /**
+   * Initializes the audio track for writing new buffers using {@link #handleBuffer}.
+   *
+   * @param sessionId Audio track session identifier to re-use, or {@link #SESSION_ID_NOT_SET} to
+   *     create a new one.
+   * @return The new (or re-used) session identifier.
+   */
+  public int initialize(int sessionId) throws InitializationException {
+    // If we're asynchronously releasing a previous audio track then we block until it has been
+    // released. This guarantees that we cannot end up in a state where we have multiple audio
+    // track instances. Without this guarantee it would be possible, in extreme cases, to exhaust
+    // the shared memory that's available for audio track buffers. This would in turn cause the
+    // initialization of the audio track to fail.
+    releasingConditionVariable.block();
+
+    if (sessionId == SESSION_ID_NOT_SET) {
+      audioTrack = new android.media.AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
+          channelConfig, encoding, bufferSize, android.media.AudioTrack.MODE_STREAM);
+    } else {
+      // Re-attach to the same audio session.
+      audioTrack = new android.media.AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
+          channelConfig, encoding, bufferSize, android.media.AudioTrack.MODE_STREAM, sessionId);
+    }
+
+    checkAudioTrackInitialized();
+    if (Util.SDK_INT >= 19) {
+      audioTrackUtil = new AudioTrackUtilV19(audioTrack);
+    } else {
+      audioTrackUtil = new AudioTrackUtil(audioTrack);
+    }
+    setVolume(volume);
+    return audioTrack.getAudioSessionId();
+  }
+
+  /**
+   * Reconfigures the audio track to play back media in {@code format}. The encoding is assumed to
+   * be {@link AudioFormat#ENCODING_PCM_16BIT}.
+   */
+  public void reconfigure(MediaFormat format) {
+    reconfigure(format, AudioFormat.ENCODING_PCM_16BIT, 0);
+  }
+
+  /**
+   * Reconfigures the audio track to play back media in {@code format}. Buffers passed to
+   * {@link #handleBuffer} must using the specified {@code encoding}, which should be a constant
+   * from {@link AudioFormat}.
+   *
+   * @param format Specifies the channel count and sample rate to play back.
+   * @param encoding The format in which audio is represented.
+   * @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to use a
+   *     size inferred from the format.
+   */
+  @SuppressLint("InlinedApi")
+  public void reconfigure(MediaFormat format, int encoding, int specifiedBufferSize) {
+    int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
+    int channelConfig;
+    switch (channelCount) {
+      case 1:
+        channelConfig = AudioFormat.CHANNEL_OUT_MONO;
+        break;
+      case 2:
+        channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
+        break;
+      case 6:
+        channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
+        break;
+      case 8:
+        channelConfig = AudioFormat.CHANNEL_OUT_7POINT1;
+        break;
+      default:
+        throw new IllegalArgumentException("Unsupported channel count: " + channelCount);
+    }
+
+    int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
+
+    // TODO: Does channelConfig determine channelCount?
+    boolean isAc3 = encoding == AudioFormat.ENCODING_AC3 || encoding == AudioFormat.ENCODING_E_AC3;
+    if (isInitialized() && this.sampleRate == sampleRate && this.channelConfig == channelConfig
+        && !this.isAc3 && !isAc3) {
+      // We already have an existing audio track with the correct sample rate and channel config.
+      return;
+    }
+
+    reset();
+
+    this.encoding = encoding;
+    this.sampleRate = sampleRate;
+    this.channelConfig = channelConfig;
+    this.isAc3 = isAc3;
+    ac3Bitrate = UNKNOWN_AC3_BITRATE; // Calculated on receiving the first buffer if isAc3 is true.
+    frameSize = 2 * channelCount; // 2 bytes per 16 bit sample * number of channels.
+    minBufferSize = android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, encoding);
+
+    if (specifiedBufferSize != 0) {
+      bufferSize = specifiedBufferSize;
+    } else {
+      int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR;
+      int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * frameSize;
+      int maxAppBufferSize = (int) Math.max(minBufferSize,
+          durationUsToFrames(MAX_BUFFER_DURATION_US) * frameSize);
+      bufferSize = multipliedBufferSize < minAppBufferSize ? minAppBufferSize
+          : multipliedBufferSize > maxAppBufferSize ? maxAppBufferSize
+          : multipliedBufferSize;
+    }
+  }
+
+  /** Starts/resumes playing audio if the audio track has been initialized. */
+  public void play() {
+    if (isInitialized()) {
+      resumeSystemTimeUs = System.nanoTime() / 1000;
+      audioTrack.play();
+    }
+  }
+
+  /** Signals to the audio track that the next buffer is discontinuous with the previous buffer. */
+  public void handleDiscontinuity() {
+    // Force resynchronization after a skipped buffer.
+    if (startMediaTimeState == START_IN_SYNC) {
+      startMediaTimeState = START_NEED_SYNC;
+    }
+  }
+
+  /**
+   * Attempts to write {@code size} bytes from {@code buffer} at {@code offset} to the audio track.
+   * Returns a bit field containing {@link #RESULT_BUFFER_CONSUMED} if the buffer can be released
+   * (due to having been written), and {@link #RESULT_POSITION_DISCONTINUITY} if the buffer was
+   * discontinuous with previously written data.
+   *
+   * @param buffer The buffer containing audio data to play back.
+   * @param offset The offset in the buffer from which to consume data.
+   * @param size The number of bytes to consume from {@code buffer}.
+   * @param presentationTimeUs Presentation timestamp of the next buffer in microseconds.
+   * @return A bit field with {@link #RESULT_BUFFER_CONSUMED} if the buffer can be released, and
+   *     {@link #RESULT_POSITION_DISCONTINUITY} if the buffer was not contiguous with previously
+   *     written data.
+   * @throws WriteException If an error occurs writing the audio data.
+   */
+  public int handleBuffer(ByteBuffer buffer, int offset, int size, long presentationTimeUs)
+      throws WriteException {
+    if (size == 0) {
+      return RESULT_BUFFER_CONSUMED;
+    }
+
+    int result = 0;
+    if (temporaryBufferSize == 0) {
+      if (isAc3 && ac3Bitrate == UNKNOWN_AC3_BITRATE) {
+        // Each AC-3 buffer contains 1536 frames of audio, so the AudioTrack playback position
+        // advances by 1536 per buffer (32 ms at 48 kHz). Calculate the bitrate in kbit/s.
+        int unscaledAc3Bitrate = size * 8 * sampleRate;
+        int divisor = 1000 * 1536;
+        ac3Bitrate = (unscaledAc3Bitrate + divisor / 2) / divisor;
+      }
+
+      // This is the first time we've seen this {@code buffer}.
+      // Note: presentationTimeUs corresponds to the end of the sample, not the start.
+      long bufferStartTime = presentationTimeUs - framesToDurationUs(bytesToFrames(size));
+      if (startMediaTimeUs == START_NOT_SET) {
+        startMediaTimeUs = Math.max(0, bufferStartTime);
+        startMediaTimeState = START_IN_SYNC;
+      } else {
+        // Sanity check that bufferStartTime is consistent with the expected value.
+        long expectedBufferStartTime = startMediaTimeUs
+            + framesToDurationUs(bytesToFrames(submittedBytes));
+        if (startMediaTimeState == START_IN_SYNC
+            && Math.abs(expectedBufferStartTime - bufferStartTime) > 200000) {
+          Log.e(TAG, "Discontinuity detected [expected " + expectedBufferStartTime + ", got "
+              + bufferStartTime + "]");
+          startMediaTimeState = START_NEED_SYNC;
+        }
+        if (startMediaTimeState == START_NEED_SYNC) {
+          // Adjust startMediaTimeUs to be consistent with the current buffer's start time and the
+          // number of bytes submitted.
+          startMediaTimeUs += (bufferStartTime - expectedBufferStartTime);
+          startMediaTimeState = START_IN_SYNC;
+          result |= RESULT_POSITION_DISCONTINUITY;
+        }
+      }
+    }
+
+    if (temporaryBufferSize == 0) {
+      temporaryBufferSize = size;
+      buffer.position(offset);
+      if (Util.SDK_INT < 21) {
+        // Copy {@code buffer} into {@code temporaryBuffer}.
+        if (temporaryBuffer == null || temporaryBuffer.length < size) {
+          temporaryBuffer = new byte[size];
+        }
+        buffer.get(temporaryBuffer, 0, size);
+        temporaryBufferOffset = 0;
+      }
+    }
+
+    int bytesWritten = 0;
+    if (Util.SDK_INT < 21) {
+      // Work out how many bytes we can write without the risk of blocking.
+      int bytesPending =
+          (int) (submittedBytes - (audioTrackUtil.getPlaybackHeadPosition() * frameSize));
+      int bytesToWrite = bufferSize - bytesPending;
+      if (bytesToWrite > 0) {
+        bytesToWrite = Math.min(temporaryBufferSize, bytesToWrite);
+        bytesWritten = audioTrack.write(temporaryBuffer, temporaryBufferOffset, bytesToWrite);
+        if (bytesWritten >= 0) {
+          temporaryBufferOffset += bytesWritten;
+        }
+      }
+    } else {
+      bytesWritten = writeNonBlockingV21(audioTrack, buffer, temporaryBufferSize);
+    }
+
+    if (bytesWritten < 0) {
+      throw new WriteException(bytesWritten);
+    }
+
+    temporaryBufferSize -= bytesWritten;
+    submittedBytes += bytesWritten;
+    if (temporaryBufferSize == 0) {
+      result |= RESULT_BUFFER_CONSUMED;
+    }
+    return result;
+  }
+
+  @TargetApi(21)
+  private static int writeNonBlockingV21(
+      android.media.AudioTrack audioTrack, ByteBuffer buffer, int size) {
+    return audioTrack.write(buffer, size, android.media.AudioTrack.WRITE_NON_BLOCKING);
+  }
+
+  /** Returns whether the audio track has more data pending that will be played back. */
+  public boolean hasPendingData() {
+    return isInitialized()
+        && bytesToFrames(submittedBytes) > audioTrackUtil.getPlaybackHeadPosition();
+  }
+
+  /** Returns whether enough data has been supplied via {@link #handleBuffer} to begin playback. */
+  public boolean hasEnoughDataToBeginPlayback() {
+    // The value of minBufferSize can be slightly less than what's actually required for playback
+    // to start, hence the multiplication factor.
+    return submittedBytes > (minBufferSize * 3) / 2;
+  }
+
+  /** Sets the playback volume. */
+  public void setVolume(float volume) {
+    this.volume = volume;
+    if (isInitialized()) {
+      if (Util.SDK_INT >= 21) {
+        setVolumeV21(audioTrack, volume);
+      } else {
+        setVolumeV3(audioTrack, volume);
+      }
+    }
+  }
+
+  @TargetApi(21)
+  private static void setVolumeV21(android.media.AudioTrack audioTrack, float volume) {
+    audioTrack.setVolume(volume);
+  }
+
+  @SuppressWarnings("deprecation")
+  private static void setVolumeV3(android.media.AudioTrack audioTrack, float volume) {
+    audioTrack.setStereoVolume(volume, volume);
+  }
+
+  /** Pauses playback. */
+  public void pause() {
+    if (isInitialized()) {
+      resetSyncParams();
+      audioTrack.pause();
+    }
+  }
+
+  /**
+   * Releases resources associated with this instance asynchronously. Calling {@link #initialize}
+   * will block until the audio track has been released, so it is safe to initialize immediately
+   * after resetting.
+   */
+  public void reset() {
+    if (isInitialized()) {
+      submittedBytes = 0;
+      temporaryBufferSize = 0;
+      startMediaTimeUs = START_NOT_SET;
+      resetSyncParams();
+      int playState = audioTrack.getPlayState();
+      if (playState == android.media.AudioTrack.PLAYSTATE_PLAYING) {
+        audioTrack.pause();
+      }
+      // AudioTrack.release can take some time, so we call it on a background thread.
+      final android.media.AudioTrack toRelease = audioTrack;
+      audioTrack = null;
+      audioTrackUtil = null;
+      releasingConditionVariable.close();
+      new Thread() {
+        @Override
+        public void run() {
+          try {
+            toRelease.release();
+          } finally {
+            releasingConditionVariable.open();
+          }
+        }
+      }.start();
+    }
+  }
+
+  /** Returns whether {@link #getCurrentPositionUs} can return the current playback position. */
+  private boolean hasCurrentPositionUs() {
+    return isInitialized() && startMediaTimeUs != START_NOT_SET;
+  }
+
+  /** Updates the audio track latency and playback position parameters. */
+  private void maybeSampleSyncParams() {
+    long playbackPositionUs = audioTrackUtil.getPlaybackHeadPositionUs();
+    if (playbackPositionUs == 0) {
+      // The AudioTrack hasn't output anything yet.
+      return;
+    }
+    long systemClockUs = System.nanoTime() / 1000;
+    if (systemClockUs - lastPlayheadSampleTimeUs >= MIN_PLAYHEAD_OFFSET_SAMPLE_INTERVAL_US) {
+      // Take a new sample and update the smoothed offset between the system clock and the playhead.
+      playheadOffsets[nextPlayheadOffsetIndex] = playbackPositionUs - systemClockUs;
+      nextPlayheadOffsetIndex = (nextPlayheadOffsetIndex + 1) % MAX_PLAYHEAD_OFFSET_COUNT;
+      if (playheadOffsetCount < MAX_PLAYHEAD_OFFSET_COUNT) {
+        playheadOffsetCount++;
+      }
+      lastPlayheadSampleTimeUs = systemClockUs;
+      smoothedPlayheadOffsetUs = 0;
+      for (int i = 0; i < playheadOffsetCount; i++) {
+        smoothedPlayheadOffsetUs += playheadOffsets[i] / playheadOffsetCount;
+      }
+    }
+
+    if (systemClockUs - lastTimestampSampleTimeUs >= MIN_TIMESTAMP_SAMPLE_INTERVAL_US) {
+      audioTimestampSet = audioTrackUtil.updateTimestamp();
+      if (audioTimestampSet) {
+        // Perform sanity checks on the timestamp.
+        long audioTimestampUs = audioTrackUtil.getTimestampNanoTime() / 1000;
+        long audioTimestampFramePosition = audioTrackUtil.getTimestampFramePosition();
+        if (audioTimestampUs < resumeSystemTimeUs) {
+          // The timestamp corresponds to a time before the track was most recently resumed.
+          audioTimestampSet = false;
+        } else if (Math.abs(audioTimestampUs - systemClockUs) > MAX_AUDIO_TIMESTAMP_OFFSET_US) {
+          // The timestamp time base is probably wrong.
+          audioTimestampSet = false;
+          Log.w(TAG, "Spurious audio timestamp (system clock mismatch): "
+              + audioTimestampFramePosition + ", " + audioTimestampUs + ", " + systemClockUs + ", "
+              + playbackPositionUs);
+        } else if (Math.abs(framesToDurationUs(audioTimestampFramePosition) - playbackPositionUs)
+            > MAX_AUDIO_TIMESTAMP_OFFSET_US) {
+          // The timestamp frame position is probably wrong.
+          audioTimestampSet = false;
+          Log.w(TAG, "Spurious audio timestamp (frame position mismatch): "
+              + audioTimestampFramePosition + ", " + audioTimestampUs + ", " + systemClockUs + ", "
+              + playbackPositionUs);
+        }
+      }
+      if (getLatencyMethod != null) {
+        try {
+          // Compute the audio track latency, excluding the latency due to the buffer (leaving
+          // latency due to the mixer and audio hardware driver).
+          latencyUs = (Integer) getLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L
+              - framesToDurationUs(bytesToFrames(bufferSize));
+          // Sanity check that the latency is non-negative.
+          latencyUs = Math.max(latencyUs, 0);
+          // Sanity check that the latency isn't too large.
+          if (latencyUs > MAX_LATENCY_US) {
+            Log.w(TAG, "Ignoring impossibly large audio latency: " + latencyUs);
+            latencyUs = 0;
+          }
+        } catch (Exception e) {
+          // The method existed, but doesn't work. Don't try again.
+          getLatencyMethod = null;
+        }
+      }
+      lastTimestampSampleTimeUs = systemClockUs;
+    }
+  }
+
+  /**
+   * Checks that {@link #audioTrack} has been successfully initialized. If it has then calling this
+   * method is a no-op. If it hasn't then {@link #audioTrack} is released and set to null, and an
+   * exception is thrown.
+   *
+   * @throws InitializationException If {@link #audioTrack} has not been successfully initialized.
+   */
+  private void checkAudioTrackInitialized() throws InitializationException {
+    int state = audioTrack.getState();
+    if (state == android.media.AudioTrack.STATE_INITIALIZED) {
+      return;
+    }
+    // The track is not successfully initialized. Release and null the track.
+    try {
+      audioTrack.release();
+    } catch (Exception e) {
+      // The track has already failed to initialize, so it wouldn't be that surprising if release
+      // were to fail too. Swallow the exception.
+    } finally {
+      audioTrack = null;
+    }
+
+    throw new InitializationException(state, sampleRate, channelConfig, bufferSize);
+  }
+
+  private long bytesToFrames(long byteCount) {
+    if (isAc3) {
+      return
+          ac3Bitrate == UNKNOWN_AC3_BITRATE ? 0L : byteCount * 8 * sampleRate / (1000 * ac3Bitrate);
+    } else {
+      return byteCount / frameSize;
+    }
+  }
+
+  private long framesToDurationUs(long frameCount) {
+    return (frameCount * C.MICROS_PER_SECOND) / sampleRate;
+  }
+
+  private long durationUsToFrames(long durationUs) {
+    return (durationUs * sampleRate) / C.MICROS_PER_SECOND;
+  }
+
+  private void resetSyncParams() {
+    smoothedPlayheadOffsetUs = 0;
+    playheadOffsetCount = 0;
+    nextPlayheadOffsetIndex = 0;
+    lastPlayheadSampleTimeUs = 0;
+    audioTimestampSet = false;
+    lastTimestampSampleTimeUs = 0;
+  }
+
+  /**
+   * Wraps an {@link android.media.AudioTrack} to expose useful utility methods.
+   */
+  private static class AudioTrackUtil {
+
+    protected final android.media.AudioTrack audioTrack;
+    private final int sampleRate;
+
+    private long lastRawPlaybackHeadPosition;
+    private long rawPlaybackHeadWrapCount;
+
+    public AudioTrackUtil(android.media.AudioTrack audioTrack) {
+      this.audioTrack = audioTrack;
+      this.sampleRate = audioTrack.getSampleRate();
+    }
+
+    /**
+     * {@link android.media.AudioTrack#getPlaybackHeadPosition()} returns a value intended to be
+     * interpreted as an unsigned 32 bit integer, which also wraps around periodically. This method
+     * returns the playback head position as a long that will only wrap around if the value exceeds
+     * {@link Long#MAX_VALUE} (which in practice will never happen).
+     *
+     * @return {@link android.media.AudioTrack#getPlaybackHeadPosition()} of {@link #audioTrack}
+     *     expressed as a long.
+     */
+    public long getPlaybackHeadPosition() {
+      long rawPlaybackHeadPosition = 0xFFFFFFFFL & audioTrack.getPlaybackHeadPosition();
+      if (lastRawPlaybackHeadPosition > rawPlaybackHeadPosition) {
+        // The value must have wrapped around.
+        rawPlaybackHeadWrapCount++;
+      }
+      lastRawPlaybackHeadPosition = rawPlaybackHeadPosition;
+      return rawPlaybackHeadPosition + (rawPlaybackHeadWrapCount << 32);
+    }
+
+    /**
+     * Returns {@link #getPlaybackHeadPosition()} expressed as microseconds.
+     */
+    public long getPlaybackHeadPositionUs() {
+      return (getPlaybackHeadPosition() * C.MICROS_PER_SECOND) / sampleRate;
+    }
+
+    /**
+     * Updates the values returned by {@link #getTimestampNanoTime()} and
+     * {@link #getTimestampFramePosition()}.
+     *
+     * @return True if the timestamp values were updated. False otherwise.
+     */
+    public boolean updateTimestamp() {
+      return false;
+    }
+
+    /**
+     * Returns the {@link android.media.AudioTimestamp#nanoTime} obtained during the most recent
+     * call to {@link #updateTimestamp()} that returned true.
+     *
+     * @return The nanoTime obtained during the most recent call to {@link #updateTimestamp()} that
+     *     returned true.
+     * @throws UnsupportedOperationException If the implementation does not support audio timestamp
+     *     queries. {@link #updateTimestamp()} will always return false in this case.
+     */
+    public long getTimestampNanoTime() {
+      // Should never be called if updateTimestamp() returned false.
+      throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Returns the {@link android.media.AudioTimestamp#framePosition} obtained during the most
+     * recent call to {@link #updateTimestamp()} that returned true. The value is adjusted so that
+     * wrap around only occurs if the value exceeds {@link Long#MAX_VALUE} (which in practice will
+     * never happen).
+     *
+     * @return The framePosition obtained during the most recent call to {@link #updateTimestamp()}
+     *     that returned true.
+     * @throws UnsupportedOperationException If the implementation does not support audio timestamp
+     *     queries. {@link #updateTimestamp()} will always return false in this case.
+     */
+    public long getTimestampFramePosition() {
+      // Should never be called if updateTimestamp() returned false.
+      throw new UnsupportedOperationException();
+    }
+
+  }
+
+  @TargetApi(19)
+  private static class AudioTrackUtilV19 extends AudioTrackUtil {
+
+    private final AudioTimestamp audioTimestamp;
+
+    private long rawTimestampFramePositionWrapCount;
+    private long lastRawTimestampFramePosition;
+    private long lastTimestampFramePosition;
+
+    public AudioTrackUtilV19(android.media.AudioTrack audioTrack) {
+      super(audioTrack);
+      audioTimestamp = new AudioTimestamp();
+    }
+
+    @Override
+    public boolean updateTimestamp() {
+      boolean updated = audioTrack.getTimestamp(audioTimestamp);
+      if (updated) {
+        long rawFramePosition = audioTimestamp.framePosition;
+        if (lastRawTimestampFramePosition > rawFramePosition) {
+          // The value must have wrapped around.
+          rawTimestampFramePositionWrapCount++;
+        }
+        lastRawTimestampFramePosition = rawFramePosition;
+        lastTimestampFramePosition = rawFramePosition + (rawTimestampFramePositionWrapCount << 32);
+      }
+      return updated;
+    }
+
+    @Override
+    public long getTimestampNanoTime() {
+      return audioTimestamp.nanoTime;
+    }
+
+    @Override
+    public long getTimestampFramePosition() {
+      return lastTimestampFramePosition;
+    }
+
+  }
+
+}

+ 185 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/Chunk.java

@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.chunk;
+
+import com.google.android.exoplayer.C;
+import com.google.android.exoplayer.upstream.Allocation;
+import com.google.android.exoplayer.upstream.Allocator;
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.upstream.DataSourceStream;
+import com.google.android.exoplayer.upstream.DataSpec;
+import com.google.android.exoplayer.upstream.Loader.Loadable;
+import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.TraceUtil;
+
+import java.io.IOException;
+
+/**
+ * An abstract base class for {@link Loadable} implementations that load chunks of data required
+ * for the playback of streams.
+ */
+public abstract class Chunk implements Loadable {
+
+  /**
+   * The format associated with the data being loaded.
+   */
+  // TODO: Consider removing this and pushing it down into MediaChunk instead.
+  public final Format format;
+  /**
+   * The reason for a {@link ChunkSource} having generated this chunk. For reporting only. Possible
+   * values for this variable are defined by the specific {@link ChunkSource} implementations.
+   */
+  public final int trigger;
+
+  private final DataSource dataSource;
+  private final DataSpec dataSpec;
+
+  private DataSourceStream dataSourceStream;
+
+  /**
+   * @param dataSource The source from which the data should be loaded.
+   * @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed
+   *     {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then
+   *     the length resolved by {@code dataSource.open(dataSpec)} must not exceed
+   *     {@link Integer#MAX_VALUE}.
+   * @param format See {@link #format}.
+   * @param trigger See {@link #trigger}.
+   */
+  public Chunk(DataSource dataSource, DataSpec dataSpec, Format format, int trigger) {
+    Assertions.checkState(dataSpec.length <= Integer.MAX_VALUE);
+    this.dataSource = Assertions.checkNotNull(dataSource);
+    this.dataSpec = Assertions.checkNotNull(dataSpec);
+    this.format = Assertions.checkNotNull(format);
+    this.trigger = trigger;
+  }
+
+  /**
+   * Initializes the {@link Chunk}.
+   *
+   * @param allocator An {@link Allocator} from which the {@link Allocation} needed to contain the
+   *     data can be obtained.
+   */
+  public final void init(Allocator allocator) {
+    Assertions.checkState(dataSourceStream == null);
+    dataSourceStream = new DataSourceStream(dataSource, dataSpec, allocator);
+  }
+
+  /**
+   * Releases the {@link Chunk}, releasing any backing {@link Allocation}s.
+   */
+  public final void release() {
+    if (dataSourceStream != null) {
+      dataSourceStream.close();
+      dataSourceStream = null;
+    }
+  }
+
+  /**
+   * Gets the length of the chunk in bytes.
+   *
+   * @return The length of the chunk in bytes, or {@link C#LENGTH_UNBOUNDED} if the length has yet
+   *     to be determined.
+   */
+  public final long getLength() {
+    return dataSourceStream.getLength();
+  }
+
+  /**
+   * Whether the whole of the data has been consumed.
+   *
+   * @return True if the whole of the data has been consumed. False otherwise.
+   */
+  public final boolean isReadFinished() {
+    return dataSourceStream.isEndOfStream();
+  }
+
+  /**
+   * Whether the whole of the chunk has been loaded.
+   *
+   * @return True if the whole of the chunk has been loaded. False otherwise.
+   */
+  public final boolean isLoadFinished() {
+    return dataSourceStream.isLoadFinished();
+  }
+
+  /**
+   * Gets the number of bytes that have been loaded.
+   *
+   * @return The number of bytes that have been loaded.
+   */
+  public final long bytesLoaded() {
+    return dataSourceStream.getLoadPosition();
+  }
+
+  /**
+   * Causes loaded data to be consumed.
+   *
+   * @throws IOException If an error occurs consuming the loaded data.
+   */
+  public final void consume() throws IOException {
+    Assertions.checkState(dataSourceStream != null);
+    consumeStream(dataSourceStream);
+  }
+
+  /**
+   * Invoked by {@link #consume()}. Implementations may override this method if they wish to
+   * consume the loaded data at this point.
+   * <p>
+   * The default implementation is a no-op.
+   *
+   * @param stream The stream of loaded data.
+   * @throws IOException If an error occurs consuming the loaded data.
+   */
+  protected void consumeStream(NonBlockingInputStream stream) throws IOException {
+    // Do nothing.
+  }
+
+  protected final NonBlockingInputStream getNonBlockingInputStream() {
+    return dataSourceStream;
+  }
+
+  protected final void resetReadPosition() {
+    if (dataSourceStream != null) {
+      dataSourceStream.resetReadPosition();
+    } else {
+      // We haven't been initialized yet, so the read position must already be 0.
+    }
+  }
+
+  // Loadable implementation
+
+  @Override
+  public final void cancelLoad() {
+    dataSourceStream.cancelLoad();
+  }
+
+  @Override
+  public final boolean isLoadCanceled() {
+    return dataSourceStream.isLoadCanceled();
+  }
+
+  @Override
+  public final void load() throws IOException, InterruptedException {
+    TraceUtil.beginSection("chunkLoad");
+    try {
+      dataSourceStream.load();
+    } finally {
+      TraceUtil.endSection();
+    }
+  }
+
+}

+ 34 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/ChunkOperationHolder.java

@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.chunk;
+
+/**
+ * Holds a chunk operation, which consists of a {@link Chunk} to load together with the number of
+ * {@link MediaChunk}s that should be retained on the queue.
+ */
+public final class ChunkOperationHolder {
+
+  /**
+   * The number of {@link MediaChunk}s to retain in a queue.
+   */
+  public int queueSize;
+
+  /**
+   * The chunk.
+   */
+  public Chunk chunk;
+
+}

+ 832 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/ChunkSampleSource.java

@@ -0,0 +1,832 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.chunk;
+
+import com.google.android.exoplayer.C;
+import com.google.android.exoplayer.LoadControl;
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.MediaFormatHolder;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.SampleSource;
+import com.google.android.exoplayer.TrackInfo;
+import com.google.android.exoplayer.TrackRenderer;
+import com.google.android.exoplayer.upstream.Loader;
+import com.google.android.exoplayer.upstream.Loader.Loadable;
+import com.google.android.exoplayer.util.Assertions;
+
+import android.os.Handler;
+import android.os.SystemClock;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A {@link SampleSource} that loads media in {@link Chunk}s, which are themselves obtained from a
+ * {@link ChunkSource}.
+ */
+public class ChunkSampleSource implements SampleSource, Loader.Callback {
+
+  /**
+   * Interface definition for a callback to be notified of {@link ChunkSampleSource} events.
+   */
+  public interface EventListener {
+
+    /**
+     * Invoked when an upstream load is started.
+     *
+     * @param sourceId The id of the reporting {@link SampleSource}.
+     * @param formatId The format id.
+     * @param trigger A trigger for the format selection, as specified by the {@link ChunkSource}.
+     * @param isInitialization Whether the load is for format initialization data.
+     * @param mediaStartTimeMs The media time of the start of the data being loaded, or -1 if this
+     *     load is for initialization data.
+     * @param mediaEndTimeMs The media time of the end of the data being loaded, or -1 if this
+     *     load is for initialization data.
+     * @param length The length of the data being loaded in bytes, or {@link C#LENGTH_UNBOUNDED} if
+     *     the length of the data has not yet been determined.
+     */
+    void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization,
+        int mediaStartTimeMs, int mediaEndTimeMs, long length);
+
+    /**
+     * Invoked when the current load operation completes.
+     *
+     * @param sourceId The id of the reporting {@link SampleSource}.
+     * @param bytesLoaded The number of bytes that were loaded.
+     */
+    void onLoadCompleted(int sourceId, long bytesLoaded);
+
+    /**
+     * Invoked when the current upstream load operation is canceled.
+     *
+     * @param sourceId The id of the reporting {@link SampleSource}.
+     * @param bytesLoaded The number of bytes that were loaded prior to the cancellation.
+     */
+    void onLoadCanceled(int sourceId, long bytesLoaded);
+
+    /**
+     * Invoked when data is removed from the back of the buffer, typically so that it can be
+     * re-buffered using a different representation.
+     *
+     * @param sourceId The id of the reporting {@link SampleSource}.
+     * @param mediaStartTimeMs The media time of the start of the discarded data.
+     * @param mediaEndTimeMs The media time of the end of the discarded data.
+     * @param bytesDiscarded The length of the data being discarded in bytes.
+     */
+    void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
+        long bytesDiscarded);
+
+    /**
+     * Invoked when an error occurs loading media data.
+     *
+     * @param sourceId The id of the reporting {@link SampleSource}.
+     * @param e The cause of the failure.
+     */
+    void onUpstreamError(int sourceId, IOException e);
+
+    /**
+     * Invoked when an error occurs consuming loaded data.
+     *
+     * @param sourceId The id of the reporting {@link SampleSource}.
+     * @param e The cause of the failure.
+     */
+    void onConsumptionError(int sourceId, IOException e);
+
+    /**
+     * Invoked when data is removed from the front of the buffer, typically due to a seek or
+     * because the data has been consumed.
+     *
+     * @param sourceId The id of the reporting {@link SampleSource}.
+     * @param mediaStartTimeMs The media time of the start of the discarded data.
+     * @param mediaEndTimeMs The media time of the end of the discarded data.
+     * @param bytesDiscarded The length of the data being discarded in bytes.
+     */
+    void onDownstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs,
+        long bytesDiscarded);
+
+    /**
+     * Invoked when the downstream format changes (i.e. when the format being supplied to the
+     * caller of {@link SampleSource#readData} changes).
+     *
+     * @param sourceId The id of the reporting {@link SampleSource}.
+     * @param formatId The format id.
+     * @param trigger The trigger specified in the corresponding upstream load, as specified by the
+     *     {@link ChunkSource}.
+     * @param mediaTimeMs The media time at which the change occurred.
+     */
+    void onDownstreamFormatChanged(int sourceId, String formatId, int trigger, int mediaTimeMs);
+
+  }
+
+  /**
+   * The default minimum number of times to retry loading data prior to failing.
+   */
+  public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3;
+
+  private static final int STATE_UNPREPARED = 0;
+  private static final int STATE_PREPARED = 1;
+  private static final int STATE_ENABLED = 2;
+
+  private static final int NO_RESET_PENDING = -1;
+
+  private final int eventSourceId;
+  private final LoadControl loadControl;
+  private final ChunkSource chunkSource;
+  private final ChunkOperationHolder currentLoadableHolder;
+  private final LinkedList<MediaChunk> mediaChunks;
+  private final List<MediaChunk> readOnlyMediaChunks;
+  private final int bufferSizeContribution;
+  private final boolean frameAccurateSeeking;
+  private final Handler eventHandler;
+  private final EventListener eventListener;
+  private final int minLoadableRetryCount;
+
+  private int state;
+  private long downstreamPositionUs;
+  private long lastSeekPositionUs;
+  private long pendingResetPositionUs;
+  private long lastPerformedBufferOperation;
+  private boolean pendingDiscontinuity;
+
+  private Loader loader;
+  private IOException currentLoadableException;
+  private boolean currentLoadableExceptionFatal;
+  private int currentLoadableExceptionCount;
+  private long currentLoadableExceptionTimestamp;
+
+  private MediaFormat downstreamMediaFormat;
+  private volatile Format downstreamFormat;
+
+  public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
+      int bufferSizeContribution, boolean frameAccurateSeeking) {
+    this(chunkSource, loadControl, bufferSizeContribution, frameAccurateSeeking, null, null, 0);
+  }
+
+  public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
+      int bufferSizeContribution, boolean frameAccurateSeeking, Handler eventHandler,
+      EventListener eventListener, int eventSourceId) {
+    this(chunkSource, loadControl, bufferSizeContribution, frameAccurateSeeking, eventHandler,
+        eventListener, eventSourceId, DEFAULT_MIN_LOADABLE_RETRY_COUNT);
+  }
+
+  public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl,
+      int bufferSizeContribution, boolean frameAccurateSeeking, Handler eventHandler,
+      EventListener eventListener, int eventSourceId, int minLoadableRetryCount) {
+    this.chunkSource = chunkSource;
+    this.loadControl = loadControl;
+    this.bufferSizeContribution = bufferSizeContribution;
+    this.frameAccurateSeeking = frameAccurateSeeking;
+    this.eventHandler = eventHandler;
+    this.eventListener = eventListener;
+    this.eventSourceId = eventSourceId;
+    this.minLoadableRetryCount = minLoadableRetryCount;
+    currentLoadableHolder = new ChunkOperationHolder();
+    mediaChunks = new LinkedList<MediaChunk>();
+    readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks);
+    state = STATE_UNPREPARED;
+  }
+
+  /**
+   * Exposes the current downstream format for debugging purposes. Can be called from any thread.
+   *
+   * @return The current downstream format.
+   */
+  public Format getFormat() {
+    return downstreamFormat;
+  }
+
+  @Override
+  public boolean prepare() {
+    Assertions.checkState(state == STATE_UNPREPARED);
+    loader = new Loader("Loader:" + chunkSource.getTrackInfo().mimeType);
+    state = STATE_PREPARED;
+    return true;
+  }
+
+  @Override
+  public int getTrackCount() {
+    Assertions.checkState(state != STATE_UNPREPARED);
+    return 1;
+  }
+
+  @Override
+  public TrackInfo getTrackInfo(int track) {
+    Assertions.checkState(state != STATE_UNPREPARED);
+    Assertions.checkState(track == 0);
+    return chunkSource.getTrackInfo();
+  }
+
+  @Override
+  public void enable(int track, long positionUs) {
+    Assertions.checkState(state == STATE_PREPARED);
+    Assertions.checkState(track == 0);
+    state = STATE_ENABLED;
+    chunkSource.enable();
+    loadControl.register(this, bufferSizeContribution);
+    downstreamFormat = null;
+    downstreamMediaFormat = null;
+    downstreamPositionUs = positionUs;
+    lastSeekPositionUs = positionUs;
+    restartFrom(positionUs);
+  }
+
+  @Override
+  public void disable(int track) {
+    Assertions.checkState(state == STATE_ENABLED);
+    Assertions.checkState(track == 0);
+    pendingDiscontinuity = false;
+    state = STATE_PREPARED;
+    try {
+      chunkSource.disable(mediaChunks);
+    } finally {
+      loadControl.unregister(this);
+      if (loader.isLoading()) {
+        loader.cancelLoading();
+      } else {
+        clearMediaChunks();
+        clearCurrentLoadable();
+        loadControl.trimAllocator();
+      }
+    }
+  }
+
+  @Override
+  public boolean continueBuffering(long positionUs) throws IOException {
+    Assertions.checkState(state == STATE_ENABLED);
+    downstreamPositionUs = positionUs;
+    chunkSource.continueBuffering(positionUs);
+    updateLoadControl();
+
+    boolean haveSamples = false;
+    if (isPendingReset() || mediaChunks.isEmpty()) {
+      // No sample available.
+    } else if (mediaChunks.getFirst().sampleAvailable()) {
+      // There's a sample available to be read from the current chunk.
+      haveSamples = true;
+    } else {
+      // It may be the case that the current chunk has been fully read but not yet discarded and
+      // that the next chunk has an available sample. Return true if so, otherwise false.
+      haveSamples = mediaChunks.size() > 1 && mediaChunks.get(1).sampleAvailable();
+    }
+
+    if (!haveSamples) {
+      maybeThrowLoadableException();
+    }
+    return haveSamples;
+  }
+
+  @Override
+  public int readData(int track, long positionUs, MediaFormatHolder formatHolder,
+      SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException {
+    Assertions.checkState(state == STATE_ENABLED);
+    Assertions.checkState(track == 0);
+
+    if (pendingDiscontinuity) {
+      pendingDiscontinuity = false;
+      return DISCONTINUITY_READ;
+    }
+
+    if (onlyReadDiscontinuity) {
+      return NOTHING_READ;
+    }
+
+    downstreamPositionUs = positionUs;
+    if (isPendingReset()) {
+      maybeThrowLoadableException();
+      IOException chunkSourceException = chunkSource.getError();
+      if (chunkSourceException != null) {
+        throw chunkSourceException;
+      }
+      return NOTHING_READ;
+    }
+
+    MediaChunk mediaChunk = mediaChunks.getFirst();
+    if (mediaChunk.isReadFinished()) {
+      // We've read all of the samples from the current media chunk.
+      if (mediaChunks.size() > 1) {
+        discardDownstreamMediaChunk();
+        mediaChunk = mediaChunks.getFirst();
+        mediaChunk.seekToStart();
+        return readData(track, positionUs, formatHolder, sampleHolder, false);
+      } else if (mediaChunk.isLastChunk()) {
+        return END_OF_STREAM;
+      }
+      IOException chunkSourceException = chunkSource.getError();
+      if (chunkSourceException != null) {
+        throw chunkSourceException;
+      }
+      return NOTHING_READ;
+    }
+
+    if (downstreamFormat == null || !downstreamFormat.equals(mediaChunk.format)) {
+      notifyDownstreamFormatChanged(mediaChunk.format.id, mediaChunk.trigger,
+          mediaChunk.startTimeUs);
+      downstreamFormat = mediaChunk.format;
+    }
+
+    if (!mediaChunk.prepare()) {
+      if (currentLoadableException != null) {
+        throw currentLoadableException;
+      }
+      return NOTHING_READ;
+    }
+
+    MediaFormat mediaFormat = mediaChunk.getMediaFormat();
+    if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormat, true)) {
+      chunkSource.getMaxVideoDimensions(mediaFormat);
+      formatHolder.format = mediaFormat;
+      formatHolder.drmInitData = mediaChunk.getPsshInfo();
+      downstreamMediaFormat = mediaFormat;
+      return FORMAT_READ;
+    }
+
+    if (mediaChunk.read(sampleHolder)) {
+      sampleHolder.decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs;
+      onSampleRead(mediaChunk, sampleHolder);
+      return SAMPLE_READ;
+    } else {
+      maybeThrowLoadableException();
+      return NOTHING_READ;
+    }
+  }
+
+  @Override
+  public void seekToUs(long positionUs) {
+    Assertions.checkState(state == STATE_ENABLED);
+    downstreamPositionUs = positionUs;
+    lastSeekPositionUs = positionUs;
+    if (pendingResetPositionUs == positionUs) {
+      return;
+    }
+
+    MediaChunk mediaChunk = getMediaChunk(positionUs);
+    if (mediaChunk == null) {
+      restartFrom(positionUs);
+      pendingDiscontinuity = true;
+    } else {
+      pendingDiscontinuity |= mediaChunk.seekTo(positionUs, mediaChunk == mediaChunks.getFirst());
+      discardDownstreamMediaChunks(mediaChunk);
+      updateLoadControl();
+    }
+  }
+
+  private void maybeThrowLoadableException() throws IOException {
+    if (currentLoadableException != null && (currentLoadableExceptionFatal
+        || currentLoadableExceptionCount > minLoadableRetryCount)) {
+      throw currentLoadableException;
+    }
+  }
+
+  private MediaChunk getMediaChunk(long positionUs) {
+    Iterator<MediaChunk> mediaChunkIterator = mediaChunks.iterator();
+    while (mediaChunkIterator.hasNext()) {
+      MediaChunk mediaChunk = mediaChunkIterator.next();
+      if (positionUs < mediaChunk.startTimeUs) {
+        return null;
+      } else if (mediaChunk.isLastChunk() || positionUs < mediaChunk.endTimeUs) {
+        return mediaChunk;
+      }
+    }
+    return null;
+  }
+
+  @Override
+  public long getBufferedPositionUs() {
+    Assertions.checkState(state == STATE_ENABLED);
+    if (isPendingReset()) {
+      return pendingResetPositionUs;
+    }
+    MediaChunk mediaChunk = mediaChunks.getLast();
+    Chunk currentLoadable = currentLoadableHolder.chunk;
+    if (currentLoadable != null && mediaChunk == currentLoadable) {
+      // Linearly interpolate partially-fetched chunk times.
+      long chunkLength = mediaChunk.getLength();
+      if (chunkLength != C.LENGTH_UNBOUNDED && chunkLength != 0) {
+        return mediaChunk.startTimeUs + ((mediaChunk.endTimeUs - mediaChunk.startTimeUs) *
+            mediaChunk.bytesLoaded()) / chunkLength;
+      } else {
+        return mediaChunk.startTimeUs;
+      }
+    } else if (mediaChunk.isLastChunk()) {
+      return TrackRenderer.END_OF_TRACK_US;
+    } else {
+      return mediaChunk.endTimeUs;
+    }
+  }
+
+  @Override
+  public void release() {
+    Assertions.checkState(state != STATE_ENABLED);
+    if (loader != null) {
+      loader.release();
+      loader = null;
+    }
+    state = STATE_UNPREPARED;
+  }
+
+  @Override
+  public void onLoadCompleted(Loadable loadable) {
+    Chunk currentLoadable = currentLoadableHolder.chunk;
+    notifyLoadCompleted(currentLoadable.bytesLoaded());
+    try {
+      currentLoadable.consume();
+    } catch (IOException e) {
+      currentLoadableException = e;
+      currentLoadableExceptionCount++;
+      currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
+      currentLoadableExceptionFatal = true;
+      notifyConsumptionError(e);
+    } finally {
+      if (!isMediaChunk(currentLoadable)) {
+        currentLoadable.release();
+      }
+      if (!currentLoadableExceptionFatal) {
+        clearCurrentLoadable();
+      }
+      updateLoadControl();
+    }
+  }
+
+  @Override
+  public void onLoadCanceled(Loadable loadable) {
+    Chunk currentLoadable = currentLoadableHolder.chunk;
+    notifyLoadCanceled(currentLoadable.bytesLoaded());
+    if (!isMediaChunk(currentLoadable)) {
+      currentLoadable.release();
+    }
+    clearCurrentLoadable();
+    if (state == STATE_ENABLED) {
+      restartFrom(pendingResetPositionUs);
+    } else {
+      clearMediaChunks();
+      loadControl.trimAllocator();
+    }
+  }
+
+  @Override
+  public void onLoadError(Loadable loadable, IOException e) {
+    currentLoadableException = e;
+    currentLoadableExceptionCount++;
+    currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
+    notifyUpstreamError(e);
+    chunkSource.onChunkLoadError(currentLoadableHolder.chunk, e);
+    updateLoadControl();
+  }
+
+  /**
+   * Called when a sample has been read from a {@link MediaChunk}. Can be used to perform any
+   * modifications necessary before the sample is returned.
+   *
+   * @param mediaChunk The MediaChunk the sample was ready from.
+   * @param sampleHolder The sample that has just been read.
+   */
+  protected void onSampleRead(MediaChunk mediaChunk, SampleHolder sampleHolder) {
+    // no-op
+  }
+
+  private void restartFrom(long positionUs) {
+    pendingResetPositionUs = positionUs;
+    if (loader.isLoading()) {
+      loader.cancelLoading();
+    } else {
+      clearMediaChunks();
+      clearCurrentLoadable();
+      updateLoadControl();
+    }
+  }
+
+  private void clearMediaChunks() {
+    discardDownstreamMediaChunks(null);
+  }
+
+  private void clearCurrentLoadable() {
+    currentLoadableHolder.chunk = null;
+    currentLoadableException = null;
+    currentLoadableExceptionCount = 0;
+    currentLoadableExceptionFatal = false;
+  }
+
+  private void updateLoadControl() {
+    if (currentLoadableExceptionFatal) {
+      // We've failed, but we still need to update the control with our current state.
+      loadControl.update(this, downstreamPositionUs, -1, false, true);
+      return;
+    }
+
+    long now = SystemClock.elapsedRealtime();
+    long nextLoadPositionUs = getNextLoadPositionUs();
+    boolean isBackedOff = currentLoadableException != null;
+    boolean loadingOrBackedOff = loader.isLoading() || isBackedOff;
+
+    // If we're not loading or backed off, evaluate the operation if (a) we don't have the next
+    // chunk yet and we're not finished, or (b) if the last evaluation was over 2000ms ago.
+    if (!loadingOrBackedOff && ((currentLoadableHolder.chunk == null && nextLoadPositionUs != -1)
+        || (now - lastPerformedBufferOperation > 2000))) {
+      // Perform the evaluation.
+      lastPerformedBufferOperation = now;
+      currentLoadableHolder.queueSize = readOnlyMediaChunks.size();
+      chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs,
+          downstreamPositionUs, currentLoadableHolder);
+      boolean chunksDiscarded = discardUpstreamMediaChunks(currentLoadableHolder.queueSize);
+      // Update the next load position as appropriate.
+      if (currentLoadableHolder.chunk == null) {
+        // Set loadPosition to -1 to indicate that we don't have anything to load.
+        nextLoadPositionUs = -1;
+      } else if (chunksDiscarded) {
+        // Chunks were discarded, so we need to re-evaluate the load position.
+        nextLoadPositionUs = getNextLoadPositionUs();
+      }
+    }
+
+    // Update the control with our current state, and determine whether we're the next loader.
+    boolean nextLoader = loadControl.update(this, downstreamPositionUs, nextLoadPositionUs,
+        loadingOrBackedOff, false);
+
+    if (isBackedOff) {
+      long elapsedMillis = now - currentLoadableExceptionTimestamp;
+      if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) {
+        resumeFromBackOff();
+      }
+      return;
+    }
+
+    if (!loader.isLoading() && nextLoader) {
+      maybeStartLoading();
+    }
+  }
+
+  /**
+   * Gets the next load time, assuming that the next load starts where the previous chunk ended (or
+   * from the pending reset time, if there is one).
+   */
+  private long getNextLoadPositionUs() {
+    if (isPendingReset()) {
+      return pendingResetPositionUs;
+    } else {
+      MediaChunk lastMediaChunk = mediaChunks.getLast();
+      return lastMediaChunk.nextChunkIndex == -1 ? -1 : lastMediaChunk.endTimeUs;
+    }
+  }
+
+  /**
+   * Resumes loading.
+   * <p>
+   * If the {@link ChunkSource} returns a chunk equivalent to the backed off chunk B, then the
+   * loading of B will be resumed. In all other cases B will be discarded and the new chunk will
+   * be loaded.
+   */
+  private void resumeFromBackOff() {
+    currentLoadableException = null;
+
+    Chunk backedOffChunk = currentLoadableHolder.chunk;
+    if (!isMediaChunk(backedOffChunk)) {
+      currentLoadableHolder.queueSize = readOnlyMediaChunks.size();
+      chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs,
+          downstreamPositionUs, currentLoadableHolder);
+      discardUpstreamMediaChunks(currentLoadableHolder.queueSize);
+      if (currentLoadableHolder.chunk == backedOffChunk) {
+        // Chunk was unchanged. Resume loading.
+        loader.startLoading(backedOffChunk, this);
+      } else {
+        backedOffChunk.release();
+        maybeStartLoading();
+      }
+      return;
+    }
+
+    if (backedOffChunk == mediaChunks.getFirst()) {
+      // We're not able to clear the first media chunk, so we have no choice but to continue
+      // loading it.
+      loader.startLoading(backedOffChunk, this);
+      return;
+    }
+
+    // The current loadable is the last media chunk. Remove it before we invoke the chunk source,
+    // and add it back again afterwards.
+    MediaChunk removedChunk = mediaChunks.removeLast();
+    Assertions.checkState(backedOffChunk == removedChunk);
+    currentLoadableHolder.queueSize = readOnlyMediaChunks.size();
+    chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs, downstreamPositionUs,
+        currentLoadableHolder);
+    mediaChunks.add(removedChunk);
+
+    if (currentLoadableHolder.chunk == backedOffChunk) {
+      // Chunk was unchanged. Resume loading.
+      loader.startLoading(backedOffChunk, this);
+    } else {
+      // This call will remove and release at least one chunk from the end of mediaChunks. Since
+      // the current loadable is the last media chunk, it is guaranteed to be removed.
+      discardUpstreamMediaChunks(currentLoadableHolder.queueSize);
+      clearCurrentLoadable();
+      maybeStartLoading();
+    }
+  }
+
+  private void maybeStartLoading() {
+    Chunk currentLoadable = currentLoadableHolder.chunk;
+    if (currentLoadable == null) {
+      // Nothing to load.
+      return;
+    }
+    currentLoadable.init(loadControl.getAllocator());
+    if (isMediaChunk(currentLoadable)) {
+      MediaChunk mediaChunk = (MediaChunk) currentLoadable;
+      if (isPendingReset()) {
+        mediaChunk.seekTo(pendingResetPositionUs, false);
+        pendingResetPositionUs = NO_RESET_PENDING;
+      }
+      mediaChunks.add(mediaChunk);
+      notifyLoadStarted(mediaChunk.format.id, mediaChunk.trigger, false,
+          mediaChunk.startTimeUs, mediaChunk.endTimeUs, mediaChunk.getLength());
+    } else {
+      notifyLoadStarted(currentLoadable.format.id, currentLoadable.trigger, true, -1, -1,
+          currentLoadable.getLength());
+    }
+    loader.startLoading(currentLoadable, this);
+  }
+
+  /**
+   * Discards downstream media chunks until {@code untilChunk} if found. {@code untilChunk} is not
+   * itself discarded. Null can be passed to discard all media chunks.
+   *
+   * @param untilChunk The first media chunk to keep, or null to discard all media chunks.
+   */
+  private void discardDownstreamMediaChunks(MediaChunk untilChunk) {
+    if (mediaChunks.isEmpty() || untilChunk == mediaChunks.getFirst()) {
+      return;
+    }
+    long totalBytes = 0;
+    long startTimeUs = mediaChunks.getFirst().startTimeUs;
+    long endTimeUs = 0;
+    while (!mediaChunks.isEmpty() && untilChunk != mediaChunks.getFirst()) {
+      MediaChunk removed = mediaChunks.removeFirst();
+      totalBytes += removed.bytesLoaded();
+      endTimeUs = removed.endTimeUs;
+      removed.release();
+    }
+    notifyDownstreamDiscarded(startTimeUs, endTimeUs, totalBytes);
+  }
+
+  /**
+   * Discards the first downstream media chunk.
+   */
+  private void discardDownstreamMediaChunk() {
+    MediaChunk removed = mediaChunks.removeFirst();
+    long totalBytes = removed.bytesLoaded();
+    removed.release();
+    notifyDownstreamDiscarded(removed.startTimeUs, removed.endTimeUs, totalBytes);
+  }
+
+  /**
+   * Discard upstream media chunks until the queue length is equal to the length specified.
+   *
+   * @param queueLength The desired length of the queue.
+   * @return True if chunks were discarded. False otherwise.
+   */
+  private boolean discardUpstreamMediaChunks(int queueLength) {
+    if (mediaChunks.size() <= queueLength) {
+      return false;
+    }
+    long totalBytes = 0;
+    long startTimeUs = 0;
+    long endTimeUs = mediaChunks.getLast().endTimeUs;
+    while (mediaChunks.size() > queueLength) {
+      MediaChunk removed = mediaChunks.removeLast();
+      totalBytes += removed.bytesLoaded();
+      startTimeUs = removed.startTimeUs;
+      removed.release();
+    }
+    notifyUpstreamDiscarded(startTimeUs, endTimeUs, totalBytes);
+    return true;
+  }
+
+  private boolean isMediaChunk(Chunk chunk) {
+    return chunk instanceof MediaChunk;
+  }
+
+  private boolean isPendingReset() {
+    return pendingResetPositionUs != NO_RESET_PENDING;
+  }
+
+  private long getRetryDelayMillis(long errorCount) {
+    return Math.min((errorCount - 1) * 1000, 5000);
+  }
+
+  protected final int usToMs(long timeUs) {
+    return (int) (timeUs / 1000);
+  }
+
+  private void notifyLoadStarted(final String formatId, final int trigger,
+      final boolean isInitialization, final long mediaStartTimeUs, final long mediaEndTimeUs,
+      final long length) {
+    if (eventHandler != null && eventListener != null) {
+      eventHandler.post(new Runnable()  {
+        @Override
+        public void run() {
+          eventListener.onLoadStarted(eventSourceId, formatId, trigger, isInitialization,
+              usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs), length);
+        }
+      });
+    }
+  }
+
+  private void notifyLoadCompleted(final long bytesLoaded) {
+    if (eventHandler != null && eventListener != null) {
+      eventHandler.post(new Runnable()  {
+        @Override
+        public void run() {
+          eventListener.onLoadCompleted(eventSourceId, bytesLoaded);
+        }
+      });
+    }
+  }
+
+  private void notifyLoadCanceled(final long bytesLoaded) {
+    if (eventHandler != null && eventListener != null) {
+      eventHandler.post(new Runnable()  {
+        @Override
+        public void run() {
+          eventListener.onLoadCanceled(eventSourceId, bytesLoaded);
+        }
+      });
+    }
+  }
+
+  private void notifyUpstreamError(final IOException e) {
+    if (eventHandler != null && eventListener != null) {
+      eventHandler.post(new Runnable()  {
+        @Override
+        public void run() {
+          eventListener.onUpstreamError(eventSourceId, e);
+        }
+      });
+    }
+  }
+
+  private void notifyConsumptionError(final IOException e) {
+    if (eventHandler != null && eventListener != null) {
+      eventHandler.post(new Runnable()  {
+        @Override
+        public void run() {
+          eventListener.onConsumptionError(eventSourceId, e);
+        }
+      });
+    }
+  }
+
+  private void notifyUpstreamDiscarded(final long mediaStartTimeUs, final long mediaEndTimeUs,
+      final long totalBytes) {
+    if (eventHandler != null && eventListener != null) {
+      eventHandler.post(new Runnable()  {
+        @Override
+        public void run() {
+          eventListener.onUpstreamDiscarded(eventSourceId, usToMs(mediaStartTimeUs),
+              usToMs(mediaEndTimeUs), totalBytes);
+        }
+      });
+    }
+  }
+
+  private void notifyDownstreamFormatChanged(final String formatId, final int trigger,
+      final long positionUs) {
+    if (eventHandler != null && eventListener != null) {
+      eventHandler.post(new Runnable()  {
+        @Override
+        public void run() {
+          eventListener.onDownstreamFormatChanged(eventSourceId, formatId, trigger,
+              usToMs(positionUs));
+        }
+      });
+    }
+  }
+
+  private void notifyDownstreamDiscarded(final long mediaStartTimeUs, final long mediaEndTimeUs,
+      final long bytesDiscarded) {
+    if (eventHandler != null && eventListener != null) {
+      eventHandler.post(new Runnable()  {
+        @Override
+        public void run() {
+          eventListener.onDownstreamDiscarded(eventSourceId, usToMs(mediaStartTimeUs),
+              usToMs(mediaEndTimeUs), bytesDiscarded);
+        }
+      });
+    }
+  }
+
+}

+ 114 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/ChunkSource.java

@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.chunk;
+
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.TrackInfo;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * A provider of {@link Chunk}s for a {@link ChunkSampleSource} to load.
+ */
+/*
+ * TODO: Share more state between this interface and {@link ChunkSampleSource}. In particular
+ * implementations of this class needs to know about errors, and should be more tightly integrated
+ * into the process of resuming loading of a chunk after an error occurs.
+ */
+public interface ChunkSource {
+
+  /**
+   * Gets information about the track for which this instance provides {@link Chunk}s.
+   * <p>
+   * May be called when the source is disabled or enabled.
+   *
+   * @return Information about the track.
+   */
+  TrackInfo getTrackInfo();
+
+  /**
+   * Adaptive video {@link ChunkSource} implementations must set the maximum video dimensions on
+   * the supplied {@link MediaFormat}. Other implementations do nothing.
+   * <p>
+   * Only called when the source is enabled.
+   *
+   * @param out The {@link MediaFormat} on which the maximum video dimensions should be set.
+   */
+  void getMaxVideoDimensions(MediaFormat out);
+
+  /**
+   * Called when the source is enabled.
+   */
+  void enable();
+
+  /**
+   * Called when the source is disabled.
+   *
+   * @param queue A representation of the currently buffered {@link MediaChunk}s.
+   */
+  void disable(List<? extends MediaChunk> queue);
+
+  /**
+   * Indicates to the source that it should still be checking for updates to the stream.
+   *
+   * @param playbackPositionUs The current playback position.
+   */
+  void continueBuffering(long playbackPositionUs);
+
+  /**
+   * Updates the provided {@link ChunkOperationHolder} to contain the next operation that should
+   * be performed by the calling {@link ChunkSampleSource}.
+   * <p>
+   * The next operation comprises of a possibly shortened queue length (shortened if the
+   * implementation wishes for the caller to discard {@link MediaChunk}s from the queue), together
+   * with the next {@link Chunk} to load. The next chunk may be a {@link MediaChunk} to be added to
+   * the queue, or another {@link Chunk} type (e.g. to load initialization data), or null if the
+   * source is not able to provide a chunk in its current state.
+   *
+   * @param queue A representation of the currently buffered {@link MediaChunk}s.
+   * @param seekPositionUs If the queue is empty, this parameter must specify the seek position. If
+   *     the queue is non-empty then this parameter is ignored.
+   * @param playbackPositionUs The current playback position.
+   * @param out A holder for the next operation, whose {@link ChunkOperationHolder#queueSize} is
+   *     initially equal to the length of the queue, and whose {@link ChunkOperationHolder#chunk} is
+   *     initially equal to null or a {@link Chunk} previously supplied by the {@link ChunkSource}
+   *     that the caller has not yet finished loading. In the latter case the chunk can either be
+   *     replaced or left unchanged. Note that leaving the chunk unchanged is both preferred and
+   *     more efficient than replacing it with a new but identical chunk.
+   */
+  void getChunkOperation(List<? extends MediaChunk> queue, long seekPositionUs,
+      long playbackPositionUs, ChunkOperationHolder out);
+
+  /**
+   * If the {@link ChunkSource} is currently unable to provide chunks through
+   * {@link ChunkSource#getChunkOperation}, then this method returns the underlying cause. Returns
+   * null otherwise.
+   *
+   * @return An {@link IOException}, or null.
+   */
+  IOException getError();
+
+  /**
+   * Invoked when the {@link ChunkSampleSource} encounters an error loading a chunk obtained from
+   * this source.
+   *
+   * @param chunk The chunk whose load encountered the error.
+   * @param e The error.
+   */
+  void onChunkLoadError(Chunk chunk, Exception e);
+
+}

+ 152 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/ContainerMediaChunk.java

@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.chunk;
+
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.ParserException;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.chunk.parser.Extractor;
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.upstream.DataSpec;
+import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+import com.google.android.exoplayer.util.Assertions;
+
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * A {@link MediaChunk} extracted from a container.
+ */
+public final class ContainerMediaChunk extends MediaChunk {
+
+  private final Extractor extractor;
+  private final boolean maybeSelfContained;
+  private final long sampleOffsetUs;
+
+  private boolean prepared;
+  private MediaFormat mediaFormat;
+  private Map<UUID, byte[]> psshInfo;
+
+  /**
+   * @deprecated Use the other constructor, passing null as {@code psshInfo}.
+   */
+  @Deprecated
+  public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
+      int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex,
+      Extractor extractor, boolean maybeSelfContained, long sampleOffsetUs) {
+    this(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex,
+        extractor, null, maybeSelfContained, sampleOffsetUs);
+  }
+
+  /**
+   * @param dataSource A {@link DataSource} for loading the data.
+   * @param dataSpec Defines the data to be loaded.
+   * @param format The format of the stream to which this chunk belongs.
+   * @param trigger The reason for this chunk being selected.
+   * @param startTimeUs The start time of the media contained by the chunk, in microseconds.
+   * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
+   * @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
+   * @param extractor The extractor that will be used to extract the samples.
+   * @param psshInfo Pssh data. May be null if pssh data is present within the stream, meaning it
+   *     can be obtained directly from {@code extractor}, or if no pssh data is required.
+   * @param maybeSelfContained Set to true if this chunk might be self contained, meaning it might
+   *     contain a moov atom defining the media format of the chunk. This parameter can always be
+   *     safely set to true. Setting to false where the chunk is known to not be self contained may
+   *     improve startup latency.
+   * @param sampleOffsetUs An offset to subtract from the sample timestamps parsed by the extractor.
+   */
+  public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
+      int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, Extractor extractor,
+      Map<UUID, byte[]> psshInfo, boolean maybeSelfContained, long sampleOffsetUs) {
+    super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
+    this.extractor = extractor;
+    this.maybeSelfContained = maybeSelfContained;
+    this.sampleOffsetUs = sampleOffsetUs;
+    this.psshInfo = psshInfo;
+  }
+
+  @Override
+  public void seekToStart() {
+    extractor.seekTo(0, false);
+    resetReadPosition();
+  }
+
+  @Override
+  public boolean seekTo(long positionUs, boolean allowNoop) {
+    long seekTimeUs = positionUs + sampleOffsetUs;
+    boolean isDiscontinuous = extractor.seekTo(seekTimeUs, allowNoop);
+    if (isDiscontinuous) {
+      resetReadPosition();
+    }
+    return isDiscontinuous;
+  }
+
+  @Override
+  public boolean prepare() throws ParserException {
+    if (!prepared) {
+      if (maybeSelfContained) {
+        // Read up to the first sample. Once we're there, we know that the extractor must have
+        // parsed a moov atom if the chunk contains one.
+        NonBlockingInputStream inputStream = getNonBlockingInputStream();
+        Assertions.checkState(inputStream != null);
+        int result = extractor.read(inputStream, null);
+        prepared = (result & Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
+      } else {
+        // We know there isn't a moov atom. The extractor must have parsed one from a separate
+        // initialization chunk.
+        prepared = true;
+      }
+      if (prepared) {
+        mediaFormat = extractor.getFormat();
+        Map<UUID, byte[]> extractorPsshInfo = extractor.getPsshInfo();
+        if (extractorPsshInfo != null) {
+          psshInfo = extractorPsshInfo;
+        }
+      }
+    }
+    return prepared;
+  }
+
+  @Override
+  public boolean sampleAvailable() throws ParserException {
+    NonBlockingInputStream inputStream = getNonBlockingInputStream();
+    int result = extractor.read(inputStream, null);
+    return (result & Extractor.RESULT_NEED_SAMPLE_HOLDER) != 0;
+  }
+
+  @Override
+  public boolean read(SampleHolder holder) throws ParserException {
+    NonBlockingInputStream inputStream = getNonBlockingInputStream();
+    Assertions.checkState(inputStream != null);
+    int result = extractor.read(inputStream, holder);
+    boolean sampleRead = (result & Extractor.RESULT_READ_SAMPLE) != 0;
+    if (sampleRead) {
+      holder.timeUs -= sampleOffsetUs;
+    }
+    return sampleRead;
+  }
+
+  @Override
+  public MediaFormat getMediaFormat() {
+    return mediaFormat;
+  }
+
+  @Override
+  public Map<UUID, byte[]> getPsshInfo() {
+    return psshInfo;
+  }
+
+}

+ 170 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/Format.java

@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.chunk;
+
+import com.google.android.exoplayer.util.Assertions;
+
+import java.util.Comparator;
+
+/**
+ * Defines the high level format of a media stream.
+ */
+public class Format {
+
+  /**
+   * Sorts {@link Format} objects in order of decreasing bandwidth.
+   */
+  public static final class DecreasingBandwidthComparator implements Comparator<Format> {
+
+    @Override
+    public int compare(Format a, Format b) {
+      return b.bitrate - a.bitrate;
+    }
+
+  }
+
+  /**
+   * An identifier for the format.
+   */
+  public final String id;
+
+  /**
+   * The mime type of the format.
+   */
+  public final String mimeType;
+
+  /**
+   * The codecs used to decode the format, or {@code null} if they are not specified.
+   */
+  public final String codecs;
+
+  /**
+   * The width of the video in pixels, or -1 for non-video formats.
+   */
+  public final int width;
+
+  /**
+   * The height of the video in pixels, or -1 for non-video formats.
+   */
+  public final int height;
+
+  /**
+   * The number of audio channels, or -1 for non-audio formats.
+   */
+  public final int numChannels;
+
+  /**
+   * The audio sampling rate in Hz, or -1 for non-audio formats.
+   */
+  public final int audioSamplingRate;
+
+  /**
+   * The average bandwidth in bits per second.
+   */
+  public final int bitrate;
+
+  /**
+   * The language of the format. Can be null if unknown.
+   * <p>
+   * The language codes are two-letter lowercase ISO language codes (such as "en") as defined by
+   * ISO 639-1.
+   */
+  public final String language;
+
+  /**
+   * The average bandwidth in bytes per second.
+   *
+   * @deprecated Use {@link #bitrate}. However note that the units of measurement are different.
+   */
+  @Deprecated
+  public final int bandwidth;
+
+  /**
+   * @param id The format identifier.
+   * @param mimeType The format mime type.
+   * @param width The width of the video in pixels, or -1 for non-video formats.
+   * @param height The height of the video in pixels, or -1 for non-video formats.
+   * @param numChannels The number of audio channels, or -1 for non-audio formats.
+   * @param audioSamplingRate The audio sampling rate in Hz, or -1 for non-audio formats.
+   * @param bitrate The average bandwidth of the format in bits per second.
+   */
+  public Format(String id, String mimeType, int width, int height, int numChannels,
+      int audioSamplingRate, int bitrate) {
+    this(id, mimeType, width, height, numChannels, audioSamplingRate, bitrate, null, null);
+  }
+
+  /**
+   * @param id The format identifier.
+   * @param mimeType The format mime type.
+   * @param width The width of the video in pixels, or -1 for non-video formats.
+   * @param height The height of the video in pixels, or -1 for non-video formats.
+   * @param numChannels The number of audio channels, or -1 for non-audio formats.
+   * @param audioSamplingRate The audio sampling rate in Hz, or -1 for non-audio formats.
+   * @param bitrate The average bandwidth of the format in bits per second.
+   * @param language The language of the format.
+   */
+  public Format(String id, String mimeType, int width, int height, int numChannels,
+      int audioSamplingRate, int bitrate, String language) {
+    this(id, mimeType, width, height, numChannels, audioSamplingRate, bitrate, language, null);
+  }
+
+
+  /**
+   * @param id The format identifier.
+   * @param mimeType The format mime type.
+   * @param width The width of the video in pixels, or -1 for non-video formats.
+   * @param height The height of the video in pixels, or -1 for non-video formats.
+   * @param numChannels The number of audio channels, or -1 for non-audio formats.
+   * @param audioSamplingRate The audio sampling rate in Hz, or -1 for non-audio formats.
+   * @param bitrate The average bandwidth of the format in bits per second.
+   * @param language The language of the format.
+   * @param codecs The codecs used to decode the format.
+   */
+  public Format(String id, String mimeType, int width, int height, int numChannels,
+      int audioSamplingRate, int bitrate, String language, String codecs) {
+    this.id = Assertions.checkNotNull(id);
+    this.mimeType = mimeType;
+    this.width = width;
+    this.height = height;
+    this.numChannels = numChannels;
+    this.audioSamplingRate = audioSamplingRate;
+    this.bitrate = bitrate;
+    this.language = language;
+    this.codecs = codecs;
+    this.bandwidth = bitrate / 8;
+  }
+
+  @Override
+  public int hashCode() {
+    return id.hashCode();
+  }
+
+  /**
+   * Implements equality based on {@link #id} only.
+   */
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (obj == null || getClass() != obj.getClass()) {
+      return false;
+    }
+    Format other = (Format) obj;
+    return other.id.equals(id);
+  }
+
+}

+ 301 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/FormatEvaluator.java

@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.chunk;
+
+import com.google.android.exoplayer.upstream.BandwidthMeter;
+
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Selects from a number of available formats during playback.
+ */
+public interface FormatEvaluator {
+
+  /**
+   * The trigger for the initial format selection.
+   */
+  static final int TRIGGER_INITIAL = 0;
+  /**
+   * The trigger for a format selection that was triggered by the user.
+   */
+  static final int TRIGGER_MANUAL = 1;
+  /**
+   * The trigger for an adaptive format selection.
+   */
+  static final int TRIGGER_ADAPTIVE = 2;
+  /**
+   * Implementations may define custom trigger codes greater than or equal to this value.
+   */
+  static final int TRIGGER_CUSTOM_BASE = 10000;
+
+  /**
+   * Enables the evaluator.
+   */
+  void enable();
+
+  /**
+   * Disables the evaluator.
+   */
+  void disable();
+
+  /**
+   * Update the supplied evaluation.
+   * <p>
+   * When the method is invoked, {@code evaluation} will contain the currently selected
+   * format (null for the first evaluation), the most recent trigger (TRIGGER_INITIAL for the
+   * first evaluation) and the current queue size. The implementation should update these
+   * fields as necessary.
+   * <p>
+   * The trigger should be considered "sticky" for as long as a given representation is selected,
+   * and so should only be changed if the representation is also changed.
+   *
+   * @param queue A read only representation of the currently buffered {@link MediaChunk}s.
+   * @param playbackPositionUs The current playback position.
+   * @param formats The formats from which to select, ordered by decreasing bandwidth.
+   * @param evaluation The evaluation.
+   */
+  // TODO: Pass more useful information into this method, and finalize the interface.
+  void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs, Format[] formats,
+      Evaluation evaluation);
+
+  /**
+   * A format evaluation.
+   */
+  public static final class Evaluation {
+
+    /**
+     * The desired size of the queue.
+     */
+    public int queueSize;
+
+    /**
+     * The sticky reason for the format selection.
+     */
+    public int trigger;
+
+    /**
+     * The selected format.
+     */
+    public Format format;
+
+    public Evaluation() {
+      trigger = TRIGGER_INITIAL;
+    }
+
+  }
+
+  /**
+   * Always selects the first format.
+   */
+  public static class FixedEvaluator implements FormatEvaluator {
+
+    @Override
+    public void enable() {
+      // Do nothing.
+    }
+
+    @Override
+    public void disable() {
+      // Do nothing.
+    }
+
+    @Override
+    public void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs,
+        Format[] formats, Evaluation evaluation) {
+      evaluation.format = formats[0];
+    }
+
+  }
+
+  /**
+   * Selects randomly between the available formats.
+   */
+  public static class RandomEvaluator implements FormatEvaluator {
+
+    private final Random random;
+
+    public RandomEvaluator() {
+      this.random = new Random();
+    }
+
+    @Override
+    public void enable() {
+      // Do nothing.
+    }
+
+    @Override
+    public void disable() {
+      // Do nothing.
+    }
+
+    @Override
+    public void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs,
+        Format[] formats, Evaluation evaluation) {
+      Format newFormat = formats[random.nextInt(formats.length)];
+      if (evaluation.format != null && !evaluation.format.id.equals(newFormat.id)) {
+        evaluation.trigger = TRIGGER_ADAPTIVE;
+      }
+      evaluation.format = newFormat;
+    }
+
+  }
+
+  /**
+   * An adaptive evaluator for video formats, which attempts to select the best quality possible
+   * given the current network conditions and state of the buffer.
+   * <p>
+   * This implementation should be used for video only, and should not be used for audio. It is a
+   * reference implementation only. It is recommended that application developers implement their
+   * own adaptive evaluator to more precisely suit their use case.
+   */
+  public static class AdaptiveEvaluator implements FormatEvaluator {
+
+    public static final int DEFAULT_MAX_INITIAL_BITRATE = 800000;
+
+    public static final int DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS = 10000;
+    public static final int DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS = 25000;
+    public static final int DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS = 25000;
+    public static final float DEFAULT_BANDWIDTH_FRACTION = 0.75f;
+
+    private final BandwidthMeter bandwidthMeter;
+
+    private final int maxInitialBitrate;
+    private final long minDurationForQualityIncreaseUs;
+    private final long maxDurationForQualityDecreaseUs;
+    private final long minDurationToRetainAfterDiscardUs;
+    private final float bandwidthFraction;
+
+    /**
+     * @param bandwidthMeter Provides an estimate of the currently available bandwidth.
+     */
+    public AdaptiveEvaluator(BandwidthMeter bandwidthMeter) {
+      this (bandwidthMeter, DEFAULT_MAX_INITIAL_BITRATE,
+          DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS,
+          DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
+          DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, DEFAULT_BANDWIDTH_FRACTION);
+    }
+
+    /**
+     * @param bandwidthMeter Provides an estimate of the currently available bandwidth.
+     * @param maxInitialBitrate The maximum bitrate in bits per second that should be assumed
+     *     when bandwidthMeter cannot provide an estimate due to playback having only just started.
+     * @param minDurationForQualityIncreaseMs The minimum duration of buffered data required for
+     *     the evaluator to consider switching to a higher quality format.
+     * @param maxDurationForQualityDecreaseMs The maximum duration of buffered data required for
+     *     the evaluator to consider switching to a lower quality format.
+     * @param minDurationToRetainAfterDiscardMs When switching to a significantly higher quality
+     *     format, the evaluator may discard some of the media that it has already buffered at the
+     *     lower quality, so as to switch up to the higher quality faster. This is the minimum
+     *     duration of media that must be retained at the lower quality.
+     * @param bandwidthFraction The fraction of the available bandwidth that the evaluator should
+     *     consider available for use. Setting to a value less than 1 is recommended to account
+     *     for inaccuracies in the bandwidth estimator.
+     */
+    public AdaptiveEvaluator(BandwidthMeter bandwidthMeter,
+        int maxInitialBitrate,
+        int minDurationForQualityIncreaseMs,
+        int maxDurationForQualityDecreaseMs,
+        int minDurationToRetainAfterDiscardMs,
+        float bandwidthFraction) {
+      this.bandwidthMeter = bandwidthMeter;
+      this.maxInitialBitrate = maxInitialBitrate;
+      this.minDurationForQualityIncreaseUs = minDurationForQualityIncreaseMs * 1000L;
+      this.maxDurationForQualityDecreaseUs = maxDurationForQualityDecreaseMs * 1000L;
+      this.minDurationToRetainAfterDiscardUs = minDurationToRetainAfterDiscardMs * 1000L;
+      this.bandwidthFraction = bandwidthFraction;
+    }
+
+    @Override
+    public void enable() {
+      // Do nothing.
+    }
+
+    @Override
+    public void disable() {
+      // Do nothing.
+    }
+
+    @Override
+    public void evaluate(List<? extends MediaChunk> queue, long playbackPositionUs,
+        Format[] formats, Evaluation evaluation) {
+      long bufferedDurationUs = queue.isEmpty() ? 0
+          : queue.get(queue.size() - 1).endTimeUs - playbackPositionUs;
+      Format current = evaluation.format;
+      Format ideal = determineIdealFormat(formats, bandwidthMeter.getBitrateEstimate());
+      boolean isHigher = ideal != null && current != null && ideal.bitrate > current.bitrate;
+      boolean isLower = ideal != null && current != null && ideal.bitrate < current.bitrate;
+      if (isHigher) {
+        if (bufferedDurationUs < minDurationForQualityIncreaseUs) {
+          // The ideal format is a higher quality, but we have insufficient buffer to
+          // safely switch up. Defer switching up for now.
+          ideal = current;
+        } else if (bufferedDurationUs >= minDurationToRetainAfterDiscardUs) {
+          // We're switching from an SD stream to a stream of higher resolution. Consider
+          // discarding already buffered media chunks. Specifically, discard media chunks starting
+          // from the first one that is of lower bandwidth, lower resolution and that is not HD.
+          for (int i = 1; i < queue.size(); i++) {
+            MediaChunk thisChunk = queue.get(i);
+            long durationBeforeThisSegmentUs = thisChunk.startTimeUs - playbackPositionUs;
+            if (durationBeforeThisSegmentUs >= minDurationToRetainAfterDiscardUs
+                && thisChunk.format.bitrate < ideal.bitrate
+                && thisChunk.format.height < ideal.height
+                && thisChunk.format.height < 720
+                && thisChunk.format.width < 1280) {
+              // Discard chunks from this one onwards.
+              evaluation.queueSize = i;
+              break;
+            }
+          }
+        }
+      } else if (isLower && current != null
+        && bufferedDurationUs >= maxDurationForQualityDecreaseUs) {
+        // The ideal format is a lower quality, but we have sufficient buffer to defer switching
+        // down for now.
+        ideal = current;
+      }
+      if (current != null && ideal != current) {
+        evaluation.trigger = FormatEvaluator.TRIGGER_ADAPTIVE;
+      }
+      evaluation.format = ideal;
+    }
+
+    /**
+     * Compute the ideal format ignoring buffer health.
+     */
+    protected Format determineIdealFormat(Format[] formats, long bitrateEstimate) {
+      long effectiveBitrate = computeEffectiveBitrateEstimate(bitrateEstimate);
+      for (int i = 0; i < formats.length; i++) {
+        Format format = formats[i];
+        if (format.bitrate <= effectiveBitrate) {
+          return format;
+        }
+      }
+      // We didn't manage to calculate a suitable format. Return the lowest quality format.
+      return formats[formats.length - 1];
+    }
+
+    /**
+     * Apply overhead factor, or default value in absence of estimate.
+     */
+    protected long computeEffectiveBitrateEstimate(long bitrateEstimate) {
+      return bitrateEstimate == BandwidthMeter.NO_ESTIMATE
+          ? maxInitialBitrate : (long) (bitrateEstimate * bandwidthFraction);
+    }
+
+  }
+
+}

+ 140 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java

@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.chunk;
+
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.ParserException;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.upstream.DataSpec;
+
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * An abstract base class for {@link Chunk}s that contain media samples.
+ */
+public abstract class MediaChunk extends Chunk {
+
+  /**
+   * The start time of the media contained by the chunk.
+   */
+  public final long startTimeUs;
+  /**
+   * The end time of the media contained by the chunk.
+   */
+  public final long endTimeUs;
+  /**
+   * The index of the next media chunk, or -1 if this is the last media chunk in the stream.
+   */
+  public final int nextChunkIndex;
+
+  /**
+   * Constructor for a chunk of media samples.
+   *
+   * @param dataSource A {@link DataSource} for loading the data.
+   * @param dataSpec Defines the data to be loaded.
+   * @param format The format of the stream to which this chunk belongs.
+   * @param trigger The reason for this chunk being selected.
+   * @param startTimeUs The start time of the media contained by the chunk, in microseconds.
+   * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
+   * @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
+   */
+  public MediaChunk(DataSource dataSource, DataSpec dataSpec, Format format, int trigger,
+      long startTimeUs, long endTimeUs, int nextChunkIndex) {
+    super(dataSource, dataSpec, format, trigger);
+    this.startTimeUs = startTimeUs;
+    this.endTimeUs = endTimeUs;
+    this.nextChunkIndex = nextChunkIndex;
+  }
+
+  /**
+   * Whether this is the last chunk in the stream.
+   *
+   * @return True if this is the last chunk in the stream. False otherwise.
+   */
+  public final boolean isLastChunk() {
+    return nextChunkIndex == -1;
+  }
+
+  /**
+   * Seeks to the beginning of the chunk.
+   */
+  public abstract void seekToStart();
+
+  /**
+   * Seeks to the specified position within the chunk.
+   *
+   * @param positionUs The desired seek time in microseconds.
+   * @param allowNoop True if the seek is allowed to do nothing if the result is more accurate than
+   *     seeking to a key frame. Always pass false if it is required that the next sample be a key
+   *     frame.
+   * @return True if the seek results in a discontinuity in the sequence of samples returned by
+   *     {@link #read(SampleHolder)}. False otherwise.
+   */
+  public abstract boolean seekTo(long positionUs, boolean allowNoop);
+
+  /**
+   * Prepares the chunk for reading. Does nothing if the chunk is already prepared.
+   * <p>
+   * Preparation may require consuming some of the chunk. If the data is not yet available then
+   * this method will return {@code false} rather than block. The method can be called repeatedly
+   * until the return value indicates success.
+   *
+   * @return True if the chunk was prepared. False otherwise.
+   * @throws ParserException If an error occurs parsing the media data.
+   */
+  public abstract boolean prepare() throws ParserException;
+
+  /**
+   * Returns whether the next sample is available.
+   *
+   * @return True if the next sample is available for reading. False otherwise.
+   * @throws ParserException
+   */
+  public abstract boolean sampleAvailable() throws ParserException;
+
+  /**
+   * Reads the next media sample from the chunk.
+   * <p>
+   * Should only be called after the chunk has been successfully prepared.
+   *
+   * @param holder A holder to store the read sample.
+   * @return True if a sample was read. False if more data is still required.
+   * @throws ParserException If an error occurs parsing the media data.
+   * @throws IllegalStateException If called before {@link #init}, or after {@link #release}
+   */
+  public abstract boolean read(SampleHolder holder) throws ParserException;
+
+  /**
+   * Returns the media format of the samples contained within this chunk.
+   * <p>
+   * Should only be called after the chunk has been successfully prepared.
+   *
+   * @return The sample media format.
+   */
+  public abstract MediaFormat getMediaFormat();
+
+  /**
+   * Returns the pssh information associated with the chunk.
+   * <p>
+   * Should only be called after the chunk has been successfully prepared.
+   *
+   * @return The pssh information.
+   */
+  public abstract Map<UUID, byte[]> getPsshInfo();
+
+}

+ 120 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/MultiTrackChunkSource.java

@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.chunk;
+
+import com.google.android.exoplayer.ExoPlaybackException;
+import com.google.android.exoplayer.ExoPlayer.ExoPlayerComponent;
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.TrackInfo;
+import com.google.android.exoplayer.util.Assertions;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * A {@link ChunkSource} providing the ability to switch between multiple other {@link ChunkSource}
+ * instances.
+ */
+public class MultiTrackChunkSource implements ChunkSource, ExoPlayerComponent {
+
+  /**
+   * A message to indicate a source selection. Source selection can only be performed when the
+   * source is disabled.
+   */
+  public static final int MSG_SELECT_TRACK = 1;
+
+  private final ChunkSource[] allSources;
+
+  private ChunkSource selectedSource;
+  private boolean enabled;
+
+  public MultiTrackChunkSource(ChunkSource... sources) {
+    this.allSources = sources;
+    this.selectedSource = sources[0];
+  }
+
+  public MultiTrackChunkSource(List<ChunkSource> sources) {
+    this(toChunkSourceArray(sources));
+  }
+
+  /**
+   * Gets the number of tracks that this source can switch between. May be called safely from any
+   * thread.
+   *
+   * @return The number of tracks.
+   */
+  public int getTrackCount() {
+    return allSources.length;
+  }
+
+  @Override
+  public TrackInfo getTrackInfo() {
+    return selectedSource.getTrackInfo();
+  }
+
+  @Override
+  public void enable() {
+    selectedSource.enable();
+    enabled = true;
+  }
+
+  @Override
+  public void disable(List<? extends MediaChunk> queue) {
+    selectedSource.disable(queue);
+    enabled = false;
+  }
+
+  @Override
+  public void continueBuffering(long playbackPositionUs) {
+    selectedSource.continueBuffering(playbackPositionUs);
+  }
+
+  @Override
+  public void getChunkOperation(List<? extends MediaChunk> queue, long seekPositionUs,
+      long playbackPositionUs, ChunkOperationHolder out) {
+    selectedSource.getChunkOperation(queue, seekPositionUs, playbackPositionUs, out);
+  }
+
+  @Override
+  public IOException getError() {
+    return null;
+  }
+
+  @Override
+  public void getMaxVideoDimensions(MediaFormat out) {
+    selectedSource.getMaxVideoDimensions(out);
+  }
+
+  @Override
+  public void handleMessage(int what, Object msg) throws ExoPlaybackException {
+    Assertions.checkState(!enabled);
+    if (what == MSG_SELECT_TRACK) {
+      selectedSource = allSources[(Integer) msg];
+    }
+  }
+
+  @Override
+  public void onChunkLoadError(Chunk chunk, Exception e) {
+    selectedSource.onChunkLoadError(chunk, e);
+  }
+
+  private static ChunkSource[] toChunkSourceArray(List<ChunkSource> sources) {
+    ChunkSource[] chunkSourceArray = new ChunkSource[sources.size()];
+    sources.toArray(chunkSourceArray);
+    return chunkSourceArray;
+  }
+
+}

+ 110 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/SingleSampleChunkSource.java

@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.chunk;
+
+import com.google.android.exoplayer.C;
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.TrackInfo;
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.upstream.DataSpec;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * A chunk source that provides a single chunk containing a single sample.
+ * <p>
+ * An example use case for this implementation is to act as the source for loading out-of-band
+ * subtitles, where subtitles for the entire video are delivered as a single file.
+ */
+public class SingleSampleChunkSource implements ChunkSource {
+
+  private final DataSource dataSource;
+  private final DataSpec dataSpec;
+  private final Format format;
+  private final long durationUs;
+  private final MediaFormat mediaFormat;
+  private final TrackInfo trackInfo;
+
+  /**
+   * @param dataSource A {@link DataSource} suitable for loading the sample data.
+   * @param dataSpec Defines the location of the sample.
+   * @param format The format of the sample.
+   * @param durationUs The duration of the sample in microseconds, or {@link C#UNKNOWN_TIME_US} if
+   *     the duration is unknown.
+   * @param mediaFormat The sample media format. May be null.
+   */
+  public SingleSampleChunkSource(DataSource dataSource, DataSpec dataSpec, Format format,
+      long durationUs, MediaFormat mediaFormat) {
+    this.dataSource = dataSource;
+    this.dataSpec = dataSpec;
+    this.format = format;
+    this.durationUs = durationUs;
+    this.mediaFormat = mediaFormat;
+    trackInfo = new TrackInfo(format.mimeType, durationUs);
+  }
+
+  @Override
+  public TrackInfo getTrackInfo() {
+    return trackInfo;
+  }
+
+  @Override
+  public void getMaxVideoDimensions(MediaFormat out) {
+    // Do nothing.
+  }
+
+  @Override
+  public void enable() {
+    // Do nothing.
+  }
+
+  @Override
+  public void continueBuffering(long playbackPositionUs) {
+    // Do nothing.
+  }
+
+  @Override
+  public void getChunkOperation(List<? extends MediaChunk> queue, long seekPositionUs,
+      long playbackPositionUs, ChunkOperationHolder out) {
+    if (!queue.isEmpty()) {
+      // We've already provided the single sample.
+      return;
+    }
+    out.chunk = initChunk();
+  }
+
+  @Override
+  public void disable(List<? extends MediaChunk> queue) {
+    // Do nothing.
+  }
+
+  @Override
+  public IOException getError() {
+    return null;
+  }
+
+  @Override
+  public void onChunkLoadError(Chunk chunk, Exception e) {
+    // Do nothing.
+  }
+
+  private SingleSampleMediaChunk initChunk() {
+    return new SingleSampleMediaChunk(dataSource, dataSpec, format, 0, 0, durationUs, -1,
+        mediaFormat);
+  }
+
+}

+ 139 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java

@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.chunk;
+
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.upstream.DataSpec;
+import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+import com.google.android.exoplayer.util.Assertions;
+
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * A {@link MediaChunk} containing a single sample.
+ */
+public class SingleSampleMediaChunk extends MediaChunk {
+
+  /**
+   * The sample header data. May be null.
+   */
+  public final byte[] headerData;
+
+  private final MediaFormat sampleFormat;
+
+  /**
+   * @param dataSource A {@link DataSource} for loading the data.
+   * @param dataSpec Defines the data to be loaded.
+   * @param format The format of the stream to which this chunk belongs.
+   * @param trigger The reason for this chunk being selected.
+   * @param startTimeUs The start time of the media contained by the chunk, in microseconds.
+   * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
+   * @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
+   * @param sampleFormat The format of the media contained by the chunk.
+   */
+  public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
+      int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, MediaFormat sampleFormat) {
+    this(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex,
+        sampleFormat, null);
+  }
+
+  /**
+   * @param dataSource A {@link DataSource} for loading the data.
+   * @param dataSpec Defines the data to be loaded.
+   * @param format The format of the stream to which this chunk belongs.
+   * @param trigger The reason for this chunk being selected.
+   * @param startTimeUs The start time of the media contained by the chunk, in microseconds.
+   * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
+   * @param nextChunkIndex The index of the next chunk, or -1 if this is the last chunk.
+   * @param sampleFormat The format of the media contained by the chunk.
+   * @param headerData Custom header data for the sample. May be null. If set, the header data is
+   *     prepended to the sample data returned when {@link #read(SampleHolder)} is called. It is not
+   *     reflected in the values returned by {@link #bytesLoaded()} and {@link #getLength()}.
+   */
+  public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, Format format,
+      int trigger, long startTimeUs, long endTimeUs, int nextChunkIndex, MediaFormat sampleFormat,
+      byte[] headerData) {
+    super(dataSource, dataSpec, format, trigger, startTimeUs, endTimeUs, nextChunkIndex);
+    this.sampleFormat = sampleFormat;
+    this.headerData = headerData;
+  }
+
+  @Override
+  public boolean prepare() {
+    return true;
+  }
+
+  @Override
+  public boolean sampleAvailable() {
+    return isLoadFinished() && !isReadFinished();
+  }
+
+  @Override
+  public boolean read(SampleHolder holder) {
+    NonBlockingInputStream inputStream = getNonBlockingInputStream();
+    Assertions.checkState(inputStream != null);
+    if (!sampleAvailable()) {
+      return false;
+    }
+    int bytesLoaded = (int) bytesLoaded();
+    int sampleSize = bytesLoaded;
+    if (headerData != null) {
+      sampleSize += headerData.length;
+    }
+    if (holder.data == null || holder.data.capacity() < sampleSize) {
+      holder.replaceBuffer(sampleSize);
+    }
+    int bytesRead;
+    if (holder.data != null) {
+      if (headerData != null) {
+        holder.data.put(headerData);
+      }
+      bytesRead = inputStream.read(holder.data, bytesLoaded);
+      holder.size = sampleSize;
+    } else {
+      bytesRead = inputStream.skip(bytesLoaded);
+      holder.size = 0;
+    }
+    Assertions.checkState(bytesRead == bytesLoaded);
+    holder.timeUs = startTimeUs;
+    return true;
+  }
+
+  @Override
+  public void seekToStart() {
+    resetReadPosition();
+  }
+
+  @Override
+  public boolean seekTo(long positionUs, boolean allowNoop) {
+    resetReadPosition();
+    return true;
+  }
+
+  @Override
+  public MediaFormat getMediaFormat() {
+    return sampleFormat;
+  }
+
+  @Override
+  public Map<UUID, byte[]> getPsshInfo() {
+    return null;
+  }
+
+}

+ 121 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/parser/Extractor.java

@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.chunk.parser;
+
+import com.google.android.exoplayer.C;
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.ParserException;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * Facilitates extraction of media samples from a container format.
+ */
+public interface Extractor {
+
+  /**
+   * An attempt to read from the input stream returned insufficient data.
+   */
+  public static final int RESULT_NEED_MORE_DATA = 1;
+  /**
+   * The end of the input stream was reached.
+   */
+  public static final int RESULT_END_OF_STREAM = 2;
+  /**
+   * A media sample was read.
+   */
+  public static final int RESULT_READ_SAMPLE = 4;
+  /**
+   * Initialization data was read. The parsed data can be read using {@link #getFormat()} and
+   * {@link #getPsshInfo}.
+   */
+  public static final int RESULT_READ_INIT = 8;
+  /**
+   * A sidx atom was read. The parsed data can be read using {@link #getIndex()}.
+   */
+  public static final int RESULT_READ_INDEX = 16;
+  /**
+   * The next thing to be read is a sample, but a {@link SampleHolder} was not supplied.
+   */
+  public static final int RESULT_NEED_SAMPLE_HOLDER = 32;
+
+  /**
+   * Returns the segment index parsed from the stream.
+   *
+   * @return The segment index, or null if a SIDX atom has yet to be parsed.
+   */
+  public SegmentIndex getIndex();
+
+  /**
+   * Returns true if the offsets in the index returned by {@link #getIndex()} are relative to the
+   * first byte following the initialization data, or false if they are absolute (i.e. relative to
+   * the first byte of the stream).
+   *
+   * @return True if the offsets are relative to the first byte following the initialization data.
+   *     False otherwise.
+   */
+  public boolean hasRelativeIndexOffsets();
+
+  /**
+   * Returns the format of the samples contained within the media stream.
+   *
+   * @return The sample media format, or null if the format has yet to be parsed.
+   */
+  public MediaFormat getFormat();
+
+  /**
+   * Returns the duration of the stream in microseconds, or {@link C#UNKNOWN_TIME_US} if unknown.
+   */
+  public long getDurationUs();
+
+  /**
+   * Returns the pssh information parsed from the stream.
+   *
+   * @return The pssh information. May be null if pssh data has yet to be parsed, or if the stream
+   *     does not contain any pssh data.
+   */
+  public Map<UUID, byte[]> getPsshInfo();
+
+  /**
+   * Consumes data from a {@link NonBlockingInputStream}.
+   * <p>
+   * The read terminates if the end of the input stream is reached, if an attempt to read from the
+   * input stream returned 0 bytes of data, or if a sample is read. The returned flags indicate
+   * both the reason for termination and data that was parsed during the read.
+   *
+   * @param inputStream The input stream from which data should be read.
+   * @param out A {@link SampleHolder} into which the next sample should be read. If null then
+   *     {@link #RESULT_NEED_SAMPLE_HOLDER} will be returned once a sample has been reached.
+   * @return One or more of the {@code RESULT_*} flags defined in this class.
+   * @throws ParserException If an error occurs parsing the media data.
+   */
+  public int read(NonBlockingInputStream inputStream, SampleHolder out) throws ParserException;
+
+  /**
+   * Seeks to a position before or equal to the requested time.
+   *
+   * @param seekTimeUs The desired seek time in microseconds.
+   * @param allowNoop Allow the seek operation to do nothing if the seek time is in the current
+   *     fragment run, is equal to or greater than the time of the current sample, and if there
+   *     does not exist a sync frame between these two times.
+   * @return True if the operation resulted in a change of state. False if it was a no-op.
+   */
+  public boolean seekTo(long seekTimeUs, boolean allowNoop);
+
+}

+ 70 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/parser/SegmentIndex.java

@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.chunk.parser;
+
+/**
+ * Defines segments within a media stream.
+ */
+public final class SegmentIndex {
+
+  /**
+   * The size in bytes of the segment index as it exists in the stream.
+   */
+  public final int sizeBytes;
+
+  /**
+   * The number of segments.
+   */
+  public final int length;
+
+  /**
+   * The segment sizes, in bytes.
+   */
+  public final int[] sizes;
+
+  /**
+   * The segment byte offsets.
+   */
+  public final long[] offsets;
+
+  /**
+   * The segment durations, in microseconds.
+   */
+  public final long[] durationsUs;
+
+  /**
+   * The start time of each segment, in microseconds.
+   */
+  public final long[] timesUs;
+
+  /**
+   * @param sizeBytes The size in bytes of the segment index as it exists in the stream.
+   * @param sizes The segment sizes, in bytes.
+   * @param offsets The segment byte offsets.
+   * @param durationsUs The segment durations, in microseconds.
+   * @param timesUs The start time of each segment, in microseconds.
+   */
+  public SegmentIndex(int sizeBytes, int[] sizes, long[] offsets, long[] durationsUs,
+      long[] timesUs) {
+    this.sizeBytes = sizeBytes;
+    this.length = sizes.length;
+    this.sizes = sizes;
+    this.offsets = offsets;
+    this.durationsUs = durationsUs;
+    this.timesUs = timesUs;
+  }
+
+}

+ 32 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/DefaultSampleValues.java

@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.chunk.parser.mp4;
+
+/* package */ final class DefaultSampleValues {
+
+  public final int sampleDescriptionIndex;
+  public final int duration;
+  public final int size;
+  public final int flags;
+
+  public DefaultSampleValues(int sampleDescriptionIndex, int duration, int size, int flags) {
+    this.sampleDescriptionIndex = sampleDescriptionIndex;
+    this.duration = duration;
+    this.size = size;
+    this.flags = flags;
+  }
+
+}

+ 841 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/FragmentedMp4Extractor.java

@@ -0,0 +1,841 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.chunk.parser.mp4;
+
+import com.google.android.exoplayer.C;
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.ParserException;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.chunk.parser.Extractor;
+import com.google.android.exoplayer.chunk.parser.SegmentIndex;
+import com.google.android.exoplayer.mp4.Atom;
+import com.google.android.exoplayer.mp4.Atom.ContainerAtom;
+import com.google.android.exoplayer.mp4.Atom.LeafAtom;
+import com.google.android.exoplayer.mp4.CommonMp4AtomParsers;
+import com.google.android.exoplayer.mp4.Mp4Util;
+import com.google.android.exoplayer.mp4.Track;
+import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+import com.google.android.exoplayer.util.ParsableByteArray;
+import com.google.android.exoplayer.util.Util;
+
+import android.annotation.SuppressLint;
+import android.media.MediaCodec;
+import android.media.MediaExtractor;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+import java.util.UUID;
+
+/**
+ * Facilitates the extraction of data from the fragmented mp4 container format.
+ * <p>
+ * This implementation only supports de-muxed (i.e. single track) streams.
+ */
+public final class FragmentedMp4Extractor implements Extractor {
+
+  /**
+   * Flag to work around an issue in some video streams where every frame is marked as a sync frame.
+   * The workaround overrides the sync frame flags in the stream, forcing them to false except for
+   * the first sample in each segment.
+   * <p>
+   * This flag does nothing if the stream is not a video stream.
+   */
+  public static final int WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME = 1;
+
+  private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM
+      | RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER;
+  private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE =
+      new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12};
+
+  // Parser states
+  private static final int STATE_READING_ATOM_HEADER = 0;
+  private static final int STATE_READING_ATOM_PAYLOAD = 1;
+  private static final int STATE_READING_ENCRYPTION_DATA = 2;
+  private static final int STATE_READING_SAMPLE = 3;
+
+  // Atoms that the parser cares about
+  private static final Set<Integer> PARSED_ATOMS;
+  static {
+    HashSet<Integer> parsedAtoms = new HashSet<Integer>();
+    parsedAtoms.add(Atom.TYPE_avc1);
+    parsedAtoms.add(Atom.TYPE_avc3);
+    parsedAtoms.add(Atom.TYPE_esds);
+    parsedAtoms.add(Atom.TYPE_hdlr);
+    parsedAtoms.add(Atom.TYPE_mdat);
+    parsedAtoms.add(Atom.TYPE_mdhd);
+    parsedAtoms.add(Atom.TYPE_moof);
+    parsedAtoms.add(Atom.TYPE_moov);
+    parsedAtoms.add(Atom.TYPE_mp4a);
+    parsedAtoms.add(Atom.TYPE_mvhd);
+    parsedAtoms.add(Atom.TYPE_sidx);
+    parsedAtoms.add(Atom.TYPE_stsd);
+    parsedAtoms.add(Atom.TYPE_tfdt);
+    parsedAtoms.add(Atom.TYPE_tfhd);
+    parsedAtoms.add(Atom.TYPE_tkhd);
+    parsedAtoms.add(Atom.TYPE_traf);
+    parsedAtoms.add(Atom.TYPE_trak);
+    parsedAtoms.add(Atom.TYPE_trex);
+    parsedAtoms.add(Atom.TYPE_trun);
+    parsedAtoms.add(Atom.TYPE_mvex);
+    parsedAtoms.add(Atom.TYPE_mdia);
+    parsedAtoms.add(Atom.TYPE_minf);
+    parsedAtoms.add(Atom.TYPE_stbl);
+    parsedAtoms.add(Atom.TYPE_pssh);
+    parsedAtoms.add(Atom.TYPE_saiz);
+    parsedAtoms.add(Atom.TYPE_uuid);
+    parsedAtoms.add(Atom.TYPE_senc);
+    parsedAtoms.add(Atom.TYPE_pasp);
+    PARSED_ATOMS = Collections.unmodifiableSet(parsedAtoms);
+  }
+
+  // Atoms that the parser considers to be containers
+  private static final Set<Integer> CONTAINER_TYPES;
+  static {
+    HashSet<Integer> atomContainerTypes = new HashSet<Integer>();
+    atomContainerTypes.add(Atom.TYPE_moov);
+    atomContainerTypes.add(Atom.TYPE_trak);
+    atomContainerTypes.add(Atom.TYPE_mdia);
+    atomContainerTypes.add(Atom.TYPE_minf);
+    atomContainerTypes.add(Atom.TYPE_stbl);
+    atomContainerTypes.add(Atom.TYPE_avcC);
+    atomContainerTypes.add(Atom.TYPE_moof);
+    atomContainerTypes.add(Atom.TYPE_traf);
+    atomContainerTypes.add(Atom.TYPE_mvex);
+    CONTAINER_TYPES = Collections.unmodifiableSet(atomContainerTypes);
+  }
+
+  private final int workaroundFlags;
+
+  // Parser state
+  private final ParsableByteArray atomHeader;
+  private final byte[] extendedTypeScratch;
+  private final Stack<ContainerAtom> containerAtoms;
+  private final TrackFragment fragmentRun;
+
+  private int parserState;
+  private int atomBytesRead;
+  private int rootAtomBytesRead;
+  private int atomType;
+  private int atomSize;
+  private ParsableByteArray atomData;
+
+  private int pendingSeekTimeMs;
+  private int sampleIndex;
+  private int pendingSeekSyncSampleIndex;
+  private int lastSyncSampleIndex;
+
+  // Data parsed from moov and sidx atoms
+  private final HashMap<UUID, byte[]> psshData;
+  private SegmentIndex segmentIndex;
+  private Track track;
+  private DefaultSampleValues extendsDefaults;
+
+  public FragmentedMp4Extractor() {
+    this(0);
+  }
+
+  /**
+   * @param workaroundFlags Flags to allow parsing of faulty streams.
+   *     {@link #WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME} is currently the only flag defined.
+   */
+  public FragmentedMp4Extractor(int workaroundFlags) {
+    this.workaroundFlags = workaroundFlags;
+    parserState = STATE_READING_ATOM_HEADER;
+    atomHeader = new ParsableByteArray(Mp4Util.ATOM_HEADER_SIZE);
+    extendedTypeScratch = new byte[16];
+    containerAtoms = new Stack<ContainerAtom>();
+    fragmentRun = new TrackFragment();
+    psshData = new HashMap<UUID, byte[]>();
+  }
+
+  /**
+   * Sideloads track information into the extractor.
+   *
+   * @param track The track to sideload.
+   */
+  public void setTrack(Track track) {
+    this.extendsDefaults = new DefaultSampleValues(0, 0, 0, 0);
+    this.track = track;
+  }
+
+  @Override
+  public Map<UUID, byte[]> getPsshInfo() {
+    return psshData.isEmpty() ? null : psshData;
+  }
+
+  @Override
+  public SegmentIndex getIndex() {
+    return segmentIndex;
+  }
+
+  @Override
+  public boolean hasRelativeIndexOffsets() {
+    return true;
+  }
+
+  @Override
+  public MediaFormat getFormat() {
+    return track == null ? null : track.mediaFormat;
+  }
+
+  @Override
+  public long getDurationUs() {
+    return track == null ? C.UNKNOWN_TIME_US : track.durationUs;
+  }
+
+  @Override
+  public int read(NonBlockingInputStream inputStream, SampleHolder out)
+      throws ParserException {
+    try {
+      int results = 0;
+      while ((results & READ_TERMINATING_RESULTS) == 0) {
+        switch (parserState) {
+          case STATE_READING_ATOM_HEADER:
+            results |= readAtomHeader(inputStream);
+            break;
+          case STATE_READING_ATOM_PAYLOAD:
+            results |= readAtomPayload(inputStream);
+            break;
+          case STATE_READING_ENCRYPTION_DATA:
+            results |= readEncryptionData(inputStream);
+            break;
+          default:
+            results |= readOrSkipSample(inputStream, out);
+            break;
+        }
+      }
+      return results;
+    } catch (Exception e) {
+      throw new ParserException(e);
+    }
+  }
+
+  @Override
+  public boolean seekTo(long seekTimeUs, boolean allowNoop) {
+    pendingSeekTimeMs = (int) (seekTimeUs / 1000);
+    if (allowNoop && fragmentRun != null && fragmentRun.length > 0
+        && pendingSeekTimeMs >= fragmentRun.getSamplePresentationTime(0)
+        && pendingSeekTimeMs <= fragmentRun.getSamplePresentationTime(fragmentRun.length - 1)) {
+      int sampleIndexFound = 0;
+      int syncSampleIndexFound = 0;
+      for (int i = 0; i < fragmentRun.length; i++) {
+        if (fragmentRun.getSamplePresentationTime(i) <= pendingSeekTimeMs) {
+          if (fragmentRun.sampleIsSyncFrameTable[i]) {
+            syncSampleIndexFound = i;
+          }
+          sampleIndexFound = i;
+        }
+      }
+      if (syncSampleIndexFound == lastSyncSampleIndex && sampleIndexFound >= sampleIndex) {
+        pendingSeekTimeMs = 0;
+        return false;
+      }
+    }
+    containerAtoms.clear();
+    enterState(STATE_READING_ATOM_HEADER);
+    return true;
+  }
+
+  private void enterState(int state) {
+    switch (state) {
+      case STATE_READING_ATOM_HEADER:
+        atomBytesRead = 0;
+        if (containerAtoms.isEmpty()) {
+          rootAtomBytesRead = 0;
+        }
+        break;
+    }
+    parserState = state;
+  }
+
+  private int readAtomHeader(NonBlockingInputStream inputStream) {
+    int remainingBytes = Mp4Util.ATOM_HEADER_SIZE - atomBytesRead;
+    int bytesRead = inputStream.read(atomHeader.data, atomBytesRead, remainingBytes);
+    if (bytesRead == -1) {
+      return RESULT_END_OF_STREAM;
+    }
+    rootAtomBytesRead += bytesRead;
+    atomBytesRead += bytesRead;
+    if (atomBytesRead != Mp4Util.ATOM_HEADER_SIZE) {
+      return RESULT_NEED_MORE_DATA;
+    }
+
+    atomHeader.setPosition(0);
+    atomSize = atomHeader.readInt();
+    atomType = atomHeader.readInt();
+
+    if (atomType == Atom.TYPE_mdat) {
+      if (fragmentRun.sampleEncryptionDataNeedsFill) {
+        enterState(STATE_READING_ENCRYPTION_DATA);
+      } else {
+        enterState(STATE_READING_SAMPLE);
+      }
+      return 0;
+    }
+
+    Integer atomTypeInteger = atomType; // Avoids boxing atomType twice.
+    if (PARSED_ATOMS.contains(atomTypeInteger)) {
+      if (CONTAINER_TYPES.contains(atomTypeInteger)) {
+        enterState(STATE_READING_ATOM_HEADER);
+        containerAtoms.add(new ContainerAtom(atomType,
+            rootAtomBytesRead + atomSize - Mp4Util.ATOM_HEADER_SIZE));
+      } else {
+        atomData = new ParsableByteArray(atomSize);
+        System.arraycopy(atomHeader.data, 0, atomData.data, 0, Mp4Util.ATOM_HEADER_SIZE);
+        enterState(STATE_READING_ATOM_PAYLOAD);
+      }
+    } else {
+      atomData = null;
+      enterState(STATE_READING_ATOM_PAYLOAD);
+    }
+
+    return 0;
+  }
+
+  private int readAtomPayload(NonBlockingInputStream inputStream) {
+    int bytesRead;
+    if (atomData != null) {
+      bytesRead = inputStream.read(atomData.data, atomBytesRead, atomSize - atomBytesRead);
+    } else {
+      bytesRead = inputStream.skip(atomSize - atomBytesRead);
+    }
+    if (bytesRead == -1) {
+      return RESULT_END_OF_STREAM;
+    }
+    rootAtomBytesRead += bytesRead;
+    atomBytesRead += bytesRead;
+    if (atomBytesRead != atomSize) {
+      return RESULT_NEED_MORE_DATA;
+    }
+
+    int results = 0;
+    if (atomData != null) {
+      results |= onLeafAtomRead(new LeafAtom(atomType, atomData));
+    }
+
+    while (!containerAtoms.isEmpty() && containerAtoms.peek().endByteOffset == rootAtomBytesRead) {
+      results |= onContainerAtomRead(containerAtoms.pop());
+    }
+
+    enterState(STATE_READING_ATOM_HEADER);
+    return results;
+  }
+
+  private int onLeafAtomRead(LeafAtom leaf) {
+    if (!containerAtoms.isEmpty()) {
+      containerAtoms.peek().add(leaf);
+    } else if (leaf.type == Atom.TYPE_sidx) {
+      segmentIndex = parseSidx(leaf.data);
+      return RESULT_READ_INDEX;
+    }
+    return 0;
+  }
+
+  private int onContainerAtomRead(ContainerAtom container) {
+    if (container.type == Atom.TYPE_moov) {
+      onMoovContainerAtomRead(container);
+      return RESULT_READ_INIT;
+    } else if (container.type == Atom.TYPE_moof) {
+      onMoofContainerAtomRead(container);
+    } else if (!containerAtoms.isEmpty()) {
+      containerAtoms.peek().add(container);
+    }
+    return 0;
+  }
+
+  private void onMoovContainerAtomRead(ContainerAtom moov) {
+    List<Atom.LeafAtom> moovChildren = moov.leafChildren;
+    int moovChildrenSize = moovChildren.size();
+    for (int i = 0; i < moovChildrenSize; i++) {
+      LeafAtom child = moovChildren.get(i);
+      if (child.type == Atom.TYPE_pssh) {
+        ParsableByteArray psshAtom = child.data;
+        psshAtom.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE);
+        UUID uuid = new UUID(psshAtom.readLong(), psshAtom.readLong());
+        int dataSize = psshAtom.readInt();
+        byte[] data = new byte[dataSize];
+        psshAtom.readBytes(data, 0, dataSize);
+        psshData.put(uuid, data);
+      }
+    }
+    ContainerAtom mvex = moov.getContainerAtomOfType(Atom.TYPE_mvex);
+    extendsDefaults = parseTrex(mvex.getLeafAtomOfType(Atom.TYPE_trex).data);
+    track = CommonMp4AtomParsers.parseTrak(moov.getContainerAtomOfType(Atom.TYPE_trak),
+        moov.getLeafAtomOfType(Atom.TYPE_mvhd));
+  }
+
+  private void onMoofContainerAtomRead(ContainerAtom moof) {
+    fragmentRun.reset();
+    parseMoof(track, extendsDefaults, moof, fragmentRun, workaroundFlags, extendedTypeScratch);
+    sampleIndex = 0;
+    lastSyncSampleIndex = 0;
+    pendingSeekSyncSampleIndex = 0;
+    if (pendingSeekTimeMs != 0) {
+      for (int i = 0; i < fragmentRun.length; i++) {
+        if (fragmentRun.sampleIsSyncFrameTable[i]) {
+          if (fragmentRun.getSamplePresentationTime(i) <= pendingSeekTimeMs) {
+            pendingSeekSyncSampleIndex = i;
+          }
+        }
+      }
+      pendingSeekTimeMs = 0;
+    }
+  }
+
+  /**
+   * Parses a trex atom (defined in 14496-12).
+   */
+  private static DefaultSampleValues parseTrex(ParsableByteArray trex) {
+    trex.setPosition(Mp4Util.FULL_ATOM_HEADER_SIZE + 4);
+    int defaultSampleDescriptionIndex = trex.readUnsignedIntToInt() - 1;
+    int defaultSampleDuration = trex.readUnsignedIntToInt();
+    int defaultSampleSize = trex.readUnsignedIntToInt();
+    int defaultSampleFlags = trex.readInt();
+    return new DefaultSampleValues(defaultSampleDescriptionIndex, defaultSampleDuration,
+        defaultSampleSize, defaultSampleFlags);
+  }
+
+  private static void parseMoof(Track track, DefaultSampleValues extendsDefaults,
+      ContainerAtom moof, TrackFragment out, int workaroundFlags, byte[] extendedTypeScratch) {
+    parseTraf(track, extendsDefaults, moof.getContainerAtomOfType(Atom.TYPE_traf),
+        out, workaroundFlags, extendedTypeScratch);
+  }
+
+  /**
+   * Parses a traf atom (defined in 14496-12).
+   */
+  private static void parseTraf(Track track, DefaultSampleValues extendsDefaults,
+      ContainerAtom traf, TrackFragment out, int workaroundFlags, byte[] extendedTypeScratch) {
+    LeafAtom tfdtAtom = traf.getLeafAtomOfType(Atom.TYPE_tfdt);
+    long decodeTime = tfdtAtom == null ? 0 : parseTfdt(traf.getLeafAtomOfType(Atom.TYPE_tfdt).data);
+
+    LeafAtom tfhd = traf.getLeafAtomOfType(Atom.TYPE_tfhd);
+    DefaultSampleValues fragmentHeader = parseTfhd(extendsDefaults, tfhd.data);
+    out.sampleDescriptionIndex = fragmentHeader.sampleDescriptionIndex;
+
+    LeafAtom trun = traf.getLeafAtomOfType(Atom.TYPE_trun);
+    parseTrun(track, fragmentHeader, decodeTime, workaroundFlags, trun.data, out);
+
+    LeafAtom saiz = traf.getLeafAtomOfType(Atom.TYPE_saiz);
+    if (saiz != null) {
+      TrackEncryptionBox trackEncryptionBox =
+          track.sampleDescriptionEncryptionBoxes[fragmentHeader.sampleDescriptionIndex];
+      parseSaiz(trackEncryptionBox, saiz.data, out);
+    }
+
+    LeafAtom senc = traf.getLeafAtomOfType(Atom.TYPE_senc);
+    if (senc != null) {
+      parseSenc(senc.data, out);
+    }
+
+    int childrenSize = traf.leafChildren.size();
+    for (int i = 0; i < childrenSize; i++) {
+      LeafAtom atom = traf.leafChildren.get(i);
+      if (atom.type == Atom.TYPE_uuid) {
+        parseUuid(atom.data, out, extendedTypeScratch);
+      }
+    }
+  }
+
+  private static void parseSaiz(TrackEncryptionBox encryptionBox, ParsableByteArray saiz,
+      TrackFragment out) {
+    int vectorSize = encryptionBox.initializationVectorSize;
+    saiz.setPosition(Mp4Util.ATOM_HEADER_SIZE);
+    int fullAtom = saiz.readInt();
+    int flags = Mp4Util.parseFullAtomFlags(fullAtom);
+    if ((flags & 0x01) == 1) {
+      saiz.skip(8);
+    }
+    int defaultSampleInfoSize = saiz.readUnsignedByte();
+
+    int sampleCount = saiz.readUnsignedIntToInt();
+    if (sampleCount != out.length) {
+      throw new IllegalStateException("Length mismatch: " + sampleCount + ", " + out.length);
+    }
+
+    int totalSize = 0;
+    if (defaultSampleInfoSize == 0) {
+      boolean[] sampleHasSubsampleEncryptionTable = out.sampleHasSubsampleEncryptionTable;
+      for (int i = 0; i < sampleCount; i++) {
+        int sampleInfoSize = saiz.readUnsignedByte();
+        totalSize += sampleInfoSize;
+        sampleHasSubsampleEncryptionTable[i] = sampleInfoSize > vectorSize;
+      }
+    } else {
+      boolean subsampleEncryption = defaultSampleInfoSize > vectorSize;
+      totalSize += defaultSampleInfoSize * sampleCount;
+      Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);
+    }
+    out.initEncryptionData(totalSize);
+  }
+
+  /**
+   * Parses a tfhd atom (defined in 14496-12).
+   *
+   * @param extendsDefaults Default sample values from the trex atom.
+   * @return The parsed default sample values.
+   */
+  private static DefaultSampleValues parseTfhd(DefaultSampleValues extendsDefaults,
+      ParsableByteArray tfhd) {
+    tfhd.setPosition(Mp4Util.ATOM_HEADER_SIZE);
+    int fullAtom = tfhd.readInt();
+    int flags = Mp4Util.parseFullAtomFlags(fullAtom);
+
+    tfhd.skip(4); // trackId
+    if ((flags & 0x01 /* base_data_offset_present */) != 0) {
+      tfhd.skip(8);
+    }
+
+    int defaultSampleDescriptionIndex =
+        ((flags & 0x02 /* default_sample_description_index_present */) != 0) ?
+        tfhd.readUnsignedIntToInt() - 1 : extendsDefaults.sampleDescriptionIndex;
+    int defaultSampleDuration = ((flags & 0x08 /* default_sample_duration_present */) != 0) ?
+        tfhd.readUnsignedIntToInt() : extendsDefaults.duration;
+    int defaultSampleSize = ((flags & 0x10 /* default_sample_size_present */) != 0) ?
+        tfhd.readUnsignedIntToInt() : extendsDefaults.size;
+    int defaultSampleFlags = ((flags & 0x20 /* default_sample_flags_present */) != 0) ?
+        tfhd.readUnsignedIntToInt() : extendsDefaults.flags;
+    return new DefaultSampleValues(defaultSampleDescriptionIndex, defaultSampleDuration,
+        defaultSampleSize, defaultSampleFlags);
+  }
+
+  /**
+   * Parses a tfdt atom (defined in 14496-12).
+   *
+   * @return baseMediaDecodeTime The sum of the decode durations of all earlier samples in the
+   *     media, expressed in the media's timescale.
+   */
+  private static long parseTfdt(ParsableByteArray tfdt) {
+    tfdt.setPosition(Mp4Util.ATOM_HEADER_SIZE);
+    int fullAtom = tfdt.readInt();
+    int version = Mp4Util.parseFullAtomVersion(fullAtom);
+    return version == 1 ? tfdt.readUnsignedLongToLong() : tfdt.readUnsignedInt();
+  }
+
+  /**
+   * Parses a trun atom (defined in 14496-12).
+   *
+   * @param track The corresponding track.
+   * @param defaultSampleValues Default sample values.
+   * @param decodeTime The decode time.
+   * @param trun The trun atom to parse.
+   * @param out The {@TrackFragment} into which parsed data should be placed.
+   */
+  private static void parseTrun(Track track, DefaultSampleValues defaultSampleValues,
+      long decodeTime, int workaroundFlags, ParsableByteArray trun, TrackFragment out) {
+    trun.setPosition(Mp4Util.ATOM_HEADER_SIZE);
+    int fullAtom = trun.readInt();
+    int flags = Mp4Util.parseFullAtomFlags(fullAtom);
+
+    int sampleCount = trun.readUnsignedIntToInt();
+    if ((flags & 0x01 /* data_offset_present */) != 0) {
+      trun.skip(4);
+    }
+
+    boolean firstSampleFlagsPresent = (flags & 0x04 /* first_sample_flags_present */) != 0;
+    int firstSampleFlags = defaultSampleValues.flags;
+    if (firstSampleFlagsPresent) {
+      firstSampleFlags = trun.readUnsignedIntToInt();
+    }
+
+    boolean sampleDurationsPresent = (flags & 0x100 /* sample_duration_present */) != 0;
+    boolean sampleSizesPresent = (flags & 0x200 /* sample_size_present */) != 0;
+    boolean sampleFlagsPresent = (flags & 0x400 /* sample_flags_present */) != 0;
+    boolean sampleCompositionTimeOffsetsPresent =
+        (flags & 0x800 /* sample_composition_time_offsets_present */) != 0;
+
+    out.initTables(sampleCount);
+    int[] sampleSizeTable = out.sampleSizeTable;
+    int[] sampleCompositionTimeOffsetTable = out.sampleCompositionTimeOffsetTable;
+    long[] sampleDecodingTimeTable = out.sampleDecodingTimeTable;
+    boolean[] sampleIsSyncFrameTable = out.sampleIsSyncFrameTable;
+
+    long timescale = track.timescale;
+    long cumulativeTime = decodeTime;
+    boolean workaroundEveryVideoFrameIsSyncFrame = track.type == Track.TYPE_VIDEO
+        && ((workaroundFlags & WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME)
+        == WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME);
+    for (int i = 0; i < sampleCount; i++) {
+      // Use trun values if present, otherwise tfhd, otherwise trex.
+      int sampleDuration = sampleDurationsPresent ? trun.readUnsignedIntToInt()
+          : defaultSampleValues.duration;
+      int sampleSize = sampleSizesPresent ? trun.readUnsignedIntToInt() : defaultSampleValues.size;
+      int sampleFlags = (i == 0 && firstSampleFlagsPresent) ? firstSampleFlags
+          : sampleFlagsPresent ? trun.readInt() : defaultSampleValues.flags;
+      if (sampleCompositionTimeOffsetsPresent) {
+        // The BMFF spec (ISO 14496-12) states that sample offsets should be unsigned integers in
+        // version 0 trun boxes, however a significant number of streams violate the spec and use
+        // signed integers instead. It's safe to always parse sample offsets as signed integers
+        // here, because unsigned integers will still be parsed correctly (unless their top bit is
+        // set, which is never true in practice because sample offsets are always small).
+        int sampleOffset = trun.readInt();
+        sampleCompositionTimeOffsetTable[i] = (int) ((sampleOffset * 1000) / timescale);
+      } else {
+        sampleCompositionTimeOffsetTable[i] = 0;
+      }
+      sampleDecodingTimeTable[i] = (cumulativeTime * 1000) / timescale;
+      sampleSizeTable[i] = sampleSize;
+      sampleIsSyncFrameTable[i] = ((sampleFlags >> 16) & 0x1) == 0
+          && (!workaroundEveryVideoFrameIsSyncFrame || i == 0);
+      cumulativeTime += sampleDuration;
+    }
+  }
+
+  private static void parseUuid(ParsableByteArray uuid, TrackFragment out,
+      byte[] extendedTypeScratch) {
+    uuid.setPosition(Mp4Util.ATOM_HEADER_SIZE);
+    uuid.readBytes(extendedTypeScratch, 0, 16);
+
+    // Currently this parser only supports Microsoft's PIFF SampleEncryptionBox.
+    if (!Arrays.equals(extendedTypeScratch, PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE)) {
+      return;
+    }
+
+    // Except for the extended type, this box is identical to a SENC box. See "Portable encoding of
+    // audio-video objects: The Protected Interoperable File Format (PIFF), John A. Bocharov et al,
+    // Section 5.3.2.1."
+    parseSenc(uuid, 16, out);
+  }
+
+  private static void parseSenc(ParsableByteArray senc, TrackFragment out) {
+    parseSenc(senc, 0, out);
+  }
+
+  private static void parseSenc(ParsableByteArray senc, int offset, TrackFragment out) {
+    senc.setPosition(Mp4Util.ATOM_HEADER_SIZE + offset);
+    int fullAtom = senc.readInt();
+    int flags = Mp4Util.parseFullAtomFlags(fullAtom);
+
+    if ((flags & 0x01 /* override_track_encryption_box_parameters */) != 0) {
+      // TODO: Implement this.
+      throw new IllegalStateException("Overriding TrackEncryptionBox parameters is unsupported");
+    }
+
+    boolean subsampleEncryption = (flags & 0x02 /* use_subsample_encryption */) != 0;
+    int sampleCount = senc.readUnsignedIntToInt();
+    if (sampleCount != out.length) {
+      throw new IllegalStateException("Length mismatch: " + sampleCount + ", " + out.length);
+    }
+
+    Arrays.fill(out.sampleHasSubsampleEncryptionTable, 0, sampleCount, subsampleEncryption);
+    out.initEncryptionData(senc.bytesLeft());
+    out.fillEncryptionData(senc);
+  }
+
+  /**
+   * Parses a sidx atom (defined in 14496-12).
+   */
+  private static SegmentIndex parseSidx(ParsableByteArray atom) {
+    atom.setPosition(Mp4Util.ATOM_HEADER_SIZE);
+    int fullAtom = atom.readInt();
+    int version = Mp4Util.parseFullAtomVersion(fullAtom);
+
+    atom.skip(4);
+    long timescale = atom.readUnsignedInt();
+    long earliestPresentationTime;
+    long firstOffset;
+    if (version == 0) {
+      earliestPresentationTime = atom.readUnsignedInt();
+      firstOffset = atom.readUnsignedInt();
+    } else {
+      earliestPresentationTime = atom.readUnsignedLongToLong();
+      firstOffset = atom.readUnsignedLongToLong();
+    }
+
+    atom.skip(2);
+
+    int referenceCount = atom.readUnsignedShort();
+    int[] sizes = new int[referenceCount];
+    long[] offsets = new long[referenceCount];
+    long[] durationsUs = new long[referenceCount];
+    long[] timesUs = new long[referenceCount];
+
+    long offset = firstOffset;
+    long time = earliestPresentationTime;
+    long timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale);
+    for (int i = 0; i < referenceCount; i++) {
+      int firstInt = atom.readInt();
+
+      int type = 0x80000000 & firstInt;
+      if (type != 0) {
+        throw new IllegalStateException("Unhandled indirect reference");
+      }
+      long referenceDuration = atom.readUnsignedInt();
+
+      sizes[i] = 0x7fffffff & firstInt;
+      offsets[i] = offset;
+
+      // Calculate time and duration values such that any rounding errors are consistent. i.e. That
+      // timesUs[i] + durationsUs[i] == timesUs[i + 1].
+      timesUs[i] = timeUs;
+      time += referenceDuration;
+      timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale);
+      durationsUs[i] = timeUs - timesUs[i];
+
+      atom.skip(4);
+      offset += sizes[i];
+    }
+
+    return new SegmentIndex(atom.limit(), sizes, offsets, durationsUs, timesUs);
+  }
+
+  private int readEncryptionData(NonBlockingInputStream inputStream) {
+    boolean success = fragmentRun.fillEncryptionData(inputStream);
+    if (!success) {
+      return RESULT_NEED_MORE_DATA;
+    }
+    enterState(STATE_READING_SAMPLE);
+    return 0;
+  }
+
+  /**
+   * Attempts to read or skip the next sample in the current mdat atom.
+   * <p>
+   * If there are no more samples in the current mdat atom then the parser state is transitioned
+   * to {@link #STATE_READING_ATOM_HEADER} and 0 is returned.
+   * <p>
+   * If there's a pending seek to a sync frame, and if the next sample is before that frame, then
+   * the sample is skipped. Otherwise it is read.
+   * <p>
+   * It is possible for a sample to be read or skipped in part if there is insufficent data
+   * available from the {@link NonBlockingInputStream}. In this case the remainder of the sample
+   * can be read in a subsequent call passing the same {@link SampleHolder}.
+   *
+   * @param inputStream The stream from which to read the sample.
+   * @param out The holder into which to write the sample.
+   * @return A combination of RESULT_* flags indicating the result of the call.
+   */
+  private int readOrSkipSample(NonBlockingInputStream inputStream, SampleHolder out) {
+    if (sampleIndex >= fragmentRun.length) {
+      // We've run out of samples in the current mdat atom.
+      enterState(STATE_READING_ATOM_HEADER);
+      return 0;
+    }
+    int sampleSize = fragmentRun.sampleSizeTable[sampleIndex];
+    if (inputStream.getAvailableByteCount() < sampleSize) {
+      return RESULT_NEED_MORE_DATA;
+    }
+    if (sampleIndex < pendingSeekSyncSampleIndex) {
+      return skipSample(inputStream, sampleSize);
+    }
+    return readSample(inputStream, sampleSize, out);
+  }
+
+  private int skipSample(NonBlockingInputStream inputStream, int sampleSize) {
+    if (fragmentRun.definesEncryptionData) {
+      ParsableByteArray sampleEncryptionData = fragmentRun.sampleEncryptionData;
+      TrackEncryptionBox encryptionBox =
+          track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex];
+      int vectorSize = encryptionBox.initializationVectorSize;
+      boolean subsampleEncryption = fragmentRun.sampleHasSubsampleEncryptionTable[sampleIndex];
+      sampleEncryptionData.skip(vectorSize);
+      int subsampleCount = subsampleEncryption ? sampleEncryptionData.readUnsignedShort() : 1;
+      if (subsampleEncryption) {
+        sampleEncryptionData.skip((2 + 4) * subsampleCount);
+      }
+    }
+
+    inputStream.skip(sampleSize);
+
+    sampleIndex++;
+    enterState(STATE_READING_SAMPLE);
+    return 0;
+  }
+
+  private int readSample(NonBlockingInputStream inputStream, int sampleSize, SampleHolder out) {
+    if (out == null) {
+      return RESULT_NEED_SAMPLE_HOLDER;
+    }
+    out.timeUs = fragmentRun.getSamplePresentationTime(sampleIndex) * 1000L;
+    out.flags = 0;
+    if (fragmentRun.sampleIsSyncFrameTable[sampleIndex]) {
+      out.flags |= C.SAMPLE_FLAG_SYNC;
+      lastSyncSampleIndex = sampleIndex;
+    }
+    if (out.data == null || out.data.capacity() < sampleSize) {
+      out.replaceBuffer(sampleSize);
+    }
+    if (fragmentRun.definesEncryptionData) {
+      readSampleEncryptionData(fragmentRun.sampleEncryptionData, out);
+    }
+
+    ByteBuffer outputData = out.data;
+    if (outputData == null) {
+      inputStream.skip(sampleSize);
+      out.size = 0;
+    } else {
+      inputStream.read(outputData, sampleSize);
+      if (track.type == Track.TYPE_VIDEO) {
+        // The mp4 file contains length-prefixed NAL units, but the decoder wants start code
+        // delimited content.
+        Mp4Util.replaceLengthPrefixesWithAvcStartCodes(outputData, sampleSize);
+      }
+      out.size = sampleSize;
+    }
+
+    sampleIndex++;
+    enterState(STATE_READING_SAMPLE);
+    return RESULT_READ_SAMPLE;
+  }
+
+  @SuppressLint("InlinedApi")
+  private void readSampleEncryptionData(ParsableByteArray sampleEncryptionData, SampleHolder out) {
+    TrackEncryptionBox encryptionBox =
+        track.sampleDescriptionEncryptionBoxes[fragmentRun.sampleDescriptionIndex];
+    byte[] keyId = encryptionBox.keyId;
+    boolean isEncrypted = encryptionBox.isEncrypted;
+    int vectorSize = encryptionBox.initializationVectorSize;
+    boolean subsampleEncryption = fragmentRun.sampleHasSubsampleEncryptionTable[sampleIndex];
+
+    byte[] vector = out.cryptoInfo.iv;
+    if (vector == null || vector.length != 16) {
+      vector = new byte[16];
+    }
+    sampleEncryptionData.readBytes(vector, 0, vectorSize);
+
+    int subsampleCount = subsampleEncryption ? sampleEncryptionData.readUnsignedShort() : 1;
+    int[] clearDataSizes = out.cryptoInfo.numBytesOfClearData;
+    if (clearDataSizes == null || clearDataSizes.length < subsampleCount) {
+      clearDataSizes = new int[subsampleCount];
+    }
+    int[] encryptedDataSizes = out.cryptoInfo.numBytesOfEncryptedData;
+    if (encryptedDataSizes == null || encryptedDataSizes.length < subsampleCount) {
+      encryptedDataSizes = new int[subsampleCount];
+    }
+    if (subsampleEncryption) {
+      for (int i = 0; i < subsampleCount; i++) {
+        clearDataSizes[i] = sampleEncryptionData.readUnsignedShort();
+        encryptedDataSizes[i] = sampleEncryptionData.readUnsignedIntToInt();
+      }
+    } else {
+      clearDataSizes[0] = 0;
+      encryptedDataSizes[0] = fragmentRun.sampleSizeTable[sampleIndex];
+    }
+    out.cryptoInfo.set(subsampleCount, clearDataSizes, encryptedDataSizes, keyId, vector,
+        isEncrypted ? MediaCodec.CRYPTO_MODE_AES_CTR : MediaCodec.CRYPTO_MODE_UNENCRYPTED);
+    if (isEncrypted) {
+      out.flags |= MediaExtractor.SAMPLE_FLAG_ENCRYPTED;
+    }
+  }
+
+}

+ 50 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/TrackEncryptionBox.java

@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.chunk.parser.mp4;
+
+/**
+ * Encapsulates information parsed from a track encryption (tenc) box in an MP4 stream.
+ */
+public final class TrackEncryptionBox {
+
+  /**
+   * Indicates the encryption state of the samples in the sample group.
+   */
+  public final boolean isEncrypted;
+
+  /**
+   * The initialization vector size in bytes for the samples in the corresponding sample group.
+   */
+  public final int initializationVectorSize;
+
+  /**
+   * The key identifier for the samples in the corresponding sample group.
+   */
+  public final byte[] keyId;
+
+  /**
+   * @param isEncrypted Indicates the encryption state of the samples in the sample group.
+   * @param initializationVectorSize The initialization vector size in bytes for the samples in the
+   *     corresponding sample group.
+   * @param keyId The key identifier for the samples in the corresponding sample group.
+   */
+  public TrackEncryptionBox(boolean isEncrypted, int initializationVectorSize, byte[] keyId) {
+    this.isEncrypted = isEncrypted;
+    this.initializationVectorSize = initializationVectorSize;
+    this.keyId = keyId;
+  }
+
+}

+ 155 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/parser/mp4/TrackFragment.java

@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.chunk.parser.mp4;
+
+import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+import com.google.android.exoplayer.util.ParsableByteArray;
+
+/**
+ * A holder for information corresponding to a single fragment of an mp4 file.
+ */
+/* package */ final class TrackFragment {
+
+  public int sampleDescriptionIndex;
+
+  /**
+   * The number of samples contained by the fragment.
+   */
+  public int length;
+  /**
+   * The size of each sample in the run.
+   */
+  public int[] sampleSizeTable;
+  /**
+   * The composition time offset of each sample in the run.
+   */
+  public int[] sampleCompositionTimeOffsetTable;
+  /**
+   * The decoding time of each sample in the run.
+   */
+  public long[] sampleDecodingTimeTable;
+  /**
+   * Indicates which samples are sync frames.
+   */
+  public boolean[] sampleIsSyncFrameTable;
+  /**
+   * True if the fragment defines encryption data. False otherwise.
+   */
+  public boolean definesEncryptionData;
+  /**
+   * If {@link #definesEncryptionData} is true, indicates which samples use sub-sample encryption.
+   * Undefined otherwise.
+   */
+  public boolean[] sampleHasSubsampleEncryptionTable;
+  /**
+   * If {@link #definesEncryptionData} is true, indicates the length of the sample encryption data.
+   * Undefined otherwise.
+   */
+  public int sampleEncryptionDataLength;
+  /**
+   * If {@link #definesEncryptionData} is true, contains binary sample encryption data. Undefined
+   * otherwise.
+   */
+  public ParsableByteArray sampleEncryptionData;
+  /**
+   * Whether {@link #sampleEncryptionData} needs populating with the actual encryption data.
+   */
+  public boolean sampleEncryptionDataNeedsFill;
+
+  /**
+   * Resets the fragment.
+   * <p>
+   * The {@link #length} is set to 0, and both {@link #definesEncryptionData} and
+   * {@link #sampleEncryptionDataNeedsFill} is set to false.
+   */
+  public void reset() {
+    length = 0;
+    definesEncryptionData = false;
+    sampleEncryptionDataNeedsFill = false;
+  }
+
+  /**
+   * Configures the fragment for the specified number of samples.
+   * <p>
+   * The {@link #length} of the fragment is set to the specified sample count, and the contained
+   * tables are resized if necessary such that they are at least this length.
+   *
+   * @param sampleCount The number of samples in the new run.
+   */
+  public void initTables(int sampleCount) {
+    length = sampleCount;
+    if (sampleSizeTable == null || sampleSizeTable.length < length) {
+      // Size the tables 25% larger than needed, so as to make future resize operations less
+      // likely. The choice of 25% is relatively arbitrary.
+      int tableSize = (sampleCount * 125) / 100;
+      sampleSizeTable = new int[tableSize];
+      sampleCompositionTimeOffsetTable = new int[tableSize];
+      sampleDecodingTimeTable = new long[tableSize];
+      sampleIsSyncFrameTable = new boolean[tableSize];
+      sampleHasSubsampleEncryptionTable = new boolean[tableSize];
+    }
+  }
+
+  /**
+   * Configures the fragment to be one that defines encryption data of the specified length.
+   * <p>
+   * {@link #definesEncryptionData} is set to true, {@link #sampleEncryptionDataLength} is set to
+   * the specified length, and {@link #sampleEncryptionData} is resized if necessary such that it
+   * is at least this length.
+   *
+   * @param length The length in bytes of the encryption data.
+   */
+  public void initEncryptionData(int length) {
+    if (sampleEncryptionData == null || sampleEncryptionData.limit() < length) {
+      sampleEncryptionData = new ParsableByteArray(length);
+    }
+    sampleEncryptionDataLength = length;
+    definesEncryptionData = true;
+    sampleEncryptionDataNeedsFill = true;
+  }
+
+  /**
+   * Fills {@link #sampleEncryptionData} from the provided source.
+   *
+   * @param source A source from which to read the encryption data.
+   */
+  public void fillEncryptionData(ParsableByteArray source) {
+    source.readBytes(sampleEncryptionData.data, 0, sampleEncryptionDataLength);
+    sampleEncryptionData.setPosition(0);
+    sampleEncryptionDataNeedsFill = false;
+  }
+
+  /**
+   * Fills {@link #sampleEncryptionData} for the current run from the provided source.
+   *
+   * @param source A source from which to read the encryption data.
+   * @return True if the encryption data was filled. False if the source had insufficient data.
+   */
+  public boolean fillEncryptionData(NonBlockingInputStream source) {
+    if (source.getAvailableByteCount() < sampleEncryptionDataLength) {
+      return false;
+    }
+    source.read(sampleEncryptionData.data, 0, sampleEncryptionDataLength);
+    sampleEncryptionData.setPosition(0);
+    sampleEncryptionDataNeedsFill = false;
+    return true;
+  }
+
+  public long getSamplePresentationTime(int index) {
+    return sampleDecodingTimeTable[index] + sampleCompositionTimeOffsetTable[index];
+  }
+
+}

+ 550 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/parser/webm/DefaultEbmlReader.java

@@ -0,0 +1,550 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.chunk.parser.webm;
+
+import com.google.android.exoplayer.C;
+import com.google.android.exoplayer.ParserException;
+import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+import com.google.android.exoplayer.util.Assertions;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.Stack;
+
+/**
+ * Default version of a basic event-driven incremental EBML parser which needs an
+ * {@link EbmlEventHandler} to define IDs/types and react to events.
+ *
+ * <p>EBML can be summarized as a binary XML format somewhat similar to Protocol Buffers.
+ * It was originally designed for the Matroska container format. More information about EBML and
+ * Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
+ */
+/* package */ final class DefaultEbmlReader implements EbmlReader {
+
+  // State values used in variables state, elementIdState, elementContentSizeState, and
+  // varintBytesState.
+  private static final int STATE_BEGIN_READING = 0;
+  private static final int STATE_READ_CONTENTS = 1;
+  private static final int STATE_FINISHED_READING = 2;
+
+  /**
+   * The first byte of a variable-length integer (varint) will have one of these bit masks
+   * indicating the total length in bytes.
+   *
+   * <p>{@code 0x80} is a one-byte integer, {@code 0x40} is two bytes, and so on up to eight bytes.
+   */
+  private static final int[] VARINT_LENGTH_MASKS = new int[] {
+    0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01
+  };
+
+  private static final int MAX_INTEGER_ELEMENT_SIZE_BYTES = 8;
+  private static final int VALID_FLOAT32_ELEMENT_SIZE_BYTES = 4;
+  private static final int VALID_FLOAT64_ELEMENT_SIZE_BYTES = 8;
+
+  /**
+   * Scratch space to read in EBML varints, unsigned ints, and floats - each of which can be
+   * up to 8 bytes.
+   */
+  private final byte[] tempByteArray = new byte[8];
+  private final Stack<MasterElement> masterElementsStack = new Stack<MasterElement>();
+
+  /**
+   * Current {@link EbmlEventHandler} which is queried for element types
+   * and informed of element events.
+   */
+  private EbmlEventHandler eventHandler;
+
+  /**
+   * Overall state for the current element. Must be one of the {@code STATE_*} constants.
+   */
+  private int state;
+
+  /**
+   * Total bytes read since starting or the last {@link #reset()}.
+   */
+  private long bytesRead;
+
+  /**
+   * The starting byte offset of the current element being parsed.
+   */
+  private long elementOffset;
+
+  /**
+   * Holds the current element ID after {@link #elementIdState} is {@link #STATE_FINISHED_READING}.
+   */
+  private int elementId;
+
+  /**
+   * State for the ID of the current element. Must be one of the {@code STATE_*} constants.
+   */
+  private int elementIdState;
+
+  /**
+   * Holds the current element content size after {@link #elementContentSizeState}
+   * is {@link #STATE_FINISHED_READING}.
+   */
+  private long elementContentSize;
+
+  /**
+   * State for the content size of the current element.
+   * Must be one of the {@code STATE_*} constants.
+   */
+  private int elementContentSizeState;
+
+  /**
+   * State for the current variable-length integer (varint) being read into
+   * {@link #tempByteArray}. Must be one of the {@code STATE_*} constants.
+   */
+  private int varintBytesState;
+
+  /**
+   * Length in bytes of the current variable-length integer (varint) being read into
+   * {@link #tempByteArray}.
+   */
+  private int varintBytesLength;
+
+  /**
+   * Counts the number of bytes being contiguously read into either {@link #tempByteArray} or
+   * {@link #stringBytes}. Used to determine when all required bytes have been read across
+   * multiple calls.
+   */
+  private int bytesState;
+
+  /**
+   * Holds string element bytes as they're being read in. Allocated after the element content
+   * size is known and released after calling {@link EbmlEventHandler#onStringElement(int, String)}.
+   */
+  private byte[] stringBytes;
+
+  @Override
+  public void setEventHandler(EbmlEventHandler eventHandler) {
+    this.eventHandler = eventHandler;
+  }
+
+  @Override
+  public int read(NonBlockingInputStream inputStream) throws ParserException {
+    Assertions.checkState(eventHandler != null);
+    while (true) {
+      while (!masterElementsStack.isEmpty()
+          && bytesRead >= masterElementsStack.peek().elementEndOffsetBytes) {
+        eventHandler.onMasterElementEnd(masterElementsStack.pop().elementId);
+        return READ_RESULT_CONTINUE;
+      }
+
+      if (state == STATE_BEGIN_READING) {
+        int idResult = readElementId(inputStream);
+        if (idResult != READ_RESULT_CONTINUE) {
+          return idResult;
+        }
+        int sizeResult = readElementContentSize(inputStream);
+        if (sizeResult != READ_RESULT_CONTINUE) {
+          return sizeResult;
+        }
+        state = STATE_READ_CONTENTS;
+        bytesState = 0;
+      }
+
+      int type = eventHandler.getElementType(elementId);
+      switch (type) {
+        case TYPE_MASTER:
+          int masterHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max.
+          masterElementsStack.add(new MasterElement(elementId, bytesRead + elementContentSize));
+          eventHandler.onMasterElementStart(elementId, elementOffset, masterHeaderSize,
+              elementContentSize);
+          prepareForNextElement();
+          return READ_RESULT_CONTINUE;
+        case TYPE_UNSIGNED_INT:
+          if (elementContentSize > MAX_INTEGER_ELEMENT_SIZE_BYTES) {
+            throw new IllegalStateException("Invalid integer size " + elementContentSize);
+          }
+          int intResult =
+              readBytesInternal(inputStream, tempByteArray, (int) elementContentSize);
+          if (intResult != READ_RESULT_CONTINUE) {
+            return intResult;
+          }
+          long intValue = getTempByteArrayValue((int) elementContentSize, false);
+          eventHandler.onIntegerElement(elementId, intValue);
+          prepareForNextElement();
+          return READ_RESULT_CONTINUE;
+        case TYPE_FLOAT:
+          if (elementContentSize != VALID_FLOAT32_ELEMENT_SIZE_BYTES
+              && elementContentSize != VALID_FLOAT64_ELEMENT_SIZE_BYTES) {
+            throw new IllegalStateException("Invalid float size " + elementContentSize);
+          }
+          int floatResult =
+              readBytesInternal(inputStream, tempByteArray, (int) elementContentSize);
+          if (floatResult != READ_RESULT_CONTINUE) {
+            return floatResult;
+          }
+          long valueBits = getTempByteArrayValue((int) elementContentSize, false);
+          double floatValue;
+          if (elementContentSize == VALID_FLOAT32_ELEMENT_SIZE_BYTES) {
+            floatValue = Float.intBitsToFloat((int) valueBits);
+          } else {
+            floatValue = Double.longBitsToDouble(valueBits);
+          }
+          eventHandler.onFloatElement(elementId, floatValue);
+          prepareForNextElement();
+          return READ_RESULT_CONTINUE;
+        case TYPE_STRING:
+          if (elementContentSize > Integer.MAX_VALUE) {
+            throw new IllegalStateException(
+                "String element size " + elementContentSize + " is larger than MAX_INT");
+          }
+          if (stringBytes == null) {
+            stringBytes = new byte[(int) elementContentSize];
+          }
+          int stringResult =
+              readBytesInternal(inputStream, stringBytes, (int) elementContentSize);
+          if (stringResult != READ_RESULT_CONTINUE) {
+            return stringResult;
+          }
+          String stringValue = new String(stringBytes, Charset.forName(C.UTF8_NAME));
+          stringBytes = null;
+          eventHandler.onStringElement(elementId, stringValue);
+          prepareForNextElement();
+          return READ_RESULT_CONTINUE;
+        case TYPE_BINARY:
+          if (elementContentSize > Integer.MAX_VALUE) {
+            throw new IllegalStateException(
+                "Binary element size " + elementContentSize + " is larger than MAX_INT");
+          }
+          if (inputStream.getAvailableByteCount() < elementContentSize) {
+            return READ_RESULT_NEED_MORE_DATA;
+          }
+          int binaryHeaderSize = (int) (bytesRead - elementOffset); // Header size is 12 bytes max.
+          boolean consumed = eventHandler.onBinaryElement(
+              elementId, elementOffset, binaryHeaderSize, (int) elementContentSize, inputStream);
+          if (consumed) {
+            long expectedBytesRead = elementOffset + binaryHeaderSize + elementContentSize;
+            if (expectedBytesRead != bytesRead) {
+              throw new IllegalStateException("Incorrect total bytes read. Expected "
+                  + expectedBytesRead + " but actually " + bytesRead);
+            }
+            prepareForNextElement();
+          }
+          return READ_RESULT_CONTINUE;
+        case TYPE_UNKNOWN:
+          if (elementContentSize > Integer.MAX_VALUE) {
+            throw new IllegalStateException(
+                "Unknown element size " + elementContentSize + " is larger than MAX_INT");
+          }
+          int skipResult = skipBytesInternal(inputStream, (int) elementContentSize);
+          if (skipResult != READ_RESULT_CONTINUE) {
+            return skipResult;
+          }
+          prepareForNextElement();
+          break;
+        default:
+          throw new IllegalStateException("Invalid element type " + type);
+      }
+    }
+  }
+
+  @Override
+  public long getBytesRead() {
+    return bytesRead;
+  }
+
+  @Override
+  public void reset() {
+    prepareForNextElement();
+    masterElementsStack.clear();
+    bytesRead = 0;
+  }
+
+  @Override
+  public long readVarint(NonBlockingInputStream inputStream) {
+    varintBytesState = STATE_BEGIN_READING;
+    int result = readVarintBytes(inputStream);
+    if (result != READ_RESULT_CONTINUE) {
+      throw new IllegalStateException("Couldn't read varint");
+    }
+    return getTempByteArrayValue(varintBytesLength, true);
+  }
+
+  @Override
+  public void readBytes(NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes) {
+    bytesState = 0;
+    int result = readBytesInternal(inputStream, byteBuffer, totalBytes);
+    if (result != READ_RESULT_CONTINUE) {
+      throw new IllegalStateException("Couldn't read bytes into buffer");
+    }
+  }
+
+  @Override
+  public void readBytes(NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes) {
+    bytesState = 0;
+    int result = readBytesInternal(inputStream, byteArray, totalBytes);
+    if (result != READ_RESULT_CONTINUE) {
+      throw new IllegalStateException("Couldn't read bytes into array");
+    }
+  }
+
+  @Override
+  public void skipBytes(NonBlockingInputStream inputStream, int totalBytes) {
+    bytesState = 0;
+    int result = skipBytesInternal(inputStream, totalBytes);
+    if (result != READ_RESULT_CONTINUE) {
+      throw new IllegalStateException("Couldn't skip bytes");
+    }
+  }
+
+  /**
+   * Resets the internal state of {@link #read(NonBlockingInputStream)} so that it can start
+   * reading a new element from scratch.
+   */
+  private void prepareForNextElement() {
+    state = STATE_BEGIN_READING;
+    elementIdState = STATE_BEGIN_READING;
+    elementContentSizeState = STATE_BEGIN_READING;
+    elementOffset = bytesRead;
+  }
+
+  /**
+   * Reads an element ID such that reading can be stopped and started again in a later call
+   * if not enough bytes are available. Returns {@link #READ_RESULT_CONTINUE} if a full element ID
+   * has been read into {@link #elementId}. Reset {@link #elementIdState} to
+   * {@link #STATE_BEGIN_READING} before calling to indicate a new element ID should be read.
+   *
+   * @param inputStream The input stream from which an element ID should be read
+   * @return One of the {@code RESULT_*} flags defined in this class
+   */
+  private int readElementId(NonBlockingInputStream inputStream) {
+    if (elementIdState == STATE_FINISHED_READING) {
+      return READ_RESULT_CONTINUE;
+    }
+    if (elementIdState == STATE_BEGIN_READING) {
+      varintBytesState = STATE_BEGIN_READING;
+      elementIdState = STATE_READ_CONTENTS;
+    }
+    int result = readVarintBytes(inputStream);
+    if (result != READ_RESULT_CONTINUE) {
+      return result;
+    }
+    // Element IDs are at most 4 bytes so cast to int now.
+    elementId = (int) getTempByteArrayValue(varintBytesLength, false);
+    elementIdState = STATE_FINISHED_READING;
+    return READ_RESULT_CONTINUE;
+  }
+
+  /**
+   * Reads an element's content size such that reading can be stopped and started again in a later
+   * call if not enough bytes are available.
+   *
+   * <p>Returns {@link #READ_RESULT_CONTINUE} if an entire element size has been
+   * read into {@link #elementContentSize}. Reset {@link #elementContentSizeState} to
+   * {@link #STATE_BEGIN_READING} before calling to indicate a new element size should be read.
+   *
+   * @param inputStream The input stream from which an element size should be read
+   * @return One of the {@code RESULT_*} flags defined in this class
+   */
+  private int readElementContentSize(NonBlockingInputStream inputStream) {
+    if (elementContentSizeState == STATE_FINISHED_READING) {
+      return READ_RESULT_CONTINUE;
+    }
+    if (elementContentSizeState == STATE_BEGIN_READING) {
+      varintBytesState = STATE_BEGIN_READING;
+      elementContentSizeState = STATE_READ_CONTENTS;
+    }
+    int result = readVarintBytes(inputStream);
+    if (result != READ_RESULT_CONTINUE) {
+      return result;
+    }
+    elementContentSize = getTempByteArrayValue(varintBytesLength, true);
+    elementContentSizeState = STATE_FINISHED_READING;
+    return READ_RESULT_CONTINUE;
+  }
+
+  /**
+   * Reads an EBML variable-length integer (varint) such that reading can be stopped and started
+   * again in a later call if not enough bytes are available.
+   *
+   * <p>Returns {@link #READ_RESULT_CONTINUE} if an entire varint has been read into
+   * {@link #tempByteArray} and the length of the varint is in {@link #varintBytesLength}.
+   * Reset {@link #varintBytesState} to {@link #STATE_BEGIN_READING} before calling to indicate
+   * a new varint should be read.
+   *
+   * @param inputStream The input stream from which a varint should be read
+   * @return One of the {@code RESULT_*} flags defined in this class
+   */
+  private int readVarintBytes(NonBlockingInputStream inputStream) {
+    if (varintBytesState == STATE_FINISHED_READING) {
+      return READ_RESULT_CONTINUE;
+    }
+
+    // Read first byte to get length.
+    if (varintBytesState == STATE_BEGIN_READING) {
+      bytesState = 0;
+      int result = readBytesInternal(inputStream, tempByteArray, 1);
+      if (result != READ_RESULT_CONTINUE) {
+        return result;
+      }
+      varintBytesState = STATE_READ_CONTENTS;
+
+      int firstByte = tempByteArray[0] & 0xff;
+      varintBytesLength = -1;
+      for (int i = 0; i < VARINT_LENGTH_MASKS.length; i++) {
+        if ((VARINT_LENGTH_MASKS[i] & firstByte) != 0) {
+          varintBytesLength = i + 1;
+          break;
+        }
+      }
+      if (varintBytesLength == -1) {
+        throw new IllegalStateException(
+            "No valid varint length mask found at bytesRead = " + bytesRead);
+      }
+    }
+
+    // Read remaining bytes.
+    int result = readBytesInternal(inputStream, tempByteArray, varintBytesLength);
+    if (result != READ_RESULT_CONTINUE) {
+      return result;
+    }
+
+    // All bytes have been read.
+    return READ_RESULT_CONTINUE;
+  }
+
+  /**
+   * Reads a set amount of bytes into a {@link ByteBuffer} such that reading can be stopped
+   * and started again later if not enough bytes are available.
+   *
+   * <p>Returns {@link #READ_RESULT_CONTINUE} if all bytes have been read. Reset
+   * {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes should be read.
+   *
+   * @param inputStream The input stream from which bytes should be read
+   * @param byteBuffer The {@link ByteBuffer} into which bytes should be read
+   * @param totalBytes The total size of bytes to be read
+   * @return One of the {@code RESULT_*} flags defined in this class
+   */
+  private int readBytesInternal(
+      NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes) {
+    if (bytesState == STATE_BEGIN_READING && totalBytes > byteBuffer.capacity()) {
+      throw new IllegalArgumentException("Byte buffer not large enough");
+    }
+    if (bytesState >= totalBytes) {
+      return READ_RESULT_CONTINUE;
+    }
+    int remainingBytes = totalBytes - bytesState;
+    int additionalBytesRead = inputStream.read(byteBuffer, remainingBytes);
+    return updateBytesState(additionalBytesRead, totalBytes);
+  }
+
+  /**
+   * Reads a set amount of bytes into a {@code byte[]} such that reading can be stopped
+   * and started again later if not enough bytes are available.
+   *
+   * <p>Returns {@link #READ_RESULT_CONTINUE} if all bytes have been read. Reset
+   * {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes should be read.
+   *
+   * @param inputStream The input stream from which bytes should be read
+   * @param byteArray The {@code byte[]} into which bytes should be read
+   * @param totalBytes The total size of bytes to be read
+   * @return One of the {@code RESULT_*} flags defined in this class
+   */
+  private int readBytesInternal(
+      NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes) {
+    if (bytesState == STATE_BEGIN_READING && totalBytes > byteArray.length) {
+      throw new IllegalArgumentException("Byte array not large enough");
+    }
+    if (bytesState >= totalBytes) {
+      return READ_RESULT_CONTINUE;
+    }
+    int remainingBytes = totalBytes - bytesState;
+    int additionalBytesRead = inputStream.read(byteArray, bytesState, remainingBytes);
+    return updateBytesState(additionalBytesRead, totalBytes);
+  }
+
+  /**
+   * Skips a set amount of bytes such that reading can be stopped and started again later if
+   * not enough bytes are available.
+   *
+   * <p>Returns {@link #READ_RESULT_CONTINUE} if all bytes have been skipped. Reset
+   * {@link #bytesState} to {@code 0} before calling to indicate a new set of bytes
+   * should be skipped.
+   *
+   * @param inputStream The input stream from which bytes should be skipped
+   * @param totalBytes The total size of bytes to be skipped
+   * @return One of the {@code RESULT_*} flags defined in this class
+   */
+  private int skipBytesInternal(NonBlockingInputStream inputStream, int totalBytes) {
+    if (bytesState >= totalBytes) {
+      return READ_RESULT_CONTINUE;
+    }
+    int remainingBytes = totalBytes - bytesState;
+    int additionalBytesRead = inputStream.skip(remainingBytes);
+    return updateBytesState(additionalBytesRead, totalBytes);
+  }
+
+  /**
+   * Updates {@link #bytesState} and {@link #bytesRead} after reading bytes in one of the
+   * {@code verbBytesInternal} methods.
+   *
+   * @param additionalBytesRead The number of additional bytes read to be accounted for
+   * @param totalBytes The total size of bytes to be read or skipped
+   * @return One of the {@code RESULT_*} flags defined in this class
+   */
+  private int updateBytesState(int additionalBytesRead, int totalBytes) {
+    if (additionalBytesRead == -1) {
+      return READ_RESULT_END_OF_STREAM;
+    }
+    bytesState += additionalBytesRead;
+    bytesRead += additionalBytesRead;
+    if (bytesState < totalBytes) {
+      return READ_RESULT_NEED_MORE_DATA;
+    } else {
+      return READ_RESULT_CONTINUE;
+    }
+  }
+
+  /**
+   * Parses and returns the integer value currently read into the first {@code byteLength} bytes
+   * of {@link #tempByteArray}. EBML varint length masks can optionally be removed.
+   *
+   * @param byteLength The number of bytes to parse from {@link #tempByteArray}
+   * @param removeLengthMask Removes the variable-length integer length mask from the value
+   * @return The resulting integer value. This value could be up to 8-bytes so a Java long is used
+   */
+  private long getTempByteArrayValue(int byteLength, boolean removeLengthMask) {
+    if (removeLengthMask) {
+      tempByteArray[0] &= ~VARINT_LENGTH_MASKS[varintBytesLength - 1];
+    }
+    long varint = 0;
+    for (int i = 0; i < byteLength; i++) {
+      // Shift all existing bits up one byte and add the next byte at the bottom.
+      varint = (varint << 8) | (tempByteArray[i] & 0xff);
+    }
+    return varint;
+  }
+
+  /**
+   * Used in {@link #masterElementsStack} to track when the current master element ends so that
+   * {@link EbmlEventHandler#onMasterElementEnd(int)} is called.
+   */
+  private static final class MasterElement {
+
+    private final int elementId;
+    private final long elementEndOffsetBytes;
+
+    private MasterElement(int elementId, long elementEndOffsetBytes) {
+      this.elementId = elementId;
+      this.elementEndOffsetBytes = elementEndOffsetBytes;
+    }
+
+  }
+
+}

+ 125 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/parser/webm/EbmlEventHandler.java

@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.chunk.parser.webm;
+
+import com.google.android.exoplayer.ParserException;
+import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Defines EBML element IDs/types and reacts to events.
+ */
+/* package */ interface EbmlEventHandler {
+
+  /**
+   * Retrieves the type of an element ID.
+   *
+   * <p>If {@link EbmlReader#TYPE_UNKNOWN} is returned then the element is skipped.
+   * Note that all children of a skipped master element are also skipped.
+   *
+   * @param id The integer ID of this element
+   * @return One of the {@code TYPE_} constants defined in this class
+   */
+  public int getElementType(int id);
+
+  /**
+   * Called when a master element is encountered in the {@link NonBlockingInputStream}.
+   *
+   * <p>Following events should be considered as taking place "within" this element until a
+   * matching call to {@link #onMasterElementEnd(int)} is made. Note that it is possible for
+   * another master element of the same ID to be nested within itself.
+   *
+   * @param id The integer ID of this element
+   * @param elementOffsetBytes The byte offset where this element starts
+   * @param headerSizeBytes The byte length of this element's ID and size header
+   * @param contentsSizeBytes The byte length of this element's children
+   * @throws ParserException If a parsing error occurs.
+   */
+  public void onMasterElementStart(
+      int id, long elementOffsetBytes, int headerSizeBytes,
+      long contentsSizeBytes) throws ParserException;
+
+  /**
+   * Called when a master element has finished reading in all of its children from the
+   * {@link NonBlockingInputStream}.
+   *
+   * @param id The integer ID of this element
+   * @throws ParserException If a parsing error occurs.
+   */
+  public void onMasterElementEnd(int id) throws ParserException;
+
+  /**
+   * Called when an integer element is encountered in the {@link NonBlockingInputStream}.
+   *
+   * @param id The integer ID of this element
+   * @param value The integer value this element contains
+   * @throws ParserException If a parsing error occurs.
+   */
+  public void onIntegerElement(int id, long value) throws ParserException;
+
+  /**
+   * Called when a float element is encountered in the {@link NonBlockingInputStream}.
+   *
+   * @param id The integer ID of this element
+   * @param value The float value this element contains
+   * @throws ParserException If a parsing error occurs.
+   */
+  public void onFloatElement(int id, double value) throws ParserException;
+
+  /**
+   * Called when a string element is encountered in the {@link NonBlockingInputStream}.
+   *
+   * @param id The integer ID of this element
+   * @param value The string value this element contains
+   * @throws ParserException If a parsing error occurs.
+   */
+  public void onStringElement(int id, String value) throws ParserException;
+
+  /**
+   * Called when a binary element is encountered in the {@link NonBlockingInputStream}.
+   *
+   * <p>The element header (containing element ID and content size) will already have been read.
+   * Subclasses must either read nothing and return {@code false}, or exactly read the entire
+   * contents of the element, which is {@code contentsSizeBytes} in length, and return {@code true}.
+   *
+   * <p>It's guaranteed that the full element contents will be immediately available from
+   * {@code inputStream}.
+   *
+   * <p>Several methods in {@link EbmlReader} are available for reading the contents of a
+   * binary element:
+   * <ul>
+   * <li>{@link EbmlReader#readVarint(NonBlockingInputStream)}.
+   * <li>{@link EbmlReader#readBytes(NonBlockingInputStream, byte[], int)}.
+   * <li>{@link EbmlReader#readBytes(NonBlockingInputStream, ByteBuffer, int)}.
+   * <li>{@link EbmlReader#skipBytes(NonBlockingInputStream, int)}.
+   * <li>{@link EbmlReader#getBytesRead()}.
+   * </ul>
+   *
+   * @param id The integer ID of this element
+   * @param elementOffsetBytes The byte offset where this element starts
+   * @param headerSizeBytes The byte length of this element's ID and size header
+   * @param contentsSizeBytes The byte length of this element's contents
+   * @param inputStream The {@link NonBlockingInputStream} from which this
+   *        element's contents should be read
+   * @return True if the element was read. False otherwise.
+   * @throws ParserException If a parsing error occurs.
+   */
+  public boolean onBinaryElement(
+      int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
+      NonBlockingInputStream inputStream) throws ParserException;
+
+}

+ 109 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/parser/webm/EbmlReader.java

@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.chunk.parser.webm;
+
+import com.google.android.exoplayer.ParserException;
+import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Basic event-driven incremental EBML parser which needs an {@link EbmlEventHandler} to
+ * define IDs/types and react to events.
+ *
+ * <p>EBML can be summarized as a binary XML format somewhat similar to Protocol Buffers.
+ * It was originally designed for the Matroska container format. More information about EBML and
+ * Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
+ */
+/* package */ interface EbmlReader {
+
+  // Element Types
+  /** Undefined element. */
+  public static final int TYPE_UNKNOWN = 0;
+  /** Contains child elements. */
+  public static final int TYPE_MASTER = 1;
+  /** Unsigned integer value of up to 8 bytes. */
+  public static final int TYPE_UNSIGNED_INT = 2;
+  public static final int TYPE_STRING = 3;
+  public static final int TYPE_BINARY = 4;
+  /** IEEE floating point value of either 4 or 8 bytes. */
+  public static final int TYPE_FLOAT = 5;
+
+  // Return values for reading methods.
+  public static final int READ_RESULT_CONTINUE = 0;
+  public static final int READ_RESULT_NEED_MORE_DATA = 1;
+  public static final int READ_RESULT_END_OF_STREAM = 2;
+
+  public void setEventHandler(EbmlEventHandler eventHandler);
+
+  /**
+   * Reads from a {@link NonBlockingInputStream}, invoking an event callback if possible.
+   *
+   * @param inputStream The input stream from which data should be read
+   * @return One of the {@code RESULT_*} flags defined in this interface
+   * @throws ParserException If parsing fails.
+   */
+  public int read(NonBlockingInputStream inputStream) throws ParserException;
+
+  /**
+   * The total number of bytes consumed by the reader since first created or last {@link #reset()}.
+   */
+  public long getBytesRead();
+
+  /**
+   * Resets the entire state of the reader so that it will read a new EBML structure from scratch.
+   *
+   * <p>This includes resetting the value returned from {@link #getBytesRead()} to 0 and discarding
+   * all pending {@link EbmlEventHandler#onMasterElementEnd(int)} events.
+   */
+  public void reset();
+
+  /**
+   * Reads, parses, and returns an EBML variable-length integer (varint) from the contents
+   * of a binary element.
+   *
+   * @param inputStream The input stream from which data should be read
+   * @return The varint value at the current position of the contents of a binary element
+   */
+  public long readVarint(NonBlockingInputStream inputStream);
+
+  /**
+   * Reads a fixed number of bytes from the contents of a binary element into a {@link ByteBuffer}.
+   *
+   * @param inputStream The input stream from which data should be read
+   * @param byteBuffer The {@link ByteBuffer} to which data should be written
+   * @param totalBytes The fixed number of bytes to be read and written
+   */
+  public void readBytes(NonBlockingInputStream inputStream, ByteBuffer byteBuffer, int totalBytes);
+
+  /**
+   * Reads a fixed number of bytes from the contents of a binary element into a {@code byte[]}.
+   *
+   * @param inputStream The input stream from which data should be read
+   * @param byteArray The byte array to which data should be written
+   * @param totalBytes The fixed number of bytes to be read and written
+   */
+  public void readBytes(NonBlockingInputStream inputStream, byte[] byteArray, int totalBytes);
+
+  /**
+   * Skips a fixed number of bytes from the contents of a binary element.
+   *
+   * @param inputStream The input stream from which data should be skipped
+   * @param totalBytes The fixed number of bytes to be skipped
+   */
+  public void skipBytes(NonBlockingInputStream inputStream, int totalBytes);
+
+}

+ 645 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/chunk/parser/webm/WebmExtractor.java

@@ -0,0 +1,645 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.chunk.parser.webm;
+
+import com.google.android.exoplayer.C;
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.ParserException;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.chunk.parser.Extractor;
+import com.google.android.exoplayer.chunk.parser.SegmentIndex;
+import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+import com.google.android.exoplayer.util.LongArray;
+import com.google.android.exoplayer.util.MimeTypes;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An extractor to facilitate data retrieval from the WebM container format.
+ *
+ * <p>WebM is a subset of the EBML elements defined for Matroska. More information about EBML and
+ * Matroska is available <a href="http://www.matroska.org/technical/specs/index.html">here</a>.
+ * More info about WebM is <a href="http://www.webmproject.org/code/specs/container/">here</a>.
+ */
+public final class WebmExtractor implements Extractor {
+
+  private static final String DOC_TYPE_WEBM = "webm";
+  private static final String CODEC_ID_VP9 = "V_VP9";
+  private static final String CODEC_ID_VORBIS = "A_VORBIS";
+  private static final String CODEC_ID_OPUS = "A_OPUS";
+  private static final int VORBIS_MAX_INPUT_SIZE = 8192;
+  private static final int OPUS_MAX_INPUT_SIZE = 5760;
+  private static final int UNKNOWN = -1;
+
+  // Element IDs
+  private static final int ID_EBML = 0x1A45DFA3;
+  private static final int ID_EBML_READ_VERSION = 0x42F7;
+  private static final int ID_DOC_TYPE = 0x4282;
+  private static final int ID_DOC_TYPE_READ_VERSION = 0x4285;
+
+  private static final int ID_SEGMENT = 0x18538067;
+
+  private static final int ID_INFO = 0x1549A966;
+  private static final int ID_TIMECODE_SCALE = 0x2AD7B1;
+  private static final int ID_DURATION = 0x4489;
+
+  private static final int ID_CLUSTER = 0x1F43B675;
+  private static final int ID_TIME_CODE = 0xE7;
+  private static final int ID_SIMPLE_BLOCK = 0xA3;
+  private static final int ID_BLOCK_GROUP = 0xA0;
+  private static final int ID_BLOCK = 0xA1;
+
+  private static final int ID_TRACKS = 0x1654AE6B;
+  private static final int ID_TRACK_ENTRY = 0xAE;
+  private static final int ID_CODEC_ID = 0x86;
+  private static final int ID_CODEC_PRIVATE = 0x63A2;
+  private static final int ID_CODEC_DELAY = 0x56AA;
+  private static final int ID_SEEK_PRE_ROLL = 0x56BB;
+  private static final int ID_VIDEO = 0xE0;
+  private static final int ID_PIXEL_WIDTH = 0xB0;
+  private static final int ID_PIXEL_HEIGHT = 0xBA;
+  private static final int ID_AUDIO = 0xE1;
+  private static final int ID_CHANNELS = 0x9F;
+  private static final int ID_SAMPLING_FREQUENCY = 0xB5;
+
+  private static final int ID_CUES = 0x1C53BB6B;
+  private static final int ID_CUE_POINT = 0xBB;
+  private static final int ID_CUE_TIME = 0xB3;
+  private static final int ID_CUE_TRACK_POSITIONS = 0xB7;
+  private static final int ID_CUE_CLUSTER_POSITION = 0xF1;
+
+  // SimpleBlock Lacing Values
+  private static final int LACING_NONE = 0;
+  private static final int LACING_XIPH = 1;
+  private static final int LACING_FIXED = 2;
+  private static final int LACING_EBML = 3;
+
+  private static final int READ_TERMINATING_RESULTS = RESULT_NEED_MORE_DATA | RESULT_END_OF_STREAM
+      | RESULT_READ_SAMPLE | RESULT_NEED_SAMPLE_HOLDER;
+
+  private final EbmlReader reader;
+  private final byte[] simpleBlockTimecodeAndFlags = new byte[3];
+
+  private SampleHolder sampleHolder;
+  private int readResults;
+
+  private long segmentStartOffsetBytes = UNKNOWN;
+  private long segmentEndOffsetBytes = UNKNOWN;
+  private long timecodeScale = 1000000L;
+  private long durationUs = UNKNOWN;
+  private int pixelWidth = UNKNOWN;
+  private int pixelHeight = UNKNOWN;
+  private int channelCount = UNKNOWN;
+  private int sampleRate = UNKNOWN;
+  private byte[] codecPrivate;
+  private String codecId;
+  private long codecDelayNs;
+  private long seekPreRollNs;
+  private boolean seenAudioTrack;
+  private long cuesSizeBytes = UNKNOWN;
+  private long clusterTimecodeUs = UNKNOWN;
+  private long simpleBlockTimecodeUs = UNKNOWN;
+  private MediaFormat format;
+  private SegmentIndex cues;
+  private LongArray cueTimesUs;
+  private LongArray cueClusterPositions;
+
+  public WebmExtractor() {
+    this(new DefaultEbmlReader());
+  }
+
+  /* package */ WebmExtractor(EbmlReader reader) {
+    this.reader = reader;
+    this.reader.setEventHandler(new InnerEbmlEventHandler());
+  }
+
+  @Override
+  public int read(
+      NonBlockingInputStream inputStream, SampleHolder sampleHolder) throws ParserException {
+    this.sampleHolder = sampleHolder;
+    this.readResults = 0;
+    while ((readResults & READ_TERMINATING_RESULTS) == 0) {
+      int ebmlReadResult = reader.read(inputStream);
+      if (ebmlReadResult == EbmlReader.READ_RESULT_NEED_MORE_DATA) {
+        readResults |= WebmExtractor.RESULT_NEED_MORE_DATA;
+      } else if (ebmlReadResult == EbmlReader.READ_RESULT_END_OF_STREAM) {
+        readResults |= WebmExtractor.RESULT_END_OF_STREAM;
+      }
+    }
+    this.sampleHolder = null;
+    return readResults;
+  }
+
+  @Override
+  public boolean seekTo(long seekTimeUs, boolean allowNoop) {
+    if (allowNoop
+        && cues != null
+        && clusterTimecodeUs != UNKNOWN
+        && simpleBlockTimecodeUs != UNKNOWN
+        && seekTimeUs >= simpleBlockTimecodeUs) {
+      int clusterIndex = Arrays.binarySearch(cues.timesUs, clusterTimecodeUs);
+      if (clusterIndex >= 0 && seekTimeUs < clusterTimecodeUs + cues.durationsUs[clusterIndex]) {
+        return false;
+      }
+    }
+    clusterTimecodeUs = UNKNOWN;
+    simpleBlockTimecodeUs = UNKNOWN;
+    reader.reset();
+    return true;
+  }
+
+  @Override
+  public SegmentIndex getIndex() {
+    return cues;
+  }
+
+  @Override
+  public boolean hasRelativeIndexOffsets() {
+    return false;
+  }
+
+  @Override
+  public MediaFormat getFormat() {
+    return format;
+  }
+
+  @Override
+  public long getDurationUs() {
+    return durationUs == UNKNOWN ? C.UNKNOWN_TIME_US : durationUs;
+  }
+
+  @Override
+  public Map<UUID, byte[]> getPsshInfo() {
+    // TODO: Parse pssh data from Webm streams.
+    return null;
+  }
+
+  /* package */ int getElementType(int id) {
+    switch (id) {
+      case ID_EBML:
+      case ID_SEGMENT:
+      case ID_INFO:
+      case ID_CLUSTER:
+      case ID_TRACKS:
+      case ID_TRACK_ENTRY:
+      case ID_AUDIO:
+      case ID_VIDEO:
+      case ID_CUES:
+      case ID_CUE_POINT:
+      case ID_CUE_TRACK_POSITIONS:
+      case ID_BLOCK_GROUP:
+        return EbmlReader.TYPE_MASTER;
+      case ID_EBML_READ_VERSION:
+      case ID_DOC_TYPE_READ_VERSION:
+      case ID_TIMECODE_SCALE:
+      case ID_TIME_CODE:
+      case ID_PIXEL_WIDTH:
+      case ID_PIXEL_HEIGHT:
+      case ID_CODEC_DELAY:
+      case ID_SEEK_PRE_ROLL:
+      case ID_CHANNELS:
+      case ID_CUE_TIME:
+      case ID_CUE_CLUSTER_POSITION:
+        return EbmlReader.TYPE_UNSIGNED_INT;
+      case ID_DOC_TYPE:
+      case ID_CODEC_ID:
+        return EbmlReader.TYPE_STRING;
+      case ID_SIMPLE_BLOCK:
+      case ID_BLOCK:
+      case ID_CODEC_PRIVATE:
+        return EbmlReader.TYPE_BINARY;
+      case ID_DURATION:
+      case ID_SAMPLING_FREQUENCY:
+        return EbmlReader.TYPE_FLOAT;
+      default:
+        return EbmlReader.TYPE_UNKNOWN;
+    }
+  }
+
+  /* package */ boolean onMasterElementStart(
+      int id, long elementOffsetBytes, int headerSizeBytes,
+      long contentsSizeBytes) throws ParserException {
+    switch (id) {
+      case ID_SEGMENT:
+        if (segmentStartOffsetBytes != UNKNOWN || segmentEndOffsetBytes != UNKNOWN) {
+          throw new ParserException("Multiple Segment elements not supported");
+        }
+        segmentStartOffsetBytes = elementOffsetBytes + headerSizeBytes;
+        segmentEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
+        break;
+      case ID_CUES:
+        cuesSizeBytes = headerSizeBytes + contentsSizeBytes;
+        cueTimesUs = new LongArray();
+        cueClusterPositions = new LongArray();
+        break;
+      default:
+        // pass
+    }
+    return true;
+  }
+
+  /* package */ boolean onMasterElementEnd(int id) throws ParserException {
+    switch (id) {
+      case ID_CUES:
+        buildCues();
+        return false;
+      case ID_VIDEO:
+        buildVideoFormat();
+        return true;
+      case ID_AUDIO:
+        seenAudioTrack = true;
+        return true;
+      case ID_TRACK_ENTRY:
+        if (seenAudioTrack) {
+          // Audio format has to be built here since codec private may not be available at the end
+          // of ID_AUDIO.
+          buildAudioFormat();
+        }
+        return true;
+      default:
+        return true;
+    }
+  }
+
+  /* package */ boolean onIntegerElement(int id, long value) throws ParserException {
+    switch (id) {
+      case ID_EBML_READ_VERSION:
+        // Validate that EBMLReadVersion is supported. This extractor only supports v1.
+        if (value != 1) {
+          throw new ParserException("EBMLReadVersion " + value + " not supported");
+        }
+        break;
+      case ID_DOC_TYPE_READ_VERSION:
+        // Validate that DocTypeReadVersion is supported. This extractor only supports up to v2.
+        if (value < 1 || value > 2) {
+          throw new ParserException("DocTypeReadVersion " + value + " not supported");
+        }
+        break;
+      case ID_TIMECODE_SCALE:
+        timecodeScale = value;
+        break;
+      case ID_PIXEL_WIDTH:
+        pixelWidth = (int) value;
+        break;
+      case ID_PIXEL_HEIGHT:
+        pixelHeight = (int) value;
+        break;
+      case ID_CODEC_DELAY:
+        codecDelayNs = value;
+        break;
+      case ID_SEEK_PRE_ROLL:
+        seekPreRollNs = value;
+        break;
+      case ID_CHANNELS:
+        channelCount = (int) value;
+        break;
+      case ID_CUE_TIME:
+        cueTimesUs.add(scaleTimecodeToUs(value));
+        break;
+      case ID_CUE_CLUSTER_POSITION:
+        cueClusterPositions.add(value);
+        break;
+      case ID_TIME_CODE:
+        clusterTimecodeUs = scaleTimecodeToUs(value);
+        break;
+      default:
+        // pass
+    }
+    return true;
+  }
+
+  /* package */ boolean onFloatElement(int id, double value) {
+    switch (id) {
+      case ID_DURATION:
+        durationUs = scaleTimecodeToUs((long) value);
+        break;
+      case ID_SAMPLING_FREQUENCY:
+        sampleRate = (int) value;
+        break;
+      default:
+        // pass
+    }
+    return true;
+  }
+
+  /* package */ boolean onStringElement(int id, String value) throws ParserException {
+    switch (id) {
+      case ID_DOC_TYPE:
+        // Validate that DocType is supported. This extractor only supports "webm".
+        if (!DOC_TYPE_WEBM.equals(value)) {
+          throw new ParserException("DocType " + value + " not supported");
+        }
+        break;
+      case ID_CODEC_ID:
+        // Validate that CodecID is supported. This extractor only supports "V_VP9" and "A_VORBIS".
+        if (!isCodecSupported(value)) {
+          throw new ParserException("CodecID " + value + " not supported");
+        }
+        codecId = value;
+        break;
+      default:
+        // pass
+    }
+    return true;
+  }
+
+  /* package */ boolean onBinaryElement(
+      int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
+      NonBlockingInputStream inputStream) throws ParserException {
+    switch (id) {
+      case ID_SIMPLE_BLOCK:
+      case ID_BLOCK:
+        // Please refer to http://www.matroska.org/technical/specs/index.html#simpleblock_structure
+        // and http://matroska.org/technical/specs/index.html#block_structure
+        // for info about how data is organized in SimpleBlock and Block elements respectively. They
+        // differ only in the way flags are specified.
+
+        // If we don't have a sample holder then don't consume the data.
+        if (sampleHolder == null) {
+          readResults |= RESULT_NEED_SAMPLE_HOLDER;
+          return false;
+        }
+
+        // Value of trackNumber is not used but needs to be read.
+        reader.readVarint(inputStream);
+
+        // Next three bytes have timecode and flags.
+        reader.readBytes(inputStream, simpleBlockTimecodeAndFlags, 3);
+
+        // First two bytes of the three are the relative timecode.
+        int timecode =
+            (simpleBlockTimecodeAndFlags[0] << 8) | (simpleBlockTimecodeAndFlags[1] & 0xff);
+        long timecodeUs = scaleTimecodeToUs(timecode);
+
+        // Last byte of the three has some flags and the lacing value.
+        boolean keyframe;
+        if (id == ID_BLOCK) {
+          // Matroska Block element does not self-sufficiently say whether it is a key frame or not.
+          // It depends on the existence of another element (ReferenceBlock) which may occur after
+          // the Block element. Since this extractor uses Block element only for Opus, we set the
+          // keyframe to be true always since all Opus frames are key frames.
+          keyframe = true;
+        } else {
+          keyframe = (simpleBlockTimecodeAndFlags[2] & 0x80) == 0x80;
+        }
+        boolean invisible = (simpleBlockTimecodeAndFlags[2] & 0x08) == 0x08;
+        int lacing = (simpleBlockTimecodeAndFlags[2] & 0x06) >> 1;
+
+        // Validate lacing and set info into sample holder.
+        switch (lacing) {
+          case LACING_NONE:
+            long elementEndOffsetBytes = elementOffsetBytes + headerSizeBytes + contentsSizeBytes;
+            simpleBlockTimecodeUs = clusterTimecodeUs + timecodeUs;
+            sampleHolder.flags = keyframe ? C.SAMPLE_FLAG_SYNC : 0;
+            sampleHolder.decodeOnly = invisible;
+            sampleHolder.timeUs = clusterTimecodeUs + timecodeUs;
+            sampleHolder.size = (int) (elementEndOffsetBytes - reader.getBytesRead());
+            break;
+          case LACING_EBML:
+          case LACING_FIXED:
+          case LACING_XIPH:
+          default:
+            throw new ParserException("Lacing mode " + lacing + " not supported");
+        }
+
+        if (sampleHolder.data == null || sampleHolder.data.capacity() < sampleHolder.size) {
+          sampleHolder.replaceBuffer(sampleHolder.size);
+        }
+
+        ByteBuffer outputData = sampleHolder.data;
+        if (outputData == null) {
+          reader.skipBytes(inputStream, sampleHolder.size);
+          sampleHolder.size = 0;
+        } else {
+          reader.readBytes(inputStream, outputData, sampleHolder.size);
+        }
+        readResults |= RESULT_READ_SAMPLE;
+        break;
+      case ID_CODEC_PRIVATE:
+        codecPrivate = new byte[contentsSizeBytes];
+        reader.readBytes(inputStream, codecPrivate, contentsSizeBytes);
+        break;
+      default:
+        // pass
+    }
+    return true;
+  }
+
+  private long scaleTimecodeToUs(long unscaledTimecode) {
+    return TimeUnit.NANOSECONDS.toMicros(unscaledTimecode * timecodeScale);
+  }
+
+  private boolean isCodecSupported(String codecId) {
+    return CODEC_ID_VP9.equals(codecId)
+        || CODEC_ID_OPUS.equals(codecId)
+        || CODEC_ID_VORBIS.equals(codecId);
+  }
+
+  /**
+   * Build a video {@link MediaFormat} containing recently gathered Video information, if needed.
+   *
+   * <p>Replaces the previous {@link #format} only if video width/height have changed.
+   * {@link #format} is guaranteed to not be null after calling this method. In
+   * the event that it can't be built, an {@link ParserException} will be thrown.
+   */
+  private void buildVideoFormat() throws ParserException {
+    if (pixelWidth != UNKNOWN && pixelHeight != UNKNOWN
+        && (format == null || format.width != pixelWidth || format.height != pixelHeight)) {
+      format = MediaFormat.createVideoFormat(
+          MimeTypes.VIDEO_VP9, MediaFormat.NO_VALUE, pixelWidth, pixelHeight, null);
+      readResults |= RESULT_READ_INIT;
+    } else if (format == null) {
+      throw new ParserException("Unable to build format");
+    }
+  }
+
+  /**
+   * Build an audio {@link MediaFormat} containing recently gathered Audio information, if needed.
+   *
+   * <p>Replaces the previous {@link #format} only if audio channel count/sample rate have changed.
+   * {@link #format} is guaranteed to not be null after calling this method.
+   *
+   * @throws ParserException If an error occurs when parsing codec's private data or if the format
+   *    can't be built.
+   */
+  private void buildAudioFormat() throws ParserException {
+    if (channelCount != UNKNOWN && sampleRate != UNKNOWN
+        && (format == null || format.channelCount != channelCount
+            || format.sampleRate != sampleRate)) {
+      if (CODEC_ID_VORBIS.equals(codecId)) {
+        format = MediaFormat.createAudioFormat(
+            MimeTypes.AUDIO_VORBIS, VORBIS_MAX_INPUT_SIZE,
+            channelCount, sampleRate, parseVorbisCodecPrivate());
+      } else if (CODEC_ID_OPUS.equals(codecId)) {
+        ArrayList<byte[]> opusInitializationData = new ArrayList<byte[]>(3);
+        opusInitializationData.add(codecPrivate);
+        opusInitializationData.add(ByteBuffer.allocate(Long.SIZE).putLong(codecDelayNs).array());
+        opusInitializationData.add(ByteBuffer.allocate(Long.SIZE).putLong(seekPreRollNs).array());
+        format = MediaFormat.createAudioFormat(
+            MimeTypes.AUDIO_OPUS, OPUS_MAX_INPUT_SIZE, channelCount, sampleRate,
+            opusInitializationData);
+      }
+      readResults |= RESULT_READ_INIT;
+    } else if (format == null) {
+      throw new ParserException("Unable to build format");
+    }
+  }
+
+  /**
+   * Build a {@link SegmentIndex} containing recently gathered Cues information.
+   *
+   * <p>{@link #cues} is guaranteed to not be null after calling this method. In
+   * the event that it can't be built, an {@link ParserException} will be thrown.
+   */
+  private void buildCues() throws ParserException {
+    if (segmentStartOffsetBytes == UNKNOWN) {
+      throw new ParserException("Segment start/end offsets unknown");
+    } else if (durationUs == UNKNOWN) {
+      throw new ParserException("Duration unknown");
+    } else if (cuesSizeBytes == UNKNOWN) {
+      throw new ParserException("Cues size unknown");
+    } else if (cueTimesUs == null || cueClusterPositions == null
+        || cueTimesUs.size() == 0 || cueTimesUs.size() != cueClusterPositions.size()) {
+      throw new ParserException("Invalid/missing cue points");
+    }
+    int cuePointsSize = cueTimesUs.size();
+    int[] sizes = new int[cuePointsSize];
+    long[] offsets = new long[cuePointsSize];
+    long[] durationsUs = new long[cuePointsSize];
+    long[] timesUs = new long[cuePointsSize];
+    for (int i = 0; i < cuePointsSize; i++) {
+      timesUs[i] = cueTimesUs.get(i);
+      offsets[i] = segmentStartOffsetBytes + cueClusterPositions.get(i);
+    }
+    for (int i = 0; i < cuePointsSize - 1; i++) {
+      sizes[i] = (int) (offsets[i + 1] - offsets[i]);
+      durationsUs[i] = timesUs[i + 1] - timesUs[i];
+    }
+    sizes[cuePointsSize - 1] = (int) (segmentEndOffsetBytes - offsets[cuePointsSize - 1]);
+    durationsUs[cuePointsSize - 1] = durationUs - timesUs[cuePointsSize - 1];
+    cues = new SegmentIndex((int) cuesSizeBytes, sizes, offsets, durationsUs, timesUs);
+    cueTimesUs = null;
+    cueClusterPositions = null;
+    readResults |= RESULT_READ_INDEX;
+  }
+
+  /**
+   * Parses Vorbis Codec Private data and adds it as initialization data to the {@link #format}.
+   * WebM Vorbis Codec Private data specification can be found
+   * <a href="http://matroska.org/technical/specs/codecid/index.html">here</a>.
+   *
+   * @return ArrayList of byte arrays containing the initialization data on success.
+   * @throws ParserException If parsing codec private data fails.
+   */
+  private ArrayList<byte[]> parseVorbisCodecPrivate() throws ParserException {
+    try {
+      if (codecPrivate[0] != 0x02) {
+        throw new ParserException("Error parsing vorbis codec private");
+      }
+      int offset = 1;
+      int vorbisInfoLength = 0;
+      while (codecPrivate[offset] == (byte) 0xFF) {
+        vorbisInfoLength += 0xFF;
+        offset++;
+      }
+      vorbisInfoLength += codecPrivate[offset++];
+
+      int vorbisSkipLength = 0;
+      while (codecPrivate[offset] == (byte) 0xFF) {
+        vorbisSkipLength += 0xFF;
+        offset++;
+      }
+      vorbisSkipLength += codecPrivate[offset++];
+
+      if (codecPrivate[offset] != 0x01) {
+        throw new ParserException("Error parsing vorbis codec private");
+      }
+      byte[] vorbisInfo = new byte[vorbisInfoLength];
+      System.arraycopy(codecPrivate, offset, vorbisInfo, 0, vorbisInfoLength);
+      offset += vorbisInfoLength;
+      if (codecPrivate[offset] != 0x03) {
+        throw new ParserException("Error parsing vorbis codec private");
+      }
+      offset += vorbisSkipLength;
+      if (codecPrivate[offset] != 0x05) {
+        throw new ParserException("Error parsing vorbis codec private");
+      }
+      byte[] vorbisBooks = new byte[codecPrivate.length - offset];
+      System.arraycopy(codecPrivate, offset, vorbisBooks, 0, codecPrivate.length - offset);
+      ArrayList<byte[]> initializationData = new ArrayList<byte[]>(2);
+      initializationData.add(vorbisInfo);
+      initializationData.add(vorbisBooks);
+      return initializationData;
+    } catch (ArrayIndexOutOfBoundsException e) {
+      throw new ParserException("Error parsing vorbis codec private");
+    }
+  }
+
+  /**
+   * Passes events through to {@link WebmExtractor} as
+   * callbacks from {@link EbmlReader} are received.
+   */
+  private final class InnerEbmlEventHandler implements EbmlEventHandler {
+
+    @Override
+    public int getElementType(int id) {
+      return WebmExtractor.this.getElementType(id);
+    }
+
+    @Override
+    public void onMasterElementStart(
+        int id, long elementOffsetBytes, int headerSizeBytes,
+        long contentsSizeBytes) throws ParserException {
+      WebmExtractor.this.onMasterElementStart(
+          id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes);
+    }
+
+    @Override
+    public void onMasterElementEnd(int id) throws ParserException {
+      WebmExtractor.this.onMasterElementEnd(id);
+    }
+
+    @Override
+    public void onIntegerElement(int id, long value) throws ParserException {
+      WebmExtractor.this.onIntegerElement(id, value);
+    }
+
+    @Override
+    public void onFloatElement(int id, double value) {
+      WebmExtractor.this.onFloatElement(id, value);
+    }
+
+    @Override
+    public void onStringElement(int id, String value) throws ParserException {
+      WebmExtractor.this.onStringElement(id, value);
+    }
+
+    @Override
+    public boolean onBinaryElement(
+        int id, long elementOffsetBytes, int headerSizeBytes, int contentsSizeBytes,
+        NonBlockingInputStream inputStream) throws ParserException {
+      return WebmExtractor.this.onBinaryElement(
+          id, elementOffsetBytes, headerSizeBytes, contentsSizeBytes, inputStream);
+    }
+
+  }
+
+}

+ 599 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java

@@ -0,0 +1,599 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.dash;
+
+import com.google.android.exoplayer.BehindLiveWindowException;
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.ParserException;
+import com.google.android.exoplayer.TrackInfo;
+import com.google.android.exoplayer.TrackRenderer;
+import com.google.android.exoplayer.chunk.Chunk;
+import com.google.android.exoplayer.chunk.ChunkOperationHolder;
+import com.google.android.exoplayer.chunk.ChunkSource;
+import com.google.android.exoplayer.chunk.ContainerMediaChunk;
+import com.google.android.exoplayer.chunk.Format;
+import com.google.android.exoplayer.chunk.Format.DecreasingBandwidthComparator;
+import com.google.android.exoplayer.chunk.FormatEvaluator;
+import com.google.android.exoplayer.chunk.FormatEvaluator.Evaluation;
+import com.google.android.exoplayer.chunk.MediaChunk;
+import com.google.android.exoplayer.chunk.SingleSampleMediaChunk;
+import com.google.android.exoplayer.chunk.parser.Extractor;
+import com.google.android.exoplayer.chunk.parser.mp4.FragmentedMp4Extractor;
+import com.google.android.exoplayer.chunk.parser.webm.WebmExtractor;
+import com.google.android.exoplayer.dash.mpd.AdaptationSet;
+import com.google.android.exoplayer.dash.mpd.ContentProtection;
+import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
+import com.google.android.exoplayer.dash.mpd.Period;
+import com.google.android.exoplayer.dash.mpd.RangedUri;
+import com.google.android.exoplayer.dash.mpd.Representation;
+import com.google.android.exoplayer.text.webvtt.WebvttParser;
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.upstream.DataSpec;
+import com.google.android.exoplayer.upstream.NonBlockingInputStream;
+import com.google.android.exoplayer.util.ManifestFetcher;
+import com.google.android.exoplayer.util.MimeTypes;
+
+import android.net.Uri;
+import android.os.SystemClock;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * An {@link ChunkSource} for DASH streams.
+ * <p>
+ * This implementation currently supports fMP4, webm, and webvtt.
+ */
+public class DashChunkSource implements ChunkSource {
+
+  /**
+   * Thrown when an AdaptationSet is missing from the MPD.
+   */
+  public static class NoAdaptationSetException extends IOException {
+
+    public NoAdaptationSetException(String message) {
+      super(message);
+    }
+
+  }
+
+  /**
+   * Specifies that we should process all tracks.
+   */
+  public static final int USE_ALL_TRACKS = -1;
+
+  private final TrackInfo trackInfo;
+  private final DataSource dataSource;
+  private final FormatEvaluator evaluator;
+  private final Evaluation evaluation;
+  private final StringBuilder headerBuilder;
+  private final long liveEdgeLatencyUs;
+  private final int maxWidth;
+  private final int maxHeight;
+
+  private final Format[] formats;
+  private final HashMap<String, RepresentationHolder> representationHolders;
+
+  private final ManifestFetcher<MediaPresentationDescription> manifestFetcher;
+  private final int adaptationSetIndex;
+  private final int[] representationIndices;
+  private final Map<UUID, byte[]> psshInfo;
+
+  private MediaPresentationDescription currentManifest;
+  private boolean finishedCurrentManifest;
+
+  private boolean lastChunkWasInitialization;
+  private IOException fatalError;
+
+  /**
+   * Lightweight constructor to use for fixed duration content.
+   *
+   * @param dataSource A {@link DataSource} suitable for loading the media data.
+   * @param formatEvaluator Selects from the available formats.
+   * @param representations The representations to be considered by the source.
+   */
+  public DashChunkSource(DataSource dataSource, FormatEvaluator formatEvaluator,
+      Representation... representations) {
+    this(buildManifest(Arrays.asList(representations)), 0, null, dataSource, formatEvaluator);
+  }
+
+  /**
+   * Lightweight constructor to use for fixed duration content.
+   *
+   * @param dataSource A {@link DataSource} suitable for loading the media data.
+   * @param formatEvaluator Selects from the available formats.
+   * @param representations The representations to be considered by the source.
+   */
+  public DashChunkSource(DataSource dataSource, FormatEvaluator formatEvaluator,
+      List<Representation> representations) {
+    this(buildManifest(representations), 0, null, dataSource, formatEvaluator);
+  }
+
+  /**
+   * Constructor to use for fixed duration content.
+   *
+   * @param manifest The manifest.
+   * @param adaptationSetIndex The index of the adaptation set that should be used.
+   * @param representationIndices The indices of the representations within the adaptations set
+   *     that should be used. May be null if all representations within the adaptation set should
+   *     be considered.
+   * @param dataSource A {@link DataSource} suitable for loading the media data.
+   * @param formatEvaluator Selects from the available formats.
+   */
+  public DashChunkSource(MediaPresentationDescription manifest, int adaptationSetIndex,
+      int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator) {
+    this(null, manifest, adaptationSetIndex, representationIndices, dataSource, formatEvaluator, 0);
+  }
+
+  /**
+   * Constructor to use for live streaming.
+   * <p>
+   * May also be used for fixed duration content, in which case the call is equivalent to calling
+   * the other constructor, passing {@code manifestFetcher.getManifest()} is the first argument.
+   *
+   * @param manifestFetcher A fetcher for the manifest, which must have already successfully
+   *     completed an initial load.
+   * @param adaptationSetIndex The index of the adaptation set that should be used.
+   * @param representationIndices The indices of the representations within the adaptations set
+   *     that should be used. May be null if all representations within the adaptation set should
+   *     be considered.
+   * @param dataSource A {@link DataSource} suitable for loading the media data.
+   * @param formatEvaluator Selects from the available formats.
+   * @param liveEdgeLatencyMs For live streams, the number of milliseconds that the playback should
+   *     lag behind the "live edge" (i.e. the end of the most recently defined media in the
+   *     manifest). Choosing a small value will minimize latency introduced by the player, however
+   *     note that the value sets an upper bound on the length of media that the player can buffer.
+   *     Hence a small value may increase the probability of rebuffering and playback failures.
+   */
+  public DashChunkSource(ManifestFetcher<MediaPresentationDescription> manifestFetcher,
+      int adaptationSetIndex, int[] representationIndices, DataSource dataSource,
+      FormatEvaluator formatEvaluator, long liveEdgeLatencyMs) {
+    this(manifestFetcher, manifestFetcher.getManifest(), adaptationSetIndex, representationIndices,
+        dataSource, formatEvaluator, liveEdgeLatencyMs * 1000);
+  }
+
+  private DashChunkSource(ManifestFetcher<MediaPresentationDescription> manifestFetcher,
+      MediaPresentationDescription initialManifest, int adaptationSetIndex,
+      int[] representationIndices, DataSource dataSource, FormatEvaluator formatEvaluator,
+      long liveEdgeLatencyUs) {
+    this.manifestFetcher = manifestFetcher;
+    this.currentManifest = initialManifest;
+    this.adaptationSetIndex = adaptationSetIndex;
+    this.representationIndices = representationIndices;
+    this.dataSource = dataSource;
+    this.evaluator = formatEvaluator;
+    this.liveEdgeLatencyUs = liveEdgeLatencyUs;
+    this.evaluation = new Evaluation();
+    this.headerBuilder = new StringBuilder();
+
+    psshInfo = getPsshInfo(currentManifest, adaptationSetIndex);
+    Representation[] representations = getFilteredRepresentations(currentManifest,
+        adaptationSetIndex, representationIndices);
+    long periodDurationUs = (representations[0].periodDurationMs == TrackRenderer.UNKNOWN_TIME_US)
+        ? TrackRenderer.UNKNOWN_TIME_US : representations[0].periodDurationMs * 1000;
+    this.trackInfo = new TrackInfo(representations[0].format.mimeType, periodDurationUs);
+
+    this.formats = new Format[representations.length];
+    this.representationHolders = new HashMap<String, RepresentationHolder>();
+    int maxWidth = 0;
+    int maxHeight = 0;
+    for (int i = 0; i < representations.length; i++) {
+      formats[i] = representations[i].format;
+      maxWidth = Math.max(formats[i].width, maxWidth);
+      maxHeight = Math.max(formats[i].height, maxHeight);
+      Extractor extractor = mimeTypeIsWebm(formats[i].mimeType) ? new WebmExtractor()
+          : new FragmentedMp4Extractor();
+      representationHolders.put(formats[i].id,
+          new RepresentationHolder(representations[i], extractor));
+    }
+    this.maxWidth = maxWidth;
+    this.maxHeight = maxHeight;
+    Arrays.sort(formats, new DecreasingBandwidthComparator());
+  }
+
+  @Override
+  public final void getMaxVideoDimensions(MediaFormat out) {
+    if (trackInfo.mimeType.startsWith("video")) {
+      out.setMaxVideoDimensions(maxWidth, maxHeight);
+    }
+  }
+
+  @Override
+  public final TrackInfo getTrackInfo() {
+    return trackInfo;
+  }
+
+  @Override
+  public void enable() {
+    evaluator.enable();
+    if (manifestFetcher != null) {
+      manifestFetcher.enable();
+    }
+  }
+
+  @Override
+  public void disable(List<? extends MediaChunk> queue) {
+    evaluator.disable();
+    if (manifestFetcher != null) {
+      manifestFetcher.disable();
+    }
+  }
+
+  @Override
+  public void continueBuffering(long playbackPositionUs) {
+    if (manifestFetcher == null || !currentManifest.dynamic || fatalError != null) {
+      return;
+    }
+
+    MediaPresentationDescription newManifest = manifestFetcher.getManifest();
+    if (currentManifest != newManifest && newManifest != null) {
+      Representation[] newRepresentations = DashChunkSource.getFilteredRepresentations(newManifest,
+          adaptationSetIndex, representationIndices);
+      for (Representation representation : newRepresentations) {
+        RepresentationHolder representationHolder =
+            representationHolders.get(representation.format.id);
+        DashSegmentIndex oldIndex = representationHolder.segmentIndex;
+        DashSegmentIndex newIndex = representation.getIndex();
+        int newFirstSegmentNum = newIndex.getFirstSegmentNum();
+        int segmentNumShift = oldIndex.getSegmentNum(newIndex.getTimeUs(newFirstSegmentNum))
+            - newFirstSegmentNum;
+        representationHolder.segmentNumShift += segmentNumShift;
+        representationHolder.segmentIndex = newIndex;
+      }
+      currentManifest = newManifest;
+      finishedCurrentManifest = false;
+    }
+
+    // TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where
+    // minUpdatePeriod is set to 0. In such cases we shouldn't refresh unless there is explicit
+    // signaling in the stream, according to:
+    // http://azure.microsoft.com/blog/2014/09/13/dash-live-streaming-with-azure-media-service/
+    long minUpdatePeriod = currentManifest.minUpdatePeriod;
+    if (minUpdatePeriod == 0) {
+      minUpdatePeriod = 5000;
+    }
+
+    if (finishedCurrentManifest && (SystemClock.elapsedRealtime()
+        > manifestFetcher.getManifestLoadTimestamp() + minUpdatePeriod)) {
+      manifestFetcher.requestRefresh();
+    }
+  }
+
+  @Override
+  public final void getChunkOperation(List<? extends MediaChunk> queue, long seekPositionUs,
+      long playbackPositionUs, ChunkOperationHolder out) {
+    if (fatalError != null) {
+      out.chunk = null;
+      return;
+    }
+
+    evaluation.queueSize = queue.size();
+    if (evaluation.format == null || !lastChunkWasInitialization) {
+      evaluator.evaluate(queue, playbackPositionUs, formats, evaluation);
+    }
+    Format selectedFormat = evaluation.format;
+    out.queueSize = evaluation.queueSize;
+
+    if (selectedFormat == null) {
+      out.chunk = null;
+      return;
+    } else if (out.queueSize == queue.size() && out.chunk != null
+        && out.chunk.format.id.equals(selectedFormat.id)) {
+      // We already have a chunk, and the evaluation hasn't changed either the format or the size
+      // of the queue. Leave unchanged.
+      return;
+    }
+
+    RepresentationHolder representationHolder = representationHolders.get(selectedFormat.id);
+    Representation selectedRepresentation = representationHolder.representation;
+    DashSegmentIndex segmentIndex = representationHolder.segmentIndex;
+    Extractor extractor = representationHolder.extractor;
+
+    RangedUri pendingInitializationUri = null;
+    RangedUri pendingIndexUri = null;
+
+    if (extractor.getFormat() == null) {
+      pendingInitializationUri = selectedRepresentation.getInitializationUri();
+    }
+    if (segmentIndex == null) {
+      pendingIndexUri = selectedRepresentation.getIndexUri();
+    }
+
+    if (pendingInitializationUri != null || pendingIndexUri != null) {
+      // We have initialization and/or index requests to make.
+      Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri,
+          selectedRepresentation, extractor, dataSource, evaluation.trigger);
+      lastChunkWasInitialization = true;
+      out.chunk = initializationChunk;
+      return;
+    }
+
+    // TODO: Use UtcTimingElement where possible.
+    long nowUs = System.currentTimeMillis() * 1000;
+
+    int firstAvailableSegmentNum = segmentIndex.getFirstSegmentNum();
+    int lastAvailableSegmentNum = segmentIndex.getLastSegmentNum();
+    boolean indexUnbounded = lastAvailableSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED;
+    if (indexUnbounded) {
+      // The index is itself unbounded. We need to use the current time to calculate the range of
+      // available segments.
+      long liveEdgeTimestampUs = nowUs - currentManifest.availabilityStartTime * 1000;
+      if (currentManifest.timeShiftBufferDepth != -1) {
+        long bufferDepthUs = currentManifest.timeShiftBufferDepth * 1000;
+        firstAvailableSegmentNum = Math.max(firstAvailableSegmentNum,
+            segmentIndex.getSegmentNum(liveEdgeTimestampUs - bufferDepthUs));
+      }
+      // getSegmentNum(liveEdgeTimestampUs) will not be completed yet, so subtract one to get the
+      // index of the last completed segment.
+      lastAvailableSegmentNum = segmentIndex.getSegmentNum(liveEdgeTimestampUs) - 1;
+    }
+
+    int segmentNum;
+    if (queue.isEmpty()) {
+      if (currentManifest.dynamic) {
+        seekPositionUs = getLiveSeekPosition(nowUs, indexUnbounded, segmentIndex.isExplicit());
+      }
+      segmentNum = segmentIndex.getSegmentNum(seekPositionUs);
+    } else {
+      segmentNum = queue.get(out.queueSize - 1).nextChunkIndex
+          - representationHolder.segmentNumShift;
+    }
+
+    if (currentManifest.dynamic) {
+      if (segmentNum < firstAvailableSegmentNum) {
+        // This is before the first chunk in the current manifest.
+        fatalError = new BehindLiveWindowException();
+        return;
+      } else if (segmentNum > lastAvailableSegmentNum) {
+        // This chunk is beyond the last chunk in the current manifest. If the index is bounded
+        // we'll need to refresh it. If it's unbounded we just need to wait for a while before
+        // attempting to load the chunk.
+        finishedCurrentManifest = !indexUnbounded;
+        return;
+      } else if (!indexUnbounded && segmentNum == lastAvailableSegmentNum) {
+        // This is the last chunk in a dynamic bounded manifest. We'll need to refresh the manifest
+        // to obtain the next chunk.
+        finishedCurrentManifest = true;
+      }
+    }
+
+    if (segmentNum == -1) {
+      out.chunk = null;
+      return;
+    }
+
+    Chunk nextMediaChunk = newMediaChunk(representationHolder, dataSource, segmentNum,
+        evaluation.trigger);
+    lastChunkWasInitialization = false;
+    out.chunk = nextMediaChunk;
+  }
+
+  @Override
+  public IOException getError() {
+    return fatalError != null ? fatalError
+        : (manifestFetcher != null ? manifestFetcher.getError() : null);
+  }
+
+  @Override
+  public void onChunkLoadError(Chunk chunk, Exception e) {
+    // Do nothing.
+  }
+
+  private boolean mimeTypeIsWebm(String mimeType) {
+    return mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM);
+  }
+
+  private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri,
+      Representation representation, Extractor extractor, DataSource dataSource,
+      int trigger) {
+    int expectedExtractorResult = Extractor.RESULT_END_OF_STREAM;
+    long indexAnchor = 0;
+    RangedUri requestUri;
+    if (initializationUri != null) {
+      // It's common for initialization and index data to be stored adjacently. Attempt to merge
+      // the two requests together to request both at once.
+      expectedExtractorResult |= Extractor.RESULT_READ_INIT;
+      requestUri = initializationUri.attemptMerge(indexUri);
+      if (requestUri != null) {
+        expectedExtractorResult |= Extractor.RESULT_READ_INDEX;
+        if (extractor.hasRelativeIndexOffsets()) {
+          indexAnchor = indexUri.start + indexUri.length;
+        }
+      } else {
+        requestUri = initializationUri;
+      }
+    } else {
+      requestUri = indexUri;
+      if (extractor.hasRelativeIndexOffsets()) {
+        indexAnchor = indexUri.start + indexUri.length;
+      }
+      expectedExtractorResult |= Extractor.RESULT_READ_INDEX;
+    }
+    DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
+        representation.getCacheKey());
+
+    return new InitializationLoadable(dataSource, dataSpec, trigger, representation.format,
+        extractor, expectedExtractorResult, indexAnchor);
+  }
+
+  private Chunk newMediaChunk(RepresentationHolder representationHolder, DataSource dataSource,
+      int segmentNum, int trigger) {
+    Representation representation = representationHolder.representation;
+    DashSegmentIndex segmentIndex = representationHolder.segmentIndex;
+
+    long startTimeUs = segmentIndex.getTimeUs(segmentNum);
+    long endTimeUs = startTimeUs + segmentIndex.getDurationUs(segmentNum);
+
+    boolean isLastSegment = !currentManifest.dynamic
+        && segmentNum == segmentIndex.getLastSegmentNum();
+    int nextAbsoluteSegmentNum = isLastSegment ? -1
+        : (representationHolder.segmentNumShift + segmentNum + 1);
+
+    RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum);
+    DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length,
+        representation.getCacheKey());
+
+    long presentationTimeOffsetUs = representation.presentationTimeOffsetUs;
+    if (representation.format.mimeType.equals(MimeTypes.TEXT_VTT)) {
+      if (representationHolder.vttHeaderOffsetUs != presentationTimeOffsetUs) {
+        // Update the VTT header.
+        headerBuilder.setLength(0);
+        headerBuilder.append(WebvttParser.EXO_HEADER).append("=")
+            .append(WebvttParser.OFFSET).append(presentationTimeOffsetUs).append("\n");
+        representationHolder.vttHeader = headerBuilder.toString().getBytes();
+        representationHolder.vttHeaderOffsetUs = presentationTimeOffsetUs;
+      }
+      return new SingleSampleMediaChunk(dataSource, dataSpec, representation.format, 0,
+          startTimeUs, endTimeUs, nextAbsoluteSegmentNum, null, representationHolder.vttHeader);
+    } else {
+      return new ContainerMediaChunk(dataSource, dataSpec, representation.format, trigger,
+          startTimeUs, endTimeUs, nextAbsoluteSegmentNum, representationHolder.extractor, psshInfo,
+          false, presentationTimeOffsetUs);
+    }
+  }
+
+  /**
+   * For live playbacks, determines the seek position that snaps playback to be
+   * {@link #liveEdgeLatencyUs} behind the live edge of the current manifest
+   *
+   * @param nowUs An estimate of the current server time, in microseconds.
+   * @param indexUnbounded True if the segment index for this source is unbounded. False otherwise.
+   * @param indexExplicit True if the segment index is explicit. False otherwise.
+   * @return The seek position in microseconds.
+   */
+  private long getLiveSeekPosition(long nowUs, boolean indexUnbounded, boolean indexExplicit) {
+    long liveEdgeTimestampUs;
+    if (indexUnbounded) {
+      liveEdgeTimestampUs = nowUs - currentManifest.availabilityStartTime * 1000;
+    } else {
+      liveEdgeTimestampUs = Long.MIN_VALUE;
+      for (RepresentationHolder representationHolder : representationHolders.values()) {
+        DashSegmentIndex segmentIndex = representationHolder.segmentIndex;
+        int lastSegmentNum = segmentIndex.getLastSegmentNum();
+        long indexLiveEdgeTimestampUs = segmentIndex.getTimeUs(lastSegmentNum)
+            + segmentIndex.getDurationUs(lastSegmentNum);
+        liveEdgeTimestampUs = Math.max(liveEdgeTimestampUs, indexLiveEdgeTimestampUs);
+      }
+      if (!indexExplicit) {
+        // Some segments defined by the index may not be available yet. Bound the calculated live
+        // edge based on the elapsed time since the manifest became available.
+        liveEdgeTimestampUs = Math.min(liveEdgeTimestampUs,
+            nowUs - currentManifest.availabilityStartTime * 1000);
+      }
+    }
+    return liveEdgeTimestampUs - liveEdgeLatencyUs;
+  }
+
+  private static Representation[] getFilteredRepresentations(MediaPresentationDescription manifest,
+      int adaptationSetIndex, int[] representationIndices) {
+    AdaptationSet adaptationSet = manifest.periods.get(0).adaptationSets.get(adaptationSetIndex);
+    List<Representation> representations = adaptationSet.representations;
+    if (representationIndices == null) {
+      Representation[] filteredRepresentations = new Representation[representations.size()];
+      representations.toArray(filteredRepresentations);
+      return filteredRepresentations;
+    } else {
+      Representation[] filteredRepresentations = new Representation[representationIndices.length];
+      for (int i = 0; i < representationIndices.length; i++) {
+        filteredRepresentations[i] = representations.get(representationIndices[i]);
+      }
+      return filteredRepresentations;
+    }
+  }
+
+  private static Map<UUID, byte[]> getPsshInfo(MediaPresentationDescription manifest,
+      int adaptationSetIndex) {
+    AdaptationSet adaptationSet = manifest.periods.get(0).adaptationSets.get(adaptationSetIndex);
+    if (adaptationSet.contentProtections.isEmpty()) {
+      return null;
+    } else {
+      Map<UUID, byte[]> psshInfo = new HashMap<UUID, byte[]>();
+      for (ContentProtection contentProtection : adaptationSet.contentProtections) {
+        if (contentProtection.uuid != null && contentProtection.data != null) {
+          psshInfo.put(contentProtection.uuid, contentProtection.data);
+        }
+      }
+      return psshInfo.isEmpty() ? null : psshInfo;
+    }
+  }
+
+  private static MediaPresentationDescription buildManifest(List<Representation> representations) {
+    Representation firstRepresentation = representations.get(0);
+    AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, representations);
+    Period period = new Period(null, firstRepresentation.periodStartMs,
+        firstRepresentation.periodDurationMs, Collections.singletonList(adaptationSet));
+    long duration = firstRepresentation.periodDurationMs - firstRepresentation.periodStartMs;
+    return new MediaPresentationDescription(-1, duration, -1, false, -1, -1, null,
+        Collections.singletonList(period));
+  }
+
+  private class InitializationLoadable extends Chunk {
+
+    private final Extractor extractor;
+    private final int expectedExtractorResult;
+    private final long indexAnchor;
+    private final Uri uri;
+
+    public InitializationLoadable(DataSource dataSource, DataSpec dataSpec, int trigger,
+        Format format, Extractor extractor, int expectedExtractorResult,
+        long indexAnchor) {
+      super(dataSource, dataSpec, format, trigger);
+      this.extractor = extractor;
+      this.expectedExtractorResult = expectedExtractorResult;
+      this.indexAnchor = indexAnchor;
+      this.uri = dataSpec.uri;
+    }
+
+    @Override
+    protected void consumeStream(NonBlockingInputStream stream) throws IOException {
+      int result = extractor.read(stream, null);
+      if (result != expectedExtractorResult) {
+        throw new ParserException("Invalid extractor result. Expected "
+            + expectedExtractorResult + ", got " + result);
+      }
+      if ((result & Extractor.RESULT_READ_INDEX) != 0) {
+        representationHolders.get(format.id).segmentIndex =
+            new DashWrappingSegmentIndex(extractor.getIndex(), uri, indexAnchor);
+      }
+    }
+
+  }
+
+  private static class RepresentationHolder {
+
+    public final Representation representation;
+    public final Extractor extractor;
+
+    public DashSegmentIndex segmentIndex;
+    public int segmentNumShift;
+
+    public long vttHeaderOffsetUs;
+    public byte[] vttHeader;
+
+    public RepresentationHolder(Representation representation, Extractor extractor) {
+      this.representation = representation;
+      this.extractor = extractor;
+      this.segmentIndex = representation.getIndex();
+    }
+
+  }
+
+}

+ 100 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/DashSegmentIndex.java

@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.dash;
+
+import com.google.android.exoplayer.dash.mpd.RangedUri;
+
+/**
+ * Indexes the segments within a media stream.
+ *
+ * TODO: Generalize to cover all chunk streaming modes (e.g. SmoothStreaming) if possible.
+ */
+public interface DashSegmentIndex {
+
+  public static final int INDEX_UNBOUNDED = -1;
+
+  /**
+   * Returns the segment number of the segment containing a given media time.
+   * <p>
+   * If the given media time is outside the range of the index, then the returned segment number is
+   * clamped to {@link #getFirstSegmentNum()} (if the given media time is earlier the start of the
+   * first segment) or {@link #getLastSegmentNum()} (if the given media time is later then the end
+   * of the last segment).
+   *
+   * @param timeUs The time in microseconds.
+   * @return The segment number of the corresponding segment.
+   */
+  int getSegmentNum(long timeUs);
+
+  /**
+   * Returns the start time of a segment.
+   *
+   * @param segmentNum The segment number.
+   * @return The corresponding start time in microseconds.
+   */
+  long getTimeUs(int segmentNum);
+
+  /**
+   * Returns the duration of a segment.
+   *
+   * @param segmentNum The segment number.
+   * @return The duration of the segment, in microseconds.
+   */
+  long getDurationUs(int segmentNum);
+
+  /**
+   * Returns a {@link RangedUri} defining the location of a segment.
+   *
+   * @param segmentNum The segment number.
+   * @return The {@link RangedUri} defining the location of the data.
+   */
+  RangedUri getSegmentUrl(int segmentNum);
+
+  /**
+   * Returns the segment number of the first segment.
+   *
+   * @return The segment number of the first segment.
+   */
+  int getFirstSegmentNum();
+
+  /**
+   * Returns the segment number of the last segment, or {@link #INDEX_UNBOUNDED}.
+   * <p>
+   * An unbounded index occurs if a live stream manifest uses SegmentTemplate elements without a
+   * SegmentTimeline element. In this case the manifest can be used to derive information about
+   * segments arbitrarily far into the future. This means that the manifest does not need to be
+   * refreshed as frequently (if at all) during playback, however it is necessary for a player to
+   * manually calculate the window of currently available segments.
+   *
+   * @return The segment number of the last segment, or {@link #INDEX_UNBOUNDED}.
+   */
+  int getLastSegmentNum();
+
+  /**
+   * Returns true if segments are defined explicitly by the index.
+   * <p>
+   * If true is returned, each segment is defined explicitly by the index data, and all of the
+   * listed segments are guaranteed to be available at the time when the index was obtained.
+   * <p>
+   * If false is returned then segment information was derived from properties such as a fixed
+   * segment duration. If the presentation is dynamic, it's possible that only a subset of the
+   * segments are available.
+   *
+   * @return True if segments are defined explicitly by the index. False otherwise.
+   */
+  boolean isExplicit();
+
+}

+ 75 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/DashSingleSegmentIndex.java

@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.dash;
+
+import com.google.android.exoplayer.dash.mpd.RangedUri;
+
+/**
+ * A {@link DashSegmentIndex} that defines a single segment.
+ */
+public class DashSingleSegmentIndex implements DashSegmentIndex {
+
+  private final long startTimeUs;
+  private final long durationUs;
+  private final RangedUri uri;
+
+  /**
+   * @param startTimeUs The start time of the segment, in microseconds.
+   * @param durationUs The duration of the segment, in microseconds.
+   * @param uri A {@link RangedUri} defining the location of the segment data.
+   */
+  public DashSingleSegmentIndex(long startTimeUs, long durationUs, RangedUri uri) {
+    this.startTimeUs = startTimeUs;
+    this.durationUs = durationUs;
+    this.uri = uri;
+  }
+
+  @Override
+  public int getSegmentNum(long timeUs) {
+    return 0;
+  }
+
+  @Override
+  public long getTimeUs(int segmentNum) {
+    return startTimeUs;
+  }
+
+  @Override
+  public long getDurationUs(int segmentNum) {
+    return durationUs;
+  }
+
+  @Override
+  public RangedUri getSegmentUrl(int segmentNum) {
+    return uri;
+  }
+
+  @Override
+  public int getFirstSegmentNum() {
+    return 0;
+  }
+
+  @Override
+  public int getLastSegmentNum() {
+    return 0;
+  }
+
+  @Override
+  public boolean isExplicit() {
+    return true;
+  }
+
+}

+ 82 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/DashWrappingSegmentIndex.java

@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.dash;
+
+import com.google.android.exoplayer.chunk.parser.SegmentIndex;
+import com.google.android.exoplayer.dash.mpd.RangedUri;
+import com.google.android.exoplayer.util.Util;
+
+import android.net.Uri;
+
+/**
+ * An implementation of {@link DashSegmentIndex} that wraps a {@link SegmentIndex} parsed from a
+ * media stream.
+ */
+public class DashWrappingSegmentIndex implements DashSegmentIndex {
+
+  private final SegmentIndex segmentIndex;
+  private final Uri uri;
+  private final long indexAnchor;
+
+  /**
+   * @param segmentIndex The {@link SegmentIndex} to wrap.
+   * @param uri The {@link Uri} where the data is located.
+   * @param indexAnchor The index anchor point. This value is added to the byte offsets specified
+   *     in the wrapped {@link SegmentIndex}.
+   */
+  public DashWrappingSegmentIndex(SegmentIndex segmentIndex, Uri uri, long indexAnchor) {
+    this.segmentIndex = segmentIndex;
+    this.uri = uri;
+    this.indexAnchor = indexAnchor;
+  }
+
+  @Override
+  public int getFirstSegmentNum() {
+    return 0;
+  }
+
+  @Override
+  public int getLastSegmentNum() {
+    return segmentIndex.length - 1;
+  }
+
+  @Override
+  public long getTimeUs(int segmentNum) {
+    return segmentIndex.timesUs[segmentNum];
+  }
+
+  @Override
+  public long getDurationUs(int segmentNum) {
+    return segmentIndex.durationsUs[segmentNum];
+  }
+
+  @Override
+  public RangedUri getSegmentUrl(int segmentNum) {
+    return new RangedUri(uri, null, indexAnchor + segmentIndex.offsets[segmentNum],
+        segmentIndex.sizes[segmentNum]);
+  }
+
+  @Override
+  public int getSegmentNum(long timeUs) {
+    return Util.binarySearchFloor(segmentIndex.timesUs, timeUs, true, true);
+  }
+
+  @Override
+  public boolean isExplicit() {
+    return true;
+  }
+
+}

+ 58 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/mpd/AdaptationSet.java

@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.dash.mpd;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents a set of interchangeable encoded versions of a media content component.
+ */
+public class AdaptationSet {
+
+  public static final int TYPE_UNKNOWN = -1;
+  public static final int TYPE_VIDEO = 0;
+  public static final int TYPE_AUDIO = 1;
+  public static final int TYPE_TEXT = 2;
+
+  public final int id;
+
+  public final int type;
+
+  public final List<Representation> representations;
+  public final List<ContentProtection> contentProtections;
+
+  public AdaptationSet(int id, int type, List<Representation> representations,
+      List<ContentProtection> contentProtections) {
+    this.id = id;
+    this.type = type;
+    this.representations = Collections.unmodifiableList(representations);
+    if (contentProtections == null) {
+      this.contentProtections = Collections.emptyList();
+    } else {
+      this.contentProtections = Collections.unmodifiableList(contentProtections);
+    }
+  }
+
+  public AdaptationSet(int id, int type, List<Representation> representations) {
+    this(id, type, representations, null);
+  }
+
+  public boolean hasContentProtection() {
+    return !contentProtections.isEmpty();
+  }
+
+}

+ 83 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/mpd/ContentProtection.java

@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.dash.mpd;
+
+import com.google.android.exoplayer.util.Util;
+
+import java.util.Arrays;
+import java.util.UUID;
+
+/**
+ * Represents a ContentProtection tag in an AdaptationSet.
+ */
+public class ContentProtection {
+
+  /**
+   * Identifies the content protection scheme.
+   */
+  public final String schemeUriId;
+
+  /**
+   * The UUID of the protection scheme. May be null.
+   */
+  public final UUID uuid;
+
+  /**
+   * Protection scheme specific data. May be null.
+   */
+  public final byte[] data;
+
+  /**
+   * @param schemeUriId Identifies the content protection scheme.
+   * @param uuid The UUID of the protection scheme, if known. May be null.
+   * @param data Protection scheme specific initialization data. May be null.
+   */
+  public ContentProtection(String schemeUriId, UUID uuid, byte[] data) {
+    this.schemeUriId = schemeUriId;
+    this.uuid = uuid;
+    this.data = data;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (!(obj instanceof ContentProtection)) {
+      return false;
+    }
+    if (obj == this) {
+      return true;
+    }
+
+    ContentProtection other = (ContentProtection) obj;
+    return schemeUriId.equals(other.schemeUriId)
+        && Util.areEqual(uuid, other.uuid)
+        && Arrays.equals(data, other.data);
+  }
+
+  @Override
+  public int hashCode() {
+    int hashCode = 1;
+
+    hashCode = hashCode * 37 + schemeUriId.hashCode();
+    if (uuid != null) {
+      hashCode = hashCode * 37 + uuid.hashCode();
+    }
+    if (data != null) {
+      hashCode = hashCode * 37 + Arrays.hashCode(data);
+    }
+    return hashCode;
+  }
+
+}

+ 55 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java

@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.dash.mpd;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents a DASH media presentation description (mpd).
+ */
+public class MediaPresentationDescription {
+
+  public final long availabilityStartTime;
+
+  public final long duration;
+
+  public final long minBufferTime;
+
+  public final boolean dynamic;
+
+  public final long minUpdatePeriod;
+
+  public final long timeShiftBufferDepth;
+
+  public final List<Period> periods;
+
+  public final UtcTimingElement utcTiming;
+
+  public MediaPresentationDescription(long availabilityStartTime, long duration, long minBufferTime,
+      boolean dynamic, long minUpdatePeriod, long timeShiftBufferDepth, UtcTimingElement utcTiming,
+      List<Period> periods) {
+    this.availabilityStartTime = availabilityStartTime;
+    this.duration = duration;
+    this.minBufferTime = minBufferTime;
+    this.dynamic = dynamic;
+    this.minUpdatePeriod = minUpdatePeriod;
+    this.timeShiftBufferDepth = timeShiftBufferDepth;
+    this.utcTiming = utcTiming;
+    this.periods = Collections.unmodifiableList(periods);
+  }
+
+}

+ 691 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java

@@ -0,0 +1,691 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.dash.mpd;
+
+import com.google.android.exoplayer.ParserException;
+import com.google.android.exoplayer.chunk.Format;
+import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentList;
+import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentTemplate;
+import com.google.android.exoplayer.dash.mpd.SegmentBase.SegmentTimelineElement;
+import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase;
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.ManifestParser;
+import com.google.android.exoplayer.util.MimeTypes;
+import com.google.android.exoplayer.util.Util;
+
+import android.net.Uri;
+import android.text.TextUtils;
+
+import org.xml.sax.helpers.DefaultHandler;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * A parser of media presentation description files.
+ */
+public class MediaPresentationDescriptionParser extends DefaultHandler
+    implements ManifestParser<MediaPresentationDescription> {
+
+  private final XmlPullParserFactory xmlParserFactory;
+
+  public MediaPresentationDescriptionParser() {
+    try {
+      xmlParserFactory = XmlPullParserFactory.newInstance();
+    } catch (XmlPullParserException e) {
+      throw new RuntimeException("Couldn't create XmlPullParserFactory instance", e);
+    }
+  }
+
+  // MPD parsing.
+
+  @Override
+  public MediaPresentationDescription parse(InputStream inputStream, String inputEncoding,
+      String contentId, Uri baseUrl) throws IOException, ParserException {
+    try {
+      XmlPullParser xpp = xmlParserFactory.newPullParser();
+      xpp.setInput(inputStream, inputEncoding);
+      int eventType = xpp.next();
+      if (eventType != XmlPullParser.START_TAG || !"MPD".equals(xpp.getName())) {
+        throw new ParserException(
+            "inputStream does not contain a valid media presentation description");
+      }
+      return parseMediaPresentationDescription(xpp, contentId, baseUrl);
+    } catch (XmlPullParserException e) {
+      throw new ParserException(e);
+    } catch (ParseException e) {
+      throw new ParserException(e);
+    }
+  }
+
+  protected MediaPresentationDescription parseMediaPresentationDescription(XmlPullParser xpp,
+      String contentId, Uri baseUrl) throws XmlPullParserException, IOException, ParseException {
+    long availabilityStartTime = parseDateTime(xpp, "availabilityStartTime", -1);
+    long durationMs = parseDuration(xpp, "mediaPresentationDuration", -1);
+    long minBufferTimeMs = parseDuration(xpp, "minBufferTime", -1);
+    String typeString = xpp.getAttributeValue(null, "type");
+    boolean dynamic = (typeString != null) ? typeString.equals("dynamic") : false;
+    long minUpdateTimeMs = (dynamic) ? parseDuration(xpp, "minimumUpdatePeriod", -1) : -1;
+    long timeShiftBufferDepthMs = (dynamic) ? parseDuration(xpp, "timeShiftBufferDepth", -1)
+        : -1;
+    UtcTimingElement utcTiming = null;
+
+    List<Period> periods = new ArrayList<Period>();
+    do {
+      xpp.next();
+      if (isStartTag(xpp, "BaseURL")) {
+        baseUrl = parseBaseUrl(xpp, baseUrl);
+      } else if (isStartTag(xpp, "UTCTiming")) {
+        utcTiming = parseUtcTiming(xpp);
+      } else if (isStartTag(xpp, "Period")) {
+        periods.add(parsePeriod(xpp, contentId, baseUrl, durationMs));
+      }
+    } while (!isEndTag(xpp, "MPD"));
+
+    return buildMediaPresentationDescription(availabilityStartTime, durationMs, minBufferTimeMs,
+        dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, utcTiming, periods);
+  }
+
+  protected MediaPresentationDescription buildMediaPresentationDescription(
+      long availabilityStartTime, long durationMs, long minBufferTimeMs, boolean dynamic,
+      long minUpdateTimeMs, long timeShiftBufferDepthMs, UtcTimingElement utcTiming,
+      List<Period> periods) {
+    return new MediaPresentationDescription(availabilityStartTime, durationMs, minBufferTimeMs,
+        dynamic, minUpdateTimeMs, timeShiftBufferDepthMs, utcTiming, periods);
+  }
+
+  protected UtcTimingElement parseUtcTiming(XmlPullParser xpp) {
+    String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri");
+    String value = xpp.getAttributeValue(null, "value");
+    return buildUtcTimingElement(schemeIdUri, value);
+  }
+
+  protected UtcTimingElement buildUtcTimingElement(String schemeIdUri, String value) {
+    return new UtcTimingElement(schemeIdUri, value);
+  }
+
+  protected Period parsePeriod(XmlPullParser xpp, String contentId, Uri baseUrl, long mpdDurationMs)
+      throws XmlPullParserException, IOException {
+    String id = xpp.getAttributeValue(null, "id");
+    long startMs = parseDuration(xpp, "start", 0);
+    long durationMs = parseDuration(xpp, "duration", mpdDurationMs);
+    SegmentBase segmentBase = null;
+    List<AdaptationSet> adaptationSets = new ArrayList<AdaptationSet>();
+    do {
+      xpp.next();
+      if (isStartTag(xpp, "BaseURL")) {
+        baseUrl = parseBaseUrl(xpp, baseUrl);
+      } else if (isStartTag(xpp, "AdaptationSet")) {
+        adaptationSets.add(parseAdaptationSet(xpp, contentId, baseUrl, startMs, durationMs,
+            segmentBase));
+      } else if (isStartTag(xpp, "SegmentBase")) {
+        segmentBase = parseSegmentBase(xpp, baseUrl, null);
+      } else if (isStartTag(xpp, "SegmentList")) {
+        segmentBase = parseSegmentList(xpp, baseUrl, null, durationMs);
+      } else if (isStartTag(xpp, "SegmentTemplate")) {
+        segmentBase = parseSegmentTemplate(xpp, baseUrl, null, durationMs);
+      }
+    } while (!isEndTag(xpp, "Period"));
+
+    return buildPeriod(id, startMs, durationMs, adaptationSets);
+  }
+
+  protected Period buildPeriod(
+      String id, long startMs, long durationMs, List<AdaptationSet> adaptationSets) {
+    return new Period(id, startMs, durationMs, adaptationSets);
+  }
+
+  // AdaptationSet parsing.
+
+  protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String contentId, Uri baseUrl,
+      long periodStartMs, long periodDurationMs, SegmentBase segmentBase)
+      throws XmlPullParserException, IOException {
+
+    String mimeType = xpp.getAttributeValue(null, "mimeType");
+    String language = xpp.getAttributeValue(null, "lang");
+    int contentType = parseAdaptationSetTypeFromMimeType(mimeType);
+
+    int id = -1;
+    ContentProtectionsBuilder contentProtectionsBuilder = new ContentProtectionsBuilder();
+    List<Representation> representations = new ArrayList<Representation>();
+    do {
+      xpp.next();
+      if (isStartTag(xpp, "BaseURL")) {
+        baseUrl = parseBaseUrl(xpp, baseUrl);
+      } else if (isStartTag(xpp, "ContentProtection")) {
+        contentProtectionsBuilder.addAdaptationSetProtection(parseContentProtection(xpp));
+      } else if (isStartTag(xpp, "ContentComponent")) {
+        id = Integer.parseInt(xpp.getAttributeValue(null, "id"));
+        contentType = checkAdaptationSetTypeConsistency(contentType,
+            parseAdaptationSetType(xpp.getAttributeValue(null, "contentType")));
+      } else if (isStartTag(xpp, "Representation")) {
+        Representation representation = parseRepresentation(xpp, contentId, baseUrl, periodStartMs,
+            periodDurationMs, mimeType, language, segmentBase, contentProtectionsBuilder);
+        contentProtectionsBuilder.endRepresentation();
+        contentType = checkAdaptationSetTypeConsistency(contentType,
+            parseAdaptationSetTypeFromMimeType(representation.format.mimeType));
+        representations.add(representation);
+      } else if (isStartTag(xpp, "SegmentBase")) {
+        segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase);
+      } else if (isStartTag(xpp, "SegmentList")) {
+        segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase, periodDurationMs);
+      } else if (isStartTag(xpp, "SegmentTemplate")) {
+        segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase,
+            periodDurationMs);
+      } else if (isStartTag(xpp)) {
+        parseAdaptationSetChild(xpp);
+      }
+    } while (!isEndTag(xpp, "AdaptationSet"));
+
+    return buildAdaptationSet(id, contentType, representations, contentProtectionsBuilder.build());
+  }
+
+  protected AdaptationSet buildAdaptationSet(int id, int contentType,
+      List<Representation> representations, List<ContentProtection> contentProtections) {
+    return new AdaptationSet(id, contentType, representations, contentProtections);
+  }
+
+  protected int parseAdaptationSetType(String contentType) {
+    return TextUtils.isEmpty(contentType) ? AdaptationSet.TYPE_UNKNOWN
+        : MimeTypes.BASE_TYPE_AUDIO.equals(contentType) ? AdaptationSet.TYPE_AUDIO
+        : MimeTypes.BASE_TYPE_VIDEO.equals(contentType) ? AdaptationSet.TYPE_VIDEO
+        : MimeTypes.BASE_TYPE_TEXT.equals(contentType) ? AdaptationSet.TYPE_TEXT
+        : AdaptationSet.TYPE_UNKNOWN;
+  }
+
+  protected int parseAdaptationSetTypeFromMimeType(String mimeType) {
+    return TextUtils.isEmpty(mimeType) ? AdaptationSet.TYPE_UNKNOWN
+        : MimeTypes.isAudio(mimeType) ? AdaptationSet.TYPE_AUDIO
+        : MimeTypes.isVideo(mimeType) ? AdaptationSet.TYPE_VIDEO
+        : MimeTypes.isText(mimeType) || MimeTypes.isTtml(mimeType) ? AdaptationSet.TYPE_TEXT
+        : AdaptationSet.TYPE_UNKNOWN;
+  }
+
+  /**
+   * Checks two adaptation set types for consistency, returning the consistent type, or throwing an
+   * {@link IllegalStateException} if the types are inconsistent.
+   * <p>
+   * Two types are consistent if they are equal, or if one is {@link AdaptationSet#TYPE_UNKNOWN}.
+   * Where one of the types is {@link AdaptationSet#TYPE_UNKNOWN}, the other is returned.
+   *
+   * @param firstType The first type.
+   * @param secondType The second type.
+   * @return The consistent type.
+   */
+  private int checkAdaptationSetTypeConsistency(int firstType, int secondType) {
+    if (firstType == AdaptationSet.TYPE_UNKNOWN) {
+      return secondType;
+    } else if (secondType == AdaptationSet.TYPE_UNKNOWN) {
+      return firstType;
+    } else {
+      Assertions.checkState(firstType == secondType);
+      return firstType;
+    }
+  }
+
+  /**
+   * Parses a ContentProtection element.
+   *
+   * @throws XmlPullParserException If an error occurs parsing the element.
+   * @throws IOException If an error occurs reading the element.
+   **/
+  protected ContentProtection parseContentProtection(XmlPullParser xpp)
+      throws XmlPullParserException, IOException {
+    String schemeIdUri = xpp.getAttributeValue(null, "schemeIdUri");
+    return buildContentProtection(schemeIdUri);
+  }
+
+  protected ContentProtection buildContentProtection(String schemeIdUri) {
+    return new ContentProtection(schemeIdUri, null, null);
+  }
+
+  /**
+   * Parses children of AdaptationSet elements not specifically parsed elsewhere.
+   *
+   * @param xpp The XmpPullParser from which the AdaptationSet child should be parsed.
+   * @throws XmlPullParserException If an error occurs parsing the element.
+   * @throws IOException If an error occurs reading the element.
+   **/
+  protected void parseAdaptationSetChild(XmlPullParser xpp)
+      throws XmlPullParserException, IOException {
+    // pass
+  }
+
+  // Representation parsing.
+
+  protected Representation parseRepresentation(XmlPullParser xpp, String contentId, Uri baseUrl,
+      long periodStartMs, long periodDurationMs, String mimeType, String language,
+      SegmentBase segmentBase, ContentProtectionsBuilder contentProtectionsBuilder)
+      throws XmlPullParserException, IOException {
+    String id = xpp.getAttributeValue(null, "id");
+    int bandwidth = parseInt(xpp, "bandwidth");
+    int audioSamplingRate = parseInt(xpp, "audioSamplingRate");
+    int width = parseInt(xpp, "width");
+    int height = parseInt(xpp, "height");
+    mimeType = parseString(xpp, "mimeType", mimeType);
+    String codecs = parseString(xpp, "codecs", null);
+
+    int numChannels = -1;
+    do {
+      xpp.next();
+      if (isStartTag(xpp, "BaseURL")) {
+        baseUrl = parseBaseUrl(xpp, baseUrl);
+      } else if (isStartTag(xpp, "AudioChannelConfiguration")) {
+        numChannels = Integer.parseInt(xpp.getAttributeValue(null, "value"));
+      } else if (isStartTag(xpp, "SegmentBase")) {
+        segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase);
+      } else if (isStartTag(xpp, "SegmentList")) {
+        segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase, periodDurationMs);
+      } else if (isStartTag(xpp, "SegmentTemplate")) {
+        segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase,
+            periodDurationMs);
+      } else if (isStartTag(xpp, "ContentProtection")) {
+        contentProtectionsBuilder.addRepresentationProtection(parseContentProtection(xpp));
+      }
+    } while (!isEndTag(xpp, "Representation"));
+
+    Format format = buildFormat(id, mimeType, width, height, numChannels, audioSamplingRate,
+        bandwidth, language, codecs);
+    return buildRepresentation(periodStartMs, periodDurationMs, contentId, -1, format,
+        segmentBase != null ? segmentBase : new SingleSegmentBase(baseUrl));
+  }
+
+  protected Format buildFormat(String id, String mimeType, int width, int height, int numChannels,
+      int audioSamplingRate, int bandwidth, String language, String codecs) {
+    return new Format(id, mimeType, width, height, numChannels, audioSamplingRate, bandwidth,
+        language, codecs);
+  }
+
+  protected Representation buildRepresentation(long periodStartMs, long periodDurationMs,
+      String contentId, int revisionId, Format format, SegmentBase segmentBase) {
+    return Representation.newInstance(periodStartMs, periodDurationMs, contentId, revisionId,
+        format, segmentBase);
+  }
+
+  // SegmentBase, SegmentList and SegmentTemplate parsing.
+
+  protected SingleSegmentBase parseSegmentBase(XmlPullParser xpp, Uri baseUrl,
+      SingleSegmentBase parent) throws XmlPullParserException, IOException {
+
+    long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
+    long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
+        parent != null ? parent.presentationTimeOffset : 0);
+
+    long indexStart = parent != null ? parent.indexStart : 0;
+    long indexLength = parent != null ? parent.indexLength : -1;
+    String indexRangeText = xpp.getAttributeValue(null, "indexRange");
+    if (indexRangeText != null) {
+      String[] indexRange = indexRangeText.split("-");
+      indexStart = Long.parseLong(indexRange[0]);
+      indexLength = Long.parseLong(indexRange[1]) - indexStart + 1;
+    }
+
+    RangedUri initialization = parent != null ? parent.initialization : null;
+    do {
+      xpp.next();
+      if (isStartTag(xpp, "Initialization")) {
+        initialization = parseInitialization(xpp, baseUrl);
+      }
+    } while (!isEndTag(xpp, "SegmentBase"));
+
+    return buildSingleSegmentBase(initialization, timescale, presentationTimeOffset, baseUrl,
+        indexStart, indexLength);
+  }
+
+  protected SingleSegmentBase buildSingleSegmentBase(RangedUri initialization, long timescale,
+      long presentationTimeOffset, Uri baseUrl, long indexStart, long indexLength) {
+    return new SingleSegmentBase(initialization, timescale, presentationTimeOffset, baseUrl,
+        indexStart, indexLength);
+  }
+
+  protected SegmentList parseSegmentList(XmlPullParser xpp, Uri baseUrl, SegmentList parent,
+      long periodDurationMs) throws XmlPullParserException, IOException {
+
+    long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
+    long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
+        parent != null ? parent.presentationTimeOffset : 0);
+    long duration = parseLong(xpp, "duration", parent != null ? parent.duration : -1);
+    int startNumber = parseInt(xpp, "startNumber", parent != null ? parent.startNumber : 1);
+
+    RangedUri initialization = null;
+    List<SegmentTimelineElement> timeline = null;
+    List<RangedUri> segments = null;
+
+    do {
+      xpp.next();
+      if (isStartTag(xpp, "Initialization")) {
+        initialization = parseInitialization(xpp, baseUrl);
+      } else if (isStartTag(xpp, "SegmentTimeline")) {
+        timeline = parseSegmentTimeline(xpp);
+      } else if (isStartTag(xpp, "SegmentURL")) {
+        if (segments == null) {
+          segments = new ArrayList<RangedUri>();
+        }
+        segments.add(parseSegmentUrl(xpp, baseUrl));
+      }
+    } while (!isEndTag(xpp, "SegmentList"));
+
+    if (parent != null) {
+      initialization = initialization != null ? initialization : parent.initialization;
+      timeline = timeline != null ? timeline : parent.segmentTimeline;
+      segments = segments != null ? segments : parent.mediaSegments;
+    }
+
+    return buildSegmentList(initialization, timescale, presentationTimeOffset, periodDurationMs,
+        startNumber, duration, timeline, segments);
+  }
+
+  protected SegmentList buildSegmentList(RangedUri initialization, long timescale,
+      long presentationTimeOffset, long periodDurationMs, int startNumber, long duration,
+      List<SegmentTimelineElement> timeline, List<RangedUri> segments) {
+    return new SegmentList(initialization, timescale, presentationTimeOffset, periodDurationMs,
+        startNumber, duration, timeline, segments);
+  }
+
+  protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, Uri baseUrl,
+      SegmentTemplate parent, long periodDurationMs) throws XmlPullParserException, IOException {
+
+    long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
+    long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
+        parent != null ? parent.presentationTimeOffset : 0);
+    long duration = parseLong(xpp, "duration", parent != null ? parent.duration : -1);
+    int startNumber = parseInt(xpp, "startNumber", parent != null ? parent.startNumber : 1);
+    UrlTemplate mediaTemplate = parseUrlTemplate(xpp, "media",
+        parent != null ? parent.mediaTemplate : null);
+    UrlTemplate initializationTemplate = parseUrlTemplate(xpp, "initialization",
+        parent != null ? parent.initializationTemplate : null);
+
+    RangedUri initialization = null;
+    List<SegmentTimelineElement> timeline = null;
+
+    do {
+      xpp.next();
+      if (isStartTag(xpp, "Initialization")) {
+        initialization = parseInitialization(xpp, baseUrl);
+      } else if (isStartTag(xpp, "SegmentTimeline")) {
+        timeline = parseSegmentTimeline(xpp);
+      }
+    } while (!isEndTag(xpp, "SegmentTemplate"));
+
+    if (parent != null) {
+      initialization = initialization != null ? initialization : parent.initialization;
+      timeline = timeline != null ? timeline : parent.segmentTimeline;
+    }
+
+    return buildSegmentTemplate(initialization, timescale, presentationTimeOffset, periodDurationMs,
+        startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl);
+  }
+
+  protected SegmentTemplate buildSegmentTemplate(RangedUri initialization, long timescale,
+      long presentationTimeOffset, long periodDurationMs, int startNumber, long duration,
+      List<SegmentTimelineElement> timeline, UrlTemplate initializationTemplate,
+      UrlTemplate mediaTemplate, Uri baseUrl) {
+    return new SegmentTemplate(initialization, timescale, presentationTimeOffset, periodDurationMs,
+        startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl);
+  }
+
+  protected List<SegmentTimelineElement> parseSegmentTimeline(XmlPullParser xpp)
+      throws XmlPullParserException, IOException {
+    List<SegmentTimelineElement> segmentTimeline = new ArrayList<SegmentTimelineElement>();
+    long elapsedTime = 0;
+    do {
+      xpp.next();
+      if (isStartTag(xpp, "S")) {
+        elapsedTime = parseLong(xpp, "t", elapsedTime);
+        long duration = parseLong(xpp, "d");
+        int count = 1 + parseInt(xpp, "r", 0);
+        for (int i = 0; i < count; i++) {
+          segmentTimeline.add(buildSegmentTimelineElement(elapsedTime, duration));
+          elapsedTime += duration;
+        }
+      }
+    } while (!isEndTag(xpp, "SegmentTimeline"));
+    return segmentTimeline;
+  }
+
+  protected SegmentTimelineElement buildSegmentTimelineElement(long elapsedTime, long duration) {
+    return new SegmentTimelineElement(elapsedTime, duration);
+  }
+
+  protected UrlTemplate parseUrlTemplate(XmlPullParser xpp, String name,
+      UrlTemplate defaultValue) {
+    String valueString = xpp.getAttributeValue(null, name);
+    if (valueString != null) {
+      return UrlTemplate.compile(valueString);
+    }
+    return defaultValue;
+  }
+
+  protected RangedUri parseInitialization(XmlPullParser xpp, Uri baseUrl) {
+    return parseRangedUrl(xpp, baseUrl, "sourceURL", "range");
+  }
+
+  protected RangedUri parseSegmentUrl(XmlPullParser xpp, Uri baseUrl) {
+    return parseRangedUrl(xpp, baseUrl, "media", "mediaRange");
+  }
+
+  protected RangedUri parseRangedUrl(XmlPullParser xpp, Uri baseUrl, String urlAttribute,
+      String rangeAttribute) {
+    String urlText = xpp.getAttributeValue(null, urlAttribute);
+    long rangeStart = 0;
+    long rangeLength = -1;
+    String rangeText = xpp.getAttributeValue(null, rangeAttribute);
+    if (rangeText != null) {
+      String[] rangeTextArray = rangeText.split("-");
+      rangeStart = Long.parseLong(rangeTextArray[0]);
+      rangeLength = Long.parseLong(rangeTextArray[1]) - rangeStart + 1;
+    }
+    return buildRangedUri(baseUrl, urlText, rangeStart, rangeLength);
+  }
+
+  protected RangedUri buildRangedUri(Uri baseUrl, String urlText, long rangeStart,
+      long rangeLength) {
+    return new RangedUri(baseUrl, urlText, rangeStart, rangeLength);
+  }
+
+  // Utility methods.
+
+  protected static boolean isEndTag(XmlPullParser xpp, String name) throws XmlPullParserException {
+    return xpp.getEventType() == XmlPullParser.END_TAG && name.equals(xpp.getName());
+  }
+
+  protected static boolean isStartTag(XmlPullParser xpp, String name)
+      throws XmlPullParserException {
+    return xpp.getEventType() == XmlPullParser.START_TAG && name.equals(xpp.getName());
+  }
+
+  protected static boolean isStartTag(XmlPullParser xpp) throws XmlPullParserException {
+    return xpp.getEventType() == XmlPullParser.START_TAG;
+  }
+
+  protected static long parseDuration(XmlPullParser xpp, String name, long defaultValue) {
+    String value = xpp.getAttributeValue(null, name);
+    if (value == null) {
+      return defaultValue;
+    } else {
+      return Util.parseXsDuration(value);
+    }
+  }
+
+  protected static long parseDateTime(XmlPullParser xpp, String name, long defaultValue)
+      throws ParseException {
+    String value = xpp.getAttributeValue(null, name);
+    if (value == null) {
+      return defaultValue;
+    } else {
+      return Util.parseXsDateTime(value);
+    }
+  }
+
+  protected static Uri parseBaseUrl(XmlPullParser xpp, Uri parentBaseUrl)
+      throws XmlPullParserException, IOException {
+    xpp.next();
+    String newBaseUrlText = xpp.getText();
+    Uri newBaseUri = Uri.parse(newBaseUrlText);
+    if (!newBaseUri.isAbsolute()) {
+      newBaseUri = Uri.withAppendedPath(parentBaseUrl, newBaseUrlText);
+    }
+    return newBaseUri;
+  }
+
+  protected static int parseInt(XmlPullParser xpp, String name) {
+    return parseInt(xpp, name, -1);
+  }
+
+  protected static int parseInt(XmlPullParser xpp, String name, int defaultValue) {
+    String value = xpp.getAttributeValue(null, name);
+    return value == null ? defaultValue : Integer.parseInt(value);
+  }
+
+  protected static long parseLong(XmlPullParser xpp, String name) {
+    return parseLong(xpp, name, -1);
+  }
+
+  protected static long parseLong(XmlPullParser xpp, String name, long defaultValue) {
+    String value = xpp.getAttributeValue(null, name);
+    return value == null ? defaultValue : Long.parseLong(value);
+  }
+
+  protected static String parseString(XmlPullParser xpp, String name, String defaultValue) {
+    String value = xpp.getAttributeValue(null, name);
+    return value == null ? defaultValue : value;
+  }
+
+  /**
+   * Builds a list of {@link ContentProtection} elements for an {@link AdaptationSet}.
+   * <p>
+   * If child Representation elements contain ContentProtection elements, then it is required that
+   * they all define the same ones. If they do, the ContentProtection elements are bubbled up to the
+   * AdaptationSet. Child Representation elements defining different ContentProtection elements is
+   * considered an error.
+   */
+  protected static final class ContentProtectionsBuilder implements Comparator<ContentProtection> {
+
+    private ArrayList<ContentProtection> adaptationSetProtections;
+    private ArrayList<ContentProtection> representationProtections;
+    private ArrayList<ContentProtection> currentRepresentationProtections;
+
+    private boolean representationProtectionsSet;
+
+    /**
+     * Adds a {@link ContentProtection} found in the AdaptationSet element.
+     *
+     * @param contentProtection The {@link ContentProtection} to add.
+     */
+    public void addAdaptationSetProtection(ContentProtection contentProtection) {
+      if (adaptationSetProtections == null) {
+        adaptationSetProtections = new ArrayList<ContentProtection>();
+      }
+      maybeAddContentProtection(adaptationSetProtections, contentProtection);
+    }
+
+    /**
+     * Adds a {@link ContentProtection} found in a child Representation element.
+     *
+     * @param contentProtection The {@link ContentProtection} to add.
+     */
+    public void addRepresentationProtection(ContentProtection contentProtection) {
+      if (currentRepresentationProtections == null) {
+        currentRepresentationProtections = new ArrayList<ContentProtection>();
+      }
+      maybeAddContentProtection(currentRepresentationProtections, contentProtection);
+    }
+
+    /**
+     * Should be invoked after processing each child Representation element, in order to apply
+     * consistency checks.
+     */
+    public void endRepresentation() {
+      if (!representationProtectionsSet) {
+        if (currentRepresentationProtections != null) {
+          Collections.sort(currentRepresentationProtections, this);
+        }
+        representationProtections = currentRepresentationProtections;
+        representationProtectionsSet = true;
+      } else {
+        // Assert that each Representation element defines the same ContentProtection elements.
+        if (currentRepresentationProtections == null) {
+          Assertions.checkState(representationProtections == null);
+        } else {
+          Collections.sort(currentRepresentationProtections, this);
+          Assertions.checkState(currentRepresentationProtections.equals(representationProtections));
+        }
+      }
+      currentRepresentationProtections = null;
+    }
+
+    /**
+     * Returns the final list of consistent {@link ContentProtection} elements.
+     */
+    public ArrayList<ContentProtection> build() {
+      if (adaptationSetProtections == null) {
+        return representationProtections;
+      } else if (representationProtections == null) {
+        return adaptationSetProtections;
+      } else {
+        // Bubble up ContentProtection elements found in the child Representation elements.
+        for (int i = 0; i < representationProtections.size(); i++) {
+          maybeAddContentProtection(adaptationSetProtections, representationProtections.get(i));
+        }
+        return adaptationSetProtections;
+      }
+    }
+
+    /**
+     * Checks a ContentProtection for consistency with the given list, adding it if necessary.
+     * <ul>
+     * <li>If the new ContentProtection matches another in the list, it's consistent and is not
+     *     added to the list.
+     * <li>If the new ContentProtection has the same schemeUriId as another ContentProtection in the
+     *     list, but its other attributes do not match, then it's inconsistent and an
+     *     {@link IllegalStateException} is thrown.
+     * <li>Else the new ContentProtection has a unique schemeUriId, it's consistent and is added.
+     * </ul>
+     *
+     * @param contentProtections The list of ContentProtection elements currently known.
+     * @param contentProtection The ContentProtection to add.
+     */
+    private void maybeAddContentProtection(List<ContentProtection> contentProtections,
+        ContentProtection contentProtection) {
+      if (!contentProtections.contains(contentProtection)) {
+        for (int i = 0; i < contentProtections.size(); i++) {
+          // If contains returned false (no complete match), but find a matching schemeUriId, then
+          // the MPD contains inconsistent ContentProtection data.
+          Assertions.checkState(
+              !contentProtections.get(i).schemeUriId.equals(contentProtection.schemeUriId));
+        }
+        contentProtections.add(contentProtection);
+      }
+    }
+
+    // Comparator implementation.
+
+    @Override
+    public int compare(ContentProtection first, ContentProtection second) {
+      return first.schemeUriId.compareTo(second.schemeUriId);
+    }
+
+  }
+
+}

+ 76 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java

@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.dash.mpd;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Encapsulates media content components over a contiguous period of time.
+ */
+public class Period {
+
+  /**
+   * The period identifier, if one exists.
+   */
+  public final String id;
+
+  /**
+   * The start time of the period in milliseconds.
+   */
+  public final long startMs;
+
+  /**
+   * The duration of the period in milliseconds, or -1 if the duration is unknown.
+   */
+  public final long durationMs;
+
+  /**
+   * The adaptation sets belonging to the period.
+   */
+  public final List<AdaptationSet> adaptationSets;
+
+  /**
+   * @param id The period identifier. May be null.
+   * @param start The start time of the period in milliseconds.
+   * @param duration The duration of the period in milliseconds, or -1 if the duration is unknown.
+   * @param adaptationSets The adaptation sets belonging to the period.
+   */
+  public Period(String id, long start, long duration, List<AdaptationSet> adaptationSets) {
+    this.id = id;
+    this.startMs = start;
+    this.durationMs = duration;
+    this.adaptationSets = Collections.unmodifiableList(adaptationSets);
+  }
+
+  /**
+   * Returns the index of the first adaptation set of a given type, or -1 if no adaptation set of
+   * the specified type exists.
+   *
+   * @param type An adaptation set type.
+   * @return The index of the first adaptation set of the specified type, or -1.
+   */
+  public int getAdaptationSetIndex(int type) {
+    int adaptationCount = adaptationSets.size();
+    for (int i = 0; i < adaptationCount; i++) {
+      if (adaptationSets.get(i).type == type) {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+}

+ 127 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/mpd/RangedUri.java

@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.dash.mpd;
+
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.Util;
+
+import android.net.Uri;
+
+/**
+ * Defines a range of data located at a {@link Uri}.
+ */
+public final class RangedUri {
+
+  /**
+   * The (zero based) index of the first byte of the range.
+   */
+  public final long start;
+
+  /**
+   * The length of the range, or -1 to indicate that the range is unbounded.
+   */
+  public final long length;
+
+  // The {@link Uri} is stored internally in two parts, {@link #baseUri} and {@link uriString}.
+  // This helps optimize memory usage in the same way that DASH manifests allow many URLs to be
+  // expressed concisely in the form of a single BaseURL and many relative paths. Note that this
+  // optimization relies on the same {@code Uri} being passed as the {@link #baseUri} to many
+  // instances of this class.
+  private final Uri baseUri;
+  private final String stringUri;
+
+  private int hashCode;
+
+  /**
+   * Constructs an ranged uri.
+   * <p>
+   * See {@link Util#getMergedUri(Uri, String)} for a description of how {@code baseUri} and
+   * {@code stringUri} are merged.
+   *
+   * @param baseUri A uri that can form the base of the uri defined by the instance.
+   * @param stringUri A relative or absolute uri in string form.
+   * @param start The (zero based) index of the first byte of the range.
+   * @param length The length of the range, or -1 to indicate that the range is unbounded.
+   */
+  public RangedUri(Uri baseUri, String stringUri, long start, long length) {
+    Assertions.checkArgument(baseUri != null || stringUri != null);
+    this.baseUri = baseUri;
+    this.stringUri = stringUri;
+    this.start = start;
+    this.length = length;
+  }
+
+  /**
+   * Returns the {@link Uri} represented by the instance.
+   *
+   * @return The {@link Uri} represented by the instance.
+   */
+  public Uri getUri() {
+    return Util.getMergedUri(baseUri, stringUri);
+  }
+
+  /**
+   * Attempts to merge this {@link RangedUri} with another.
+   * <p>
+   * A merge is successful if both instances define the same {@link Uri}, and if one starte the
+   * byte after the other ends, forming a contiguous region with no overlap.
+   * <p>
+   * If {@code other} is null then the merge is considered unsuccessful, and null is returned.
+   *
+   * @param other The {@link RangedUri} to merge.
+   * @return The merged {@link RangedUri} if the merge was successful. Null otherwise.
+   */
+  public RangedUri attemptMerge(RangedUri other) {
+    if (other == null || !getUri().equals(other.getUri())) {
+      return null;
+    } else if (length != -1 && start + length == other.start) {
+      return new RangedUri(baseUri, stringUri, start,
+          other.length == -1 ? -1 : length + other.length);
+    } else if (other.length != -1 && other.start + other.length == start) {
+      return new RangedUri(baseUri, stringUri, other.start,
+          length == -1 ? -1 : other.length + length);
+    } else {
+      return null;
+    }
+  }
+
+  @Override
+  public int hashCode() {
+    if (hashCode == 0) {
+      int result = 17;
+      result = 31 * result + (int) start;
+      result = 31 * result + (int) length;
+      result = 31 * result + getUri().hashCode();
+      hashCode = result;
+    }
+    return hashCode;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (obj == null || getClass() != obj.getClass()) {
+      return false;
+    }
+    RangedUri other = (RangedUri) obj;
+    return this.start == other.start
+        && this.length == other.length
+        && getUri().equals(other.getUri());
+  }
+
+}

+ 293 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/mpd/Representation.java

@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.dash.mpd;
+
+import com.google.android.exoplayer.chunk.Format;
+import com.google.android.exoplayer.dash.DashSegmentIndex;
+import com.google.android.exoplayer.dash.DashSingleSegmentIndex;
+import com.google.android.exoplayer.dash.mpd.SegmentBase.MultiSegmentBase;
+import com.google.android.exoplayer.dash.mpd.SegmentBase.SingleSegmentBase;
+
+import android.net.Uri;
+
+/**
+ * A DASH representation.
+ */
+public abstract class Representation {
+
+  /**
+   * Identifies the piece of content to which this {@link Representation} belongs.
+   * <p>
+   * For example, all {@link Representation}s belonging to a video should have the same
+   * {@link #contentId}, which should uniquely identify that video.
+   */
+  public final String contentId;
+
+  /**
+   * Identifies the revision of the content.
+   * <p>
+   * If the media for a given ({@link #contentId} can change over time without a change to the
+   * {@link #format}'s {@link Format#id} (e.g. as a result of re-encoding the media with an
+   * updated encoder), then this identifier must uniquely identify the revision of the media. The
+   * timestamp at which the media was encoded is often a suitable.
+   */
+  public final long revisionId;
+
+  /**
+   * The format of the representation.
+   */
+  public final Format format;
+
+  /**
+   * The start time of the enclosing period in milliseconds since the epoch.
+   */
+  public final long periodStartMs;
+
+  /**
+   * The duration of the enclosing period in milliseconds.
+   */
+  public final long periodDurationMs;
+
+  /**
+   * The offset of the presentation timestamps in the media stream relative to media time.
+   */
+  public final long presentationTimeOffsetUs;
+
+  private final RangedUri initializationUri;
+
+  /**
+   * Constructs a new instance.
+   *
+   * @param periodStartMs The start time of the enclosing period in milliseconds.
+   * @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
+   *     duration is unknown.
+   * @param contentId Identifies the piece of content to which this representation belongs.
+   * @param revisionId Identifies the revision of the content.
+   * @param format The format of the representation.
+   * @param segmentBase A segment base element for the representation.
+   * @return The constructed instance.
+   */
+  public static Representation newInstance(long periodStartMs, long periodDurationMs,
+      String contentId, long revisionId, Format format, SegmentBase segmentBase) {
+    if (segmentBase instanceof SingleSegmentBase) {
+      return new SingleSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId,
+          format, (SingleSegmentBase) segmentBase, -1);
+    } else if (segmentBase instanceof MultiSegmentBase) {
+      return new MultiSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId,
+          format, (MultiSegmentBase) segmentBase);
+    } else {
+      throw new IllegalArgumentException("segmentBase must be of type SingleSegmentBase or "
+          + "MultiSegmentBase");
+    }
+  }
+
+  private Representation(long periodStartMs, long periodDurationMs, String contentId,
+      long revisionId, Format format, SegmentBase segmentBase) {
+    this.periodStartMs = periodStartMs;
+    this.periodDurationMs = periodDurationMs;
+    this.contentId = contentId;
+    this.revisionId = revisionId;
+    this.format = format;
+    initializationUri = segmentBase.getInitialization(this);
+    presentationTimeOffsetUs = segmentBase.getPresentationTimeOffsetUs();
+  }
+
+  /**
+   * Gets a {@link RangedUri} defining the location of the representation's initialization data.
+   * May be null if no initialization data exists.
+   *
+   * @return A {@link RangedUri} defining the location of the initialization data, or null.
+   */
+  public RangedUri getInitializationUri() {
+    return initializationUri;
+  }
+
+  /**
+   * Gets a {@link RangedUri} defining the location of the representation's segment index. Null if
+   * the representation provides an index directly.
+   *
+   * @return The location of the segment index, or null.
+   */
+  public abstract RangedUri getIndexUri();
+
+  /**
+   * Gets a segment index, if the representation is able to provide one directly. Null if the
+   * segment index is defined externally.
+   *
+   * @return The segment index, or null.
+   */
+  public abstract DashSegmentIndex getIndex();
+
+  /**
+   * Generates a cache key for the {@link Representation}, in the format
+   * {@code contentId + "." + format.id + "." + revisionId}.
+   *
+   * @return A cache key.
+   */
+  public String getCacheKey() {
+    return contentId + "." + format.id + "." + revisionId;
+  }
+
+  /**
+   * A DASH representation consisting of a single segment.
+   */
+  public static class SingleSegmentRepresentation extends Representation {
+
+    /**
+     * The {@link Uri} of the single segment.
+     */
+    public final Uri uri;
+
+    /**
+     * The content length, or -1 if unknown.
+     */
+    public final long contentLength;
+
+    private final RangedUri indexUri;
+    private final DashSingleSegmentIndex segmentIndex;
+
+    /**
+     * @param periodStartMs The start time of the enclosing period in milliseconds.
+     * @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
+     *     duration is unknown.
+     * @param contentId Identifies the piece of content to which this representation belongs.
+     * @param revisionId Identifies the revision of the content.
+     * @param format The format of the representation.
+     * @param uri The uri of the media.
+     * @param initializationStart The offset of the first byte of initialization data.
+     * @param initializationEnd The offset of the last byte of initialization data.
+     * @param indexStart The offset of the first byte of index data.
+     * @param indexEnd The offset of the last byte of index data.
+     * @param contentLength The content length, or -1 if unknown.
+     */
+    public static SingleSegmentRepresentation newInstance(long periodStartMs, long periodDurationMs,
+        String contentId, long revisionId, Format format, Uri uri, long initializationStart,
+        long initializationEnd, long indexStart, long indexEnd, long contentLength) {
+      RangedUri rangedUri = new RangedUri(uri, null, initializationStart,
+          initializationEnd - initializationStart + 1);
+      SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0, uri, indexStart,
+          indexEnd - indexStart + 1);
+      return new SingleSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId,
+          format, segmentBase, contentLength);
+    }
+
+    /**
+     * @param periodStartMs The start time of the enclosing period in milliseconds.
+     * @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
+     *     duration is unknown.
+     * @param contentId Identifies the piece of content to which this representation belongs.
+     * @param revisionId Identifies the revision of the content.
+     * @param format The format of the representation.
+     * @param segmentBase The segment base underlying the representation.
+     * @param contentLength The content length, or -1 if unknown.
+     */
+    public SingleSegmentRepresentation(long periodStartMs, long periodDurationMs, String contentId,
+        long revisionId, Format format, SingleSegmentBase segmentBase, long contentLength) {
+      super(periodStartMs, periodDurationMs, contentId, revisionId, format, segmentBase);
+      this.uri = segmentBase.uri;
+      this.indexUri = segmentBase.getIndex();
+      this.contentLength = contentLength;
+      // If we have an index uri then the index is defined externally, and we shouldn't return one
+      // directly. If we don't, then we can't do better than an index defining a single segment.
+      segmentIndex = indexUri != null ? null : new DashSingleSegmentIndex(periodStartMs * 1000,
+          periodDurationMs * 1000, new RangedUri(uri, null, 0, -1));
+    }
+
+    @Override
+    public RangedUri getIndexUri() {
+      return indexUri;
+    }
+
+    @Override
+    public DashSegmentIndex getIndex() {
+      return segmentIndex;
+    }
+
+  }
+
+  /**
+   * A DASH representation consisting of multiple segments.
+   */
+  public static class MultiSegmentRepresentation extends Representation
+      implements DashSegmentIndex {
+
+    private final MultiSegmentBase segmentBase;
+
+    /**
+     * @param periodStartMs The start time of the enclosing period in milliseconds.
+     * @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
+     *     duration is unknown.
+     * @param contentId Identifies the piece of content to which this representation belongs.
+     * @param revisionId Identifies the revision of the content.
+     * @param format The format of the representation.
+     * @param segmentBase The segment base underlying the representation.
+     */
+    public MultiSegmentRepresentation(long periodStartMs, long periodDurationMs, String contentId,
+        long revisionId, Format format, MultiSegmentBase segmentBase) {
+      super(periodStartMs, periodDurationMs, contentId, revisionId, format, segmentBase);
+      this.segmentBase = segmentBase;
+    }
+
+    @Override
+    public RangedUri getIndexUri() {
+      return null;
+    }
+
+    @Override
+    public DashSegmentIndex getIndex() {
+      return this;
+    }
+
+    // DashSegmentIndex implementation.
+
+    @Override
+    public RangedUri getSegmentUrl(int segmentIndex) {
+      return segmentBase.getSegmentUrl(this, segmentIndex);
+    }
+
+    @Override
+    public int getSegmentNum(long timeUs) {
+      return segmentBase.getSegmentNum(timeUs);
+    }
+
+    @Override
+    public long getTimeUs(int segmentIndex) {
+      return segmentBase.getSegmentTimeUs(segmentIndex);
+    }
+
+    @Override
+    public long getDurationUs(int segmentIndex) {
+      return segmentBase.getSegmentDurationUs(segmentIndex);
+    }
+
+    @Override
+    public int getFirstSegmentNum() {
+      return segmentBase.getFirstSegmentNum();
+    }
+
+    @Override
+    public int getLastSegmentNum() {
+      return segmentBase.getLastSegmentNum();
+    }
+
+    @Override
+    public boolean isExplicit() {
+      return segmentBase.isExplicit();
+    }
+
+  }
+
+}

+ 384 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java

@@ -0,0 +1,384 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.dash.mpd;
+
+import com.google.android.exoplayer.C;
+import com.google.android.exoplayer.dash.DashSegmentIndex;
+import com.google.android.exoplayer.util.Util;
+
+import android.net.Uri;
+
+import java.util.List;
+
+/**
+ * An approximate representation of a SegmentBase manifest element.
+ */
+public abstract class SegmentBase {
+
+  /* package */ final RangedUri initialization;
+  /* package */ final long timescale;
+  /* package */ final long presentationTimeOffset;
+
+  /**
+   * @param initialization A {@link RangedUri} corresponding to initialization data, if such data
+   *     exists.
+   * @param timescale The timescale in units per second.
+   * @param presentationTimeOffset The presentation time offset. The value in seconds is the
+   *     division of this value and {@code timescale}.
+   */
+  public SegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset) {
+    this.initialization = initialization;
+    this.timescale = timescale;
+    this.presentationTimeOffset = presentationTimeOffset;
+  }
+
+  /**
+   * Gets the {@link RangedUri} defining the location of initialization data for a given
+   * representation. May be null if no initialization data exists.
+   *
+   * @param representation The {@link Representation} for which initialization data is required.
+   * @return A {@link RangedUri} defining the location of the initialization data, or null.
+   */
+  public RangedUri getInitialization(Representation representation) {
+    return initialization;
+  }
+
+  /**
+   * Gets the presentation time offset, in microseconds.
+   *
+   * @return The presentation time offset, in microseconds.
+   */
+  public long getPresentationTimeOffsetUs() {
+    return Util.scaleLargeTimestamp(presentationTimeOffset, C.MICROS_PER_SECOND, timescale);
+  }
+
+  /**
+   * A {@link SegmentBase} that defines a single segment.
+   */
+  public static class SingleSegmentBase extends SegmentBase {
+
+    /**
+     * The uri of the segment.
+     */
+    public final Uri uri;
+
+    /* package */ final long indexStart;
+    /* package */ final long indexLength;
+
+    /**
+     * @param initialization A {@link RangedUri} corresponding to initialization data, if such data
+     *     exists.
+     * @param timescale The timescale in units per second.
+     * @param presentationTimeOffset The presentation time offset. The value in seconds is the
+     *     division of this value and {@code timescale}.
+     * @param uri The uri of the segment.
+     * @param indexStart The byte offset of the index data in the segment.
+     * @param indexLength The length of the index data in bytes.
+     */
+    public SingleSegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset,
+        Uri uri, long indexStart, long indexLength) {
+      super(initialization, timescale, presentationTimeOffset);
+      this.uri = uri;
+      this.indexStart = indexStart;
+      this.indexLength = indexLength;
+    }
+
+    /**
+     * @param uri The uri of the segment.
+     */
+    public SingleSegmentBase(Uri uri) {
+      this(null, 1, 0, uri, 0, -1);
+    }
+
+    public RangedUri getIndex() {
+      return indexLength <= 0 ? null : new RangedUri(uri, null, indexStart, indexLength);
+    }
+
+  }
+
+  /**
+   * A {@link SegmentBase} that consists of multiple segments.
+   */
+  public abstract static class MultiSegmentBase extends SegmentBase {
+
+    /* package */ final long periodDurationMs;
+    /* package */ final int startNumber;
+    /* package */ final long duration;
+    /* package */ final List<SegmentTimelineElement> segmentTimeline;
+
+    /**
+     * @param initialization A {@link RangedUri} corresponding to initialization data, if such data
+     *     exists.
+     * @param timescale The timescale in units per second.
+     * @param presentationTimeOffset The presentation time offset. The value in seconds is the
+     *     division of this value and {@code timescale}.
+     * @param periodDurationMs The duration of the enclosing period in milliseconds.
+     * @param startNumber The sequence number of the first segment.
+     * @param duration The duration of each segment in the case of fixed duration segments. The
+     *     value in seconds is the division of this value and {@code timescale}. If
+     *     {@code segmentTimeline} is non-null then this parameter is ignored.
+     * @param segmentTimeline A segment timeline corresponding to the segments. If null, then
+     *     segments are assumed to be of fixed duration as specified by the {@code duration}
+     *     parameter.
+     */
+    public MultiSegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset,
+        long periodDurationMs, int startNumber, long duration,
+        List<SegmentTimelineElement> segmentTimeline) {
+      super(initialization, timescale, presentationTimeOffset);
+      this.periodDurationMs = periodDurationMs;
+      this.startNumber = startNumber;
+      this.duration = duration;
+      this.segmentTimeline = segmentTimeline;
+    }
+
+    /**
+     * @see DashSegmentIndex#getSegmentNum(long)
+     */
+    public int getSegmentNum(long timeUs) {
+      int lowIndex = getFirstSegmentNum();
+      int highIndex = getLastSegmentNum();
+      if (segmentTimeline == null) {
+        // All segments are of equal duration (with the possible exception of the last one).
+        long durationUs = (duration * C.MICROS_PER_SECOND) / timescale;
+        int segmentNum = startNumber + (int) (timeUs / durationUs);
+        // Ensure we stay within bounds.
+        return segmentNum < lowIndex ? lowIndex
+            : highIndex != DashSegmentIndex.INDEX_UNBOUNDED && segmentNum > highIndex ? highIndex
+            : segmentNum;
+      } else {
+        // The high index cannot be unbounded. Identify the segment using binary search.
+        while (lowIndex <= highIndex) {
+          int midIndex = (lowIndex + highIndex) / 2;
+          long midTimeUs = getSegmentTimeUs(midIndex);
+          if (midTimeUs < timeUs) {
+            lowIndex = midIndex + 1;
+          } else if (midTimeUs > timeUs) {
+            highIndex = midIndex - 1;
+          } else {
+            return midIndex;
+          }
+        }
+        return lowIndex - 1;
+      }
+    }
+
+    /**
+     * @see DashSegmentIndex#getDurationUs(int)
+     */
+    public final long getSegmentDurationUs(int sequenceNumber) {
+      if (segmentTimeline != null) {
+        long duration = segmentTimeline.get(sequenceNumber - startNumber).duration;
+        return (duration * C.MICROS_PER_SECOND) / timescale;
+      } else {
+        return sequenceNumber == getLastSegmentNum()
+            ? ((periodDurationMs * 1000) - getSegmentTimeUs(sequenceNumber))
+            : ((duration * C.MICROS_PER_SECOND) / timescale);
+      }
+    }
+
+    /**
+     * @see DashSegmentIndex#getTimeUs(int)
+     */
+    public final long getSegmentTimeUs(int sequenceNumber) {
+      long unscaledSegmentTime;
+      if (segmentTimeline != null) {
+        unscaledSegmentTime = segmentTimeline.get(sequenceNumber - startNumber).startTime
+            - presentationTimeOffset;
+      } else {
+        unscaledSegmentTime = (sequenceNumber - startNumber) * duration;
+      }
+      return Util.scaleLargeTimestamp(unscaledSegmentTime, C.MICROS_PER_SECOND, timescale);
+    }
+
+    /**
+     * Returns a {@link RangedUri} defining the location of a segment for the given index in the
+     * given representation.
+     *
+     * @see DashSegmentIndex#getSegmentUrl(int)
+     */
+    public abstract RangedUri getSegmentUrl(Representation representation, int index);
+
+    /**
+     * @see DashSegmentIndex#getFirstSegmentNum()
+     */
+    public int getFirstSegmentNum() {
+      return startNumber;
+    }
+
+    /**
+     * @see DashSegmentIndex#getLastSegmentNum()
+     */
+    public abstract int getLastSegmentNum();
+
+    /**
+     * @see DashSegmentIndex#isExplicit()
+     */
+    public boolean isExplicit() {
+      return segmentTimeline != null;
+    }
+
+  }
+
+  /**
+   * A {@link MultiSegmentBase} that uses a SegmentList to define its segments.
+   */
+  public static class SegmentList extends MultiSegmentBase {
+
+    /* package */ final List<RangedUri> mediaSegments;
+
+    /**
+     * @param initialization A {@link RangedUri} corresponding to initialization data, if such data
+     *     exists.
+     * @param timescale The timescale in units per second.
+     * @param presentationTimeOffset The presentation time offset. The value in seconds is the
+     *     division of this value and {@code timescale}.
+     * @param periodDurationMs The duration of the enclosing period in milliseconds.
+     * @param startNumber The sequence number of the first segment.
+     * @param duration The duration of each segment in the case of fixed duration segments. The
+     *     value in seconds is the division of this value and {@code timescale}. If
+     *     {@code segmentTimeline} is non-null then this parameter is ignored.
+     * @param segmentTimeline A segment timeline corresponding to the segments. If null, then
+     *     segments are assumed to be of fixed duration as specified by the {@code duration}
+     *     parameter.
+     * @param mediaSegments A list of {@link RangedUri}s indicating the locations of the segments.
+     */
+    public SegmentList(RangedUri initialization, long timescale, long presentationTimeOffset,
+        long periodDurationMs, int startNumber, long duration,
+        List<SegmentTimelineElement> segmentTimeline, List<RangedUri> mediaSegments) {
+      super(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber,
+          duration, segmentTimeline);
+      this.mediaSegments = mediaSegments;
+    }
+
+    @Override
+    public RangedUri getSegmentUrl(Representation representation, int sequenceNumber) {
+      return mediaSegments.get(sequenceNumber - startNumber);
+    }
+
+    @Override
+    public int getLastSegmentNum() {
+      return startNumber + mediaSegments.size() - 1;
+    }
+
+    @Override
+    public boolean isExplicit() {
+      return true;
+    }
+
+  }
+
+  /**
+   * A {@link MultiSegmentBase} that uses a SegmentTemplate to define its segments.
+   */
+  public static class SegmentTemplate extends MultiSegmentBase {
+
+    /* package */ final UrlTemplate initializationTemplate;
+    /* package */ final UrlTemplate mediaTemplate;
+
+    private final Uri baseUrl;
+
+    /**
+     * @param initialization A {@link RangedUri} corresponding to initialization data, if such data
+     *     exists. The value of this parameter is ignored if {@code initializationTemplate} is
+     *     non-null.
+     * @param timescale The timescale in units per second.
+     * @param presentationTimeOffset The presentation time offset. The value in seconds is the
+     *     division of this value and {@code timescale}.
+     * @param periodDurationMs The duration of the enclosing period in milliseconds.
+     * @param startNumber The sequence number of the first segment.
+     * @param duration The duration of each segment in the case of fixed duration segments. The
+     *     value in seconds is the division of this value and {@code timescale}. If
+     *     {@code segmentTimeline} is non-null then this parameter is ignored.
+     * @param segmentTimeline A segment timeline corresponding to the segments. If null, then
+     *     segments are assumed to be of fixed duration as specified by the {@code duration}
+     *     parameter.
+     * @param initializationTemplate A template defining the location of initialization data, if
+     *     such data exists. If non-null then the {@code initialization} parameter is ignored. If
+     *     null then {@code initialization} will be used.
+     * @param mediaTemplate A template defining the location of each media segment.
+     * @param baseUrl A url to use as the base for relative urls generated by the templates.
+     */
+    public SegmentTemplate(RangedUri initialization, long timescale, long presentationTimeOffset,
+        long periodDurationMs, int startNumber, long duration,
+        List<SegmentTimelineElement> segmentTimeline, UrlTemplate initializationTemplate,
+        UrlTemplate mediaTemplate, Uri baseUrl) {
+      super(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber,
+          duration, segmentTimeline);
+      this.initializationTemplate = initializationTemplate;
+      this.mediaTemplate = mediaTemplate;
+      this.baseUrl = baseUrl;
+    }
+
+    @Override
+    public RangedUri getInitialization(Representation representation) {
+      if (initializationTemplate != null) {
+        String urlString = initializationTemplate.buildUri(representation.format.id, 0,
+            representation.format.bitrate, 0);
+        return new RangedUri(baseUrl, urlString, 0, -1);
+      } else {
+        return super.getInitialization(representation);
+      }
+    }
+
+    @Override
+    public RangedUri getSegmentUrl(Representation representation, int sequenceNumber) {
+      long time = 0;
+      if (segmentTimeline != null) {
+        time = segmentTimeline.get(sequenceNumber - startNumber).startTime;
+      } else {
+        time = (sequenceNumber - startNumber) * duration;
+      }
+      String uriString = mediaTemplate.buildUri(representation.format.id, sequenceNumber,
+          representation.format.bitrate, time);
+      return new RangedUri(baseUrl, uriString, 0, -1);
+    }
+
+    @Override
+    public int getLastSegmentNum() {
+      if (segmentTimeline != null) {
+        return segmentTimeline.size() + startNumber - 1;
+      } else if (periodDurationMs == -1) {
+        return DashSegmentIndex.INDEX_UNBOUNDED;
+      } else {
+        long durationMs = (duration * 1000) / timescale;
+        return startNumber + (int) ((periodDurationMs + durationMs - 1) / durationMs) - 1;
+      }
+    }
+
+  }
+
+  /**
+   * Represents a timeline segment from the MPD's SegmentTimeline list.
+   */
+  public static class SegmentTimelineElement {
+
+    /* package */ long startTime;
+    /* package */ long duration;
+
+    /**
+     * @param startTime The start time of the element. The value in seconds is the division of this
+     *     value and the {@code timescale} of the enclosing element.
+     * @param duration The duration of the element. The value in seconds is the division of this
+     *     value and the {@code timescale} of the enclosing element.
+     */
+    public SegmentTimelineElement(long startTime, long duration) {
+      this.startTime = startTime;
+      this.duration = duration;
+    }
+
+  }
+
+}

+ 164 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/mpd/UrlTemplate.java

@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.dash.mpd;
+
+/**
+ * A template from which URLs can be built.
+ * <p>
+ * URLs are built according to the substitution rules defined in ISO/IEC 23009-1:2014 5.3.9.4.4.
+ */
+public final class UrlTemplate {
+
+  private static final String REPRESENTATION = "RepresentationID";
+  private static final String NUMBER = "Number";
+  private static final String BANDWIDTH = "Bandwidth";
+  private static final String TIME = "Time";
+  private static final String ESCAPED_DOLLAR = "$$";
+  private static final String DEFAULT_FORMAT_TAG = "%01d";
+
+  private static final int REPRESENTATION_ID = 1;
+  private static final int NUMBER_ID = 2;
+  private static final int BANDWIDTH_ID = 3;
+  private static final int TIME_ID = 4;
+
+  private final String[] urlPieces;
+  private final int[] identifiers;
+  private final String[] identifierFormatTags;
+  private final int identifierCount;
+
+  /**
+   * Compile an instance from the provided template string.
+   *
+   * @param template The template.
+   * @return The compiled instance.
+   * @throws IllegalArgumentException If the template string is malformed.
+   */
+  public static UrlTemplate compile(String template) {
+    // These arrays are sizes assuming each of the four possible identifiers will be present at
+    // most once in the template, which seems like a reasonable assumption.
+    String[] urlPieces = new String[5];
+    int[] identifiers = new int[4];
+    String[] identifierFormatTags = new String[4];
+    int identifierCount = parseTemplate(template, urlPieces, identifiers, identifierFormatTags);
+    return new UrlTemplate(urlPieces, identifiers, identifierFormatTags, identifierCount);
+  }
+
+  /**
+   * Internal constructor. Use {@link #compile(String)} to build instances of this class.
+   */
+  private UrlTemplate(String[] urlPieces, int[] identifiers, String[] identifierFormatTags,
+      int identifierCount) {
+    this.urlPieces = urlPieces;
+    this.identifiers = identifiers;
+    this.identifierFormatTags = identifierFormatTags;
+    this.identifierCount = identifierCount;
+  }
+
+  /**
+   * Constructs a Uri from the template, substituting in the provided arguments.
+   * <p>
+   * Arguments whose corresponding identifiers are not present in the template will be ignored.
+   *
+   * @param representationId The representation identifier.
+   * @param segmentNumber The segment number.
+   * @param bandwidth The bandwidth.
+   * @param time The time as specified by the segment timeline.
+   * @return The built Uri.
+   */
+  public String buildUri(String representationId, int segmentNumber, int bandwidth, long time) {
+    StringBuilder builder = new StringBuilder();
+    for (int i = 0; i < identifierCount; i++) {
+      builder.append(urlPieces[i]);
+      if (identifiers[i] == REPRESENTATION_ID) {
+        builder.append(representationId);
+      } else if (identifiers[i] == NUMBER_ID) {
+        builder.append(String.format(identifierFormatTags[i], segmentNumber));
+      } else if (identifiers[i] == BANDWIDTH_ID) {
+        builder.append(String.format(identifierFormatTags[i], bandwidth));
+      } else if (identifiers[i] == TIME_ID) {
+        builder.append(String.format(identifierFormatTags[i], time));
+      }
+    }
+    builder.append(urlPieces[identifierCount]);
+    return builder.toString();
+  }
+
+  /**
+   * Parses {@code template}, placing the decomposed components into the provided arrays.
+   * <p>
+   * If the return value is N, {@code urlPieces} will contain (N+1) strings that must be
+   * interleaved with N arguments in order to construct a url. The N identifiers that correspond to
+   * the required arguments, together with the tags that define their required formatting, are
+   * returned in {@code identifiers} and {@code identifierFormatTags} respectively.
+   *
+   * @param template The template to parse.
+   * @param urlPieces A holder for pieces of url parsed from the template.
+   * @param identifiers A holder for identifiers parsed from the template.
+   * @param identifierFormatTags A holder for format tags corresponding to the parsed identifiers.
+   * @return The number of identifiers in the template url.
+   * @throws IllegalArgumentException If the template string is malformed.
+   */
+  private static int parseTemplate(String template, String[] urlPieces, int[] identifiers,
+      String[] identifierFormatTags) {
+    urlPieces[0] = "";
+    int templateIndex = 0;
+    int identifierCount = 0;
+    while (templateIndex < template.length()) {
+      int dollarIndex = template.indexOf("$", templateIndex);
+      if (dollarIndex == -1) {
+        urlPieces[identifierCount] += template.substring(templateIndex);
+        templateIndex = template.length();
+      } else if (dollarIndex != templateIndex) {
+        urlPieces[identifierCount] += template.substring(templateIndex, dollarIndex);
+        templateIndex = dollarIndex;
+      } else if (template.startsWith(ESCAPED_DOLLAR, templateIndex)) {
+        urlPieces[identifierCount] += "$";
+        templateIndex += 2;
+      } else {
+        int secondIndex = template.indexOf("$", templateIndex + 1);
+        String identifier = template.substring(templateIndex + 1, secondIndex);
+        if (identifier.equals(REPRESENTATION)) {
+          identifiers[identifierCount] = REPRESENTATION_ID;
+        } else {
+          int formatTagIndex = identifier.indexOf("%0");
+          String formatTag = DEFAULT_FORMAT_TAG;
+          if (formatTagIndex != -1) {
+            formatTag = identifier.substring(formatTagIndex);
+            if (!formatTag.endsWith("d")) {
+              formatTag += "d";
+            }
+            identifier = identifier.substring(0, formatTagIndex);
+          }
+          if (identifier.equals(NUMBER)) {
+            identifiers[identifierCount] = NUMBER_ID;
+          } else if (identifier.equals(BANDWIDTH)) {
+            identifiers[identifierCount] = BANDWIDTH_ID;
+          } else if (identifier.equals(TIME)) {
+            identifiers[identifierCount] = TIME_ID;
+          } else {
+            throw new IllegalArgumentException("Invalid template: " + template);
+          }
+          identifierFormatTags[identifierCount] = formatTag;
+        }
+        identifierCount++;
+        urlPieces[identifierCount] = "";
+        templateIndex = secondIndex + 1;
+      }
+    }
+    return identifierCount;
+  }
+
+}

+ 31 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/dash/mpd/UtcTimingElement.java

@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.dash.mpd;
+
+/**
+ * Represents a UTCTiming element.
+ */
+public class UtcTimingElement {
+
+  public final String schemeIdUri;
+  public final String value;
+
+  public UtcTimingElement(String schemeIdUri, String value) {
+    this.schemeIdUri = schemeIdUri;
+    this.value = value;
+  }
+
+}

+ 109 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/drm/DrmSessionManager.java

@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.drm;
+
+import android.annotation.TargetApi;
+import android.media.MediaCrypto;
+
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * Manages a DRM session.
+ */
+@TargetApi(16)
+public interface DrmSessionManager {
+
+  /**
+   * The error state. {@link #getError()} can be used to retrieve the cause.
+   */
+  public static final int STATE_ERROR = 0;
+  /**
+   * The session is closed.
+   */
+  public static final int STATE_CLOSED = 1;
+  /**
+   * The session is being opened (i.e. {@link #open(Map, String)} has been called, but the session
+   * is not yet open).
+   */
+  public static final int STATE_OPENING = 2;
+  /**
+   * The session is open, but does not yet have the keys required for decryption.
+   */
+  public static final int STATE_OPENED = 3;
+  /**
+   * The session is open and has the keys required for decryption.
+   */
+  public static final int STATE_OPENED_WITH_KEYS = 4;
+
+  /**
+   * Opens the session, possibly asynchronously.
+   *
+   * @param drmInitData Initialization data for the drm schemes supported by the media, keyed by
+   *     scheme UUID.
+   * @param mimeType The mimeType of the media.
+   */
+  void open(Map<UUID, byte[]> drmInitData, String mimeType);
+
+  /**
+   * Closes the session.
+   */
+  void close();
+
+  /**
+   * Gets the current state of the session.
+   *
+   * @return One of {@link #STATE_ERROR}, {@link #STATE_CLOSED}, {@link #STATE_OPENING},
+   *     {@link #STATE_OPENED} and {@link #STATE_OPENED_WITH_KEYS}.
+   */
+  int getState();
+
+  /**
+   * Gets a {@link MediaCrypto} for the open session.
+   * <p>
+   * This method may be called when the manager is in the following states:
+   * {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS}
+   *
+   * @return A {@link MediaCrypto} for the open session.
+   * @throws IllegalStateException If called when a session isn't opened.
+   */
+  MediaCrypto getMediaCrypto();
+
+  /**
+   * Whether the session requires a secure decoder for the specified mime type.
+   * <p>
+   * Normally this method should return {@link MediaCrypto#requiresSecureDecoderComponent(String)},
+   * however in some cases implementations  may wish to modify the return value (i.e. to force a
+   * secure decoder even when one is not required).
+   * <p>
+   * This method may be called when the manager is in the following states:
+   * {@link #STATE_OPENED}, {@link #STATE_OPENED_WITH_KEYS}
+   *
+   * @return Whether the open session requires a secure decoder for the specified mime type.
+   * @throws IllegalStateException If called when a session isn't opened.
+   */
+  boolean requiresSecureDecoderComponent(String mimeType);
+
+  /**
+   * Gets the cause of the error state.
+   * <p>
+   * This method may be called when the manager is in any state.
+   *
+   * @return An exception if the state is {@link #STATE_ERROR}. Null otherwise.
+   */
+  Exception getError();
+
+}

+ 49 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/drm/MediaDrmCallback.java

@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.drm;
+
+import android.annotation.TargetApi;
+import android.media.MediaDrm;
+
+import java.util.UUID;
+
+/**
+ * Performs {@link MediaDrm} key and provisioning requests.
+ */
+@TargetApi(18)
+public interface MediaDrmCallback {
+
+  /**
+   * Executes a provisioning request.
+   *
+   * @param uuid The UUID of the content protection scheme.
+   * @param request The request.
+   * @return The response data.
+   * @throws Exception If an error occurred executing the request.
+   */
+  byte[] executeProvisionRequest(UUID uuid, MediaDrm.ProvisionRequest request) throws Exception;
+
+  /**
+   * Executes a key request.
+   *
+   * @param uuid The UUID of the content protection scheme.
+   * @param request The request.
+   * @return The response data.
+   * @throws Exception If an error occurred executing the request.
+   */
+  byte[] executeKeyRequest(UUID uuid, MediaDrm.KeyRequest request) throws Exception;
+
+}

+ 405 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/drm/StreamingDrmSessionManager.java

@@ -0,0 +1,405 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.drm;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.media.DeniedByServerException;
+import android.media.MediaCrypto;
+import android.media.MediaDrm;
+import android.media.MediaDrm.KeyRequest;
+import android.media.MediaDrm.OnEventListener;
+import android.media.MediaDrm.ProvisionRequest;
+import android.media.NotProvisionedException;
+import android.media.UnsupportedSchemeException;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * A base class for {@link DrmSessionManager} implementations that support streaming playbacks
+ * using {@link MediaDrm}.
+ */
+@TargetApi(18)
+public class StreamingDrmSessionManager implements DrmSessionManager {
+
+  /**
+   * Interface definition for a callback to be notified of {@link StreamingDrmSessionManager}
+   * events.
+   */
+  public interface EventListener {
+
+    /**
+     * Invoked when a drm error occurs.
+     *
+     * @param e The corresponding exception.
+     */
+    void onDrmSessionManagerError(Exception e);
+
+  }
+
+  private static final int MSG_PROVISION = 0;
+  private static final int MSG_KEYS = 1;
+
+  private final Handler eventHandler;
+  private final EventListener eventListener;
+  private final MediaDrm mediaDrm;
+  private final HashMap<String, String> optionalKeyRequestParameters;
+
+  /* package */ final MediaDrmHandler mediaDrmHandler;
+  /* package */ final MediaDrmCallback callback;
+  /* package */ final PostResponseHandler postResponseHandler;
+  /* package */ final UUID uuid;
+
+  private HandlerThread requestHandlerThread;
+  private Handler postRequestHandler;
+
+  private int openCount;
+  private boolean provisioningInProgress;
+  private int state;
+  private MediaCrypto mediaCrypto;
+  private Exception lastException;
+  private String mimeType;
+  private byte[] schemePsshData;
+  private byte[] sessionId;
+
+  /**
+   * @deprecated Use the other constructor, passing null as {@code optionalKeyRequestParameters}.
+   */
+  @Deprecated
+  public StreamingDrmSessionManager(UUID uuid, Looper playbackLooper, MediaDrmCallback callback,
+      Handler eventHandler, EventListener eventListener) throws UnsupportedSchemeException {
+    this(uuid, playbackLooper, callback, null, eventHandler, eventListener);
+  }
+
+  /**
+   * @param uuid The UUID of the drm scheme.
+   * @param playbackLooper The looper associated with the media playback thread. Should usually be
+   *     obtained using {@link com.google.android.exoplayer.ExoPlayer#getPlaybackLooper()}.
+   * @param callback Performs key and provisioning requests.
+   * @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
+   *     to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
+   * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
+   *     null if delivery of events is not required.
+   * @param eventListener A listener of events. May be null if delivery of events is not required.
+   * @throws UnsupportedSchemeException If the specified DRM scheme is not supported.
+   */
+  public StreamingDrmSessionManager(UUID uuid, Looper playbackLooper, MediaDrmCallback callback,
+      HashMap<String, String> optionalKeyRequestParameters, Handler eventHandler,
+      EventListener eventListener) throws UnsupportedSchemeException {
+    this.uuid = uuid;
+    this.callback = callback;
+    this.optionalKeyRequestParameters = optionalKeyRequestParameters;
+    this.eventHandler = eventHandler;
+    this.eventListener = eventListener;
+    mediaDrm = new MediaDrm(uuid);
+    mediaDrm.setOnEventListener(new MediaDrmEventListener());
+    mediaDrmHandler = new MediaDrmHandler(playbackLooper);
+    postResponseHandler = new PostResponseHandler(playbackLooper);
+    state = STATE_CLOSED;
+  }
+
+  @Override
+  public int getState() {
+    return state;
+  }
+
+  @Override
+  public MediaCrypto getMediaCrypto() {
+    if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
+      throw new IllegalStateException();
+    }
+    return mediaCrypto;
+  }
+
+  @Override
+  public boolean requiresSecureDecoderComponent(String mimeType) {
+    if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
+      throw new IllegalStateException();
+    }
+    return mediaCrypto.requiresSecureDecoderComponent(mimeType);
+  }
+
+  @Override
+  public Exception getError() {
+    return state == STATE_ERROR ? lastException : null;
+  }
+
+  /**
+   * Provides access to {@link MediaDrm#getPropertyString(String)}.
+   * <p>
+   * This method may be called when the manager is in any state.
+   *
+   * @param key The key to request.
+   * @return The retrieved property.
+   */
+  public final String getPropertyString(String key) {
+    return mediaDrm.getPropertyString(key);
+  }
+
+  /**
+   * Provides access to {@link MediaDrm#getPropertyByteArray(String)}.
+   * <p>
+   * This method may be called when the manager is in any state.
+   *
+   * @param key The key to request.
+   * @return The retrieved property.
+   */
+  public final byte[] getPropertyByteArray(String key) {
+    return mediaDrm.getPropertyByteArray(key);
+  }
+
+  @Override
+  public void open(Map<UUID, byte[]> psshData, String mimeType) {
+    if (++openCount != 1) {
+      return;
+    }
+    if (postRequestHandler == null) {
+      requestHandlerThread = new HandlerThread("DrmRequestHandler");
+      requestHandlerThread.start();
+      postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
+    }
+    if (this.schemePsshData == null) {
+      this.mimeType = mimeType;
+      schemePsshData = psshData.get(uuid);
+      if (schemePsshData == null) {
+        onError(new IllegalStateException("Media does not support uuid: " + uuid));
+        return;
+      }
+    }
+    state = STATE_OPENING;
+    openInternal(true);
+  }
+
+  @Override
+  public void close() {
+    if (--openCount != 0) {
+      return;
+    }
+    state = STATE_CLOSED;
+    provisioningInProgress = false;
+    mediaDrmHandler.removeCallbacksAndMessages(null);
+    postResponseHandler.removeCallbacksAndMessages(null);
+    postRequestHandler.removeCallbacksAndMessages(null);
+    postRequestHandler = null;
+    requestHandlerThread.quit();
+    requestHandlerThread = null;
+    schemePsshData = null;
+    mediaCrypto = null;
+    lastException = null;
+    if (sessionId != null) {
+      mediaDrm.closeSession(sessionId);
+      sessionId = null;
+    }
+  }
+
+  private void openInternal(boolean allowProvisioning) {
+    try {
+      sessionId = mediaDrm.openSession();
+      mediaCrypto = new MediaCrypto(uuid, sessionId);
+      state = STATE_OPENED;
+      postKeyRequest();
+    } catch (NotProvisionedException e) {
+      if (allowProvisioning) {
+        postProvisionRequest();
+      } else {
+        onError(e);
+      }
+    } catch (Exception e) {
+      onError(e);
+    }
+  }
+
+  private void postProvisionRequest() {
+    if (provisioningInProgress) {
+      return;
+    }
+    provisioningInProgress = true;
+    ProvisionRequest request = mediaDrm.getProvisionRequest();
+    postRequestHandler.obtainMessage(MSG_PROVISION, request).sendToTarget();
+  }
+
+  private void onProvisionResponse(Object response) {
+    provisioningInProgress = false;
+    if (state != STATE_OPENING && state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
+      // This event is stale.
+      return;
+    }
+
+    if (response instanceof Exception) {
+      onError((Exception) response);
+      return;
+    }
+
+    try {
+      mediaDrm.provideProvisionResponse((byte[]) response);
+      if (state == STATE_OPENING) {
+        openInternal(false);
+      } else {
+        postKeyRequest();
+      }
+    } catch (DeniedByServerException e) {
+      onError(e);
+    }
+  }
+
+  private void postKeyRequest() {
+    KeyRequest keyRequest;
+    try {
+      keyRequest = mediaDrm.getKeyRequest(sessionId, schemePsshData, mimeType,
+          MediaDrm.KEY_TYPE_STREAMING, optionalKeyRequestParameters);
+      postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
+    } catch (NotProvisionedException e) {
+      onKeysError(e);
+    }
+  }
+
+  private void onKeyResponse(Object response) {
+    if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
+      // This event is stale.
+      return;
+    }
+
+    if (response instanceof Exception) {
+      onKeysError((Exception) response);
+      return;
+    }
+
+    try {
+      mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
+      state = STATE_OPENED_WITH_KEYS;
+    } catch (Exception e) {
+      onKeysError(e);
+    }
+  }
+
+  private void onKeysError(Exception e) {
+    if (e instanceof NotProvisionedException) {
+      postProvisionRequest();
+    } else {
+      onError(e);
+    }
+  }
+
+  private void onError(final Exception e) {
+    lastException = e;
+    if (eventHandler != null && eventListener != null) {
+      eventHandler.post(new Runnable() {
+        @Override
+        public void run() {
+          eventListener.onDrmSessionManagerError(e);
+        }
+      });
+    }
+    if (state != STATE_OPENED_WITH_KEYS) {
+      state = STATE_ERROR;
+    }
+  }
+
+  @SuppressLint("HandlerLeak")
+  private class MediaDrmHandler extends Handler {
+
+    public MediaDrmHandler(Looper looper) {
+      super(looper);
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+      if (openCount == 0 || (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS)) {
+        return;
+      }
+      switch (msg.what) {
+        case MediaDrm.EVENT_KEY_REQUIRED:
+          postKeyRequest();
+          return;
+        case MediaDrm.EVENT_KEY_EXPIRED:
+          state = STATE_OPENED;
+          postKeyRequest();
+          return;
+        case MediaDrm.EVENT_PROVISION_REQUIRED:
+          state = STATE_OPENED;
+          postProvisionRequest();
+          return;
+      }
+    }
+
+  }
+
+  private class MediaDrmEventListener implements OnEventListener {
+
+    @Override
+    public void onEvent(MediaDrm md, byte[] sessionId, int event, int extra, byte[] data) {
+      mediaDrmHandler.sendEmptyMessage(event);
+    }
+
+  }
+
+  @SuppressLint("HandlerLeak")
+  private class PostResponseHandler extends Handler {
+
+    public PostResponseHandler(Looper looper) {
+      super(looper);
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+      switch (msg.what) {
+        case MSG_PROVISION:
+          onProvisionResponse(msg.obj);
+          return;
+        case MSG_KEYS:
+          onKeyResponse(msg.obj);
+          return;
+      }
+    }
+
+  }
+
+  @SuppressLint("HandlerLeak")
+  private class PostRequestHandler extends Handler {
+
+    public PostRequestHandler(Looper backgroundLooper) {
+      super(backgroundLooper);
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+      Object response;
+      try {
+        switch (msg.what) {
+          case MSG_PROVISION:
+            response = callback.executeProvisionRequest(uuid, (ProvisionRequest) msg.obj);
+            break;
+          case MSG_KEYS:
+            response = callback.executeKeyRequest(uuid, (KeyRequest) msg.obj);
+            break;
+          default:
+            throw new RuntimeException();
+        }
+      } catch (Exception e) {
+        response = e;
+      }
+      postResponseHandler.obtainMessage(msg.what, response).sendToTarget();
+    }
+
+  }
+
+}

+ 117 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/DataChunk.java

@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.hls;
+
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.upstream.DataSpec;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+/**
+ * An abstract base class for {@link HlsChunk} implementations where the data should be loaded into
+ * a {@code byte[]} before being consumed.
+ */
+public abstract class DataChunk extends HlsChunk {
+
+  private static final int READ_GRANULARITY = 16 * 1024;
+
+  private byte[] data;
+  private int limit;
+
+  private volatile boolean loadFinished;
+  private volatile boolean loadCanceled;
+
+  /**
+   * @param dataSource The source from which the data should be loaded.
+   * @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed
+   *     {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then
+   *     the length resolved by {@code dataSource.open(dataSpec)} must not exceed
+   *     {@link Integer#MAX_VALUE}.
+   * @param data An optional recycled array that can be used as a holder for the data.
+   */
+  public DataChunk(DataSource dataSource, DataSpec dataSpec, byte[] data) {
+    super(dataSource, dataSpec);
+    this.data = data;
+  }
+
+  @Override
+  public void consume() throws IOException {
+    consume(data, limit);
+  }
+
+  /**
+   * Invoked by {@link #consume()}. Implementations should override this method to consume the
+   * loaded data.
+   *
+   * @param data An array containing the data.
+   * @param limit The limit of the data.
+   * @throws IOException If an error occurs consuming the loaded data.
+   */
+  protected abstract void consume(byte[] data, int limit) throws IOException;
+
+  /**
+   * Whether the whole of the chunk has been loaded.
+   *
+   * @return True if the whole of the chunk has been loaded. False otherwise.
+   */
+  @Override
+  public boolean isLoadFinished() {
+    return loadFinished;
+  }
+
+  // Loadable implementation
+
+  @Override
+  public final void cancelLoad() {
+    loadCanceled = true;
+  }
+
+  @Override
+  public final boolean isLoadCanceled() {
+    return loadCanceled;
+  }
+
+  @Override
+  public final void load() throws IOException, InterruptedException {
+    try {
+      dataSource.open(dataSpec);
+      limit = 0;
+      int bytesRead = 0;
+      while (bytesRead != -1 && !loadCanceled) {
+        maybeExpandData();
+        bytesRead = dataSource.read(data, limit, READ_GRANULARITY);
+        if (bytesRead != -1) {
+          limit += bytesRead;
+        }
+      }
+      loadFinished = !loadCanceled;
+    } finally {
+      dataSource.close();
+    }
+  }
+
+  private void maybeExpandData() {
+    if (data == null) {
+      data = new byte[READ_GRANULARITY];
+    } else if (data.length < limit + READ_GRANULARITY) {
+      // The new length is calculated as (data.length + READ_GRANULARITY) rather than
+      // (limit + READ_GRANULARITY) in order to avoid small increments in the length.
+      data = Arrays.copyOf(data, data.length + READ_GRANULARITY);
+    }
+  }
+
+}

+ 51 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/HlsChunk.java

@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.hls;
+
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.upstream.DataSpec;
+import com.google.android.exoplayer.upstream.Loader.Loadable;
+import com.google.android.exoplayer.util.Assertions;
+
+import java.io.IOException;
+
+/**
+ * An abstract base class for {@link Loadable} implementations that load chunks of data required
+ * for the playback of HLS streams.
+ */
+public abstract class HlsChunk implements Loadable {
+
+  protected final DataSource dataSource;
+  protected final DataSpec dataSpec;
+
+  /**
+   * @param dataSource The source from which the data should be loaded.
+   * @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed
+   *     {@link Integer#MAX_VALUE}. If {@code dataSpec.length == C.LENGTH_UNBOUNDED} then
+   *     the length resolved by {@code dataSource.open(dataSpec)} must not exceed
+   *     {@link Integer#MAX_VALUE}.
+   */
+  public HlsChunk(DataSource dataSource, DataSpec dataSpec) {
+    Assertions.checkState(dataSpec.length <= Integer.MAX_VALUE);
+    this.dataSource = Assertions.checkNotNull(dataSource);
+    this.dataSpec = Assertions.checkNotNull(dataSpec);
+  }
+
+  public abstract void consume() throws IOException;
+
+  public abstract boolean isLoadFinished();
+
+}

+ 589 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/HlsChunkSource.java

@@ -0,0 +1,589 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.hls;
+
+import com.google.android.exoplayer.C;
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.hls.parser.AdtsExtractor;
+import com.google.android.exoplayer.hls.parser.HlsExtractor;
+import com.google.android.exoplayer.hls.parser.TsExtractor;
+import com.google.android.exoplayer.upstream.Aes128DataSource;
+import com.google.android.exoplayer.upstream.BandwidthMeter;
+import com.google.android.exoplayer.upstream.BufferPool;
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.upstream.DataSpec;
+import com.google.android.exoplayer.upstream.HttpDataSource.InvalidResponseCodeException;
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.Util;
+
+import android.net.Uri;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * A temporary test source of HLS chunks.
+ * <p>
+ * TODO: Figure out whether this should merge with the chunk package, or whether the hls
+ * implementation is going to naturally diverge.
+ */
+public class HlsChunkSource {
+
+  /**
+   * Adaptive switching is disabled.
+   * <p>
+   * The initially selected variant will be used throughout playback.
+   */
+  public static final int ADAPTIVE_MODE_NONE = 0;
+
+  /**
+   * Adaptive switches splice overlapping segments of the old and new variants.
+   * <p>
+   * When performing a switch from one variant to another, overlapping segments will be requested
+   * from both the old and new variants. These segments will then be spliced together, allowing
+   * a seamless switch from one variant to another even if keyframes are misaligned or if keyframes
+   * are not positioned at the start of each segment.
+   * <p>
+   * Note that where it can be guaranteed that the source content has keyframes positioned at the
+   * start of each segment, {@link #ADAPTIVE_MODE_ABRUPT} should always be used in preference to
+   * this mode.
+   */
+  public static final int ADAPTIVE_MODE_SPLICE = 1;
+
+  /**
+   * Adaptive switches are performed at segment boundaries.
+   * <p>
+   * For this mode to perform seamless switches, the source content is required to have keyframes
+   * positioned at the start of each segment. If this is not the case a visual discontinuity may
+   * be experienced when switching from one variant to another.
+   * <p>
+   * Note that where it can be guaranteed that the source content does have keyframes positioned at
+   * the start of each segment, this mode should always be used in preference to
+   * {@link #ADAPTIVE_MODE_SPLICE} because it requires fetching less data.
+   */
+  public static final int ADAPTIVE_MODE_ABRUPT = 3;
+
+  /**
+   * The default target buffer size in bytes.
+   */
+  public static final int DEFAULT_TARGET_BUFFER_SIZE = 18 * 1024 * 1024;
+
+  /**
+   * The default target buffer duration in milliseconds.
+   */
+  public static final long DEFAULT_TARGET_BUFFER_DURATION_MS = 40000;
+
+  /**
+   * The default minimum duration of media that needs to be buffered for a switch to a higher
+   * quality variant to be considered.
+   */
+  public static final long DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS = 5000;
+
+  /**
+   * The default maximum duration of media that needs to be buffered for a switch to a lower
+   * quality variant to be considered.
+   */
+  public static final long DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS = 20000;
+
+  private static final String TAG = "HlsChunkSource";
+  private static final String AAC_FILE_EXTENSION = ".aac";
+  private static final float BANDWIDTH_FRACTION = 0.8f;
+
+  private final BufferPool bufferPool;
+  private final DataSource upstreamDataSource;
+  private final HlsPlaylistParser playlistParser;
+  private final Variant[] enabledVariants;
+  private final BandwidthMeter bandwidthMeter;
+  private final int adaptiveMode;
+  private final Uri baseUri;
+  private final int maxWidth;
+  private final int maxHeight;
+  private final int targetBufferSize;
+  private final long targetBufferDurationUs;
+  private final long minBufferDurationToSwitchUpUs;
+  private final long maxBufferDurationToSwitchDownUs;
+
+  /* package */ byte[] scratchSpace;
+  /* package */ final HlsMediaPlaylist[] mediaPlaylists;
+  /* package */ final boolean[] mediaPlaylistBlacklistFlags;
+  /* package */ final long[] lastMediaPlaylistLoadTimesMs;
+  /* package */ boolean live;
+  /* package */ long durationUs;
+
+  private int variantIndex;
+  private DataSource encryptedDataSource;
+  private Uri encryptionKeyUri;
+  private String encryptedDataSourceIv;
+  private byte[] encryptedDataSourceSecretKey;
+
+  public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist,
+      BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode) {
+    this(dataSource, playlistUrl, playlist, bandwidthMeter, variantIndices, adaptiveMode,
+        DEFAULT_TARGET_BUFFER_SIZE, DEFAULT_TARGET_BUFFER_DURATION_MS,
+        DEFAULT_MIN_BUFFER_TO_SWITCH_UP_MS, DEFAULT_MAX_BUFFER_TO_SWITCH_DOWN_MS);
+  }
+
+  /**
+   * @param dataSource A {@link DataSource} suitable for loading the media data.
+   * @param playlistUrl The playlist URL.
+   * @param playlist The hls playlist.
+   * @param bandwidthMeter provides an estimate of the currently available bandwidth.
+   * @param variantIndices A subset of variant indices to consider, or null to consider all of the
+   *     variants in the master playlist.
+   * @param adaptiveMode The mode for switching from one variant to another. One of
+   *     {@link #ADAPTIVE_MODE_NONE}, {@link #ADAPTIVE_MODE_ABRUPT} and
+   *     {@link #ADAPTIVE_MODE_SPLICE}.
+   * @param targetBufferSize The targeted buffer size in bytes. The buffer will not be filled more
+   *     than one chunk beyond this amount of data.
+   * @param targetBufferDurationMs The targeted duration of media to buffer ahead of the current
+   *     playback position. The buffer will not be filled more than one chunk beyond this position.
+   * @param minBufferDurationToSwitchUpMs The minimum duration of media that needs to be buffered
+   *     for a switch to a higher quality variant to be considered.
+   * @param maxBufferDurationToSwitchDownMs The maximum duration of media that needs to be buffered
+   *     for a switch to a lower quality variant to be considered.
+   */
+  public HlsChunkSource(DataSource dataSource, String playlistUrl, HlsPlaylist playlist,
+      BandwidthMeter bandwidthMeter, int[] variantIndices, int adaptiveMode,
+      int targetBufferSize, long targetBufferDurationMs, long minBufferDurationToSwitchUpMs,
+      long maxBufferDurationToSwitchDownMs) {
+    this.upstreamDataSource = dataSource;
+    this.bandwidthMeter = bandwidthMeter;
+    this.adaptiveMode = adaptiveMode;
+    this.targetBufferSize = targetBufferSize;
+    targetBufferDurationUs = targetBufferDurationMs * 1000;
+    minBufferDurationToSwitchUpUs = minBufferDurationToSwitchUpMs * 1000;
+    maxBufferDurationToSwitchDownUs = maxBufferDurationToSwitchDownMs * 1000;
+    baseUri = playlist.baseUri;
+    playlistParser = new HlsPlaylistParser();
+    bufferPool = new BufferPool(256 * 1024);
+
+    if (playlist.type == HlsPlaylist.TYPE_MEDIA) {
+      enabledVariants = new Variant[] {new Variant(0, playlistUrl, 0, null, -1, -1)};
+      mediaPlaylists = new HlsMediaPlaylist[1];
+      mediaPlaylistBlacklistFlags = new boolean[1];
+      lastMediaPlaylistLoadTimesMs = new long[1];
+      setMediaPlaylist(0, (HlsMediaPlaylist) playlist);
+    } else {
+      Assertions.checkState(playlist.type == HlsPlaylist.TYPE_MASTER);
+      enabledVariants = filterVariants((HlsMasterPlaylist) playlist, variantIndices);
+      mediaPlaylists = new HlsMediaPlaylist[enabledVariants.length];
+      mediaPlaylistBlacklistFlags = new boolean[enabledVariants.length];
+      lastMediaPlaylistLoadTimesMs = new long[enabledVariants.length];
+    }
+
+    int maxWidth = -1;
+    int maxHeight = -1;
+    // Select the first variant from the master playlist that's enabled.
+    long minOriginalVariantIndex = Integer.MAX_VALUE;
+    for (int i = 0; i < enabledVariants.length; i++) {
+      if (enabledVariants[i].index < minOriginalVariantIndex) {
+        minOriginalVariantIndex = enabledVariants[i].index;
+        variantIndex = i;
+      }
+      maxWidth = Math.max(enabledVariants[i].width, maxWidth);
+      maxHeight = Math.max(enabledVariants[i].height, maxHeight);
+    }
+    // TODO: We should allow the default values to be passed through the constructor.
+    this.maxWidth = maxWidth > 0 ? maxWidth : 1920;
+    this.maxHeight = maxHeight > 0 ? maxHeight : 1080;
+  }
+
+  public long getDurationUs() {
+    return live ? C.UNKNOWN_TIME_US : durationUs;
+  }
+
+  /**
+   * Adaptive implementations must set the maximum video dimensions on the supplied
+   * {@link MediaFormat}. Other implementations do nothing.
+   * <p>
+   * Only called when the source is enabled.
+   *
+   * @param out The {@link MediaFormat} on which the maximum video dimensions should be set.
+   */
+  public void getMaxVideoDimensions(MediaFormat out) {
+    out.setMaxVideoDimensions(maxWidth, maxHeight);
+  }
+
+  /**
+   * Returns the next {@link HlsChunk} that should be loaded.
+   *
+   * @param previousTsChunk The previously loaded chunk that the next chunk should follow.
+   * @param seekPositionUs If there is no previous chunk, this parameter must specify the seek
+   *     position. If there is a previous chunk then this parameter is ignored.
+   * @param playbackPositionUs The current playback position.
+   * @return The next chunk to load.
+   */
+  public HlsChunk getChunkOperation(TsChunk previousTsChunk, long seekPositionUs,
+      long playbackPositionUs) {
+    if (previousTsChunk != null && (previousTsChunk.isLastChunk
+        || previousTsChunk.endTimeUs - playbackPositionUs >= targetBufferDurationUs)
+        || bufferPool.getAllocatedSize() >= targetBufferSize) {
+      // We're either finished, or we have the target amount of data or time buffered.
+      return null;
+    }
+
+    int nextVariantIndex = variantIndex;
+    boolean switchingVariant = false;
+    boolean switchingVariantSpliced = false;
+    if (adaptiveMode == ADAPTIVE_MODE_NONE) {
+      // Do nothing.
+    } else {
+      nextVariantIndex = getNextVariantIndex(previousTsChunk, playbackPositionUs);
+      switchingVariant = nextVariantIndex != variantIndex;
+      switchingVariantSpliced = switchingVariant && adaptiveMode == ADAPTIVE_MODE_SPLICE;
+    }
+
+    HlsMediaPlaylist mediaPlaylist = mediaPlaylists[nextVariantIndex];
+    if (mediaPlaylist == null) {
+      // We don't have the media playlist for the next variant. Request it now.
+      return newMediaPlaylistChunk(nextVariantIndex);
+    }
+
+    variantIndex = nextVariantIndex;
+    int chunkMediaSequence = 0;
+    boolean liveDiscontinuity = false;
+    if (live) {
+      if (previousTsChunk == null) {
+        chunkMediaSequence = getLiveStartChunkMediaSequence(variantIndex);
+      } else {
+        chunkMediaSequence = switchingVariantSpliced
+            ? previousTsChunk.chunkIndex : previousTsChunk.chunkIndex + 1;
+        if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
+          // If the chunk is no longer in the playlist. Skip ahead and start again.
+          chunkMediaSequence = getLiveStartChunkMediaSequence(variantIndex);
+          liveDiscontinuity = true;
+        }
+      }
+    } else {
+      // Not live.
+      if (previousTsChunk == null) {
+        chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, seekPositionUs, true,
+            true) + mediaPlaylist.mediaSequence;
+      } else {
+        chunkMediaSequence = switchingVariantSpliced
+            ? previousTsChunk.chunkIndex : previousTsChunk.chunkIndex + 1;
+      }
+    }
+
+    int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence;
+    if (chunkIndex >= mediaPlaylist.segments.size()) {
+      if (mediaPlaylist.live && shouldRerequestMediaPlaylist(variantIndex)) {
+        return newMediaPlaylistChunk(variantIndex);
+      } else {
+        return null;
+      }
+    }
+
+    HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex);
+    Uri chunkUri = Util.getMergedUri(mediaPlaylist.baseUri, segment.url);
+
+    // Check if encryption is specified.
+    if (HlsMediaPlaylist.ENCRYPTION_METHOD_AES_128.equals(segment.encryptionMethod)) {
+      Uri keyUri = Util.getMergedUri(mediaPlaylist.baseUri, segment.encryptionKeyUri);
+      if (!keyUri.equals(encryptionKeyUri)) {
+        // Encryption is specified and the key has changed.
+        HlsChunk toReturn = newEncryptionKeyChunk(keyUri, segment.encryptionIV);
+        return toReturn;
+      }
+      if (!Util.areEqual(segment.encryptionIV, encryptedDataSourceIv)) {
+        initEncryptedDataSource(keyUri, segment.encryptionIV, encryptedDataSourceSecretKey);
+      }
+    } else {
+      clearEncryptedDataSource();
+    }
+
+    // Configure the data source and spec for the chunk.
+    DataSource dataSource = encryptedDataSource != null ? encryptedDataSource : upstreamDataSource;
+    DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength,
+        null);
+
+    // Compute start and end times, and the sequence number of the next chunk.
+    long startTimeUs;
+    if (live) {
+      if (previousTsChunk == null) {
+        startTimeUs = 0;
+      } else if (switchingVariantSpliced) {
+        startTimeUs = previousTsChunk.startTimeUs;
+      } else {
+        startTimeUs = previousTsChunk.endTimeUs;
+      }
+    } else /* Not live */ {
+      startTimeUs = segment.startTimeUs;
+    }
+    long endTimeUs = startTimeUs + (long) (segment.durationSecs * C.MICROS_PER_SECOND);
+    boolean isLastChunk = !mediaPlaylist.live && chunkIndex == mediaPlaylist.segments.size() - 1;
+
+    // Configure the extractor that will read the chunk.
+    HlsExtractor extractor;
+    if (previousTsChunk == null || segment.discontinuity || switchingVariant || liveDiscontinuity) {
+      extractor = chunkUri.getLastPathSegment().endsWith(AAC_FILE_EXTENSION)
+          ? new AdtsExtractor(switchingVariantSpliced, startTimeUs, bufferPool)
+          : new TsExtractor(switchingVariantSpliced, startTimeUs, bufferPool);
+    } else {
+      extractor = previousTsChunk.extractor;
+    }
+
+    return new TsChunk(dataSource, dataSpec, extractor, enabledVariants[variantIndex].index,
+        startTimeUs, endTimeUs, chunkMediaSequence, isLastChunk);
+  }
+
+  /**
+   * Invoked when an error occurs loading a chunk.
+   *
+   * @param chunk The chunk whose load failed.
+   * @param e The failure.
+   * @return True if the error was handled by the source. False otherwise.
+   */
+  public boolean onLoadError(HlsChunk chunk, IOException e) {
+    if ((chunk instanceof MediaPlaylistChunk) && (e instanceof InvalidResponseCodeException)) {
+      InvalidResponseCodeException responseCodeException = (InvalidResponseCodeException) e;
+      int responseCode = responseCodeException.responseCode;
+      if (responseCode == 404 || responseCode == 410) {
+        MediaPlaylistChunk playlistChunk = (MediaPlaylistChunk) chunk;
+        mediaPlaylistBlacklistFlags[playlistChunk.variantIndex] = true;
+        if (!allPlaylistsBlacklisted()) {
+          // We've handled the 404/410 by blacklisting the playlist.
+          Log.w(TAG, "Blacklisted playlist (" + responseCode + "): "
+              + playlistChunk.dataSpec.uri);
+          return true;
+        } else {
+          // This was the last non-blacklisted playlist. Don't blacklist it.
+          Log.w(TAG, "Final playlist not blacklisted (" + responseCode + "): "
+              + playlistChunk.dataSpec.uri);
+          mediaPlaylistBlacklistFlags[playlistChunk.variantIndex] = false;
+          return false;
+        }
+      }
+    }
+    return false;
+  }
+
+  private int getNextVariantIndex(TsChunk previousTsChunk, long playbackPositionUs) {
+    int idealVariantIndex = getVariantIndexForBandwdith(
+        (int) (bandwidthMeter.getBitrateEstimate() * BANDWIDTH_FRACTION));
+    if (idealVariantIndex == variantIndex) {
+      // We're already using the ideal variant.
+      return variantIndex;
+    }
+    // We're not using the ideal variant for the available bandwidth, but only switch if the
+    // conditions are appropriate.
+    long bufferedPositionUs = previousTsChunk == null ? playbackPositionUs
+        : adaptiveMode == ADAPTIVE_MODE_SPLICE ? previousTsChunk.startTimeUs
+        : previousTsChunk.endTimeUs;
+    long bufferedUs = bufferedPositionUs - playbackPositionUs;
+    if (mediaPlaylistBlacklistFlags[variantIndex]
+        || (idealVariantIndex > variantIndex && bufferedUs < maxBufferDurationToSwitchDownUs)
+        || (idealVariantIndex < variantIndex && bufferedUs > minBufferDurationToSwitchUpUs)) {
+      // Switch variant.
+      return idealVariantIndex;
+    }
+    // Stick with the current variant for now.
+    return variantIndex;
+  }
+
+  private int getVariantIndexForBandwdith(int bandwidth) {
+    int lowestQualityEnabledVariant = 0;
+    for (int i = 0; i < enabledVariants.length; i++) {
+      if (!mediaPlaylistBlacklistFlags[i]) {
+        if (enabledVariants[i].bandwidth <= bandwidth) {
+          return i;
+        }
+        lowestQualityEnabledVariant = i;
+      }
+    }
+    return lowestQualityEnabledVariant;
+  }
+
+  private boolean shouldRerequestMediaPlaylist(int variantIndex) {
+    // Don't re-request media playlist more often than one-half of the target duration.
+    HlsMediaPlaylist mediaPlaylist = mediaPlaylists[variantIndex];
+    long timeSinceLastMediaPlaylistLoadMs =
+        SystemClock.elapsedRealtime() - lastMediaPlaylistLoadTimesMs[variantIndex];
+    return timeSinceLastMediaPlaylistLoadMs >= (mediaPlaylist.targetDurationSecs * 1000) / 2;
+  }
+
+  private int getLiveStartChunkMediaSequence(int variantIndex) {
+    // For live start playback from the third chunk from the end.
+    HlsMediaPlaylist mediaPlaylist = mediaPlaylists[variantIndex];
+    int chunkIndex = mediaPlaylist.segments.size() > 3 ? mediaPlaylist.segments.size() - 3 : 0;
+    return chunkIndex + mediaPlaylist.mediaSequence;
+  }
+
+  private MediaPlaylistChunk newMediaPlaylistChunk(int variantIndex) {
+    Uri mediaPlaylistUri = Util.getMergedUri(baseUri, enabledVariants[variantIndex].url);
+    DataSpec dataSpec = new DataSpec(mediaPlaylistUri, 0, C.LENGTH_UNBOUNDED, null);
+    Uri baseUri = Util.parseBaseUri(mediaPlaylistUri.toString());
+    return new MediaPlaylistChunk(variantIndex, upstreamDataSource, dataSpec, baseUri);
+  }
+
+  private EncryptionKeyChunk newEncryptionKeyChunk(Uri keyUri, String iv) {
+    DataSpec dataSpec = new DataSpec(keyUri, 0, C.LENGTH_UNBOUNDED, null);
+    return new EncryptionKeyChunk(upstreamDataSource, dataSpec, iv);
+  }
+
+  /* package */ void initEncryptedDataSource(Uri keyUri, String iv, byte[] secretKey) {
+    String trimmedIv;
+    if (iv.toLowerCase(Locale.getDefault()).startsWith("0x")) {
+      trimmedIv = iv.substring(2);
+    } else {
+      trimmedIv = iv;
+    }
+
+    byte[] ivData = new BigInteger(trimmedIv, 16).toByteArray();
+    byte[] ivDataWithPadding = new byte[16];
+    int offset = ivData.length > 16 ? ivData.length - 16 : 0;
+    System.arraycopy(ivData, offset, ivDataWithPadding, ivDataWithPadding.length - ivData.length
+        + offset, ivData.length - offset);
+
+    encryptedDataSource = new Aes128DataSource(secretKey, ivDataWithPadding, upstreamDataSource);
+    encryptionKeyUri = keyUri;
+    encryptedDataSourceIv = iv;
+    encryptedDataSourceSecretKey = secretKey;
+  }
+
+  private void clearEncryptedDataSource() {
+    encryptionKeyUri = null;
+    encryptedDataSource = null;
+    encryptedDataSourceIv = null;
+    encryptedDataSourceSecretKey = null;
+  }
+
+  /* package */ void setMediaPlaylist(int variantIndex, HlsMediaPlaylist mediaPlaylist) {
+    lastMediaPlaylistLoadTimesMs[variantIndex] = SystemClock.elapsedRealtime();
+    mediaPlaylists[variantIndex] = mediaPlaylist;
+    live |= mediaPlaylist.live;
+    durationUs = mediaPlaylist.durationUs;
+  }
+
+  private static Variant[] filterVariants(HlsMasterPlaylist masterPlaylist, int[] variantIndices) {
+    List<Variant> masterVariants = masterPlaylist.variants;
+    ArrayList<Variant> enabledVariants = new ArrayList<Variant>();
+    if (variantIndices != null) {
+      for (int i = 0; i < variantIndices.length; i++) {
+        enabledVariants.add(masterVariants.get(variantIndices[i]));
+      }
+    } else {
+      // If variantIndices is null then all variants are initially considered.
+      enabledVariants.addAll(masterVariants);
+    }
+
+    ArrayList<Variant> definiteVideoVariants = new ArrayList<Variant>();
+    ArrayList<Variant> definiteAudioOnlyVariants = new ArrayList<Variant>();
+    for (int i = 0; i < enabledVariants.size(); i++) {
+      Variant variant = enabledVariants.get(i);
+      if (variant.height > 0 || variantHasExplicitCodecWithPrefix(variant, "avc")) {
+        definiteVideoVariants.add(variant);
+      } else if (variantHasExplicitCodecWithPrefix(variant, "mp4a")) {
+        definiteAudioOnlyVariants.add(variant);
+      }
+    }
+
+    if (!definiteVideoVariants.isEmpty()) {
+      // We've identified some variants as definitely containing video. Assume variants within the
+      // master playlist are marked consistently, and hence that we have the full set. Filter out
+      // any other variants, which are likely to be audio only.
+      enabledVariants = definiteVideoVariants;
+    } else if (definiteAudioOnlyVariants.size() < enabledVariants.size()) {
+      // We've identified some variants, but not all, as being audio only. Filter them out to leave
+      // the remaining variants, which are likely to contain video.
+      enabledVariants.removeAll(definiteAudioOnlyVariants);
+    } else {
+      // Leave the enabled variants unchanged. They're likely either all video or all audio.
+    }
+
+    Collections.sort(enabledVariants, new Variant.DecreasingBandwidthComparator());
+
+    Variant[] enabledVariantsArray = new Variant[enabledVariants.size()];
+    enabledVariants.toArray(enabledVariantsArray);
+    return enabledVariantsArray;
+  }
+
+  private static boolean variantHasExplicitCodecWithPrefix(Variant variant, String prefix) {
+    String[] codecs = variant.codecs;
+    if (codecs == null) {
+      return false;
+    }
+    for (int i = 0; i < codecs.length; i++) {
+      if (codecs[i].startsWith(prefix)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private boolean allPlaylistsBlacklisted() {
+    for (int i = 0; i < mediaPlaylistBlacklistFlags.length; i++) {
+      if (!mediaPlaylistBlacklistFlags[i]) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private class MediaPlaylistChunk extends DataChunk {
+
+    @SuppressWarnings("hiding")
+    /* package */ final int variantIndex;
+
+    private final Uri playlistBaseUri;
+
+    public MediaPlaylistChunk(int variantIndex, DataSource dataSource, DataSpec dataSpec,
+        Uri playlistBaseUri) {
+      super(dataSource, dataSpec, scratchSpace);
+      this.variantIndex = variantIndex;
+      this.playlistBaseUri = playlistBaseUri;
+    }
+
+    @Override
+    protected void consume(byte[] data, int limit) throws IOException {
+      HlsPlaylist playlist = playlistParser.parse(new ByteArrayInputStream(data, 0, limit),
+          null, null, playlistBaseUri);
+      Assertions.checkState(playlist.type == HlsPlaylist.TYPE_MEDIA);
+      HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
+      setMediaPlaylist(variantIndex, mediaPlaylist);
+      // Recycle the allocation.
+      scratchSpace = data;
+    }
+
+  }
+
+  private class EncryptionKeyChunk extends DataChunk {
+
+    private final String iv;
+
+    public EncryptionKeyChunk(DataSource dataSource, DataSpec dataSpec, String iv) {
+      super(dataSource, dataSpec, scratchSpace);
+      this.iv = iv;
+    }
+
+    @Override
+    protected void consume(byte[] data, int limit) throws IOException {
+      initEncryptedDataSource(dataSpec.uri, iv, Arrays.copyOf(data, limit));
+      // Recycle the allocation.
+      scratchSpace = data;
+    }
+
+  }
+
+}

+ 34 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/HlsMasterPlaylist.java

@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.hls;
+
+import android.net.Uri;
+
+import java.util.List;
+
+/**
+ * Represents an HLS master playlist.
+ */
+public final class HlsMasterPlaylist extends HlsPlaylist {
+
+  public final List<Variant> variants;
+
+  public HlsMasterPlaylist(Uri baseUri, List<Variant> variants) {
+    super(baseUri, HlsPlaylist.TYPE_MASTER);
+    this.variants = variants;
+  }
+
+}

+ 90 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/HlsMediaPlaylist.java

@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.hls;
+
+import com.google.android.exoplayer.C;
+
+import android.net.Uri;
+
+import java.util.List;
+
+/**
+ * Represents an HLS media playlist.
+ */
+public final class HlsMediaPlaylist extends HlsPlaylist {
+
+  /**
+   * Media segment reference.
+   */
+  public static final class Segment implements Comparable<Long> {
+    public final boolean discontinuity;
+    public final double durationSecs;
+    public final String url;
+    public final long startTimeUs;
+    public final String encryptionMethod;
+    public final String encryptionKeyUri;
+    public final String encryptionIV;
+    public final int byterangeOffset;
+    public final int byterangeLength;
+
+    public Segment(String uri, double durationSecs, boolean discontinuity, long startTimeUs,
+        String encryptionMethod, String encryptionKeyUri, String encryptionIV,
+        int byterangeOffset, int byterangeLength) {
+      this.url = uri;
+      this.durationSecs = durationSecs;
+      this.discontinuity = discontinuity;
+      this.startTimeUs = startTimeUs;
+      this.encryptionMethod = encryptionMethod;
+      this.encryptionKeyUri = encryptionKeyUri;
+      this.encryptionIV = encryptionIV;
+      this.byterangeOffset = byterangeOffset;
+      this.byterangeLength = byterangeLength;
+    }
+
+    @Override
+    public int compareTo(Long startTimeUs) {
+      return this.startTimeUs > startTimeUs ? 1 : (this.startTimeUs < startTimeUs ? -1 : 0);
+    }
+  }
+
+  public static final String ENCRYPTION_METHOD_NONE = "NONE";
+  public static final String ENCRYPTION_METHOD_AES_128 = "AES-128";
+
+  public final int mediaSequence;
+  public final int targetDurationSecs;
+  public final int version;
+  public final List<Segment> segments;
+  public final boolean live;
+  public final long durationUs;
+
+  public HlsMediaPlaylist(Uri baseUri, int mediaSequence, int targetDurationSecs, int version,
+      boolean live, List<Segment> segments) {
+    super(baseUri, HlsPlaylist.TYPE_MEDIA);
+    this.mediaSequence = mediaSequence;
+    this.targetDurationSecs = targetDurationSecs;
+    this.version = version;
+    this.live = live;
+    this.segments = segments;
+
+    if (!segments.isEmpty()) {
+      Segment last = segments.get(segments.size() - 1);
+      durationUs = last.startTimeUs + (long) (last.durationSecs * C.MICROS_PER_SECOND);
+    } else {
+      durationUs = 0;
+    }
+  }
+
+}

+ 57 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/HlsParserUtil.java

@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.hls;
+
+import com.google.android.exoplayer.ParserException;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utility methods for HLS manifest parsing.
+ */
+/* package */ class HlsParserUtil {
+
+  private HlsParserUtil() {}
+
+  public static String parseStringAttr(String line, Pattern pattern, String tag)
+      throws ParserException {
+    Matcher matcher = pattern.matcher(line);
+    if (matcher.find() && matcher.groupCount() == 1) {
+      return matcher.group(1);
+    }
+    throw new ParserException(String.format("Couldn't match %s tag in %s", tag, line));
+  }
+
+  public static String parseOptionalStringAttr(String line, Pattern pattern) {
+    Matcher matcher = pattern.matcher(line);
+    if (matcher.find() && matcher.groupCount() == 1) {
+      return matcher.group(1);
+    }
+    return null;
+  }
+
+  public static int parseIntAttr(String line, Pattern pattern, String tag)
+      throws ParserException {
+    return Integer.parseInt(parseStringAttr(line, pattern, tag));
+  }
+
+  public static double parseDoubleAttr(String line, Pattern pattern, String tag)
+      throws ParserException {
+    return Double.parseDouble(parseStringAttr(line, pattern, tag));
+  }
+
+}

+ 36 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/HlsPlaylist.java

@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.hls;
+
+import android.net.Uri;
+
+/**
+ * Represents an HLS playlist.
+ */
+public abstract class HlsPlaylist {
+
+  public final static int TYPE_MASTER = 0;
+  public final static int TYPE_MEDIA = 1;
+
+  public final Uri baseUri;
+  public final int type;
+
+  protected HlsPlaylist(Uri baseUri, int type) {
+    this.baseUri = baseUri;
+    this.type = type;
+  }
+
+}

+ 282 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/HlsPlaylistParser.java

@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.hls;
+
+import com.google.android.exoplayer.C;
+import com.google.android.exoplayer.ParserException;
+import com.google.android.exoplayer.hls.HlsMediaPlaylist.Segment;
+import com.google.android.exoplayer.util.ManifestParser;
+
+import android.net.Uri;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.regex.Pattern;
+
+/**
+ * HLS playlists parsing logic.
+ */
+public final class HlsPlaylistParser implements ManifestParser<HlsPlaylist> {
+
+  private static final String VERSION_TAG = "#EXT-X-VERSION";
+
+  private static final String STREAM_INF_TAG = "#EXT-X-STREAM-INF";
+  private static final String BANDWIDTH_ATTR = "BANDWIDTH";
+  private static final String CODECS_ATTR = "CODECS";
+  private static final String RESOLUTION_ATTR = "RESOLUTION";
+
+  private static final String DISCONTINUITY_TAG = "#EXT-X-DISCONTINUITY";
+  private static final String MEDIA_DURATION_TAG = "#EXTINF";
+  private static final String MEDIA_SEQUENCE_TAG = "#EXT-X-MEDIA-SEQUENCE";
+  private static final String TARGET_DURATION_TAG = "#EXT-X-TARGETDURATION";
+  private static final String ENDLIST_TAG = "#EXT-X-ENDLIST";
+  private static final String KEY_TAG = "#EXT-X-KEY";
+  private static final String BYTERANGE_TAG = "#EXT-X-BYTERANGE";
+
+  private static final String METHOD_ATTR = "METHOD";
+  private static final String URI_ATTR = "URI";
+  private static final String IV_ATTR = "IV";
+
+  private static final Pattern BANDWIDTH_ATTR_REGEX =
+      Pattern.compile(BANDWIDTH_ATTR + "=(\\d+)\\b");
+  private static final Pattern CODECS_ATTR_REGEX =
+      Pattern.compile(CODECS_ATTR + "=\"(.+?)\"");
+  private static final Pattern RESOLUTION_ATTR_REGEX =
+      Pattern.compile(RESOLUTION_ATTR + "=(\\d+x\\d+)");
+
+  private static final Pattern MEDIA_DURATION_REGEX =
+      Pattern.compile(MEDIA_DURATION_TAG + ":([\\d.]+),");
+  private static final Pattern MEDIA_SEQUENCE_REGEX =
+      Pattern.compile(MEDIA_SEQUENCE_TAG + ":(\\d+)\\b");
+  private static final Pattern TARGET_DURATION_REGEX =
+      Pattern.compile(TARGET_DURATION_TAG + ":(\\d+)\\b");
+  private static final Pattern VERSION_REGEX =
+      Pattern.compile(VERSION_TAG + ":(\\d+)\\b");
+  private static final Pattern BYTERANGE_REGEX =
+      Pattern.compile(BYTERANGE_TAG + ":(\\d+(?:@\\d+)?)\\b");
+
+  private static final Pattern METHOD_ATTR_REGEX =
+      Pattern.compile(METHOD_ATTR + "=([^,.*]+)");
+  private static final Pattern URI_ATTR_REGEX =
+      Pattern.compile(URI_ATTR + "=\"(.+)\"");
+  private static final Pattern IV_ATTR_REGEX =
+      Pattern.compile(IV_ATTR + "=([^,.*]+)");
+
+  @Override
+  public HlsPlaylist parse(InputStream inputStream, String inputEncoding,
+      String contentId, Uri baseUri) throws IOException {
+    BufferedReader reader = new BufferedReader((inputEncoding == null)
+        ? new InputStreamReader(inputStream) : new InputStreamReader(inputStream, inputEncoding));
+    Queue<String> extraLines = new LinkedList<String>();
+    String line;
+    try {
+      while ((line = reader.readLine()) != null) {
+        line = line.trim();
+        if (line.isEmpty()) {
+          // Do nothing.
+        } else if (line.startsWith(STREAM_INF_TAG)) {
+          extraLines.add(line);
+          return parseMasterPlaylist(new LineIterator(extraLines, reader), baseUri);
+        } else if (line.startsWith(TARGET_DURATION_TAG)
+            || line.startsWith(MEDIA_SEQUENCE_TAG)
+            || line.startsWith(MEDIA_DURATION_TAG)
+            || line.startsWith(KEY_TAG)
+            || line.startsWith(BYTERANGE_TAG)
+            || line.equals(DISCONTINUITY_TAG)
+            || line.equals(ENDLIST_TAG)) {
+          extraLines.add(line);
+          return parseMediaPlaylist(new LineIterator(extraLines, reader), baseUri);
+        } else if (line.startsWith(VERSION_TAG)) {
+          extraLines.add(line);
+        } else if (!line.startsWith("#")) {
+          throw new ParserException("Missing a tag before URL.");
+        }
+      }
+    } finally {
+      reader.close();
+    }
+    throw new ParserException("Failed to parse the playlist, could not identify any tags.");
+  }
+
+  private static HlsMasterPlaylist parseMasterPlaylist(LineIterator iterator, Uri baseUri)
+      throws IOException {
+    List<Variant> variants = new ArrayList<Variant>();
+    int bandwidth = 0;
+    String[] codecs = null;
+    int width = -1;
+    int height = -1;
+    int variantIndex = 0;
+
+    String line;
+    while (iterator.hasNext()) {
+      line = iterator.next();
+      if (line.startsWith(STREAM_INF_TAG)) {
+        bandwidth = HlsParserUtil.parseIntAttr(line, BANDWIDTH_ATTR_REGEX, BANDWIDTH_ATTR);
+        String codecsString = HlsParserUtil.parseOptionalStringAttr(line, CODECS_ATTR_REGEX);
+        if (codecsString != null) {
+          codecs = codecsString.split("(\\s*,\\s*)|(\\s*$)");
+        } else {
+          codecs = null;
+        }
+        String resolutionString = HlsParserUtil.parseOptionalStringAttr(line,
+            RESOLUTION_ATTR_REGEX);
+        if (resolutionString != null) {
+          String[] widthAndHeight = resolutionString.split("x");
+          width = Integer.parseInt(widthAndHeight[0]);
+          height = Integer.parseInt(widthAndHeight[1]);
+        } else {
+          width = -1;
+          height = -1;
+        }
+      } else if (!line.startsWith("#")) {
+        variants.add(new Variant(variantIndex++, line, bandwidth, codecs, width, height));
+        bandwidth = 0;
+        codecs = null;
+        width = -1;
+        height = -1;
+      }
+    }
+    return new HlsMasterPlaylist(baseUri, Collections.unmodifiableList(variants));
+  }
+
+  private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, Uri baseUri)
+      throws IOException {
+    int mediaSequence = 0;
+    int targetDurationSecs = 0;
+    int version = 1; // Default version == 1.
+    boolean live = true;
+    List<Segment> segments = new ArrayList<Segment>();
+
+    double segmentDurationSecs = 0.0;
+    boolean segmentDiscontinuity = false;
+    long segmentStartTimeUs = 0;
+    String segmentEncryptionMethod = null;
+    String segmentEncryptionKeyUri = null;
+    String segmentEncryptionIV = null;
+    int segmentByterangeOffset = 0;
+    int segmentByterangeLength = C.LENGTH_UNBOUNDED;
+
+    int segmentMediaSequence = 0;
+
+    String line;
+    while (iterator.hasNext()) {
+      line = iterator.next();
+      if (line.startsWith(TARGET_DURATION_TAG)) {
+        targetDurationSecs = HlsParserUtil.parseIntAttr(line, TARGET_DURATION_REGEX,
+            TARGET_DURATION_TAG);
+      } else if (line.startsWith(MEDIA_SEQUENCE_TAG)) {
+        mediaSequence = HlsParserUtil.parseIntAttr(line, MEDIA_SEQUENCE_REGEX, MEDIA_SEQUENCE_TAG);
+        segmentMediaSequence = mediaSequence;
+      } else if (line.startsWith(VERSION_TAG)) {
+        version = HlsParserUtil.parseIntAttr(line, VERSION_REGEX, VERSION_TAG);
+      } else if (line.startsWith(MEDIA_DURATION_TAG)) {
+        segmentDurationSecs = HlsParserUtil.parseDoubleAttr(line, MEDIA_DURATION_REGEX,
+            MEDIA_DURATION_TAG);
+      } else if (line.startsWith(KEY_TAG)) {
+        segmentEncryptionMethod = HlsParserUtil.parseStringAttr(line, METHOD_ATTR_REGEX,
+            METHOD_ATTR);
+        if (segmentEncryptionMethod.equals(HlsMediaPlaylist.ENCRYPTION_METHOD_NONE)) {
+          segmentEncryptionKeyUri = null;
+          segmentEncryptionIV = null;
+        } else {
+          segmentEncryptionKeyUri = HlsParserUtil.parseStringAttr(line, URI_ATTR_REGEX,
+              URI_ATTR);
+          segmentEncryptionIV = HlsParserUtil.parseOptionalStringAttr(line, IV_ATTR_REGEX);
+          if (segmentEncryptionIV == null) {
+            segmentEncryptionIV = Integer.toHexString(segmentMediaSequence);
+          }
+        }
+      } else if (line.startsWith(BYTERANGE_TAG)) {
+        String byteRange = HlsParserUtil.parseStringAttr(line, BYTERANGE_REGEX, BYTERANGE_TAG);
+        String[] splitByteRange = byteRange.split("@");
+        segmentByterangeLength = Integer.parseInt(splitByteRange[0]);
+        if (splitByteRange.length > 1) {
+          segmentByterangeOffset = Integer.parseInt(splitByteRange[1]);
+        }
+      } else if (line.equals(DISCONTINUITY_TAG)) {
+        segmentDiscontinuity = true;
+      } else if (!line.startsWith("#")) {
+        segmentMediaSequence++;
+        if (segmentByterangeLength == C.LENGTH_UNBOUNDED) {
+          segmentByterangeOffset = 0;
+        }
+        segments.add(new Segment(line, segmentDurationSecs, segmentDiscontinuity,
+            segmentStartTimeUs, segmentEncryptionMethod, segmentEncryptionKeyUri,
+            segmentEncryptionIV, segmentByterangeOffset, segmentByterangeLength));
+        segmentStartTimeUs += (long) (segmentDurationSecs * C.MICROS_PER_SECOND);
+        segmentDiscontinuity = false;
+        segmentDurationSecs = 0.0;
+        if (segmentByterangeLength != C.LENGTH_UNBOUNDED) {
+          segmentByterangeOffset += segmentByterangeLength;
+        }
+        segmentByterangeLength = C.LENGTH_UNBOUNDED;
+      } else if (line.equals(ENDLIST_TAG)) {
+        live = false;
+        break;
+      }
+    }
+    return new HlsMediaPlaylist(baseUri, mediaSequence, targetDurationSecs, version, live,
+        Collections.unmodifiableList(segments));
+  }
+
+  private static class LineIterator {
+
+    private final BufferedReader reader;
+    private final Queue<String> extraLines;
+
+    private String next;
+
+    public LineIterator(Queue<String> extraLines, BufferedReader reader) {
+      this.extraLines = extraLines;
+      this.reader = reader;
+    }
+
+    public boolean hasNext() throws IOException {
+      if (next != null) {
+        return true;
+      }
+      if (!extraLines.isEmpty()) {
+        next = extraLines.poll();
+        return true;
+      }
+      while ((next = reader.readLine()) != null) {
+        next = next.trim();
+        if (!next.isEmpty()) {
+          return true;
+        }
+      }
+      return false;
+    }
+
+    public String next() throws IOException {
+      String result = null;
+      if (hasNext()) {
+        result = next;
+        next = null;
+      }
+      return result;
+    }
+
+  }
+
+}

+ 448 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/HlsSampleSource.java

@@ -0,0 +1,448 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.hls;
+
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.MediaFormatHolder;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.SampleSource;
+import com.google.android.exoplayer.TrackInfo;
+import com.google.android.exoplayer.TrackRenderer;
+import com.google.android.exoplayer.hls.parser.HlsExtractor;
+import com.google.android.exoplayer.upstream.Loader;
+import com.google.android.exoplayer.upstream.Loader.Loadable;
+import com.google.android.exoplayer.util.Assertions;
+
+import android.os.SystemClock;
+
+import java.io.IOException;
+import java.util.LinkedList;
+
+/**
+ * A {@link SampleSource} for HLS streams.
+ */
+public class HlsSampleSource implements SampleSource, Loader.Callback {
+
+  /**
+   * The default minimum number of times to retry loading data prior to failing.
+   */
+  public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3;
+
+  private static final int NO_RESET_PENDING = -1;
+
+  private final HlsChunkSource chunkSource;
+  private final LinkedList<HlsExtractor> extractors;
+  private final boolean frameAccurateSeeking;
+  private final int minLoadableRetryCount;
+
+  private int remainingReleaseCount;
+  private boolean prepared;
+  private int trackCount;
+  private int enabledTrackCount;
+  private boolean[] trackEnabledStates;
+  private boolean[] pendingDiscontinuities;
+  private TrackInfo[] trackInfos;
+  private MediaFormat[] downstreamMediaFormats;
+
+  private long downstreamPositionUs;
+  private long lastSeekPositionUs;
+  private long pendingResetPositionUs;
+
+  private TsChunk previousTsLoadable;
+  private HlsChunk currentLoadable;
+  private boolean loadingFinished;
+
+  private Loader loader;
+  private IOException currentLoadableException;
+  private boolean currentLoadableExceptionFatal;
+  private int currentLoadableExceptionCount;
+  private long currentLoadableExceptionTimestamp;
+
+  public HlsSampleSource(HlsChunkSource chunkSource, boolean frameAccurateSeeking,
+      int downstreamRendererCount) {
+    this(chunkSource, frameAccurateSeeking, downstreamRendererCount,
+        DEFAULT_MIN_LOADABLE_RETRY_COUNT);
+  }
+
+  public HlsSampleSource(HlsChunkSource chunkSource, boolean frameAccurateSeeking,
+      int downstreamRendererCount, int minLoadableRetryCount) {
+    this.chunkSource = chunkSource;
+    this.frameAccurateSeeking = frameAccurateSeeking;
+    this.remainingReleaseCount = downstreamRendererCount;
+    this.minLoadableRetryCount = minLoadableRetryCount;
+    extractors = new LinkedList<HlsExtractor>();
+  }
+
+  @Override
+  public boolean prepare() throws IOException {
+    if (prepared) {
+      return true;
+    }
+    if (loader == null) {
+      loader = new Loader("Loader:HLS");
+    }
+    continueBufferingInternal();
+    if (!extractors.isEmpty()) {
+      HlsExtractor extractor = extractors.getFirst();
+      if (extractor.isPrepared()) {
+        trackCount = extractor.getTrackCount();
+        trackEnabledStates = new boolean[trackCount];
+        pendingDiscontinuities = new boolean[trackCount];
+        downstreamMediaFormats = new MediaFormat[trackCount];
+        trackInfos = new TrackInfo[trackCount];
+        for (int i = 0; i < trackCount; i++) {
+          MediaFormat format = extractor.getFormat(i);
+          trackInfos[i] = new TrackInfo(format.mimeType, chunkSource.getDurationUs());
+        }
+        prepared = true;
+      }
+    }
+    if (!prepared) {
+      maybeThrowLoadableException();
+    }
+    return prepared;
+  }
+
+  @Override
+  public int getTrackCount() {
+    Assertions.checkState(prepared);
+    return trackCount;
+  }
+
+  @Override
+  public TrackInfo getTrackInfo(int track) {
+    Assertions.checkState(prepared);
+    return trackInfos[track];
+  }
+
+  @Override
+  public void enable(int track, long positionUs) {
+    Assertions.checkState(prepared);
+    Assertions.checkState(!trackEnabledStates[track]);
+    enabledTrackCount++;
+    trackEnabledStates[track] = true;
+    downstreamMediaFormats[track] = null;
+    if (enabledTrackCount == 1) {
+      seekToUs(positionUs);
+    }
+  }
+
+  @Override
+  public void disable(int track) {
+    Assertions.checkState(prepared);
+    Assertions.checkState(trackEnabledStates[track]);
+    enabledTrackCount--;
+    trackEnabledStates[track] = false;
+    pendingDiscontinuities[track] = false;
+    if (enabledTrackCount == 0) {
+      if (loader.isLoading()) {
+        loader.cancelLoading();
+      } else {
+        clearState();
+      }
+    }
+  }
+
+  @Override
+  public boolean continueBuffering(long playbackPositionUs) throws IOException {
+    Assertions.checkState(prepared);
+    Assertions.checkState(enabledTrackCount > 0);
+    downstreamPositionUs = playbackPositionUs;
+    if (!extractors.isEmpty()) {
+      discardSamplesForDisabledTracks(extractors.getFirst(), downstreamPositionUs);
+    }
+    return loadingFinished || continueBufferingInternal();
+  }
+
+  private boolean continueBufferingInternal() throws IOException {
+    maybeStartLoading();
+    if (isPendingReset() || extractors.isEmpty()) {
+      return false;
+    }
+    boolean haveSamples = prepared && haveSamplesForEnabledTracks(getCurrentExtractor());
+    if (!haveSamples) {
+      maybeThrowLoadableException();
+    }
+    return haveSamples;
+  }
+
+  @Override
+  public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder,
+      SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException {
+    Assertions.checkState(prepared);
+    downstreamPositionUs = playbackPositionUs;
+
+    if (pendingDiscontinuities[track]) {
+      pendingDiscontinuities[track] = false;
+      return DISCONTINUITY_READ;
+    }
+
+    if (onlyReadDiscontinuity || isPendingReset() || extractors.isEmpty()) {
+      maybeThrowLoadableException();
+      return NOTHING_READ;
+    }
+
+    HlsExtractor extractor = getCurrentExtractor();
+    if (extractors.size() > 1) {
+      // If there's more than one extractor, attempt to configure a seamless splice from the
+      // current one to the next one.
+      extractor.configureSpliceTo(extractors.get(1));
+    }
+
+    int extractorIndex = 0;
+    while (extractors.size() > extractorIndex + 1 && !extractor.hasSamples(track)) {
+      // We're finished reading from the extractor for this particular track, so advance to the
+      // next one for the current read.
+      extractor = extractors.get(++extractorIndex);
+    }
+
+    if (!extractor.isPrepared()) {
+      maybeThrowLoadableException();
+      return NOTHING_READ;
+    }
+
+    MediaFormat mediaFormat = extractor.getFormat(track);
+    if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormats[track], true)) {
+      chunkSource.getMaxVideoDimensions(mediaFormat);
+      formatHolder.format = mediaFormat;
+      downstreamMediaFormats[track] = mediaFormat;
+      return FORMAT_READ;
+    }
+
+    if (extractor.getSample(track, sampleHolder)) {
+      sampleHolder.decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs;
+      return SAMPLE_READ;
+    }
+
+    if (loadingFinished) {
+      return END_OF_STREAM;
+    }
+
+    maybeThrowLoadableException();
+    return NOTHING_READ;
+  }
+
+  @Override
+  public void seekToUs(long positionUs) {
+    Assertions.checkState(prepared);
+    Assertions.checkState(enabledTrackCount > 0);
+    lastSeekPositionUs = positionUs;
+    if (pendingResetPositionUs == positionUs || downstreamPositionUs == positionUs) {
+      downstreamPositionUs = positionUs;
+      return;
+    }
+    downstreamPositionUs = positionUs;
+    for (int i = 0; i < pendingDiscontinuities.length; i++) {
+      pendingDiscontinuities[i] = true;
+    }
+    restartFrom(positionUs);
+  }
+
+  @Override
+  public long getBufferedPositionUs() {
+    Assertions.checkState(prepared);
+    Assertions.checkState(enabledTrackCount > 0);
+    if (isPendingReset()) {
+      return pendingResetPositionUs;
+    } else if (loadingFinished) {
+      return TrackRenderer.END_OF_TRACK_US;
+    } else {
+      long largestSampleTimestamp = extractors.getLast().getLargestSampleTimestamp();
+      return largestSampleTimestamp == Long.MIN_VALUE ? downstreamPositionUs
+          : largestSampleTimestamp;
+    }
+  }
+
+  @Override
+  public void release() {
+    Assertions.checkState(remainingReleaseCount > 0);
+    if (--remainingReleaseCount == 0 && loader != null) {
+      loader.release();
+      loader = null;
+    }
+  }
+
+  @Override
+  public void onLoadCompleted(Loadable loadable) {
+    try {
+      currentLoadable.consume();
+    } catch (IOException e) {
+      currentLoadableException = e;
+      currentLoadableExceptionCount++;
+      currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
+      currentLoadableExceptionFatal = true;
+    } finally {
+      if (isTsChunk(currentLoadable)) {
+        TsChunk tsChunk = (TsChunk) loadable;
+        loadingFinished = tsChunk.isLastChunk;
+      }
+      if (!currentLoadableExceptionFatal) {
+        clearCurrentLoadable();
+      }
+      maybeStartLoading();
+    }
+  }
+
+  @Override
+  public void onLoadCanceled(Loadable loadable) {
+    if (enabledTrackCount > 0) {
+      restartFrom(pendingResetPositionUs);
+    } else {
+      clearState();
+    }
+  }
+
+  @Override
+  public void onLoadError(Loadable loadable, IOException e) {
+    if (chunkSource.onLoadError(currentLoadable, e)) {
+      // Error handled by source.
+      clearCurrentLoadable();
+    } else {
+      currentLoadableException = e;
+      currentLoadableExceptionCount++;
+      currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime();
+    }
+    maybeStartLoading();
+  }
+
+  /**
+   * Gets the current extractor from which samples should be read.
+   * <p>
+   * Calling this method discards extractors without any samples from the front of the queue. The
+   * last extractor is retained even if it doesn't have any samples.
+   * <p>
+   * This method must not be called unless {@link #extractors} is non-empty.
+   *
+   * @return The current extractor from which samples should be read. Guaranteed to be non-null.
+   */
+  private HlsExtractor getCurrentExtractor() {
+    HlsExtractor extractor = extractors.getFirst();
+    while (extractors.size() > 1 && !haveSamplesForEnabledTracks(extractor)) {
+      // We're finished reading from the extractor for all tracks, and so can discard it.
+      extractors.removeFirst().release();
+      extractor = extractors.getFirst();
+    }
+    return extractor;
+  }
+
+  private void discardSamplesForDisabledTracks(HlsExtractor extractor, long timeUs) {
+    if (!extractor.isPrepared()) {
+      return;
+    }
+    for (int i = 0; i < trackEnabledStates.length; i++) {
+      if (!trackEnabledStates[i]) {
+        extractor.discardUntil(i, timeUs);
+      }
+    }
+  }
+
+  private boolean haveSamplesForEnabledTracks(HlsExtractor extractor) {
+    if (!extractor.isPrepared()) {
+      return false;
+    }
+    for (int i = 0; i < trackEnabledStates.length; i++) {
+      if (trackEnabledStates[i] && extractor.hasSamples(i)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private void maybeThrowLoadableException() throws IOException {
+    if (currentLoadableException != null && (currentLoadableExceptionFatal
+        || currentLoadableExceptionCount > minLoadableRetryCount)) {
+      throw currentLoadableException;
+    }
+  }
+
+  private void restartFrom(long positionUs) {
+    pendingResetPositionUs = positionUs;
+    loadingFinished = false;
+    if (loader.isLoading()) {
+      loader.cancelLoading();
+    } else {
+      clearState();
+      maybeStartLoading();
+    }
+  }
+
+  private void clearState() {
+    for (int i = 0; i < extractors.size(); i++) {
+      extractors.get(i).release();
+    }
+    extractors.clear();
+    clearCurrentLoadable();
+    previousTsLoadable = null;
+  }
+
+  private void clearCurrentLoadable() {
+    currentLoadable = null;
+    currentLoadableException = null;
+    currentLoadableExceptionCount = 0;
+    currentLoadableExceptionFatal = false;
+  }
+
+  private void maybeStartLoading() {
+    if (currentLoadableExceptionFatal || loadingFinished || loader.isLoading()) {
+      return;
+    }
+
+    boolean isBackedOff = currentLoadableException != null;
+    if (isBackedOff) {
+      long elapsedMillis = SystemClock.elapsedRealtime() - currentLoadableExceptionTimestamp;
+      if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) {
+        currentLoadableException = null;
+        loader.startLoading(currentLoadable, this);
+      }
+      return;
+    }
+
+    HlsChunk nextLoadable = chunkSource.getChunkOperation(previousTsLoadable,
+        pendingResetPositionUs, downstreamPositionUs);
+    if (nextLoadable == null) {
+      return;
+    }
+
+    currentLoadable = nextLoadable;
+    if (isTsChunk(currentLoadable)) {
+      previousTsLoadable = (TsChunk) currentLoadable;
+      if (isPendingReset()) {
+        pendingResetPositionUs = NO_RESET_PENDING;
+      }
+      if (extractors.isEmpty() || extractors.getLast() != previousTsLoadable.extractor) {
+        extractors.addLast(previousTsLoadable.extractor);
+      }
+    }
+    loader.startLoading(currentLoadable, this);
+  }
+
+  private boolean isTsChunk(HlsChunk chunk) {
+    return chunk instanceof TsChunk;
+  }
+
+  private boolean isPendingReset() {
+    return pendingResetPositionUs != NO_RESET_PENDING;
+  }
+
+  private long getRetryDelayMillis(long errorCount) {
+    return Math.min((errorCount - 1) * 1000, 5000);
+  }
+
+  protected final int usToMs(long timeUs) {
+    return (int) (timeUs / 1000);
+  }
+
+}

+ 134 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/TsChunk.java

@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.hls;
+
+import com.google.android.exoplayer.hls.parser.HlsExtractor;
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.upstream.DataSpec;
+
+import java.io.IOException;
+
+/**
+ * A MPEG2TS chunk.
+ */
+public final class TsChunk extends HlsChunk {
+
+  private static final byte[] SCRATCH_SPACE = new byte[4096];
+
+  /**
+   * The index of the variant in the master playlist.
+   */
+  public final int variantIndex;
+  /**
+   * The start time of the media contained by the chunk.
+   */
+  public final long startTimeUs;
+  /**
+   * The end time of the media contained by the chunk.
+   */
+  public final long endTimeUs;
+  /**
+   * The chunk index.
+   */
+  public final int chunkIndex;
+  /**
+   * True if this is the last chunk in the media. False otherwise.
+   */
+  public final boolean isLastChunk;
+  /**
+   * The extractor into which this chunk is being consumed.
+   */
+  public final HlsExtractor extractor;
+
+  private int loadPosition;
+  private volatile boolean loadFinished;
+  private volatile boolean loadCanceled;
+
+  /**
+   * @param dataSource A {@link DataSource} for loading the data.
+   * @param dataSpec Defines the data to be loaded.
+   * @param extractor An extractor to parse samples from the data.
+   * @param variantIndex The index of the variant in the master playlist.
+   * @param startTimeUs The start time of the media contained by the chunk, in microseconds.
+   * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
+   * @param chunkIndex The index of the chunk.
+   * @param isLastChunk True if this is the last chunk in the media. False otherwise.
+   */
+  public TsChunk(DataSource dataSource, DataSpec dataSpec, HlsExtractor extractor,
+      int variantIndex, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk) {
+    super(dataSource, dataSpec);
+    this.extractor = extractor;
+    this.variantIndex = variantIndex;
+    this.startTimeUs = startTimeUs;
+    this.endTimeUs = endTimeUs;
+    this.chunkIndex = chunkIndex;
+    this.isLastChunk = isLastChunk;
+  }
+
+  @Override
+  public void consume() throws IOException {
+    // Do nothing.
+  }
+
+  @Override
+  public boolean isLoadFinished() {
+    return loadFinished;
+  }
+
+  // Loadable implementation
+
+  @Override
+  public void cancelLoad() {
+    loadCanceled = true;
+  }
+
+  @Override
+  public boolean isLoadCanceled() {
+    return loadCanceled;
+  }
+
+  @Override
+  public void load() throws IOException, InterruptedException {
+    try {
+      dataSource.open(dataSpec);
+      int bytesRead = 0;
+      int bytesSkipped = 0;
+      // If we previously fed part of this chunk to the extractor, skip it this time.
+      // TODO: Ideally we'd construct a dataSpec that only loads the remainder of the data here,
+      // rather than loading the whole chunk again and then skipping data we previously loaded. To
+      // do this is straightforward for non-encrypted content, but more complicated for content
+      // encrypted with AES, for which we'll need to modify the way that decryption is performed.
+      while (bytesRead != -1 && !loadCanceled && bytesSkipped < loadPosition) {
+        int skipLength = Math.min(loadPosition - bytesSkipped, SCRATCH_SPACE.length);
+        bytesRead = dataSource.read(SCRATCH_SPACE, 0, skipLength);
+        if (bytesRead != -1) {
+          bytesSkipped += bytesRead;
+        }
+      }
+      // Feed the remaining data into the extractor.
+      while (bytesRead != -1 && !loadCanceled) {
+        bytesRead = extractor.read(dataSource);
+        if (bytesRead != -1) {
+          loadPosition += bytesRead;
+        }
+      }
+      loadFinished = !loadCanceled;
+    } finally {
+      dataSource.close();
+    }
+  }
+
+}

+ 56 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/Variant.java

@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.hls;
+
+import java.util.Comparator;
+
+/**
+ * Variant stream reference.
+ */
+public final class Variant {
+
+  /**
+   * Sorts {@link Variant} objects in order of decreasing bandwidth.
+   * <p>
+   * When two {@link Variant}s have the same bandwidth, the one with the lowest index comes first.
+   */
+  public static final class DecreasingBandwidthComparator implements Comparator<Variant> {
+
+    @Override
+    public int compare(Variant a, Variant b) {
+      int bandwidthDifference = b.bandwidth - a.bandwidth;
+      return bandwidthDifference != 0 ? bandwidthDifference : a.index - b.index;
+    }
+
+  }
+
+  public final int index;
+  public final int bandwidth;
+  public final String url;
+  public final String[] codecs;
+  public final int width;
+  public final int height;
+
+  public Variant(int index, String url, int bandwidth, String[] codecs, int width, int height) {
+    this.index = index;
+    this.bandwidth = bandwidth;
+    this.url = url;
+    this.codecs = codecs;
+    this.width = width;
+    this.height = height;
+  }
+
+}

+ 126 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/parser/AdtsExtractor.java

@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.hls.parser;
+
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.SampleHolder;
+import com.google.android.exoplayer.upstream.BufferPool;
+import com.google.android.exoplayer.upstream.DataSource;
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.ParsableByteArray;
+
+import java.io.IOException;
+
+/**
+ * Facilitates the extraction of AAC samples from elementary audio files formatted as AAC with ADTS
+ * headers.
+ */
+public class AdtsExtractor extends HlsExtractor {
+
+  private static final int MAX_PACKET_SIZE = 200;
+
+  private final long firstSampleTimestamp;
+  private final ParsableByteArray packetBuffer;
+  private final AdtsReader adtsReader;
+
+  // Accessed only by the loading thread.
+  private boolean firstPacket;
+  // Accessed by both the loading and consuming threads.
+  private volatile boolean prepared;
+
+  public AdtsExtractor(boolean shouldSpliceIn, long firstSampleTimestamp, BufferPool bufferPool) {
+    super(shouldSpliceIn);
+    this.firstSampleTimestamp = firstSampleTimestamp;
+    packetBuffer = new ParsableByteArray(MAX_PACKET_SIZE);
+    adtsReader = new AdtsReader(bufferPool);
+    firstPacket = true;
+  }
+
+  @Override
+  public int getTrackCount() {
+    Assertions.checkState(prepared);
+    return 1;
+  }
+
+  @Override
+  public MediaFormat getFormat(int track) {
+    Assertions.checkState(prepared);
+    return adtsReader.getMediaFormat();
+  }
+
+  @Override
+  public boolean isPrepared() {
+    return prepared;
+  }
+
+  @Override
+  public void release() {
+    adtsReader.release();
+  }
+
+  @Override
+  public long getLargestSampleTimestamp() {
+    return adtsReader.getLargestParsedTimestampUs();
+  }
+
+  @Override
+  public boolean getSample(int track, SampleHolder holder) {
+    Assertions.checkState(prepared);
+    Assertions.checkState(track == 0);
+    return adtsReader.getSample(holder);
+  }
+
+  @Override
+  public void discardUntil(int track, long timeUs) {
+    Assertions.checkState(prepared);
+    Assertions.checkState(track == 0);
+    adtsReader.discardUntil(timeUs);
+  }
+
+  @Override
+  public boolean hasSamples(int track) {
+    Assertions.checkState(prepared);
+    Assertions.checkState(track == 0);
+    return !adtsReader.isEmpty();
+  }
+
+  @Override
+  public int read(DataSource dataSource) throws IOException {
+    int bytesRead = dataSource.read(packetBuffer.data, 0, MAX_PACKET_SIZE);
+    if (bytesRead == -1) {
+      return -1;
+    }
+
+    packetBuffer.setPosition(0);
+    packetBuffer.setLimit(bytesRead);
+
+    // TODO: Make it possible for adtsReader to consume the dataSource directly, so that it becomes
+    // unnecessary to copy the data through packetBuffer.
+    adtsReader.consume(packetBuffer, firstSampleTimestamp, firstPacket);
+    firstPacket = false;
+    if (!prepared) {
+      prepared = adtsReader.hasMediaFormat();
+    }
+    return bytesRead;
+  }
+
+  @Override
+  protected SampleQueue getSampleQueue(int track) {
+    Assertions.checkState(track == 0);
+    return adtsReader;
+  }
+
+}

+ 182 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/parser/AdtsReader.java

@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.hls.parser;
+
+import com.google.android.exoplayer.C;
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.upstream.BufferPool;
+import com.google.android.exoplayer.util.CodecSpecificDataUtil;
+import com.google.android.exoplayer.util.MimeTypes;
+import com.google.android.exoplayer.util.ParsableBitArray;
+import com.google.android.exoplayer.util.ParsableByteArray;
+
+import android.util.Pair;
+
+import java.util.Collections;
+
+/**
+ * Parses a continuous ADTS byte stream and extracts individual frames.
+ */
+/* package */ class AdtsReader extends ElementaryStreamReader {
+
+  private static final int STATE_FINDING_SYNC = 0;
+  private static final int STATE_READING_HEADER = 1;
+  private static final int STATE_READING_SAMPLE = 2;
+
+  private static final int HEADER_SIZE = 5;
+  private static final int CRC_SIZE = 2;
+
+  private final ParsableBitArray adtsScratch;
+
+  private int state;
+  private int bytesRead;
+
+  // Used to find the header.
+  private boolean lastByteWasFF;
+  private boolean hasCrc;
+
+  // Parsed from the header.
+  private long frameDurationUs;
+  private int sampleSize;
+
+  // Used when reading the samples.
+  private long timeUs;
+
+  public AdtsReader(BufferPool bufferPool) {
+    super(bufferPool);
+    adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]);
+    state = STATE_FINDING_SYNC;
+  }
+
+  @Override
+  public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) {
+    if (startOfPacket) {
+      timeUs = pesTimeUs;
+    }
+    while (data.bytesLeft() > 0) {
+      switch (state) {
+        case STATE_FINDING_SYNC:
+          if (skipToNextSync(data)) {
+            bytesRead = 0;
+            state = STATE_READING_HEADER;
+          }
+          break;
+        case STATE_READING_HEADER:
+          int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE;
+          if (continueRead(data, adtsScratch.getData(), targetLength)) {
+            parseHeader();
+            startSample(timeUs);
+            bytesRead = 0;
+            state = STATE_READING_SAMPLE;
+          }
+          break;
+        case STATE_READING_SAMPLE:
+          int bytesToRead = Math.min(data.bytesLeft(), sampleSize - bytesRead);
+          appendData(data, bytesToRead);
+          bytesRead += bytesToRead;
+          if (bytesRead == sampleSize) {
+            commitSample(true);
+            timeUs += frameDurationUs;
+            bytesRead = 0;
+            state = STATE_FINDING_SYNC;
+          }
+          break;
+      }
+    }
+  }
+
+  @Override
+  public void packetFinished() {
+    // Do nothing.
+  }
+
+  /**
+   * Continues a read from the provided {@code source} into a given {@code target}. It's assumed
+   * that the data should be written into {@code target} starting from an offset of zero.
+   *
+   * @param source The source from which to read.
+   * @param target The target into which data is to be read.
+   * @param targetLength The target length of the read.
+   * @return Whether the target length was reached.
+   */
+  private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) {
+    int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead);
+    source.readBytes(target, bytesRead, bytesToRead);
+    bytesRead += bytesToRead;
+    return bytesRead == targetLength;
+  }
+
+  /**
+   * Locates the next sync word, advancing the position to the byte that immediately follows it.
+   * If a sync word was not located, the position is advanced to the limit.
+   *
+   * @param pesBuffer The buffer whose position should be advanced.
+   * @return True if a sync word position was found. False otherwise.
+   */
+  private boolean skipToNextSync(ParsableByteArray pesBuffer) {
+    byte[] adtsData = pesBuffer.data;
+    int startOffset = pesBuffer.getPosition();
+    int endOffset = pesBuffer.limit();
+    for (int i = startOffset; i < endOffset; i++) {
+      boolean byteIsFF = (adtsData[i] & 0xFF) == 0xFF;
+      boolean found = lastByteWasFF && !byteIsFF && (adtsData[i] & 0xF0) == 0xF0;
+      lastByteWasFF = byteIsFF;
+      if (found) {
+        hasCrc = (adtsData[i] & 0x1) == 0;
+        pesBuffer.setPosition(i + 1);
+        // Reset lastByteWasFF for next time.
+        lastByteWasFF = false;
+        return true;
+      }
+    }
+    pesBuffer.setPosition(endOffset);
+    return false;
+  }
+
+  /**
+   * Parses the sample header.
+   */
+  private void parseHeader() {
+    adtsScratch.setPosition(0);
+
+    if (!hasMediaFormat()) {
+      int audioObjectType = adtsScratch.readBits(2) + 1;
+      int sampleRateIndex = adtsScratch.readBits(4);
+      adtsScratch.skipBits(1);
+      int channelConfig = adtsScratch.readBits(3);
+
+      byte[] audioSpecificConfig = CodecSpecificDataUtil.buildAudioSpecificConfig(
+          audioObjectType, sampleRateIndex, channelConfig);
+      Pair<Integer, Integer> audioParams = CodecSpecificDataUtil.parseAudioSpecificConfig(
+          audioSpecificConfig);
+
+      MediaFormat mediaFormat = MediaFormat.createAudioFormat(MimeTypes.AUDIO_AAC,
+          MediaFormat.NO_VALUE, audioParams.second, audioParams.first,
+          Collections.singletonList(audioSpecificConfig));
+      frameDurationUs = (C.MICROS_PER_SECOND * 1024L) / mediaFormat.sampleRate;
+      setMediaFormat(mediaFormat);
+    } else {
+      adtsScratch.skipBits(10);
+    }
+
+    adtsScratch.skipBits(4);
+    sampleSize = adtsScratch.readBits(13) - 2 /* the sync word */ - HEADER_SIZE;
+    if (hasCrc) {
+      sampleSize -= CRC_SIZE;
+    }
+  }
+
+}

+ 47 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/parser/ElementaryStreamReader.java

@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.hls.parser;
+
+import com.google.android.exoplayer.upstream.BufferPool;
+import com.google.android.exoplayer.util.ParsableByteArray;
+
+/**
+ * Extracts individual samples from an elementary media stream, preserving original order.
+ */
+/* package */ abstract class ElementaryStreamReader extends SampleQueue {
+
+  protected ElementaryStreamReader(BufferPool bufferPool) {
+    super(bufferPool);
+  }
+
+  /**
+   * Consumes (possibly partial) payload data.
+   *
+   * @param data The payload data to consume.
+   * @param pesTimeUs The timestamp associated with the payload.
+   * @param startOfPacket True if this is the first time this method is being called for the
+   *     current packet. False otherwise.
+   */
+  public abstract void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket);
+
+  /**
+   * Invoked once all of the payload data for a packet has been passed to
+   * {@link #consume(ParsableByteArray, long, boolean)}. The next call to
+   * {@link #consume(ParsableByteArray, long, boolean)} will have {@code startOfPacket == true}.
+   */
+  public abstract void packetFinished();
+
+}

+ 378 - 0
IZPL-A/IZPL/exoplayer/src/main/java/com/google/android/exoplayer/hls/parser/H264Reader.java

@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2014 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.google.android.exoplayer.hls.parser;
+
+import com.google.android.exoplayer.MediaFormat;
+import com.google.android.exoplayer.mp4.Mp4Util;
+import com.google.android.exoplayer.upstream.BufferPool;
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.MimeTypes;
+import com.google.android.exoplayer.util.ParsableBitArray;
+import com.google.android.exoplayer.util.ParsableByteArray;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Parses a continuous H264 byte stream and extracts individual frames.
+ */
+/* package */ class H264Reader extends ElementaryStreamReader {
+
+  private static final int NAL_UNIT_TYPE_IDR = 5;
+  private static final int NAL_UNIT_TYPE_SEI = 6;
+  private static final int NAL_UNIT_TYPE_SPS = 7;
+  private static final int NAL_UNIT_TYPE_PPS = 8;
+  private static final int NAL_UNIT_TYPE_AUD = 9;
+
+  private final SeiReader seiReader;
+  private final boolean[] prefixFlags;
+  private final NalUnitTargetBuffer sps;
+  private final NalUnitTargetBuffer pps;
+  private final NalUnitTargetBuffer sei;
+
+  private int scratchEscapeCount;
+  private int[] scratchEscapePositions;
+  private boolean isKeyframe;
+
+  public H264Reader(BufferPool bufferPool, SeiReader seiReader) {
+    super(bufferPool);
+    this.seiReader = seiReader;
+    prefixFlags = new boolean[3];
+    sps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SPS, 128);
+    pps = new NalUnitTargetBuffer(NAL_UNIT_TYPE_PPS, 128);
+    sei = new NalUnitTargetBuffer(NAL_UNIT_TYPE_SEI, 128);
+    scratchEscapePositions = new int[10];
+  }
+
+  @Override
+  public void consume(ParsableByteArray data, long pesTimeUs, boolean startOfPacket) {
+    while (data.bytesLeft() > 0) {
+      int offset = data.getPosition();
+      int limit = data.limit();
+      byte[] dataArray = data.data;
+
+      // Append the data to the buffer.
+      appendData(data, data.bytesLeft());
+
+      // Scan the appended data, processing NAL units as they are encountered
+      while (offset < limit) {
+        int nextNalUnitOffset = Mp4Util.findNalUnit(dataArray, offset, limit, prefixFlags);
+        if (nextNalUnitOffset < limit) {
+          // We've seen the start of a NAL unit.
+
+          // This is the length to the start of the unit. It may be negative if the NAL unit
+          // actually started in previously consumed data.
+          int lengthToNalUnit = nextNalUnitOffset - offset;
+          if (lengthToNalUnit > 0) {
+            feedNalUnitTargetBuffersData(dataArray, offset, nextNalUnitOffset);
+          }
+
+          int nalUnitType = Mp4Util.getNalUnitType(dataArray, nextNalUnitOffset);
+          int nalUnitOffsetInData = nextNalUnitOffset - limit;
+          if (nalUnitType == NAL_UNIT_TYPE_AUD) {
+            if (writingSample()) {
+              if (isKeyframe && !hasMediaFormat() && sps.isCompleted() && pps.isCompleted()) {
+                parseMediaFormat(sps, pps);
+              }
+              commitSample(isKeyframe, nalUnitOffsetInData);
+            }
+            startSample(pesTimeUs, nalUnitOffsetInData);
+            isKeyframe = false;
+          } else if (nalUnitType == NAL_UNIT_TYPE_IDR) {
+            isKeyframe = true;
+          }
+
+          // If the length to the start of the unit is negative then we wrote too many bytes to the
+          // NAL buffers. Discard the excess bytes when notifying that the unit has ended.
+          feedNalUnitTargetEnd(pesTimeUs, lengthToNalUnit < 0 ? -lengthToNalUnit : 0);
+          // Notify the start of the next NAL unit.
+          feedNalUnitTargetBuffersStart(nalUnitType);
+          // Continue scanning the data.
+          offset = nextNalUnitOffset + 4;
+        } else {
+          feedNalUnitTargetBuffersData(dataArray, offset, limit);
+          offset = limit;
+        }
+      }
+    }
+  }
+
+  @Override
+  public void packetFinished() {
+    // Do nothing.
+  }
+
+  private void feedNalUnitTargetBuffersStart(int nalUnitType) {
+    if (!hasMediaFormat()) {
+      sps.startNalUnit(nalUnitType);
+      pps.startNalUnit(nalUnitType);
+    }
+    sei.startNalUnit(nalUnitType);
+  }
+
+  private void feedNalUnitTargetBuffersData(byte[] dataArray, int offset, int limit) {
+    if (!hasMediaFormat()) {
+      sps.appendToNalUnit(dataArray, offset, limit);
+      pps.appendToNalUnit(dataArray, offset, limit);
+    }
+    sei.appendToNalUnit(dataArray, offset, limit);
+  }
+
+  private void feedNalUnitTargetEnd(long pesTimeUs, int discardPadding) {
+    sps.endNalUnit(discardPadding);
+    pps.endNalUnit(discardPadding);
+    if (sei.endNalUnit(discardPadding)) {
+      int unescapedLength = unescapeStream(sei.nalData, sei.nalLength);
+      seiReader.read(sei.nalData, 0, unescapedLength, pesTimeUs);
+    }
+  }
+
+  private void parseMediaFormat(NalUnitTargetBuffer sps, NalUnitTargetBuffer pps) {
+    byte[] spsData = new byte[sps.nalLength];
+    byte[] ppsData = new byte[pps.nalLength];
+    System.arraycopy(sps.nalData, 0, spsData, 0, sps.nalLength);
+    System.arraycopy(pps.nalData, 0, ppsData, 0, pps.nalLength);
+    List<byte[]> initializationData = new ArrayList<byte[]>();
+    initializationData.add(spsData);
+    initializationData.add(ppsData);
+
+    // Unescape and then parse the SPS unit.
+    unescapeStream(sps.nalData, sps.nalLength);
+    ParsableBitArray bitArray = new ParsableBitArray(sps.nalData);
+    bitArray.skipBits(32); // NAL header
+    int profileIdc = bitArray.readBits(8);
+    bitArray.skipBits(16); // constraint bits (6), reserved (2) and level_idc (8)
+    bitArray.readUnsignedExpGolombCodedInt(); // seq_parameter_set_id
+
+    int chromaFormatIdc = 1; // Default is 4:2:0
+    if (profileIdc == 100 || profileIdc == 110 || profileIdc == 122 || profileIdc == 244
+        || profileIdc == 44 || profileIdc == 83 || profileIdc == 86 || profileIdc == 118
+        || profileIdc == 128 || profileIdc == 138) {
+      chromaFormatIdc = bitArray.readUnsignedExpGolombCodedInt();
+      if (chromaFormatIdc == 3) {
+        bitArray.skipBits(1); // separate_colour_plane_flag
+      }
+      bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_luma_minus8
+      bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_chroma_minus8
+      bitArray.skipBits(1); // qpprime_y_zero_transform_bypass_flag
+      boolean seqScalingMatrixPresentFlag = bitArray.readBit();
+      if (seqScalingMatrixPresentFlag) {
+        int limit = (chromaFormatIdc != 3) ? 8 : 12;
+        for (int i = 0; i < limit; i++) {
+          boolean seqScalingListPresentFlag = bitArray.readBit();
+          if (seqScalingListPresentFlag) {
+            skipScalingList(bitArray, i < 6 ? 16 : 64);
+          }
+        }
+      }
+    }
+
+    bitArray.readUnsignedExpGolombCodedInt(); // log2_max_frame_num_minus4
+    long picOrderCntType = bitArray.readUnsignedExpGolombCodedInt();
+    if (picOrderCntType == 0) {
+      bitArray.readUnsignedExpGolombCodedInt(); // log2_max_pic_order_cnt_lsb_minus4
+    } else if (picOrderCntType == 1) {
+      bitArray.skipBits(1); // delta_pic_order_always_zero_flag
+      bitArray.readSignedExpGolombCodedInt(); // offset_for_non_ref_pic
+      bitArray.readSignedExpGolombCodedInt(); // offset_for_top_to_bottom_field
+      long numRefFramesInPicOrderCntCycle = bitArray.readUnsignedExpGolombCodedInt();
+      for (int i = 0; i < numRefFramesInPicOrderCntCycle; i++) {
+        bitArray.readUnsignedExpGolombCodedInt(); // offset_for_ref_frame[i]
+      }
+    }
+    bitArray.readUnsignedExpGolombCodedInt(); // max_num_ref_frames
+    bitArray.skipBits(1); // gaps_in_frame_num_value_allowed_flag
+
+    int picWidthInMbs = bitArray.readUnsignedExpGolombCodedInt() + 1;
+    int picHeightInMapUnits = bitArray.readUnsignedExpGolombCodedInt() + 1;
+    boolean frameMbsOnlyFlag = bitArray.readBit();
+    int frameHeightInMbs = (2 - (frameMbsOnlyFlag ? 1 : 0)) * picHeightInMapUnits;
+    if (!frameMbsOnlyFlag) {
+      bitArray.skipBits(1); // mb_adaptive_frame_field_flag
+    }
+
+    bitArray.skipBits(1); // direct_8x8_inference_flag
+    int frameWidth = picWidthInMbs * 16;
+    int frameHeight = frameHeightInMbs * 16;
+    boolean frameCroppingFlag = bitArray.readBit();
+    if (frameCroppingFlag) {
+      int frameCropLeftOffset = bitArray.readUnsignedExpGolombCodedInt();
+      int frameCropRightOffset = bitArray.readUnsignedExpGolombCodedInt();
+      int frameCropTopOffset = bitArray.readUnsignedExpGolombCodedInt();
+      int frameCropBottomOffset = bitArray.readUnsignedExpGolombCodedInt();
+      int cropUnitX, cropUnitY;
+      if (chromaFormatIdc == 0) {
+        cropUnitX = 1;
+        cropUnitY = 2 - (frameMbsOnlyFlag ? 1 : 0);
+      } else {
+        int subWidthC = (chromaFormatIdc == 3) ? 1 : 2;
+        int subHeightC = (chromaFormatIdc == 1) ? 2 : 1;
+        cropUnitX = subWidthC;
+        cropUnitY = subHeightC * (2 - (frameMbsOnlyFlag ? 1 : 0));
+      }
+      frameWidth -= (frameCropLeftOffset + frameCropRightOffset) * cropUnitX;
+      frameHeight -= (frameCropTopOffset + frameCropBottomOffset) * cropUnitY;
+    }
+
+    // Set the format.
+    setMediaFormat(MediaFormat.createVideoFormat(MimeTypes.VIDEO_H264, MediaFormat.NO_VALUE,
+        frameWidth, frameHeight, initializationData));
+  }
+
+  private void skipScalingList(ParsableBitArray bitArray, int size) {
+    int lastScale = 8;
+    int nextScale = 8;
+    for (int i = 0; i < size; i++) {
+      if (nextScale != 0) {
+        int deltaScale = bitArray.readSignedExpGolombCodedInt();
+        nextScale = (lastScale + deltaScale + 256) % 256;
+      }
+      lastScale = (nextScale == 0) ? lastScale : nextScale;
+    }
+  }
+
+  /**
+   * Unescapes {@code data} up to the specified limit, replacing occurrences of [0, 0, 3] with
+   * [0, 0]. The unescaped data is returned in-place, with the return value indicating its length.
+   * <p>
+   * See ISO/IEC 14496-10:2005(E) page 36 for more information.
+   *
+   * @param data The data to unescape.
+   * @param limit The limit (exclusive) of the data to unescape.
+   * @return The length of the unescaped data.
+   */
+  private int unescapeStream(byte[] data, int limit) {
+    int position = 0;
+    scratchEscapeCount = 0;
+    while (position < limit) {
+      position = findNextUnescapeIndex(data, position, limit);
+      if (position < limit) {
+        if (scratchEscapePositions.length <= scratchEscapeCount) {
+          // Grow scratchEscapePositions to hold a larger number of positions.
+          scratchEscapePositions = Arrays.copyOf(scratchEscapePositions,
+              scratchEscapePositions.length * 2);
+        }
+        scratchEscapePositions[scratchEscapeCount++] = position;
+        position += 3;
+      }
+    }
+
+    int unescapedLength = limit - scratchEscapeCount;
+    int escapedPosition = 0; // The position being read from.
+    int unescapedPosition = 0; // The position being written to.
+    for (int i = 0; i < scratchEscapeCount; i++) {
+      int nextEscapePosition = scratchEscapePositions[i];
+      int copyLength = nextEscapePosition - escapedPosition;
+      System.arraycopy(data, escapedPosition, data, unescapedPosition, copyLength);
+      escapedPosition += copyLength + 3;
+      unescapedPosition += copyLength + 2;
+    }
+
+    int remainingLength = unescapedLength - unescapedPosition;
+    System.arraycopy(data, escapedPosition, data, unescapedPosition, remainingLength);
+    return unescapedLength;
+  }
+
+  private int findNextUnescapeIndex(byte[] bytes, int offset, int limit) {
+    for (int i = offset; i < limit - 2; i++) {
+      if (bytes[i] == 0x00 && bytes[i + 1] == 0x00 && bytes[i + 2] == 0x03) {
+        return i;
+      }
+    }
+    return limit;
+  }
+
+  /**
+   * A buffer that fills itself with data corresponding to a specific NAL unit, as it is
+   * encountered in the stream.
+   */
+  private static final class NalUnitTargetBuffer {
+
+    private final int targetType;
+
+    private boolean isFilling;
+    private boolean isCompleted;
+
+    public byte[] nalData;
+    public int nalLength;
+
+    public NalUnitTargetBuffer(int targetType, int initialCapacity) {
+      this.targetType = targetType;
+      // Initialize data, writing the known NAL prefix into the first four bytes.
+      nalData = new byte[4 + initialCapacity];
+      nalData[2] = 1;
+      nalData[3] = (byte) targetType;
+    }
+
+    public boolean isCompleted() {
+      return isCompleted;
+    }
+
+    /**
+     * Invoked to indicate that a NAL unit has started.
+     *
+     * @param type The type of the NAL unit.
+     */
+    public void startNalUnit(int type) {
+      Assertions.checkState(!isFilling);
+      isFilling = type == targetType;
+      if (isFilling) {
+        // Length is initially the length of the NAL prefix.
+        nalLength = 4;
+        isCompleted = false;
+      }
+    }
+
+    /**
+     * Invoked to pass stream data. The data passed should not include 4 byte NAL unit prefixes.
+     *
+     * @param data Holds the data being passed.
+     * @param offset The offset of the data in {@code data}.
+     * @param limit The limit (exclusive) of the data in {@code data}.
+     */
+    public void appendToNalUnit(byte[] data, int offset, int limit) {
+      if (!isFilling) {
+        return;
+      }
+      int readLength = limit - offset;
+      if (nalData.length < nalLength + readLength) {
+        nalData = Arrays.copyOf(nalData, (nalLength + readLength) * 2);
+      }
+      System.arraycopy(data, offset, nalData, nalLength, readLength);
+      nalLength += readLength;
+    }
+
+    /**
+     * Invoked to indicate that a NAL unit has ended.
+     *
+     * @param discardPadding The number of excess bytes that were passed to
+     *     {@link #appendToNalUnit(byte[], int, int)}, which should be discarded.
+     * @return True if the ended NAL unit is of the target type. False otherwise.
+     */
+    public boolean endNalUnit(int discardPadding) {
+      if (!isFilling) {
+        return false;
+      }
+      nalLength -= discardPadding;
+      isFilling = false;
+      isCompleted = true;
+      return true;
+    }
+
+  }
+
+}

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.