Commit e6778c90 by Oliver Woodman

Merge branch 'dev-v2-r2.1.0' into release-v2

parents d79f8f64 8853050c
Showing with 2336 additions and 603 deletions
*** PLEASE DO NOT IGNORE THIS ISSUE TEMPLATE ***
Please search the existing issues before filing a new one, including issues that Please search the existing issues before filing a new one, including issues that
are closed. When filing a new issue please include all of the following, unless are closed. When filing a new issue please include ALL of the following, unless
you're certain that they're not useful for the particular issue being reported. you're certain that they're not useful for the particular issue being reported.
- A description of the issue. - A description of the issue.
......
# Release notes # # Release notes #
### r2.1.0 ###
This release contains important bug fixes. Users of r2.0.x should proactively
update to this version.
* HLS: Support for seeking in live streams
([87](https://github.com/google/ExoPlayer/issues/87)).
* HLS: Improved support:
* Support for EXT-X-PROGRAM-DATE-TIME
([747](https://github.com/google/ExoPlayer/issues/747)).
* Improved handling of sample timestamps and their alignment across variants
and renditions.
* Fix issue that could cause playbacks to get stuck in an endless initial
buffering state.
* Correctly propagate BehindLiveWindowException instead of
IndexOutOfBoundsException exception
([1695](https://github.com/google/ExoPlayer/issues/1695)).
* MP3/MP4: Support for ID3 metadata, including embedded album art
([979](https://github.com/google/ExoPlayer/issues/979)).
* Improved customization of UI components. You can read about customization of
ExoPlayer's UI components
[here](https://medium.com/google-exoplayer/customizing-exoplayers-ui-components-728cf55ee07a#.9ewjg7avi).
* Robustness improvements when handling MediaSource timeline changes and
MediaPeriod transitions.
* EIA608: Support for caption styling and positioning.
* MPEG-TS: Improved support:
* Support injection of custom TS payload readers.
* Support injection of custom section payload readers.
* Support SCTE-35 splice information messages.
* Support multiple table sections in a single PSI section.
* Fix NullPointerException when an unsupported stream type is encountered
([2149](https://github.com/google/ExoPlayer/issues/2149)).
* Avoid failure when expected ID3 header not found
([1966](https://github.com/google/ExoPlayer/issues/1966)).
* Improvements to the upstream cache package.
* Support caching of media segments for DASH, HLS and SmoothStreaming. Note
that caching of manifest and playlist files is still not supported in the
(normal) case where the corresponding responses are compressed.
* Support caching for ExtractorMediaSource based playbacks.
* Improved flexibility of SimpleExoPlayer
([2102](https://github.com/google/ExoPlayer/issues/2102)).
* Fix issue where only the audio of a video would play due to capability
detection issues ([2007](https://github.com/google/ExoPlayer/issues/2007))
([2034](https://github.com/google/ExoPlayer/issues/2034))
([2157](https://github.com/google/ExoPlayer/issues/2157)).
* Fix issues that could cause ExtractorMediaSource based playbacks to get stuck
buffering ([1962](https://github.com/google/ExoPlayer/issues/1962)).
* Correctly set SimpleExoPlayerView surface aspect ratio when an active player
is attached ([2077](https://github.com/google/ExoPlayer/issues/1976)).
* OGG: Fix playback of short OGG files
([1976](https://github.com/google/ExoPlayer/issues/1976)).
* MP4: Support `.mp3` tracks
([2066](https://github.com/google/ExoPlayer/issues/2066)).
* SubRip: Don't fail playbacks if SubRip file contains negative timestamps
([2145](https://github.com/google/ExoPlayer/issues/2145)).
* Misc bugfixes.
### r2.0.4 ### ### r2.0.4 ###
This release contains important bug fixes. Users of earlier r2.0.x versions * Fix crash on Jellybean devices when using playback controls
should proactively update to this version. ([#1965](https://github.com/google/ExoPlayer/issues/1965)).
### r2.0.3 ###
* Fix crash on Jellybean devices when using playback controls * Fix crash on Jellybean devices when using playback controls
([#1965](https://github.com/google/ExoPlayer/issues/1965)). ([#1965](https://github.com/google/ExoPlayer/issues/1965)).
...@@ -113,6 +172,26 @@ some of the motivations behind ExoPlayer 2.x ...@@ -113,6 +172,26 @@ some of the motivations behind ExoPlayer 2.x
* Suppressed "Sending message to a Handler on a dead thread" warnings * Suppressed "Sending message to a Handler on a dead thread" warnings
([#426](https://github.com/google/ExoPlayer/issues/426)). ([#426](https://github.com/google/ExoPlayer/issues/426)).
# Legacy release notes #
Note: Since ExoPlayer V1 is still being maintained alongside V2, there is some
overlap between these notes and the notes above. r2.0.0 followed from r1.5.11,
and hence it can be assumed that all changes in r1.5.11 and earlier are included
in all V2 releases. This cannot be assumed for changes in r1.5.12 and later,
however it can be assumed that all such changes are included in the most recent
V2 release.
### r1.5.13 ###
* Improvements to the upstream cache package.
* MP4: Support `.mp3` tracks
([2066](https://github.com/google/ExoPlayer/issues/2066)).
* SubRip: Don't fail playbacks if SubRip file contains negative timestamps
([2145](https://github.com/google/ExoPlayer/issues/2145)).
* MPEG-TS: Avoid failure when expected ID3 header not found
([1966](https://github.com/google/ExoPlayer/issues/1966)).
* Misc bugfixes.
### r1.5.12 ### ### r1.5.12 ###
* Improvements to Cronet network stack extension. * Improvements to Cronet network stack extension.
......
...@@ -19,7 +19,7 @@ buildscript { ...@@ -19,7 +19,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:2.1.2' classpath 'com.android.tools.build:gradle:2.2.1'
classpath 'com.novoda:bintray-release:0.3.4' classpath 'com.novoda:bintray-release:0.3.4'
} }
} }
...@@ -35,7 +35,7 @@ allprojects { ...@@ -35,7 +35,7 @@ allprojects {
releaseRepoName = 'exoplayer' releaseRepoName = 'exoplayer'
releaseUserOrg = 'google' releaseUserOrg = 'google'
releaseGroupId = 'com.google.android.exoplayer' releaseGroupId = 'com.google.android.exoplayer'
releaseVersion = 'r2.0.4' releaseVersion = 'r2.1.0'
releaseWebsite = 'https://github.com/google/ExoPlayer' releaseWebsite = 'https://github.com/google/ExoPlayer'
} }
} }
...@@ -41,6 +41,7 @@ android { ...@@ -41,6 +41,7 @@ android {
noExtensions noExtensions
withExtensions withExtensions
} }
} }
dependencies { dependencies {
......
...@@ -16,17 +16,19 @@ ...@@ -16,17 +16,19 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.demo" package="com.google.android.exoplayer2.demo"
android:versionCode="2004" android:versionCode="2100"
android:versionName="2.0.4"> android:versionName="2.1.0">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-feature android:name="android.software.leanback" android:required="false"/>
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="24"/> <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="24"/>
<application <application
android:label="@string/application_name" android:label="@string/application_name"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:banner="@drawable/ic_banner"
android:largeHeap="true" android:largeHeap="true"
android:allowBackup="false" android:allowBackup="false"
android:name="com.google.android.exoplayer2.demo.DemoApplication"> android:name="com.google.android.exoplayer2.demo.DemoApplication">
...@@ -37,6 +39,7 @@ ...@@ -37,6 +39,7 @@
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW"/> <action android:name="android.intent.action.VIEW"/>
......
...@@ -36,13 +36,17 @@ public class DemoApplication extends Application { ...@@ -36,13 +36,17 @@ public class DemoApplication extends Application {
userAgent = Util.getUserAgent(this, "ExoPlayerDemo"); userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
} }
DataSource.Factory buildDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) { public DataSource.Factory buildDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
return new DefaultDataSourceFactory(this, bandwidthMeter, return new DefaultDataSourceFactory(this, bandwidthMeter,
buildHttpDataSourceFactory(bandwidthMeter)); buildHttpDataSourceFactory(bandwidthMeter));
} }
HttpDataSource.Factory buildHttpDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) { public HttpDataSource.Factory buildHttpDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
return new DefaultHttpDataSourceFactory(userAgent, bandwidthMeter); return new DefaultHttpDataSourceFactory(userAgent, bandwidthMeter);
} }
public boolean useExtensionRenderers() {
return BuildConfig.FLAVOR.equals("withExtensions");
}
} }
...@@ -22,6 +22,7 @@ import android.net.Uri; ...@@ -22,6 +22,7 @@ import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.widget.Button; import android.widget.Button;
...@@ -55,11 +56,9 @@ import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource ...@@ -55,11 +56,9 @@ import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveVideoTrackSelection; import com.google.android.exoplayer2.trackselection.AdaptiveVideoTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelections; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.ui.DebugTextViewHelper; import com.google.android.exoplayer2.ui.DebugTextViewHelper;
import com.google.android.exoplayer2.ui.PlaybackControlView; import com.google.android.exoplayer2.ui.PlaybackControlView;
import com.google.android.exoplayer2.ui.SimpleExoPlayerView; import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
...@@ -78,7 +77,7 @@ import java.util.UUID; ...@@ -78,7 +77,7 @@ import java.util.UUID;
* An activity that plays media using {@link SimpleExoPlayer}. * An activity that plays media using {@link SimpleExoPlayer}.
*/ */
public class PlayerActivity extends Activity implements OnClickListener, ExoPlayer.EventListener, public class PlayerActivity extends Activity implements OnClickListener, ExoPlayer.EventListener,
TrackSelector.EventListener<MappedTrackInfo>, PlaybackControlView.VisibilityListener { PlaybackControlView.VisibilityListener {
public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid"; public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
public static final String DRM_LICENSE_URL = "drm_license_url"; public static final String DRM_LICENSE_URL = "drm_license_url";
...@@ -110,7 +109,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -110,7 +109,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
private DataSource.Factory mediaDataSourceFactory; private DataSource.Factory mediaDataSourceFactory;
private SimpleExoPlayer player; private SimpleExoPlayer player;
private MappingTrackSelector trackSelector; private DefaultTrackSelector trackSelector;
private TrackSelectionHelper trackSelectionHelper; private TrackSelectionHelper trackSelectionHelper;
private DebugTextViewHelper debugViewHelper; private DebugTextViewHelper debugViewHelper;
private boolean playerNeedsSource; private boolean playerNeedsSource;
...@@ -196,6 +195,16 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -196,6 +195,16 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
} }
} }
// Activity input
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// Show the controls on any key event.
simpleExoPlayerView.showController();
// If the event was not handled then see if the player view can handle it as a media key event.
return super.dispatchKeyEvent(event) || simpleExoPlayerView.dispatchMediaKeyEvent(event);
}
// OnClickListener methods // OnClickListener methods
@Override @Override
...@@ -203,8 +212,11 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -203,8 +212,11 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
if (view == retryButton) { if (view == retryButton) {
initializePlayer(); initializePlayer();
} else if (view.getParent() == debugRootView) { } else if (view.getParent() == debugRootView) {
trackSelectionHelper.showSelectionDialog(this, ((Button) view).getText(), MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
trackSelector.getCurrentSelections().info, (int) view.getTag()); if (mappedTrackInfo != null) {
trackSelectionHelper.showSelectionDialog(this, ((Button) view).getText(),
trackSelector.getCurrentMappedTrackInfo(), (int) view.getTag());
}
} }
} }
...@@ -249,20 +261,25 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -249,20 +261,25 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
} }
} }
eventLogger = new EventLogger(); @SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode =
((DemoApplication) getApplication()).useExtensionRenderers()
? (preferExtensionDecoders ? SimpleExoPlayer.EXTENSION_RENDERER_MODE_PREFER
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_ON)
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF;
TrackSelection.Factory videoTrackSelectionFactory = TrackSelection.Factory videoTrackSelectionFactory =
new AdaptiveVideoTrackSelection.Factory(BANDWIDTH_METER); new AdaptiveVideoTrackSelection.Factory(BANDWIDTH_METER);
trackSelector = new DefaultTrackSelector(mainHandler, videoTrackSelectionFactory); trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
trackSelector.addListener(this);
trackSelector.addListener(eventLogger);
trackSelectionHelper = new TrackSelectionHelper(trackSelector, videoTrackSelectionFactory); trackSelectionHelper = new TrackSelectionHelper(trackSelector, videoTrackSelectionFactory);
player = ExoPlayerFactory.newSimpleInstance(this, trackSelector, new DefaultLoadControl(), player = ExoPlayerFactory.newSimpleInstance(this, trackSelector, new DefaultLoadControl(),
drmSessionManager, preferExtensionDecoders); drmSessionManager, extensionRendererMode);
player.addListener(this); player.addListener(this);
eventLogger = new EventLogger(trackSelector);
player.addListener(eventLogger); player.addListener(eventLogger);
player.setAudioDebugListener(eventLogger); player.setAudioDebugListener(eventLogger);
player.setVideoDebugListener(eventLogger); player.setVideoDebugListener(eventLogger);
player.setId3Output(eventLogger); player.setId3Output(eventLogger);
simpleExoPlayerView.setPlayer(player); simpleExoPlayerView.setPlayer(player);
if (isTimelineStatic) { if (isTimelineStatic) {
if (playerPosition == C.TIME_UNSET) { if (playerPosition == C.TIME_UNSET) {
...@@ -353,7 +370,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -353,7 +370,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
playerWindow = player.getCurrentWindowIndex(); playerWindow = player.getCurrentWindowIndex();
playerPosition = C.TIME_UNSET; playerPosition = C.TIME_UNSET;
Timeline timeline = player.getCurrentTimeline(); Timeline timeline = player.getCurrentTimeline();
if (timeline != null && timeline.getWindow(playerWindow, window).isSeekable) { if (!timeline.isEmpty() && timeline.getWindow(playerWindow, window).isSeekable) {
playerPosition = player.getCurrentPosition(); playerPosition = player.getCurrentPosition();
} }
player.release(); player.release();
...@@ -410,7 +427,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -410,7 +427,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest) { public void onTimelineChanged(Timeline timeline, Object manifest) {
isTimelineStatic = timeline != null && timeline.getWindowCount() > 0 isTimelineStatic = !timeline.isEmpty()
&& !timeline.getWindow(timeline.getWindowCount() - 1, window).isDynamic; && !timeline.getWindow(timeline.getWindowCount() - 1, window).isDynamic;
} }
...@@ -447,17 +464,19 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -447,17 +464,19 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
showControls(); showControls();
} }
// MappingTrackSelector.EventListener implementation
@Override @Override
public void onTrackSelectionsChanged(TrackSelections<? extends MappedTrackInfo> trackSelections) { public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
updateButtonVisibilities(); updateButtonVisibilities();
MappedTrackInfo trackInfo = trackSelections.info; MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (trackInfo.hasOnlyUnplayableTracks(C.TRACK_TYPE_VIDEO)) { if (mappedTrackInfo != null) {
showToast(R.string.error_unsupported_video); if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_VIDEO)
} == MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
if (trackInfo.hasOnlyUnplayableTracks(C.TRACK_TYPE_AUDIO)) { showToast(R.string.error_unsupported_video);
showToast(R.string.error_unsupported_audio); }
if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_AUDIO)
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
showToast(R.string.error_unsupported_audio);
}
} }
} }
...@@ -473,14 +492,13 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -473,14 +492,13 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
return; return;
} }
TrackSelections<MappedTrackInfo> trackSelections = trackSelector.getCurrentSelections(); MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (trackSelections == null) { if (mappedTrackInfo == null) {
return; return;
} }
int rendererCount = trackSelections.length; for (int i = 0; i < mappedTrackInfo.length; i++) {
for (int i = 0; i < rendererCount; i++) { TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(i);
TrackGroupArray trackGroups = trackSelections.info.getTrackGroups(i);
if (trackGroups.length != 0) { if (trackGroups.length != 0) {
Button button = new Button(this); Button button = new Button(this);
int label; int label;
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.demo; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.demo;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.res.AssetManager;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
...@@ -43,6 +44,7 @@ import java.io.IOException; ...@@ -43,6 +44,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
...@@ -63,9 +65,21 @@ public class SampleChooserActivity extends Activity { ...@@ -63,9 +65,21 @@ public class SampleChooserActivity extends Activity {
if (dataUri != null) { if (dataUri != null) {
uris = new String[] {dataUri}; uris = new String[] {dataUri};
} else { } else {
uris = new String[] { ArrayList<String> uriList = new ArrayList<>();
"asset:///media.exolist.json", AssetManager assetManager = getAssets();
}; try {
for (String asset : assetManager.list("")) {
if (asset.endsWith(".exolist.json")) {
uriList.add("asset:///" + asset);
}
}
} catch (IOException e) {
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
.show();
}
uris = new String[uriList.size()];
uriList.toArray(uris);
Arrays.sort(uris);
} }
SampleListLoader loaderTask = new SampleListLoader(); SampleListLoader loaderTask = new SampleListLoader();
loaderTask.execute(uris); loaderTask.execute(uris);
......
...@@ -18,7 +18,9 @@ package com.google.android.exoplayer2.demo; ...@@ -18,7 +18,9 @@ package com.google.android.exoplayer2.demo;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.res.TypedArray;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Pair; import android.util.Pair;
import android.view.LayoutInflater; import android.view.LayoutInflater;
...@@ -100,7 +102,7 @@ import java.util.Locale; ...@@ -100,7 +102,7 @@ import java.util.Locale;
AlertDialog.Builder builder = new AlertDialog.Builder(activity); AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(title) builder.setTitle(title)
.setView(buildView(LayoutInflater.from(builder.getContext()))) .setView(buildView(builder.getContext()))
.setPositiveButton(android.R.string.ok, this) .setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.create() .create()
...@@ -108,13 +110,20 @@ import java.util.Locale; ...@@ -108,13 +110,20 @@ import java.util.Locale;
} }
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
private View buildView(LayoutInflater inflater) { private View buildView(Context context) {
LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.track_selection_dialog, null); View view = inflater.inflate(R.layout.track_selection_dialog, null);
ViewGroup root = (ViewGroup) view.findViewById(R.id.root); ViewGroup root = (ViewGroup) view.findViewById(R.id.root);
TypedArray attributeArray = context.getTheme().obtainStyledAttributes(
new int[] {android.R.attr.selectableItemBackground});
int selectableItemBackgroundResourceId = attributeArray.getResourceId(0, 0);
attributeArray.recycle();
// View for disabling the renderer. // View for disabling the renderer.
disableView = (CheckedTextView) inflater.inflate( disableView = (CheckedTextView) inflater.inflate(
android.R.layout.simple_list_item_single_choice, root, false); android.R.layout.simple_list_item_single_choice, root, false);
disableView.setBackgroundResource(selectableItemBackgroundResourceId);
disableView.setText(R.string.selection_disabled); disableView.setText(R.string.selection_disabled);
disableView.setFocusable(true); disableView.setFocusable(true);
disableView.setOnClickListener(this); disableView.setOnClickListener(this);
...@@ -123,6 +132,7 @@ import java.util.Locale; ...@@ -123,6 +132,7 @@ import java.util.Locale;
// View for clearing the override to allow the selector to use its default selection logic. // View for clearing the override to allow the selector to use its default selection logic.
defaultView = (CheckedTextView) inflater.inflate( defaultView = (CheckedTextView) inflater.inflate(
android.R.layout.simple_list_item_single_choice, root, false); android.R.layout.simple_list_item_single_choice, root, false);
defaultView.setBackgroundResource(selectableItemBackgroundResourceId);
defaultView.setText(R.string.selection_default); defaultView.setText(R.string.selection_default);
defaultView.setFocusable(true); defaultView.setFocusable(true);
defaultView.setOnClickListener(this); defaultView.setOnClickListener(this);
...@@ -146,6 +156,7 @@ import java.util.Locale; ...@@ -146,6 +156,7 @@ import java.util.Locale;
: android.R.layout.simple_list_item_single_choice; : android.R.layout.simple_list_item_single_choice;
CheckedTextView trackView = (CheckedTextView) inflater.inflate( CheckedTextView trackView = (CheckedTextView) inflater.inflate(
trackViewLayoutId, root, false); trackViewLayoutId, root, false);
trackView.setBackgroundResource(selectableItemBackgroundResourceId);
trackView.setText(buildTrackName(group.getFormat(trackIndex))); trackView.setText(buildTrackName(group.getFormat(trackIndex)));
if (trackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex) if (trackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex)
== RendererCapabilities.FORMAT_HANDLED) { == RendererCapabilities.FORMAT_HANDLED) {
...@@ -169,6 +180,7 @@ import java.util.Locale; ...@@ -169,6 +180,7 @@ import java.util.Locale;
// View for using random adaptation. // View for using random adaptation.
enableRandomAdaptationView = (CheckedTextView) inflater.inflate( enableRandomAdaptationView = (CheckedTextView) inflater.inflate(
android.R.layout.simple_list_item_multiple_choice, root, false); android.R.layout.simple_list_item_multiple_choice, root, false);
enableRandomAdaptationView.setBackgroundResource(selectableItemBackgroundResourceId);
enableRandomAdaptationView.setText(R.string.enable_random_adaptation); enableRandomAdaptationView.setText(R.string.enable_random_adaptation);
enableRandomAdaptationView.setOnClickListener(this); enableRandomAdaptationView.setOnClickListener(this);
root.addView(inflater.inflate(R.layout.list_divider, root, false)); root.addView(inflater.inflate(R.layout.list_divider, root, false));
......
...@@ -16,13 +16,11 @@ ...@@ -16,13 +16,11 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root" android:id="@+id/root"
android:focusable="true"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:keepScreenOn="true"> android:keepScreenOn="true">
<com.google.android.exoplayer2.ui.SimpleExoPlayerView android:id="@+id/player_view" <com.google.android.exoplayer2.ui.SimpleExoPlayerView android:id="@+id/player_view"
android:focusable="true"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="match_parent"/>
......
...@@ -21,8 +21,9 @@ git clone https://github.com/google/ExoPlayer.git ...@@ -21,8 +21,9 @@ git clone https://github.com/google/ExoPlayer.git
1. Find the latest Cronet release [here][] and navigate to its `Release/cronet` 1. Find the latest Cronet release [here][] and navigate to its `Release/cronet`
directory directory
1. Download `cronet.jar`, `cronet_api.jar` and the `libs` directory 1. Download `cronet_api.jar`, `cronet_impl_common_java.jar`,
1. Copy the two jar files into the `libs` directory of this extension `cronet_impl_native_java.jar` and the `libs` directory
1. Copy the three jar files into the `libs` directory of this extension
1. Copy the content of the downloaded `libs` directory into the `jniLibs` 1. Copy the content of the downloaded `libs` directory into the `jniLibs`
directory of this extension directory of this extension
......
...@@ -42,10 +42,11 @@ android { ...@@ -42,10 +42,11 @@ android {
dependencies { dependencies {
compile project(':library') compile project(':library')
compile files('libs/cronet_api.jar') compile files('libs/cronet_api.jar')
compile files('libs/cronet.jar') compile files('libs/cronet_impl_common_java.jar')
compile files('libs/cronet_impl_native_java.jar')
androidTestCompile 'com.google.dexmaker:dexmaker:1.2' androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
androidTestCompile 'org.mockito:mockito-core:1.9.5' androidTestCompile 'org.mockito:mockito-core:1.9.5'
androidTestCompile project(':library') androidTestCompile project(':library')
androidTestCompile 'com.android.support.test:runner:0.4' androidTestCompile 'com.android.support.test:runner:0.5'
} }
...@@ -22,7 +22,6 @@ import static org.junit.Assert.assertFalse; ...@@ -22,7 +22,6 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.mockito.Matchers.any; import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq; import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
...@@ -52,7 +51,6 @@ import java.net.UnknownHostException; ...@@ -52,7 +51,6 @@ import java.net.UnknownHostException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
...@@ -62,6 +60,7 @@ import org.chromium.net.CronetEngine; ...@@ -62,6 +60,7 @@ import org.chromium.net.CronetEngine;
import org.chromium.net.UrlRequest; import org.chromium.net.UrlRequest;
import org.chromium.net.UrlRequestException; import org.chromium.net.UrlRequestException;
import org.chromium.net.UrlResponseInfo; import org.chromium.net.UrlResponseInfo;
import org.chromium.net.impl.UrlResponseInfoImpl;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
...@@ -88,20 +87,7 @@ public final class CronetDataSourceTest { ...@@ -88,20 +87,7 @@ public final class CronetDataSourceTest {
private Map<String, String> testResponseHeader; private Map<String, String> testResponseHeader;
private UrlResponseInfo testUrlResponseInfo; private UrlResponseInfo testUrlResponseInfo;
/** @Mock private UrlRequest.Builder mockUrlRequestBuilder;
* MockableCronetEngine is an abstract class for helping creating new Requests.
*/
public abstract static class MockableCronetEngine extends CronetEngine {
@Override
public abstract UrlRequest createRequest(String url, UrlRequest.Callback callback,
Executor executor, int priority,
Collection<Object> connectionAnnotations,
boolean disableCache,
boolean disableConnectionMigration,
boolean allowDirectExecutor);
}
@Mock @Mock
private UrlRequest mockUrlRequest; private UrlRequest mockUrlRequest;
@Mock @Mock
...@@ -114,8 +100,7 @@ public final class CronetDataSourceTest { ...@@ -114,8 +100,7 @@ public final class CronetDataSourceTest {
private Executor mockExecutor; private Executor mockExecutor;
@Mock @Mock
private UrlRequestException mockUrlRequestException; private UrlRequestException mockUrlRequestException;
@Mock @Mock private CronetEngine mockCronetEngine;
private MockableCronetEngine mockCronetEngine;
private CronetDataSource dataSourceUnderTest; private CronetDataSource dataSourceUnderTest;
...@@ -135,15 +120,10 @@ public final class CronetDataSourceTest { ...@@ -135,15 +120,10 @@ public final class CronetDataSourceTest {
true, // resetTimeoutOnRedirects true, // resetTimeoutOnRedirects
mockClock)); mockClock));
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true); when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);
when(mockCronetEngine.createRequest( when(mockCronetEngine.newUrlRequestBuilder(
anyString(), anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
any(UrlRequest.Callback.class), .thenReturn(mockUrlRequestBuilder);
any(Executor.class), when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest);
anyInt(),
eq(Collections.emptyList()),
any(Boolean.class),
any(Boolean.class),
any(Boolean.class))).thenReturn(mockUrlRequest);
mockStatusResponse(); mockStatusResponse();
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, C.LENGTH_UNSET, null); testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, C.LENGTH_UNSET, null);
...@@ -159,7 +139,7 @@ public final class CronetDataSourceTest { ...@@ -159,7 +139,7 @@ public final class CronetDataSourceTest {
private UrlResponseInfo createUrlResponseInfo(int statusCode) { private UrlResponseInfo createUrlResponseInfo(int statusCode) {
ArrayList<Map.Entry<String, String>> responseHeaderList = new ArrayList<>(); ArrayList<Map.Entry<String, String>> responseHeaderList = new ArrayList<>();
responseHeaderList.addAll(testResponseHeader.entrySet()); responseHeaderList.addAll(testResponseHeader.entrySet());
return new UrlResponseInfo( return new UrlResponseInfoImpl(
Collections.singletonList(TEST_URL), Collections.singletonList(TEST_URL),
statusCode, statusCode,
null, // httpStatusText null, // httpStatusText
...@@ -184,15 +164,7 @@ public final class CronetDataSourceTest { ...@@ -184,15 +164,7 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.close(); dataSourceUnderTest.close();
// Prepare a mock UrlRequest to be used in the second open() call. // Prepare a mock UrlRequest to be used in the second open() call.
final UrlRequest mockUrlRequest2 = mock(UrlRequest.class); final UrlRequest mockUrlRequest2 = mock(UrlRequest.class);
when(mockCronetEngine.createRequest( when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest2);
anyString(),
any(UrlRequest.Callback.class),
any(Executor.class),
anyInt(),
eq(Collections.emptyList()),
any(Boolean.class),
any(Boolean.class),
any(Boolean.class))).thenReturn(mockUrlRequest2);
doAnswer(new Answer<Object>() { doAnswer(new Answer<Object>() {
@Override @Override
public Object answer(InvocationOnMock invocation) throws Throwable { public Object answer(InvocationOnMock invocation) throws Throwable {
...@@ -215,15 +187,8 @@ public final class CronetDataSourceTest { ...@@ -215,15 +187,8 @@ public final class CronetDataSourceTest {
mockResponseStartSuccess(); mockResponseStartSuccess();
dataSourceUnderTest.open(testDataSpec); dataSourceUnderTest.open(testDataSpec);
verify(mockCronetEngine).createRequest( verify(mockCronetEngine)
eq(TEST_URL), .newUrlRequestBuilder(eq(TEST_URL), any(UrlRequest.Callback.class), any(Executor.class));
any(UrlRequest.Callback.class),
any(Executor.class),
anyInt(),
eq(Collections.emptyList()),
any(Boolean.class),
any(Boolean.class),
any(Boolean.class));
verify(mockUrlRequest).start(); verify(mockUrlRequest).start();
} }
...@@ -237,9 +202,9 @@ public final class CronetDataSourceTest { ...@@ -237,9 +202,9 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.open(testDataSpec); dataSourceUnderTest.open(testDataSpec);
// The header value to add is current position to current position + length - 1. // The header value to add is current position to current position + length - 1.
verify(mockUrlRequest).addHeader("Range", "bytes=1000-5999"); verify(mockUrlRequestBuilder).addHeader("Range", "bytes=1000-5999");
verify(mockUrlRequest).addHeader("firstHeader", "firstValue"); verify(mockUrlRequestBuilder).addHeader("firstHeader", "firstValue");
verify(mockUrlRequest).addHeader("secondHeader", "secondValue"); verify(mockUrlRequestBuilder).addHeader("secondHeader", "secondValue");
verify(mockUrlRequest).start(); verify(mockUrlRequest).start();
} }
......
...@@ -412,8 +412,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou ...@@ -412,8 +412,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
// Internal methods. // Internal methods.
private UrlRequest buildRequest(DataSpec dataSpec) throws OpenException { private UrlRequest buildRequest(DataSpec dataSpec) throws OpenException {
UrlRequest.Builder requestBuilder = new UrlRequest.Builder(dataSpec.uri.toString(), this, UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(dataSpec.uri.toString(),
executor, cronetEngine); this, executor);
// Set the headers. // Set the headers.
synchronized (requestProperties) { synchronized (requestProperties) {
if (dataSpec.postBody != null && dataSpec.postBody.length != 0 if (dataSpec.postBody != null && dataSpec.postBody.length != 0
......
...@@ -31,7 +31,7 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main" ...@@ -31,7 +31,7 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main"
NDK_PATH="<path to Android NDK>" NDK_PATH="<path to Android NDK>"
``` ```
* Fetch and build ffmpeg. * Fetch and build FFmpeg.
For example, to fetch and build for armv7a: For example, to fetch and build for armv7a:
...@@ -75,7 +75,7 @@ cd "${FFMPEG_EXT_PATH}"/jni && \ ...@@ -75,7 +75,7 @@ cd "${FFMPEG_EXT_PATH}"/jni && \
${NDK_PATH}/ndk-build APP_ABI=armeabi-v7a -j4 ${NDK_PATH}/ndk-build APP_ABI=armeabi-v7a -j4
``` ```
TODO: Add instructions for other ABIs. Repeat these steps for any other architectures you need to support.
* In your project, you can add a dependency on the extension by using a rule * In your project, you can add a dependency on the extension by using a rule
like this: like this:
......
...@@ -20,8 +20,8 @@ import com.google.android.exoplayer2.C; ...@@ -20,8 +20,8 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioCapabilities;
import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.audio.AudioTrack;
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
/** /**
...@@ -53,11 +53,10 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -53,11 +53,10 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
* @param eventListener A listener of events. 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 audioCapabilities The audio capabilities for playback on this device. May be null if the * @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed. * default capabilities (no encoded audio passthrough support) should be assumed.
* @param streamType The type of audio stream for the {@link AudioTrack}.
*/ */
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioCapabilities audioCapabilities, int streamType) { AudioCapabilities audioCapabilities) {
super(eventHandler, eventListener, audioCapabilities, streamType); super(eventHandler, eventListener, audioCapabilities);
} }
@Override @Override
...@@ -71,7 +70,8 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -71,7 +70,8 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
} }
@Override @Override
protected FfmpegDecoder createDecoder(Format format) throws FfmpegDecoderException { protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
throws FfmpegDecoderException {
decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
format.sampleMimeType, format.initializationData); format.sampleMimeType, format.initializationData);
return decoder; return decoder;
......
...@@ -267,7 +267,7 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, ...@@ -267,7 +267,7 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
sampleFormat, 1); sampleFormat, 1);
AVAudioResampleContext *resampleContext; AVAudioResampleContext *resampleContext;
if (context->opaque) { if (context->opaque) {
resampleContext = (AVAudioResampleContext *)context->opaque; resampleContext = (AVAudioResampleContext *) context->opaque;
} else { } else {
resampleContext = avresample_alloc_context(); resampleContext = avresample_alloc_context();
av_opt_set_int(resampleContext, "in_channel_layout", channelLayout, 0); av_opt_set_int(resampleContext, "in_channel_layout", channelLayout, 0);
...@@ -326,7 +326,7 @@ void releaseContext(AVCodecContext *context) { ...@@ -326,7 +326,7 @@ void releaseContext(AVCodecContext *context) {
return; return;
} }
AVAudioResampleContext *resampleContext; AVAudioResampleContext *resampleContext;
if (resampleContext = (AVAudioResampleContext *)context->opaque) { if ((resampleContext = (AVAudioResampleContext *) context->opaque)) {
avresample_free(&resampleContext); avresample_free(&resampleContext);
context->opaque = NULL; context->opaque = NULL;
} }
......
...@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.flac; ...@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.flac;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
...@@ -27,7 +26,9 @@ import com.google.android.exoplayer2.Renderer; ...@@ -27,7 +26,9 @@ import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
/** /**
...@@ -72,7 +73,7 @@ public class FlacPlaybackTest extends InstrumentationTestCase { ...@@ -72,7 +73,7 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
public void run() { public void run() {
Looper.prepare(); Looper.prepare();
LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer(); LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer();
DefaultTrackSelector trackSelector = new DefaultTrackSelector(new Handler()); DefaultTrackSelector trackSelector = new DefaultTrackSelector();
player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector); player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this); player.addListener(this);
ExtractorMediaSource mediaSource = new ExtractorMediaSource( ExtractorMediaSource mediaSource = new ExtractorMediaSource(
...@@ -92,6 +93,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase { ...@@ -92,6 +93,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
} }
@Override @Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
// Do nothing.
}
@Override
public void onPositionDiscontinuity() { public void onPositionDiscontinuity() {
// Do nothing. // Do nothing.
} }
......
...@@ -95,7 +95,7 @@ public final class FlacExtractor implements Extractor { ...@@ -95,7 +95,7 @@ public final class FlacExtractor implements Extractor {
if (streamInfo == null) { if (streamInfo == null) {
throw new IOException("Metadata decoding failed"); throw new IOException("Metadata decoding failed");
} }
} catch (IOException e){ } catch (IOException e) {
decoderJni.reset(0); decoderJni.reset(0);
input.setRetryPosition(0, e); input.setRetryPosition(0, e);
throw e; // never executes throw e; // never executes
...@@ -137,7 +137,7 @@ public final class FlacExtractor implements Extractor { ...@@ -137,7 +137,7 @@ public final class FlacExtractor implements Extractor {
int size; int size;
try { try {
size = decoderJni.decodeSample(outputByteBuffer); size = decoderJni.decodeSample(outputByteBuffer);
} catch (IOException e){ } catch (IOException e) {
if (lastDecodePosition >= 0) { if (lastDecodePosition >= 0) {
decoderJni.reset(lastDecodePosition); decoderJni.reset(lastDecodePosition);
input.setRetryPosition(lastDecodePosition, e); input.setRetryPosition(lastDecodePosition, e);
...@@ -155,7 +155,7 @@ public final class FlacExtractor implements Extractor { ...@@ -155,7 +155,7 @@ public final class FlacExtractor implements Extractor {
} }
@Override @Override
public void seek(long position) { public void seek(long position, long timeUs) {
if (position == 0) { if (position == 0) {
metadataParsed = false; metadataParsed = false;
} }
......
...@@ -19,8 +19,8 @@ import android.os.Handler; ...@@ -19,8 +19,8 @@ import android.os.Handler;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioCapabilities;
import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.audio.AudioTrack;
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
/** /**
...@@ -49,11 +49,10 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -49,11 +49,10 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
* @param eventListener A listener of events. 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 audioCapabilities The audio capabilities for playback on this device. May be null if the * @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed. * default capabilities (no encoded audio passthrough support) should be assumed.
* @param streamType The type of audio stream for the {@link AudioTrack}.
*/ */
public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioCapabilities audioCapabilities, int streamType) { AudioCapabilities audioCapabilities) {
super(eventHandler, eventListener, audioCapabilities, streamType); super(eventHandler, eventListener, audioCapabilities);
} }
@Override @Override
...@@ -63,7 +62,8 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -63,7 +62,8 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
} }
@Override @Override
protected FlacDecoder createDecoder(Format format) throws FlacDecoderException { protected FlacDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
throws FlacDecoderException {
return new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, format.initializationData); return new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, format.initializationData);
} }
......
...@@ -37,7 +37,7 @@ android { ...@@ -37,7 +37,7 @@ android {
dependencies { dependencies {
compile project(':library') compile project(':library')
compile('com.squareup.okhttp3:okhttp:+') { compile('com.squareup.okhttp3:okhttp:3.4.1') {
exclude group: 'org.json' exclude group: 'org.json'
} }
} }
......
...@@ -65,7 +65,8 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -65,7 +65,8 @@ public class OkHttpDataSource implements HttpDataSource {
private long bytesRead; private long bytesRead;
/** /**
* @param callFactory An {@link Call.Factory} for use by the source. * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the source.
* @param userAgent The User-Agent string that should be used. * @param userAgent The User-Agent string that should be used.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then a InvalidContentTypeException} is thrown from {@link #open(DataSpec)}. * predicate then a InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
...@@ -76,7 +77,8 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -76,7 +77,8 @@ public class OkHttpDataSource implements HttpDataSource {
} }
/** /**
* @param callFactory An {@link Call.Factory} for use by the source. * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the source.
* @param userAgent The User-Agent string that should be used. * @param userAgent The User-Agent string that should be used.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then a {@link InvalidContentTypeException} is thrown from * predicate then a {@link InvalidContentTypeException} is thrown from
...@@ -89,14 +91,14 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -89,14 +91,14 @@ public class OkHttpDataSource implements HttpDataSource {
} }
/** /**
* @param callFactory An {@link Call.Factory} for use by the source. * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the source.
* @param userAgent The User-Agent string that should be used. * @param userAgent The User-Agent string that should be used.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then a {@link InvalidContentTypeException} is thrown from * predicate then a {@link InvalidContentTypeException} is thrown from
* {@link #open(DataSpec)}. * {@link #open(DataSpec)}.
* @param listener An optional listener. * @param listener An optional listener.
* @param cacheControl An optional {@link CacheControl} which sets all requests' Cache-Control * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
* header. For example, you could force the network response for all requests.
*/ */
public OkHttpDataSource(Call.Factory callFactory, String userAgent, public OkHttpDataSource(Call.Factory callFactory, String userAgent,
Predicate<String> contentTypePredicate, TransferListener<? super OkHttpDataSource> listener, Predicate<String> contentTypePredicate, TransferListener<? super OkHttpDataSource> listener,
......
...@@ -28,25 +28,38 @@ public final class OkHttpDataSourceFactory implements Factory { ...@@ -28,25 +28,38 @@ public final class OkHttpDataSourceFactory implements Factory {
private final Call.Factory callFactory; private final Call.Factory callFactory;
private final String userAgent; private final String userAgent;
private final TransferListener<? super DataSource> transferListener; private final TransferListener<? super DataSource> listener;
private final CacheControl cacheControl; private final CacheControl cacheControl;
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the sources created by the factory.
* @param userAgent The User-Agent string that should be used.
* @param listener An optional listener.
*/
public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent, public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent,
TransferListener<? super DataSource> transferListener) { TransferListener<? super DataSource> listener) {
this(callFactory, userAgent, transferListener, null); this(callFactory, userAgent, listener, null);
} }
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the sources created by the factory.
* @param userAgent The User-Agent string that should be used.
* @param listener An optional listener.
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
*/
public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent, public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent,
TransferListener<? super DataSource> transferListener, CacheControl cacheControl) { TransferListener<? super DataSource> listener, CacheControl cacheControl) {
this.callFactory = callFactory; this.callFactory = callFactory;
this.userAgent = userAgent; this.userAgent = userAgent;
this.transferListener = transferListener; this.listener = listener;
this.cacheControl = cacheControl; this.cacheControl = cacheControl;
} }
@Override @Override
public OkHttpDataSource createDataSource() { public OkHttpDataSource createDataSource() {
return new OkHttpDataSource(callFactory, userAgent, null, transferListener, cacheControl); return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl);
} }
} }
...@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.opus; ...@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.opus;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
...@@ -27,7 +26,9 @@ import com.google.android.exoplayer2.Renderer; ...@@ -27,7 +26,9 @@ import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
/** /**
...@@ -72,7 +73,7 @@ public class OpusPlaybackTest extends InstrumentationTestCase { ...@@ -72,7 +73,7 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
public void run() { public void run() {
Looper.prepare(); Looper.prepare();
LibopusAudioRenderer audioRenderer = new LibopusAudioRenderer(); LibopusAudioRenderer audioRenderer = new LibopusAudioRenderer();
DefaultTrackSelector trackSelector = new DefaultTrackSelector(new Handler()); DefaultTrackSelector trackSelector = new DefaultTrackSelector();
player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector); player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this); player.addListener(this);
ExtractorMediaSource mediaSource = new ExtractorMediaSource( ExtractorMediaSource mediaSource = new ExtractorMediaSource(
...@@ -92,6 +93,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase { ...@@ -92,6 +93,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
} }
@Override @Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
// Do nothing.
}
@Override
public void onPositionDiscontinuity() { public void onPositionDiscontinuity() {
// Do nothing. // Do nothing.
} }
......
...@@ -19,8 +19,9 @@ import android.os.Handler; ...@@ -19,8 +19,9 @@ import android.os.Handler;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioCapabilities;
import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.audio.AudioTrack;
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer; import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
/** /**
...@@ -50,11 +51,24 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -50,11 +51,24 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
* @param eventListener A listener of events. 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 audioCapabilities The audio capabilities for playback on this device. May be null if the * @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed. * default capabilities (no encoded audio passthrough support) should be assumed.
* @param streamType The type of audio stream for the {@link AudioTrack}.
*/ */
public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioCapabilities audioCapabilities, int streamType) { AudioCapabilities audioCapabilities) {
super(eventHandler, eventListener, audioCapabilities, streamType); super(eventHandler, eventListener, audioCapabilities);
}
/**
* @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 audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
*/
public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioCapabilities audioCapabilities, DrmSessionManager<ExoMediaCrypto> drmSessionManager,
boolean playClearSamplesWithoutKeys) {
super(eventHandler, eventListener, audioCapabilities, drmSessionManager,
playClearSamplesWithoutKeys);
} }
@Override @Override
...@@ -64,9 +78,10 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -64,9 +78,10 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
} }
@Override @Override
protected OpusDecoder createDecoder(Format format) throws OpusDecoderException { protected OpusDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
throws OpusDecoderException {
return new OpusDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, return new OpusDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
format.initializationData); format.initializationData, mediaCrypto);
} }
} }
...@@ -16,9 +16,12 @@ ...@@ -16,9 +16,12 @@
package com.google.android.exoplayer2.ext.opus; package com.google.android.exoplayer2.ext.opus;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
import com.google.android.exoplayer2.drm.DecryptionException;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.util.List; import java.util.List;
...@@ -36,6 +39,12 @@ import java.util.List; ...@@ -36,6 +39,12 @@ import java.util.List;
*/ */
private static final int SAMPLE_RATE = 48000; private static final int SAMPLE_RATE = 48000;
private static final int NO_ERROR = 0;
private static final int DECODE_ERROR = -1;
private static final int DRM_ERROR = -2;
private final ExoMediaCrypto exoMediaCrypto;
private final int channelCount; private final int channelCount;
private final int headerSkipSamples; private final int headerSkipSamples;
private final int headerSeekPreRollSamples; private final int headerSeekPreRollSamples;
...@@ -52,14 +61,20 @@ import java.util.List; ...@@ -52,14 +61,20 @@ import java.util.List;
* @param initializationData Codec-specific initialization data. The first element must contain an * @param initializationData Codec-specific initialization data. The first element must contain an
* opus header. Optionally, the list may contain two additional buffers, which must contain * opus header. Optionally, the list may contain two additional buffers, which must contain
* the encoder delay and seek pre roll values in nanoseconds, encoded as longs. * the encoder delay and seek pre roll values in nanoseconds, encoded as longs.
* @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted
* content. Maybe null and can be ignored if decoder does not handle encrypted content.
* @throws OpusDecoderException Thrown if an exception occurs when initializing the decoder. * @throws OpusDecoderException Thrown if an exception occurs when initializing the decoder.
*/ */
public OpusDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize, public OpusDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
List<byte[]> initializationData) throws OpusDecoderException { List<byte[]> initializationData, ExoMediaCrypto exoMediaCrypto) throws OpusDecoderException {
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]); super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
if (!OpusLibrary.isAvailable()) { if (!OpusLibrary.isAvailable()) {
throw new OpusDecoderException("Failed to load decoder native libraries."); throw new OpusDecoderException("Failed to load decoder native libraries.");
} }
this.exoMediaCrypto = exoMediaCrypto;
if (exoMediaCrypto != null && !OpusLibrary.opusIsSecureDecodeSupported()) {
throw new OpusDecoderException("Opus decoder does not support secure decode.");
}
byte[] headerBytes = initializationData.get(0); byte[] headerBytes = initializationData.get(0);
if (headerBytes.length < 19) { if (headerBytes.length < 19) {
throw new OpusDecoderException("Header size is too small."); throw new OpusDecoderException("Header size is too small.");
...@@ -139,11 +154,25 @@ import java.util.List; ...@@ -139,11 +154,25 @@ import java.util.List;
skipSamples = (inputBuffer.timeUs == 0) ? headerSkipSamples : headerSeekPreRollSamples; skipSamples = (inputBuffer.timeUs == 0) ? headerSkipSamples : headerSeekPreRollSamples;
} }
ByteBuffer inputData = inputBuffer.data; ByteBuffer inputData = inputBuffer.data;
int result = opusDecode(nativeDecoderContext, inputBuffer.timeUs, inputData, inputData.limit(), CryptoInfo cryptoInfo = inputBuffer.cryptoInfo;
outputBuffer, SAMPLE_RATE); int result = inputBuffer.isEncrypted()
? opusSecureDecode(nativeDecoderContext, inputBuffer.timeUs, inputData, inputData.limit(),
outputBuffer, SAMPLE_RATE, exoMediaCrypto, cryptoInfo.mode,
cryptoInfo.key, cryptoInfo.iv, cryptoInfo.numSubSamples,
cryptoInfo.numBytesOfClearData, cryptoInfo.numBytesOfEncryptedData)
: opusDecode(nativeDecoderContext, inputBuffer.timeUs, inputData, inputData.limit(),
outputBuffer, SAMPLE_RATE);
if (result < 0) { if (result < 0) {
return new OpusDecoderException("Decode error: " + opusGetErrorMessage(result)); if (result == DRM_ERROR) {
String message = "Drm error: " + opusGetErrorMessage(nativeDecoderContext);
DecryptionException cause = new DecryptionException(
opusGetErrorCode(nativeDecoderContext), message);
return new OpusDecoderException(message, cause);
} else {
return new OpusDecoderException("Decode error: " + opusGetErrorMessage(result));
}
} }
ByteBuffer outputData = outputBuffer.data; ByteBuffer outputData = outputBuffer.data;
outputData.position(0); outputData.position(0);
outputData.limit(result); outputData.limit(result);
...@@ -182,8 +211,13 @@ import java.util.List; ...@@ -182,8 +211,13 @@ import java.util.List;
int gain, byte[] streamMap); int gain, byte[] streamMap);
private native int opusDecode(long decoder, long timeUs, ByteBuffer inputBuffer, int inputSize, private native int opusDecode(long decoder, long timeUs, ByteBuffer inputBuffer, int inputSize,
SimpleOutputBuffer outputBuffer, int sampleRate); SimpleOutputBuffer outputBuffer, int sampleRate);
private native int opusSecureDecode(long decoder, long timeUs, ByteBuffer inputBuffer,
int inputSize, SimpleOutputBuffer outputBuffer, int sampleRate,
ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv,
int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
private native void opusClose(long decoder); private native void opusClose(long decoder);
private native void opusReset(long decoder); private native void opusReset(long decoder);
private native String opusGetErrorMessage(int errorCode); private native int opusGetErrorCode(long decoder);
private native String opusGetErrorMessage(long decoder);
} }
...@@ -26,4 +26,8 @@ public final class OpusDecoderException extends AudioDecoderException { ...@@ -26,4 +26,8 @@ public final class OpusDecoderException extends AudioDecoderException {
super(message); super(message);
} }
/* package */ OpusDecoderException(String message, Throwable cause) {
super(message, cause);
}
} }
...@@ -50,5 +50,5 @@ public final class OpusLibrary { ...@@ -50,5 +50,5 @@ public final class OpusLibrary {
} }
public static native String opusGetVersion(); public static native String opusGetVersion();
public static native boolean opusIsSecureDecodeSupported();
} }
...@@ -60,11 +60,13 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { ...@@ -60,11 +60,13 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
static const int kBytesPerSample = 2; // opus fixed point uses 16 bit samples. static const int kBytesPerSample = 2; // opus fixed point uses 16 bit samples.
static int channelCount; static int channelCount;
static int errorCode;
DECODER_FUNC(jlong, opusInit, jint sampleRate, jint channelCount, DECODER_FUNC(jlong, opusInit, jint sampleRate, jint channelCount,
jint numStreams, jint numCoupled, jint gain, jbyteArray jStreamMap) { jint numStreams, jint numCoupled, jint gain, jbyteArray jStreamMap) {
int status = OPUS_INVALID_STATE; int status = OPUS_INVALID_STATE;
::channelCount = channelCount; ::channelCount = channelCount;
errorCode = 0;
jbyte* streamMapBytes = env->GetByteArrayElements(jStreamMap, 0); jbyte* streamMapBytes = env->GetByteArrayElements(jStreamMap, 0);
uint8_t* streamMap = reinterpret_cast<uint8_t*>(streamMapBytes); uint8_t* streamMap = reinterpret_cast<uint8_t*>(streamMapBytes);
OpusMSDecoder* decoder = opus_multistream_decoder_create( OpusMSDecoder* decoder = opus_multistream_decoder_create(
...@@ -109,10 +111,24 @@ DECODER_FUNC(jint, opusDecode, jlong jDecoder, jlong jTimeUs, ...@@ -109,10 +111,24 @@ DECODER_FUNC(jint, opusDecode, jlong jDecoder, jlong jTimeUs,
env->GetDirectBufferAddress(jOutputBufferData)); env->GetDirectBufferAddress(jOutputBufferData));
int sampleCount = opus_multistream_decode(decoder, inputBuffer, inputSize, int sampleCount = opus_multistream_decode(decoder, inputBuffer, inputSize,
outputBufferData, outputSize, 0); outputBufferData, outputSize, 0);
// record error code
errorCode = (sampleCount < 0) ? sampleCount : 0;
return (sampleCount < 0) ? sampleCount return (sampleCount < 0) ? sampleCount
: sampleCount * kBytesPerSample * channelCount; : sampleCount * kBytesPerSample * channelCount;
} }
DECODER_FUNC(jint, opusSecureDecode, jlong jDecoder, jlong jTimeUs,
jobject jInputBuffer, jint inputSize, jobject jOutputBuffer,
jint sampleRate, jobject mediaCrypto, jint inputMode, jbyteArray key,
jbyteArray javaIv, jint inputNumSubSamples, jintArray numBytesOfClearData,
jintArray numBytesOfEncryptedData) {
// Doesn't support
// Java client should have checked vpxSupportSecureDecode
// and avoid calling this
// return -2 (DRM Error)
return -2;
}
DECODER_FUNC(void, opusClose, jlong jDecoder) { DECODER_FUNC(void, opusClose, jlong jDecoder) {
OpusMSDecoder* decoder = reinterpret_cast<OpusMSDecoder*>(jDecoder); OpusMSDecoder* decoder = reinterpret_cast<OpusMSDecoder*>(jDecoder);
opus_multistream_decoder_destroy(decoder); opus_multistream_decoder_destroy(decoder);
...@@ -123,10 +139,19 @@ DECODER_FUNC(void, opusReset, jlong jDecoder) { ...@@ -123,10 +139,19 @@ DECODER_FUNC(void, opusReset, jlong jDecoder) {
opus_multistream_decoder_ctl(decoder, OPUS_RESET_STATE); opus_multistream_decoder_ctl(decoder, OPUS_RESET_STATE);
} }
DECODER_FUNC(jstring, opusGetErrorMessage, jint errorCode) { DECODER_FUNC(jstring, opusGetErrorMessage, jlong jContext) {
return env->NewStringUTF(opus_strerror(errorCode)); return env->NewStringUTF(opus_strerror(errorCode));
} }
DECODER_FUNC(jint, opusGetErrorCode, jlong jContext) {
return errorCode;
}
LIBRARY_FUNC(jstring, opusIsSecureDecodeSupported) {
// Doesn't support
return 0;
}
LIBRARY_FUNC(jstring, opusGetVersion) { LIBRARY_FUNC(jstring, opusGetVersion) {
return env->NewStringUTF(opus_get_version_string()); return env->NewStringUTF(opus_get_version_string());
} }
...@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.vp9; ...@@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.vp9;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
...@@ -27,7 +26,9 @@ import com.google.android.exoplayer2.Renderer; ...@@ -27,7 +26,9 @@ import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
/** /**
...@@ -88,7 +89,7 @@ public class VpxPlaybackTest extends InstrumentationTestCase { ...@@ -88,7 +89,7 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
public void run() { public void run() {
Looper.prepare(); Looper.prepare();
LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(true, 0); LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(true, 0);
DefaultTrackSelector trackSelector = new DefaultTrackSelector(new Handler()); DefaultTrackSelector trackSelector = new DefaultTrackSelector();
player = ExoPlayerFactory.newInstance(new Renderer[] {videoRenderer}, trackSelector); player = ExoPlayerFactory.newInstance(new Renderer[] {videoRenderer}, trackSelector);
player.addListener(this); player.addListener(this);
ExtractorMediaSource mediaSource = new ExtractorMediaSource( ExtractorMediaSource mediaSource = new ExtractorMediaSource(
...@@ -111,6 +112,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase { ...@@ -111,6 +112,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
} }
@Override @Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
// Do nothing.
}
@Override
public void onPositionDiscontinuity() { public void onPositionDiscontinuity() {
// Do nothing. // Do nothing.
} }
......
...@@ -16,8 +16,11 @@ ...@@ -16,8 +16,11 @@
package com.google.android.exoplayer2.ext.vp9; package com.google.android.exoplayer2.ext.vp9;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.drm.DecryptionException;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
/** /**
...@@ -30,6 +33,11 @@ import java.nio.ByteBuffer; ...@@ -30,6 +33,11 @@ import java.nio.ByteBuffer;
public static final int OUTPUT_MODE_YUV = 0; public static final int OUTPUT_MODE_YUV = 0;
public static final int OUTPUT_MODE_RGB = 1; public static final int OUTPUT_MODE_RGB = 1;
private static final int NO_ERROR = 0;
private static final int DECODE_ERROR = 1;
private static final int DRM_ERROR = 2;
private final ExoMediaCrypto exoMediaCrypto;
private final long vpxDecContext; private final long vpxDecContext;
private volatile int outputMode; private volatile int outputMode;
...@@ -40,14 +48,20 @@ import java.nio.ByteBuffer; ...@@ -40,14 +48,20 @@ import java.nio.ByteBuffer;
* @param numInputBuffers The number of input buffers. * @param numInputBuffers The number of input buffers.
* @param numOutputBuffers The number of output buffers. * @param numOutputBuffers The number of output buffers.
* @param initialInputBufferSize The initial size of each input buffer. * @param initialInputBufferSize The initial size of each input buffer.
* @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted
* content. Maybe null and can be ignored if decoder does not handle encrypted content.
* @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder. * @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder.
*/ */
public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize) public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
throws VpxDecoderException { ExoMediaCrypto exoMediaCrypto) throws VpxDecoderException {
super(new DecoderInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]); super(new DecoderInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
if (!VpxLibrary.isAvailable()) { if (!VpxLibrary.isAvailable()) {
throw new VpxDecoderException("Failed to load decoder native libraries."); throw new VpxDecoderException("Failed to load decoder native libraries.");
} }
this.exoMediaCrypto = exoMediaCrypto;
if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) {
throw new VpxDecoderException("Vpx decoder does not support secure decode.");
}
vpxDecContext = vpxInit(); vpxDecContext = vpxInit();
if (vpxDecContext == 0) { if (vpxDecContext == 0) {
throw new VpxDecoderException("Failed to initialize decoder"); throw new VpxDecoderException("Failed to initialize decoder");
...@@ -90,12 +104,29 @@ import java.nio.ByteBuffer; ...@@ -90,12 +104,29 @@ import java.nio.ByteBuffer;
boolean reset) { boolean reset) {
ByteBuffer inputData = inputBuffer.data; ByteBuffer inputData = inputBuffer.data;
int inputSize = inputData.limit(); int inputSize = inputData.limit();
if (vpxDecode(vpxDecContext, inputData, inputSize) != 0) { CryptoInfo cryptoInfo = inputBuffer.cryptoInfo;
return new VpxDecoderException("Decode error: " + vpxGetErrorMessage(vpxDecContext)); final long result = inputBuffer.isEncrypted()
? vpxSecureDecode(vpxDecContext, inputData, inputSize, exoMediaCrypto,
cryptoInfo.mode, cryptoInfo.key, cryptoInfo.iv, cryptoInfo.numSubSamples,
cryptoInfo.numBytesOfClearData, cryptoInfo.numBytesOfEncryptedData)
: vpxDecode(vpxDecContext, inputData, inputSize);
if (result != NO_ERROR) {
if (result == DRM_ERROR) {
String message = "Drm error: " + vpxGetErrorMessage(vpxDecContext);
DecryptionException cause = new DecryptionException(
vpxGetErrorCode(vpxDecContext), message);
return new VpxDecoderException(message, cause);
} else {
return new VpxDecoderException("Decode error: " + vpxGetErrorMessage(vpxDecContext));
}
} }
outputBuffer.init(inputBuffer.timeUs, outputMode); outputBuffer.init(inputBuffer.timeUs, outputMode);
if (vpxGetFrame(vpxDecContext, outputBuffer) != 0) { int getFrameResult = vpxGetFrame(vpxDecContext, outputBuffer);
if (getFrameResult == 1) {
outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
} else if (getFrameResult == -1) {
return new VpxDecoderException("Buffer initialization failed.");
} }
return null; return null;
} }
...@@ -109,7 +140,11 @@ import java.nio.ByteBuffer; ...@@ -109,7 +140,11 @@ import java.nio.ByteBuffer;
private native long vpxInit(); private native long vpxInit();
private native long vpxClose(long context); private native long vpxClose(long context);
private native long vpxDecode(long context, ByteBuffer encoded, int length); private native long vpxDecode(long context, ByteBuffer encoded, int length);
private native long vpxSecureDecode(long context, ByteBuffer encoded, int length,
ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv,
int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer); private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer);
private native int vpxGetErrorCode(long context);
private native String vpxGetErrorMessage(long context); private native String vpxGetErrorMessage(long context);
} }
...@@ -20,8 +20,11 @@ package com.google.android.exoplayer2.ext.vp9; ...@@ -20,8 +20,11 @@ package com.google.android.exoplayer2.ext.vp9;
*/ */
public class VpxDecoderException extends Exception { public class VpxDecoderException extends Exception {
/* package */ VpxDecoderException(String message) { /* package */ VpxDecoderException(String message) {
super(message); super(message);
} }
/* package */ VpxDecoderException(String message, Throwable cause) {
super(message, cause);
}
} }
...@@ -59,5 +59,5 @@ public final class VpxLibrary { ...@@ -59,5 +59,5 @@ public final class VpxLibrary {
private static native String vpxGetVersion(); private static native String vpxGetVersion();
private static native String vpxGetBuildConfig(); private static native String vpxGetBuildConfig();
public static native boolean vpxIsSecureDecodeSupported();
} }
...@@ -66,28 +66,39 @@ import java.nio.ByteBuffer; ...@@ -66,28 +66,39 @@ import java.nio.ByteBuffer;
/** /**
* Resizes the buffer based on the given dimensions. Called via JNI after decoding completes. * Resizes the buffer based on the given dimensions. Called via JNI after decoding completes.
* @return Whether the buffer was resized successfully.
*/ */
public void initForRgbFrame(int width, int height) { public boolean initForRgbFrame(int width, int height) {
this.width = width; this.width = width;
this.height = height; this.height = height;
this.yuvPlanes = null; this.yuvPlanes = null;
if (!isSafeToMultiply(width, height) || !isSafeToMultiply(width * height, 2)) {
return false;
}
int minimumRgbSize = width * height * 2; int minimumRgbSize = width * height * 2;
initData(minimumRgbSize); initData(minimumRgbSize);
return true;
} }
/** /**
* Resizes the buffer based on the given stride. Called via JNI after decoding completes. * Resizes the buffer based on the given stride. Called via JNI after decoding completes.
* @return Whether the buffer was resized successfully.
*/ */
public void initForYuvFrame(int width, int height, int yStride, int uvStride, public boolean initForYuvFrame(int width, int height, int yStride, int uvStride,
int colorspace) { int colorspace) {
this.width = width; this.width = width;
this.height = height; this.height = height;
this.colorspace = colorspace; this.colorspace = colorspace;
int uvHeight = (int) (((long) height + 1) / 2);
if (!isSafeToMultiply(yStride, height) || !isSafeToMultiply(uvStride, uvHeight)) {
return false;
}
int yLength = yStride * height; int yLength = yStride * height;
int uvLength = uvStride * ((height + 1) / 2); int uvLength = uvStride * uvHeight;
int minimumYuvSize = yLength + (uvLength * 2); int minimumYuvSize = yLength + (uvLength * 2);
if (!isSafeToMultiply(uvLength, 2) || minimumYuvSize < yLength) {
return false;
}
initData(minimumYuvSize); initData(minimumYuvSize);
if (yuvPlanes == null) { if (yuvPlanes == null) {
...@@ -108,6 +119,7 @@ import java.nio.ByteBuffer; ...@@ -108,6 +119,7 @@ import java.nio.ByteBuffer;
yuvStrides[0] = yStride; yuvStrides[0] = yStride;
yuvStrides[1] = uvStride; yuvStrides[1] = uvStride;
yuvStrides[2] = uvStride; yuvStrides[2] = uvStride;
return true;
} }
private void initData(int size) { private void initData(int size) {
...@@ -119,4 +131,12 @@ import java.nio.ByteBuffer; ...@@ -119,4 +131,12 @@ import java.nio.ByteBuffer;
} }
} }
/**
* Ensures that the result of multiplying individual numbers can fit into the size limit of an
* integer.
*/
private boolean isSafeToMultiply(int a, int b) {
return a >= 0 && b >= 0 && !(b > 0 && a >= Integer.MAX_VALUE / b);
}
} }
...@@ -73,10 +73,13 @@ import javax.microedition.khronos.opengles.GL10; ...@@ -73,10 +73,13 @@ import javax.microedition.khronos.opengles.GL10;
private final int[] yuvTextures = new int[3]; private final int[] yuvTextures = new int[3];
private final AtomicReference<VpxOutputBuffer> pendingOutputBufferReference; private final AtomicReference<VpxOutputBuffer> pendingOutputBufferReference;
// Kept in a field rather than a local variable so that it doesn't get garbage collected before
// glDrawArrays uses it.
@SuppressWarnings("FieldCanBeLocal")
private FloatBuffer textureCoords;
private int program; private int program;
private int texLocation; private int texLocation;
private int colorMatrixLocation; private int colorMatrixLocation;
private FloatBuffer textureCoords;
private int previousWidth; private int previousWidth;
private int previousStride; private int previousStride;
......
...@@ -59,6 +59,7 @@ static jmethodID initForRgbFrame; ...@@ -59,6 +59,7 @@ static jmethodID initForRgbFrame;
static jmethodID initForYuvFrame; static jmethodID initForYuvFrame;
static jfieldID dataField; static jfieldID dataField;
static jfieldID outputModeField; static jfieldID outputModeField;
static int errorCode;
jint JNI_OnLoad(JavaVM* vm, void* reserved) { jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env; JNIEnv* env;
...@@ -72,6 +73,7 @@ DECODER_FUNC(jlong, vpxInit) { ...@@ -72,6 +73,7 @@ DECODER_FUNC(jlong, vpxInit) {
vpx_codec_ctx_t* context = new vpx_codec_ctx_t(); vpx_codec_ctx_t* context = new vpx_codec_ctx_t();
vpx_codec_dec_cfg_t cfg = {0, 0, 0}; vpx_codec_dec_cfg_t cfg = {0, 0, 0};
cfg.threads = android_getCpuCount(); cfg.threads = android_getCpuCount();
errorCode = 0;
if (vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo, &cfg, 0)) { if (vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo, &cfg, 0)) {
LOGE("ERROR: Fail to initialize libvpx decoder."); LOGE("ERROR: Fail to initialize libvpx decoder.");
return 0; return 0;
...@@ -81,9 +83,9 @@ DECODER_FUNC(jlong, vpxInit) { ...@@ -81,9 +83,9 @@ DECODER_FUNC(jlong, vpxInit) {
const jclass outputBufferClass = env->FindClass( const jclass outputBufferClass = env->FindClass(
"com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer"); "com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer");
initForYuvFrame = env->GetMethodID(outputBufferClass, "initForYuvFrame", initForYuvFrame = env->GetMethodID(outputBufferClass, "initForYuvFrame",
"(IIIII)V"); "(IIIII)Z");
initForRgbFrame = env->GetMethodID(outputBufferClass, "initForRgbFrame", initForRgbFrame = env->GetMethodID(outputBufferClass, "initForRgbFrame",
"(II)V"); "(II)Z");
dataField = env->GetFieldID(outputBufferClass, "data", dataField = env->GetFieldID(outputBufferClass, "data",
"Ljava/nio/ByteBuffer;"); "Ljava/nio/ByteBuffer;");
outputModeField = env->GetFieldID(outputBufferClass, "mode", "I"); outputModeField = env->GetFieldID(outputBufferClass, "mode", "I");
...@@ -97,13 +99,26 @@ DECODER_FUNC(jlong, vpxDecode, jlong jContext, jobject encoded, jint len) { ...@@ -97,13 +99,26 @@ DECODER_FUNC(jlong, vpxDecode, jlong jContext, jobject encoded, jint len) {
reinterpret_cast<const uint8_t*>(env->GetDirectBufferAddress(encoded)); reinterpret_cast<const uint8_t*>(env->GetDirectBufferAddress(encoded));
const vpx_codec_err_t status = const vpx_codec_err_t status =
vpx_codec_decode(context, buffer, len, NULL, 0); vpx_codec_decode(context, buffer, len, NULL, 0);
errorCode = 0;
if (status != VPX_CODEC_OK) { if (status != VPX_CODEC_OK) {
LOGE("ERROR: vpx_codec_decode() failed, status= %d", status); LOGE("ERROR: vpx_codec_decode() failed, status= %d", status);
errorCode = status;
return -1; return -1;
} }
return 0; return 0;
} }
DECODER_FUNC(jlong, vpxSecureDecode, jlong jContext, jobject encoded, jint len,
jobject mediaCrypto, jint inputMode, jbyteArray&, jbyteArray&,
jint inputNumSubSamples, jintArray numBytesOfClearData,
jintArray numBytesOfEncryptedData) {
// Doesn't support
// Java client should have checked vpxSupportSecureDecode
// and avoid calling this
// return -2 (DRM Error)
return -2;
}
DECODER_FUNC(jlong, vpxClose, jlong jContext) { DECODER_FUNC(jlong, vpxClose, jlong jContext) {
vpx_codec_ctx_t* const context = reinterpret_cast<vpx_codec_ctx_t*>(jContext); vpx_codec_ctx_t* const context = reinterpret_cast<vpx_codec_ctx_t*>(jContext);
vpx_codec_destroy(context); vpx_codec_destroy(context);
...@@ -126,7 +141,11 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { ...@@ -126,7 +141,11 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
int outputMode = env->GetIntField(jOutputBuffer, outputModeField); int outputMode = env->GetIntField(jOutputBuffer, outputModeField);
if (outputMode == kOutputModeRgb) { if (outputMode == kOutputModeRgb) {
// resize buffer if required. // resize buffer if required.
env->CallVoidMethod(jOutputBuffer, initForRgbFrame, img->d_w, img->d_h); jboolean initResult = env->CallBooleanMethod(jOutputBuffer, initForRgbFrame,
img->d_w, img->d_h);
if (initResult == JNI_FALSE) {
return -1;
}
// get pointer to the data buffer. // get pointer to the data buffer.
const jobject dataObject = env->GetObjectField(jOutputBuffer, dataField); const jobject dataObject = env->GetObjectField(jOutputBuffer, dataField);
...@@ -155,9 +174,12 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { ...@@ -155,9 +174,12 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
} }
// resize buffer if required. // resize buffer if required.
env->CallVoidMethod(jOutputBuffer, initForYuvFrame, img->d_w, img->d_h, jboolean initResult = env->CallBooleanMethod(
img->stride[VPX_PLANE_Y], img->stride[VPX_PLANE_U], jOutputBuffer, initForYuvFrame, img->d_w, img->d_h,
colorspace); img->stride[VPX_PLANE_Y], img->stride[VPX_PLANE_U], colorspace);
if (initResult == JNI_FALSE) {
return -1;
}
// get pointer to the data buffer. // get pointer to the data buffer.
const jobject dataObject = env->GetObjectField(jOutputBuffer, dataField); const jobject dataObject = env->GetObjectField(jOutputBuffer, dataField);
...@@ -181,6 +203,15 @@ DECODER_FUNC(jstring, vpxGetErrorMessage, jlong jContext) { ...@@ -181,6 +203,15 @@ DECODER_FUNC(jstring, vpxGetErrorMessage, jlong jContext) {
return env->NewStringUTF(vpx_codec_error(context)); return env->NewStringUTF(vpx_codec_error(context));
} }
DECODER_FUNC(jint, vpxGetErrorCode, jlong jContext) {
return errorCode;
}
LIBRARY_FUNC(jstring, vpxIsSecureDecodeSupported) {
// Doesn't support
return 0;
}
LIBRARY_FUNC(jstring, vpxGetVersion) { LIBRARY_FUNC(jstring, vpxGetVersion) {
return env->NewStringUTF(vpx_codec_version_str()); return env->NewStringUTF(vpx_codec_version_str());
} }
......
#Thu Sep 01 11:39:15 BST 2016 #Mon Oct 24 14:40:37 BST 2016
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
...@@ -35,9 +35,11 @@ android { ...@@ -35,9 +35,11 @@ android {
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
} }
debug { // Re-enable test coverage when the following issue is fixed:
testCoverageEnabled = true // https://code.google.com/p/android/issues/detail?id=226070
} // debug {
// testCoverageEnabled = true
// }
} }
lintOptions { lintOptions {
...@@ -55,7 +57,7 @@ dependencies { ...@@ -55,7 +57,7 @@ dependencies {
androidTestCompile 'com.google.dexmaker:dexmaker:1.2' androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
androidTestCompile 'org.mockito:mockito-core:1.9.5' androidTestCompile 'org.mockito:mockito-core:1.9.5'
compile 'com.android.support:support-annotations:24.2.0' compile 'com.android.support:support-annotations:25.0.1'
} }
android.libraryVariants.all { variant -> android.libraryVariants.all { variant ->
......
<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:mpeg:DASH:schema:MPD:2011" xmlns:yt="http://youtube.com/yt/2012/10/10" xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd" minBufferTime="PT1.500S" profiles="urn:mpeg:dash:profile:isoff-main:2011" type="dynamic" availabilityStartTime="2016-10-14T17:00:17" timeShiftBufferDepth="PT7200.000S" minimumUpdatePeriod="PT2.000S" yt:earliestMediaSequence="0" yt:mpdRequestTime="2016-10-14T18:29:17.082" yt:mpdResponseTime="2016-10-14T18:29:17.194">
<Period start="PT0.000S" yt:segmentIngestTime="2016-10-14T17:00:14.257">
<SegmentTemplate startNumber="0" timescale="1000" media="sq/$Number$">
<SegmentTimeline>
<S d="2002" t="6009" r="2"/>
<S d="1985"/>
<S d="2000"/>
</SegmentTimeline>
</SegmentTemplate>
<AdaptationSet id="0" mimeType="audio/mp4" subsegmentAlignment="true">
<Role schemeIdUri="urn:mpeg:DASH:role:2011" value="main"/>
<Representation id="140" codecs="mp4a.40.2" audioSamplingRate="48000" startWithSAP="1" bandwidth="144000">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
<BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/140/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/audio%2Fmp4/live/1/gir/yes/noclen/1/signature/B5137EA0CC278C07DD056D204E863CC81EDEB39E.1AD5D242EBC94922EDA7165353A89A5E08A4103A/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>
</Representation>
</AdaptationSet>
<AdaptationSet id="1" mimeType="video/mp4" subsegmentAlignment="true">
<Role schemeIdUri="urn:mpeg:DASH:role:2011" value="main"/>
<Representation id="133" codecs="avc1.4d4015" width="426" height="240" startWithSAP="1" maxPlayoutRate="1" bandwidth="258000" frameRate="30">
<BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/133/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/90154AE9C5C9D9D519CBF2E43AB0A1778375992D.40E2E855ADFB38FA7E95E168FEEEA6796B080BD7/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>
</Representation>
<Representation id="134" codecs="avc1.4d401e" width="640" height="360" startWithSAP="1" maxPlayoutRate="1" bandwidth="646000" frameRate="30">
<BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/134/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/5C094AEFDCEB1A4D2F3C05F8BD095C336EF0E1C3.7AE6B9951B0237AAE6F031927AACAC4974BAFFAA/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>
</Representation>
<Representation id="135" codecs="avc1.4d401f" width="854" height="480" startWithSAP="1" maxPlayoutRate="1" bandwidth="1171000" frameRate="30">
<BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/135/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/1F7660CA4E5B4AE4D60E18795680E34CDD2EF3C9.800B0A1D5F490DE142CCF4C88C64FD21D42129/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>
</Representation>
<Representation id="160" codecs="avc1.42c00b" width="256" height="144" startWithSAP="1" maxPlayoutRate="1" bandwidth="124000" frameRate="30">
<BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/160/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/94EB61673784DF0C4237A1A866F2E171C8A64ADB.AEC00AA06C2278FEA8702FB62693B70D8977F46C/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>
</Representation>
<Representation id="136" codecs="avc1.4d401f" width="1280" height="720" startWithSAP="1" maxPlayoutRate="1" bandwidth="2326000" frameRate="30">
<BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/136/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/6D8C34FC30A1F1A4F700B61180D1C4CCF6274844.29EBCB4A837DE626C52C66CF650519E61C2FF0BF/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>
</Representation>
</AdaptationSet>
</Period>
</MPD>
...@@ -22,7 +22,7 @@ track 0: ...@@ -22,7 +22,7 @@ track 0:
encoderPadding = -1 encoderPadding = -1
subsampleOffsetUs = 9223372036854775807 subsampleOffsetUs = 9223372036854775807
selectionFlags = 0 selectionFlags = 0
language = und language = null
drmInitData = - drmInitData = -
initializationData: initializationData:
data = length 19, hash BFE794DB data = length 19, hash BFE794DB
......
...@@ -22,7 +22,7 @@ track 0: ...@@ -22,7 +22,7 @@ track 0:
encoderPadding = -1 encoderPadding = -1
subsampleOffsetUs = 9223372036854775807 subsampleOffsetUs = 9223372036854775807
selectionFlags = 0 selectionFlags = 0
language = und language = null
drmInitData = - drmInitData = -
initializationData: initializationData:
data = length 19, hash BFE794DB data = length 19, hash BFE794DB
......
...@@ -22,7 +22,7 @@ track 0: ...@@ -22,7 +22,7 @@ track 0:
encoderPadding = -1 encoderPadding = -1
subsampleOffsetUs = 9223372036854775807 subsampleOffsetUs = 9223372036854775807
selectionFlags = 0 selectionFlags = 0
language = und language = null
drmInitData = - drmInitData = -
initializationData: initializationData:
data = length 19, hash BFE794DB data = length 19, hash BFE794DB
......
...@@ -22,7 +22,7 @@ track 0: ...@@ -22,7 +22,7 @@ track 0:
encoderPadding = -1 encoderPadding = -1
subsampleOffsetUs = 9223372036854775807 subsampleOffsetUs = 9223372036854775807
selectionFlags = 0 selectionFlags = 0
language = und language = null
drmInitData = - drmInitData = -
initializationData: initializationData:
data = length 19, hash BFE794DB data = length 19, hash BFE794DB
......
...@@ -22,7 +22,7 @@ track 0: ...@@ -22,7 +22,7 @@ track 0:
encoderPadding = -1 encoderPadding = -1
subsampleOffsetUs = 9223372036854775807 subsampleOffsetUs = 9223372036854775807
selectionFlags = 0 selectionFlags = 0
language = und language = null
drmInitData = - drmInitData = -
initializationData: initializationData:
data = length 19, hash BFE794DB data = length 19, hash BFE794DB
......
1
-0:00:04,567 --> -0:00:03,456
This is the first subtitle.
2
-00:00:02,345 --> 00:00:01,234
This is the second subtitle.
Second subtitle with second line.
3
00:00:04,567 --> 00:00:08,901
This is the third subtitle.
...@@ -24,6 +24,8 @@ import android.annotation.TargetApi; ...@@ -24,6 +24,8 @@ import android.annotation.TargetApi;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.os.Parcel; import android.os.Parcel;
import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
...@@ -56,11 +58,14 @@ public final class FormatTest extends TestCase { ...@@ -56,11 +58,14 @@ public final class FormatTest extends TestCase {
TestUtil.buildTestData(128, 1 /* data seed */)); TestUtil.buildTestData(128, 1 /* data seed */));
DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2); DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2);
byte[] projectionData = new byte[] {1, 2, 3}; byte[] projectionData = new byte[] {1, 2, 3};
Metadata metadata = new Metadata(
new TextInformationFrame("id1", "description1"),
new TextInformationFrame("id2", "description2"));
Format formatToParcel = new Format("id", MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null, Format formatToParcel = new Format("id", MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null,
1024, 2048, 1920, 1080, 24, 90, 2, projectionData, C.STEREO_MODE_TOP_BOTTOM, 6, 44100, 1024, 2048, 1920, 1080, 24, 90, 2, projectionData, C.STEREO_MODE_TOP_BOTTOM, 6, 44100,
C.ENCODING_PCM_24BIT, 1001, 1002, 0, "und", Format.OFFSET_SAMPLE_RELATIVE, INIT_DATA, C.ENCODING_PCM_24BIT, 1001, 1002, 0, "und", Format.NO_VALUE, Format.OFFSET_SAMPLE_RELATIVE,
drmInitData); INIT_DATA, drmInitData, metadata);
Parcel parcel = Parcel.obtain(); Parcel parcel = Parcel.obtain();
formatToParcel.writeToParcel(parcel, 0); formatToParcel.writeToParcel(parcel, 0);
......
...@@ -27,9 +27,9 @@ import junit.framework.TestCase; ...@@ -27,9 +27,9 @@ import junit.framework.TestCase;
*/ */
public final class DefaultOggSeekerTest extends TestCase { public final class DefaultOggSeekerTest extends TestCase {
public void testSetupUnboundAudioLength() { public void testSetupWithUnsetEndPositionFails() {
try { try {
new DefaultOggSeeker(0, C.LENGTH_UNSET, new TestStreamReader()); new DefaultOggSeeker(0, C.LENGTH_UNSET, new TestStreamReader(), 1, 1);
fail(); fail();
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
// ignored // ignored
...@@ -43,11 +43,12 @@ public final class DefaultOggSeekerTest extends TestCase { ...@@ -43,11 +43,12 @@ public final class DefaultOggSeekerTest extends TestCase {
} }
} }
public void testSeeking(Random random) throws IOException, InterruptedException { private void testSeeking(Random random) throws IOException, InterruptedException {
OggTestFile testFile = OggTestFile.generate(random, 1000); OggTestFile testFile = OggTestFile.generate(random, 1000);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(testFile.data).build(); FakeExtractorInput input = new FakeExtractorInput.Builder().setData(testFile.data).build();
TestStreamReader streamReader = new TestStreamReader(); TestStreamReader streamReader = new TestStreamReader();
DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, testFile.data.length, streamReader); DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, testFile.data.length, streamReader,
testFile.firstPayloadPageSize, testFile.firstPayloadPageGranulePosition);
OggPageHeader pageHeader = new OggPageHeader(); OggPageHeader pageHeader = new OggPageHeader();
while (true) { while (true) {
...@@ -109,8 +110,8 @@ public final class DefaultOggSeekerTest extends TestCase { ...@@ -109,8 +110,8 @@ public final class DefaultOggSeekerTest extends TestCase {
long granuleDiff = currentGranule - targetGranule; long granuleDiff = currentGranule - targetGranule;
if ((granuleDiff > DefaultOggSeeker.MATCH_RANGE || granuleDiff < 0) if ((granuleDiff > DefaultOggSeeker.MATCH_RANGE || granuleDiff < 0)
&& positionDiff > DefaultOggSeeker.MATCH_BYTE_RANGE) { && positionDiff > DefaultOggSeeker.MATCH_BYTE_RANGE) {
fail(String.format("granuleDiff (%d) or positionDiff (%d) is more than allowed.", fail("granuleDiff (" + granuleDiff + ") or positionDiff (" + positionDiff
granuleDiff, positionDiff)); + ") is more than allowed.");
} }
} }
} }
......
...@@ -28,8 +28,8 @@ import junit.framework.TestCase; ...@@ -28,8 +28,8 @@ import junit.framework.TestCase;
*/ */
public class DefaultOggSeekerUtilMethodsTest extends TestCase { public class DefaultOggSeekerUtilMethodsTest extends TestCase {
private Random random = new Random(0); private final Random random = new Random(0);
public void testSkipToNextPage() throws Exception { public void testSkipToNextPage() throws Exception {
FakeExtractorInput extractorInput = TestData.createInput( FakeExtractorInput extractorInput = TestData.createInput(
TestUtil.joinByteArrays( TestUtil.joinByteArrays(
...@@ -75,7 +75,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { ...@@ -75,7 +75,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
private static void skipToNextPage(ExtractorInput extractorInput) private static void skipToNextPage(ExtractorInput extractorInput)
throws IOException, InterruptedException { throws IOException, InterruptedException {
DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, extractorInput.getLength(), DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, extractorInput.getLength(),
new FlacReader()); new FlacReader(), 1, 2);
while (true) { while (true) {
try { try {
oggSeeker.skipToNextPage(extractorInput); oggSeeker.skipToNextPage(extractorInput);
...@@ -143,7 +143,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { ...@@ -143,7 +143,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
private void skipToPageOfGranule(ExtractorInput input, long granule, private void skipToPageOfGranule(ExtractorInput input, long granule,
long elapsedSamplesExpected) throws IOException, InterruptedException { long elapsedSamplesExpected) throws IOException, InterruptedException {
DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, input.getLength(), new FlacReader()); DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, input.getLength(), new FlacReader(), 1, 2);
while (true) { while (true) {
try { try {
assertEquals(elapsedSamplesExpected, oggSeeker.skipToPageOfGranule(input, granule, -1)); assertEquals(elapsedSamplesExpected, oggSeeker.skipToPageOfGranule(input, granule, -1));
...@@ -193,7 +193,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase { ...@@ -193,7 +193,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
private void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected) private void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected)
throws IOException, InterruptedException { throws IOException, InterruptedException {
DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, input.getLength(), new FlacReader()); DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, input.getLength(), new FlacReader(), 1, 2);
while (true) { while (true) {
try { try {
assertEquals(expected, oggSeeker.readGranuleOfLastPage(input)); assertEquals(expected, oggSeeker.readGranuleOfLastPage(input));
......
...@@ -25,20 +25,25 @@ import junit.framework.Assert; ...@@ -25,20 +25,25 @@ import junit.framework.Assert;
*/ */
/* package */ final class OggTestFile { /* package */ final class OggTestFile {
public static final int MAX_PACKET_LENGTH = 2048; private static final int MAX_PACKET_LENGTH = 2048;
public static final int MAX_SEGMENT_COUNT = 10; private static final int MAX_SEGMENT_COUNT = 10;
public static final int MAX_GRANULES_IN_PAGE = 100000; private static final int MAX_GRANULES_IN_PAGE = 100000;
byte[] data; public final byte[] data;
long lastGranule; public final long lastGranule;
int packetCount; public final int packetCount;
int pageCount; public final int pageCount;
public final int firstPayloadPageSize;
public final long firstPayloadPageGranulePosition;
private OggTestFile(byte[] data, long lastGranule, int packetCount, int pageCount) { private OggTestFile(byte[] data, long lastGranule, int packetCount, int pageCount,
int firstPayloadPageSize, long firstPayloadPageGranulePosition) {
this.data = data; this.data = data;
this.lastGranule = lastGranule; this.lastGranule = lastGranule;
this.packetCount = packetCount; this.packetCount = packetCount;
this.pageCount = pageCount; this.pageCount = pageCount;
this.firstPayloadPageSize = firstPayloadPageSize;
this.firstPayloadPageGranulePosition = firstPayloadPageGranulePosition;
} }
public static OggTestFile generate(Random random, int pageCount) { public static OggTestFile generate(Random random, int pageCount) {
...@@ -47,6 +52,8 @@ import junit.framework.Assert; ...@@ -47,6 +52,8 @@ import junit.framework.Assert;
long granule = 0; long granule = 0;
int packetLength = -1; int packetLength = -1;
int packetCount = 0; int packetCount = 0;
int firstPayloadPageSize = 0;
long firstPayloadPageGranulePosition = 0;
for (int i = 0; i < pageCount; i++) { for (int i = 0; i < pageCount; i++) {
int headerType = 0x00; int headerType = 0x00;
...@@ -89,6 +96,10 @@ import junit.framework.Assert; ...@@ -89,6 +96,10 @@ import junit.framework.Assert;
byte[] payload = TestUtil.buildTestData(bodySize, random); byte[] payload = TestUtil.buildTestData(bodySize, random);
fileData.add(payload); fileData.add(payload);
fileSize += payload.length; fileSize += payload.length;
if (i == 0) {
firstPayloadPageSize = header.length + bodySize;
firstPayloadPageGranulePosition = granule;
}
} }
byte[] file = new byte[fileSize]; byte[] file = new byte[fileSize];
...@@ -97,7 +108,8 @@ import junit.framework.Assert; ...@@ -97,7 +108,8 @@ import junit.framework.Assert;
System.arraycopy(data, 0, file, position, data.length); System.arraycopy(data, 0, file, position, data.length);
position += data.length; position += data.length;
} }
return new OggTestFile(file, granule, packetCount, pageCount); return new OggTestFile(file, granule, packetCount, pageCount, firstPayloadPageSize,
firstPayloadPageGranulePosition);
} }
public int findPreviousPageStart(long position) { public int findPreviousPageStart(long position) {
......
...@@ -17,8 +17,10 @@ package com.google.android.exoplayer2.extractor.rawcc; ...@@ -17,8 +17,10 @@ package com.google.android.exoplayer2.extractor.rawcc;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.MimeTypes;
/** /**
* Tests for {@link RawCcExtractor}. * Tests for {@link RawCcExtractor}.
...@@ -27,12 +29,15 @@ import com.google.android.exoplayer2.testutil.TestUtil; ...@@ -27,12 +29,15 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class RawCcExtractorTest extends InstrumentationTestCase { public final class RawCcExtractorTest extends InstrumentationTestCase {
public void testRawCcSample() throws Exception { public void testRawCcSample() throws Exception {
TestUtil.assertOutput(new TestUtil.ExtractorFactory() { TestUtil.assertOutput(
@Override new TestUtil.ExtractorFactory() {
public Extractor create() { @Override
return new RawCcExtractor(); public Extractor create() {
} return new RawCcExtractor(
}, "rawcc/sample.rawcc", getInstrumentation()); Format.createTextContainerFormat(null, null, MimeTypes.APPLICATION_CEA608,
"cea608", Format.NO_VALUE, 0, null, 1));
}
}, "rawcc/sample.rawcc", getInstrumentation());
} }
} }
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.TrackIdGenerator; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
import com.google.android.exoplayer2.testutil.FakeTrackOutput; import com.google.android.exoplayer2.testutil.FakeTrackOutput;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
...@@ -52,7 +52,7 @@ public class AdtsReaderTest extends TestCase { ...@@ -52,7 +52,7 @@ public class AdtsReaderTest extends TestCase {
public static final byte[] ADTS_CONTENT = TestUtil.createByteArray( public static final byte[] ADTS_CONTENT = TestUtil.createByteArray(
0x20, 0x00, 0x20, 0x00, 0x00, 0x80, 0x0e); 0x20, 0x00, 0x20, 0x00, 0x00, 0x80, 0x0e);
private static final byte TEST_DATA[] = TestUtil.joinByteArrays( private static final byte[] TEST_DATA = TestUtil.joinByteArrays(
ID3_DATA_1, ID3_DATA_1,
ID3_DATA_2, ID3_DATA_2,
ADTS_HEADER, ADTS_HEADER,
...@@ -73,7 +73,7 @@ public class AdtsReaderTest extends TestCase { ...@@ -73,7 +73,7 @@ public class AdtsReaderTest extends TestCase {
id3Output = fakeExtractorOutput.track(1); id3Output = fakeExtractorOutput.track(1);
adtsReader = new AdtsReader(true); adtsReader = new AdtsReader(true);
TrackIdGenerator idGenerator = new TrackIdGenerator(0, 1); TrackIdGenerator idGenerator = new TrackIdGenerator(0, 1);
adtsReader.init(fakeExtractorOutput, idGenerator); adtsReader.createTracks(fakeExtractorOutput, idGenerator);
data = new ParsableByteArray(TEST_DATA); data = new ParsableByteArray(TEST_DATA);
firstFeed = true; firstFeed = true;
} }
......
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.extractor.ts;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import junit.framework.TestCase;
/**
* Test for {@link SectionReader}.
*/
public class SectionReaderTest extends TestCase {
private byte[] packetPayload;
private CustomSectionPayloadReader payloadReader;
private SectionReader reader;
@Override
public void setUp() {
packetPayload = new byte[512];
Arrays.fill(packetPayload, (byte) 0xFF);
payloadReader = new CustomSectionPayloadReader();
reader = new SectionReader(payloadReader);
reader.init(new TimestampAdjuster(0), new FakeExtractorOutput(),
new TsPayloadReader.TrackIdGenerator(0, 1));
}
public void testSingleOnePacketSection() {
packetPayload[0] = 3;
insertTableSection(4, (byte) 99, 3);
reader.consume(new ParsableByteArray(packetPayload), true);
assertEquals(Collections.singletonList(99), payloadReader.parsedTableIds);
}
public void testHeaderSplitAcrossPackets() {
packetPayload[0] = 3; // The first packet includes a pointer_field.
insertTableSection(4, (byte) 100, 3); // This section header spreads across both packets.
ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 5);
reader.consume(firstPacket, true);
assertEquals(Collections.emptyList(), payloadReader.parsedTableIds);
ParsableByteArray secondPacket = new ParsableByteArray(packetPayload);
secondPacket.setPosition(5);
reader.consume(secondPacket, false);
assertEquals(Collections.singletonList(100), payloadReader.parsedTableIds);
}
public void testFiveSectionsInTwoPackets() {
packetPayload[0] = 0; // The first packet includes a pointer_field.
insertTableSection(1, (byte) 101, 10);
insertTableSection(14, (byte) 102, 10);
insertTableSection(27, (byte) 103, 10);
packetPayload[40] = 0; // The second packet includes a pointer_field.
insertTableSection(41, (byte) 104, 10);
insertTableSection(54, (byte) 105, 10);
ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 40);
reader.consume(firstPacket, true);
assertEquals(Arrays.asList(101, 102, 103), payloadReader.parsedTableIds);
ParsableByteArray secondPacket = new ParsableByteArray(packetPayload);
secondPacket.setPosition(40);
reader.consume(secondPacket, true);
assertEquals(Arrays.asList(101, 102, 103, 104, 105), payloadReader.parsedTableIds);
}
public void testLongSectionAcrossFourPackets() {
packetPayload[0] = 13; // The first packet includes a pointer_field.
insertTableSection(1, (byte) 106, 10); // First section. Should be skipped.
// Second section spread across four packets. Should be consumed.
insertTableSection(14, (byte) 107, 300);
packetPayload[300] = 17; // The third packet includes a pointer_field.
// Third section, at the payload start of the fourth packet. Should be consumed.
insertTableSection(318, (byte) 108, 10);
ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 100);
reader.consume(firstPacket, true);
assertEquals(Collections.emptyList(), payloadReader.parsedTableIds);
ParsableByteArray secondPacket = new ParsableByteArray(packetPayload, 200);
secondPacket.setPosition(100);
reader.consume(secondPacket, false);
assertEquals(Collections.emptyList(), payloadReader.parsedTableIds);
ParsableByteArray thirdPacket = new ParsableByteArray(packetPayload, 300);
thirdPacket.setPosition(200);
reader.consume(thirdPacket, false);
assertEquals(Collections.emptyList(), payloadReader.parsedTableIds);
ParsableByteArray fourthPacket = new ParsableByteArray(packetPayload);
fourthPacket.setPosition(300);
reader.consume(fourthPacket, true);
assertEquals(Arrays.asList(107, 108), payloadReader.parsedTableIds);
}
public void testSeek() {
packetPayload[0] = 13; // The first packet includes a pointer_field.
insertTableSection(1, (byte) 109, 10); // First section. Should be skipped.
// Second section spread across four packets. Should be consumed.
insertTableSection(14, (byte) 110, 300);
packetPayload[300] = 17; // The third packet includes a pointer_field.
// Third section, at the payload start of the fourth packet. Should be consumed.
insertTableSection(318, (byte) 111, 10);
ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 100);
reader.consume(firstPacket, true);
assertEquals(Collections.emptyList(), payloadReader.parsedTableIds);
ParsableByteArray secondPacket = new ParsableByteArray(packetPayload, 200);
secondPacket.setPosition(100);
reader.consume(secondPacket, false);
assertEquals(Collections.emptyList(), payloadReader.parsedTableIds);
ParsableByteArray thirdPacket = new ParsableByteArray(packetPayload, 300);
thirdPacket.setPosition(200);
reader.consume(thirdPacket, false);
assertEquals(Collections.emptyList(), payloadReader.parsedTableIds);
reader.seek();
ParsableByteArray fourthPacket = new ParsableByteArray(packetPayload);
fourthPacket.setPosition(300);
reader.consume(fourthPacket, true);
assertEquals(Collections.singletonList(111), payloadReader.parsedTableIds);
}
public void testCrcChecks() {
byte[] correctCrcPat = new byte[] {
(byte) 0x0, (byte) 0x0, (byte) 0xb0, (byte) 0xd, (byte) 0x0, (byte) 0x1, (byte) 0xc1,
(byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x1, (byte) 0xe1, (byte) 0x0, (byte) 0xe8,
(byte) 0xf9, (byte) 0x5e, (byte) 0x7d};
byte[] incorrectCrcPat = Arrays.copyOf(correctCrcPat, correctCrcPat.length);
// Crc field is incorrect, and should not be passed to the payload reader.
incorrectCrcPat[16]--;
reader.consume(new ParsableByteArray(correctCrcPat), true);
assertEquals(Collections.singletonList(0), payloadReader.parsedTableIds);
reader.consume(new ParsableByteArray(incorrectCrcPat), true);
assertEquals(Collections.singletonList(0), payloadReader.parsedTableIds);
}
// Internal methods.
/**
* Inserts a private section header to {@link #packetPayload}.
*
* @param offset The position at which the header is inserted.
* @param tableId The table_id for the inserted section.
* @param sectionLength The value to use for private_section_length.
*/
private void insertTableSection(int offset, byte tableId, int sectionLength) {
packetPayload[offset++] = tableId;
packetPayload[offset++] = (byte) ((sectionLength >> 8) & 0x0F);
packetPayload[offset] = (byte) (sectionLength & 0xFF);
}
// Internal classes.
private static final class CustomSectionPayloadReader implements SectionPayloadReader {
List<Integer> parsedTableIds;
@Override
public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
TsPayloadReader.TrackIdGenerator idGenerator) {
parsedTableIds = new ArrayList<>();
}
@Override
public void consume(ParsableByteArray sectionData) {
parsedTableIds.add(sectionData.readUnsignedByte());
}
}
}
...@@ -16,20 +16,21 @@ ...@@ -16,20 +16,21 @@
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import android.util.SparseArray;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.TimestampAdjuster; import com.google.android.exoplayer2.extractor.TimestampAdjuster;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.EsInfo; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.FakeExtractorOutput; import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
import com.google.android.exoplayer2.testutil.FakeTrackOutput; import com.google.android.exoplayer2.testutil.FakeTrackOutput;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Random; import java.util.Random;
/** /**
...@@ -72,7 +73,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { ...@@ -72,7 +73,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
} }
public void testCustomPesReader() throws Exception { public void testCustomPesReader() throws Exception {
CustomEsReaderFactory factory = new CustomEsReaderFactory(); CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(true, false);
TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory, false); TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory, false);
FakeExtractorInput input = new FakeExtractorInput.Builder() FakeExtractorInput input = new FakeExtractorInput.Builder()
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts")) .setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts"))
...@@ -81,13 +82,12 @@ public final class TsExtractorTest extends InstrumentationTestCase { ...@@ -81,13 +82,12 @@ public final class TsExtractorTest extends InstrumentationTestCase {
.setSimulatePartialReads(false).build(); .setSimulatePartialReads(false).build();
FakeExtractorOutput output = new FakeExtractorOutput(); FakeExtractorOutput output = new FakeExtractorOutput();
tsExtractor.init(output); tsExtractor.init(output);
tsExtractor.seek(input.getPosition());
PositionHolder seekPositionHolder = new PositionHolder(); PositionHolder seekPositionHolder = new PositionHolder();
int readResult = Extractor.RESULT_CONTINUE; int readResult = Extractor.RESULT_CONTINUE;
while (readResult != Extractor.RESULT_END_OF_INPUT) { while (readResult != Extractor.RESULT_END_OF_INPUT) {
readResult = tsExtractor.read(input, seekPositionHolder); readResult = tsExtractor.read(input, seekPositionHolder);
} }
CustomEsReader reader = factory.reader; CustomEsReader reader = factory.esReader;
assertEquals(2, reader.packetsRead); assertEquals(2, reader.packetsRead);
TrackOutput trackOutput = reader.getTrackOutput(); TrackOutput trackOutput = reader.getTrackOutput();
assertTrue(trackOutput == output.trackOutputs.get(257 /* PID of audio track. */)); assertTrue(trackOutput == output.trackOutputs.get(257 /* PID of audio track. */));
...@@ -96,7 +96,24 @@ public final class TsExtractorTest extends InstrumentationTestCase { ...@@ -96,7 +96,24 @@ public final class TsExtractorTest extends InstrumentationTestCase {
((FakeTrackOutput) trackOutput).format); ((FakeTrackOutput) trackOutput).format);
} }
private static void writeJunkData(ByteArrayOutputStream out, int length) throws IOException { public void testCustomInitialSectionReader() throws Exception {
CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(false, true);
TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory, false);
FakeExtractorInput input = new FakeExtractorInput.Builder()
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample_with_sdt.ts"))
.setSimulateIOErrors(false)
.setSimulateUnknownLength(false)
.setSimulatePartialReads(false).build();
tsExtractor.init(new FakeExtractorOutput());
PositionHolder seekPositionHolder = new PositionHolder();
int readResult = Extractor.RESULT_CONTINUE;
while (readResult != Extractor.RESULT_END_OF_INPUT) {
readResult = tsExtractor.read(input, seekPositionHolder);
}
assertEquals(1, factory.sdtReader.consumedSdts);
}
private static void writeJunkData(ByteArrayOutputStream out, int length) {
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
if (((byte) i) == TS_SYNC_BYTE) { if (((byte) i) == TS_SYNC_BYTE) {
out.write(0); out.write(0);
...@@ -106,7 +123,46 @@ public final class TsExtractorTest extends InstrumentationTestCase { ...@@ -106,7 +123,46 @@ public final class TsExtractorTest extends InstrumentationTestCase {
} }
} }
private static final class CustomEsReader extends ElementaryStreamReader { private static final class CustomTsPayloadReaderFactory implements TsPayloadReader.Factory {
private final boolean provideSdtReader;
private final boolean provideCustomEsReader;
private final TsPayloadReader.Factory defaultFactory;
private CustomEsReader esReader;
private SdtSectionReader sdtReader;
public CustomTsPayloadReaderFactory(boolean provideCustomEsReader, boolean provideSdtReader) {
this.provideCustomEsReader = provideCustomEsReader;
this.provideSdtReader = provideSdtReader;
defaultFactory = new DefaultTsPayloadReaderFactory();
}
@Override
public SparseArray<TsPayloadReader> createInitialPayloadReaders() {
if (provideSdtReader) {
assertNull(sdtReader);
SparseArray<TsPayloadReader> mapping = new SparseArray<>();
sdtReader = new SdtSectionReader();
mapping.put(17, new SectionReader(sdtReader));
return mapping;
} else {
return defaultFactory.createInitialPayloadReaders();
}
}
@Override
public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) {
if (provideCustomEsReader && streamType == 3) {
esReader = new CustomEsReader(esInfo.language);
return new PesReader(esReader);
} else {
return defaultFactory.createPayloadReader(streamType, esInfo);
}
}
}
private static final class CustomEsReader implements ElementaryStreamReader {
private final String language; private final String language;
private TrackOutput output; private TrackOutput output;
...@@ -121,7 +177,7 @@ public final class TsExtractorTest extends InstrumentationTestCase { ...@@ -121,7 +177,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
} }
@Override @Override
public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
output = extractorOutput.track(idGenerator.getNextId()); output = extractorOutput.track(idGenerator.getNextId());
output.format(Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0, output.format(Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0,
language, null, 0)); language, null, 0));
...@@ -146,23 +202,44 @@ public final class TsExtractorTest extends InstrumentationTestCase { ...@@ -146,23 +202,44 @@ public final class TsExtractorTest extends InstrumentationTestCase {
} }
private static final class CustomEsReaderFactory implements ElementaryStreamReader.Factory { private static final class SdtSectionReader implements SectionPayloadReader {
private final ElementaryStreamReader.Factory defaultFactory; private int consumedSdts;
private CustomEsReader reader;
public CustomEsReaderFactory() { @Override
defaultFactory = new DefaultStreamReaderFactory(); public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
TrackIdGenerator idGenerator) {
// Do nothing.
} }
@Override @Override
public ElementaryStreamReader createStreamReader(int streamType, EsInfo esInfo) { public void consume(ParsableByteArray sectionData) {
if (streamType == 3) { // table_id(8), section_syntax_indicator(1), reserved_future_use(1), reserved(2),
reader = new CustomEsReader(esInfo.language); // section_length(12), transport_stream_id(16), reserved(2), version_number(5),
return reader; // current_next_indicator(1), section_number(8), last_section_number(8),
} else { // original_network_id(16), reserved_future_use(8)
return defaultFactory.createStreamReader(streamType, esInfo); sectionData.skipBytes(11);
// Start of the service loop.
assertEquals(0x5566 /* arbitrary service id */, sectionData.readUnsignedShort());
// reserved_future_use(6), EIT_schedule_flag(1), EIT_present_following_flag(1)
sectionData.skipBytes(1);
// Assert there is only one service.
// Remove running_status(3), free_CA_mode(1) from the descriptors_loop_length with the mask.
assertEquals(sectionData.readUnsignedShort() & 0xFFF, sectionData.bytesLeft());
while (sectionData.bytesLeft() > 0) {
int descriptorTag = sectionData.readUnsignedByte();
int descriptorLength = sectionData.readUnsignedByte();
if (descriptorTag == 72 /* service descriptor */) {
assertEquals(1, sectionData.readUnsignedByte()); // Service type: Digital TV.
int serviceProviderNameLength = sectionData.readUnsignedByte();
assertEquals("Some provider", sectionData.readString(serviceProviderNameLength));
int serviceNameLength = sectionData.readUnsignedByte();
assertEquals("Some Channel", sectionData.readString(serviceNameLength));
} else {
sectionData.skipBytes(descriptorLength);
}
} }
consumedSdts++;
} }
} }
......
...@@ -16,8 +16,8 @@ ...@@ -16,8 +16,8 @@
package com.google.android.exoplayer2.metadata.id3; package com.google.android.exoplayer2.metadata.id3;
import android.test.MoreAsserts; import android.test.MoreAsserts;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoderException; import com.google.android.exoplayer2.metadata.MetadataDecoderException;
import java.util.List;
import junit.framework.TestCase; import junit.framework.TestCase;
/** /**
...@@ -30,9 +30,9 @@ public class Id3DecoderTest extends TestCase { ...@@ -30,9 +30,9 @@ public class Id3DecoderTest extends TestCase {
3, 0, 109, 100, 105, 97, 108, 111, 103, 95, 86, 73, 78, 68, 73, 67, 79, 49, 53, 50, 55, 54, 3, 0, 109, 100, 105, 97, 108, 111, 103, 95, 86, 73, 78, 68, 73, 67, 79, 49, 53, 50, 55, 54,
54, 52, 95, 115, 116, 97, 114, 116, 0}; 54, 52, 95, 115, 116, 97, 114, 116, 0};
Id3Decoder decoder = new Id3Decoder(); Id3Decoder decoder = new Id3Decoder();
List<Id3Frame> id3Frames = decoder.decode(rawId3, rawId3.length); Metadata metadata = decoder.decode(rawId3, rawId3.length);
assertEquals(1, id3Frames.size()); assertEquals(1, metadata.length());
TxxxFrame txxxFrame = (TxxxFrame) id3Frames.get(0); TxxxFrame txxxFrame = (TxxxFrame) metadata.get(0);
assertEquals("", txxxFrame.description); assertEquals("", txxxFrame.description);
assertEquals("mdialog_VINDICO1527664_start", txxxFrame.value); assertEquals("mdialog_VINDICO1527664_start", txxxFrame.value);
} }
...@@ -42,9 +42,9 @@ public class Id3DecoderTest extends TestCase { ...@@ -42,9 +42,9 @@ public class Id3DecoderTest extends TestCase {
3, 105, 109, 97, 103, 101, 47, 106, 112, 101, 103, 0, 16, 72, 101, 108, 108, 111, 32, 87, 3, 105, 109, 97, 103, 101, 47, 106, 112, 101, 103, 0, 16, 72, 101, 108, 108, 111, 32, 87,
111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0}; 111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
Id3Decoder decoder = new Id3Decoder(); Id3Decoder decoder = new Id3Decoder();
List<Id3Frame> id3Frames = decoder.decode(rawId3, rawId3.length); Metadata metadata = decoder.decode(rawId3, rawId3.length);
assertEquals(1, id3Frames.size()); assertEquals(1, metadata.length());
ApicFrame apicFrame = (ApicFrame) id3Frames.get(0); ApicFrame apicFrame = (ApicFrame) metadata.get(0);
assertEquals("image/jpeg", apicFrame.mimeType); assertEquals("image/jpeg", apicFrame.mimeType);
assertEquals(16, apicFrame.pictureType); assertEquals(16, apicFrame.pictureType);
assertEquals("Hello World", apicFrame.description); assertEquals("Hello World", apicFrame.description);
...@@ -56,9 +56,9 @@ public class Id3DecoderTest extends TestCase { ...@@ -56,9 +56,9 @@ public class Id3DecoderTest extends TestCase {
byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 23, 84, 73, 84, 50, 0, 0, 0, 13, 0, 0, byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 23, 84, 73, 84, 50, 0, 0, 0, 13, 0, 0,
3, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0}; 3, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0};
Id3Decoder decoder = new Id3Decoder(); Id3Decoder decoder = new Id3Decoder();
List<Id3Frame> id3Frames = decoder.decode(rawId3, rawId3.length); Metadata metadata = decoder.decode(rawId3, rawId3.length);
assertEquals(1, id3Frames.size()); assertEquals(1, metadata.length());
TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frames.get(0); TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0);
assertEquals("TIT2", textInformationFrame.id); assertEquals("TIT2", textInformationFrame.id);
assertEquals("Hello World", textInformationFrame.description); assertEquals("Hello World", textInformationFrame.description);
} }
......
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source;
import static org.mockito.Mockito.doAnswer;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.Timeline.Window;
import com.google.android.exoplayer2.source.MediaSource.Listener;
import com.google.android.exoplayer2.testutil.TestUtil;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
/**
* Unit tests for {@link ClippingMediaSource}.
*/
public final class ClippingMediaSourceTest extends InstrumentationTestCase {
private static final long TEST_PERIOD_DURATION_US = 1000000;
private static final long TEST_CLIP_AMOUNT_US = 300000;
@Mock
private MediaSource mockMediaSource;
private Timeline clippedTimeline;
private Window window;
private Period period;
@Override
protected void setUp() throws Exception {
TestUtil.setUpMockito(this);
window = new Timeline.Window();
period = new Timeline.Period();
}
public void testNoClipping() {
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true);
Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US);
assertEquals(1, clippedTimeline.getWindowCount());
assertEquals(1, clippedTimeline.getPeriodCount());
assertEquals(TEST_PERIOD_DURATION_US, clippedTimeline.getWindow(0, window).getDurationUs());
assertEquals(TEST_PERIOD_DURATION_US, clippedTimeline.getPeriod(0, period).getDurationUs());
}
public void testClippingUnseekableWindowThrows() {
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false);
// If the unseekable window isn't clipped, clipping succeeds.
getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US);
try {
// If the unseekable window is clipped, clipping fails.
getClippedTimeline(timeline, 1, TEST_PERIOD_DURATION_US);
fail("Expected clipping to fail.");
} catch (IllegalArgumentException e) {
// Expected.
}
}
public void testClippingStart() {
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true);
Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US,
TEST_PERIOD_DURATION_US);
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
clippedTimeline.getWindow(0, window).getDurationUs());
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
clippedTimeline.getPeriod(0, period).getDurationUs());
}
public void testClippingEnd() {
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true);
Timeline clippedTimeline = getClippedTimeline(timeline, 0,
TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
clippedTimeline.getWindow(0, window).getDurationUs());
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
clippedTimeline.getPeriod(0, period).getDurationUs());
}
public void testClippingStartAndEnd() {
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true);
Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US,
TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2);
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3,
clippedTimeline.getWindow(0, window).getDurationUs());
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3,
clippedTimeline.getPeriod(0, period).getDurationUs());
}
/**
* Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline.
*/
private Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) {
mockMediaSourceSourceWithTimeline(timeline);
new ClippingMediaSource(mockMediaSource, startMs, endMs).prepareSource(null, true,
new Listener() {
@Override
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
clippedTimeline = timeline;
}
});
return clippedTimeline;
}
/**
* Returns a mock {@link MediaSource} with the specified {@link Timeline} in its source info.
*/
private MediaSource mockMediaSourceSourceWithTimeline(final Timeline timeline) {
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
MediaSource.Listener listener = (MediaSource.Listener) invocation.getArguments()[2];
listener.onSourceInfoRefreshed(timeline, null);
return null;
}
}).when(mockMediaSource).prepareSource(Mockito.any(ExoPlayer.class), Mockito.anyBoolean(),
Mockito.any(MediaSource.Listener.class));
return mockMediaSource;
}
}
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.dash.manifest; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.dash.manifest;
import android.net.Uri; import android.net.Uri;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.IOException; import java.io.IOException;
...@@ -28,6 +29,8 @@ public class DashManifestParserTest extends InstrumentationTestCase { ...@@ -28,6 +29,8 @@ public class DashManifestParserTest extends InstrumentationTestCase {
private static final String SAMPLE_MPD_1 = "dash/sample_mpd_1"; private static final String SAMPLE_MPD_1 = "dash/sample_mpd_1";
private static final String SAMPLE_MPD_2_UNKNOWN_MIME_TYPE = private static final String SAMPLE_MPD_2_UNKNOWN_MIME_TYPE =
"dash/sample_mpd_2_unknown_mime_type"; "dash/sample_mpd_2_unknown_mime_type";
private static final String SAMPLE_MPD_3_SEGMENT_TEMPLATE =
"dash/sample_mpd_3_segment_template";
/** /**
* Simple test to ensure the sample manifests parse without any exceptions being thrown. * Simple test to ensure the sample manifests parse without any exceptions being thrown.
...@@ -40,4 +43,61 @@ public class DashManifestParserTest extends InstrumentationTestCase { ...@@ -40,4 +43,61 @@ public class DashManifestParserTest extends InstrumentationTestCase {
TestUtil.getInputStream(getInstrumentation(), SAMPLE_MPD_2_UNKNOWN_MIME_TYPE)); TestUtil.getInputStream(getInstrumentation(), SAMPLE_MPD_2_UNKNOWN_MIME_TYPE));
} }
public void testParseMediaPresentationDescriptionWithSegmentTemplate() throws IOException {
DashManifestParser parser = new DashManifestParser();
DashManifest mpd = parser.parse(Uri.parse("https://example.com/test.mpd"),
TestUtil.getInputStream(getInstrumentation(), SAMPLE_MPD_3_SEGMENT_TEMPLATE));
assertEquals(1, mpd.getPeriodCount());
Period period = mpd.getPeriod(0);
assertNotNull(period);
assertEquals(2, period.adaptationSets.size());
for (AdaptationSet adaptationSet : period.adaptationSets) {
assertNotNull(adaptationSet);
for (Representation representation : adaptationSet.representations) {
if (representation instanceof Representation.MultiSegmentRepresentation) {
Representation.MultiSegmentRepresentation multiSegmentRepresentation =
(Representation.MultiSegmentRepresentation) representation;
int firstSegmentIndex = multiSegmentRepresentation.getFirstSegmentNum();
RangedUri uri = multiSegmentRepresentation.getSegmentUrl(firstSegmentIndex);
assertTrue(uri.resolveUriString(representation.baseUrl).contains(
"redirector.googlevideo.com"));
}
}
}
}
public void testParseCea608AccessibilityChannel() {
assertEquals(1, DashManifestParser.parseCea608AccessibilityChannel("CC1=eng"));
assertEquals(2, DashManifestParser.parseCea608AccessibilityChannel("CC2=eng"));
assertEquals(3, DashManifestParser.parseCea608AccessibilityChannel("CC3=eng"));
assertEquals(4, DashManifestParser.parseCea608AccessibilityChannel("CC4=eng"));
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(null));
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(""));
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel("CC0=eng"));
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel("CC5=eng"));
assertEquals(Format.NO_VALUE,
DashManifestParser.parseCea608AccessibilityChannel("Wrong format"));
}
public void testParseCea708AccessibilityChannel() {
assertEquals(1, DashManifestParser.parseCea708AccessibilityChannel("1=lang:eng"));
assertEquals(2, DashManifestParser.parseCea708AccessibilityChannel("2=lang:eng"));
assertEquals(3, DashManifestParser.parseCea708AccessibilityChannel("3=lang:eng"));
assertEquals(62, DashManifestParser.parseCea708AccessibilityChannel("62=lang:eng"));
assertEquals(63, DashManifestParser.parseCea708AccessibilityChannel("63=lang:eng"));
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(null));
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(""));
assertEquals(Format.NO_VALUE,
DashManifestParser.parseCea708AccessibilityChannel("0=lang:eng"));
assertEquals(Format.NO_VALUE,
DashManifestParser.parseCea708AccessibilityChannel("64=lang:eng"));
assertEquals(Format.NO_VALUE,
DashManifestParser.parseCea708AccessibilityChannel("Wrong format"));
}
} }
...@@ -23,56 +23,64 @@ import junit.framework.TestCase; ...@@ -23,56 +23,64 @@ import junit.framework.TestCase;
*/ */
public class RangedUriTest extends TestCase { public class RangedUriTest extends TestCase {
private static final String FULL_URI = "http://www.test.com/path/file.ext"; private static final String BASE_URI = "http://www.test.com/";
private static final String PARTIAL_URI = "path/file.ext";
private static final String FULL_URI = BASE_URI + PARTIAL_URI;
public void testMerge() { public void testMerge() {
RangedUri rangeA = new RangedUri(null, FULL_URI, 0, 10); RangedUri rangeA = new RangedUri(FULL_URI, 0, 10);
RangedUri rangeB = new RangedUri(null, FULL_URI, 10, 10); RangedUri rangeB = new RangedUri(FULL_URI, 10, 10);
RangedUri expected = new RangedUri(null, FULL_URI, 0, 20); RangedUri expected = new RangedUri(FULL_URI, 0, 20);
assertMerge(rangeA, rangeB, expected); assertMerge(rangeA, rangeB, expected, null);
} }
public void testMergeUnbounded() { public void testMergeUnbounded() {
RangedUri rangeA = new RangedUri(null, FULL_URI, 0, 10); RangedUri rangeA = new RangedUri(FULL_URI, 0, 10);
RangedUri rangeB = new RangedUri(null, FULL_URI, 10, C.LENGTH_UNSET); RangedUri rangeB = new RangedUri(FULL_URI, 10, C.LENGTH_UNSET);
RangedUri expected = new RangedUri(null, FULL_URI, 0, C.LENGTH_UNSET); RangedUri expected = new RangedUri(FULL_URI, 0, C.LENGTH_UNSET);
assertMerge(rangeA, rangeB, expected); assertMerge(rangeA, rangeB, expected, null);
} }
public void testNonMerge() { public void testNonMerge() {
// A and B do not overlap, so should not merge // A and B do not overlap, so should not merge
RangedUri rangeA = new RangedUri(null, FULL_URI, 0, 10); RangedUri rangeA = new RangedUri(FULL_URI, 0, 10);
RangedUri rangeB = new RangedUri(null, FULL_URI, 11, 10); RangedUri rangeB = new RangedUri(FULL_URI, 11, 10);
assertNonMerge(rangeA, rangeB); assertNonMerge(rangeA, rangeB, null);
// A and B do not overlap, so should not merge // A and B do not overlap, so should not merge
rangeA = new RangedUri(null, FULL_URI, 0, 10); rangeA = new RangedUri(FULL_URI, 0, 10);
rangeB = new RangedUri(null, FULL_URI, 11, C.LENGTH_UNSET); rangeB = new RangedUri(FULL_URI, 11, C.LENGTH_UNSET);
assertNonMerge(rangeA, rangeB); assertNonMerge(rangeA, rangeB, null);
// A and B are bounded but overlap, so should not merge // A and B are bounded but overlap, so should not merge
rangeA = new RangedUri(null, FULL_URI, 0, 11); rangeA = new RangedUri(FULL_URI, 0, 11);
rangeB = new RangedUri(null, FULL_URI, 10, 10); rangeB = new RangedUri(FULL_URI, 10, 10);
assertNonMerge(rangeA, rangeB); assertNonMerge(rangeA, rangeB, null);
// A and B overlap due to unboundedness, so should not merge // A and B overlap due to unboundedness, so should not merge
rangeA = new RangedUri(null, FULL_URI, 0, C.LENGTH_UNSET); rangeA = new RangedUri(FULL_URI, 0, C.LENGTH_UNSET);
rangeB = new RangedUri(null, FULL_URI, 10, C.LENGTH_UNSET); rangeB = new RangedUri(FULL_URI, 10, C.LENGTH_UNSET);
assertNonMerge(rangeA, rangeB); assertNonMerge(rangeA, rangeB, null);
}
public void testMergeWithBaseUri() {
RangedUri rangeA = new RangedUri(PARTIAL_URI, 0, 10);
RangedUri rangeB = new RangedUri(FULL_URI, 10, 10);
RangedUri expected = new RangedUri(FULL_URI, 0, 20);
assertMerge(rangeA, rangeB, expected, BASE_URI);
} }
private void assertMerge(RangedUri rangeA, RangedUri rangeB, RangedUri expected) { private void assertMerge(RangedUri rangeA, RangedUri rangeB, RangedUri expected, String baseUrl) {
RangedUri merged = rangeA.attemptMerge(rangeB); RangedUri merged = rangeA.attemptMerge(rangeB, baseUrl);
assertEquals(expected, merged); assertEquals(expected, merged);
merged = rangeB.attemptMerge(rangeA); merged = rangeB.attemptMerge(rangeA, baseUrl);
assertEquals(expected, merged); assertEquals(expected, merged);
} }
private void assertNonMerge(RangedUri rangeA, RangedUri rangeB) { private void assertNonMerge(RangedUri rangeA, RangedUri rangeB, String baseUrl) {
RangedUri merged = rangeA.attemptMerge(rangeB); RangedUri merged = rangeA.attemptMerge(rangeB, baseUrl);
assertNull(merged); assertNull(merged);
merged = rangeB.attemptMerge(rangeA); merged = rangeB.attemptMerge(rangeA, baseUrl);
assertNull(merged); assertNull(merged);
} }
......
...@@ -27,16 +27,17 @@ public class RepresentationTest extends TestCase { ...@@ -27,16 +27,17 @@ public class RepresentationTest extends TestCase {
public void testGetCacheKey() { public void testGetCacheKey() {
String uri = "http://www.google.com"; String uri = "http://www.google.com";
SegmentBase base = new SingleSegmentBase(new RangedUri(uri, null, 0, 1), 1, 0, uri, 1, 1); SegmentBase base = new SingleSegmentBase(new RangedUri(null, 0, 1), 1, 0, 1, 1);
Format format = Format.createVideoContainerFormat("0", MimeTypes.APPLICATION_MP4, null, Format format = Format.createVideoContainerFormat("0", MimeTypes.APPLICATION_MP4, null,
MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null); MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null);
Representation representation = Representation.newInstance("test_stream_1", 3, format, base); Representation representation = Representation.newInstance("test_stream_1", 3, format, uri,
base);
assertEquals("test_stream_1.0.3", representation.getCacheKey()); assertEquals("test_stream_1.0.3", representation.getCacheKey());
format = Format.createVideoContainerFormat("150", MimeTypes.APPLICATION_MP4, null, format = Format.createVideoContainerFormat("150", MimeTypes.APPLICATION_MP4, null,
MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null); MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null);
representation = Representation.newInstance("test_stream_1", Representation.REVISION_ID_DEFAULT, representation = Representation.newInstance("test_stream_1", Representation.REVISION_ID_DEFAULT,
format, base); format, uri, base);
assertEquals("test_stream_1.150.-1", representation.getCacheKey()); assertEquals("test_stream_1.150.-1", representation.getCacheKey());
} }
......
...@@ -72,15 +72,14 @@ public class HlsMediaPlaylistParserTest extends TestCase { ...@@ -72,15 +72,14 @@ public class HlsMediaPlaylistParserTest extends TestCase {
HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist; HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
assertEquals(2679, mediaPlaylist.mediaSequence); assertEquals(2679, mediaPlaylist.mediaSequence);
assertEquals(8, mediaPlaylist.targetDurationSecs);
assertEquals(3, mediaPlaylist.version); assertEquals(3, mediaPlaylist.version);
assertEquals(false, mediaPlaylist.live); assertEquals(true, mediaPlaylist.hasEndTag);
List<HlsMediaPlaylist.Segment> segments = mediaPlaylist.segments; List<HlsMediaPlaylist.Segment> segments = mediaPlaylist.segments;
assertNotNull(segments); assertNotNull(segments);
assertEquals(5, segments.size()); assertEquals(5, segments.size());
assertEquals(4, segments.get(0).discontinuitySequenceNumber); assertEquals(4, segments.get(0).discontinuitySequenceNumber);
assertEquals(7.975, segments.get(0).durationSecs); assertEquals(7975000, segments.get(0).durationUs);
assertEquals(false, segments.get(0).isEncrypted); assertEquals(false, segments.get(0).isEncrypted);
assertEquals(null, segments.get(0).encryptionKeyUri); assertEquals(null, segments.get(0).encryptionKeyUri);
assertEquals(null, segments.get(0).encryptionIV); assertEquals(null, segments.get(0).encryptionIV);
...@@ -89,7 +88,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { ...@@ -89,7 +88,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals("https://priv.example.com/fileSequence2679.ts", segments.get(0).url); assertEquals("https://priv.example.com/fileSequence2679.ts", segments.get(0).url);
assertEquals(4, segments.get(1).discontinuitySequenceNumber); assertEquals(4, segments.get(1).discontinuitySequenceNumber);
assertEquals(7.975, segments.get(1).durationSecs); assertEquals(7975000, segments.get(1).durationUs);
assertEquals(true, segments.get(1).isEncrypted); assertEquals(true, segments.get(1).isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2680", segments.get(1).encryptionKeyUri); assertEquals("https://priv.example.com/key.php?r=2680", segments.get(1).encryptionKeyUri);
assertEquals("0x1566B", segments.get(1).encryptionIV); assertEquals("0x1566B", segments.get(1).encryptionIV);
...@@ -98,7 +97,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { ...@@ -98,7 +97,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals("https://priv.example.com/fileSequence2680.ts", segments.get(1).url); assertEquals("https://priv.example.com/fileSequence2680.ts", segments.get(1).url);
assertEquals(4, segments.get(2).discontinuitySequenceNumber); assertEquals(4, segments.get(2).discontinuitySequenceNumber);
assertEquals(7.941, segments.get(2).durationSecs); assertEquals(7941000, segments.get(2).durationUs);
assertEquals(false, segments.get(2).isEncrypted); assertEquals(false, segments.get(2).isEncrypted);
assertEquals(null, segments.get(2).encryptionKeyUri); assertEquals(null, segments.get(2).encryptionKeyUri);
assertEquals(null, segments.get(2).encryptionIV); assertEquals(null, segments.get(2).encryptionIV);
...@@ -107,7 +106,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { ...@@ -107,7 +106,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals("https://priv.example.com/fileSequence2681.ts", segments.get(2).url); assertEquals("https://priv.example.com/fileSequence2681.ts", segments.get(2).url);
assertEquals(5, segments.get(3).discontinuitySequenceNumber); assertEquals(5, segments.get(3).discontinuitySequenceNumber);
assertEquals(7.975, segments.get(3).durationSecs); assertEquals(7975000, segments.get(3).durationUs);
assertEquals(true, segments.get(3).isEncrypted); assertEquals(true, segments.get(3).isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2682", segments.get(3).encryptionKeyUri); assertEquals("https://priv.example.com/key.php?r=2682", segments.get(3).encryptionKeyUri);
// 0xA7A == 2682. // 0xA7A == 2682.
...@@ -118,7 +117,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { ...@@ -118,7 +117,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals("https://priv.example.com/fileSequence2682.ts", segments.get(3).url); assertEquals("https://priv.example.com/fileSequence2682.ts", segments.get(3).url);
assertEquals(5, segments.get(4).discontinuitySequenceNumber); assertEquals(5, segments.get(4).discontinuitySequenceNumber);
assertEquals(7.975, segments.get(4).durationSecs); assertEquals(7975000, segments.get(4).durationUs);
assertEquals(true, segments.get(4).isEncrypted); assertEquals(true, segments.get(4).isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2682", segments.get(4).encryptionKeyUri); assertEquals("https://priv.example.com/key.php?r=2682", segments.get(4).encryptionKeyUri);
// 0xA7B == 2683. // 0xA7B == 2683.
......
...@@ -30,6 +30,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase { ...@@ -30,6 +30,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
private static final String TYPICAL_EXTRA_BLANK_LINE = "subrip/typical_extra_blank_line"; private static final String TYPICAL_EXTRA_BLANK_LINE = "subrip/typical_extra_blank_line";
private static final String TYPICAL_MISSING_TIMECODE = "subrip/typical_missing_timecode"; private static final String TYPICAL_MISSING_TIMECODE = "subrip/typical_missing_timecode";
private static final String TYPICAL_MISSING_SEQUENCE = "subrip/typical_missing_sequence"; private static final String TYPICAL_MISSING_SEQUENCE = "subrip/typical_missing_sequence";
private static final String TYPICAL_NEGATIVE_TIMESTAMPS = "subrip/typical_negative_timestamps";
private static final String NO_END_TIMECODES_FILE = "subrip/no_end_timecodes"; private static final String NO_END_TIMECODES_FILE = "subrip/no_end_timecodes";
public void testDecodeEmpty() throws IOException { public void testDecodeEmpty() throws IOException {
...@@ -91,6 +92,15 @@ public final class SubripDecoderTest extends InstrumentationTestCase { ...@@ -91,6 +92,15 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
assertTypicalCue3(subtitle, 2); assertTypicalCue3(subtitle, 2);
} }
public void testDecodeTypicalNegativeTimestamps() throws IOException {
// Parsing should succeed, parsing the third cue only.
SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_NEGATIVE_TIMESTAMPS);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length);
assertEquals(2, subtitle.getEventTimeCount());
assertTypicalCue3(subtitle, 0);
}
public void testDecodeNoEndTimecodes() throws IOException { public void testDecodeNoEndTimecodes() throws IOException {
SubripDecoder decoder = new SubripDecoder(); SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_END_TIMECODES_FILE); byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_END_TIMECODES_FILE);
......
...@@ -97,7 +97,7 @@ public final class Mp4WebvttDecoderTest extends TestCase { ...@@ -97,7 +97,7 @@ public final class Mp4WebvttDecoderTest extends TestCase {
public void testNoCueSample() throws SubtitleDecoderException { public void testNoCueSample() throws SubtitleDecoderException {
Mp4WebvttDecoder decoder = new Mp4WebvttDecoder(); Mp4WebvttDecoder decoder = new Mp4WebvttDecoder();
Subtitle result = decoder.decode(NO_CUE_SAMPLE, NO_CUE_SAMPLE.length); Subtitle result = decoder.decode(NO_CUE_SAMPLE, NO_CUE_SAMPLE.length);
assertMp4WebvttSubtitleEquals(result, new Cue[0]); assertMp4WebvttSubtitleEquals(result);
} }
// Negative tests. // Negative tests.
......
...@@ -116,7 +116,7 @@ public class WebvttDecoderTest extends InstrumentationTestCase { ...@@ -116,7 +116,7 @@ public class WebvttDecoderTest extends InstrumentationTestCase {
Alignment.ALIGN_CENTER, 0.45f, Cue.LINE_TYPE_FRACTION, Cue.ANCHOR_TYPE_END, Cue.DIMEN_UNSET, Alignment.ALIGN_CENTER, 0.45f, Cue.LINE_TYPE_FRACTION, Cue.ANCHOR_TYPE_END, Cue.DIMEN_UNSET,
Cue.TYPE_UNSET, 0.35f); Cue.TYPE_UNSET, 0.35f);
assertCue(subtitle, 6, 6000000, 7000000, "This is the fourth subtitle.", assertCue(subtitle, 6, 6000000, 7000000, "This is the fourth subtitle.",
Alignment.ALIGN_CENTER, -10f, Cue.LINE_TYPE_NUMBER, Cue.TYPE_UNSET, Cue.DIMEN_UNSET, Alignment.ALIGN_CENTER, -11f, Cue.LINE_TYPE_NUMBER, Cue.TYPE_UNSET, Cue.DIMEN_UNSET,
Cue.TYPE_UNSET, Cue.DIMEN_UNSET); Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
assertCue(subtitle, 8, 7000000, 8000000, "This is the fifth subtitle.", assertCue(subtitle, 8, 7000000, 8000000, "This is the fifth subtitle.",
Alignment.ALIGN_OPPOSITE, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, 0.1f, Alignment.ALIGN_OPPOSITE, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, 0.1f,
......
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.upstream;
import android.test.MoreAsserts;
import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.IOException;
import java.util.Arrays;
import junit.framework.TestCase;
/**
* Unit tests for {@link DataSourceInputStream}.
*/
public class DataSourceInputStreamTest extends TestCase {
private static final byte[] TEST_DATA = TestUtil.buildTestData(16);
public void testReadSingleBytes() throws IOException {
DataSourceInputStream inputStream = buildTestInputStream();
// No bytes read yet.
assertEquals(0, inputStream.bytesRead());
// Read bytes.
for (int i = 0; i < TEST_DATA.length; i++) {
int readByte = inputStream.read();
assertTrue(0 <= readByte && readByte < 256);
assertEquals(TEST_DATA[i] & 0xFF, readByte);
assertEquals(i + 1, inputStream.bytesRead());
}
// Check end of stream.
assertEquals(-1, inputStream.read());
assertEquals(TEST_DATA.length, inputStream.bytesRead());
// Check close succeeds.
inputStream.close();
}
public void testRead() throws IOException {
DataSourceInputStream inputStream = buildTestInputStream();
// Read bytes.
byte[] readBytes = new byte[TEST_DATA.length];
int totalBytesRead = 0;
while (totalBytesRead < TEST_DATA.length) {
long bytesRead = inputStream.read(readBytes, totalBytesRead,
TEST_DATA.length - totalBytesRead);
assertTrue(bytesRead > 0);
totalBytesRead += bytesRead;
assertEquals(totalBytesRead, inputStream.bytesRead());
}
// Check the read data.
MoreAsserts.assertEquals(TEST_DATA, readBytes);
// Check end of stream.
assertEquals(TEST_DATA.length, inputStream.bytesRead());
assertEquals(TEST_DATA.length, totalBytesRead);
assertEquals(-1, inputStream.read());
// Check close succeeds.
inputStream.close();
}
public void testSkip() throws IOException {
DataSourceInputStream inputStream = buildTestInputStream();
// Skip bytes.
long totalBytesSkipped = 0;
while (totalBytesSkipped < TEST_DATA.length) {
long bytesSkipped = inputStream.skip(Long.MAX_VALUE);
assertTrue(bytesSkipped > 0);
totalBytesSkipped += bytesSkipped;
assertEquals(totalBytesSkipped, inputStream.bytesRead());
}
// Check end of stream.
assertEquals(TEST_DATA.length, inputStream.bytesRead());
assertEquals(TEST_DATA.length, totalBytesSkipped);
assertEquals(-1, inputStream.read());
// Check close succeeds.
inputStream.close();
}
private static DataSourceInputStream buildTestInputStream() {
FakeDataSource.Builder fakeDataSourceBuilder = new FakeDataSource.Builder()
.appendReadData(Arrays.copyOfRange(TEST_DATA, 0, 5))
.appendReadData(Arrays.copyOfRange(TEST_DATA, 5, 10))
.appendReadData(Arrays.copyOfRange(TEST_DATA, 10, 15))
.appendReadData(Arrays.copyOfRange(TEST_DATA, 15, TEST_DATA.length));
return new DataSourceInputStream(fakeDataSourceBuilder.build(), new DataSpec(null));
}
}
...@@ -23,7 +23,6 @@ import com.google.android.exoplayer2.testutil.FakeDataSource; ...@@ -23,7 +23,6 @@ import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.testutil.FakeDataSource.Builder; import com.google.android.exoplayer2.testutil.FakeDataSource.Builder;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
...@@ -41,11 +40,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase { ...@@ -41,11 +40,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
// Create a temporary folder cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
cacheDir = File.createTempFile("CacheDataSourceTest", null);
assertTrue(cacheDir.delete());
assertTrue(cacheDir.mkdir());
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
} }
...@@ -57,8 +52,12 @@ public class CacheDataSourceTest extends InstrumentationTestCase { ...@@ -57,8 +52,12 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
public void testMaxCacheFileSize() throws Exception { public void testMaxCacheFileSize() throws Exception {
CacheDataSource cacheDataSource = createCacheDataSource(false, false); CacheDataSource cacheDataSource = createCacheDataSource(false, false);
assertReadDataContentLength(cacheDataSource, false, false); assertReadDataContentLength(cacheDataSource, false, false);
assertEquals((int) Math.ceil((double) TEST_DATA.length / MAX_CACHE_FILE_SIZE), File[] files = cacheDir.listFiles();
cacheDir.listFiles().length); for (File file : files) {
if (!file.getName().equals(CachedContentIndex.FILE_NAME)) {
assertTrue(file.length() <= MAX_CACHE_FILE_SIZE);
}
}
} }
public void testCacheAndRead() throws Exception { public void testCacheAndRead() throws Exception {
...@@ -177,8 +176,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase { ...@@ -177,8 +176,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
builder.setSimulateUnknownLength(simulateUnknownLength); builder.setSimulateUnknownLength(simulateUnknownLength);
builder.appendReadData(TEST_DATA); builder.appendReadData(TEST_DATA);
FakeDataSource upstream = builder.build(); FakeDataSource upstream = builder.build();
return new CacheDataSource(simpleCache, upstream, return new CacheDataSource(simpleCache, upstream, CacheDataSource.FLAG_BLOCK_ON_CACHE,
CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_CACHE_UNBOUNDED_REQUESTS,
MAX_CACHE_FILE_SIZE); MAX_CACHE_FILE_SIZE);
} }
......
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.upstream.cache;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.File;
import java.util.Random;
import junit.framework.TestCase;
/**
* Unit tests for {@link CacheSpan}.
*/
public class CacheSpanTest extends TestCase {
public void testCacheFile() throws Exception {
assertCacheSpan(new File("parent"), "key", 0, 0);
assertCacheSpan(new File("parent/"), "key", 1, 2);
assertCacheSpan(new File("parent"), "<>:\"/\\|?*%", 1, 2);
assertCacheSpan(new File("/"), "key", 1, 2);
assertNullCacheSpan(new File("parent"), "", 1, 2);
assertNullCacheSpan(new File("parent"), "key", -1, 2);
assertNullCacheSpan(new File("parent"), "key", 1, -2);
assertNotNull(CacheSpan.createCacheEntry(new File("/asd%aa.1.2.v2.exo")));
assertNull(CacheSpan.createCacheEntry(new File("/asd%za.1.2.v2.exo")));
assertCacheSpan(new File("parent"),
"A newline (line feed) character \n"
+ "A carriage-return character followed immediately by a newline character \r\n"
+ "A standalone carriage-return character \r"
+ "A next-line character \u0085"
+ "A line-separator character \u2028"
+ "A paragraph-separator character \u2029", 1, 2);
}
public void testCacheFileNameRandomData() throws Exception {
Random random = new Random(0);
File parent = new File("parent");
for (int i = 0; i < 1000; i++) {
String key = TestUtil.buildTestString(1000, random);
long offset = Math.abs(random.nextLong());
long lastAccessTimestamp = Math.abs(random.nextLong());
assertCacheSpan(parent, key, offset, lastAccessTimestamp);
}
}
private void assertCacheSpan(File parent, String key, long offset, long lastAccessTimestamp) {
File cacheFile = CacheSpan.getCacheFileName(parent, key, offset, lastAccessTimestamp);
CacheSpan cacheSpan = CacheSpan.createCacheEntry(cacheFile);
String message = cacheFile.toString();
assertNotNull(message, cacheSpan);
assertEquals(message, parent, cacheFile.getParentFile());
assertEquals(message, key, cacheSpan.key);
assertEquals(message, offset, cacheSpan.position);
assertEquals(message, lastAccessTimestamp, cacheSpan.lastAccessTimestamp);
}
private void assertNullCacheSpan(File parent, String key, long offset,
long lastAccessTimestamp) {
File cacheFile = CacheSpan.getCacheFileName(parent, key, offset, lastAccessTimestamp);
CacheSpan cacheSpan = CacheSpan.createCacheEntry(cacheFile);
assertNull(cacheFile.toString(), cacheSpan);
}
}
package com.google.android.exoplayer2.upstream.cache;
import android.test.InstrumentationTestCase;
import android.test.MoreAsserts;
import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Set;
import junit.framework.AssertionFailedError;
/**
* Tests {@link CachedContentIndex}.
*/
public class CachedContentIndexTest extends InstrumentationTestCase {
private final byte[] testIndexV1File = {
0, 0, 0, 1, // version
0, 0, 0, 0, // flags
0, 0, 0, 2, // number_of_CachedContent
0, 0, 0, 5, // cache_id
0, 5, 65, 66, 67, 68, 69, // cache_key
0, 0, 0, 0, 0, 0, 0, 10, // original_content_length
0, 0, 0, 2, // cache_id
0, 5, 75, 76, 77, 78, 79, // cache_key
0, 0, 0, 0, 0, 0, 10, 0, // original_content_length
(byte) 0xF6, (byte) 0xFB, 0x50, 0x41 // hashcode_of_CachedContent_array
};
private CachedContentIndex index;
private File cacheDir;
@Override
public void setUp() throws Exception {
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
index = new CachedContentIndex(cacheDir);
}
@Override
protected void tearDown() throws Exception {
TestUtil.recursiveDelete(cacheDir);
}
public void testAddGetRemove() throws Exception {
final String key1 = "key1";
final String key2 = "key2";
final String key3 = "key3";
// Add two CachedContents with add methods
CachedContent cachedContent1 = new CachedContent(5, key1, 10);
index.addNew(cachedContent1);
CachedContent cachedContent2 = index.add(key2);
assertTrue(cachedContent1.id != cachedContent2.id);
// add a span
File cacheSpanFile = SimpleCacheSpanTest
.createCacheSpanFile(cacheDir, cachedContent1.id, 10, 20, 30);
SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(cacheSpanFile, index);
assertNotNull(span);
cachedContent1.addSpan(span);
// Check if they are added and get method returns null if the key isn't found
assertEquals(cachedContent1, index.get(key1));
assertEquals(cachedContent2, index.get(key2));
assertNull(index.get(key3));
// test getAll()
Collection<CachedContent> cachedContents = index.getAll();
assertEquals(2, cachedContents.size());
assertTrue(Arrays.asList(cachedContent1, cachedContent2).containsAll(cachedContents));
// test getKeys()
Set<String> keys = index.getKeys();
assertEquals(2, keys.size());
assertTrue(Arrays.asList(key1, key2).containsAll(keys));
// test getKeyForId()
assertEquals(key1, index.getKeyForId(cachedContent1.id));
assertEquals(key2, index.getKeyForId(cachedContent2.id));
// test remove()
index.removeEmpty(key2);
index.removeEmpty(key3);
assertEquals(cachedContent1, index.get(key1));
assertNull(index.get(key2));
assertTrue(cacheSpanFile.exists());
// test removeEmpty()
index.addNew(cachedContent2);
index.removeEmpty();
assertEquals(cachedContent1, index.get(key1));
assertNull(index.get(key2));
assertTrue(cacheSpanFile.exists());
}
public void testStoreAndLoad() throws Exception {
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir));
}
public void testLoadV1() throws Exception {
FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
fos.write(testIndexV1File);
fos.close();
index.load();
assertEquals(2, index.getAll().size());
assertEquals(5, index.assignIdForKey("ABCDE"));
assertEquals(10, index.getContentLength("ABCDE"));
assertEquals(2, index.assignIdForKey("KLMNO"));
assertEquals(2560, index.getContentLength("KLMNO"));
}
public void testStoreV1() throws Exception {
index.addNew(new CachedContent(2, "KLMNO", 2560));
index.addNew(new CachedContent(5, "ABCDE", 10));
index.store();
byte[] buffer = new byte[testIndexV1File.length];
FileInputStream fos = new FileInputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
assertEquals(testIndexV1File.length, fos.read(buffer));
assertEquals(-1, fos.read());
fos.close();
// TODO: The order of the CachedContent stored in index file isn't defined so this test may fail
// on a different implementation of the underlying set
MoreAsserts.assertEquals(testIndexV1File, buffer);
}
public void testAssignIdForKeyAndGetKeyForId() throws Exception {
final String key1 = "key1";
final String key2 = "key2";
int id1 = index.assignIdForKey(key1);
int id2 = index.assignIdForKey(key2);
assertEquals(key1, index.getKeyForId(id1));
assertEquals(key2, index.getKeyForId(id2));
assertTrue(id1 != id2);
assertEquals(id1, index.assignIdForKey(key1));
assertEquals(id2, index.assignIdForKey(key2));
}
public void testSetGetContentLength() throws Exception {
final String key1 = "key1";
assertEquals(C.LENGTH_UNSET, index.getContentLength(key1));
index.setContentLength(key1, 10);
assertEquals(10, index.getContentLength(key1));
}
public void testGetNewId() throws Exception {
SparseArray<String> idToKey = new SparseArray<>();
assertEquals(0, CachedContentIndex.getNewId(idToKey));
idToKey.put(10, "");
assertEquals(11, CachedContentIndex.getNewId(idToKey));
idToKey.put(Integer.MAX_VALUE, "");
assertEquals(0, CachedContentIndex.getNewId(idToKey));
idToKey.put(0, "");
assertEquals(1, CachedContentIndex.getNewId(idToKey));
}
public void testEncryption() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
byte[] key2 = "bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
new CachedContentIndex(cacheDir, key));
// Rename the index file from the test above
File file1 = new File(cacheDir, CachedContentIndex.FILE_NAME);
File file2 = new File(cacheDir, "file2compare");
assertTrue(file1.renameTo(file2));
// Write a new index file
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
new CachedContentIndex(cacheDir, key));
assertEquals(file2.length(), file1.length());
// Assert file content is different
FileInputStream fis1 = new FileInputStream(file1);
FileInputStream fis2 = new FileInputStream(file2);
for (int b; (b = fis1.read()) == fis2.read();) {
assertTrue(b != -1);
}
boolean threw = false;
try {
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
new CachedContentIndex(cacheDir, key2));
} catch (AssertionFailedError e) {
threw = true;
}
assertTrue("Encrypted index file can not be read with different encryption key", threw);
try {
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
new CachedContentIndex(cacheDir));
} catch (AssertionFailedError e) {
threw = true;
}
assertTrue("Encrypted index file can not be read without encryption key", threw);
// Non encrypted index file can be read even when encryption key provided.
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir),
new CachedContentIndex(cacheDir, key));
}
private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentIndex index2)
throws IOException {
index.addNew(new CachedContent(5, "key1", 10));
index.add("key2");
index.store();
index2.load();
Set<String> keys = index.getKeys();
Set<String> keys2 = index2.getKeys();
assertEquals(keys, keys2);
for (String key : keys) {
assertEquals(index.getContentLength(key), index2.getContentLength(key));
assertEquals(index.get(key).getSpans(), index2.get(key).getSpans());
}
}
}
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.upstream.cache;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Set;
import java.util.TreeSet;
/**
* Unit tests for {@link SimpleCacheSpan}.
*/
public class SimpleCacheSpanTest extends InstrumentationTestCase {
private CachedContentIndex index;
private File cacheDir;
public static File createCacheSpanFile(File cacheDir, int id, long offset, int length,
long lastAccessTimestamp) throws IOException {
File cacheFile = SimpleCacheSpan.getCacheFile(cacheDir, id, offset, lastAccessTimestamp);
createTestFile(cacheFile, length);
return cacheFile;
}
public static CacheSpan createCacheSpan(CachedContentIndex index, File cacheDir, String key,
long offset, int length, long lastAccessTimestamp) throws IOException {
int id = index.assignIdForKey(key);
File cacheFile = createCacheSpanFile(cacheDir, id, offset, length, lastAccessTimestamp);
return SimpleCacheSpan.createCacheEntry(cacheFile, index);
}
@Override
protected void setUp() throws Exception {
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
index = new CachedContentIndex(cacheDir);
}
@Override
protected void tearDown() throws Exception {
TestUtil.recursiveDelete(cacheDir);
}
public void testCacheFile() throws Exception {
assertCacheSpan("key1", 0, 0);
assertCacheSpan("key2", 1, 2);
assertCacheSpan("<>:\"/\\|?*%", 1, 2);
assertCacheSpan("key3", 1, 2);
assertNullCacheSpan(new File("parent"), "key4", -1, 2);
assertNullCacheSpan(new File("parent"), "key5", 1, -2);
assertCacheSpan(
"A newline (line feed) character \n"
+ "A carriage-return character followed immediately by a newline character \r\n"
+ "A standalone carriage-return character \r"
+ "A next-line character \u0085"
+ "A line-separator character \u2028"
+ "A paragraph-separator character \u2029", 1, 2);
}
public void testUpgradeFileName() throws Exception {
String key = "asd\u00aa";
int id = index.assignIdForKey(key);
File v3file = createTestFile(id + ".0.1.v3.exo");
File v2file = createTestFile("asd%aa.1.2.v2.exo");
File wrongEscapedV2file = createTestFile("asd%za.3.4.v2.exo");
File v1File = createTestFile("asd\u00aa.5.6.v1.exo");
for (File file : cacheDir.listFiles()) {
SimpleCacheSpan cacheEntry = SimpleCacheSpan.createCacheEntry(file, index);
if (file.equals(wrongEscapedV2file)) {
assertNull(cacheEntry);
} else {
assertNotNull(cacheEntry);
}
}
assertTrue(v3file.exists());
assertFalse(v2file.exists());
assertTrue(wrongEscapedV2file.exists());
assertFalse(v1File.exists());
File[] files = cacheDir.listFiles();
assertEquals(4, files.length);
Set<String> keys = index.getKeys();
assertEquals("There should be only one key for all files.", 1, keys.size());
assertTrue(keys.contains(key));
TreeSet<SimpleCacheSpan> spans = index.get(key).getSpans();
assertTrue("upgradeOldFiles() shouldn't add any spans.", spans.isEmpty());
HashMap<Long, Long> cachedPositions = new HashMap<>();
for (File file : files) {
SimpleCacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(file, index);
if (cacheSpan != null) {
assertEquals(key, cacheSpan.key);
cachedPositions.put(cacheSpan.position, cacheSpan.lastAccessTimestamp);
}
}
assertEquals(1, (long) cachedPositions.get((long) 0));
assertEquals(2, (long) cachedPositions.get((long) 1));
assertEquals(6, (long) cachedPositions.get((long) 5));
}
private static void createTestFile(File file, int length) throws IOException {
FileOutputStream output = new FileOutputStream(file);
for (int i = 0; i < length; i++) {
output.write(i);
}
output.close();
}
private File createTestFile(String name) throws IOException {
File file = new File(cacheDir, name);
createTestFile(file, 1);
return file;
}
private void assertCacheSpan(String key, long offset, long lastAccessTimestamp)
throws IOException {
int id = index.assignIdForKey(key);
File cacheFile = createCacheSpanFile(cacheDir, id, offset, 1, lastAccessTimestamp);
SimpleCacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(cacheFile, index);
String message = cacheFile.toString();
assertNotNull(message, cacheSpan);
assertEquals(message, cacheDir, cacheFile.getParentFile());
assertEquals(message, key, cacheSpan.key);
assertEquals(message, offset, cacheSpan.position);
assertEquals(message, 1, cacheSpan.length);
assertTrue(message, cacheSpan.isCached);
assertEquals(message, cacheFile, cacheSpan.file);
assertEquals(message, lastAccessTimestamp, cacheSpan.lastAccessTimestamp);
}
private void assertNullCacheSpan(File parent, String key, long offset,
long lastAccessTimestamp) {
File cacheFile = SimpleCacheSpan.getCacheFile(parent, index.assignIdForKey(key), offset,
lastAccessTimestamp);
CacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(cacheFile, index);
assertNull(cacheFile.toString(), cacheSpan);
}
}
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
package com.google.android.exoplayer2.upstream.cache; package com.google.android.exoplayer2.upstream.cache;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.File; import java.io.File;
...@@ -36,10 +35,7 @@ public class SimpleCacheTest extends InstrumentationTestCase { ...@@ -36,10 +35,7 @@ public class SimpleCacheTest extends InstrumentationTestCase {
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
// Create a temporary folder this.cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
cacheDir = File.createTempFile("SimpleCacheTest", null);
assertTrue(cacheDir.delete());
assertTrue(cacheDir.mkdir());
} }
@Override @Override
...@@ -48,7 +44,7 @@ public class SimpleCacheTest extends InstrumentationTestCase { ...@@ -48,7 +44,7 @@ public class SimpleCacheTest extends InstrumentationTestCase {
} }
public void testCommittingOneFile() throws Exception { public void testCommittingOneFile() throws Exception {
SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); SimpleCache simpleCache = getSimpleCache();
CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0); CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0);
assertFalse(cacheSpan.isCached); assertFalse(cacheSpan.isCached);
...@@ -79,37 +75,40 @@ public class SimpleCacheTest extends InstrumentationTestCase { ...@@ -79,37 +75,40 @@ public class SimpleCacheTest extends InstrumentationTestCase {
} }
public void testSetGetLength() throws Exception { public void testSetGetLength() throws Exception {
SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); SimpleCache simpleCache = getSimpleCache();
assertEquals(C.LENGTH_UNSET, simpleCache.getContentLength(KEY_1)); assertEquals(C.LENGTH_UNSET, simpleCache.getContentLength(KEY_1));
assertTrue(simpleCache.setContentLength(KEY_1, 15)); simpleCache.setContentLength(KEY_1, 15);
assertEquals(15, simpleCache.getContentLength(KEY_1)); assertEquals(15, simpleCache.getContentLength(KEY_1));
simpleCache.startReadWrite(KEY_1, 0); simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, 0, 15); addCache(simpleCache, 0, 15);
assertTrue(simpleCache.setContentLength(KEY_1, 150)); simpleCache.setContentLength(KEY_1, 150);
assertEquals(150, simpleCache.getContentLength(KEY_1)); assertEquals(150, simpleCache.getContentLength(KEY_1));
addCache(simpleCache, 140, 10); addCache(simpleCache, 140, 10);
// Try to set length shorter then the content
assertFalse(simpleCache.setContentLength(KEY_1, 15));
assertEquals("Content length should be unchanged.",
150, simpleCache.getContentLength(KEY_1));
/* TODO Enable when the length persistance is fixed
// Check if values are kept after cache is reloaded. // Check if values are kept after cache is reloaded.
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); SimpleCache simpleCache2 = getSimpleCache();
assertEquals(150, simpleCache.getContentLength(KEY_1)); Set<String> keys = simpleCache.getKeys();
CacheSpan lastSpan = simpleCache.startReadWrite(KEY_1, 145); Set<String> keys2 = simpleCache2.getKeys();
assertEquals(keys, keys2);
for (String key : keys) {
assertEquals(simpleCache.getContentLength(key), simpleCache2.getContentLength(key));
assertEquals(simpleCache.getCachedSpans(key), simpleCache2.getCachedSpans(key));
}
// Removing the last span shouldn't cause the length be change next time cache loaded // Removing the last span shouldn't cause the length be change next time cache loaded
simpleCache.removeSpan(lastSpan); SimpleCacheSpan lastSpan = simpleCache2.startReadWrite(KEY_1, 145);
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); simpleCache2.removeSpan(lastSpan);
assertEquals(150, simpleCache.getContentLength(KEY_1)); simpleCache2 = getSimpleCache();
*/ assertEquals(150, simpleCache2.getContentLength(KEY_1));
}
private SimpleCache getSimpleCache() {
return new SimpleCache(cacheDir, new NoOpCacheEvictor());
} }
private void addCache(SimpleCache simpleCache, int position, int length) throws IOException { private void addCache(SimpleCache simpleCache, int position, int length) throws IOException {
......
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.util;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Tests {@link AtomicFile}.
*/
public class AtomicFileTest extends InstrumentationTestCase {
private File tempFolder;
private File file;
private AtomicFile atomicFile;
@Override
public void setUp() throws Exception {
tempFolder = TestUtil.createTempFolder(getInstrumentation().getContext());
file = new File(tempFolder, "atomicFile");
atomicFile = new AtomicFile(file);
}
@Override
protected void tearDown() throws Exception {
TestUtil.recursiveDelete(tempFolder);
}
public void testDelete() throws Exception {
assertTrue(file.createNewFile());
atomicFile.delete();
assertFalse(file.exists());
}
public void testWriteRead() throws Exception {
OutputStream output = atomicFile.startWrite();
output.write(5);
atomicFile.endWrite(output);
output.close();
assertRead();
output = atomicFile.startWrite();
output.write(5);
output.write(6);
output.close();
assertRead();
output = atomicFile.startWrite();
output.write(6);
assertRead();
output.close();
output = atomicFile.startWrite();
assertRead();
output.close();
}
private void assertRead() throws IOException {
InputStream input = atomicFile.openRead();
assertEquals(5, input.read());
assertEquals(-1, input.read());
input.close();
}
}
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.util;
import android.test.MoreAsserts;
import java.io.ByteArrayOutputStream;
import junit.framework.TestCase;
/**
* Tests {@link ReusableBufferedOutputStream}.
*/
public class ReusableBufferedOutputStreamTest extends TestCase {
private static final byte[] TEST_DATA_1 = "test data 1".getBytes();
private static final byte[] TEST_DATA_2 = "2 test data".getBytes();
public void testReset() throws Exception {
ByteArrayOutputStream byteArrayOutputStream1 = new ByteArrayOutputStream(1000);
ReusableBufferedOutputStream outputStream = new ReusableBufferedOutputStream(
byteArrayOutputStream1, 1000);
outputStream.write(TEST_DATA_1);
outputStream.close();
ByteArrayOutputStream byteArrayOutputStream2 = new ByteArrayOutputStream(1000);
outputStream.reset(byteArrayOutputStream2);
outputStream.write(TEST_DATA_2);
outputStream.close();
MoreAsserts.assertEquals(TEST_DATA_1, byteArrayOutputStream1.toByteArray());
MoreAsserts.assertEquals(TEST_DATA_2, byteArrayOutputStream2.toByteArray());
}
}
...@@ -15,9 +15,7 @@ ...@@ -15,9 +15,7 @@
*/ */
package com.google.android.exoplayer2.util; package com.google.android.exoplayer2.util;
import android.test.MoreAsserts;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import java.text.ParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
...@@ -141,23 +139,11 @@ public class UtilTest extends TestCase { ...@@ -141,23 +139,11 @@ public class UtilTest extends TestCase {
assertEquals(1500L, Util.parseXsDuration("PT1.500S")); assertEquals(1500L, Util.parseXsDuration("PT1.500S"));
} }
public void testParseXsDateTime() throws ParseException { public void testParseXsDateTime() throws Exception {
assertEquals(1403219262000L, Util.parseXsDateTime("2014-06-19T23:07:42")); assertEquals(1403219262000L, Util.parseXsDateTime("2014-06-19T23:07:42"));
assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00Z")); assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00Z"));
} assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-08:00"));
assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-0800"));
public void testGetHexStringByteArray() throws Exception {
assertHexStringByteArray("", new byte[] {});
assertHexStringByteArray("01", new byte[] {1});
assertHexStringByteArray("FF", new byte[] {(byte) 255});
assertHexStringByteArray("01020304", new byte[] {1, 2, 3, 4});
assertHexStringByteArray("0123456789ABCDEF",
new byte[] {1, 0x23, 0x45, 0x67, (byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF});
}
private void assertHexStringByteArray(String hex, byte[] array) {
assertEquals(hex, Util.getHexString(array));
MoreAsserts.assertEquals(array, Util.getBytesFromHexString(hex));
} }
public void testUnescapeInvalidFileName() { public void testUnescapeInvalidFileName() {
......
...@@ -70,8 +70,8 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { ...@@ -70,8 +70,8 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
} }
@Override @Override
public final void enable(Format[] formats, SampleStream stream, long positionUs, public final void enable(Format[] formats, SampleStream stream, long positionUs, boolean joining,
boolean joining, long offsetUs) throws ExoPlaybackException { long offsetUs) throws ExoPlaybackException {
Assertions.checkState(state == STATE_DISABLED); Assertions.checkState(state == STATE_DISABLED);
state = STATE_ENABLED; state = STATE_ENABLED;
onEnabled(joining); onEnabled(joining);
...@@ -107,11 +107,16 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { ...@@ -107,11 +107,16 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
} }
@Override @Override
public final void setCurrentStreamIsFinal() { public final void setCurrentStreamFinal() {
streamIsFinal = true; streamIsFinal = true;
} }
@Override @Override
public final boolean isCurrentStreamFinal() {
return streamIsFinal;
}
@Override
public final void maybeThrowStreamError() throws IOException { public final void maybeThrowStreamError() throws IOException {
stream.maybeThrowError(); stream.maybeThrowError();
} }
...@@ -119,6 +124,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { ...@@ -119,6 +124,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
@Override @Override
public final void resetPosition(long positionUs) throws ExoPlaybackException { public final void resetPosition(long positionUs) throws ExoPlaybackException {
streamIsFinal = false; streamIsFinal = false;
readEndOfStream = false;
onPositionReset(positionUs, false); onPositionReset(positionUs, false);
} }
...@@ -194,8 +200,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { ...@@ -194,8 +200,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
* @param joining Whether this renderer is being enabled to join an ongoing playback. * @param joining Whether this renderer is being enabled to join an ongoing playback.
* @throws ExoPlaybackException If an error occurs. * @throws ExoPlaybackException If an error occurs.
*/ */
protected void onPositionReset(long positionUs, boolean joining) protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
throws ExoPlaybackException {
// Do nothing. // Do nothing.
} }
...@@ -243,7 +248,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { ...@@ -243,7 +248,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
/** /**
* Reads from the enabled upstream source. If the upstream source has been read to the end then * Reads from the enabled upstream source. If the upstream source has been read to the end then
* {@link C#RESULT_BUFFER_READ} is only returned if {@link #setCurrentStreamIsFinal()} has been * {@link C#RESULT_BUFFER_READ} is only returned if {@link #setCurrentStreamFinal()} has been
* called. {@link C#RESULT_NOTHING_READ} is returned otherwise. * called. {@link C#RESULT_NOTHING_READ} is returned otherwise.
* *
* @see SampleStream#readData(FormatHolder, DecoderInputBuffer) * @see SampleStream#readData(FormatHolder, DecoderInputBuffer)
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import android.media.AudioFormat; import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.support.annotation.IntDef; import android.support.annotation.IntDef;
import android.view.Surface; import android.view.Surface;
...@@ -160,6 +161,42 @@ public final class C { ...@@ -160,6 +161,42 @@ public final class C {
? AudioFormat.CHANNEL_OUT_7POINT1 : AudioFormat.CHANNEL_OUT_7POINT1_SURROUND; ? AudioFormat.CHANNEL_OUT_7POINT1 : AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
/** /**
* Stream types for an {@link android.media.AudioTrack}.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({STREAM_TYPE_ALARM, STREAM_TYPE_MUSIC, STREAM_TYPE_NOTIFICATION, STREAM_TYPE_RING,
STREAM_TYPE_SYSTEM, STREAM_TYPE_VOICE_CALL})
public @interface StreamType {}
/**
* @see AudioManager#STREAM_ALARM
*/
public static final int STREAM_TYPE_ALARM = AudioManager.STREAM_ALARM;
/**
* @see AudioManager#STREAM_MUSIC
*/
public static final int STREAM_TYPE_MUSIC = AudioManager.STREAM_MUSIC;
/**
* @see AudioManager#STREAM_NOTIFICATION
*/
public static final int STREAM_TYPE_NOTIFICATION = AudioManager.STREAM_NOTIFICATION;
/**
* @see AudioManager#STREAM_RING
*/
public static final int STREAM_TYPE_RING = AudioManager.STREAM_RING;
/**
* @see AudioManager#STREAM_SYSTEM
*/
public static final int STREAM_TYPE_SYSTEM = AudioManager.STREAM_SYSTEM;
/**
* @see AudioManager#STREAM_VOICE_CALL
*/
public static final int STREAM_TYPE_VOICE_CALL = AudioManager.STREAM_VOICE_CALL;
/**
* The default stream type used by audio renderers.
*/
public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC;
/**
* Flags which can apply to a buffer containing a media sample. * Flags which can apply to a buffer containing a media sample.
*/ */
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
...@@ -186,6 +223,29 @@ public final class C { ...@@ -186,6 +223,29 @@ public final class C {
public static final int BUFFER_FLAG_DECODE_ONLY = 0x80000000; public static final int BUFFER_FLAG_DECODE_ONLY = 0x80000000;
/** /**
* Video scaling modes for {@link MediaCodec}-based {@link Renderer}s.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {VIDEO_SCALING_MODE_SCALE_TO_FIT, VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING})
public @interface VideoScalingMode {}
/**
* @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT
*/
@SuppressWarnings("InlinedApi")
public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT =
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT;
/**
* @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT
*/
@SuppressWarnings("InlinedApi")
public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING =
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING;
/**
* A default video scaling mode for {@link MediaCodec}-based {@link Renderer}s.
*/
public static final int VIDEO_SCALING_MODE_DEFAULT = VIDEO_SCALING_MODE_SCALE_TO_FIT;
/**
* Track selection flags. * Track selection flags.
*/ */
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
...@@ -397,22 +457,46 @@ public final class C { ...@@ -397,22 +457,46 @@ public final class C {
public static final int MSG_SET_SURFACE = 1; public static final int MSG_SET_SURFACE = 1;
/** /**
* The type of a message that can be passed to an audio {@link Renderer} via * A type of a message that can be passed to an audio {@link Renderer} via
* {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
* should be a {@link Float} with 0 being silence and 1 being unity gain. * should be a {@link Float} with 0 being silence and 1 being unity gain.
*/ */
public static final int MSG_SET_VOLUME = 2; public static final int MSG_SET_VOLUME = 2;
/** /**
* The type of a message that can be passed to an audio {@link Renderer} via * A type of a message that can be passed to an audio {@link Renderer} via
* {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object * {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
* should be a {@link android.media.PlaybackParams}, which will be used to configure the * should be a {@link android.media.PlaybackParams}, or null, which will be used to configure the
* underlying {@link android.media.AudioTrack}. The message object should not be modified by the * underlying {@link android.media.AudioTrack}. The message object should not be modified by the
* caller after it has been passed * caller after it has been passed
*/ */
public static final int MSG_SET_PLAYBACK_PARAMS = 3; public static final int MSG_SET_PLAYBACK_PARAMS = 3;
/** /**
* A type of a message that can be passed to an audio {@link Renderer} via
* {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
* should be one of the integer stream types in {@link C.StreamType}, and will specify the stream
* type of the underlying {@link android.media.AudioTrack}. See also
* {@link android.media.AudioTrack#AudioTrack(int, int, int, int, int, int)}. If the stream type
* is not set, audio renderers use {@link #STREAM_TYPE_DEFAULT}.
* <p>
* Note that when the stream type changes, the AudioTrack must be reinitialized, which can
* introduce a brief gap in audio output. Note also that tracks in the same audio session must
* share the same routing, so a new audio session id will be generated.
*/
public static final int MSG_SET_STREAM_TYPE = 4;
/**
* The type of a message that can be passed to a {@link MediaCodec}-based video {@link Renderer}
* via {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message
* object should be one of the integer scaling modes in {@link C.VideoScalingMode}.
* <p>
* Note that the scaling mode only applies if the {@link Surface} targeted by the renderer is
* owned by a {@link android.view.SurfaceView}.
*/
public static final int MSG_SET_SCALING_MODE = 5;
/**
* Applications or extensions may define custom {@code MSG_*} constants greater than or equal to * Applications or extensions may define custom {@code MSG_*} constants greater than or equal to
* this value. * this value.
*/ */
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelections; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.upstream.DefaultAllocator;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
...@@ -111,7 +111,7 @@ public final class DefaultLoadControl implements LoadControl { ...@@ -111,7 +111,7 @@ public final class DefaultLoadControl implements LoadControl {
@Override @Override
public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups, public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,
TrackSelections<?> trackSelections) { TrackSelectionArray trackSelections) {
targetBufferSize = 0; targetBufferSize = 0;
for (int i = 0; i < renderers.length; i++) { for (int i = 0; i < renderers.length; i++) {
if (trackSelections.get(i) != null) { if (trackSelections.get(i) != null) {
......
...@@ -22,11 +22,13 @@ import com.google.android.exoplayer2.source.ExtractorMediaSource; ...@@ -22,11 +22,13 @@ import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource; import com.google.android.exoplayer2.source.MergingMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource; import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.text.TextRenderer;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
...@@ -111,6 +113,28 @@ public interface ExoPlayer { ...@@ -111,6 +113,28 @@ public interface ExoPlayer {
interface EventListener { interface EventListener {
/** /**
* Called when the timeline and/or manifest has been refreshed.
* <p>
* Note that if the timeline has changed then a position discontinuity may also have occurred.
* For example the current period index may have changed as a result of periods being added or
* removed from the timeline. The will <em>not</em> be reported via a separate call to
* {@link #onPositionDiscontinuity()}.
*
* @param timeline The latest timeline. Never null, but may be empty.
* @param manifest The latest manifest. May be null.
*/
void onTimelineChanged(Timeline timeline, Object manifest);
/**
* Called when the available or selected tracks change.
*
* @param trackGroups The available tracks. Never null, but may be of length zero.
* @param trackSelections The track selections for each {@link Renderer}. Never null and always
* of length {@link #getRendererCount()}, but may contain null elements.
*/
void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections);
/**
* Called when the player starts or stops loading the source. * Called when the player starts or stops loading the source.
* *
* @param isLoading Whether the source is currently being loaded. * @param isLoading Whether the source is currently being loaded.
...@@ -128,14 +152,6 @@ public interface ExoPlayer { ...@@ -128,14 +152,6 @@ public interface ExoPlayer {
void onPlayerStateChanged(boolean playWhenReady, int playbackState); void onPlayerStateChanged(boolean playWhenReady, int playbackState);
/** /**
* Called when timeline and/or manifest has been refreshed.
*
* @param timeline The latest timeline, or null if the timeline is being cleared.
* @param manifest The latest manifest, or null if the manifest is being cleared.
*/
void onTimelineChanged(Timeline timeline, Object manifest);
/**
* Called when an error occurs. The playback state will transition to {@link #STATE_IDLE} * Called when an error occurs. The playback state will transition to {@link #STATE_IDLE}
* immediately after this method is called. The player instance can still be used, and * immediately after this method is called. The player instance can still be used, and
* {@link #release()} must still be called on the player should it no longer be required. * {@link #release()} must still be called on the player should it no longer be required.
...@@ -145,9 +161,14 @@ public interface ExoPlayer { ...@@ -145,9 +161,14 @@ public interface ExoPlayer {
void onPlayerError(ExoPlaybackException error); void onPlayerError(ExoPlaybackException error);
/** /**
* Called when a position discontinuity occurs. Position discontinuities occur when seeks are * Called when a position discontinuity occurs without a change to the timeline. A position
* performed, when playbacks transition from one period in the timeline to the next, and when * discontinuity occurs when the current window or period index changes (as a result of playback
* the player introduces discontinuities internally. * transitioning from one period in the timeline to the next), or when the playback position
* jumps within the period currently being played (as a result of a seek being performed, or
* when the source introduces a discontinuity internally).
* <p>
* When a position discontinuity occurs as a result of a change to the timeline this method is
* <em>not</em> called. {@link #onTimelineChanged(Timeline, Object)} is called in this case.
*/ */
void onPositionDiscontinuity(); void onPositionDiscontinuity();
...@@ -259,11 +280,11 @@ public interface ExoPlayer { ...@@ -259,11 +280,11 @@ public interface ExoPlayer {
* @param resetPosition Whether the playback position should be reset to the default position in * @param resetPosition Whether the playback position should be reset to the default position in
* the first {@link Timeline.Window}. If false, playback will start from the position defined * the first {@link Timeline.Window}. If false, playback will start from the position defined
* by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}. * by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}.
* @param resetTimeline Whether the timeline and manifest should be reset. Should be true unless * @param resetState Whether the timeline, manifest, tracks and track selections should be reset.
* the player is being prepared to play the same media as it was playing previously (e.g. if * Should be true unless the player is being prepared to play the same media as it was playing
* playback failed and is being retried). * previously (e.g. if playback failed and is being retried).
*/ */
void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetTimeline); void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState);
/** /**
* Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}. * Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
...@@ -309,17 +330,19 @@ public interface ExoPlayer { ...@@ -309,17 +330,19 @@ public interface ExoPlayer {
/** /**
* Seeks to a position specified in milliseconds in the current window. * Seeks to a position specified in milliseconds in the current window.
* *
* @param windowPositionMs The seek position in the current window. * @param positionMs The seek position in the current window, or {@link C#TIME_UNSET} to seek to
* the window's default position.
*/ */
void seekTo(long windowPositionMs); void seekTo(long positionMs);
/** /**
* Seeks to a position specified in milliseconds in the specified window. * Seeks to a position specified in milliseconds in the specified window.
* *
* @param windowIndex The index of the window. * @param windowIndex The index of the window.
* @param windowPositionMs The seek position in the specified window. * @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek to
* the window's default position.
*/ */
void seekTo(int windowIndex, long windowPositionMs); void seekTo(int windowIndex, long positionMs);
/** /**
* Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention * Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention
...@@ -357,18 +380,42 @@ public interface ExoPlayer { ...@@ -357,18 +380,42 @@ public interface ExoPlayer {
void blockingSendMessages(ExoPlayerMessage... messages); void blockingSendMessages(ExoPlayerMessage... messages);
/** /**
* Returns the number of renderers.
*/
int getRendererCount();
/**
* Returns the track type that the renderer at a given index handles.
*
* @see Renderer#getTrackType()
* @param index The index of the renderer.
* @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
*/
int getRendererType(int index);
/**
* Returns the available track groups.
*/
TrackGroupArray getCurrentTrackGroups();
/**
* Returns the current track selections for each renderer.
*/
TrackSelectionArray getCurrentTrackSelections();
/**
* Returns the current manifest. The type depends on the {@link MediaSource} passed to * Returns the current manifest. The type depends on the {@link MediaSource} passed to
* {@link #prepare}. * {@link #prepare}. May be null.
*/ */
Object getCurrentManifest(); Object getCurrentManifest();
/** /**
* Returns the current {@link Timeline}, or {@code null} if there is no timeline. * Returns the current {@link Timeline}. Never null, but may be empty.
*/ */
Timeline getCurrentTimeline(); Timeline getCurrentTimeline();
/** /**
* Returns the index of the period currently being played, or {@link C#INDEX_UNSET} if unknown. * Returns the index of the period currently being played.
*/ */
int getCurrentPeriodIndex(); int getCurrentPeriodIndex();
......
...@@ -42,14 +42,14 @@ public final class ExoPlayerFactory { ...@@ -42,14 +42,14 @@ public final class ExoPlayerFactory {
* @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance.
* @param loadControl The {@link LoadControl} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance.
*/ */
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector<?> trackSelector, public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
LoadControl loadControl) { LoadControl loadControl) {
return newSimpleInstance(context, trackSelector, loadControl, null); return newSimpleInstance(context, trackSelector, loadControl, null);
} }
/** /**
* Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated * Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
* {@link Looper}. * {@link Looper}. Available extension renderers are not used.
* *
* @param context A {@link Context}. * @param context A {@link Context}.
* @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance.
...@@ -57,9 +57,10 @@ public final class ExoPlayerFactory { ...@@ -57,9 +57,10 @@ public final class ExoPlayerFactory {
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
* will not be used for DRM protected playbacks. * will not be used for DRM protected playbacks.
*/ */
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector<?> trackSelector, public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) { LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
return newSimpleInstance(context, trackSelector, loadControl, drmSessionManager, false); return newSimpleInstance(context, trackSelector, loadControl,
drmSessionManager, SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF);
} }
/** /**
...@@ -71,15 +72,15 @@ public final class ExoPlayerFactory { ...@@ -71,15 +72,15 @@ public final class ExoPlayerFactory {
* @param loadControl The {@link LoadControl} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
* will not be used for DRM protected playbacks. * will not be used for DRM protected playbacks.
* @param preferExtensionDecoders True to prefer {@link Renderer} instances defined in * @param extensionRendererMode The extension renderer mode, which determines if and how available
* available extensions over those defined in the core library. Note that extensions must be * extension renderers are used. Note that extensions must be included in the application
* included in the application build for setting this flag to have any effect. * build for them to be considered available.
*/ */
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector<?> trackSelector, public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
boolean preferExtensionDecoders) { @SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode) {
return newSimpleInstance(context, trackSelector, loadControl, drmSessionManager, return newSimpleInstance(context, trackSelector, loadControl, drmSessionManager,
preferExtensionDecoders, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS);
} }
/** /**
...@@ -91,17 +92,18 @@ public final class ExoPlayerFactory { ...@@ -91,17 +92,18 @@ public final class ExoPlayerFactory {
* @param loadControl The {@link LoadControl} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
* will not be used for DRM protected playbacks. * will not be used for DRM protected playbacks.
* @param preferExtensionDecoders True to prefer {@link Renderer} instances defined in * @param extensionRendererMode The extension renderer mode, which determines if and how available
* available extensions over those defined in the core library. Note that extensions must be * extension renderers are used. Note that extensions must be included in the application
* included in the application build for setting this flag to have any effect. * build for them to be considered available.
* @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to * @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to
* seamlessly join an ongoing playback. * seamlessly join an ongoing playback.
*/ */
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector<?> trackSelector, public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
boolean preferExtensionDecoders, long allowedVideoJoiningTimeMs) { @SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode,
long allowedVideoJoiningTimeMs) {
return new SimpleExoPlayer(context, trackSelector, loadControl, drmSessionManager, return new SimpleExoPlayer(context, trackSelector, loadControl, drmSessionManager,
preferExtensionDecoders, allowedVideoJoiningTimeMs); extensionRendererMode, allowedVideoJoiningTimeMs);
} }
/** /**
...@@ -111,7 +113,7 @@ public final class ExoPlayerFactory { ...@@ -111,7 +113,7 @@ public final class ExoPlayerFactory {
* @param renderers The {@link Renderer}s that will be used by the instance. * @param renderers The {@link Renderer}s that will be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance.
*/ */
public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector<?> trackSelector) { public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector) {
return newInstance(renderers, trackSelector, new DefaultLoadControl()); return newInstance(renderers, trackSelector, new DefaultLoadControl());
} }
...@@ -123,7 +125,7 @@ public final class ExoPlayerFactory { ...@@ -123,7 +125,7 @@ public final class ExoPlayerFactory {
* @param trackSelector The {@link TrackSelector} that will be used by the instance. * @param trackSelector The {@link TrackSelector} that will be used by the instance.
* @param loadControl The {@link LoadControl} that will be used by the instance. * @param loadControl The {@link LoadControl} that will be used by the instance.
*/ */
public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector<?> trackSelector, public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector,
LoadControl loadControl) { LoadControl loadControl) {
return new ExoPlayerImpl(renderers, trackSelector, loadControl); return new ExoPlayerImpl(renderers, trackSelector, loadControl);
} }
......
...@@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo { ...@@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo {
/** /**
* The version of the library, expressed as a string. * The version of the library, expressed as a string.
*/ */
String VERSION = "2.0.4"; String VERSION = "2.1.0";
/** /**
* The version of the library, expressed as an integer. * The version of the library, expressed as an integer.
...@@ -32,7 +32,7 @@ public interface ExoPlayerLibraryInfo { ...@@ -32,7 +32,7 @@ public interface ExoPlayerLibraryInfo {
* corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding * corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding
* integer version 123045006 (123-045-006). * integer version 123045006 (123-045-006).
*/ */
int VERSION_INT = 2000004; int VERSION_INT = 2001000;
/** /**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
...@@ -45,5 +45,5 @@ public interface ExoPlayerLibraryInfo { ...@@ -45,5 +45,5 @@ public interface ExoPlayerLibraryInfo {
* trace enabled. * trace enabled.
*/ */
boolean TRACE_ENABLED = true; boolean TRACE_ENABLED = true;
} }
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2;
/**
* Thrown when an attempt is made to seek to a position that does not exist in the player's
* {@link Timeline}.
*/
public final class IllegalSeekPositionException extends IllegalStateException {
/**
* The {@link Timeline} in which the seek was attempted.
*/
public final Timeline timeline;
/**
* The index of the window being seeked to.
*/
public final int windowIndex;
/**
* The seek position in the specified window.
*/
public final long positionMs;
/**
* @param timeline The {@link Timeline} in which the seek was attempted.
* @param windowIndex The index of the window being seeked to.
* @param positionMs The seek position in the specified window.
*/
public IllegalSeekPositionException(Timeline timeline, int windowIndex, long positionMs) {
this.timeline = timeline;
this.windowIndex = windowIndex;
this.positionMs = positionMs;
}
}
...@@ -17,7 +17,7 @@ package com.google.android.exoplayer2; ...@@ -17,7 +17,7 @@ package com.google.android.exoplayer2;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelections; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
/** /**
...@@ -38,7 +38,7 @@ public interface LoadControl { ...@@ -38,7 +38,7 @@ public interface LoadControl {
* @param trackSelections The track selections that were made. * @param trackSelections The track selections that were made.
*/ */
void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups, void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,
TrackSelections<?> trackSelections); TrackSelectionArray trackSelections);
/** /**
* Called by the player when stopped. * Called by the player when stopped.
......
...@@ -149,7 +149,13 @@ public interface Renderer extends ExoPlayerComponent { ...@@ -149,7 +149,13 @@ public interface Renderer extends ExoPlayerComponent {
* This method may be called when the renderer is in the following states: * This method may be called when the renderer is in the following states:
* {@link #STATE_ENABLED}, {@link #STATE_STARTED}. * {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
*/ */
void setCurrentStreamIsFinal(); void setCurrentStreamFinal();
/**
* Returns whether the current {@link SampleStream} will be the final one supplied before the
* renderer is next disabled or reset.
*/
boolean isCurrentStreamFinal();
/** /**
* Throws an error that's preventing the renderer from reading from its {@link SampleStream}. Does * Throws an error that's preventing the renderer from reading from its {@link SampleStream}. Does
......
...@@ -92,6 +92,46 @@ package com.google.android.exoplayer2; ...@@ -92,6 +92,46 @@ package com.google.android.exoplayer2;
public abstract class Timeline { public abstract class Timeline {
/** /**
* An empty timeline.
*/
public static final Timeline EMPTY = new Timeline() {
@Override
public int getWindowCount() {
return 0;
}
@Override
public Window getWindow(int windowIndex, Window window, boolean setIds,
long defaultPositionProjectionUs) {
throw new IndexOutOfBoundsException();
}
@Override
public int getPeriodCount() {
return 0;
}
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
throw new IndexOutOfBoundsException();
}
@Override
public int getIndexOfPeriod(Object uid) {
return C.INDEX_UNSET;
}
};
/**
* Returns whether the timeline is empty.
*/
public final boolean isEmpty() {
return getWindowCount() == 0;
}
/**
* Returns the number of windows in the timeline. * Returns the number of windows in the timeline.
*/ */
public abstract int getWindowCount(); public abstract int getWindowCount();
...@@ -114,10 +154,26 @@ public abstract class Timeline { ...@@ -114,10 +154,26 @@ public abstract class Timeline {
* @param windowIndex The index of the window. * @param windowIndex The index of the window.
* @param window The {@link Window} to populate. Must not be null. * @param window The {@link Window} to populate. Must not be null.
* @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to * @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to
* null. The caller should pass false for efficiency reasons unless the field is required. * null. The caller should pass false for efficiency reasons unless the field is required.
* @return The populated {@link Window}, for convenience. * @return The populated {@link Window}, for convenience.
*/ */
public abstract Window getWindow(int windowIndex, Window window, boolean setIds); public Window getWindow(int windowIndex, Window window, boolean setIds) {
return getWindow(windowIndex, window, setIds, 0);
}
/**
* Populates a {@link Window} with data for the window at the specified index.
*
* @param windowIndex The index of the window.
* @param window The {@link Window} to populate. Must not be null.
* @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to
* null. The caller should pass false for efficiency reasons unless the field is required.
* @param defaultPositionProjectionUs A duration into the future that the populated window's
* default start position should be projected.
* @return The populated {@link Window}, for convenience.
*/
public abstract Window getWindow(int windowIndex, Window window, boolean setIds,
long defaultPositionProjectionUs);
/** /**
* Returns the number of periods in the timeline. * Returns the number of periods in the timeline.
...@@ -181,8 +237,8 @@ public abstract class Timeline { ...@@ -181,8 +237,8 @@ public abstract class Timeline {
public long presentationStartTimeMs; public long presentationStartTimeMs;
/** /**
* The windows start time in milliseconds since the epoch, or {@link C#TIME_UNSET} if unknown or * The window's start time in milliseconds since the epoch, or {@link C#TIME_UNSET} if unknown
* not applicable. For informational purposes only. * or not applicable. For informational purposes only.
*/ */
public long windowStartTimeMs; public long windowStartTimeMs;
...@@ -206,9 +262,24 @@ public abstract class Timeline { ...@@ -206,9 +262,24 @@ public abstract class Timeline {
*/ */
public int lastPeriodIndex; public int lastPeriodIndex;
private long defaultPositionUs; /**
private long durationUs; * The default position relative to the start of the window at which to begin playback, in
private long positionInFirstPeriodUs; * microseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a
* non-zero default position projection, and if the specified projection cannot be performed
* whilst remaining within the bounds of the window.
*/
public long defaultPositionUs;
/**
* The duration of this window in microseconds, or {@link C#TIME_UNSET} if unknown.
*/
public long durationUs;
/**
* The position of the start of this window relative to the start of the first period belonging
* to it, in microseconds.
*/
public long positionInFirstPeriodUs;
/** /**
* Sets the data held by this window. * Sets the data held by this window.
...@@ -231,7 +302,9 @@ public abstract class Timeline { ...@@ -231,7 +302,9 @@ public abstract class Timeline {
/** /**
* Returns the default position relative to the start of the window at which to begin playback, * Returns the default position relative to the start of the window at which to begin playback,
* in milliseconds. * in milliseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a
* non-zero default position projection, and if the specified projection cannot be performed
* whilst remaining within the bounds of the window.
*/ */
public long getDefaultPositionMs() { public long getDefaultPositionMs() {
return C.usToMs(defaultPositionUs); return C.usToMs(defaultPositionUs);
...@@ -239,7 +312,9 @@ public abstract class Timeline { ...@@ -239,7 +312,9 @@ public abstract class Timeline {
/** /**
* Returns the default position relative to the start of the window at which to begin playback, * Returns the default position relative to the start of the window at which to begin playback,
* in microseconds. * in microseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a
* non-zero default position projection, and if the specified projection cannot be performed
* whilst remaining within the bounds of the window.
*/ */
public long getDefaultPositionUs() { public long getDefaultPositionUs() {
return defaultPositionUs; return defaultPositionUs;
...@@ -303,7 +378,11 @@ public abstract class Timeline { ...@@ -303,7 +378,11 @@ public abstract class Timeline {
*/ */
public int windowIndex; public int windowIndex;
private long durationUs; /**
* The duration of this period in microseconds, or {@link C#TIME_UNSET} if unknown.
*/
public long durationUs;
private long positionInWindowUs; private long positionInWindowUs;
/** /**
......
...@@ -27,4 +27,15 @@ public abstract class AudioDecoderException extends Exception { ...@@ -27,4 +27,15 @@ public abstract class AudioDecoderException extends Exception {
super(detailMessage); super(detailMessage);
} }
/**
* @param detailMessage The detail message for this exception.
* @param cause the cause (which is saved for later retrieval by the
* {@link #getCause()} method). (A <tt>null</tt> value is
* permitted, and indicates that the cause is nonexistent or
* unknown.)
*/
public AudioDecoderException(String detailMessage, Throwable cause) {
super(detailMessage, cause);
}
} }
...@@ -16,14 +16,12 @@ ...@@ -16,14 +16,12 @@
package com.google.android.exoplayer2.audio; package com.google.android.exoplayer2.audio;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.media.AudioManager;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.MediaCrypto; import android.media.MediaCrypto;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.media.PlaybackParams; import android.media.PlaybackParams;
import android.media.audiofx.Virtualizer; import android.media.audiofx.Virtualizer;
import android.os.Handler; import android.os.Handler;
import android.os.SystemClock;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
...@@ -43,7 +41,8 @@ import java.nio.ByteBuffer; ...@@ -43,7 +41,8 @@ import java.nio.ByteBuffer;
* Decodes and renders audio using {@link MediaCodec} and {@link AudioTrack}. * Decodes and renders audio using {@link MediaCodec} and {@link AudioTrack}.
*/ */
@TargetApi(16) @TargetApi(16)
public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock { public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock,
AudioTrack.Listener {
private final EventDispatcher eventDispatcher; private final EventDispatcher eventDispatcher;
private final AudioTrack audioTrack; private final AudioTrack audioTrack;
...@@ -55,9 +54,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -55,9 +54,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
private long currentPositionUs; private long currentPositionUs;
private boolean allowPositionDiscontinuity; private boolean allowPositionDiscontinuity;
private boolean audioTrackHasData;
private long lastFeedElapsedRealtimeMs;
/** /**
* @param mediaCodecSelector A decoder selector. * @param mediaCodecSelector A decoder selector.
*/ */
...@@ -76,7 +72,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -76,7 +72,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
* has obtained the keys necessary to decrypt encrypted regions of the media. * has obtained the keys necessary to decrypt encrypted regions of the media.
*/ */
public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector,
DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys) { DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
boolean playClearSamplesWithoutKeys) {
this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, null, null); this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, null, null);
} }
...@@ -109,7 +106,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -109,7 +106,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
boolean playClearSamplesWithoutKeys, Handler eventHandler, boolean playClearSamplesWithoutKeys, Handler eventHandler,
AudioRendererEventListener eventListener) { AudioRendererEventListener eventListener) {
this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler,
eventListener, null, AudioManager.STREAM_MUSIC); eventListener, null);
} }
/** /**
...@@ -126,16 +123,14 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -126,16 +123,14 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
* @param eventListener A listener of events. 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 audioCapabilities The audio capabilities for playback on this device. May be null if the * @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed. * default capabilities (no encoded audio passthrough support) should be assumed.
* @param streamType The type of audio stream for the {@link AudioTrack}.
*/ */
public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector,
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
boolean playClearSamplesWithoutKeys, Handler eventHandler, boolean playClearSamplesWithoutKeys, Handler eventHandler,
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) {
int streamType) {
super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys);
audioSessionId = AudioTrack.SESSION_ID_NOT_SET; audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
audioTrack = new AudioTrack(audioCapabilities, streamType); audioTrack = new AudioTrack(audioCapabilities, this);
eventDispatcher = new EventDispatcher(eventHandler, eventListener); eventDispatcher = new EventDispatcher(eventHandler, eventListener);
} }
...@@ -149,7 +144,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -149,7 +144,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
if (allowPassthrough(mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null) { if (allowPassthrough(mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null) {
return ADAPTIVE_NOT_SEAMLESS | FORMAT_HANDLED; return ADAPTIVE_NOT_SEAMLESS | FORMAT_HANDLED;
} }
MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, false); MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, false, false);
if (decoderInfo == null) { if (decoderInfo == null) {
return FORMAT_UNSUPPORTED_SUBTYPE; return FORMAT_UNSUPPORTED_SUBTYPE;
} }
...@@ -340,29 +335,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -340,29 +335,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
} else { } else {
audioTrack.initialize(audioSessionId); audioTrack.initialize(audioSessionId);
} }
audioTrackHasData = false;
} catch (AudioTrack.InitializationException e) { } catch (AudioTrack.InitializationException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex()); throw ExoPlaybackException.createForRenderer(e, getIndex());
} }
if (getState() == STATE_STARTED) { if (getState() == STATE_STARTED) {
audioTrack.play(); audioTrack.play();
} }
} else {
// Check for AudioTrack underrun.
boolean audioTrackHadData = audioTrackHasData;
audioTrackHasData = audioTrack.hasPendingData();
if (audioTrackHadData && !audioTrackHasData && getState() == STATE_STARTED) {
long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs;
long bufferSizeMs = C.usToMs(audioTrack.getBufferSizeUs());
eventDispatcher.audioTrackUnderrun(audioTrack.getBufferSize(), bufferSizeMs,
elapsedSinceLastFeedMs);
}
} }
int handleBufferResult; int handleBufferResult;
try { try {
handleBufferResult = audioTrack.handleBuffer(buffer, bufferPresentationTimeUs); handleBufferResult = audioTrack.handleBuffer(buffer, bufferPresentationTimeUs);
lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime();
} catch (AudioTrack.WriteException e) { } catch (AudioTrack.WriteException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex()); throw ExoPlaybackException.createForRenderer(e, getIndex());
} }
...@@ -401,10 +384,23 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -401,10 +384,23 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
case C.MSG_SET_PLAYBACK_PARAMS: case C.MSG_SET_PLAYBACK_PARAMS:
audioTrack.setPlaybackParams((PlaybackParams) message); audioTrack.setPlaybackParams((PlaybackParams) message);
break; break;
case C.MSG_SET_STREAM_TYPE:
@C.StreamType int streamType = (Integer) message;
if (audioTrack.setStreamType(streamType)) {
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
}
break;
default: default:
super.handleMessage(messageType, message); super.handleMessage(messageType, message);
break; break;
} }
} }
// AudioTrack.Listener implementation.
@Override
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment