Commit 86230c45 by ojw28 Committed by GitHub

Merge pull request #2570 from google/dev-v2

r2.3.0
parents aaaa23e5 c8059e59
Showing with 2304 additions and 550 deletions
# Release notes # # Release notes #
### r2.3.0 ###
* GVR extension: Wraps the Google VR Audio SDK to provide spatial audio
rendering. You can read more about the GVR extension
[here](https://medium.com/google-exoplayer/spatial-audio-with-exoplayer-and-gvr-cecb00e9da5f#.xdjebjd7g).
* DASH improvements:
* Support embedded CEA-608 closed captions
([#2362](https://github.com/google/ExoPlayer/issues/2362)).
* Support embedded EMSG events
([#2176](https://github.com/google/ExoPlayer/issues/2176)).
* Support mspr:pro manifest element
([#2386](https://github.com/google/ExoPlayer/issues/2386)).
* Correct handling of empty segment indices at the start of live events
([#1865](https://github.com/google/ExoPlayer/issues/1865)).
* HLS improvements:
* Respect initial track selection
([#2353](https://github.com/google/ExoPlayer/issues/2353)).
* Reduced frequency of media playlist requests when playback position is close
to the live edge ([#2548](https://github.com/google/ExoPlayer/issues/2548)).
* Exposed the master playlist through ExoPlayer.getCurrentManifest()
([#2537](https://github.com/google/ExoPlayer/issues/2537)).
* Support CLOSED-CAPTIONS #EXT-X-MEDIA type
([#341](https://github.com/google/ExoPlayer/issues/341)).
* Fixed handling of negative values in #EXT-X-SUPPORT
([#2495](https://github.com/google/ExoPlayer/issues/2495)).
* Fixed potential endless buffering state for streams with WebVTT subtitles
([#2424](https://github.com/google/ExoPlayer/issues/2424)).
* MPEG-TS improvements:
* Support for multiple programs.
* Support for multiple closed captions and caption service descriptors
([#2161](https://github.com/google/ExoPlayer/issues/2161)).
* MP3: Add `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` extractor option to enable
constant bitrate seeking in MP3 files that would otherwise be unseekable
([#2445](https://github.com/google/ExoPlayer/issues/2445)).
* ID3: Better handle malformed ID3 data
([#2486](https://github.com/google/ExoPlayer/issues/2486)).
* Track selection: Added maxVideoBitrate parameter to DefaultTrackSelector.
* DRM: Add support for CENC ClearKey on API level 21+
([#2361](https://github.com/google/ExoPlayer/issues/2361)).
* DRM: Support dynamic setting of key request headers
([#1924](https://github.com/google/ExoPlayer/issues/1924)).
* SmoothStreaming: Fixed handling of start_time placeholder
([#2447](https://github.com/google/ExoPlayer/issues/2447)).
* FLAC extension: Fix proguard configuration
([#2427](https://github.com/google/ExoPlayer/issues/2427)).
* Misc bugfixes.
### r2.2.0 ### ### r2.2.0 ###
* Demo app: Automatic recovery from BehindLiveWindowException, plus improved * Demo app: Automatic recovery from BehindLiveWindowException, plus improved
...@@ -246,6 +293,12 @@ in all V2 releases. This cannot be assumed for changes in r1.5.12 and later, ...@@ -246,6 +293,12 @@ 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 however it can be assumed that all such changes are included in the most recent
V2 release. V2 release.
### r1.5.15 ###
* SmoothStreaming: Fixed handling of start_time placeholder
([#2447](https://github.com/google/ExoPlayer/issues/2447)).
* Misc bugfixes.
### r1.5.14 ### ### r1.5.14 ###
* Fixed cache failures when using an encrypted cache content index. * Fixed cache failures when using an encrypted cache content index.
......
...@@ -11,16 +11,13 @@ ...@@ -11,16 +11,13 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
repositories { repositories {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:2.2.1' classpath 'com.android.tools.build:gradle:2.3.0'
classpath 'com.novoda:bintray-release:0.3.4' classpath 'com.novoda:bintray-release:0.4.0'
} }
} }
...@@ -29,13 +26,24 @@ allprojects { ...@@ -29,13 +26,24 @@ allprojects {
jcenter() jcenter()
} }
project.ext { project.ext {
compileSdkVersion=24 // Important: ExoPlayer specifies a minSdkVersion of 9 because various
targetSdkVersion=24 // components provided by the library may be of use on older devices.
buildToolsVersion='23.0.3' // However, please note that the core media playback functionality
releaseRepoName = 'exoplayer' // provided by the library requires API level 16 or greater.
minSdkVersion=9
compileSdkVersion=25
targetSdkVersion=25
buildToolsVersion='25'
releaseRepoName = getBintrayRepo()
releaseUserOrg = 'google' releaseUserOrg = 'google'
releaseGroupId = 'com.google.android.exoplayer' releaseGroupId = 'com.google.android.exoplayer'
releaseVersion = 'r2.2.0' releaseVersion = 'r2.3.0'
releaseWebsite = 'https://github.com/google/ExoPlayer' releaseWebsite = 'https://github.com/google/ExoPlayer'
} }
} }
def getBintrayRepo() {
boolean publicRepo = hasProperty('publicRepo') &&
property('publicRepo').toBoolean()
return publicRepo ? 'exoplayer' : 'exoplayer-test'
}
...@@ -33,6 +33,11 @@ android { ...@@ -33,6 +33,11 @@ android {
} }
} }
lintOptions {
// The demo app does not have translations.
disable 'MissingTranslation'
}
productFlavors { productFlavors {
noExtensions noExtensions
withExtensions withExtensions
......
...@@ -16,8 +16,8 @@ ...@@ -16,8 +16,8 @@
<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="2200" android:versionCode="2300"
android:versionName="2.2.0"> android:versionName="2.3.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"/>
......
...@@ -278,6 +278,18 @@ ...@@ -278,6 +278,18 @@
] ]
}, },
{ {
"name": "ClearKey DASH",
"samples": [
{
"name": "Big Buck Bunny (CENC ClearKey)",
"uri": "http://html5.cablelabs.com:8100/cenc/ck/dash.mpd",
"extension": "mpd",
"drm_scheme": "cenc",
"drm_license_url": "https://wasabeef.jp/demos/cenc-ck-dash.json"
}
]
},
{
"name": "SmoothStreaming", "name": "SmoothStreaming",
"samples": [ "samples": [
{ {
......
...@@ -101,9 +101,6 @@ import java.util.Locale; ...@@ -101,9 +101,6 @@ import java.util.Locale;
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest) { public void onTimelineChanged(Timeline timeline, Object manifest) {
if (timeline == null) {
return;
}
int periodCount = timeline.getPeriodCount(); int periodCount = timeline.getPeriodCount();
int windowCount = timeline.getWindowCount(); int windowCount = timeline.getWindowCount();
Log.d(TAG, "sourceInfo [periodCount=" + periodCount + ", windowCount=" + windowCount); Log.d(TAG, "sourceInfo [periodCount=" + periodCount + ", windowCount=" + windowCount);
......
...@@ -55,7 +55,7 @@ import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; ...@@ -55,7 +55,7 @@ import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; 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.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
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;
...@@ -70,8 +70,6 @@ import com.google.android.exoplayer2.util.Util; ...@@ -70,8 +70,6 @@ import com.google.android.exoplayer2.util.Util;
import java.net.CookieHandler; import java.net.CookieHandler;
import java.net.CookieManager; import java.net.CookieManager;
import java.net.CookiePolicy; import java.net.CookiePolicy;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
/** /**
...@@ -112,7 +110,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -112,7 +110,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
private DefaultTrackSelector trackSelector; private DefaultTrackSelector trackSelector;
private TrackSelectionHelper trackSelectionHelper; private TrackSelectionHelper trackSelectionHelper;
private DebugTextViewHelper debugViewHelper; private DebugTextViewHelper debugViewHelper;
private boolean playerNeedsSource; private boolean needRetrySource;
private boolean shouldAutoPlay; private boolean shouldAutoPlay;
private int resumeWindow; private int resumeWindow;
...@@ -231,7 +229,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -231,7 +229,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
private void initializePlayer() { private void initializePlayer() {
Intent intent = getIntent(); Intent intent = getIntent();
if (player == null) { boolean needNewPlayer = player == null;
if (needNewPlayer) {
boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false); boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false);
UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA) UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA)
? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null; ? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null;
...@@ -239,19 +238,9 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -239,19 +238,9 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
if (drmSchemeUuid != null) { if (drmSchemeUuid != null) {
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL); String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL);
String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES); String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES);
Map<String, String> keyRequestProperties;
if (keyRequestPropertiesArray == null || keyRequestPropertiesArray.length < 2) {
keyRequestProperties = null;
} else {
keyRequestProperties = new HashMap<>();
for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
keyRequestProperties.put(keyRequestPropertiesArray[i],
keyRequestPropertiesArray[i + 1]);
}
}
try { try {
drmSessionManager = buildDrmSessionManager(drmSchemeUuid, drmLicenseUrl, drmSessionManager = buildDrmSessionManager(drmSchemeUuid, drmLicenseUrl,
keyRequestProperties); keyRequestPropertiesArray);
} catch (UnsupportedDrmException e) { } catch (UnsupportedDrmException e) {
int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported
: (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME : (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
...@@ -267,7 +256,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -267,7 +256,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_ON) : SimpleExoPlayer.EXTENSION_RENDERER_MODE_ON)
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF; : SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF;
TrackSelection.Factory videoTrackSelectionFactory = TrackSelection.Factory videoTrackSelectionFactory =
new AdaptiveVideoTrackSelection.Factory(BANDWIDTH_METER); new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
trackSelectionHelper = new TrackSelectionHelper(trackSelector, videoTrackSelectionFactory); trackSelectionHelper = new TrackSelectionHelper(trackSelector, videoTrackSelectionFactory);
player = ExoPlayerFactory.newSimpleInstance(this, trackSelector, new DefaultLoadControl(), player = ExoPlayerFactory.newSimpleInstance(this, trackSelector, new DefaultLoadControl(),
...@@ -284,9 +273,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -284,9 +273,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
player.setPlayWhenReady(shouldAutoPlay); player.setPlayWhenReady(shouldAutoPlay);
debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper = new DebugTextViewHelper(player, debugTextView);
debugViewHelper.start(); debugViewHelper.start();
playerNeedsSource = true;
} }
if (playerNeedsSource) { if (needNewPlayer || needRetrySource) {
String action = intent.getAction(); String action = intent.getAction();
Uri[] uris; Uri[] uris;
String[] extensions; String[] extensions;
...@@ -322,14 +310,14 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -322,14 +310,14 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
player.seekTo(resumeWindow, resumePosition); player.seekTo(resumeWindow, resumePosition);
} }
player.prepare(mediaSource, !haveResumePosition, false); player.prepare(mediaSource, !haveResumePosition, false);
playerNeedsSource = false; needRetrySource = false;
updateButtonVisibilities(); updateButtonVisibilities();
} }
} }
private MediaSource buildMediaSource(Uri uri, String overrideExtension) { private MediaSource buildMediaSource(Uri uri, String overrideExtension) {
int type = Util.inferContentType(!TextUtils.isEmpty(overrideExtension) ? "." + overrideExtension int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri)
: uri.getLastPathSegment()); : Util.inferContentType("." + overrideExtension);
switch (type) { switch (type) {
case C.TYPE_SS: case C.TYPE_SS:
return new SsMediaSource(uri, buildDataSourceFactory(false), return new SsMediaSource(uri, buildDataSourceFactory(false),
...@@ -349,12 +337,18 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -349,12 +337,18 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
} }
private DrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManager(UUID uuid, private DrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManager(UUID uuid,
String licenseUrl, Map<String, String> keyRequestProperties) throws UnsupportedDrmException { String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException {
if (Util.SDK_INT < 18) { if (Util.SDK_INT < 18) {
return null; return null;
} }
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl,
buildHttpDataSourceFactory(false), keyRequestProperties); buildHttpDataSourceFactory(false));
if (keyRequestPropertiesArray != null) {
for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i],
keyRequestPropertiesArray[i + 1]);
}
}
return new DefaultDrmSessionManager<>(uuid, return new DefaultDrmSessionManager<>(uuid,
FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger); FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger);
} }
...@@ -425,7 +419,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -425,7 +419,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
@Override @Override
public void onPositionDiscontinuity() { public void onPositionDiscontinuity() {
if (playerNeedsSource) { if (needRetrySource) {
// This will only occur if the user has performed a seek whilst in the error state. Update the // This will only occur if the user has performed a seek whilst in the error state. Update the
// resume position so that if the user then retries, playback will resume from the position to // resume position so that if the user then retries, playback will resume from the position to
// which they seeked. // which they seeked.
...@@ -466,7 +460,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -466,7 +460,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
if (errorString != null) { if (errorString != null) {
showToast(errorString); showToast(errorString);
} }
playerNeedsSource = true; needRetrySource = true;
if (isBehindLiveWindow(e)) { if (isBehindLiveWindow(e)) {
clearResumePosition(); clearResumePosition();
initializePlayer(); initializePlayer();
...@@ -498,7 +492,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay ...@@ -498,7 +492,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
private void updateButtonVisibilities() { private void updateButtonVisibilities() {
debugRootView.removeAllViews(); debugRootView.removeAllViews();
retryButton.setVisibility(playerNeedsSource ? View.VISIBLE : View.GONE); retryButton.setVisibility(needRetrySource ? View.VISIBLE : View.GONE);
debugRootView.addView(retryButton); debugRootView.addView(retryButton);
if (player == null) { if (player == null) {
......
...@@ -262,11 +262,13 @@ public class SampleChooserActivity extends Activity { ...@@ -262,11 +262,13 @@ public class SampleChooserActivity extends Activity {
} }
private UUID getDrmUuid(String typeString) throws ParserException { private UUID getDrmUuid(String typeString) throws ParserException {
switch (typeString.toLowerCase()) { switch (Util.toLowerInvariant(typeString)) {
case "widevine": case "widevine":
return C.WIDEVINE_UUID; return C.WIDEVINE_UUID;
case "playready": case "playready":
return C.PLAYREADY_UUID; return C.PLAYREADY_UUID;
case "cenc":
return C.CLEARKEY_UUID;
default: default:
try { try {
return UUID.fromString(typeString); return UUID.fromString(typeString);
......
...@@ -51,7 +51,7 @@ import java.util.Locale; ...@@ -51,7 +51,7 @@ import java.util.Locale;
private static final TrackSelection.Factory RANDOM_FACTORY = new RandomTrackSelection.Factory(); private static final TrackSelection.Factory RANDOM_FACTORY = new RandomTrackSelection.Factory();
private final MappingTrackSelector selector; private final MappingTrackSelector selector;
private final TrackSelection.Factory adaptiveVideoTrackSelectionFactory; private final TrackSelection.Factory adaptiveTrackSelectionFactory;
private MappedTrackInfo trackInfo; private MappedTrackInfo trackInfo;
private int rendererIndex; private int rendererIndex;
...@@ -67,13 +67,13 @@ import java.util.Locale; ...@@ -67,13 +67,13 @@ import java.util.Locale;
/** /**
* @param selector The track selector. * @param selector The track selector.
* @param adaptiveVideoTrackSelectionFactory A factory for adaptive video {@link TrackSelection}s, * @param adaptiveTrackSelectionFactory A factory for adaptive {@link TrackSelection}s, or null
* or null if the selection helper should not support adaptive video. * if the selection helper should not support adaptive tracks.
*/ */
public TrackSelectionHelper(MappingTrackSelector selector, public TrackSelectionHelper(MappingTrackSelector selector,
TrackSelection.Factory adaptiveVideoTrackSelectionFactory) { TrackSelection.Factory adaptiveTrackSelectionFactory) {
this.selector = selector; this.selector = selector;
this.adaptiveVideoTrackSelectionFactory = adaptiveVideoTrackSelectionFactory; this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory;
} }
/** /**
...@@ -92,7 +92,7 @@ import java.util.Locale; ...@@ -92,7 +92,7 @@ import java.util.Locale;
trackGroups = trackInfo.getTrackGroups(rendererIndex); trackGroups = trackInfo.getTrackGroups(rendererIndex);
trackGroupsAdaptive = new boolean[trackGroups.length]; trackGroupsAdaptive = new boolean[trackGroups.length];
for (int i = 0; i < trackGroups.length; i++) { for (int i = 0; i < trackGroups.length; i++) {
trackGroupsAdaptive[i] = adaptiveVideoTrackSelectionFactory != null trackGroupsAdaptive[i] = adaptiveTrackSelectionFactory != null
&& trackInfo.getAdaptiveSupport(rendererIndex, i, false) && trackInfo.getAdaptiveSupport(rendererIndex, i, false)
!= RendererCapabilities.ADAPTIVE_NOT_SUPPORTED != RendererCapabilities.ADAPTIVE_NOT_SUPPORTED
&& trackGroups.get(i).length > 1; && trackGroups.get(i).length > 1;
...@@ -271,7 +271,7 @@ import java.util.Locale; ...@@ -271,7 +271,7 @@ import java.util.Locale;
private void setOverride(int group, int[] tracks, boolean enableRandomAdaptation) { private void setOverride(int group, int[] tracks, boolean enableRandomAdaptation) {
TrackSelection.Factory factory = tracks.length == 1 ? FIXED_FACTORY TrackSelection.Factory factory = tracks.length == 1 ? FIXED_FACTORY
: (enableRandomAdaptation ? RANDOM_FACTORY : adaptiveVideoTrackSelectionFactory); : (enableRandomAdaptation ? RANDOM_FACTORY : adaptiveTrackSelectionFactory);
override = new SelectionOverride(factory, group, tracks); override = new SelectionOverride(factory, group, tracks);
} }
...@@ -301,15 +301,18 @@ import java.util.Locale; ...@@ -301,15 +301,18 @@ import java.util.Locale;
private static String buildTrackName(Format format) { private static String buildTrackName(Format format) {
String trackName; String trackName;
if (MimeTypes.isVideo(format.sampleMimeType)) { if (MimeTypes.isVideo(format.sampleMimeType)) {
trackName = joinWithSeparator(joinWithSeparator(buildResolutionString(format), trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(
buildBitrateString(format)), buildTrackIdString(format)); buildResolutionString(format), buildBitrateString(format)), buildTrackIdString(format)),
buildSampleMimeTypeString(format));
} else if (MimeTypes.isAudio(format.sampleMimeType)) { } else if (MimeTypes.isAudio(format.sampleMimeType)) {
trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format), trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(joinWithSeparator(
buildAudioPropertyString(format)), buildBitrateString(format)), buildLanguageString(format), buildAudioPropertyString(format)),
buildTrackIdString(format)); buildBitrateString(format)), buildTrackIdString(format)),
buildSampleMimeTypeString(format));
} else { } else {
trackName = joinWithSeparator(joinWithSeparator(buildLanguageString(format), trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format),
buildBitrateString(format)), buildTrackIdString(format)); buildBitrateString(format)), buildTrackIdString(format)),
buildSampleMimeTypeString(format));
} }
return trackName.length() == 0 ? "unknown" : trackName; return trackName.length() == 0 ? "unknown" : trackName;
} }
...@@ -342,4 +345,8 @@ import java.util.Locale; ...@@ -342,4 +345,8 @@ import java.util.Locale;
return format.id == null ? "" : ("id:" + format.id); return format.id == null ? "" : ("id:" + format.id);
} }
private static String buildSampleMimeTypeString(Format format) {
return format.sampleMimeType == null ? "" : format.sampleMimeType;
}
} }
...@@ -18,7 +18,7 @@ android { ...@@ -18,7 +18,7 @@ android {
buildToolsVersion project.ext.buildToolsVersion buildToolsVersion project.ext.buildToolsVersion
defaultConfig { defaultConfig {
minSdkVersion 9 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
......
...@@ -118,7 +118,8 @@ public final class CronetDataSourceTest { ...@@ -118,7 +118,8 @@ public final class CronetDataSourceTest {
TEST_CONNECT_TIMEOUT_MS, TEST_CONNECT_TIMEOUT_MS,
TEST_READ_TIMEOUT_MS, TEST_READ_TIMEOUT_MS,
true, // resetTimeoutOnRedirects true, // resetTimeoutOnRedirects
mockClock)); mockClock,
null));
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true); when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);
when(mockCronetEngine.newUrlRequestBuilder( when(mockCronetEngine.newUrlRequestBuilder(
anyString(), any(UrlRequest.Callback.class), any(Executor.class))) anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
......
...@@ -32,7 +32,6 @@ import java.io.IOException; ...@@ -32,7 +32,6 @@ import java.io.IOException;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
...@@ -98,7 +97,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou ...@@ -98,7 +97,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
private final int connectTimeoutMs; private final int connectTimeoutMs;
private final int readTimeoutMs; private final int readTimeoutMs;
private final boolean resetTimeoutOnRedirects; private final boolean resetTimeoutOnRedirects;
private final Map<String, String> requestProperties; private final RequestProperties defaultRequestProperties;
private final RequestProperties requestProperties;
private final ConditionVariable operation; private final ConditionVariable operation;
private final Clock clock; private final Clock clock;
...@@ -136,7 +136,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou ...@@ -136,7 +136,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
public CronetDataSource(CronetEngine cronetEngine, Executor executor, public CronetDataSource(CronetEngine cronetEngine, Executor executor,
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener) { Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener) {
this(cronetEngine, executor, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS, this(cronetEngine, executor, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS,
DEFAULT_READ_TIMEOUT_MILLIS, false); DEFAULT_READ_TIMEOUT_MILLIS, false, null);
} }
/** /**
...@@ -149,17 +149,20 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou ...@@ -149,17 +149,20 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
* @param connectTimeoutMs The connection timeout, in milliseconds. * @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds. * @param readTimeoutMs The read timeout, in milliseconds.
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs. * @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
* @param defaultRequestProperties The default request properties to be used.
*/ */
public CronetDataSource(CronetEngine cronetEngine, Executor executor, public CronetDataSource(CronetEngine cronetEngine, Executor executor,
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener, Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener,
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects) { int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects,
RequestProperties defaultRequestProperties) {
this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs, this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs,
readTimeoutMs, resetTimeoutOnRedirects, new SystemClock()); readTimeoutMs, resetTimeoutOnRedirects, new SystemClock(), defaultRequestProperties);
} }
/* package */ CronetDataSource(CronetEngine cronetEngine, Executor executor, /* package */ CronetDataSource(CronetEngine cronetEngine, Executor executor,
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener, Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener,
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, Clock clock) { int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, Clock clock,
RequestProperties defaultRequestProperties) {
this.cronetEngine = Assertions.checkNotNull(cronetEngine); this.cronetEngine = Assertions.checkNotNull(cronetEngine);
this.executor = Assertions.checkNotNull(executor); this.executor = Assertions.checkNotNull(executor);
this.contentTypePredicate = contentTypePredicate; this.contentTypePredicate = contentTypePredicate;
...@@ -168,7 +171,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou ...@@ -168,7 +171,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
this.readTimeoutMs = readTimeoutMs; this.readTimeoutMs = readTimeoutMs;
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects; this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
this.clock = Assertions.checkNotNull(clock); this.clock = Assertions.checkNotNull(clock);
requestProperties = new HashMap<>(); this.defaultRequestProperties = defaultRequestProperties;
requestProperties = new RequestProperties();
operation = new ConditionVariable(); operation = new ConditionVariable();
} }
...@@ -176,23 +180,17 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou ...@@ -176,23 +180,17 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
@Override @Override
public void setRequestProperty(String name, String value) { public void setRequestProperty(String name, String value) {
synchronized (requestProperties) { requestProperties.set(name, value);
requestProperties.put(name, value);
}
} }
@Override @Override
public void clearRequestProperty(String name) { public void clearRequestProperty(String name) {
synchronized (requestProperties) { requestProperties.remove(name);
requestProperties.remove(name);
}
} }
@Override @Override
public void clearAllRequestProperties() { public void clearAllRequestProperties() {
synchronized (requestProperties) { requestProperties.clear();
requestProperties.clear();
}
} }
@Override @Override
...@@ -421,16 +419,24 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou ...@@ -421,16 +419,24 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(dataSpec.uri.toString(), UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(dataSpec.uri.toString(),
this, executor); this, executor);
// Set the headers. // Set the headers.
synchronized (requestProperties) { boolean isContentTypeHeaderSet = false;
if (dataSpec.postBody != null && dataSpec.postBody.length != 0 if (defaultRequestProperties != null) {
&& !requestProperties.containsKey(CONTENT_TYPE)) { for (Entry<String, String> headerEntry : defaultRequestProperties.getSnapshot().entrySet()) {
throw new OpenException("POST request with non-empty body must set Content-Type", dataSpec, String key = headerEntry.getKey();
Status.IDLE); isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key);
} requestBuilder.addHeader(key, headerEntry.getValue());
for (Entry<String, String> headerEntry : requestProperties.entrySet()) {
requestBuilder.addHeader(headerEntry.getKey(), headerEntry.getValue());
} }
} }
Map<String, String> requestPropertiesSnapshot = requestProperties.getSnapshot();
for (Entry<String, String> headerEntry : requestPropertiesSnapshot.entrySet()) {
String key = headerEntry.getKey();
isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key);
requestBuilder.addHeader(key, headerEntry.getValue());
}
if (dataSpec.postBody != null && dataSpec.postBody.length != 0 && !isContentTypeHeaderSet) {
throw new OpenException("POST request with non-empty body must set Content-Type", dataSpec,
Status.IDLE);
}
// Set the Range header. // Set the Range header.
if (currentDataSpec.position != 0 || currentDataSpec.length != C.LENGTH_UNSET) { if (currentDataSpec.position != 0 || currentDataSpec.length != C.LENGTH_UNSET) {
StringBuilder rangeValue = new StringBuilder(); StringBuilder rangeValue = new StringBuilder();
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.cronet; package com.google.android.exoplayer2.ext.cronet;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
...@@ -68,9 +69,10 @@ public final class CronetDataSourceFactory extends BaseFactory { ...@@ -68,9 +69,10 @@ public final class CronetDataSourceFactory extends BaseFactory {
} }
@Override @Override
protected CronetDataSource createDataSourceInternal() { protected CronetDataSource createDataSourceInternal(HttpDataSource.RequestProperties
defaultRequestProperties) {
return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener, return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener,
connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects); connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, null, defaultRequestProperties);
} }
} }
...@@ -31,9 +31,7 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main" ...@@ -31,9 +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:
``` ```
cd "${FFMPEG_EXT_PATH}/jni" && \ cd "${FFMPEG_EXT_PATH}/jni" && \
...@@ -69,15 +67,14 @@ make -j4 && \ ...@@ -69,15 +67,14 @@ make -j4 && \
make install-libs make install-libs
``` ```
* Build the JNI native libraries. * Build the JNI native libraries. Repeat this step for any other architectures
you need to support.
``` ```
cd "${FFMPEG_EXT_PATH}"/jni && \ cd "${FFMPEG_EXT_PATH}"/jni && \
${NDK_PATH}/ndk-build APP_ABI=armeabi-v7a -j4 ${NDK_PATH}/ndk-build APP_ABI=armeabi-v7a -j4
``` ```
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:
......
...@@ -18,7 +18,7 @@ android { ...@@ -18,7 +18,7 @@ android {
buildToolsVersion project.ext.buildToolsVersion buildToolsVersion project.ext.buildToolsVersion
defaultConfig { defaultConfig {
minSdkVersion 9 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt' consumerProguardFiles 'proguard-rules.txt'
} }
......
...@@ -19,7 +19,7 @@ import android.os.Handler; ...@@ -19,7 +19,7 @@ import android.os.Handler;
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;
import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener;
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.drm.ExoMediaCrypto;
...@@ -43,21 +43,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -43,21 +43,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required. * 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 eventListener A listener of events. May be null if delivery of events is not required.
*/ * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) {
super(eventHandler, eventListener);
}
/**
* @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 FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioCapabilities audioCapabilities) { AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, audioCapabilities); super(eventHandler, eventListener, audioProcessors);
} }
@Override @Override
......
...@@ -18,7 +18,7 @@ android { ...@@ -18,7 +18,7 @@ android {
buildToolsVersion project.ext.buildToolsVersion buildToolsVersion project.ext.buildToolsVersion
defaultConfig { defaultConfig {
minSdkVersion 9 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt' consumerProguardFiles 'proguard-rules.txt'
} }
......
...@@ -5,7 +5,10 @@ ...@@ -5,7 +5,10 @@
native <methods>; native <methods>;
} }
# Some members of this class are being accessed from native methods. Keep them unobfuscated. # Some members of these classes are being accessed from native methods. Keep them unobfuscated.
-keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni { -keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni {
*; *;
} }
-keep class com.google.android.exoplayer2.util.FlacStreamInfo {
*;
}
...@@ -67,7 +67,7 @@ public final class FlacExtractor implements Extractor { ...@@ -67,7 +67,7 @@ public final class FlacExtractor implements Extractor {
@Override @Override
public void init(ExtractorOutput output) { public void init(ExtractorOutput output) {
extractorOutput = output; extractorOutput = output;
trackOutput = extractorOutput.track(0); trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO);
extractorOutput.endTracks(); extractorOutput.endTracks();
try { try {
decoderJni = new FlacDecoderJni(); decoderJni = new FlacDecoderJni();
......
...@@ -17,7 +17,7 @@ package com.google.android.exoplayer2.ext.flac; ...@@ -17,7 +17,7 @@ package com.google.android.exoplayer2.ext.flac;
import android.os.Handler; 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.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener;
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.drm.ExoMediaCrypto;
...@@ -38,21 +38,11 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -38,21 +38,11 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required. * 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 eventListener A listener of events. May be null if delivery of events is not required.
*/ * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) {
super(eventHandler, eventListener);
}
/**
* @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 LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioCapabilities audioCapabilities) { AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, audioCapabilities); super(eventHandler, eventListener, audioProcessors);
} }
@Override @Override
......
...@@ -31,7 +31,7 @@ LOCAL_C_INCLUDES := \ ...@@ -31,7 +31,7 @@ LOCAL_C_INCLUDES := \
LOCAL_SRC_FILES := $(FLAC_SOURCES) LOCAL_SRC_FILES := $(FLAC_SOURCES)
LOCAL_CFLAGS += '-DVERSION="1.3.1"' -DFLAC__NO_MD5 -DFLAC__INTEGER_ONLY_LIBRARY -DFLAC__NO_ASM LOCAL_CFLAGS += '-DVERSION="1.3.1"' -DFLAC__NO_MD5 -DFLAC__INTEGER_ONLY_LIBRARY -DFLAC__NO_ASM
LOCAL_CFLAGS += -D_REENTRANT -DPIC -DU_COMMON_IMPLEMENTATION -fPIC LOCAL_CFLAGS += -D_REENTRANT -DPIC -DU_COMMON_IMPLEMENTATION -fPIC -DHAVE_SYS_PARAM_H
LOCAL_CFLAGS += -O3 -funroll-loops -finline-functions LOCAL_CFLAGS += -O3 -funroll-loops -finline-functions
LOCAL_LDLIBS := -llog -lz -lm LOCAL_LDLIBS := -llog -lz -lm
......
...@@ -453,7 +453,8 @@ int64_t FLACParser::getSeekPosition(int64_t timeUs) { ...@@ -453,7 +453,8 @@ int64_t FLACParser::getSeekPosition(int64_t timeUs) {
} }
FLAC__StreamMetadata_SeekPoint* points = mSeekTable->points; FLAC__StreamMetadata_SeekPoint* points = mSeekTable->points;
for (unsigned i = mSeekTable->num_points - 1; i >= 0; i--) { for (unsigned i = mSeekTable->num_points; i > 0; ) {
i--;
if (points[i].sample_number <= sample) { if (points[i].sample_number <= sample) {
return firstFrameOffset + points[i].stream_offset; return firstFrameOffset + points[i].stream_offset;
} }
......
# ExoPlayer GVR Extension #
## Description ##
The GVR extension wraps the [Google VR SDK for Android][]. It provides a
GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering
of surround sound and ambisonic soundfields.
## Using the extension ##
The easiest way to use the extension is to add it as a gradle dependency. You
need to make sure you have the jcenter repository included in the `build.gradle`
file in the root of your project:
```gradle
repositories {
jcenter()
}
```
Next, include the following in your module's `build.gradle` file:
```gradle
compile 'com.google.android.exoplayer:extension-gvr:rX.X.X'
```
where `rX.X.X` is the version, which must match the version of the ExoPlayer
library being used.
## Using GvrAudioProcessor ##
* If using SimpleExoPlayer, override SimpleExoPlayer.buildAudioProcessors to
return a GvrAudioProcessor.
* If constructing renderers directly, pass a GvrAudioProcessor to
MediaCodecAudioRenderer's constructor.
[Google VR SDK for Android]: https://developers.google.com/vr/android/
[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
apply plugin: 'com.android.library'
android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion 19
targetSdkVersion project.ext.targetSdkVersion
}
}
dependencies {
compile project(':library')
compile 'com.google.vr:sdk-audio:1.30.0'
}
ext {
releaseArtifact = 'extension-gvr'
releaseDescription = 'Google VR extension for ExoPlayer.'
}
apply from: '../../publish.gradle'
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest package="com.google.android.exoplayer2.ext.gvr"/>
/*
* Copyright (C) 2017 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.ext.gvr;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.vr.sdk.audio.GvrAudioSurround;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* An {@link AudioProcessor} that uses {@code GvrAudioSurround} to provide binaural rendering of
* surround sound and ambisonic soundfields.
*/
public final class GvrAudioProcessor implements AudioProcessor {
private static final int FRAMES_PER_OUTPUT_BUFFER = 1024;
private static final int OUTPUT_CHANNEL_COUNT = 2;
private static final int OUTPUT_FRAME_SIZE = OUTPUT_CHANNEL_COUNT * 2; // 16-bit stereo output.
private int sampleRateHz;
private int channelCount;
private GvrAudioSurround gvrAudioSurround;
private ByteBuffer buffer;
private boolean inputEnded;
private float w;
private float x;
private float y;
private float z;
/**
* Creates a new GVR audio processor.
*/
public GvrAudioProcessor() {
// Use the identity for the initial orientation.
w = 1f;
sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE;
}
/**
* Updates the listener head orientation. May be called on any thread. See
* {@code GvrAudioSurround.updateNativeOrientation}.
*/
public synchronized void updateOrientation(float w, float x, float y, float z) {
this.w = w;
this.x = x;
this.y = y;
this.z = z;
if (gvrAudioSurround != null) {
gvrAudioSurround.updateNativeOrientation(w, x, y, z);
}
}
@Override
public synchronized boolean configure(int sampleRateHz, int channelCount,
@C.Encoding int encoding) throws UnhandledFormatException {
if (encoding != C.ENCODING_PCM_16BIT) {
maybeReleaseGvrAudioSurround();
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
}
if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount) {
return false;
}
this.sampleRateHz = sampleRateHz;
this.channelCount = channelCount;
maybeReleaseGvrAudioSurround();
int surroundFormat;
switch (channelCount) {
case 2:
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO;
break;
case 4:
surroundFormat = GvrAudioSurround.SurroundFormat.FIRST_ORDER_AMBISONICS;
break;
case 6:
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_FIVE_DOT_ONE;
break;
case 9:
surroundFormat = GvrAudioSurround.SurroundFormat.SECOND_ORDER_AMBISONICS;
break;
case 16:
surroundFormat = GvrAudioSurround.SurroundFormat.THIRD_ORDER_AMBISONICS;
break;
default:
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
}
gvrAudioSurround = new GvrAudioSurround(surroundFormat, sampleRateHz, channelCount,
FRAMES_PER_OUTPUT_BUFFER);
gvrAudioSurround.updateNativeOrientation(w, x, y, z);
if (buffer == null) {
buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE)
.order(ByteOrder.nativeOrder());
}
return true;
}
@Override
public boolean isActive() {
return gvrAudioSurround != null;
}
@Override
public int getOutputChannelCount() {
return OUTPUT_CHANNEL_COUNT;
}
@Override
public int getOutputEncoding() {
return C.ENCODING_PCM_16BIT;
}
@Override
public void queueInput(ByteBuffer input) {
int position = input.position();
int readBytes = gvrAudioSurround.addInput(input, position, input.limit() - position);
input.position(position + readBytes);
}
@Override
public void queueEndOfStream() {
inputEnded = true;
gvrAudioSurround.triggerProcessing();
}
@Override
public ByteBuffer getOutput() {
int writtenBytes = gvrAudioSurround.getOutput(buffer, 0, buffer.capacity());
buffer.position(0).limit(writtenBytes);
return buffer;
}
@Override
public boolean isEnded() {
return inputEnded && gvrAudioSurround.getAvailableOutputSize() == 0;
}
@Override
public void flush() {
gvrAudioSurround.flush();
inputEnded = false;
}
@Override
public synchronized void release() {
buffer = null;
maybeReleaseGvrAudioSurround();
}
private void maybeReleaseGvrAudioSurround() {
if (this.gvrAudioSurround != null) {
GvrAudioSurround gvrAudioSurround = this.gvrAudioSurround;
this.gvrAudioSurround = null;
gvrAudioSurround.release();
}
}
}
...@@ -12,31 +12,31 @@ ...@@ -12,31 +12,31 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply plugin: 'bintray-release'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion buildToolsVersion project.ext.buildToolsVersion
defaultConfig { defaultConfig {
minSdkVersion 9 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} }
lintOptions {
// See: https://github.com/square/okio/issues/58
warning 'InvalidPackage'
}
} }
dependencies { dependencies {
compile project(':library') compile project(':library')
compile('com.squareup.okhttp3:okhttp:3.4.1') { compile('com.squareup.okhttp3:okhttp:3.6.0') {
exclude group: 'org.json' exclude group: 'org.json'
} }
} }
publish { ext {
artifactId = 'extension-okhttp' releaseArtifact = 'extension-okhttp'
description = 'An OkHttp extension for ExoPlayer.' releaseDescription = 'OkHttp extension for ExoPlayer.'
repoName = releaseRepoName
userOrg = releaseUserOrg
groupId = releaseGroupId
version = releaseVersion
website = releaseWebsite
} }
apply from: '../../publish.gradle'
...@@ -27,7 +27,6 @@ import java.io.EOFException; ...@@ -27,7 +27,6 @@ import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InterruptedIOException; import java.io.InterruptedIOException;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
...@@ -51,7 +50,8 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -51,7 +50,8 @@ public class OkHttpDataSource implements HttpDataSource {
private final Predicate<String> contentTypePredicate; private final Predicate<String> contentTypePredicate;
private final TransferListener<? super OkHttpDataSource> listener; private final TransferListener<? super OkHttpDataSource> listener;
private final CacheControl cacheControl; private final CacheControl cacheControl;
private final HashMap<String, String> requestProperties; private final RequestProperties defaultRequestProperties;
private final RequestProperties requestProperties;
private DataSpec dataSpec; private DataSpec dataSpec;
private Response response; private Response response;
...@@ -87,7 +87,7 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -87,7 +87,7 @@ public class OkHttpDataSource implements HttpDataSource {
*/ */
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) {
this(callFactory, userAgent, contentTypePredicate, listener, null); this(callFactory, userAgent, contentTypePredicate, listener, null, null);
} }
/** /**
...@@ -99,16 +99,19 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -99,16 +99,19 @@ public class OkHttpDataSource implements HttpDataSource {
* {@link #open(DataSpec)}. * {@link #open(DataSpec)}.
* @param listener An optional listener. * @param listener An optional listener.
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header. * @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
* @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to
* the server as HTTP headers on every request.
*/ */
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,
CacheControl cacheControl) { CacheControl cacheControl, RequestProperties defaultRequestProperties) {
this.callFactory = Assertions.checkNotNull(callFactory); this.callFactory = Assertions.checkNotNull(callFactory);
this.userAgent = Assertions.checkNotEmpty(userAgent); this.userAgent = Assertions.checkNotEmpty(userAgent);
this.contentTypePredicate = contentTypePredicate; this.contentTypePredicate = contentTypePredicate;
this.listener = listener; this.listener = listener;
this.cacheControl = cacheControl; this.cacheControl = cacheControl;
this.requestProperties = new HashMap<>(); this.defaultRequestProperties = defaultRequestProperties;
this.requestProperties = new RequestProperties();
} }
@Override @Override
...@@ -125,24 +128,18 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -125,24 +128,18 @@ public class OkHttpDataSource implements HttpDataSource {
public void setRequestProperty(String name, String value) { public void setRequestProperty(String name, String value) {
Assertions.checkNotNull(name); Assertions.checkNotNull(name);
Assertions.checkNotNull(value); Assertions.checkNotNull(value);
synchronized (requestProperties) { requestProperties.set(name, value);
requestProperties.put(name, value);
}
} }
@Override @Override
public void clearRequestProperty(String name) { public void clearRequestProperty(String name) {
Assertions.checkNotNull(name); Assertions.checkNotNull(name);
synchronized (requestProperties) { requestProperties.remove(name);
requestProperties.remove(name);
}
} }
@Override @Override
public void clearAllRequestProperties() { public void clearAllRequestProperties() {
synchronized (requestProperties) { requestProperties.clear();
requestProperties.clear();
}
} }
@Override @Override
...@@ -268,11 +265,14 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -268,11 +265,14 @@ public class OkHttpDataSource implements HttpDataSource {
if (cacheControl != null) { if (cacheControl != null) {
builder.cacheControl(cacheControl); builder.cacheControl(cacheControl);
} }
synchronized (requestProperties) { if (defaultRequestProperties != null) {
for (Map.Entry<String, String> property : requestProperties.entrySet()) { for (Map.Entry<String, String> property : defaultRequestProperties.getSnapshot().entrySet()) {
builder.addHeader(property.getKey(), property.getValue()); builder.header(property.getKey(), property.getValue());
} }
} }
for (Map.Entry<String, String> property : requestProperties.getSnapshot().entrySet()) {
builder.header(property.getKey(), property.getValue());
}
if (!(position == 0 && length == C.LENGTH_UNSET)) { if (!(position == 0 && length == C.LENGTH_UNSET)) {
String rangeRequest = "bytes=" + position + "-"; String rangeRequest = "bytes=" + position + "-";
if (length != C.LENGTH_UNSET) { if (length != C.LENGTH_UNSET) {
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.okhttp; package com.google.android.exoplayer2.ext.okhttp;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
...@@ -59,8 +60,10 @@ public final class OkHttpDataSourceFactory extends BaseFactory { ...@@ -59,8 +60,10 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
} }
@Override @Override
protected OkHttpDataSource createDataSourceInternal() { protected OkHttpDataSource createDataSourceInternal(
return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl); HttpDataSource.RequestProperties defaultRequestProperties) {
return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl,
defaultRequestProperties);
} }
} }
...@@ -18,7 +18,7 @@ android { ...@@ -18,7 +18,7 @@ android {
buildToolsVersion project.ext.buildToolsVersion buildToolsVersion project.ext.buildToolsVersion
defaultConfig { defaultConfig {
minSdkVersion 9 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt' consumerProguardFiles 'proguard-rules.txt'
} }
...@@ -32,4 +32,3 @@ android { ...@@ -32,4 +32,3 @@ android {
dependencies { dependencies {
compile project(':library') compile project(':library')
} }
...@@ -17,7 +17,7 @@ package com.google.android.exoplayer2.ext.opus; ...@@ -17,7 +17,7 @@ package com.google.android.exoplayer2.ext.opus;
import android.os.Handler; 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.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener;
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.DrmSessionManager;
...@@ -40,35 +40,24 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -40,35 +40,24 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required. * 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 eventListener A listener of events. May be null if delivery of events is not required.
*/ * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) {
super(eventHandler, eventListener);
}
/**
* @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, public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioCapabilities audioCapabilities) { AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, audioCapabilities); super(eventHandler, eventListener, audioProcessors);
} }
/** /**
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required. * 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 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 audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
* default capabilities (no encoded audio passthrough support) should be assumed.
*/ */
public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener, public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioCapabilities audioCapabilities, DrmSessionManager<ExoMediaCrypto> drmSessionManager, DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys,
boolean playClearSamplesWithoutKeys) { AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, audioCapabilities, drmSessionManager, super(eventHandler, eventListener, null, drmSessionManager, playClearSamplesWithoutKeys,
playClearSamplesWithoutKeys); audioProcessors);
} }
@Override @Override
......
...@@ -213,7 +213,7 @@ import java.util.List; ...@@ -213,7 +213,7 @@ import java.util.List;
SimpleOutputBuffer outputBuffer, int sampleRate); SimpleOutputBuffer outputBuffer, int sampleRate);
private native int opusSecureDecode(long decoder, long timeUs, ByteBuffer inputBuffer, private native int opusSecureDecode(long decoder, long timeUs, ByteBuffer inputBuffer,
int inputSize, SimpleOutputBuffer outputBuffer, int sampleRate, int inputSize, SimpleOutputBuffer outputBuffer, int sampleRate,
ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv, ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv,
int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData); 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);
......
...@@ -40,6 +40,18 @@ git clone https://chromium.googlesource.com/webm/libvpx libvpx && \ ...@@ -40,6 +40,18 @@ git clone https://chromium.googlesource.com/webm/libvpx libvpx && \
git clone https://chromium.googlesource.com/libyuv/libyuv libyuv git clone https://chromium.googlesource.com/libyuv/libyuv libyuv
``` ```
* Checkout the appropriate branches of libvpx and libyuv (the scripts and
makefiles bundled in this repo are known to work only at these versions of the
libraries - we will update this periodically as newer versions of
libvpx/libyuv are released):
```
cd "${VP9_EXT_PATH}/jni/libvpx" && \
git checkout tags/v1.6.1 -b v1.6.1 && \
cd "${VP9_EXT_PATH}/jni/libyuv" && \
git checkout e2611a73
```
* Run a script that generates necessary configuration files for libvpx: * Run a script that generates necessary configuration files for libvpx:
``` ```
...@@ -79,5 +91,7 @@ dependencies { ...@@ -79,5 +91,7 @@ dependencies {
`generate_libvpx_android_configs.sh` `generate_libvpx_android_configs.sh`
* Clean and re-build the project. * Clean and re-build the project.
* If you want to use your own version of libvpx or libyuv, place it in * If you want to use your own version of libvpx or libyuv, place it in
`${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. `${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. But
please note that `generate_libvpx_android_configs.sh` and the makefiles need
to be modified to work with arbitrary versions of libvpx and libyuv.
...@@ -18,7 +18,7 @@ android { ...@@ -18,7 +18,7 @@ android {
buildToolsVersion project.ext.buildToolsVersion buildToolsVersion project.ext.buildToolsVersion
defaultConfig { defaultConfig {
minSdkVersion 9 minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt' consumerProguardFiles 'proguard-rules.txt'
} }
......
...@@ -19,6 +19,7 @@ import android.content.Context; ...@@ -19,6 +19,7 @@ import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.os.Looper; import android.os.Looper;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import android.util.Log;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.ExoPlayerFactory;
...@@ -38,8 +39,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase { ...@@ -38,8 +39,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
private static final String BEAR_URI = "asset:///bear-vp9.webm"; private static final String BEAR_URI = "asset:///bear-vp9.webm";
private static final String BEAR_ODD_DIMENSIONS_URI = "asset:///bear-vp9-odd-dimensions.webm"; private static final String BEAR_ODD_DIMENSIONS_URI = "asset:///bear-vp9-odd-dimensions.webm";
private static final String ROADTRIP_10BIT_URI = "asset:///roadtrip-vp92-10bit.webm";
private static final String INVALID_BITSTREAM_URI = "asset:///invalid-bitstream.webm"; private static final String INVALID_BITSTREAM_URI = "asset:///invalid-bitstream.webm";
private static final String TAG = "VpxPlaybackTest";
public void testBasicPlayback() throws ExoPlaybackException { public void testBasicPlayback() throws ExoPlaybackException {
playUri(BEAR_URI); playUri(BEAR_URI);
} }
...@@ -48,6 +52,15 @@ public class VpxPlaybackTest extends InstrumentationTestCase { ...@@ -48,6 +52,15 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
playUri(BEAR_ODD_DIMENSIONS_URI); playUri(BEAR_ODD_DIMENSIONS_URI);
} }
public void test10BitProfile2Playback() throws ExoPlaybackException {
if (VpxLibrary.isHighBitDepthSupported()) {
Log.d(TAG, "High Bit Depth supported.");
playUri(ROADTRIP_10BIT_URI);
return;
}
Log.d(TAG, "High Bit Depth not supported.");
}
public void testInvalidBitstream() { public void testInvalidBitstream() {
try { try {
playUri(INVALID_BITSTREAM_URI); playUri(INVALID_BITSTREAM_URI);
......
...@@ -65,6 +65,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { ...@@ -65,6 +65,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
private final boolean playClearSamplesWithoutKeys; private final boolean playClearSamplesWithoutKeys;
private final EventDispatcher eventDispatcher; private final EventDispatcher eventDispatcher;
private final FormatHolder formatHolder; private final FormatHolder formatHolder;
private final DecoderInputBuffer flagsOnlyBuffer;
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager; private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
private DecoderCounters decoderCounters; private DecoderCounters decoderCounters;
...@@ -149,6 +150,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { ...@@ -149,6 +150,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
joiningDeadlineMs = -1; joiningDeadlineMs = -1;
clearLastReportedVideoSize(); clearLastReportedVideoSize();
formatHolder = new FormatHolder(); formatHolder = new FormatHolder();
flagsOnlyBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
eventDispatcher = new EventDispatcher(eventHandler, eventListener); eventDispatcher = new EventDispatcher(eventHandler, eventListener);
outputMode = VpxDecoder.OUTPUT_MODE_NONE; outputMode = VpxDecoder.OUTPUT_MODE_NONE;
} }
...@@ -165,10 +167,22 @@ public final class LibvpxVideoRenderer extends BaseRenderer { ...@@ -165,10 +167,22 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
return; return;
} }
// Try and read a format if we don't have one already. if (format == null) {
if (format == null && !readFormat()) { // We don't have a format yet, so try and read one.
// We can't make progress without one. flagsOnlyBuffer.clear();
return; int result = readSource(formatHolder, flagsOnlyBuffer, true);
if (result == C.RESULT_FORMAT_READ) {
onInputFormatChanged(formatHolder.format);
} else if (result == C.RESULT_BUFFER_READ) {
// End of stream read having not read a format.
Assertions.checkState(flagsOnlyBuffer.isEndOfStream());
inputStreamEnded = true;
outputStreamEnded = true;
return;
} else {
// We still don't have a format and can't make progress without one.
return;
}
} }
if (isRendererAvailable()) { if (isRendererAvailable()) {
...@@ -327,7 +341,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer { ...@@ -327,7 +341,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
// We've already read an encrypted sample into buffer, and are waiting for keys. // We've already read an encrypted sample into buffer, and are waiting for keys.
result = C.RESULT_BUFFER_READ; result = C.RESULT_BUFFER_READ;
} else { } else {
result = readSource(formatHolder, inputBuffer); result = readSource(formatHolder, inputBuffer, false);
} }
if (result == C.RESULT_NOTHING_READ) { if (result == C.RESULT_NOTHING_READ) {
...@@ -485,15 +499,6 @@ public final class LibvpxVideoRenderer extends BaseRenderer { ...@@ -485,15 +499,6 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
} }
} }
private boolean readFormat() throws ExoPlaybackException {
int result = readSource(formatHolder, null);
if (result == C.RESULT_FORMAT_READ) {
onInputFormatChanged(formatHolder.format);
return true;
}
return false;
}
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
Format oldFormat = format; Format oldFormat = format;
format = newFormat; format = newFormat;
......
...@@ -141,7 +141,7 @@ import java.nio.ByteBuffer; ...@@ -141,7 +141,7 @@ import java.nio.ByteBuffer;
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, private native long vpxSecureDecode(long context, ByteBuffer encoded, int length,
ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv, ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv,
int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData); 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 int vpxGetErrorCode(long context);
......
...@@ -57,6 +57,16 @@ public final class VpxLibrary { ...@@ -57,6 +57,16 @@ public final class VpxLibrary {
return isAvailable() ? vpxGetBuildConfig() : null; return isAvailable() ? vpxGetBuildConfig() : null;
} }
/**
* Returns true if the underlying libvpx library supports high bit depth.
*/
public static boolean isHighBitDepthSupported() {
String config = getBuildConfig();
int indexHbd = config != null
? config.indexOf("--enable-vp9-highbitdepth") : -1;
return indexHbd >= 0;
}
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(); public static native boolean vpxIsSecureDecodeSupported();
......
...@@ -26,6 +26,7 @@ import java.nio.ByteBuffer; ...@@ -26,6 +26,7 @@ import java.nio.ByteBuffer;
public static final int COLORSPACE_UNKNOWN = 0; public static final int COLORSPACE_UNKNOWN = 0;
public static final int COLORSPACE_BT601 = 1; public static final int COLORSPACE_BT601 = 1;
public static final int COLORSPACE_BT709 = 2; public static final int COLORSPACE_BT709 = 2;
public static final int COLORSPACE_BT2020 = 3;
private final VpxDecoder owner; private final VpxDecoder owner;
......
...@@ -42,6 +42,12 @@ import javax.microedition.khronos.opengles.GL10; ...@@ -42,6 +42,12 @@ import javax.microedition.khronos.opengles.GL10;
1.793f, -0.533f, 0.0f, 1.793f, -0.533f, 0.0f,
}; };
private static final float[] kColorConversion2020 = {
1.168f, 1.168f, 1.168f,
0.0f, -0.188f, 2.148f,
1.683f, -0.652f, 0.0f,
};
private static final String VERTEX_SHADER = private static final String VERTEX_SHADER =
"varying vec2 interp_tc;\n" "varying vec2 interp_tc;\n"
+ "attribute vec4 in_pos;\n" + "attribute vec4 in_pos;\n"
...@@ -59,12 +65,13 @@ import javax.microedition.khronos.opengles.GL10; ...@@ -59,12 +65,13 @@ import javax.microedition.khronos.opengles.GL10;
+ "uniform sampler2D v_tex;\n" + "uniform sampler2D v_tex;\n"
+ "uniform mat3 mColorConversion;\n" + "uniform mat3 mColorConversion;\n"
+ "void main() {\n" + "void main() {\n"
+ " vec3 yuv;" + " vec3 yuv;\n"
+ " yuv.x = texture2D(y_tex, interp_tc).r - 0.0625;\n" + " yuv.x = texture2D(y_tex, interp_tc).r - 0.0625;\n"
+ " yuv.y = texture2D(u_tex, interp_tc).r - 0.5;\n" + " yuv.y = texture2D(u_tex, interp_tc).r - 0.5;\n"
+ " yuv.z = texture2D(v_tex, interp_tc).r - 0.5;\n" + " yuv.z = texture2D(v_tex, interp_tc).r - 0.5;\n"
+ " gl_FragColor = vec4(mColorConversion * yuv, 1.0);" + " gl_FragColor = vec4(mColorConversion * yuv, 1.0);\n"
+ "}\n"; + "}\n";
private static final FloatBuffer TEXTURE_VERTICES = nativeFloatBuffer( private static final FloatBuffer TEXTURE_VERTICES = nativeFloatBuffer(
-1.0f, 1.0f, -1.0f, 1.0f,
-1.0f, -1.0f, -1.0f, -1.0f,
...@@ -156,8 +163,18 @@ import javax.microedition.khronos.opengles.GL10; ...@@ -156,8 +163,18 @@ import javax.microedition.khronos.opengles.GL10;
} }
VpxOutputBuffer outputBuffer = renderedOutputBuffer; VpxOutputBuffer outputBuffer = renderedOutputBuffer;
// Set color matrix. Assume BT709 if the color space is unknown. // Set color matrix. Assume BT709 if the color space is unknown.
float[] colorConversion = outputBuffer.colorspace == VpxOutputBuffer.COLORSPACE_BT601 float[] colorConversion = kColorConversion709;
? kColorConversion601 : kColorConversion709; switch (outputBuffer.colorspace) {
case VpxOutputBuffer.COLORSPACE_BT601:
colorConversion = kColorConversion601;
break;
case VpxOutputBuffer.COLORSPACE_BT2020:
colorConversion = kColorConversion2020;
break;
case VpxOutputBuffer.COLORSPACE_BT709:
default:
break; // Do nothing
}
GLES20.glUniformMatrix3fv(colorMatrixLocation, 1, false, colorConversion, 0); GLES20.glUniformMatrix3fv(colorMatrixLocation, 1, false, colorConversion, 0);
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
......
...@@ -40,7 +40,7 @@ config[0]+=" --enable-neon-asm" ...@@ -40,7 +40,7 @@ config[0]+=" --enable-neon-asm"
arch[1]="armeabi" arch[1]="armeabi"
config[1]="--target=armv7-android-gcc --sdk-path=$ndk --disable-neon" config[1]="--target=armv7-android-gcc --sdk-path=$ndk --disable-neon"
config[1]+=" --disable-neon-asm --disable-media" config[1]+=" --disable-neon-asm"
arch[2]="mips" arch[2]="mips"
config[2]="--force-target=mips32-android-gcc --sdk-path=$ndk" config[2]="--force-target=mips32-android-gcc --sdk-path=$ndk"
...@@ -78,12 +78,12 @@ convert_asm() { ...@@ -78,12 +78,12 @@ convert_asm() {
for i in $(seq 0 ${limit}); do for i in $(seq 0 ${limit}); do
while read file; do while read file; do
case "${file}" in case "${file}" in
*.asm.s) *.asm.[sS])
# Some files may already have been processed (there are duplicated # Some files may already have been processed (there are duplicated
# .asm.s files for vp8 in the armeabi/armeabi-v7a configurations). # .asm.s files for vp8 in the armeabi/armeabi-v7a configurations).
file="libvpx/${file}" file="libvpx/${file}"
if [[ ! -e "${file}" ]]; then if [[ ! -e "${file}" ]]; then
asm_file="${file%.s}" asm_file="${file%.[sS]}"
cat "${asm_file}" | libvpx/build/make/ads2gas.pl > "${file}" cat "${asm_file}" | libvpx/build/make/ads2gas.pl > "${file}"
remove_trailing_whitespace "${file}" remove_trailing_whitespace "${file}"
rm "${asm_file}" rm "${asm_file}"
...@@ -105,7 +105,11 @@ for i in $(seq 0 ${limit}); do ...@@ -105,7 +105,11 @@ for i in $(seq 0 ${limit}); do
echo "configure ${config[${i}]} ${common_params}" echo "configure ${config[${i}]} ${common_params}"
../../libvpx/configure ${config[${i}]} ${common_params} ../../libvpx/configure ${config[${i}]} ${common_params}
rm -f libvpx_srcs.txt rm -f libvpx_srcs.txt
make libvpx_srcs.txt for f in ${allowed_files}; do
# the build system supports multiple different configurations. avoid
# failing out when, for example, vp8_rtcd.h is not part of a configuration
make "${f}" || true
done
# remove files that aren't needed # remove files that aren't needed
rm -rf !(${allowed_files// /|}) rm -rf !(${allowed_files// /|})
......
...@@ -35,16 +35,22 @@ LOCAL_SRC_FILES += $(addprefix libvpx/, $(filter-out vpx_config.c, \ ...@@ -35,16 +35,22 @@ LOCAL_SRC_FILES += $(addprefix libvpx/, $(filter-out vpx_config.c, \
$(filter %.c, $(libvpx_codec_srcs)))) $(filter %.c, $(libvpx_codec_srcs))))
# include assembly files if they exist # include assembly files if they exist
# "%.asm.s" covers neon assembly and "%.asm" covers x86 assembly # "%.asm.[sS]" covers neon assembly and "%.asm" covers x86 assembly
LOCAL_SRC_FILES += $(addprefix libvpx/, \ LOCAL_SRC_FILES += $(addprefix libvpx/, \
$(filter %.asm.s %.asm, $(libvpx_codec_srcs))) $(filter %.asm.s %.asm, $(libvpx_codec_srcs)))
LOCAL_SRC_FILES += $(addprefix libvpx/, \
$(filter %.asm.S %.asm, $(libvpx_codec_srcs)))
ifneq ($(findstring armeabi-v7a, $(TARGET_ARCH_ABI)),) ifneq ($(findstring armeabi-v7a, $(TARGET_ARCH_ABI)),)
# append .neon to *_neon.c and *.s # append .neon to *_neon.c and *.[sS]
LOCAL_SRC_FILES := $(subst _neon.c,_neon.c.neon,$(LOCAL_SRC_FILES)) LOCAL_SRC_FILES := $(subst _neon.c,_neon.c.neon,$(LOCAL_SRC_FILES))
LOCAL_SRC_FILES := $(subst .s,.s.neon,$(LOCAL_SRC_FILES)) LOCAL_SRC_FILES := $(subst .s,.s.neon,$(LOCAL_SRC_FILES))
LOCAL_SRC_FILES := $(subst .S,.S.neon,$(LOCAL_SRC_FILES))
endif endif
# remove duplicates
LOCAL_SRC_FILES := $(sort $(LOCAL_SRC_FILES))
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/libvpx \ LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/libvpx \
$(LOCAL_PATH)/libvpx/vpx $(LOCAL_PATH)/libvpx/vpx
......
...@@ -74,8 +74,11 @@ DECODER_FUNC(jlong, vpxInit) { ...@@ -74,8 +74,11 @@ DECODER_FUNC(jlong, vpxInit) {
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; errorCode = 0;
if (vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo, &cfg, 0)) { vpx_codec_err_t err = vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo,
LOGE("ERROR: Fail to initialize libvpx decoder."); &cfg, 0);
if (err) {
LOGE("ERROR: Failed to initialize libvpx decoder, error = %d.", err);
errorCode = err;
return 0; return 0;
} }
...@@ -160,6 +163,7 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { ...@@ -160,6 +163,7 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
const int kColorspaceUnknown = 0; const int kColorspaceUnknown = 0;
const int kColorspaceBT601 = 1; const int kColorspaceBT601 = 1;
const int kColorspaceBT709 = 2; const int kColorspaceBT709 = 2;
const int kColorspaceBT2020 = 3;
int colorspace = kColorspaceUnknown; int colorspace = kColorspaceUnknown;
switch (img->cs) { switch (img->cs) {
...@@ -169,6 +173,9 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { ...@@ -169,6 +173,9 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
case VPX_CS_BT_709: case VPX_CS_BT_709:
colorspace = kColorspaceBT709; colorspace = kColorspaceBT709;
break; break;
case VPX_CS_BT_2020:
colorspace = kColorspaceBT2020;
break;
default: default:
break; break;
} }
...@@ -186,14 +193,55 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { ...@@ -186,14 +193,55 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
jbyte* const data = jbyte* const data =
reinterpret_cast<jbyte*>(env->GetDirectBufferAddress(dataObject)); reinterpret_cast<jbyte*>(env->GetDirectBufferAddress(dataObject));
// TODO: This copy can be eliminated by using external frame buffers. NOLINT const int32_t uvHeight = (img->d_h + 1) / 2;
// This is insignificant for smaller videos but takes ~1.5ms for 1080p const uint64_t yLength = img->stride[VPX_PLANE_Y] * img->d_h;
// clips. So this should eventually be gotten rid of. const uint64_t uvLength = img->stride[VPX_PLANE_U] * uvHeight;
const uint64_t y_length = img->stride[VPX_PLANE_Y] * img->d_h; int sample = 0;
const uint64_t uv_length = img->stride[VPX_PLANE_U] * ((img->d_h + 1) / 2); if (img->fmt == VPX_IMG_FMT_I42016) { // HBD planar 420.
memcpy(data, img->planes[VPX_PLANE_Y], y_length); // Note: The stride for BT2020 is twice of what we use so this is wasting
memcpy(data + y_length, img->planes[VPX_PLANE_U], uv_length); // memory. The long term goal however is to upload half-float/short so
memcpy(data + y_length + uv_length, img->planes[VPX_PLANE_V], uv_length); // it's not important to optimize the stride at this time.
// Y
for (int y = 0; y < img->d_h; y++) {
const uint16_t* srcBase = reinterpret_cast<uint16_t*>(
img->planes[VPX_PLANE_Y] + img->stride[VPX_PLANE_Y] * y);
int8_t* destBase = data + img->stride[VPX_PLANE_Y] * y;
for (int x = 0; x < img->d_w; x++) {
// Lightweight dither. Carryover the remainder of each 10->8 bit
// conversion to the next pixel.
sample += *srcBase++;
*destBase++ = sample >> 2;
sample = sample & 3; // Remainder.
}
}
// UV
const int32_t uvWidth = (img->d_w + 1) / 2;
for (int y = 0; y < uvHeight; y++) {
const uint16_t* srcUBase = reinterpret_cast<uint16_t*>(
img->planes[VPX_PLANE_U] + img->stride[VPX_PLANE_U] * y);
const uint16_t* srcVBase = reinterpret_cast<uint16_t*>(
img->planes[VPX_PLANE_V] + img->stride[VPX_PLANE_V] * y);
int8_t* destUBase = data + yLength + img->stride[VPX_PLANE_U] * y;
int8_t* destVBase = data + yLength + uvLength
+ img->stride[VPX_PLANE_V] * y;
for (int x = 0; x < uvWidth; x++) {
// Lightweight dither. Carryover the remainder of each 10->8 bit
// conversion to the next pixel.
sample += *srcUBase++;
*destUBase++ = sample >> 2;
sample = (*srcVBase++) + (sample & 3); // srcV + previousRemainder.
*destVBase++ = sample >> 2;
sample = sample & 3; // Remainder.
}
}
} else {
// TODO: This copy can be eliminated by using external frame buffers. This
// is insignificant for smaller videos but takes ~1.5ms for 1080p clips.
// So this should eventually be gotten rid of.
memcpy(data, img->planes[VPX_PLANE_Y], yLength);
memcpy(data + yLength, img->planes[VPX_PLANE_U], uvLength);
memcpy(data + yLength + uvLength, img->planes[VPX_PLANE_V], uvLength);
}
} }
return 0; return 0;
} }
......
#Mon Oct 24 14:40:37 BST 2016 #Mon Mar 13 11:17:14 GMT 2017
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.14.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
...@@ -14,19 +14,13 @@ ...@@ -14,19 +14,13 @@
import com.android.builder.core.BuilderConstants import com.android.builder.core.BuilderConstants
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply plugin: 'bintray-release'
android { android {
compileSdkVersion project.ext.compileSdkVersion compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion buildToolsVersion project.ext.buildToolsVersion
defaultConfig { defaultConfig {
// Important: ExoPlayerLib specifies a minSdkVersion of 9 because minSdkVersion project.ext.minSdkVersion
// various components provided by the library may be of use on older
// devices. However, please note that the core video playback
// functionality provided by the library requires API level 16 or
// greater.
minSdkVersion 9
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt' consumerProguardFiles 'proguard-rules.txt'
} }
...@@ -47,10 +41,10 @@ android { ...@@ -47,10 +41,10 @@ android {
} }
dependencies { dependencies {
compile 'com.android.support:support-annotations:25.2.0'
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:25.0.1'
} }
android.libraryVariants.all { variant -> android.libraryVariants.all { variant ->
...@@ -86,12 +80,8 @@ android.libraryVariants.all { variant -> ...@@ -86,12 +80,8 @@ android.libraryVariants.all { variant ->
} }
} }
publish { ext {
artifactId = 'exoplayer' releaseArtifact = 'exoplayer'
description = 'The ExoPlayer library.' releaseDescription = 'The ExoPlayer library.'
repoName = releaseRepoName
userOrg = releaseUserOrg
groupId = releaseGroupId
version = releaseVersion
website = releaseWebsite
} }
apply from: '../publish.gradle'
seekMap:
isSeekable = false
duration = UNSET TIME
getPosition(0) = 0
numberOfTracks = 3
track 0:
format:
bitrate = -1
id = 1
containerMimeType = null
sampleMimeType = video/avc
maxInputSize = -1
width = 1080
height = 720
frameRate = -1.0
rotationDegrees = 0
pixelWidthHeightRatio = 1.0
channelCount = -1
sampleRate = -1
pcmEncoding = -1
encoderDelay = -1
encoderPadding = -1
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
data = length 29, hash 4746B5D9
data = length 10, hash 7A0D0F2B
sample count = 30
sample 0:
time = 66000
flags = 1
data = length 38070, hash B58E1AEE
sample 1:
time = 199000
flags = 0
data = length 8340, hash 8AC449FF
sample 2:
time = 132000
flags = 0
data = length 1295, hash C0DA5090
sample 3:
time = 100000
flags = 0
data = length 469, hash D6E0A200
sample 4:
time = 166000
flags = 0
data = length 564, hash E5F56C5B
sample 5:
time = 332000
flags = 0
data = length 6075, hash 8756E49E
sample 6:
time = 266000
flags = 0
data = length 847, hash DCC2B618
sample 7:
time = 233000
flags = 0
data = length 455, hash B9CCE047
sample 8:
time = 299000
flags = 0
data = length 467, hash 69806D94
sample 9:
time = 466000
flags = 0
data = length 4549, hash 3944F501
sample 10:
time = 399000
flags = 0
data = length 1087, hash 491BF106
sample 11:
time = 367000
flags = 0
data = length 380, hash 5FED016A
sample 12:
time = 433000
flags = 0
data = length 455, hash 8A0610
sample 13:
time = 599000
flags = 0
data = length 5190, hash B9031D8
sample 14:
time = 533000
flags = 0
data = length 1071, hash 684E7DC8
sample 15:
time = 500000
flags = 0
data = length 653, hash 8494F326
sample 16:
time = 566000
flags = 0
data = length 485, hash 2CCC85F4
sample 17:
time = 733000
flags = 0
data = length 4884, hash D16B6A96
sample 18:
time = 666000
flags = 0
data = length 997, hash 164FF210
sample 19:
time = 633000
flags = 0
data = length 640, hash F664125B
sample 20:
time = 700000
flags = 0
data = length 491, hash B5930C7C
sample 21:
time = 866000
flags = 0
data = length 2989, hash 92CF4FCF
sample 22:
time = 800000
flags = 0
data = length 838, hash 294A3451
sample 23:
time = 767000
flags = 0
data = length 544, hash FCCE2DE6
sample 24:
time = 833000
flags = 0
data = length 329, hash A654FFA1
sample 25:
time = 1000000
flags = 0
data = length 1517, hash 5F7EBF8B
sample 26:
time = 933000
flags = 0
data = length 803, hash 7A5C4C1D
sample 27:
time = 900000
flags = 0
data = length 415, hash B31BBC3B
sample 28:
time = 967000
flags = 0
data = length 415, hash 850DFEA3
sample 29:
time = 1033000
flags = 0
data = length 619, hash AB5E56CA
track 1:
format:
bitrate = -1
id = 2
containerMimeType = null
sampleMimeType = audio/mp4a-latm
maxInputSize = -1
width = -1
height = -1
frameRate = -1.0
rotationDegrees = -1
pixelWidthHeightRatio = -1.0
channelCount = 1
sampleRate = 44100
pcmEncoding = -1
encoderDelay = -1
encoderPadding = -1
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = und
drmInitData = -
initializationData:
data = length 5, hash 2B7623A
sample count = 46
sample 0:
time = 0
flags = 1
data = length 18, hash 96519432
sample 1:
time = 23000
flags = 1
data = length 4, hash EE9DF
sample 2:
time = 46000
flags = 1
data = length 4, hash EEDBF
sample 3:
time = 69000
flags = 1
data = length 157, hash E2F078F4
sample 4:
time = 92000
flags = 1
data = length 371, hash B9471F94
sample 5:
time = 116000
flags = 1
data = length 373, hash 2AB265CB
sample 6:
time = 139000
flags = 1
data = length 402, hash 1295477C
sample 7:
time = 162000
flags = 1
data = length 455, hash 2D8146C8
sample 8:
time = 185000
flags = 1
data = length 434, hash F2C5D287
sample 9:
time = 208000
flags = 1
data = length 450, hash 84143FCD
sample 10:
time = 232000
flags = 1
data = length 429, hash EF769D50
sample 11:
time = 255000
flags = 1
data = length 450, hash EC3DE692
sample 12:
time = 278000
flags = 1
data = length 447, hash 3E519E13
sample 13:
time = 301000
flags = 1
data = length 457, hash 1E4F23A0
sample 14:
time = 325000
flags = 1
data = length 447, hash A439EA97
sample 15:
time = 348000
flags = 1
data = length 456, hash 1E9034C6
sample 16:
time = 371000
flags = 1
data = length 398, hash 99DB7345
sample 17:
time = 394000
flags = 1
data = length 474, hash 3F05F10A
sample 18:
time = 417000
flags = 1
data = length 416, hash C105EE09
sample 19:
time = 441000
flags = 1
data = length 454, hash 5FDBE458
sample 20:
time = 464000
flags = 1
data = length 438, hash 41A93AC3
sample 21:
time = 487000
flags = 1
data = length 443, hash 10FDA652
sample 22:
time = 510000
flags = 1
data = length 412, hash 1F791E25
sample 23:
time = 534000
flags = 1
data = length 482, hash A6D983D
sample 24:
time = 557000
flags = 1
data = length 386, hash BED7392F
sample 25:
time = 580000
flags = 1
data = length 463, hash 5309F8C9
sample 26:
time = 603000
flags = 1
data = length 394, hash 21C7321F
sample 27:
time = 626000
flags = 1
data = length 489, hash 71B4730D
sample 28:
time = 650000
flags = 1
data = length 403, hash D9C6DE89
sample 29:
time = 673000
flags = 1
data = length 447, hash 9B14B73B
sample 30:
time = 696000
flags = 1
data = length 439, hash 4760D35B
sample 31:
time = 719000
flags = 1
data = length 463, hash 1601F88D
sample 32:
time = 743000
flags = 1
data = length 423, hash D4AE6773
sample 33:
time = 766000
flags = 1
data = length 497, hash A3C674D3
sample 34:
time = 789000
flags = 1
data = length 419, hash D3734A1F
sample 35:
time = 812000
flags = 1
data = length 474, hash DFB41F9
sample 36:
time = 835000
flags = 1
data = length 413, hash 53E7CB9F
sample 37:
time = 859000
flags = 1
data = length 445, hash D15B0E39
sample 38:
time = 882000
flags = 1
data = length 453, hash 77ED81E4
sample 39:
time = 905000
flags = 1
data = length 545, hash 3321AEB9
sample 40:
time = 928000
flags = 1
data = length 317, hash F557D0E
sample 41:
time = 952000
flags = 1
data = length 537, hash ED58CF7B
sample 42:
time = 975000
flags = 1
data = length 458, hash 51CDAA10
sample 43:
time = 998000
flags = 1
data = length 465, hash CBA1EFD7
sample 44:
time = 1021000
flags = 1
data = length 446, hash D6735B8A
sample 45:
time = 1044000
flags = 1
data = length 10, hash A453EEBE
track 3:
format:
bitrate = -1
id = null
containerMimeType = null
sampleMimeType = application/cea-608
maxInputSize = -1
width = -1
height = -1
frameRate = -1.0
rotationDegrees = -1
pixelWidthHeightRatio = -1.0
channelCount = -1
sampleRate = -1
pcmEncoding = -1
encoderDelay = -1
encoderPadding = -1
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = null
drmInitData = -
initializationData:
sample count = 0
tracksEnded = true
...@@ -6,7 +6,7 @@ numberOfTracks = 1 ...@@ -6,7 +6,7 @@ numberOfTracks = 1
track 0: track 0:
format: format:
bitrate = -1 bitrate = -1
id = null id = 0
containerMimeType = null containerMimeType = null
sampleMimeType = audio/ac3 sampleMimeType = audio/ac3
maxInputSize = -1 maxInputSize = -1
......
...@@ -6,7 +6,7 @@ numberOfTracks = 2 ...@@ -6,7 +6,7 @@ numberOfTracks = 2
track 0: track 0:
format: format:
bitrate = -1 bitrate = -1
id = null id = 0
containerMimeType = null containerMimeType = null
sampleMimeType = audio/mp4a-latm sampleMimeType = audio/mp4a-latm
maxInputSize = -1 maxInputSize = -1
...@@ -606,7 +606,7 @@ track 0: ...@@ -606,7 +606,7 @@ track 0:
track 1: track 1:
format: format:
bitrate = -1 bitrate = -1
id = null id = 1
containerMimeType = null containerMimeType = null
sampleMimeType = application/id3 sampleMimeType = application/id3
maxInputSize = -1 maxInputSize = -1
......
...@@ -6,7 +6,7 @@ numberOfTracks = 2 ...@@ -6,7 +6,7 @@ numberOfTracks = 2
track 192: track 192:
format: format:
bitrate = -1 bitrate = -1
id = null id = 192
containerMimeType = null containerMimeType = null
sampleMimeType = audio/mpeg-L2 sampleMimeType = audio/mpeg-L2
maxInputSize = 4096 maxInputSize = 4096
...@@ -45,7 +45,7 @@ track 192: ...@@ -45,7 +45,7 @@ track 192:
track 224: track 224:
format: format:
bitrate = -1 bitrate = -1
id = null id = 224
containerMimeType = null containerMimeType = null
sampleMimeType = video/mpeg2 sampleMimeType = video/mpeg2
maxInputSize = -1 maxInputSize = -1
......
...@@ -6,7 +6,7 @@ numberOfTracks = 2 ...@@ -6,7 +6,7 @@ numberOfTracks = 2
track 256: track 256:
format: format:
bitrate = -1 bitrate = -1
id = null id = 1/256
containerMimeType = null containerMimeType = null
sampleMimeType = video/mpeg2 sampleMimeType = video/mpeg2
maxInputSize = -1 maxInputSize = -1
...@@ -38,7 +38,7 @@ track 256: ...@@ -38,7 +38,7 @@ track 256:
track 257: track 257:
format: format:
bitrate = -1 bitrate = -1
id = null id = 1/257
containerMimeType = null containerMimeType = null
sampleMimeType = audio/mpeg-L2 sampleMimeType = audio/mpeg-L2
maxInputSize = 4096 maxInputSize = 4096
......
...@@ -461,6 +461,11 @@ public final class ExoPlayerTest extends TestCase { ...@@ -461,6 +461,11 @@ public final class ExoPlayerTest extends TestCase {
} }
@Override @Override
public void discardBuffer(long positionUs) {
// Do nothing.
}
@Override
public long readDiscontinuity() { public long readDiscontinuity() {
assertTrue(preparedPeriod); assertTrue(preparedPeriod);
return C.TIME_UNSET; return C.TIME_UNSET;
...@@ -513,8 +518,9 @@ public final class ExoPlayerTest extends TestCase { ...@@ -513,8 +518,9 @@ public final class ExoPlayerTest extends TestCase {
} }
@Override @Override
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
if (buffer == null || !readFormat) { boolean formatRequired) {
if (formatRequired || !readFormat) {
formatHolder.format = format; formatHolder.format = format;
readFormat = true; readFormat = true;
return C.RESULT_FORMAT_READ; return C.RESULT_FORMAT_READ;
...@@ -571,7 +577,7 @@ public final class ExoPlayerTest extends TestCase { ...@@ -571,7 +577,7 @@ public final class ExoPlayerTest extends TestCase {
FormatHolder formatHolder = new FormatHolder(); FormatHolder formatHolder = new FormatHolder();
DecoderInputBuffer buffer = DecoderInputBuffer buffer =
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
int result = readSource(formatHolder, buffer); int result = readSource(formatHolder, buffer, false);
if (result == C.RESULT_FORMAT_READ) { if (result == C.RESULT_FORMAT_READ) {
formatReadCount++; formatReadCount++;
assertEquals(expectedFormat, formatHolder.format); assertEquals(expectedFormat, formatHolder.format);
......
...@@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.dash.manifest.Representation; ...@@ -32,6 +32,7 @@ import com.google.android.exoplayer2.source.dash.manifest.Representation;
import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase; import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.MimeTypes;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import org.mockito.Mock; import org.mockito.Mock;
...@@ -217,7 +218,11 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase { ...@@ -217,7 +218,11 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase {
} }
private static Representation newRepresentations(DrmInitData drmInitData) { private static Representation newRepresentations(DrmInitData drmInitData) {
Format format = Format.createVideoSampleFormat("", "", "", 0, 0, 0, 0, 0, null, drmInitData); Format format = Format.createVideoContainerFormat("id", MimeTypes.VIDEO_MP4,
MimeTypes.VIDEO_H264, "", Format.NO_VALUE, 1024, 768, Format.NO_VALUE, null, 0);
if (drmInitData != null) {
format = format.copyWithDrmInitData(drmInitData);
}
return Representation.newInstance("", 0, format, "", new SingleSegmentBase()); return Representation.newInstance("", 0, format, "", new SingleSegmentBase());
} }
......
...@@ -25,21 +25,32 @@ import com.google.android.exoplayer2.testutil.TestUtil; ...@@ -25,21 +25,32 @@ import com.google.android.exoplayer2.testutil.TestUtil;
*/ */
public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase { public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase {
private static final TestUtil.ExtractorFactory EXTRACTOR_FACTORY =
new TestUtil.ExtractorFactory() {
@Override
public Extractor create() {
return new FragmentedMp4Extractor();
}
};
public void testSample() throws Exception { public void testSample() throws Exception {
TestUtil.assertOutput(EXTRACTOR_FACTORY, "mp4/sample_fragmented.mp4", getInstrumentation()); TestUtil.assertOutput(getExtractorFactory(), "mp4/sample_fragmented.mp4", getInstrumentation());
}
public void testSampleWithSeiPayloadParsing() throws Exception {
// Enabling the CEA-608 track enables SEI payload parsing.
TestUtil.assertOutput(getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK),
"mp4/sample_fragmented_sei.mp4", getInstrumentation());
} }
public void testAtomWithZeroSize() throws Exception { public void testAtomWithZeroSize() throws Exception {
TestUtil.assertThrows(EXTRACTOR_FACTORY, "mp4/sample_fragmented_zero_size_atom.mp4", TestUtil.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4",
getInstrumentation(), ParserException.class); getInstrumentation(), ParserException.class);
} }
private static TestUtil.ExtractorFactory getExtractorFactory() {
return getExtractorFactory(0);
}
private static TestUtil.ExtractorFactory getExtractorFactory(final int flags) {
return new TestUtil.ExtractorFactory() {
@Override
public Extractor create() {
return new FragmentedMp4Extractor(flags, null);
}
};
}
} }
...@@ -69,8 +69,8 @@ public class AdtsReaderTest extends TestCase { ...@@ -69,8 +69,8 @@ public class AdtsReaderTest extends TestCase {
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput(); FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput();
adtsOutput = fakeExtractorOutput.track(0); adtsOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_AUDIO);
id3Output = fakeExtractorOutput.track(1); id3Output = fakeExtractorOutput.track(1, C.TRACK_TYPE_METADATA);
adtsReader = new AdtsReader(true); adtsReader = new AdtsReader(true);
TrackIdGenerator idGenerator = new TrackIdGenerator(0, 1); TrackIdGenerator idGenerator = new TrackIdGenerator(0, 1);
adtsReader.createTracks(fakeExtractorOutput, idGenerator); adtsReader.createTracks(fakeExtractorOutput, idGenerator);
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import android.util.SparseArray; import android.util.SparseArray;
import com.google.android.exoplayer2.C;
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;
...@@ -74,7 +75,8 @@ public final class TsExtractorTest extends InstrumentationTestCase { ...@@ -74,7 +75,8 @@ public final class TsExtractorTest extends InstrumentationTestCase {
public void testCustomPesReader() throws Exception { public void testCustomPesReader() throws Exception {
CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(true, false); CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(true, false);
TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory, false); TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_NORMAL, new TimestampAdjuster(0),
factory);
FakeExtractorInput input = new FakeExtractorInput.Builder() FakeExtractorInput input = new FakeExtractorInput.Builder()
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts")) .setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts"))
.setSimulateIOErrors(false) .setSimulateIOErrors(false)
...@@ -92,13 +94,14 @@ public final class TsExtractorTest extends InstrumentationTestCase { ...@@ -92,13 +94,14 @@ public final class TsExtractorTest extends InstrumentationTestCase {
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. */));
assertEquals( assertEquals(
Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0, "und", null, 0), Format.createTextSampleFormat("1/257", "mime", null, 0, 0, "und", null, 0),
((FakeTrackOutput) trackOutput).format); ((FakeTrackOutput) trackOutput).format);
} }
public void testCustomInitialSectionReader() throws Exception { public void testCustomInitialSectionReader() throws Exception {
CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(false, true); CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(false, true);
TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory, false); TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_NORMAL, new TimestampAdjuster(0),
factory);
FakeExtractorInput input = new FakeExtractorInput.Builder() FakeExtractorInput input = new FakeExtractorInput.Builder()
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample_with_sdt.ts")) .setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample_with_sdt.ts"))
.setSimulateIOErrors(false) .setSimulateIOErrors(false)
...@@ -178,8 +181,9 @@ public final class TsExtractorTest extends InstrumentationTestCase { ...@@ -178,8 +181,9 @@ public final class TsExtractorTest extends InstrumentationTestCase {
@Override @Override
public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
output = extractorOutput.track(idGenerator.getNextId()); idGenerator.generateNewId();
output.format(Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0, output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_UNKNOWN);
output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), "mime", null, 0, 0,
language, null, 0)); language, null, 0));
} }
......
/*
* Copyright (C) 2017 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.dash.manifest;
import android.net.Uri;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import junit.framework.TestCase;
/**
* Unit tests for {@link DashManifest}.
*/
public class DashManifestTest extends TestCase {
private static final UtcTimingElement DUMMY_UTC_TIMING = new UtcTimingElement("", "");
private static final List<SchemeValuePair> DUMMY_ACCESSIBILITY_DESCRIPTORS =
Collections.emptyList();
private static final SingleSegmentBase DUMMY_SEGMENT_BASE = new SingleSegmentBase();
private static final Format DUMMY_FORMAT = Format.createSampleFormat("", "", 0);
public void testCopy() throws Exception {
Representation[][][] representations = newRepresentations(3, 2, 3);
DashManifest sourceManifest = newDashManifest(10,
newPeriod("1", 1,
newAdaptationSet(2, representations[0][0]),
newAdaptationSet(3, representations[0][1])),
newPeriod("4", 4,
newAdaptationSet(5, representations[1][0]),
newAdaptationSet(6, representations[1][1])),
newPeriod("7", 7,
newAdaptationSet(8, representations[2][0]),
newAdaptationSet(9, representations[2][1])));
List<RepresentationKey> keys = Arrays.asList(
new RepresentationKey(0, 0, 0),
new RepresentationKey(0, 0, 1),
new RepresentationKey(0, 1, 2),
new RepresentationKey(1, 0, 1),
new RepresentationKey(1, 1, 0),
new RepresentationKey(1, 1, 2),
new RepresentationKey(2, 0, 1),
new RepresentationKey(2, 0, 2),
new RepresentationKey(2, 1, 0));
// Keys don't need to be in any particular order
Collections.shuffle(keys, new Random(0));
DashManifest copyManifest = sourceManifest.copy(keys);
DashManifest expectedManifest = newDashManifest(10,
newPeriod("1", 1,
newAdaptationSet(2, representations[0][0][0], representations[0][0][1]),
newAdaptationSet(3, representations[0][1][2])),
newPeriod("4", 4,
newAdaptationSet(5, representations[1][0][1]),
newAdaptationSet(6, representations[1][1][0], representations[1][1][2])),
newPeriod("7", 7,
newAdaptationSet(8, representations[2][0][1], representations[2][0][2]),
newAdaptationSet(9, representations[2][1][0])));
assertManifestEquals(expectedManifest, copyManifest);
}
public void testCopySameAdaptationIndexButDifferentPeriod() throws Exception {
Representation[][][] representations = newRepresentations(2, 1, 1);
DashManifest sourceManifest = newDashManifest(10,
newPeriod("1", 1,
newAdaptationSet(2, representations[0][0])),
newPeriod("4", 4,
newAdaptationSet(5, representations[1][0])));
DashManifest copyManifest = sourceManifest.copy(Arrays.asList(
new RepresentationKey(0, 0, 0),
new RepresentationKey(1, 0, 0)));
DashManifest expectedManifest = newDashManifest(10,
newPeriod("1", 1,
newAdaptationSet(2, representations[0][0])),
newPeriod("4", 4,
newAdaptationSet(5, representations[1][0])));
assertManifestEquals(expectedManifest, copyManifest);
}
public void testCopySkipPeriod() throws Exception {
Representation[][][] representations = newRepresentations(3, 2, 3);
DashManifest sourceManifest = newDashManifest(10,
newPeriod("1", 1,
newAdaptationSet(2, representations[0][0]),
newAdaptationSet(3, representations[0][1])),
newPeriod("4", 4,
newAdaptationSet(5, representations[1][0]),
newAdaptationSet(6, representations[1][1])),
newPeriod("7", 7,
newAdaptationSet(8, representations[2][0]),
newAdaptationSet(9, representations[2][1])));
DashManifest copyManifest = sourceManifest.copy(Arrays.asList(
new RepresentationKey(0, 0, 0),
new RepresentationKey(0, 0, 1),
new RepresentationKey(0, 1, 2),
new RepresentationKey(2, 0, 1),
new RepresentationKey(2, 0, 2),
new RepresentationKey(2, 1, 0)));
DashManifest expectedManifest = newDashManifest(7,
newPeriod("1", 1,
newAdaptationSet(2, representations[0][0][0], representations[0][0][1]),
newAdaptationSet(3, representations[0][1][2])),
newPeriod("7", 4,
newAdaptationSet(8, representations[2][0][1], representations[2][0][2]),
newAdaptationSet(9, representations[2][1][0])));
assertManifestEquals(expectedManifest, copyManifest);
}
private static void assertManifestEquals(DashManifest expected, DashManifest actual) {
assertEquals(expected.availabilityStartTime, actual.availabilityStartTime);
assertEquals(expected.duration, actual.duration);
assertEquals(expected.minBufferTime, actual.minBufferTime);
assertEquals(expected.dynamic, actual.dynamic);
assertEquals(expected.minUpdatePeriod, actual.minUpdatePeriod);
assertEquals(expected.timeShiftBufferDepth, actual.timeShiftBufferDepth);
assertEquals(expected.suggestedPresentationDelay, actual.suggestedPresentationDelay);
assertEquals(expected.utcTiming, actual.utcTiming);
assertEquals(expected.location, actual.location);
assertEquals(expected.getPeriodCount(), actual.getPeriodCount());
for (int i = 0; i < expected.getPeriodCount(); i++) {
Period expectedPeriod = expected.getPeriod(i);
Period actualPeriod = actual.getPeriod(i);
assertEquals(expectedPeriod.id, actualPeriod.id);
assertEquals(expectedPeriod.startMs, actualPeriod.startMs);
List<AdaptationSet> expectedAdaptationSets = expectedPeriod.adaptationSets;
List<AdaptationSet> actualAdaptationSets = actualPeriod.adaptationSets;
assertEquals(expectedAdaptationSets.size(), actualAdaptationSets.size());
for (int j = 0; j < expectedAdaptationSets.size(); j++) {
AdaptationSet expectedAdaptationSet = expectedAdaptationSets.get(j);
AdaptationSet actualAdaptationSet = actualAdaptationSets.get(j);
assertEquals(expectedAdaptationSet.id, actualAdaptationSet.id);
assertEquals(expectedAdaptationSet.type, actualAdaptationSet.type);
assertEquals(expectedAdaptationSet.accessibilityDescriptors,
actualAdaptationSet.accessibilityDescriptors);
assertEquals(expectedAdaptationSet.representations, actualAdaptationSet.representations);
}
}
}
private static Representation[][][] newRepresentations(int periodCount, int adaptationSetCounts,
int representationCounts) {
Representation[][][] representations = new Representation[periodCount][][];
for (int i = 0; i < periodCount; i++) {
representations[i] = new Representation[adaptationSetCounts][];
for (int j = 0; j < adaptationSetCounts; j++) {
representations[i][j] = new Representation[representationCounts];
for (int k = 0; k < representationCounts; k++) {
representations[i][j][k] = newRepresentation();
}
}
}
return representations;
}
private static Representation newRepresentation() {
return Representation.newInstance("", 0, DUMMY_FORMAT, "", DUMMY_SEGMENT_BASE);
}
private static DashManifest newDashManifest(int duration, Period... periods) {
return new DashManifest(0, duration, 1, false, 2, 3, 4, DUMMY_UTC_TIMING, Uri.EMPTY,
Arrays.asList(periods));
}
private static Period newPeriod(String id, int startMs, AdaptationSet... adaptationSets) {
return new Period(id, startMs, Arrays.asList(adaptationSets));
}
private static AdaptationSet newAdaptationSet(int seed, Representation... representations) {
return new AdaptationSet(++seed, ++seed, Arrays.asList(representations),
DUMMY_ACCESSIBILITY_DESCRIPTORS);
}
}
...@@ -19,6 +19,7 @@ import android.net.Uri; ...@@ -19,6 +19,7 @@ import android.net.Uri;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.util.MimeTypes;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
...@@ -53,12 +54,14 @@ public class HlsMasterPlaylistParserTest extends TestCase { ...@@ -53,12 +54,14 @@ public class HlsMasterPlaylistParserTest extends TestCase {
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+ "http://example.com/low.m3u8\n"; + "http://example.com/low.m3u8\n";
public void testParseMasterPlaylist() throws IOException{ private static final String MASTER_PLAYLIST_WITH_CC = " #EXTM3U \n"
HlsPlaylist playlist = parsePlaylist(PLAYLIST_URI, MASTER_PLAYLIST); + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n"
assertNotNull(playlist); + "\n"
assertEquals(HlsPlaylist.TYPE_MASTER, playlist.type); + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+ "http://example.com/low.m3u8\n";
HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; public void testParseMasterPlaylist() throws IOException{
HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST);
List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants; List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
assertNotNull(variants); assertNotNull(variants);
...@@ -98,18 +101,28 @@ public class HlsMasterPlaylistParserTest extends TestCase { ...@@ -98,18 +101,28 @@ public class HlsMasterPlaylistParserTest extends TestCase {
public void testPlaylistWithInvalidHeader() throws IOException { public void testPlaylistWithInvalidHeader() throws IOException {
try { try {
parsePlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER); parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER);
fail("Expected exception not thrown."); fail("Expected exception not thrown.");
} catch (ParserException e) { } catch (ParserException e) {
// Expected due to invalid header. // Expected due to invalid header.
} }
} }
private static HlsPlaylist parsePlaylist(String uri, String playlistString) throws IOException { public void testPlaylistWithClosedCaption() throws IOException {
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST_WITH_CC);
assertEquals(1, playlist.muxedCaptionFormats.size());
Format closedCaptionFormat = playlist.muxedCaptionFormats.get(0);
assertEquals(MimeTypes.APPLICATION_CEA708, closedCaptionFormat.sampleMimeType);
assertEquals(4, closedCaptionFormat.accessibilityChannel);
assertEquals("es", closedCaptionFormat.language);
}
private static HlsMasterPlaylist parseMasterPlaylist(String uri, String playlistString)
throws IOException {
Uri playlistUri = Uri.parse(uri); Uri playlistUri = Uri.parse(uri);
ByteArrayInputStream inputStream = new ByteArrayInputStream( ByteArrayInputStream inputStream = new ByteArrayInputStream(
playlistString.getBytes(Charset.forName(C.UTF8_NAME))); playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
return new HlsPlaylistParser().parse(playlistUri, inputStream); return (HlsMasterPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
} }
} }
...@@ -36,6 +36,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { ...@@ -36,6 +36,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
String playlistString = "#EXTM3U\n" String playlistString = "#EXTM3U\n"
+ "#EXT-X-VERSION:3\n" + "#EXT-X-VERSION:3\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n" + "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-START:TIME-OFFSET=-25"
+ "#EXT-X-TARGETDURATION:8\n" + "#EXT-X-TARGETDURATION:8\n"
+ "#EXT-X-MEDIA-SEQUENCE:2679\n" + "#EXT-X-MEDIA-SEQUENCE:2679\n"
+ "#EXT-X-DISCONTINUITY-SEQUENCE:4\n" + "#EXT-X-DISCONTINUITY-SEQUENCE:4\n"
...@@ -73,6 +74,7 @@ public class HlsMediaPlaylistParserTest extends TestCase { ...@@ -73,6 +74,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist; HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
assertEquals(HlsMediaPlaylist.PLAYLIST_TYPE_VOD, mediaPlaylist.playlistType); assertEquals(HlsMediaPlaylist.PLAYLIST_TYPE_VOD, mediaPlaylist.playlistType);
assertEquals(mediaPlaylist.durationUs - 25000000, mediaPlaylist.startOffsetUs);
assertEquals(2679, mediaPlaylist.mediaSequence); assertEquals(2679, mediaPlaylist.mediaSequence);
assertEquals(3, mediaPlaylist.version); assertEquals(3, mediaPlaylist.version);
......
...@@ -20,9 +20,9 @@ import android.test.InstrumentationTestCase; ...@@ -20,9 +20,9 @@ import android.test.InstrumentationTestCase;
import android.test.MoreAsserts; import android.test.MoreAsserts;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.FakeDataSource; import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.testutil.FakeDataSource.Builder;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
...@@ -42,13 +42,13 @@ public class CacheDataSourceTest extends InstrumentationTestCase { ...@@ -42,13 +42,13 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor()); simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
} }
@Override @Override
protected void tearDown() throws Exception { protected void tearDown() throws Exception {
TestUtil.recursiveDelete(cacheDir); Util.recursiveDelete(cacheDir);
} }
public void testMaxCacheFileSize() throws Exception { public void testMaxCacheFileSize() throws Exception {
...@@ -126,9 +126,15 @@ public class CacheDataSourceTest extends InstrumentationTestCase { ...@@ -126,9 +126,15 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
MoreAsserts.assertEmpty(simpleCache.getKeys()); MoreAsserts.assertEmpty(simpleCache.getKeys());
} }
public void testReadOnlyCache() throws Exception {
CacheDataSource cacheDataSource = createCacheDataSource(false, false, 0, null);
assertReadDataContentLength(cacheDataSource, false, false);
assertEquals(0, cacheDir.list().length);
}
private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength) private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength)
throws IOException { throws IOException {
// Read all data from upstream and cache // Read all data from upstream and write to cache
CacheDataSource cacheDataSource = createCacheDataSource(false, simulateUnknownLength); CacheDataSource cacheDataSource = createCacheDataSource(false, simulateUnknownLength);
assertReadDataContentLength(cacheDataSource, unboundedRequest, simulateUnknownLength); assertReadDataContentLength(cacheDataSource, unboundedRequest, simulateUnknownLength);
...@@ -184,14 +190,21 @@ public class CacheDataSourceTest extends InstrumentationTestCase { ...@@ -184,14 +190,21 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
private CacheDataSource createCacheDataSource(boolean setReadException, private CacheDataSource createCacheDataSource(boolean setReadException,
boolean simulateUnknownLength, @CacheDataSource.Flags int flags) { boolean simulateUnknownLength, @CacheDataSource.Flags int flags) {
Builder builder = new Builder(); return createCacheDataSource(setReadException, simulateUnknownLength, flags,
new CacheDataSink(simpleCache, MAX_CACHE_FILE_SIZE));
}
private CacheDataSource createCacheDataSource(boolean setReadException,
boolean simulateUnknownLength, @CacheDataSource.Flags int flags,
CacheDataSink cacheWriteDataSink) {
FakeDataSource.Builder builder = new FakeDataSource.Builder();
if (setReadException) { if (setReadException) {
builder.appendReadError(new IOException("Shouldn't read from upstream")); builder.appendReadError(new IOException("Shouldn't read from upstream"));
} }
builder.setSimulateUnknownLength(simulateUnknownLength); FakeDataSource upstream =
builder.appendReadData(TEST_DATA); builder.setSimulateUnknownLength(simulateUnknownLength).appendReadData(TEST_DATA).build();
FakeDataSource upstream = builder.build(); return new CacheDataSource(simpleCache, upstream, new FileDataSource(), cacheWriteDataSink,
return new CacheDataSource(simpleCache, upstream, flags, MAX_CACHE_FILE_SIZE); flags, null);
} }
} }
...@@ -4,7 +4,7 @@ import android.test.InstrumentationTestCase; ...@@ -4,7 +4,7 @@ import android.test.InstrumentationTestCase;
import android.test.MoreAsserts; import android.test.MoreAsserts;
import android.util.SparseArray; import android.util.SparseArray;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
...@@ -36,13 +36,13 @@ public class CachedContentIndexTest extends InstrumentationTestCase { ...@@ -36,13 +36,13 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
@Override @Override
public void setUp() throws Exception { public void setUp() throws Exception {
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
index = new CachedContentIndex(cacheDir); index = new CachedContentIndex(cacheDir);
} }
@Override @Override
protected void tearDown() throws Exception { protected void tearDown() throws Exception {
TestUtil.recursiveDelete(cacheDir); Util.recursiveDelete(cacheDir);
} }
public void testAddGetRemove() throws Exception { public void testAddGetRemove() throws Exception {
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream.cache; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream.cache;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.ChunkIndex; import com.google.android.exoplayer2.extractor.ChunkIndex;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import org.mockito.Mock; import org.mockito.Mock;
...@@ -49,13 +50,13 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase { ...@@ -49,13 +50,13 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase {
tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX); tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX);
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
index = new CachedContentIndex(cacheDir); index = new CachedContentIndex(cacheDir);
} }
@Override @Override
protected void tearDown() throws Exception { protected void tearDown() throws Exception {
TestUtil.recursiveDelete(cacheDir); Util.recursiveDelete(cacheDir);
} }
public void testGetRegion_noSpansInCache() { public void testGetRegion_noSpansInCache() {
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
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.testutil.TestUtil; import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
...@@ -48,13 +48,13 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase { ...@@ -48,13 +48,13 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
index = new CachedContentIndex(cacheDir); index = new CachedContentIndex(cacheDir);
} }
@Override @Override
protected void tearDown() throws Exception { protected void tearDown() throws Exception {
TestUtil.recursiveDelete(cacheDir); Util.recursiveDelete(cacheDir);
} }
public void testCacheFile() throws Exception { public void testCacheFile() throws Exception {
......
...@@ -18,7 +18,6 @@ package com.google.android.exoplayer2.upstream.cache; ...@@ -18,7 +18,6 @@ package com.google.android.exoplayer2.upstream.cache;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import android.test.MoreAsserts; import android.test.MoreAsserts;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
...@@ -39,12 +38,12 @@ public class SimpleCacheTest extends InstrumentationTestCase { ...@@ -39,12 +38,12 @@ public class SimpleCacheTest extends InstrumentationTestCase {
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
this.cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext()); cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
} }
@Override @Override
protected void tearDown() throws Exception { protected void tearDown() throws Exception {
TestUtil.recursiveDelete(cacheDir); Util.recursiveDelete(cacheDir);
} }
public void testCommittingOneFile() throws Exception { public void testCommittingOneFile() throws Exception {
...@@ -192,6 +191,41 @@ public class SimpleCacheTest extends InstrumentationTestCase { ...@@ -192,6 +191,41 @@ public class SimpleCacheTest extends InstrumentationTestCase {
assertEquals(0, cacheDir.listFiles().length); assertEquals(0, cacheDir.listFiles().length);
} }
public void testGetCachedBytes() throws Exception {
SimpleCache simpleCache = getSimpleCache();
CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0);
// No cached bytes, returns -'length'
assertEquals(-100, simpleCache.getCachedBytes(KEY_1, 0, 100));
// Position value doesn't affect the return value
assertEquals(-100, simpleCache.getCachedBytes(KEY_1, 20, 100));
addCache(simpleCache, KEY_1, 0, 15);
// Returns the length of a single span
assertEquals(15, simpleCache.getCachedBytes(KEY_1, 0, 100));
// Value is capped by the 'length'
assertEquals(10, simpleCache.getCachedBytes(KEY_1, 0, 10));
addCache(simpleCache, KEY_1, 15, 35);
// Returns the length of two adjacent spans
assertEquals(50, simpleCache.getCachedBytes(KEY_1, 0, 100));
addCache(simpleCache, KEY_1, 60, 10);
// Not adjacent span doesn't affect return value
assertEquals(50, simpleCache.getCachedBytes(KEY_1, 0, 100));
// Returns length of hole up to the next cached span
assertEquals(-5, simpleCache.getCachedBytes(KEY_1, 55, 100));
simpleCache.releaseHoleSpan(cacheSpan);
}
private SimpleCache getSimpleCache() { private SimpleCache getSimpleCache() {
return new SimpleCache(cacheDir, new NoOpCacheEvictor()); return new SimpleCache(cacheDir, new NoOpCacheEvictor());
} }
......
...@@ -17,7 +17,6 @@ ...@@ -17,7 +17,6 @@
package com.google.android.exoplayer2.util; package com.google.android.exoplayer2.util;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
...@@ -34,14 +33,14 @@ public class AtomicFileTest extends InstrumentationTestCase { ...@@ -34,14 +33,14 @@ public class AtomicFileTest extends InstrumentationTestCase {
@Override @Override
public void setUp() throws Exception { public void setUp() throws Exception {
tempFolder = TestUtil.createTempFolder(getInstrumentation().getContext()); tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
file = new File(tempFolder, "atomicFile"); file = new File(tempFolder, "atomicFile");
atomicFile = new AtomicFile(file); atomicFile = new AtomicFile(file);
} }
@Override @Override
protected void tearDown() throws Exception { protected void tearDown() throws Exception {
TestUtil.recursiveDelete(tempFolder); Util.recursiveDelete(tempFolder);
} }
public void testDelete() throws Exception { public void testDelete() throws Exception {
......
...@@ -142,8 +142,10 @@ public class UtilTest extends TestCase { ...@@ -142,8 +142,10 @@ public class UtilTest extends TestCase {
public void testParseXsDateTime() throws Exception { 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(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00,000Z"));
assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-08:00")); assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-08:00"));
assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-0800")); assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-0800"));
assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55.000-0800"));
} }
public void testUnescapeInvalidFileName() { public void testUnescapeInvalidFileName() {
......
...@@ -255,6 +255,14 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { ...@@ -255,6 +255,14 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
} }
/** /**
* Use {@link #readSource(FormatHolder, DecoderInputBuffer, boolean)} instead.
*/
@Deprecated
protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer) {
return readSource(formatHolder, buffer, false);
}
/**
* 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 #setCurrentStreamFinal()} 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.
...@@ -262,13 +270,16 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { ...@@ -262,13 +270,16 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
* @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
* @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
* end of the stream. If the end of the stream has been reached, the * end of the stream. If the end of the stream has been reached, the
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. May be null if the * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.
* caller requires that the format of the stream be read even if it's not changing. * @param formatRequired Whether the caller requires that the format of the stream be read even if
* it's not changing. A sample will never be read if set to true, however it is still possible
* for the end of stream or nothing to be read.
* @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
* {@link C#RESULT_BUFFER_READ}. * {@link C#RESULT_BUFFER_READ}.
*/ */
protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer) { protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer,
int result = stream.readData(formatHolder, buffer); boolean formatRequired) {
int result = stream.readData(formatHolder, buffer, formatRequired);
if (result == C.RESULT_BUFFER_READ) { if (result == C.RESULT_BUFFER_READ) {
if (buffer.isEndOfStream()) { if (buffer.isEndOfStream()) {
readEndOfStream = true; readEndOfStream = true;
......
...@@ -444,8 +444,15 @@ public final class C { ...@@ -444,8 +444,15 @@ public final class C {
public static final UUID UUID_NIL = new UUID(0L, 0L); public static final UUID UUID_NIL = new UUID(0L, 0L);
/** /**
* UUID for the ClearKey DRM scheme.
* <p>
* ClearKey is supported on Android devices running Android 5.0 (API Level 21) and up.
*/
public static final UUID CLEARKEY_UUID = new UUID(0x1077EFECC0B24D02L, 0xACE33C1E52E2FB4BL);
/**
* UUID for the Widevine DRM scheme. * UUID for the Widevine DRM scheme.
* <p></p> * <p>
* Widevine is supported on Android devices running Android 4.3 (API Level 18) and up. * Widevine is supported on Android devices running Android 4.3 (API Level 18) and up.
*/ */
public static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL); public static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL);
...@@ -477,7 +484,7 @@ public final class C { ...@@ -477,7 +484,7 @@ public final class C {
* {@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}, or null, 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;
...@@ -515,7 +522,13 @@ public final class C { ...@@ -515,7 +522,13 @@ public final class C {
* The stereo mode for 360/3D/VR videos. * The stereo mode for 360/3D/VR videos.
*/ */
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef({Format.NO_VALUE, STEREO_MODE_MONO, STEREO_MODE_TOP_BOTTOM, STEREO_MODE_LEFT_RIGHT}) @IntDef({
Format.NO_VALUE,
STEREO_MODE_MONO,
STEREO_MODE_TOP_BOTTOM,
STEREO_MODE_LEFT_RIGHT,
STEREO_MODE_STEREO_MESH
})
public @interface StereoMode {} public @interface StereoMode {}
/** /**
* Indicates Monoscopic stereo layout, used with 360/3D/VR videos. * Indicates Monoscopic stereo layout, used with 360/3D/VR videos.
...@@ -529,6 +542,16 @@ public final class C { ...@@ -529,6 +542,16 @@ public final class C {
* Indicates Left-Right stereo layout, used with 360/3D/VR videos. * Indicates Left-Right stereo layout, used with 360/3D/VR videos.
*/ */
public static final int STEREO_MODE_LEFT_RIGHT = 2; public static final int STEREO_MODE_LEFT_RIGHT = 2;
/**
* Indicates a stereo layout where the left and right eyes have separate meshes,
* used with 360/3D/VR videos.
*/
public static final int STEREO_MODE_STEREO_MESH = 3;
/**
* Priority for media playback.
*/
public static final int PRIORITY_PLAYBACK = 0;
/** /**
* Converts a time in microseconds to the corresponding time in milliseconds, preserving * Converts a time in microseconds to the corresponding time in milliseconds, preserving
......
...@@ -51,11 +51,6 @@ public final class DefaultLoadControl implements LoadControl { ...@@ -51,11 +51,6 @@ public final class DefaultLoadControl implements LoadControl {
*/ */
public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000; public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000;
/**
* Priority for media loading.
*/
public static final int LOADING_PRIORITY = 0;
private static final int ABOVE_HIGH_WATERMARK = 0; private static final int ABOVE_HIGH_WATERMARK = 0;
private static final int BETWEEN_WATERMARKS = 1; private static final int BETWEEN_WATERMARKS = 1;
private static final int BELOW_LOW_WATERMARK = 2; private static final int BELOW_LOW_WATERMARK = 2;
...@@ -122,7 +117,7 @@ public final class DefaultLoadControl implements LoadControl { ...@@ -122,7 +117,7 @@ public final class DefaultLoadControl implements LoadControl {
* playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by * playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by
* buffer depletion rather than a user action. * buffer depletion rather than a user action.
* @param priorityTaskManager If not null, registers itself as a task with priority * @param priorityTaskManager If not null, registers itself as a task with priority
* {@link #LOADING_PRIORITY} during loading periods, and unregisters itself during draining * {@link C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining
* periods. * periods.
*/ */
public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
...@@ -183,9 +178,9 @@ public final class DefaultLoadControl implements LoadControl { ...@@ -183,9 +178,9 @@ public final class DefaultLoadControl implements LoadControl {
|| (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached); || (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached);
if (priorityTaskManager != null && isBuffering != wasBuffering) { if (priorityTaskManager != null && isBuffering != wasBuffering) {
if (isBuffering) { if (isBuffering) {
priorityTaskManager.add(LOADING_PRIORITY); priorityTaskManager.add(C.PRIORITY_PLAYBACK);
} else { } else {
priorityTaskManager.remove(LOADING_PRIORITY); priorityTaskManager.remove(C.PRIORITY_PLAYBACK);
} }
} }
return isBuffering; return isBuffering;
...@@ -199,7 +194,7 @@ public final class DefaultLoadControl implements LoadControl { ...@@ -199,7 +194,7 @@ public final class DefaultLoadControl implements LoadControl {
private void reset(boolean resetAllocator) { private void reset(boolean resetAllocator) {
targetBufferSize = 0; targetBufferSize = 0;
if (priorityTaskManager != null && isBuffering) { if (priorityTaskManager != null && isBuffering) {
priorityTaskManager.remove(LOADING_PRIORITY); priorityTaskManager.remove(C.PRIORITY_PLAYBACK);
} }
isBuffering = false; isBuffering = false;
if (resetAllocator) { if (resetAllocator) {
......
...@@ -56,8 +56,7 @@ public final class ExoPlaybackException extends Exception { ...@@ -56,8 +56,7 @@ public final class ExoPlaybackException extends Exception {
* The type of the playback failure. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER} and * The type of the playback failure. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER} and
* {@link #TYPE_UNEXPECTED}. * {@link #TYPE_UNEXPECTED}.
*/ */
@Type @Type public final int type;
public final int type;
/** /**
* If {@link #type} is {@link #TYPE_RENDERER}, this is the index of the renderer. * If {@link #type} is {@link #TYPE_RENDERER}, this is the index of the renderer.
......
...@@ -455,6 +455,8 @@ import java.io.IOException; ...@@ -455,6 +455,8 @@ import java.io.IOException;
TraceUtil.beginSection("doSomeWork"); TraceUtil.beginSection("doSomeWork");
updatePlaybackPositions(); updatePlaybackPositions();
playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs);
boolean allRenderersEnded = true; boolean allRenderersEnded = true;
boolean allRenderersReadyOrEnded = true; boolean allRenderersReadyOrEnded = true;
for (Renderer renderer : enabledRenderers) { for (Renderer renderer : enabledRenderers) {
......
...@@ -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.2.0"; String VERSION = "2.3.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 = 2002000; int VERSION_INT = 2003000;
/** /**
* 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}
......
...@@ -120,7 +120,7 @@ public final class Format implements Parcelable { ...@@ -120,7 +120,7 @@ public final class Format implements Parcelable {
/** /**
* The stereo layout for 360/3D/VR video, or {@link #NO_VALUE} if not applicable. Valid stereo * The stereo layout for 360/3D/VR video, or {@link #NO_VALUE} if not applicable. Valid stereo
* modes are {@link C#STEREO_MODE_MONO}, {@link C#STEREO_MODE_TOP_BOTTOM}, {@link * modes are {@link C#STEREO_MODE_MONO}, {@link C#STEREO_MODE_TOP_BOTTOM}, {@link
* C#STEREO_MODE_LEFT_RIGHT}. * C#STEREO_MODE_LEFT_RIGHT}, {@link C#STEREO_MODE_STEREO_MESH}.
*/ */
@C.StereoMode @C.StereoMode
public final int stereoMode; public final int stereoMode;
...@@ -438,16 +438,19 @@ public final class Format implements Parcelable { ...@@ -438,16 +438,19 @@ public final class Format implements Parcelable {
drmInitData, metadata); drmInitData, metadata);
} }
public Format copyWithManifestFormatInfo(Format manifestFormat, public Format copyWithManifestFormatInfo(Format manifestFormat) {
boolean preferManifestDrmInitData) { if (this == manifestFormat) {
// No need to copy from ourselves.
return this;
}
String id = manifestFormat.id; String id = manifestFormat.id;
String codecs = this.codecs == null ? manifestFormat.codecs : this.codecs; String codecs = this.codecs == null ? manifestFormat.codecs : this.codecs;
int bitrate = this.bitrate == NO_VALUE ? manifestFormat.bitrate : this.bitrate; int bitrate = this.bitrate == NO_VALUE ? manifestFormat.bitrate : this.bitrate;
float frameRate = this.frameRate == NO_VALUE ? manifestFormat.frameRate : this.frameRate; float frameRate = this.frameRate == NO_VALUE ? manifestFormat.frameRate : this.frameRate;
@C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags; @C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags;
String language = this.language == null ? manifestFormat.language : this.language; String language = this.language == null ? manifestFormat.language : this.language;
DrmInitData drmInitData = (preferManifestDrmInitData && manifestFormat.drmInitData != null) DrmInitData drmInitData = manifestFormat.drmInitData != null ? manifestFormat.drmInitData
|| this.drmInitData == null ? manifestFormat.drmInitData : this.drmInitData; : this.drmInitData;
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width, return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags,
...@@ -672,9 +675,6 @@ public final class Format implements Parcelable { ...@@ -672,9 +675,6 @@ public final class Format implements Parcelable {
dest.writeParcelable(metadata, 0); dest.writeParcelable(metadata, 0);
} }
/**
* {@link Creator} implementation.
*/
public static final Creator<Format> CREATOR = new Creator<Format>() { public static final Creator<Format> CREATOR = new Creator<Format>() {
@Override @Override
......
...@@ -28,6 +28,7 @@ import android.view.SurfaceHolder; ...@@ -28,6 +28,7 @@ import android.view.SurfaceHolder;
import android.view.SurfaceView; import android.view.SurfaceView;
import android.view.TextureView; import android.view.TextureView;
import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioCapabilities;
import com.google.android.exoplayer2.audio.AudioProcessor;
import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderCounters;
...@@ -624,7 +625,7 @@ public class SimpleExoPlayer implements ExoPlayer { ...@@ -624,7 +625,7 @@ public class SimpleExoPlayer implements ExoPlayer {
buildVideoRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, buildVideoRenderers(context, mainHandler, drmSessionManager, extensionRendererMode,
componentListener, allowedVideoJoiningTimeMs, out); componentListener, allowedVideoJoiningTimeMs, out);
buildAudioRenderers(context, mainHandler, drmSessionManager, extensionRendererMode, buildAudioRenderers(context, mainHandler, drmSessionManager, extensionRendererMode,
componentListener, out); componentListener, buildAudioProcessors(), out);
buildTextRenderers(context, mainHandler, extensionRendererMode, componentListener, out); buildTextRenderers(context, mainHandler, extensionRendererMode, componentListener, out);
buildMetadataRenderers(context, mainHandler, extensionRendererMode, componentListener, out); buildMetadataRenderers(context, mainHandler, extensionRendererMode, componentListener, out);
buildMiscellaneousRenderers(context, mainHandler, extensionRendererMode, out); buildMiscellaneousRenderers(context, mainHandler, extensionRendererMode, out);
...@@ -636,7 +637,7 @@ public class SimpleExoPlayer implements ExoPlayer { ...@@ -636,7 +637,7 @@ public class SimpleExoPlayer implements ExoPlayer {
* @param context The {@link Context} associated with the player. * @param context The {@link Context} associated with the player.
* @param mainHandler A handler associated with the main thread's looper. * @param mainHandler A handler associated with the main thread's looper.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will
* not be used for DRM protected playbacks. * not be used for DRM protected playbacks.
* @param extensionRendererMode The extension renderer mode. * @param extensionRendererMode The extension renderer mode.
* @param eventListener An event listener. * @param eventListener An event listener.
* @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video renderers * @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video renderers
...@@ -681,17 +682,19 @@ public class SimpleExoPlayer implements ExoPlayer { ...@@ -681,17 +682,19 @@ public class SimpleExoPlayer implements ExoPlayer {
* @param context The {@link Context} associated with the player. * @param context The {@link Context} associated with the player.
* @param mainHandler A handler associated with the main thread's looper. * @param mainHandler A handler associated with the main thread's looper.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will
* not be used for DRM protected playbacks. * not be used for DRM protected playbacks.
* @param extensionRendererMode The extension renderer mode. * @param extensionRendererMode The extension renderer mode.
* @param eventListener An event listener. * @param eventListener An event listener.
* @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio buffers
* before output. May be empty.
* @param out An array to which the built renderers should be appended. * @param out An array to which the built renderers should be appended.
*/ */
protected void buildAudioRenderers(Context context, Handler mainHandler, protected void buildAudioRenderers(Context context, Handler mainHandler,
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
@ExtensionRendererMode int extensionRendererMode, AudioRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode, AudioRendererEventListener eventListener,
ArrayList<Renderer> out) { AudioProcessor[] audioProcessors, ArrayList<Renderer> out) {
out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true, out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true,
mainHandler, eventListener, AudioCapabilities.getCapabilities(context))); mainHandler, eventListener, AudioCapabilities.getCapabilities(context), audioProcessors));
if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) { if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
return; return;
...@@ -705,8 +708,9 @@ public class SimpleExoPlayer implements ExoPlayer { ...@@ -705,8 +708,9 @@ public class SimpleExoPlayer implements ExoPlayer {
Class<?> clazz = Class<?> clazz =
Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer"); Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer");
Constructor<?> constructor = clazz.getConstructor(Handler.class, Constructor<?> constructor = clazz.getConstructor(Handler.class,
AudioRendererEventListener.class); AudioRendererEventListener.class, AudioProcessor[].class);
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener,
audioProcessors);
out.add(extensionRendererIndex++, renderer); out.add(extensionRendererIndex++, renderer);
Log.i(TAG, "Loaded LibopusAudioRenderer."); Log.i(TAG, "Loaded LibopusAudioRenderer.");
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {
...@@ -719,8 +723,9 @@ public class SimpleExoPlayer implements ExoPlayer { ...@@ -719,8 +723,9 @@ public class SimpleExoPlayer implements ExoPlayer {
Class<?> clazz = Class<?> clazz =
Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer"); Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer");
Constructor<?> constructor = clazz.getConstructor(Handler.class, Constructor<?> constructor = clazz.getConstructor(Handler.class,
AudioRendererEventListener.class); AudioRendererEventListener.class, AudioProcessor[].class);
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener,
audioProcessors);
out.add(extensionRendererIndex++, renderer); out.add(extensionRendererIndex++, renderer);
Log.i(TAG, "Loaded LibflacAudioRenderer."); Log.i(TAG, "Loaded LibflacAudioRenderer.");
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {
...@@ -733,8 +738,9 @@ public class SimpleExoPlayer implements ExoPlayer { ...@@ -733,8 +738,9 @@ public class SimpleExoPlayer implements ExoPlayer {
Class<?> clazz = Class<?> clazz =
Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer"); Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer");
Constructor<?> constructor = clazz.getConstructor(Handler.class, Constructor<?> constructor = clazz.getConstructor(Handler.class,
AudioRendererEventListener.class); AudioRendererEventListener.class, AudioProcessor[].class);
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener); Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener,
audioProcessors);
out.add(extensionRendererIndex++, renderer); out.add(extensionRendererIndex++, renderer);
Log.i(TAG, "Loaded FfmpegAudioRenderer."); Log.i(TAG, "Loaded FfmpegAudioRenderer.");
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {
...@@ -787,6 +793,13 @@ public class SimpleExoPlayer implements ExoPlayer { ...@@ -787,6 +793,13 @@ public class SimpleExoPlayer implements ExoPlayer {
// Do nothing. // Do nothing.
} }
/**
* Builds an array of {@link AudioProcessor}s that will process PCM audio before output.
*/
protected AudioProcessor[] buildAudioProcessors() {
return new AudioProcessor[0];
}
// Internal methods. // Internal methods.
private void removeSurfaceCallbacks() { private void removeSurfaceCallbacks() {
......
/*
* Copyright (C) 2017 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.audio;
import com.google.android.exoplayer2.C;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* Interface for audio processors.
*/
public interface AudioProcessor {
/**
* Exception thrown when a processor can't be configured for a given input audio format.
*/
final class UnhandledFormatException extends Exception {
public UnhandledFormatException(int sampleRateHz, int channelCount, @C.Encoding int encoding) {
super("Unhandled format: " + sampleRateHz + " Hz, " + channelCount + " channels in encoding "
+ encoding);
}
}
/**
* An empty, direct {@link ByteBuffer}.
*/
ByteBuffer EMPTY_BUFFER = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder());
/**
* Configures the processor to process input audio with the specified format. After calling this
* method, {@link #isActive()} returns whether the processor needs to handle buffers; if not, the
* processor will not accept any buffers until it is reconfigured. Returns {@code true} if the
* processor must be flushed, or if the value returned by {@link #isActive()} has changed as a
* result of the call. If it's active, {@link #getOutputChannelCount()} and
* {@link #getOutputEncoding()} return the processor's output format.
*
* @param sampleRateHz The sample rate of input audio in Hz.
* @param channelCount The number of interleaved channels in input audio.
* @param encoding The encoding of input audio.
* @return {@code true} if the processor must be flushed or the value returned by
* {@link #isActive()} has changed as a result of the call.
* @throws UnhandledFormatException Thrown if the specified format can't be handled as input.
*/
boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding)
throws UnhandledFormatException;
/**
* Returns whether the processor is configured and active.
*/
boolean isActive();
/**
* Returns the number of audio channels in the data output by the processor.
*/
int getOutputChannelCount();
/**
* Returns the audio encoding used in the data output by the processor.
*/
@C.Encoding
int getOutputEncoding();
/**
* Queues audio data between the position and limit of the input {@code buffer} for processing.
* {@code buffer} must be a direct byte buffer with native byte order. Its contents are treated as
* read-only. Its position will be advanced by the number of bytes consumed (which may be zero).
* The caller retains ownership of the provided buffer. Calling this method invalidates any
* previous buffer returned by {@link #getOutput()}.
*
* @param buffer The input buffer to process.
*/
void queueInput(ByteBuffer buffer);
/**
* Queues an end of stream signal. After this method has been called,
* {@link #queueInput(ByteBuffer)} may not be called until after the next call to
* {@link #flush()}. Calling {@link #getOutput()} will return any remaining output data. Multiple
* calls may be required to read all of the remaining output data. {@link #isEnded()} will return
* {@code true} once all remaining output data has been read.
*/
void queueEndOfStream();
/**
* Returns a buffer containing processed output data between its position and limit. The buffer
* will always be a direct byte buffer with native byte order. Calling this method invalidates any
* previously returned buffer. The buffer will be empty if no output is available.
*
* @return A buffer containing processed output data between its position and limit.
*/
ByteBuffer getOutput();
/**
* Returns whether this processor will return no more output from {@link #getOutput()} until it
* has been {@link #flush()}ed and more input has been queued.
*/
boolean isEnded();
/**
* Clears any state in preparation for receiving a new stream of input buffers.
*/
void flush();
/**
* Releases any resources associated with this instance.
*/
void release();
}
/*
* Copyright (C) 2017 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.audio;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.Encoding;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
/**
* An {@link AudioProcessor} that applies a mapping from input channels onto specified output
* channels. This can be used to reorder, duplicate or discard channels.
*/
/* package */ final class ChannelMappingAudioProcessor implements AudioProcessor {
private int channelCount;
private int sampleRateHz;
private int[] pendingOutputChannels;
private boolean active;
private int[] outputChannels;
private ByteBuffer buffer;
private ByteBuffer outputBuffer;
private boolean inputEnded;
/**
* Creates a new processor that applies a channel mapping.
*/
public ChannelMappingAudioProcessor() {
buffer = EMPTY_BUFFER;
outputBuffer = EMPTY_BUFFER;
}
/**
* Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)}
* to start using the new channel map.
*
* @see AudioTrack#configure(String, int, int, int, int, int[])
*/
public void setChannelMap(int[] outputChannels) {
pendingOutputChannels = outputChannels;
}
@Override
public boolean configure(int sampleRateHz, int channelCount, @Encoding int encoding)
throws UnhandledFormatException {
boolean outputChannelsChanged = !Arrays.equals(pendingOutputChannels, outputChannels);
outputChannels = pendingOutputChannels;
if (outputChannels == null) {
active = false;
return outputChannelsChanged;
}
if (encoding != C.ENCODING_PCM_16BIT) {
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
}
if (!outputChannelsChanged && this.sampleRateHz == sampleRateHz
&& this.channelCount == channelCount) {
return false;
}
this.sampleRateHz = sampleRateHz;
this.channelCount = channelCount;
active = channelCount != outputChannels.length;
for (int i = 0; i < outputChannels.length; i++) {
int channelIndex = outputChannels[i];
if (channelIndex >= channelCount) {
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
}
active |= (channelIndex != i);
}
return true;
}
@Override
public boolean isActive() {
return active;
}
@Override
public int getOutputChannelCount() {
return outputChannels == null ? channelCount : outputChannels.length;
}
@Override
public int getOutputEncoding() {
return C.ENCODING_PCM_16BIT;
}
@Override
public void queueInput(ByteBuffer inputBuffer) {
int position = inputBuffer.position();
int limit = inputBuffer.limit();
int frameCount = (limit - position) / (2 * channelCount);
int outputSize = frameCount * outputChannels.length * 2;
if (buffer.capacity() < outputSize) {
buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder());
} else {
buffer.clear();
}
while (position < limit) {
for (int channelIndex : outputChannels) {
buffer.putShort(inputBuffer.getShort(position + 2 * channelIndex));
}
position += channelCount * 2;
}
inputBuffer.position(limit);
buffer.flip();
outputBuffer = buffer;
}
@Override
public void queueEndOfStream() {
inputEnded = true;
}
@Override
public ByteBuffer getOutput() {
ByteBuffer outputBuffer = this.outputBuffer;
this.outputBuffer = EMPTY_BUFFER;
return outputBuffer;
}
@SuppressWarnings("ReferenceEquality")
@Override
public boolean isEnded() {
return inputEnded && outputBuffer == EMPTY_BUFFER;
}
@Override
public void flush() {
outputBuffer = EMPTY_BUFFER;
inputEnded = false;
}
@Override
public void release() {
flush();
buffer = EMPTY_BUFFER;
}
}
...@@ -47,8 +47,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -47,8 +47,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
private final AudioTrack audioTrack; private final AudioTrack audioTrack;
private boolean passthroughEnabled; private boolean passthroughEnabled;
private boolean codecNeedsDiscardChannelsWorkaround;
private android.media.MediaFormat passthroughMediaFormat; private android.media.MediaFormat passthroughMediaFormat;
private int pcmEncoding; private int pcmEncoding;
private int channelCount;
private long currentPositionUs; private long currentPositionUs;
private boolean allowPositionDiscontinuity; private boolean allowPositionDiscontinuity;
...@@ -121,13 +123,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -121,13 +123,16 @@ 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 audioProcessors Optional {@link AudioProcessor}s that will process PCM audio before
* output.
*/ */
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,
AudioProcessor... audioProcessors) {
super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys);
audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener()); audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener());
eventDispatcher = new EventDispatcher(eventHandler, eventListener); eventDispatcher = new EventDispatcher(eventHandler, eventListener);
} }
...@@ -185,6 +190,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -185,6 +190,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Override @Override
protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
MediaCrypto crypto) { MediaCrypto crypto) {
codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name);
if (passthroughEnabled) { if (passthroughEnabled) {
// Override the MIME type used to configure the codec if we are using a passthrough decoder. // Override the MIME type used to configure the codec if we are using a passthrough decoder.
passthroughMediaFormat = format.getFrameworkMediaFormatV16(); passthroughMediaFormat = format.getFrameworkMediaFormatV16();
...@@ -216,17 +222,33 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -216,17 +222,33 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
// output 16-bit PCM. // output 16-bit PCM.
pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding
: C.ENCODING_PCM_16BIT; : C.ENCODING_PCM_16BIT;
channelCount = newFormat.channelCount;
} }
@Override @Override
protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) { protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat)
throws ExoPlaybackException {
boolean passthrough = passthroughMediaFormat != null; boolean passthrough = passthroughMediaFormat != null;
String mimeType = passthrough ? passthroughMediaFormat.getString(MediaFormat.KEY_MIME) String mimeType = passthrough ? passthroughMediaFormat.getString(MediaFormat.KEY_MIME)
: MimeTypes.AUDIO_RAW; : MimeTypes.AUDIO_RAW;
MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat; MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat;
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0); int[] channelMap;
if (codecNeedsDiscardChannelsWorkaround && channelCount == 6 && this.channelCount < 6) {
channelMap = new int[this.channelCount];
for (int i = 0; i < this.channelCount; i++) {
channelMap[i] = i;
}
} else {
channelMap = null;
}
try {
audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0, channelMap);
} catch (AudioTrack.ConfigurationException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
} }
/** /**
...@@ -304,7 +326,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -304,7 +326,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Override @Override
public boolean isEnded() { public boolean isEnded() {
return super.isEnded() && !audioTrack.hasPendingData(); return super.isEnded() && audioTrack.isEnded();
} }
@Override @Override
...@@ -353,8 +375,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -353,8 +375,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
} }
@Override @Override
protected void onOutputStreamEnded() { protected void renderToEndOfStream() throws ExoPlaybackException {
audioTrack.handleEndOfStream(); try {
audioTrack.playToEndOfStream();
} catch (AudioTrack.WriteException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
} }
@Override @Override
...@@ -376,6 +402,20 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media ...@@ -376,6 +402,20 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
} }
} }
/**
* Returns whether the decoder is known to output six audio channels when provided with input with
* fewer than six channels.
* <p>
* See [Internal: b/35655036].
*/
private static boolean codecNeedsDiscardChannelsWorkaround(String codecName) {
// The workaround applies to Samsung Galaxy S6 and Samsung Galaxy S7.
return Util.SDK_INT < 24 && "OMX.SEC.aac.dec".equals(codecName)
&& "samsung".equals(Util.MANUFACTURER)
&& (Util.DEVICE.startsWith("zeroflte") || Util.DEVICE.startsWith("herolte")
|| Util.DEVICE.startsWith("heroqlte"));
}
private final class AudioTrackListener implements AudioTrack.Listener { private final class AudioTrackListener implements AudioTrack.Listener {
@Override @Override
......
/*
* Copyright (C) 2017 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.audio;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* An {@link AudioProcessor} that converts audio data to {@link C#ENCODING_PCM_16BIT}.
*/
/* package */ final class ResamplingAudioProcessor implements AudioProcessor {
private int sampleRateHz;
private int channelCount;
@C.PcmEncoding
private int encoding;
private ByteBuffer buffer;
private ByteBuffer outputBuffer;
private boolean inputEnded;
/**
* Creates a new audio processor that converts audio data to {@link C#ENCODING_PCM_16BIT}.
*/
public ResamplingAudioProcessor() {
sampleRateHz = Format.NO_VALUE;
channelCount = Format.NO_VALUE;
encoding = C.ENCODING_INVALID;
buffer = EMPTY_BUFFER;
outputBuffer = EMPTY_BUFFER;
}
@Override
public boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding)
throws UnhandledFormatException {
if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT
&& encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) {
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
}
if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount
&& this.encoding == encoding) {
return false;
}
this.sampleRateHz = sampleRateHz;
this.channelCount = channelCount;
this.encoding = encoding;
if (encoding == C.ENCODING_PCM_16BIT) {
buffer = EMPTY_BUFFER;
}
return true;
}
@Override
public boolean isActive() {
return encoding != C.ENCODING_INVALID && encoding != C.ENCODING_PCM_16BIT;
}
@Override
public int getOutputChannelCount() {
return channelCount;
}
@Override
public int getOutputEncoding() {
return C.ENCODING_PCM_16BIT;
}
@Override
public void queueInput(ByteBuffer inputBuffer) {
// Prepare the output buffer.
int position = inputBuffer.position();
int limit = inputBuffer.limit();
int size = limit - position;
int resampledSize;
switch (encoding) {
case C.ENCODING_PCM_8BIT:
resampledSize = size * 2;
break;
case C.ENCODING_PCM_24BIT:
resampledSize = (size / 3) * 2;
break;
case C.ENCODING_PCM_32BIT:
resampledSize = size / 2;
break;
case C.ENCODING_PCM_16BIT:
case C.ENCODING_INVALID:
case Format.NO_VALUE:
default:
throw new IllegalStateException();
}
if (buffer.capacity() < resampledSize) {
buffer = ByteBuffer.allocateDirect(resampledSize).order(ByteOrder.nativeOrder());
} else {
buffer.clear();
}
// Resample the little endian input and update the input/output buffers.
switch (encoding) {
case C.ENCODING_PCM_8BIT:
// 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up.
for (int i = position; i < limit; i++) {
buffer.put((byte) 0);
buffer.put((byte) ((inputBuffer.get(i) & 0xFF) - 128));
}
break;
case C.ENCODING_PCM_24BIT:
// 24->16 bit resampling. Drop the least significant byte.
for (int i = position; i < limit; i += 3) {
buffer.put(inputBuffer.get(i + 1));
buffer.put(inputBuffer.get(i + 2));
}
break;
case C.ENCODING_PCM_32BIT:
// 32->16 bit resampling. Drop the two least significant bytes.
for (int i = position; i < limit; i += 4) {
buffer.put(inputBuffer.get(i + 2));
buffer.put(inputBuffer.get(i + 3));
}
break;
case C.ENCODING_PCM_16BIT:
case C.ENCODING_INVALID:
case Format.NO_VALUE:
default:
// Never happens.
throw new IllegalStateException();
}
inputBuffer.position(inputBuffer.limit());
buffer.flip();
outputBuffer = buffer;
}
@Override
public void queueEndOfStream() {
inputEnded = true;
}
@Override
public ByteBuffer getOutput() {
ByteBuffer outputBuffer = this.outputBuffer;
this.outputBuffer = EMPTY_BUFFER;
return outputBuffer;
}
@SuppressWarnings("ReferenceEquality")
@Override
public boolean isEnded() {
return inputEnded && outputBuffer == EMPTY_BUFFER;
}
@Override
public void flush() {
outputBuffer = EMPTY_BUFFER;
inputEnded = false;
}
@Override
public void release() {
flush();
buffer = EMPTY_BUFFER;
}
}
...@@ -34,6 +34,7 @@ import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; ...@@ -34,6 +34,7 @@ import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MediaClock; import com.google.android.exoplayer2.util.MediaClock;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.TraceUtil;
...@@ -67,12 +68,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ...@@ -67,12 +68,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
*/ */
private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2; private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2;
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
private final boolean playClearSamplesWithoutKeys; private final boolean playClearSamplesWithoutKeys;
private final EventDispatcher eventDispatcher; private final EventDispatcher eventDispatcher;
private final AudioTrack audioTrack; private final AudioTrack audioTrack;
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
private final FormatHolder formatHolder; private final FormatHolder formatHolder;
private final DecoderInputBuffer flagsOnlyBuffer;
private DecoderCounters decoderCounters; private DecoderCounters decoderCounters;
private Format inputFormat; private Format inputFormat;
...@@ -83,8 +84,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ...@@ -83,8 +84,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
private DrmSession<ExoMediaCrypto> drmSession; private DrmSession<ExoMediaCrypto> drmSession;
private DrmSession<ExoMediaCrypto> pendingDrmSession; private DrmSession<ExoMediaCrypto> pendingDrmSession;
@ReinitializationState @ReinitializationState private int decoderReinitializationState;
private int decoderReinitializationState;
private boolean decoderReceivedBuffers; private boolean decoderReceivedBuffers;
private boolean audioTrackNeedsConfigure; private boolean audioTrackNeedsConfigure;
...@@ -102,10 +102,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ...@@ -102,10 +102,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required. * 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 eventListener A listener of events. May be null if delivery of events is not required.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/ */
public SimpleDecoderAudioRenderer(Handler eventHandler, public SimpleDecoderAudioRenderer(Handler eventHandler,
AudioRendererEventListener eventListener) { AudioRendererEventListener eventListener, AudioProcessor... audioProcessors) {
this(eventHandler, eventListener, null); this(eventHandler, eventListener, null, null, false, audioProcessors);
} }
/** /**
...@@ -133,16 +134,19 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ...@@ -133,16 +134,19 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
* begin in parallel with key acquisition. This parameter specifies whether the renderer is * begin in parallel with key acquisition. This parameter specifies whether the renderer is
* permitted to play clear regions of encrypted media files before {@code drmSessionManager} * permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media. * has obtained the keys necessary to decrypt encrypted regions of the media.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/ */
public SimpleDecoderAudioRenderer(Handler eventHandler, public SimpleDecoderAudioRenderer(Handler eventHandler,
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities, AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities,
DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys) { DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys,
AudioProcessor... audioProcessors) {
super(C.TRACK_TYPE_AUDIO); super(C.TRACK_TYPE_AUDIO);
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener());
this.drmSessionManager = drmSessionManager; this.drmSessionManager = drmSessionManager;
formatHolder = new FormatHolder();
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener());
formatHolder = new FormatHolder();
flagsOnlyBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
decoderReinitializationState = REINITIALIZATION_STATE_NONE; decoderReinitializationState = REINITIALIZATION_STATE_NONE;
audioTrackNeedsConfigure = true; audioTrackNeedsConfigure = true;
} }
...@@ -174,13 +178,31 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ...@@ -174,13 +178,31 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
@Override @Override
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
if (outputStreamEnded) { if (outputStreamEnded) {
try {
audioTrack.playToEndOfStream();
} catch (AudioTrack.WriteException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
return; return;
} }
// Try and read a format if we don't have one already. // Try and read a format if we don't have one already.
if (inputFormat == null && !readFormat()) { if (inputFormat == null) {
// We can't make progress without one. // We don't have a format yet, so try and read one.
return; flagsOnlyBuffer.clear();
int result = readSource(formatHolder, flagsOnlyBuffer, true);
if (result == C.RESULT_FORMAT_READ) {
onInputFormatChanged(formatHolder.format);
} else if (result == C.RESULT_BUFFER_READ) {
// End of stream read having not read a format.
Assertions.checkState(flagsOnlyBuffer.isEndOfStream());
inputStreamEnded = true;
processEndOfStream();
return;
} else {
// We still don't have a format and can't make progress without one.
return;
}
} }
// If we don't have a decoder yet, we need to instantiate one. // If we don't have a decoder yet, we need to instantiate one.
...@@ -193,8 +215,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ...@@ -193,8 +215,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
while (drainOutputBuffer()) {} while (drainOutputBuffer()) {}
while (feedInputBuffer()) {} while (feedInputBuffer()) {}
TraceUtil.endSection(); TraceUtil.endSection();
} catch (AudioTrack.InitializationException | AudioTrack.WriteException } catch (AudioDecoderException | AudioTrack.ConfigurationException
| AudioDecoderException e) { | AudioTrack.InitializationException | AudioTrack.WriteException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex()); throw ExoPlaybackException.createForRenderer(e, getIndex());
} }
decoderCounters.ensureUpdated(); decoderCounters.ensureUpdated();
...@@ -255,7 +277,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ...@@ -255,7 +277,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
} }
private boolean drainOutputBuffer() throws ExoPlaybackException, AudioDecoderException, private boolean drainOutputBuffer() throws ExoPlaybackException, AudioDecoderException,
AudioTrack.InitializationException, AudioTrack.WriteException { AudioTrack.ConfigurationException, AudioTrack.InitializationException,
AudioTrack.WriteException {
if (outputBuffer == null) { if (outputBuffer == null) {
outputBuffer = decoder.dequeueOutputBuffer(); outputBuffer = decoder.dequeueOutputBuffer();
if (outputBuffer == null) { if (outputBuffer == null) {
...@@ -274,8 +297,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ...@@ -274,8 +297,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
} else { } else {
outputBuffer.release(); outputBuffer.release();
outputBuffer = null; outputBuffer = null;
outputStreamEnded = true; processEndOfStream();
audioTrack.handleEndOfStream();
} }
return false; return false;
} }
...@@ -324,7 +346,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ...@@ -324,7 +346,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
// We've already read an encrypted sample into buffer, and are waiting for keys. // We've already read an encrypted sample into buffer, and are waiting for keys.
result = C.RESULT_BUFFER_READ; result = C.RESULT_BUFFER_READ;
} else { } else {
result = readSource(formatHolder, inputBuffer); result = readSource(formatHolder, inputBuffer, false);
} }
if (result == C.RESULT_NOTHING_READ) { if (result == C.RESULT_NOTHING_READ) {
...@@ -365,6 +387,15 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ...@@ -365,6 +387,15 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
&& (bufferEncrypted || !playClearSamplesWithoutKeys); && (bufferEncrypted || !playClearSamplesWithoutKeys);
} }
private void processEndOfStream() throws ExoPlaybackException {
outputStreamEnded = true;
try {
audioTrack.playToEndOfStream();
} catch (AudioTrack.WriteException e) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
}
}
private void flushDecoder() throws ExoPlaybackException { private void flushDecoder() throws ExoPlaybackException {
waitingForKeys = false; waitingForKeys = false;
if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) { if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) {
...@@ -383,7 +414,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ...@@ -383,7 +414,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
@Override @Override
public boolean isEnded() { public boolean isEnded() {
return outputStreamEnded && !audioTrack.hasPendingData(); return outputStreamEnded && audioTrack.isEnded();
} }
@Override @Override
...@@ -513,15 +544,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ...@@ -513,15 +544,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
decoderReceivedBuffers = false; decoderReceivedBuffers = false;
} }
private boolean readFormat() throws ExoPlaybackException {
int result = readSource(formatHolder, null);
if (result == C.RESULT_FORMAT_READ) {
onInputFormatChanged(formatHolder.format);
return true;
}
return false;
}
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
Format oldFormat = inputFormat; Format oldFormat = inputFormat;
inputFormat = newFormat; inputFormat = newFormat;
......
...@@ -61,8 +61,7 @@ public class DecoderInputBuffer extends Buffer { ...@@ -61,8 +61,7 @@ public class DecoderInputBuffer extends Buffer {
*/ */
public long timeUs; public long timeUs;
@BufferReplacementMode @BufferReplacementMode private final int bufferReplacementMode;
private final int bufferReplacementMode;
/** /**
* @param bufferReplacementMode Determines the behavior of {@link #ensureSpaceForWrite(int)}. One * @param bufferReplacementMode Determines the behavior of {@link #ensureSpaceForWrite(int)}. One
......
...@@ -35,6 +35,7 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener; ...@@ -35,6 +35,7 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener;
import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
...@@ -103,6 +104,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe ...@@ -103,6 +104,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
public static final int MODE_RELEASE = 3; public static final int MODE_RELEASE = 3;
private static final String TAG = "OfflineDrmSessionMngr"; private static final String TAG = "OfflineDrmSessionMngr";
private static final String CENC_SCHEME_MIME_TYPE = "cenc";
private static final int MSG_PROVISION = 0; private static final int MSG_PROVISION = 0;
private static final int MSG_KEYS = 1; private static final int MSG_KEYS = 1;
...@@ -280,6 +282,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe ...@@ -280,6 +282,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
* required. * required.
* *
* <p>{@code mode} must be one of these: * <p>{@code mode} must be one of these:
* <ul>
* <li>{@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is * <li>{@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is
* requested otherwise the offline license is restored. * requested otherwise the offline license is restored.
* <li>{@link #MODE_QUERY}: {@code offlineLicenseKeySetId} can not be null. The offline license * <li>{@link #MODE_QUERY}: {@code offlineLicenseKeySetId} can not be null. The offline license
...@@ -288,6 +291,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe ...@@ -288,6 +291,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
* requested otherwise the offline license is renewed. * requested otherwise the offline license is renewed.
* <li>{@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline license * <li>{@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline license
* is released. * is released.
* </ul>
* *
* @param mode The mode to be set. * @param mode The mode to be set.
* @param offlineLicenseKeySetId The key set id of the license to be used with the given mode. * @param offlineLicenseKeySetId The key set id of the license to be used with the given mode.
...@@ -337,6 +341,12 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe ...@@ -337,6 +341,12 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
schemeInitData = psshData; schemeInitData = psshData;
} }
} }
if (Util.SDK_INT < 26 && C.CLEARKEY_UUID.equals(uuid)
&& (MimeTypes.VIDEO_MP4.equals(schemeMimeType)
|| MimeTypes.AUDIO_MP4.equals(schemeMimeType))) {
// Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4.
schemeMimeType = CENC_SCHEME_MIME_TYPE;
}
} }
state = STATE_OPENING; state = STATE_OPENING;
openInternal(true); openInternal(true);
......
...@@ -31,7 +31,7 @@ public interface DrmSession<T extends ExoMediaCrypto> { ...@@ -31,7 +31,7 @@ public interface DrmSession<T extends ExoMediaCrypto> {
/** Wraps the exception which is the cause of the error state. */ /** Wraps the exception which is the cause of the error state. */
class DrmSessionException extends Exception { class DrmSessionException extends Exception {
DrmSessionException(Exception e) { public DrmSessionException(Exception e) {
super(e); super(e);
} }
...@@ -70,8 +70,7 @@ public interface DrmSession<T extends ExoMediaCrypto> { ...@@ -70,8 +70,7 @@ public interface DrmSession<T extends ExoMediaCrypto> {
* @return One of {@link #STATE_ERROR}, {@link #STATE_CLOSED}, {@link #STATE_OPENING}, * @return One of {@link #STATE_ERROR}, {@link #STATE_CLOSED}, {@link #STATE_OPENING},
* {@link #STATE_OPENED} and {@link #STATE_OPENED_WITH_KEYS}. * {@link #STATE_OPENED} and {@link #STATE_OPENED_WITH_KEYS}.
*/ */
@State @State int getState();
int getState();
/** /**
* Returns a {@link ExoMediaCrypto} for the open session. * Returns a {@link ExoMediaCrypto} for the open session.
......
...@@ -24,6 +24,8 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest; ...@@ -24,6 +24,8 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSourceInputStream;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
...@@ -57,21 +59,62 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { ...@@ -57,21 +59,62 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
} }
/** /**
* @deprecated Use {@link HttpMediaDrmCallback#HttpMediaDrmCallback(String, Factory)}. Request
* properties can be set by calling {@link #setKeyRequestProperty(String, String)}.
* @param defaultUrl The default license URL. * @param defaultUrl The default license URL.
* @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances. * @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
* @param keyRequestProperties Request properties to set when making key requests, or null. * @param keyRequestProperties Request properties to set when making key requests, or null.
*/ */
@Deprecated
public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory, public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory,
Map<String, String> keyRequestProperties) { Map<String, String> keyRequestProperties) {
this.dataSourceFactory = dataSourceFactory; this.dataSourceFactory = dataSourceFactory;
this.defaultUrl = defaultUrl; this.defaultUrl = defaultUrl;
this.keyRequestProperties = keyRequestProperties; this.keyRequestProperties = new HashMap<>();
if (keyRequestProperties != null) {
this.keyRequestProperties.putAll(keyRequestProperties);
}
}
/**
* Sets a header for key requests made by the callback.
*
* @param name The name of the header field.
* @param value The value of the field.
*/
public void setKeyRequestProperty(String name, String value) {
Assertions.checkNotNull(name);
Assertions.checkNotNull(value);
synchronized (keyRequestProperties) {
keyRequestProperties.put(name, value);
}
}
/**
* Clears a header for key requests made by the callback.
*
* @param name The name of the header field.
*/
public void clearKeyRequestProperty(String name) {
Assertions.checkNotNull(name);
synchronized (keyRequestProperties) {
keyRequestProperties.remove(name);
}
}
/**
* Clears all headers for key requests made by the callback.
*/
public void clearAllKeyRequestProperties() {
synchronized (keyRequestProperties) {
keyRequestProperties.clear();
}
} }
@Override @Override
public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException { public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException {
String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData()); String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData());
return executePost(url, new byte[0], null); return executePost(dataSourceFactory, url, new byte[0], null);
} }
@Override @Override
...@@ -85,14 +128,14 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { ...@@ -85,14 +128,14 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
if (C.PLAYREADY_UUID.equals(uuid)) { if (C.PLAYREADY_UUID.equals(uuid)) {
requestProperties.putAll(PLAYREADY_KEY_REQUEST_PROPERTIES); requestProperties.putAll(PLAYREADY_KEY_REQUEST_PROPERTIES);
} }
if (keyRequestProperties != null) { synchronized (keyRequestProperties) {
requestProperties.putAll(keyRequestProperties); requestProperties.putAll(keyRequestProperties);
} }
return executePost(url, request.getData(), requestProperties); return executePost(dataSourceFactory, url, request.getData(), requestProperties);
} }
private byte[] executePost(String url, byte[] data, Map<String, String> requestProperties) private static byte[] executePost(HttpDataSource.Factory dataSourceFactory, String url,
throws IOException { byte[] data, Map<String, String> requestProperties) throws IOException {
HttpDataSource dataSource = dataSourceFactory.createDataSource(); HttpDataSource dataSource = dataSourceFactory.createDataSource();
if (requestProperties != null) { if (requestProperties != null) {
for (Map.Entry<String, String> requestProperty : requestProperties.entrySet()) { for (Map.Entry<String, String> requestProperty : requestProperties.entrySet()) {
......
...@@ -17,7 +17,6 @@ ...@@ -17,7 +17,6 @@
package com.google.android.exoplayer2.drm; package com.google.android.exoplayer2.drm;
import android.media.MediaDrm; import android.media.MediaDrm;
import android.net.Uri;
import android.os.ConditionVariable; import android.os.ConditionVariable;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
...@@ -27,24 +26,14 @@ import com.google.android.exoplayer2.Format; ...@@ -27,24 +26,14 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.EventListener; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.EventListener;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.Mode; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.Mode;
import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.source.dash.DashUtil;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper;
import com.google.android.exoplayer2.source.chunk.InitializationChunk;
import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
import com.google.android.exoplayer2.source.dash.manifest.Period; import com.google.android.exoplayer2.source.dash.manifest.Period;
import com.google.android.exoplayer2.source.dash.manifest.RangedUri;
import com.google.android.exoplayer2.source.dash.manifest.Representation; import com.google.android.exoplayer2.source.dash.manifest.Representation;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
...@@ -59,28 +48,6 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> { ...@@ -59,28 +48,6 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
private final HandlerThread handlerThread; private final HandlerThread handlerThread;
/** /**
* Helper method to download a DASH manifest.
*
* @param dataSource The {@link HttpDataSource} from which the manifest should be read.
* @param manifestUriString The URI of the manifest to be read.
* @return An instance of {@link DashManifest}.
* @throws IOException If an error occurs reading data from the stream.
* @see DashManifestParser
*/
public static DashManifest downloadManifest(HttpDataSource dataSource, String manifestUriString)
throws IOException {
DataSourceInputStream inputStream = new DataSourceInputStream(
dataSource, new DataSpec(Uri.parse(manifestUriString)));
try {
inputStream.open();
DashManifestParser parser = new DashManifestParser();
return parser.parse(dataSource.getUri(), inputStream);
} finally {
inputStream.close();
}
}
/**
* Instantiates a new instance which uses Widevine CDM. Call {@link #releaseResources()} when * Instantiates a new instance which uses Widevine CDM. Call {@link #releaseResources()} when
* you're done with the helper instance. * you're done with the helper instance.
* *
...@@ -93,7 +60,7 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> { ...@@ -93,7 +60,7 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
public static OfflineLicenseHelper<FrameworkMediaCrypto> newWidevineInstance( public static OfflineLicenseHelper<FrameworkMediaCrypto> newWidevineInstance(
String licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException { String licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException {
return newWidevineInstance( return newWidevineInstance(
new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory, null), null); new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory), null);
} }
/** /**
...@@ -174,7 +141,7 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> { ...@@ -174,7 +141,7 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
*/ */
public byte[] download(HttpDataSource dataSource, String manifestUriString) public byte[] download(HttpDataSource dataSource, String manifestUriString)
throws IOException, InterruptedException, DrmSessionException { throws IOException, InterruptedException, DrmSessionException {
return download(dataSource, downloadManifest(dataSource, manifestUriString)); return download(dataSource, DashUtil.loadManifest(dataSource, manifestUriString));
} }
/** /**
...@@ -210,11 +177,7 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> { ...@@ -210,11 +177,7 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
Representation representation = adaptationSet.representations.get(0); Representation representation = adaptationSet.representations.get(0);
DrmInitData drmInitData = representation.format.drmInitData; DrmInitData drmInitData = representation.format.drmInitData;
if (drmInitData == null) { if (drmInitData == null) {
InitializationChunk initializationChunk = loadInitializationChunk(dataSource, representation); Format sampleFormat = DashUtil.loadSampleFormat(dataSource, representation);
if (initializationChunk == null) {
return null;
}
Format sampleFormat = initializationChunk.getSampleFormat();
if (sampleFormat != null) { if (sampleFormat != null) {
drmInitData = sampleFormat.drmInitData; drmInitData = sampleFormat.drmInitData;
} }
...@@ -288,28 +251,4 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> { ...@@ -288,28 +251,4 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
return session; return session;
} }
private static InitializationChunk loadInitializationChunk(final DataSource dataSource,
final Representation representation) throws IOException, InterruptedException {
RangedUri rangedUri = representation.getInitializationUri();
if (rangedUri == null) {
return null;
}
DataSpec dataSpec = new DataSpec(rangedUri.resolveUri(representation.baseUrl), rangedUri.start,
rangedUri.length, representation.getCacheKey());
InitializationChunk initializationChunk = new InitializationChunk(dataSource, dataSpec,
representation.format, C.SELECTION_REASON_UNKNOWN, null /* trackSelectionData */,
newWrappedExtractor(representation.format));
initializationChunk.load();
return initializationChunk;
}
private static ChunkExtractorWrapper newWrappedExtractor(final Format format) {
final String mimeType = format.containerMimeType;
final boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM)
|| mimeType.startsWith(MimeTypes.AUDIO_WEBM);
final Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor();
return new ChunkExtractorWrapper(extractor, format, false /* preferManifestDrmInitData */,
false /* resendFormatOnInit */);
}
} }
...@@ -43,8 +43,7 @@ public final class UnsupportedDrmException extends Exception { ...@@ -43,8 +43,7 @@ public final class UnsupportedDrmException extends Exception {
/** /**
* Either {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}. * Either {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.
*/ */
@Reason @Reason public final int reason;
public final int reason;
/** /**
* @param reason {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}. * @param reason {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
...@@ -27,6 +28,8 @@ import java.util.Arrays; ...@@ -27,6 +28,8 @@ import java.util.Arrays;
*/ */
public final class DefaultExtractorInput implements ExtractorInput { public final class DefaultExtractorInput implements ExtractorInput {
private static final int PEEK_MIN_FREE_SPACE_AFTER_RESIZE = 64 * 1024;
private static final int PEEK_MAX_FREE_SPACE = 512 * 1024;
private static final byte[] SCRATCH_SPACE = new byte[4096]; private static final byte[] SCRATCH_SPACE = new byte[4096];
private final DataSource dataSource; private final DataSource dataSource;
...@@ -46,7 +49,7 @@ public final class DefaultExtractorInput implements ExtractorInput { ...@@ -46,7 +49,7 @@ public final class DefaultExtractorInput implements ExtractorInput {
this.dataSource = dataSource; this.dataSource = dataSource;
this.position = position; this.position = position;
this.streamLength = length; this.streamLength = length;
peekBuffer = new byte[8 * 1024]; peekBuffer = new byte[PEEK_MIN_FREE_SPACE_AFTER_RESIZE];
} }
@Override @Override
...@@ -176,7 +179,9 @@ public final class DefaultExtractorInput implements ExtractorInput { ...@@ -176,7 +179,9 @@ public final class DefaultExtractorInput implements ExtractorInput {
private void ensureSpaceForPeek(int length) { private void ensureSpaceForPeek(int length) {
int requiredLength = peekBufferPosition + length; int requiredLength = peekBufferPosition + length;
if (requiredLength > peekBuffer.length) { if (requiredLength > peekBuffer.length) {
peekBuffer = Arrays.copyOf(peekBuffer, Math.max(peekBuffer.length * 2, requiredLength)); int newPeekCapacity = Util.constrainValue(peekBuffer.length * 2,
requiredLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE, requiredLength + PEEK_MAX_FREE_SPACE);
peekBuffer = Arrays.copyOf(peekBuffer, newPeekCapacity);
} }
} }
...@@ -218,7 +223,12 @@ public final class DefaultExtractorInput implements ExtractorInput { ...@@ -218,7 +223,12 @@ public final class DefaultExtractorInput implements ExtractorInput {
private void updatePeekBuffer(int bytesConsumed) { private void updatePeekBuffer(int bytesConsumed) {
peekBufferLength -= bytesConsumed; peekBufferLength -= bytesConsumed;
peekBufferPosition = 0; peekBufferPosition = 0;
System.arraycopy(peekBuffer, bytesConsumed, peekBuffer, 0, peekBufferLength); byte[] newPeekBuffer = peekBuffer;
if (peekBufferLength < peekBuffer.length - PEEK_MAX_FREE_SPACE) {
newPeekBuffer = new byte[peekBufferLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE];
}
System.arraycopy(peekBuffer, bytesConsumed, newPeekBuffer, 0, peekBufferLength);
peekBuffer = newPeekBuffer;
} }
/** /**
......
...@@ -70,6 +70,8 @@ public final class DefaultTrackOutput implements TrackOutput { ...@@ -70,6 +70,8 @@ public final class DefaultTrackOutput implements TrackOutput {
private Format downstreamFormat; private Format downstreamFormat;
// Accessed only by the loading thread (or the consuming thread when there is no loading thread). // Accessed only by the loading thread (or the consuming thread when there is no loading thread).
private boolean pendingFormatAdjustment;
private Format lastUnadjustedFormat;
private long sampleOffsetUs; private long sampleOffsetUs;
private long totalBytesWritten; private long totalBytesWritten;
private Allocation lastAllocation; private Allocation lastAllocation;
...@@ -265,40 +267,42 @@ public final class DefaultTrackOutput implements TrackOutput { ...@@ -265,40 +267,42 @@ public final class DefaultTrackOutput implements TrackOutput {
* @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
* @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
* end of the stream. If the end of the stream has been reached, the * end of the stream. If the end of the stream has been reached, the
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. May be null if the * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.
* caller requires that the format of the stream be read even if it's not changing. * @param formatRequired Whether the caller requires that the format of the stream be read even if
* it's not changing. A sample will never be read if set to true, however it is still possible
* for the end of stream or nothing to be read.
* @param loadingFinished True if an empty queue should be considered the end of the stream. * @param loadingFinished True if an empty queue should be considered the end of the stream.
* @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will * @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will
* be set if the buffer's timestamp is less than this value. * be set if the buffer's timestamp is less than this value.
* @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
* {@link C#RESULT_BUFFER_READ}. * {@link C#RESULT_BUFFER_READ}.
*/ */
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean loadingFinished, public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired,
long decodeOnlyUntilUs) { boolean loadingFinished, long decodeOnlyUntilUs) {
switch (infoQueue.readData(formatHolder, buffer, downstreamFormat, extrasHolder)) { int result = infoQueue.readData(formatHolder, buffer, formatRequired, loadingFinished,
case C.RESULT_NOTHING_READ: downstreamFormat, extrasHolder);
if (loadingFinished) { switch (result) {
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
return C.RESULT_BUFFER_READ;
}
return C.RESULT_NOTHING_READ;
case C.RESULT_FORMAT_READ: case C.RESULT_FORMAT_READ:
downstreamFormat = formatHolder.format; downstreamFormat = formatHolder.format;
return C.RESULT_FORMAT_READ; return C.RESULT_FORMAT_READ;
case C.RESULT_BUFFER_READ: case C.RESULT_BUFFER_READ:
if (buffer.timeUs < decodeOnlyUntilUs) { if (!buffer.isEndOfStream()) {
buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); if (buffer.timeUs < decodeOnlyUntilUs) {
buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
}
// Read encryption data if the sample is encrypted.
if (buffer.isEncrypted()) {
readEncryptionData(buffer, extrasHolder);
}
// Write the sample data into the holder.
buffer.ensureSpaceForWrite(extrasHolder.size);
readData(extrasHolder.offset, buffer.data, extrasHolder.size);
// Advance the read head.
dropDownstreamTo(extrasHolder.nextOffset);
} }
// Read encryption data if the sample is encrypted.
if (buffer.isEncrypted()) {
readEncryptionData(buffer, extrasHolder);
}
// Write the sample data into the holder.
buffer.ensureSpaceForWrite(extrasHolder.size);
readData(extrasHolder.offset, buffer.data, extrasHolder.size);
// Advance the read head.
dropDownstreamTo(extrasHolder.nextOffset);
return C.RESULT_BUFFER_READ; return C.RESULT_BUFFER_READ;
case C.RESULT_NOTHING_READ:
return C.RESULT_NOTHING_READ;
default: default:
throw new IllegalStateException(); throw new IllegalStateException();
} }
...@@ -445,23 +449,24 @@ public final class DefaultTrackOutput implements TrackOutput { ...@@ -445,23 +449,24 @@ public final class DefaultTrackOutput implements TrackOutput {
} }
/** /**
* Like {@link #format(Format)}, but with an offset that will be added to the timestamps of * Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples
* samples subsequently queued to the buffer. The offset is also used to adjust * subsequently queued to the buffer.
* {@link Format#subsampleOffsetUs} for both the {@link Format} passed and those subsequently
* passed to {@link #format(Format)}.
* *
* @param format The format.
* @param sampleOffsetUs The timestamp offset in microseconds. * @param sampleOffsetUs The timestamp offset in microseconds.
*/ */
public void formatWithOffset(Format format, long sampleOffsetUs) { public void setSampleOffsetUs(long sampleOffsetUs) {
this.sampleOffsetUs = sampleOffsetUs; if (this.sampleOffsetUs != sampleOffsetUs) {
format(format); this.sampleOffsetUs = sampleOffsetUs;
pendingFormatAdjustment = true;
}
} }
@Override @Override
public void format(Format format) { public void format(Format format) {
Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs); Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs);
boolean formatChanged = infoQueue.format(adjustedFormat); boolean formatChanged = infoQueue.format(adjustedFormat);
lastUnadjustedFormat = format;
pendingFormatAdjustment = false;
if (upstreamFormatChangeListener != null && formatChanged) { if (upstreamFormatChangeListener != null && formatChanged) {
upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat); upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat);
} }
...@@ -518,6 +523,9 @@ public final class DefaultTrackOutput implements TrackOutput { ...@@ -518,6 +523,9 @@ public final class DefaultTrackOutput implements TrackOutput {
@Override @Override
public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset, public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
byte[] encryptionKey) { byte[] encryptionKey) {
if (pendingFormatAdjustment) {
format(lastUnadjustedFormat);
}
if (!startWriteOperation()) { if (!startWriteOperation()) {
infoQueue.commitSampleTimestamp(timeUs); infoQueue.commitSampleTimestamp(timeUs);
return; return;
...@@ -754,23 +762,34 @@ public final class DefaultTrackOutput implements TrackOutput { ...@@ -754,23 +762,34 @@ public final class DefaultTrackOutput implements TrackOutput {
* and the absolute position of the first byte that may still be required after the current * and the absolute position of the first byte that may still be required after the current
* sample has been read. May be null if the caller requires that the format of the stream be * sample has been read. May be null if the caller requires that the format of the stream be
* read even if it's not changing. * read even if it's not changing.
* @param formatRequired Whether the caller requires that the format of the stream be read even
* if it's not changing. A sample will never be read if set to true, however it is still
* possible for the end of stream or nothing to be read.
* @param loadingFinished True if an empty queue should be considered the end of the stream.
* @param downstreamFormat The current downstream {@link Format}. If the format of the next * @param downstreamFormat The current downstream {@link Format}. If the format of the next
* sample is different to the current downstream format then a format will be read. * sample is different to the current downstream format then a format will be read.
* @param extrasHolder The holder into which extra sample information should be written. * @param extrasHolder The holder into which extra sample information should be written.
* @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ}
* or {@link C#RESULT_BUFFER_READ}. * or {@link C#RESULT_BUFFER_READ}.
*/ */
@SuppressWarnings("ReferenceEquality")
public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
Format downstreamFormat, BufferExtrasHolder extrasHolder) { boolean formatRequired, boolean loadingFinished, Format downstreamFormat,
BufferExtrasHolder extrasHolder) {
if (queueSize == 0) { if (queueSize == 0) {
if (upstreamFormat != null && (buffer == null || upstreamFormat != downstreamFormat)) { if (loadingFinished) {
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
return C.RESULT_BUFFER_READ;
} else if (upstreamFormat != null
&& (formatRequired || upstreamFormat != downstreamFormat)) {
formatHolder.format = upstreamFormat; formatHolder.format = upstreamFormat;
return C.RESULT_FORMAT_READ; return C.RESULT_FORMAT_READ;
} else {
return C.RESULT_NOTHING_READ;
} }
return C.RESULT_NOTHING_READ;
} }
if (buffer == null || formats[relativeReadIndex] != downstreamFormat) { if (formatRequired || formats[relativeReadIndex] != downstreamFormat) {
formatHolder.format = formats[relativeReadIndex]; formatHolder.format = formats[relativeReadIndex];
return C.RESULT_FORMAT_READ; return C.RESULT_FORMAT_READ;
} }
......
...@@ -102,4 +102,5 @@ public interface Extractor { ...@@ -102,4 +102,5 @@ public interface Extractor {
* Releases all kept resources. * Releases all kept resources.
*/ */
void release(); void release();
} }
...@@ -23,17 +23,18 @@ public interface ExtractorOutput { ...@@ -23,17 +23,18 @@ public interface ExtractorOutput {
/** /**
* Called by the {@link Extractor} to get the {@link TrackOutput} for a specific track. * Called by the {@link Extractor} to get the {@link TrackOutput} for a specific track.
* <p> * <p>
* The same {@link TrackOutput} is returned if multiple calls are made with the same * The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}.
* {@code trackId}.
* *
* @param trackId A track identifier. * @param id A track identifier.
* @param type The type of the track. Typically one of the {@link com.google.android.exoplayer2.C}
* {@code TRACK_TYPE_*} constants.
* @return The {@link TrackOutput} for the given track identifier. * @return The {@link TrackOutput} for the given track identifier.
*/ */
TrackOutput track(int trackId); TrackOutput track(int id, int type);
/** /**
* Called when all tracks have been identified, meaning no new {@code trackId} values will be * Called when all tracks have been identified, meaning no new {@code trackId} values will be
* passed to {@link #track(int)}. * passed to {@link #track(int, int)}.
*/ */
void endTracks(); void endTracks();
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.id3.CommentFrame;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
...@@ -26,6 +27,18 @@ import java.util.regex.Pattern; ...@@ -26,6 +27,18 @@ import java.util.regex.Pattern;
*/ */
public final class GaplessInfoHolder { public final class GaplessInfoHolder {
/**
* A {@link FramePredicate} suitable for use when decoding {@link Metadata} that will be passed
* to {@link #setFromMetadata(Metadata)}. Only frames that might contain gapless playback
* information are decoded.
*/
public static final FramePredicate GAPLESS_INFO_ID3_FRAME_PREDICATE = new FramePredicate() {
@Override
public boolean evaluate(int majorVersion, int id0, int id1, int id2, int id3) {
return id0 == 'C' && id1 == 'O' && id2 == 'M' && (id3 == 'M' || majorVersion == 2);
}
};
private static final String GAPLESS_COMMENT_ID = "iTunSMPB"; private static final String GAPLESS_COMMENT_ID = "iTunSMPB";
private static final Pattern GAPLESS_COMMENT_PATTERN = private static final Pattern GAPLESS_COMMENT_PATTERN =
Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})"); Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})");
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.extractor.flv; package com.google.android.exoplayer2.extractor.flv;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
...@@ -183,10 +184,12 @@ public final class FlvExtractor implements Extractor, SeekMap { ...@@ -183,10 +184,12 @@ public final class FlvExtractor implements Extractor, SeekMap {
boolean hasAudio = (flags & 0x04) != 0; boolean hasAudio = (flags & 0x04) != 0;
boolean hasVideo = (flags & 0x01) != 0; boolean hasVideo = (flags & 0x01) != 0;
if (hasAudio && audioReader == null) { if (hasAudio && audioReader == null) {
audioReader = new AudioTagPayloadReader(extractorOutput.track(TAG_TYPE_AUDIO)); audioReader = new AudioTagPayloadReader(
extractorOutput.track(TAG_TYPE_AUDIO, C.TRACK_TYPE_AUDIO));
} }
if (hasVideo && videoReader == null) { if (hasVideo && videoReader == null) {
videoReader = new VideoTagPayloadReader(extractorOutput.track(TAG_TYPE_VIDEO)); videoReader = new VideoTagPayloadReader(
extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO));
} }
if (metadataReader == null) { if (metadataReader == null) {
metadataReader = new ScriptTagPayloadReader(null); metadataReader = new ScriptTagPayloadReader(null);
......
...@@ -673,6 +673,9 @@ public final class MatroskaExtractor implements Extractor { ...@@ -673,6 +673,9 @@ public final class MatroskaExtractor implements Extractor {
case 3: case 3:
currentTrack.stereoMode = C.STEREO_MODE_TOP_BOTTOM; currentTrack.stereoMode = C.STEREO_MODE_TOP_BOTTOM;
break; break;
case 15:
currentTrack.stereoMode = C.STEREO_MODE_STEREO_MESH;
break;
default: default:
break; break;
} }
...@@ -1462,6 +1465,7 @@ public final class MatroskaExtractor implements Extractor { ...@@ -1462,6 +1465,7 @@ public final class MatroskaExtractor implements Extractor {
throw new ParserException("Unrecognized codec identifier."); throw new ParserException("Unrecognized codec identifier.");
} }
int type;
Format format; Format format;
@C.SelectionFlags int selectionFlags = 0; @C.SelectionFlags int selectionFlags = 0;
selectionFlags |= flagDefault ? C.SELECTION_FLAG_DEFAULT : 0; selectionFlags |= flagDefault ? C.SELECTION_FLAG_DEFAULT : 0;
...@@ -1469,10 +1473,12 @@ public final class MatroskaExtractor implements Extractor { ...@@ -1469,10 +1473,12 @@ public final class MatroskaExtractor implements Extractor {
// TODO: Consider reading the name elements of the tracks and, if present, incorporating them // TODO: Consider reading the name elements of the tracks and, if present, incorporating them
// into the trackId passed when creating the formats. // into the trackId passed when creating the formats.
if (MimeTypes.isAudio(mimeType)) { if (MimeTypes.isAudio(mimeType)) {
type = C.TRACK_TYPE_AUDIO;
format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
Format.NO_VALUE, maxInputSize, channelCount, sampleRate, pcmEncoding, Format.NO_VALUE, maxInputSize, channelCount, sampleRate, pcmEncoding,
initializationData, drmInitData, selectionFlags, language); initializationData, drmInitData, selectionFlags, language);
} else if (MimeTypes.isVideo(mimeType)) { } else if (MimeTypes.isVideo(mimeType)) {
type = C.TRACK_TYPE_VIDEO;
if (displayUnit == Track.DISPLAY_UNIT_PIXELS) { if (displayUnit == Track.DISPLAY_UNIT_PIXELS) {
displayWidth = displayWidth == Format.NO_VALUE ? width : displayWidth; displayWidth = displayWidth == Format.NO_VALUE ? width : displayWidth;
displayHeight = displayHeight == Format.NO_VALUE ? height : displayHeight; displayHeight = displayHeight == Format.NO_VALUE ? height : displayHeight;
...@@ -1485,17 +1491,19 @@ public final class MatroskaExtractor implements Extractor { ...@@ -1485,17 +1491,19 @@ public final class MatroskaExtractor implements Extractor {
Format.NO_VALUE, maxInputSize, width, height, Format.NO_VALUE, initializationData, Format.NO_VALUE, maxInputSize, width, height, Format.NO_VALUE, initializationData,
Format.NO_VALUE, pixelWidthHeightRatio, projectionData, stereoMode, drmInitData); Format.NO_VALUE, pixelWidthHeightRatio, projectionData, stereoMode, drmInitData);
} else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) { } else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) {
type = C.TRACK_TYPE_TEXT;
format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null, format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null,
Format.NO_VALUE, selectionFlags, language, drmInitData); Format.NO_VALUE, selectionFlags, language, drmInitData);
} else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType) } else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType)
|| MimeTypes.APPLICATION_PGS.equals(mimeType)) { || MimeTypes.APPLICATION_PGS.equals(mimeType)) {
type = C.TRACK_TYPE_TEXT;
format = Format.createImageSampleFormat(Integer.toString(trackId), mimeType, null, format = Format.createImageSampleFormat(Integer.toString(trackId), mimeType, null,
Format.NO_VALUE, initializationData, language, drmInitData); Format.NO_VALUE, initializationData, language, drmInitData);
} else { } else {
throw new ParserException("Unexpected MIME type."); throw new ParserException("Unexpected MIME type.");
} }
this.output = output.track(number); this.output = output.track(number, type);
this.output.format(format); this.output.format(format);
} }
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.extractor.mp3; package com.google.android.exoplayer2.extractor.mp3;
import android.support.annotation.IntDef;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
...@@ -33,6 +34,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray; ...@@ -33,6 +34,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/** /**
* Extracts data from an MP3 file. * Extracts data from an MP3 file.
...@@ -52,6 +55,23 @@ public final class Mp3Extractor implements Extractor { ...@@ -52,6 +55,23 @@ public final class Mp3Extractor implements Extractor {
}; };
/** /**
* Flags controlling the behavior of the extractor.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, FLAG_DISABLE_ID3_METADATA})
public @interface Flags {}
/**
* Flag to force enable seeking using a constant bitrate assumption in cases where seeking would
* otherwise not be possible.
*/
public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1;
/**
* Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not
* required.
*/
public static final int FLAG_DISABLE_ID3_METADATA = 2;
/**
* The maximum number of bytes to search when synchronizing, before giving up. * The maximum number of bytes to search when synchronizing, before giving up.
*/ */
private static final int MAX_SYNC_BYTES = 128 * 1024; private static final int MAX_SYNC_BYTES = 128 * 1024;
...@@ -72,6 +92,7 @@ public final class Mp3Extractor implements Extractor { ...@@ -72,6 +92,7 @@ public final class Mp3Extractor implements Extractor {
private static final int INFO_HEADER = Util.getIntegerCodeForString("Info"); private static final int INFO_HEADER = Util.getIntegerCodeForString("Info");
private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI"); private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI");
@Flags private final int flags;
private final long forcedFirstSampleTimestampUs; private final long forcedFirstSampleTimestampUs;
private final ParsableByteArray scratch; private final ParsableByteArray scratch;
private final MpegAudioHeader synchronizedHeader; private final MpegAudioHeader synchronizedHeader;
...@@ -93,16 +114,27 @@ public final class Mp3Extractor implements Extractor { ...@@ -93,16 +114,27 @@ public final class Mp3Extractor implements Extractor {
* Constructs a new {@link Mp3Extractor}. * Constructs a new {@link Mp3Extractor}.
*/ */
public Mp3Extractor() { public Mp3Extractor() {
this(C.TIME_UNSET); this(0);
} }
/** /**
* Constructs a new {@link Mp3Extractor}. * Constructs a new {@link Mp3Extractor}.
* *
* @param flags Flags that control the extractor's behavior.
*/
public Mp3Extractor(@Flags int flags) {
this(flags, C.TIME_UNSET);
}
/**
* Constructs a new {@link Mp3Extractor}.
*
* @param flags Flags that control the extractor's behavior.
* @param forcedFirstSampleTimestampUs A timestamp to force for the first sample, or * @param forcedFirstSampleTimestampUs A timestamp to force for the first sample, or
* {@link C#TIME_UNSET} if forcing is not required. * {@link C#TIME_UNSET} if forcing is not required.
*/ */
public Mp3Extractor(long forcedFirstSampleTimestampUs) { public Mp3Extractor(@Flags int flags, long forcedFirstSampleTimestampUs) {
this.flags = flags;
this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
scratch = new ParsableByteArray(SCRATCH_LENGTH); scratch = new ParsableByteArray(SCRATCH_LENGTH);
synchronizedHeader = new MpegAudioHeader(); synchronizedHeader = new MpegAudioHeader();
...@@ -118,7 +150,7 @@ public final class Mp3Extractor implements Extractor { ...@@ -118,7 +150,7 @@ public final class Mp3Extractor implements Extractor {
@Override @Override
public void init(ExtractorOutput output) { public void init(ExtractorOutput output) {
extractorOutput = output; extractorOutput = output;
trackOutput = extractorOutput.track(0); trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO);
extractorOutput.endTracks(); extractorOutput.endTracks();
} }
...@@ -151,7 +183,8 @@ public final class Mp3Extractor implements Extractor { ...@@ -151,7 +183,8 @@ public final class Mp3Extractor implements Extractor {
trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null, trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null,
Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels, Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels,
synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay, synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay,
gaplessInfoHolder.encoderPadding, null, null, 0, null, metadata)); gaplessInfoHolder.encoderPadding, null, null, 0, null,
(flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata));
} }
return readSample(input); return readSample(input);
} }
...@@ -284,7 +317,11 @@ public final class Mp3Extractor implements Extractor { ...@@ -284,7 +317,11 @@ public final class Mp3Extractor implements Extractor {
byte[] id3Data = new byte[tagLength]; byte[] id3Data = new byte[tagLength];
System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH); System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH);
input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength); input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength);
metadata = new Id3Decoder().decode(id3Data, tagLength); // We need to parse enough ID3 metadata to retrieve any gapless playback information even
// if ID3 metadata parsing is disabled.
Id3Decoder.FramePredicate id3FramePredicate = (flags & FLAG_DISABLE_ID3_METADATA) != 0
? GaplessInfoHolder.GAPLESS_INFO_ID3_FRAME_PREDICATE : null;
metadata = new Id3Decoder(id3FramePredicate).decode(id3Data, tagLength);
if (metadata != null) { if (metadata != null) {
gaplessInfoHolder.setFromMetadata(metadata); gaplessInfoHolder.setFromMetadata(metadata);
} }
...@@ -350,7 +387,8 @@ public final class Mp3Extractor implements Extractor { ...@@ -350,7 +387,8 @@ public final class Mp3Extractor implements Extractor {
} }
} }
if (seeker == null) { if (seeker == null || (!seeker.isSeekable()
&& (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) {
// Repopulate the synchronized header in case we had to skip an invalid seeking header, which // Repopulate the synchronized header in case we had to skip an invalid seeking header, which
// would give an invalid CBR bitrate. // would give an invalid CBR bitrate.
input.resetPeekPosition(); input.resetPeekPosition();
......
...@@ -332,6 +332,9 @@ import java.util.List; ...@@ -332,6 +332,9 @@ import java.util.List;
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags); return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
} }
// Omit any sample at the end point of an edit for audio tracks.
boolean omitClippedSample = track.type == C.TRACK_TYPE_AUDIO;
// Count the number of samples after applying edits. // Count the number of samples after applying edits.
int editedSampleCount = 0; int editedSampleCount = 0;
int nextSampleIndex = 0; int nextSampleIndex = 0;
...@@ -342,7 +345,8 @@ import java.util.List; ...@@ -342,7 +345,8 @@ import java.util.List;
long duration = Util.scaleLargeTimestamp(track.editListDurations[i], track.timescale, long duration = Util.scaleLargeTimestamp(track.editListDurations[i], track.timescale,
track.movieTimescale); track.movieTimescale);
int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true); int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true);
int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, true, false); int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, omitClippedSample,
false);
editedSampleCount += endIndex - startIndex; editedSampleCount += endIndex - startIndex;
copyMetadata |= nextSampleIndex != startIndex; copyMetadata |= nextSampleIndex != startIndex;
nextSampleIndex = endIndex; nextSampleIndex = endIndex;
...@@ -365,7 +369,7 @@ import java.util.List; ...@@ -365,7 +369,7 @@ import java.util.List;
long endMediaTime = mediaTime + Util.scaleLargeTimestamp(duration, track.timescale, long endMediaTime = mediaTime + Util.scaleLargeTimestamp(duration, track.timescale,
track.movieTimescale); track.movieTimescale);
int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true); int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true);
int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, true, false); int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, omitClippedSample, false);
if (copyMetadata) { if (copyMetadata) {
int count = endIndex - startIndex; int count = endIndex - startIndex;
System.arraycopy(offsets, startIndex, editedOffsets, sampleIndex, count); System.arraycopy(offsets, startIndex, editedOffsets, sampleIndex, count);
...@@ -716,6 +720,9 @@ import java.util.List; ...@@ -716,6 +720,9 @@ import java.util.List;
case 2: case 2:
stereoMode = C.STEREO_MODE_LEFT_RIGHT; stereoMode = C.STEREO_MODE_LEFT_RIGHT;
break; break;
case 3:
stereoMode = C.STEREO_MODE_STEREO_MESH;
break;
default: default:
break; break;
} }
......
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