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 #
### 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 ###
* 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,
however it can be assumed that all such changes are included in the most recent
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 ###
* Fixed cache failures when using an encrypted cache content index.
......
......@@ -11,16 +11,13 @@
// 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.
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.1'
classpath 'com.novoda:bintray-release:0.3.4'
classpath 'com.android.tools.build:gradle:2.3.0'
classpath 'com.novoda:bintray-release:0.4.0'
}
}
......@@ -29,13 +26,24 @@ allprojects {
jcenter()
}
project.ext {
compileSdkVersion=24
targetSdkVersion=24
buildToolsVersion='23.0.3'
releaseRepoName = 'exoplayer'
// Important: ExoPlayer specifies a minSdkVersion of 9 because various
// components provided by the library may be of use on older devices.
// However, please note that the core media playback functionality
// provided by the library requires API level 16 or greater.
minSdkVersion=9
compileSdkVersion=25
targetSdkVersion=25
buildToolsVersion='25'
releaseRepoName = getBintrayRepo()
releaseUserOrg = 'google'
releaseGroupId = 'com.google.android.exoplayer'
releaseVersion = 'r2.2.0'
releaseVersion = 'r2.3.0'
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 {
}
}
lintOptions {
// The demo app does not have translations.
disable 'MissingTranslation'
}
productFlavors {
noExtensions
withExtensions
......
......@@ -16,8 +16,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.demo"
android:versionCode="2200"
android:versionName="2.2.0">
android:versionCode="2300"
android:versionName="2.3.0">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
......
......@@ -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",
"samples": [
{
......
......@@ -101,9 +101,6 @@ import java.util.Locale;
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
if (timeline == null) {
return;
}
int periodCount = timeline.getPeriodCount();
int windowCount = timeline.getWindowCount();
Log.d(TAG, "sourceInfo [periodCount=" + periodCount + ", windowCount=" + windowCount);
......
......@@ -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.smoothstreaming.DefaultSsChunkSource;
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.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.trackselection.TrackSelection;
......@@ -70,8 +70,6 @@ import com.google.android.exoplayer2.util.Util;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
......@@ -112,7 +110,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
private DefaultTrackSelector trackSelector;
private TrackSelectionHelper trackSelectionHelper;
private DebugTextViewHelper debugViewHelper;
private boolean playerNeedsSource;
private boolean needRetrySource;
private boolean shouldAutoPlay;
private int resumeWindow;
......@@ -231,7 +229,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
private void initializePlayer() {
Intent intent = getIntent();
if (player == null) {
boolean needNewPlayer = player == null;
if (needNewPlayer) {
boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false);
UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA)
? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null;
......@@ -239,19 +238,9 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
if (drmSchemeUuid != null) {
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL);
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 {
drmSessionManager = buildDrmSessionManager(drmSchemeUuid, drmLicenseUrl,
keyRequestProperties);
keyRequestPropertiesArray);
} catch (UnsupportedDrmException e) {
int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported
: (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
......@@ -267,7 +256,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_ON)
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF;
TrackSelection.Factory videoTrackSelectionFactory =
new AdaptiveVideoTrackSelection.Factory(BANDWIDTH_METER);
new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
trackSelectionHelper = new TrackSelectionHelper(trackSelector, videoTrackSelectionFactory);
player = ExoPlayerFactory.newSimpleInstance(this, trackSelector, new DefaultLoadControl(),
......@@ -284,9 +273,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
player.setPlayWhenReady(shouldAutoPlay);
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
debugViewHelper.start();
playerNeedsSource = true;
}
if (playerNeedsSource) {
if (needNewPlayer || needRetrySource) {
String action = intent.getAction();
Uri[] uris;
String[] extensions;
......@@ -322,14 +310,14 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
player.seekTo(resumeWindow, resumePosition);
}
player.prepare(mediaSource, !haveResumePosition, false);
playerNeedsSource = false;
needRetrySource = false;
updateButtonVisibilities();
}
}
private MediaSource buildMediaSource(Uri uri, String overrideExtension) {
int type = Util.inferContentType(!TextUtils.isEmpty(overrideExtension) ? "." + overrideExtension
: uri.getLastPathSegment());
int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri)
: Util.inferContentType("." + overrideExtension);
switch (type) {
case C.TYPE_SS:
return new SsMediaSource(uri, buildDataSourceFactory(false),
......@@ -349,12 +337,18 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
}
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) {
return null;
}
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,
FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger);
}
......@@ -425,7 +419,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
@Override
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
// resume position so that if the user then retries, playback will resume from the position to
// which they seeked.
......@@ -466,7 +460,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
if (errorString != null) {
showToast(errorString);
}
playerNeedsSource = true;
needRetrySource = true;
if (isBehindLiveWindow(e)) {
clearResumePosition();
initializePlayer();
......@@ -498,7 +492,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
private void updateButtonVisibilities() {
debugRootView.removeAllViews();
retryButton.setVisibility(playerNeedsSource ? View.VISIBLE : View.GONE);
retryButton.setVisibility(needRetrySource ? View.VISIBLE : View.GONE);
debugRootView.addView(retryButton);
if (player == null) {
......
......@@ -262,11 +262,13 @@ public class SampleChooserActivity extends Activity {
}
private UUID getDrmUuid(String typeString) throws ParserException {
switch (typeString.toLowerCase()) {
switch (Util.toLowerInvariant(typeString)) {
case "widevine":
return C.WIDEVINE_UUID;
case "playready":
return C.PLAYREADY_UUID;
case "cenc":
return C.CLEARKEY_UUID;
default:
try {
return UUID.fromString(typeString);
......
......@@ -51,7 +51,7 @@ import java.util.Locale;
private static final TrackSelection.Factory RANDOM_FACTORY = new RandomTrackSelection.Factory();
private final MappingTrackSelector selector;
private final TrackSelection.Factory adaptiveVideoTrackSelectionFactory;
private final TrackSelection.Factory adaptiveTrackSelectionFactory;
private MappedTrackInfo trackInfo;
private int rendererIndex;
......@@ -67,13 +67,13 @@ import java.util.Locale;
/**
* @param selector The track selector.
* @param adaptiveVideoTrackSelectionFactory A factory for adaptive video {@link TrackSelection}s,
* or null if the selection helper should not support adaptive video.
* @param adaptiveTrackSelectionFactory A factory for adaptive {@link TrackSelection}s, or null
* if the selection helper should not support adaptive tracks.
*/
public TrackSelectionHelper(MappingTrackSelector selector,
TrackSelection.Factory adaptiveVideoTrackSelectionFactory) {
TrackSelection.Factory adaptiveTrackSelectionFactory) {
this.selector = selector;
this.adaptiveVideoTrackSelectionFactory = adaptiveVideoTrackSelectionFactory;
this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory;
}
/**
......@@ -92,7 +92,7 @@ import java.util.Locale;
trackGroups = trackInfo.getTrackGroups(rendererIndex);
trackGroupsAdaptive = new boolean[trackGroups.length];
for (int i = 0; i < trackGroups.length; i++) {
trackGroupsAdaptive[i] = adaptiveVideoTrackSelectionFactory != null
trackGroupsAdaptive[i] = adaptiveTrackSelectionFactory != null
&& trackInfo.getAdaptiveSupport(rendererIndex, i, false)
!= RendererCapabilities.ADAPTIVE_NOT_SUPPORTED
&& trackGroups.get(i).length > 1;
......@@ -271,7 +271,7 @@ import java.util.Locale;
private void setOverride(int group, int[] tracks, boolean enableRandomAdaptation) {
TrackSelection.Factory factory = tracks.length == 1 ? FIXED_FACTORY
: (enableRandomAdaptation ? RANDOM_FACTORY : adaptiveVideoTrackSelectionFactory);
: (enableRandomAdaptation ? RANDOM_FACTORY : adaptiveTrackSelectionFactory);
override = new SelectionOverride(factory, group, tracks);
}
......@@ -301,15 +301,18 @@ import java.util.Locale;
private static String buildTrackName(Format format) {
String trackName;
if (MimeTypes.isVideo(format.sampleMimeType)) {
trackName = joinWithSeparator(joinWithSeparator(buildResolutionString(format),
buildBitrateString(format)), buildTrackIdString(format));
trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(
buildResolutionString(format), buildBitrateString(format)), buildTrackIdString(format)),
buildSampleMimeTypeString(format));
} else if (MimeTypes.isAudio(format.sampleMimeType)) {
trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format),
buildAudioPropertyString(format)), buildBitrateString(format)),
buildTrackIdString(format));
trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(joinWithSeparator(
buildLanguageString(format), buildAudioPropertyString(format)),
buildBitrateString(format)), buildTrackIdString(format)),
buildSampleMimeTypeString(format));
} else {
trackName = joinWithSeparator(joinWithSeparator(buildLanguageString(format),
buildBitrateString(format)), buildTrackIdString(format));
trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format),
buildBitrateString(format)), buildTrackIdString(format)),
buildSampleMimeTypeString(format));
}
return trackName.length() == 0 ? "unknown" : trackName;
}
......@@ -342,4 +345,8 @@ import java.util.Locale;
return format.id == null ? "" : ("id:" + format.id);
}
private static String buildSampleMimeTypeString(Format format) {
return format.sampleMimeType == null ? "" : format.sampleMimeType;
}
}
......@@ -18,7 +18,7 @@ android {
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion 9
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
......
......@@ -118,7 +118,8 @@ public final class CronetDataSourceTest {
TEST_CONNECT_TIMEOUT_MS,
TEST_READ_TIMEOUT_MS,
true, // resetTimeoutOnRedirects
mockClock));
mockClock,
null));
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);
when(mockCronetEngine.newUrlRequestBuilder(
anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
......
......@@ -32,7 +32,6 @@ import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
......@@ -98,7 +97,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
private final int connectTimeoutMs;
private final int readTimeoutMs;
private final boolean resetTimeoutOnRedirects;
private final Map<String, String> requestProperties;
private final RequestProperties defaultRequestProperties;
private final RequestProperties requestProperties;
private final ConditionVariable operation;
private final Clock clock;
......@@ -136,7 +136,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
public CronetDataSource(CronetEngine cronetEngine, Executor executor,
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener) {
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
* @param connectTimeoutMs The connection timeout, in milliseconds.
* @param readTimeoutMs The read timeout, in milliseconds.
* @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,
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,
readTimeoutMs, resetTimeoutOnRedirects, new SystemClock());
readTimeoutMs, resetTimeoutOnRedirects, new SystemClock(), defaultRequestProperties);
}
/* package */ CronetDataSource(CronetEngine cronetEngine, Executor executor,
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.executor = Assertions.checkNotNull(executor);
this.contentTypePredicate = contentTypePredicate;
......@@ -168,7 +171,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
this.readTimeoutMs = readTimeoutMs;
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
this.clock = Assertions.checkNotNull(clock);
requestProperties = new HashMap<>();
this.defaultRequestProperties = defaultRequestProperties;
requestProperties = new RequestProperties();
operation = new ConditionVariable();
}
......@@ -176,23 +180,17 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
@Override
public void setRequestProperty(String name, String value) {
synchronized (requestProperties) {
requestProperties.put(name, value);
}
requestProperties.set(name, value);
}
@Override
public void clearRequestProperty(String name) {
synchronized (requestProperties) {
requestProperties.remove(name);
}
requestProperties.remove(name);
}
@Override
public void clearAllRequestProperties() {
synchronized (requestProperties) {
requestProperties.clear();
}
requestProperties.clear();
}
@Override
......@@ -421,16 +419,24 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(dataSpec.uri.toString(),
this, executor);
// Set the headers.
synchronized (requestProperties) {
if (dataSpec.postBody != null && dataSpec.postBody.length != 0
&& !requestProperties.containsKey(CONTENT_TYPE)) {
throw new OpenException("POST request with non-empty body must set Content-Type", dataSpec,
Status.IDLE);
}
for (Entry<String, String> headerEntry : requestProperties.entrySet()) {
requestBuilder.addHeader(headerEntry.getKey(), headerEntry.getValue());
boolean isContentTypeHeaderSet = false;
if (defaultRequestProperties != null) {
for (Entry<String, String> headerEntry : defaultRequestProperties.getSnapshot().entrySet()) {
String key = headerEntry.getKey();
isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key);
requestBuilder.addHeader(key, 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.
if (currentDataSpec.position != 0 || currentDataSpec.length != C.LENGTH_UNSET) {
StringBuilder rangeValue = new StringBuilder();
......
......@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.cronet;
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.Factory;
import com.google.android.exoplayer2.upstream.TransferListener;
......@@ -68,9 +69,10 @@ public final class CronetDataSourceFactory extends BaseFactory {
}
@Override
protected CronetDataSource createDataSourceInternal() {
protected CronetDataSource createDataSourceInternal(HttpDataSource.RequestProperties
defaultRequestProperties) {
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"
NDK_PATH="<path to Android NDK>"
```
* Fetch and build FFmpeg.
For example, to fetch and build for armv7a:
* Fetch and build FFmpeg. For example, to fetch and build for armv7a:
```
cd "${FFMPEG_EXT_PATH}/jni" && \
......@@ -69,15 +67,14 @@ make -j4 && \
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 && \
${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
like this:
......
......@@ -18,7 +18,7 @@ android {
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion 9
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
}
......
......@@ -19,7 +19,7 @@ import android.os.Handler;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
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.SimpleDecoderAudioRenderer;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
......@@ -43,21 +43,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
*/
public 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.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioCapabilities audioCapabilities) {
super(eventHandler, eventListener, audioCapabilities);
AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, audioProcessors);
}
@Override
......
......@@ -18,7 +18,7 @@ android {
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion 9
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
}
......
......@@ -5,7 +5,10 @@
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.util.FlacStreamInfo {
*;
}
......@@ -67,7 +67,7 @@ public final class FlacExtractor implements Extractor {
@Override
public void init(ExtractorOutput output) {
extractorOutput = output;
trackOutput = extractorOutput.track(0);
trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO);
extractorOutput.endTracks();
try {
decoderJni = new FlacDecoderJni();
......
......@@ -17,7 +17,7 @@ package com.google.android.exoplayer2.ext.flac;
import android.os.Handler;
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.SimpleDecoderAudioRenderer;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
......@@ -38,21 +38,11 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
*/
public 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.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioCapabilities audioCapabilities) {
super(eventHandler, eventListener, audioCapabilities);
AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, audioProcessors);
}
@Override
......
......@@ -31,7 +31,7 @@ LOCAL_C_INCLUDES := \
LOCAL_SRC_FILES := $(FLAC_SOURCES)
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_LDLIBS := -llog -lz -lm
......
......@@ -453,7 +453,8 @@ int64_t FLACParser::getSeekPosition(int64_t timeUs) {
}
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) {
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 @@
// See the License for the specific language governing permissions and
// limitations under the License.
apply plugin: 'com.android.library'
apply plugin: 'bintray-release'
android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion 9
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
}
lintOptions {
// See: https://github.com/square/okio/issues/58
warning 'InvalidPackage'
}
}
dependencies {
compile project(':library')
compile('com.squareup.okhttp3:okhttp:3.4.1') {
compile('com.squareup.okhttp3:okhttp:3.6.0') {
exclude group: 'org.json'
}
}
publish {
artifactId = 'extension-okhttp'
description = 'An OkHttp extension for ExoPlayer.'
repoName = releaseRepoName
userOrg = releaseUserOrg
groupId = releaseGroupId
version = releaseVersion
website = releaseWebsite
ext {
releaseArtifact = 'extension-okhttp'
releaseDescription = 'OkHttp extension for ExoPlayer.'
}
apply from: '../../publish.gradle'
......@@ -27,7 +27,6 @@ import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
......@@ -51,7 +50,8 @@ public class OkHttpDataSource implements HttpDataSource {
private final Predicate<String> contentTypePredicate;
private final TransferListener<? super OkHttpDataSource> listener;
private final CacheControl cacheControl;
private final HashMap<String, String> requestProperties;
private final RequestProperties defaultRequestProperties;
private final RequestProperties requestProperties;
private DataSpec dataSpec;
private Response response;
......@@ -87,7 +87,7 @@ public class OkHttpDataSource implements HttpDataSource {
*/
public OkHttpDataSource(Call.Factory callFactory, String userAgent,
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 {
* {@link #open(DataSpec)}.
* @param listener An optional listener.
* @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,
Predicate<String> contentTypePredicate, TransferListener<? super OkHttpDataSource> listener,
CacheControl cacheControl) {
CacheControl cacheControl, RequestProperties defaultRequestProperties) {
this.callFactory = Assertions.checkNotNull(callFactory);
this.userAgent = Assertions.checkNotEmpty(userAgent);
this.contentTypePredicate = contentTypePredicate;
this.listener = listener;
this.cacheControl = cacheControl;
this.requestProperties = new HashMap<>();
this.defaultRequestProperties = defaultRequestProperties;
this.requestProperties = new RequestProperties();
}
@Override
......@@ -125,24 +128,18 @@ public class OkHttpDataSource implements HttpDataSource {
public void setRequestProperty(String name, String value) {
Assertions.checkNotNull(name);
Assertions.checkNotNull(value);
synchronized (requestProperties) {
requestProperties.put(name, value);
}
requestProperties.set(name, value);
}
@Override
public void clearRequestProperty(String name) {
Assertions.checkNotNull(name);
synchronized (requestProperties) {
requestProperties.remove(name);
}
requestProperties.remove(name);
}
@Override
public void clearAllRequestProperties() {
synchronized (requestProperties) {
requestProperties.clear();
}
requestProperties.clear();
}
@Override
......@@ -268,11 +265,14 @@ public class OkHttpDataSource implements HttpDataSource {
if (cacheControl != null) {
builder.cacheControl(cacheControl);
}
synchronized (requestProperties) {
for (Map.Entry<String, String> property : requestProperties.entrySet()) {
builder.addHeader(property.getKey(), property.getValue());
if (defaultRequestProperties != null) {
for (Map.Entry<String, String> property : defaultRequestProperties.getSnapshot().entrySet()) {
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)) {
String rangeRequest = "bytes=" + position + "-";
if (length != C.LENGTH_UNSET) {
......
......@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.okhttp;
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.Factory;
import com.google.android.exoplayer2.upstream.TransferListener;
......@@ -59,8 +60,10 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
}
@Override
protected OkHttpDataSource createDataSourceInternal() {
return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl);
protected OkHttpDataSource createDataSourceInternal(
HttpDataSource.RequestProperties defaultRequestProperties) {
return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl,
defaultRequestProperties);
}
}
......@@ -18,7 +18,7 @@ android {
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion 9
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
}
......@@ -32,4 +32,3 @@ android {
dependencies {
compile project(':library')
}
......@@ -17,7 +17,7 @@ package com.google.android.exoplayer2.ext.opus;
import android.os.Handler;
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.SimpleDecoderAudioRenderer;
import com.google.android.exoplayer2.drm.DrmSessionManager;
......@@ -40,35 +40,24 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
*/
public 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.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioCapabilities audioCapabilities) {
super(eventHandler, eventListener, audioCapabilities);
AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, audioProcessors);
}
/**
* @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.
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
*/
public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioCapabilities audioCapabilities, DrmSessionManager<ExoMediaCrypto> drmSessionManager,
boolean playClearSamplesWithoutKeys) {
super(eventHandler, eventListener, audioCapabilities, drmSessionManager,
playClearSamplesWithoutKeys);
DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys,
AudioProcessor... audioProcessors) {
super(eventHandler, eventListener, null, drmSessionManager, playClearSamplesWithoutKeys,
audioProcessors);
}
@Override
......
......@@ -213,7 +213,7 @@ import java.util.List;
SimpleOutputBuffer outputBuffer, int sampleRate);
private native int opusSecureDecode(long decoder, long timeUs, ByteBuffer inputBuffer,
int inputSize, SimpleOutputBuffer outputBuffer, int sampleRate,
ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv,
ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv,
int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
private native void opusClose(long decoder);
private native void opusReset(long decoder);
......
......@@ -40,6 +40,18 @@ git clone https://chromium.googlesource.com/webm/libvpx libvpx && \
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:
```
......@@ -79,5 +91,7 @@ dependencies {
`generate_libvpx_android_configs.sh`
* Clean and re-build the project.
* 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 {
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
minSdkVersion 9
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
}
......
......@@ -19,6 +19,7 @@ import android.content.Context;
import android.net.Uri;
import android.os.Looper;
import android.test.InstrumentationTestCase;
import android.util.Log;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
......@@ -38,8 +39,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
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 ROADTRIP_10BIT_URI = "asset:///roadtrip-vp92-10bit.webm";
private static final String INVALID_BITSTREAM_URI = "asset:///invalid-bitstream.webm";
private static final String TAG = "VpxPlaybackTest";
public void testBasicPlayback() throws ExoPlaybackException {
playUri(BEAR_URI);
}
......@@ -48,6 +52,15 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
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() {
try {
playUri(INVALID_BITSTREAM_URI);
......
......@@ -65,6 +65,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
private final boolean playClearSamplesWithoutKeys;
private final EventDispatcher eventDispatcher;
private final FormatHolder formatHolder;
private final DecoderInputBuffer flagsOnlyBuffer;
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
private DecoderCounters decoderCounters;
......@@ -149,6 +150,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
joiningDeadlineMs = -1;
clearLastReportedVideoSize();
formatHolder = new FormatHolder();
flagsOnlyBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
outputMode = VpxDecoder.OUTPUT_MODE_NONE;
}
......@@ -165,10 +167,22 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
return;
}
// Try and read a format if we don't have one already.
if (format == null && !readFormat()) {
// We can't make progress without one.
return;
if (format == null) {
// We don't have a format yet, so try and read one.
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;
outputStreamEnded = true;
return;
} else {
// We still don't have a format and can't make progress without one.
return;
}
}
if (isRendererAvailable()) {
......@@ -327,7 +341,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
// We've already read an encrypted sample into buffer, and are waiting for keys.
result = C.RESULT_BUFFER_READ;
} else {
result = readSource(formatHolder, inputBuffer);
result = readSource(formatHolder, inputBuffer, false);
}
if (result == C.RESULT_NOTHING_READ) {
......@@ -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 {
Format oldFormat = format;
format = newFormat;
......
......@@ -141,7 +141,7 @@ import java.nio.ByteBuffer;
private native long vpxClose(long context);
private native long vpxDecode(long context, ByteBuffer encoded, int length);
private native long vpxSecureDecode(long context, ByteBuffer encoded, int length,
ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv,
ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv,
int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer);
private native int vpxGetErrorCode(long context);
......
......@@ -57,6 +57,16 @@ public final class VpxLibrary {
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 vpxGetBuildConfig();
public static native boolean vpxIsSecureDecodeSupported();
......
......@@ -26,6 +26,7 @@ import java.nio.ByteBuffer;
public static final int COLORSPACE_UNKNOWN = 0;
public static final int COLORSPACE_BT601 = 1;
public static final int COLORSPACE_BT709 = 2;
public static final int COLORSPACE_BT2020 = 3;
private final VpxDecoder owner;
......
......@@ -42,6 +42,12 @@ import javax.microedition.khronos.opengles.GL10;
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 =
"varying vec2 interp_tc;\n"
+ "attribute vec4 in_pos;\n"
......@@ -59,12 +65,13 @@ import javax.microedition.khronos.opengles.GL10;
+ "uniform sampler2D v_tex;\n"
+ "uniform mat3 mColorConversion;\n"
+ "void main() {\n"
+ " vec3 yuv;"
+ " vec3 yuv;\n"
+ " yuv.x = texture2D(y_tex, interp_tc).r - 0.0625;\n"
+ " yuv.y = texture2D(u_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";
private static final FloatBuffer TEXTURE_VERTICES = nativeFloatBuffer(
-1.0f, 1.0f,
-1.0f, -1.0f,
......@@ -156,8 +163,18 @@ import javax.microedition.khronos.opengles.GL10;
}
VpxOutputBuffer outputBuffer = renderedOutputBuffer;
// Set color matrix. Assume BT709 if the color space is unknown.
float[] colorConversion = outputBuffer.colorspace == VpxOutputBuffer.COLORSPACE_BT601
? kColorConversion601 : kColorConversion709;
float[] colorConversion = 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);
for (int i = 0; i < 3; i++) {
......
......@@ -40,7 +40,7 @@ config[0]+=" --enable-neon-asm"
arch[1]="armeabi"
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"
config[2]="--force-target=mips32-android-gcc --sdk-path=$ndk"
......@@ -78,12 +78,12 @@ convert_asm() {
for i in $(seq 0 ${limit}); do
while read file; do
case "${file}" in
*.asm.s)
*.asm.[sS])
# Some files may already have been processed (there are duplicated
# .asm.s files for vp8 in the armeabi/armeabi-v7a configurations).
file="libvpx/${file}"
if [[ ! -e "${file}" ]]; then
asm_file="${file%.s}"
asm_file="${file%.[sS]}"
cat "${asm_file}" | libvpx/build/make/ads2gas.pl > "${file}"
remove_trailing_whitespace "${file}"
rm "${asm_file}"
......@@ -105,7 +105,11 @@ for i in $(seq 0 ${limit}); do
echo "configure ${config[${i}]} ${common_params}"
../../libvpx/configure ${config[${i}]} ${common_params}
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
rm -rf !(${allowed_files// /|})
......
......@@ -35,16 +35,22 @@ LOCAL_SRC_FILES += $(addprefix libvpx/, $(filter-out vpx_config.c, \
$(filter %.c, $(libvpx_codec_srcs))))
# 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/, \
$(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)),)
# 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 .s,.s.neon,$(LOCAL_SRC_FILES))
LOCAL_SRC_FILES := $(subst .S,.S.neon,$(LOCAL_SRC_FILES))
endif
# remove duplicates
LOCAL_SRC_FILES := $(sort $(LOCAL_SRC_FILES))
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/libvpx \
$(LOCAL_PATH)/libvpx/vpx
......
......@@ -74,8 +74,11 @@ DECODER_FUNC(jlong, vpxInit) {
vpx_codec_dec_cfg_t cfg = {0, 0, 0};
cfg.threads = android_getCpuCount();
errorCode = 0;
if (vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo, &cfg, 0)) {
LOGE("ERROR: Fail to initialize libvpx decoder.");
vpx_codec_err_t err = vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo,
&cfg, 0);
if (err) {
LOGE("ERROR: Failed to initialize libvpx decoder, error = %d.", err);
errorCode = err;
return 0;
}
......@@ -160,6 +163,7 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
const int kColorspaceUnknown = 0;
const int kColorspaceBT601 = 1;
const int kColorspaceBT709 = 2;
const int kColorspaceBT2020 = 3;
int colorspace = kColorspaceUnknown;
switch (img->cs) {
......@@ -169,6 +173,9 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
case VPX_CS_BT_709:
colorspace = kColorspaceBT709;
break;
case VPX_CS_BT_2020:
colorspace = kColorspaceBT2020;
break;
default:
break;
}
......@@ -186,14 +193,55 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
jbyte* const data =
reinterpret_cast<jbyte*>(env->GetDirectBufferAddress(dataObject));
// TODO: This copy can be eliminated by using external frame buffers. NOLINT
// This is insignificant for smaller videos but takes ~1.5ms for 1080p
// clips. So this should eventually be gotten rid of.
const uint64_t y_length = img->stride[VPX_PLANE_Y] * img->d_h;
const uint64_t uv_length = img->stride[VPX_PLANE_U] * ((img->d_h + 1) / 2);
memcpy(data, img->planes[VPX_PLANE_Y], y_length);
memcpy(data + y_length, img->planes[VPX_PLANE_U], uv_length);
memcpy(data + y_length + uv_length, img->planes[VPX_PLANE_V], uv_length);
const int32_t uvHeight = (img->d_h + 1) / 2;
const uint64_t yLength = img->stride[VPX_PLANE_Y] * img->d_h;
const uint64_t uvLength = img->stride[VPX_PLANE_U] * uvHeight;
int sample = 0;
if (img->fmt == VPX_IMG_FMT_I42016) { // HBD planar 420.
// Note: The stride for BT2020 is twice of what we use so this is wasting
// memory. The long term goal however is to upload half-float/short so
// 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;
}
......
#Mon Oct 24 14:40:37 BST 2016
#Mon Mar 13 11:17:14 GMT 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
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 @@
import com.android.builder.core.BuilderConstants
apply plugin: 'com.android.library'
apply plugin: 'bintray-release'
android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion
defaultConfig {
// Important: ExoPlayerLib specifies a minSdkVersion of 9 because
// various components provided by the library may be of use on older
// devices. However, please note that the core video playback
// functionality provided by the library requires API level 16 or
// greater.
minSdkVersion 9
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
}
......@@ -47,10 +41,10 @@ android {
}
dependencies {
compile 'com.android.support:support-annotations:25.2.0'
androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
androidTestCompile 'org.mockito:mockito-core:1.9.5'
compile 'com.android.support:support-annotations:25.0.1'
}
android.libraryVariants.all { variant ->
......@@ -86,12 +80,8 @@ android.libraryVariants.all { variant ->
}
}
publish {
artifactId = 'exoplayer'
description = 'The ExoPlayer library.'
repoName = releaseRepoName
userOrg = releaseUserOrg
groupId = releaseGroupId
version = releaseVersion
website = releaseWebsite
ext {
releaseArtifact = 'exoplayer'
releaseDescription = 'The ExoPlayer library.'
}
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
track 0:
format:
bitrate = -1
id = null
id = 0
containerMimeType = null
sampleMimeType = audio/ac3
maxInputSize = -1
......
......@@ -6,7 +6,7 @@ numberOfTracks = 2
track 0:
format:
bitrate = -1
id = null
id = 0
containerMimeType = null
sampleMimeType = audio/mp4a-latm
maxInputSize = -1
......@@ -606,7 +606,7 @@ track 0:
track 1:
format:
bitrate = -1
id = null
id = 1
containerMimeType = null
sampleMimeType = application/id3
maxInputSize = -1
......
......@@ -6,7 +6,7 @@ numberOfTracks = 2
track 192:
format:
bitrate = -1
id = null
id = 192
containerMimeType = null
sampleMimeType = audio/mpeg-L2
maxInputSize = 4096
......@@ -45,7 +45,7 @@ track 192:
track 224:
format:
bitrate = -1
id = null
id = 224
containerMimeType = null
sampleMimeType = video/mpeg2
maxInputSize = -1
......
......@@ -6,7 +6,7 @@ numberOfTracks = 2
track 256:
format:
bitrate = -1
id = null
id = 1/256
containerMimeType = null
sampleMimeType = video/mpeg2
maxInputSize = -1
......@@ -38,7 +38,7 @@ track 256:
track 257:
format:
bitrate = -1
id = null
id = 1/257
containerMimeType = null
sampleMimeType = audio/mpeg-L2
maxInputSize = 4096
......
......@@ -461,6 +461,11 @@ public final class ExoPlayerTest extends TestCase {
}
@Override
public void discardBuffer(long positionUs) {
// Do nothing.
}
@Override
public long readDiscontinuity() {
assertTrue(preparedPeriod);
return C.TIME_UNSET;
......@@ -513,8 +518,9 @@ public final class ExoPlayerTest extends TestCase {
}
@Override
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) {
if (buffer == null || !readFormat) {
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
boolean formatRequired) {
if (formatRequired || !readFormat) {
formatHolder.format = format;
readFormat = true;
return C.RESULT_FORMAT_READ;
......@@ -571,7 +577,7 @@ public final class ExoPlayerTest extends TestCase {
FormatHolder formatHolder = new FormatHolder();
DecoderInputBuffer buffer =
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
int result = readSource(formatHolder, buffer);
int result = readSource(formatHolder, buffer, false);
if (result == C.RESULT_FORMAT_READ) {
formatReadCount++;
assertEquals(expectedFormat, formatHolder.format);
......
......@@ -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.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.MimeTypes;
import java.util.Arrays;
import java.util.HashMap;
import org.mockito.Mock;
......@@ -217,7 +218,11 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase {
}
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());
}
......
......@@ -25,21 +25,32 @@ import com.google.android.exoplayer2.testutil.TestUtil;
*/
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 {
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 {
TestUtil.assertThrows(EXTRACTOR_FACTORY, "mp4/sample_fragmented_zero_size_atom.mp4",
TestUtil.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4",
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 {
@Override
protected void setUp() throws Exception {
FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput();
adtsOutput = fakeExtractorOutput.track(0);
id3Output = fakeExtractorOutput.track(1);
adtsOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_AUDIO);
id3Output = fakeExtractorOutput.track(1, C.TRACK_TYPE_METADATA);
adtsReader = new AdtsReader(true);
TrackIdGenerator idGenerator = new TrackIdGenerator(0, 1);
adtsReader.createTracks(fakeExtractorOutput, idGenerator);
......
......@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts;
import android.test.InstrumentationTestCase;
import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
......@@ -74,7 +75,8 @@ public final class TsExtractorTest extends InstrumentationTestCase {
public void testCustomPesReader() throws Exception {
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()
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts"))
.setSimulateIOErrors(false)
......@@ -92,13 +94,14 @@ public final class TsExtractorTest extends InstrumentationTestCase {
TrackOutput trackOutput = reader.getTrackOutput();
assertTrue(trackOutput == output.trackOutputs.get(257 /* PID of audio track. */));
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);
}
public void testCustomInitialSectionReader() throws Exception {
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()
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample_with_sdt.ts"))
.setSimulateIOErrors(false)
......@@ -178,8 +181,9 @@ public final class TsExtractorTest extends InstrumentationTestCase {
@Override
public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
output = extractorOutput.track(idGenerator.getNextId());
output.format(Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0,
idGenerator.generateNewId();
output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_UNKNOWN);
output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), "mime", null, 0, 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;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.util.MimeTypes;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.Charset;
......@@ -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"
+ "http://example.com/low.m3u8\n";
public void testParseMasterPlaylist() throws IOException{
HlsPlaylist playlist = parsePlaylist(PLAYLIST_URI, MASTER_PLAYLIST);
assertNotNull(playlist);
assertEquals(HlsPlaylist.TYPE_MASTER, playlist.type);
private static final String MASTER_PLAYLIST_WITH_CC = " #EXTM3U \n"
+ "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n"
+ "\n"
+ "#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;
assertNotNull(variants);
......@@ -98,18 +101,28 @@ public class HlsMasterPlaylistParserTest extends TestCase {
public void testPlaylistWithInvalidHeader() throws IOException {
try {
parsePlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER);
parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER);
fail("Expected exception not thrown.");
} catch (ParserException e) {
// 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);
ByteArrayInputStream inputStream = new ByteArrayInputStream(
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 {
String playlistString = "#EXTM3U\n"
+ "#EXT-X-VERSION:3\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-START:TIME-OFFSET=-25"
+ "#EXT-X-TARGETDURATION:8\n"
+ "#EXT-X-MEDIA-SEQUENCE:2679\n"
+ "#EXT-X-DISCONTINUITY-SEQUENCE:4\n"
......@@ -73,6 +74,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
assertEquals(HlsMediaPlaylist.PLAYLIST_TYPE_VOD, mediaPlaylist.playlistType);
assertEquals(mediaPlaylist.durationUs - 25000000, mediaPlaylist.startOffsetUs);
assertEquals(2679, mediaPlaylist.mediaSequence);
assertEquals(3, mediaPlaylist.version);
......
......@@ -20,9 +20,9 @@ import android.test.InstrumentationTestCase;
import android.test.MoreAsserts;
import com.google.android.exoplayer2.C;
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.FileDataSource;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
......@@ -42,13 +42,13 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
@Override
protected void setUp() throws Exception {
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
}
@Override
protected void tearDown() throws Exception {
TestUtil.recursiveDelete(cacheDir);
Util.recursiveDelete(cacheDir);
}
public void testMaxCacheFileSize() throws Exception {
......@@ -126,9 +126,15 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
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)
throws IOException {
// Read all data from upstream and cache
// Read all data from upstream and write to cache
CacheDataSource cacheDataSource = createCacheDataSource(false, simulateUnknownLength);
assertReadDataContentLength(cacheDataSource, unboundedRequest, simulateUnknownLength);
......@@ -184,14 +190,21 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
private CacheDataSource createCacheDataSource(boolean setReadException,
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) {
builder.appendReadError(new IOException("Shouldn't read from upstream"));
}
builder.setSimulateUnknownLength(simulateUnknownLength);
builder.appendReadData(TEST_DATA);
FakeDataSource upstream = builder.build();
return new CacheDataSource(simpleCache, upstream, flags, MAX_CACHE_FILE_SIZE);
FakeDataSource upstream =
builder.setSimulateUnknownLength(simulateUnknownLength).appendReadData(TEST_DATA).build();
return new CacheDataSource(simpleCache, upstream, new FileDataSource(), cacheWriteDataSink,
flags, null);
}
}
......@@ -4,7 +4,7 @@ import android.test.InstrumentationTestCase;
import android.test.MoreAsserts;
import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
......@@ -36,13 +36,13 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
@Override
public void setUp() throws Exception {
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
index = new CachedContentIndex(cacheDir);
}
@Override
protected void tearDown() throws Exception {
TestUtil.recursiveDelete(cacheDir);
Util.recursiveDelete(cacheDir);
}
public void testAddGetRemove() throws Exception {
......
......@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream.cache;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.ChunkIndex;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException;
import org.mockito.Mock;
......@@ -49,13 +50,13 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase {
tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX);
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
index = new CachedContentIndex(cacheDir);
}
@Override
protected void tearDown() throws Exception {
TestUtil.recursiveDelete(cacheDir);
Util.recursiveDelete(cacheDir);
}
public void testGetRegion_noSpansInCache() {
......
......@@ -16,7 +16,7 @@
package com.google.android.exoplayer2.upstream.cache;
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.FileOutputStream;
import java.io.IOException;
......@@ -48,13 +48,13 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
@Override
protected void setUp() throws Exception {
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
index = new CachedContentIndex(cacheDir);
}
@Override
protected void tearDown() throws Exception {
TestUtil.recursiveDelete(cacheDir);
Util.recursiveDelete(cacheDir);
}
public void testCacheFile() throws Exception {
......
......@@ -18,7 +18,6 @@ package com.google.android.exoplayer2.upstream.cache;
import android.test.InstrumentationTestCase;
import android.test.MoreAsserts;
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.FileInputStream;
......@@ -39,12 +38,12 @@ public class SimpleCacheTest extends InstrumentationTestCase {
@Override
protected void setUp() throws Exception {
this.cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
}
@Override
protected void tearDown() throws Exception {
TestUtil.recursiveDelete(cacheDir);
Util.recursiveDelete(cacheDir);
}
public void testCommittingOneFile() throws Exception {
......@@ -192,6 +191,41 @@ public class SimpleCacheTest extends InstrumentationTestCase {
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() {
return new SimpleCache(cacheDir, new NoOpCacheEvictor());
}
......
......@@ -17,7 +17,6 @@
package com.google.android.exoplayer2.util;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
......@@ -34,14 +33,14 @@ public class AtomicFileTest extends InstrumentationTestCase {
@Override
public void setUp() throws Exception {
tempFolder = TestUtil.createTempFolder(getInstrumentation().getContext());
tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
file = new File(tempFolder, "atomicFile");
atomicFile = new AtomicFile(file);
}
@Override
protected void tearDown() throws Exception {
TestUtil.recursiveDelete(tempFolder);
Util.recursiveDelete(tempFolder);
}
public void testDelete() throws Exception {
......
......@@ -142,8 +142,10 @@ public class UtilTest extends TestCase {
public void testParseXsDateTime() throws Exception {
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:00,000Z"));
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.000-0800"));
}
public void testUnescapeInvalidFileName() {
......
......@@ -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
* {@link C#RESULT_BUFFER_READ} is only returned if {@link #setCurrentStreamFinal()} has been
* called. {@link C#RESULT_NOTHING_READ} is returned otherwise.
......@@ -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 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
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. May be null if the
* caller requires that the format of the stream be read even if it's not changing.
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.
* @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
* {@link C#RESULT_BUFFER_READ}.
*/
protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer) {
int result = stream.readData(formatHolder, buffer);
protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer,
boolean formatRequired) {
int result = stream.readData(formatHolder, buffer, formatRequired);
if (result == C.RESULT_BUFFER_READ) {
if (buffer.isEndOfStream()) {
readEndOfStream = true;
......
......@@ -444,8 +444,15 @@ public final class C {
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.
* <p></p>
* <p>
* 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);
......@@ -477,7 +484,7 @@ public final class C {
* {@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
* 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;
......@@ -515,7 +522,13 @@ public final class C {
* The stereo mode for 360/3D/VR videos.
*/
@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 {}
/**
* Indicates Monoscopic stereo layout, used with 360/3D/VR videos.
......@@ -529,6 +542,16 @@ public final class C {
* Indicates Left-Right stereo layout, used with 360/3D/VR videos.
*/
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
......
......@@ -51,11 +51,6 @@ public final class DefaultLoadControl implements LoadControl {
*/
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 BETWEEN_WATERMARKS = 1;
private static final int BELOW_LOW_WATERMARK = 2;
......@@ -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
* buffer depletion rather than a user action.
* @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.
*/
public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
......@@ -183,9 +178,9 @@ public final class DefaultLoadControl implements LoadControl {
|| (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached);
if (priorityTaskManager != null && isBuffering != wasBuffering) {
if (isBuffering) {
priorityTaskManager.add(LOADING_PRIORITY);
priorityTaskManager.add(C.PRIORITY_PLAYBACK);
} else {
priorityTaskManager.remove(LOADING_PRIORITY);
priorityTaskManager.remove(C.PRIORITY_PLAYBACK);
}
}
return isBuffering;
......@@ -199,7 +194,7 @@ public final class DefaultLoadControl implements LoadControl {
private void reset(boolean resetAllocator) {
targetBufferSize = 0;
if (priorityTaskManager != null && isBuffering) {
priorityTaskManager.remove(LOADING_PRIORITY);
priorityTaskManager.remove(C.PRIORITY_PLAYBACK);
}
isBuffering = false;
if (resetAllocator) {
......
......@@ -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
* {@link #TYPE_UNEXPECTED}.
*/
@Type
public final int type;
@Type public final int type;
/**
* If {@link #type} is {@link #TYPE_RENDERER}, this is the index of the renderer.
......
......@@ -455,6 +455,8 @@ import java.io.IOException;
TraceUtil.beginSection("doSomeWork");
updatePlaybackPositions();
playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs);
boolean allRenderersEnded = true;
boolean allRenderersReadyOrEnded = true;
for (Renderer renderer : enabledRenderers) {
......
......@@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo {
/**
* 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.
......@@ -32,7 +32,7 @@ public interface ExoPlayerLibraryInfo {
* corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding
* 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}
......
......@@ -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
* 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
public final int stereoMode;
......@@ -438,16 +438,19 @@ public final class Format implements Parcelable {
drmInitData, metadata);
}
public Format copyWithManifestFormatInfo(Format manifestFormat,
boolean preferManifestDrmInitData) {
public Format copyWithManifestFormatInfo(Format manifestFormat) {
if (this == manifestFormat) {
// No need to copy from ourselves.
return this;
}
String id = manifestFormat.id;
String codecs = this.codecs == null ? manifestFormat.codecs : this.codecs;
int bitrate = this.bitrate == NO_VALUE ? manifestFormat.bitrate : this.bitrate;
float frameRate = this.frameRate == NO_VALUE ? manifestFormat.frameRate : this.frameRate;
@C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags;
String language = this.language == null ? manifestFormat.language : this.language;
DrmInitData drmInitData = (preferManifestDrmInitData && manifestFormat.drmInitData != null)
|| this.drmInitData == null ? manifestFormat.drmInitData : this.drmInitData;
DrmInitData drmInitData = manifestFormat.drmInitData != null ? manifestFormat.drmInitData
: this.drmInitData;
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags,
......@@ -672,9 +675,6 @@ public final class Format implements Parcelable {
dest.writeParcelable(metadata, 0);
}
/**
* {@link Creator} implementation.
*/
public static final Creator<Format> CREATOR = new Creator<Format>() {
@Override
......
......@@ -28,6 +28,7 @@ import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
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.MediaCodecAudioRenderer;
import com.google.android.exoplayer2.decoder.DecoderCounters;
......@@ -624,7 +625,7 @@ public class SimpleExoPlayer implements ExoPlayer {
buildVideoRenderers(context, mainHandler, drmSessionManager, extensionRendererMode,
componentListener, allowedVideoJoiningTimeMs, out);
buildAudioRenderers(context, mainHandler, drmSessionManager, extensionRendererMode,
componentListener, out);
componentListener, buildAudioProcessors(), out);
buildTextRenderers(context, mainHandler, extensionRendererMode, componentListener, out);
buildMetadataRenderers(context, mainHandler, extensionRendererMode, componentListener, out);
buildMiscellaneousRenderers(context, mainHandler, extensionRendererMode, out);
......@@ -636,7 +637,7 @@ public class SimpleExoPlayer implements ExoPlayer {
* @param context The {@link Context} associated with the player.
* @param mainHandler A handler associated with the main thread's looper.
* @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 eventListener An event listener.
* @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video renderers
......@@ -681,17 +682,19 @@ public class SimpleExoPlayer implements ExoPlayer {
* @param context The {@link Context} associated with the player.
* @param mainHandler A handler associated with the main thread's looper.
* @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 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.
*/
protected void buildAudioRenderers(Context context, Handler mainHandler,
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
@ExtensionRendererMode int extensionRendererMode, AudioRendererEventListener eventListener,
ArrayList<Renderer> out) {
AudioProcessor[] audioProcessors, ArrayList<Renderer> out) {
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) {
return;
......@@ -705,8 +708,9 @@ public class SimpleExoPlayer implements ExoPlayer {
Class<?> clazz =
Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer");
Constructor<?> constructor = clazz.getConstructor(Handler.class,
AudioRendererEventListener.class);
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener);
AudioRendererEventListener.class, AudioProcessor[].class);
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener,
audioProcessors);
out.add(extensionRendererIndex++, renderer);
Log.i(TAG, "Loaded LibopusAudioRenderer.");
} catch (ClassNotFoundException e) {
......@@ -719,8 +723,9 @@ public class SimpleExoPlayer implements ExoPlayer {
Class<?> clazz =
Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer");
Constructor<?> constructor = clazz.getConstructor(Handler.class,
AudioRendererEventListener.class);
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener);
AudioRendererEventListener.class, AudioProcessor[].class);
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener,
audioProcessors);
out.add(extensionRendererIndex++, renderer);
Log.i(TAG, "Loaded LibflacAudioRenderer.");
} catch (ClassNotFoundException e) {
......@@ -733,8 +738,9 @@ public class SimpleExoPlayer implements ExoPlayer {
Class<?> clazz =
Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer");
Constructor<?> constructor = clazz.getConstructor(Handler.class,
AudioRendererEventListener.class);
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener);
AudioRendererEventListener.class, AudioProcessor[].class);
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener,
audioProcessors);
out.add(extensionRendererIndex++, renderer);
Log.i(TAG, "Loaded FfmpegAudioRenderer.");
} catch (ClassNotFoundException e) {
......@@ -787,6 +793,13 @@ public class SimpleExoPlayer implements ExoPlayer {
// 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.
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
private final AudioTrack audioTrack;
private boolean passthroughEnabled;
private boolean codecNeedsDiscardChannelsWorkaround;
private android.media.MediaFormat passthroughMediaFormat;
private int pcmEncoding;
private int channelCount;
private long currentPositionUs;
private boolean allowPositionDiscontinuity;
......@@ -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 audioCapabilities The audio capabilities for playback on this device. May be null if the
* 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,
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
boolean playClearSamplesWithoutKeys, Handler eventHandler,
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) {
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities,
AudioProcessor... audioProcessors) {
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);
}
......@@ -185,6 +190,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Override
protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
MediaCrypto crypto) {
codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name);
if (passthroughEnabled) {
// Override the MIME type used to configure the codec if we are using a passthrough decoder.
passthroughMediaFormat = format.getFrameworkMediaFormatV16();
......@@ -216,17 +222,33 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
// output 16-bit PCM.
pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding
: C.ENCODING_PCM_16BIT;
channelCount = newFormat.channelCount;
}
@Override
protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) {
protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat)
throws ExoPlaybackException {
boolean passthrough = passthroughMediaFormat != null;
String mimeType = passthrough ? passthroughMediaFormat.getString(MediaFormat.KEY_MIME)
: MimeTypes.AUDIO_RAW;
MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat;
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
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
@Override
public boolean isEnded() {
return super.isEnded() && !audioTrack.hasPendingData();
return super.isEnded() && audioTrack.isEnded();
}
@Override
......@@ -353,8 +375,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
}
@Override
protected void onOutputStreamEnded() {
audioTrack.handleEndOfStream();
protected void renderToEndOfStream() throws ExoPlaybackException {
try {
audioTrack.playToEndOfStream();
} catch (AudioTrack.WriteException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
}
@Override
......@@ -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 {
@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;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSessionManager;
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.MimeTypes;
import com.google.android.exoplayer2.util.TraceUtil;
......@@ -67,12 +68,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
*/
private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2;
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
private final boolean playClearSamplesWithoutKeys;
private final EventDispatcher eventDispatcher;
private final AudioTrack audioTrack;
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
private final FormatHolder formatHolder;
private final DecoderInputBuffer flagsOnlyBuffer;
private DecoderCounters decoderCounters;
private Format inputFormat;
......@@ -83,8 +84,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
private DrmSession<ExoMediaCrypto> drmSession;
private DrmSession<ExoMediaCrypto> pendingDrmSession;
@ReinitializationState
private int decoderReinitializationState;
@ReinitializationState private int decoderReinitializationState;
private boolean decoderReceivedBuffers;
private boolean audioTrackNeedsConfigure;
......@@ -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
* 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,
AudioRendererEventListener eventListener) {
this(eventHandler, eventListener, null);
AudioRendererEventListener eventListener, AudioProcessor... audioProcessors) {
this(eventHandler, eventListener, null, null, false, audioProcessors);
}
/**
......@@ -133,16 +134,19 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
* 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}
* 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,
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities,
DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys) {
DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys,
AudioProcessor... audioProcessors) {
super(C.TRACK_TYPE_AUDIO);
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener());
this.drmSessionManager = drmSessionManager;
formatHolder = new FormatHolder();
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;
audioTrackNeedsConfigure = true;
}
......@@ -174,13 +178,31 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
@Override
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
if (outputStreamEnded) {
try {
audioTrack.playToEndOfStream();
} catch (AudioTrack.WriteException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
return;
}
// Try and read a format if we don't have one already.
if (inputFormat == null && !readFormat()) {
// We can't make progress without one.
return;
if (inputFormat == null) {
// We don't have a format yet, so try and read one.
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.
......@@ -193,8 +215,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
while (drainOutputBuffer()) {}
while (feedInputBuffer()) {}
TraceUtil.endSection();
} catch (AudioTrack.InitializationException | AudioTrack.WriteException
| AudioDecoderException e) {
} catch (AudioDecoderException | AudioTrack.ConfigurationException
| AudioTrack.InitializationException | AudioTrack.WriteException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
decoderCounters.ensureUpdated();
......@@ -255,7 +277,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
}
private boolean drainOutputBuffer() throws ExoPlaybackException, AudioDecoderException,
AudioTrack.InitializationException, AudioTrack.WriteException {
AudioTrack.ConfigurationException, AudioTrack.InitializationException,
AudioTrack.WriteException {
if (outputBuffer == null) {
outputBuffer = decoder.dequeueOutputBuffer();
if (outputBuffer == null) {
......@@ -274,8 +297,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
} else {
outputBuffer.release();
outputBuffer = null;
outputStreamEnded = true;
audioTrack.handleEndOfStream();
processEndOfStream();
}
return false;
}
......@@ -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.
result = C.RESULT_BUFFER_READ;
} else {
result = readSource(formatHolder, inputBuffer);
result = readSource(formatHolder, inputBuffer, false);
}
if (result == C.RESULT_NOTHING_READ) {
......@@ -365,6 +387,15 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
&& (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 {
waitingForKeys = false;
if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) {
......@@ -383,7 +414,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
@Override
public boolean isEnded() {
return outputStreamEnded && !audioTrack.hasPendingData();
return outputStreamEnded && audioTrack.isEnded();
}
@Override
......@@ -513,15 +544,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
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 {
Format oldFormat = inputFormat;
inputFormat = newFormat;
......
......@@ -61,8 +61,7 @@ public class DecoderInputBuffer extends Buffer {
*/
public long timeUs;
@BufferReplacementMode
private final int bufferReplacementMode;
@BufferReplacementMode private final int bufferReplacementMode;
/**
* @param bufferReplacementMode Determines the behavior of {@link #ensureSpaceForWrite(int)}. One
......
......@@ -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.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
......@@ -103,6 +104,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
public static final int MODE_RELEASE = 3;
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_KEYS = 1;
......@@ -280,6 +282,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
* required.
*
* <p>{@code mode} must be one of these:
* <ul>
* <li>{@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is
* requested otherwise the offline license is restored.
* <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
* requested otherwise the offline license is renewed.
* <li>{@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline license
* is released.
* </ul>
*
* @param mode The mode to be set.
* @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
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;
openInternal(true);
......
......@@ -31,7 +31,7 @@ public interface DrmSession<T extends ExoMediaCrypto> {
/** Wraps the exception which is the cause of the error state. */
class DrmSessionException extends Exception {
DrmSessionException(Exception e) {
public DrmSessionException(Exception e) {
super(e);
}
......@@ -70,8 +70,7 @@ public interface DrmSession<T extends ExoMediaCrypto> {
* @return One of {@link #STATE_ERROR}, {@link #STATE_CLOSED}, {@link #STATE_OPENING},
* {@link #STATE_OPENED} and {@link #STATE_OPENED_WITH_KEYS}.
*/
@State
int getState();
@State int getState();
/**
* Returns a {@link ExoMediaCrypto} for the open session.
......
......@@ -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.DataSpec;
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 java.io.IOException;
import java.util.HashMap;
......@@ -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 dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
* @param keyRequestProperties Request properties to set when making key requests, or null.
*/
@Deprecated
public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory,
Map<String, String> keyRequestProperties) {
this.dataSourceFactory = dataSourceFactory;
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
public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException {
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
......@@ -85,14 +128,14 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
if (C.PLAYREADY_UUID.equals(uuid)) {
requestProperties.putAll(PLAYREADY_KEY_REQUEST_PROPERTIES);
}
if (keyRequestProperties != null) {
synchronized (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)
throws IOException {
private static byte[] executePost(HttpDataSource.Factory dataSourceFactory, String url,
byte[] data, Map<String, String> requestProperties) throws IOException {
HttpDataSource dataSource = dataSourceFactory.createDataSource();
if (requestProperties != null) {
for (Map.Entry<String, String> requestProperty : requestProperties.entrySet()) {
......
......@@ -17,7 +17,6 @@
package com.google.android.exoplayer2.drm;
import android.media.MediaDrm;
import android.net.Uri;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.HandlerThread;
......@@ -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.Mode;
import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.extractor.Extractor;
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.DashUtil;
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.DashManifestParser;
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.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.Factory;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException;
import java.util.HashMap;
......@@ -59,28 +48,6 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
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
* you're done with the helper instance.
*
......@@ -93,7 +60,7 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
public static OfflineLicenseHelper<FrameworkMediaCrypto> newWidevineInstance(
String licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException {
return newWidevineInstance(
new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory, null), null);
new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory), null);
}
/**
......@@ -174,7 +141,7 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
*/
public byte[] download(HttpDataSource dataSource, String manifestUriString)
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> {
Representation representation = adaptationSet.representations.get(0);
DrmInitData drmInitData = representation.format.drmInitData;
if (drmInitData == null) {
InitializationChunk initializationChunk = loadInitializationChunk(dataSource, representation);
if (initializationChunk == null) {
return null;
}
Format sampleFormat = initializationChunk.getSampleFormat();
Format sampleFormat = DashUtil.loadSampleFormat(dataSource, representation);
if (sampleFormat != null) {
drmInitData = sampleFormat.drmInitData;
}
......@@ -288,28 +251,4 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
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 {
/**
* Either {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.
*/
@Reason
public final int reason;
@Reason public final int reason;
/**
* @param reason {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.
......
......@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.EOFException;
import java.io.IOException;
import java.util.Arrays;
......@@ -27,6 +28,8 @@ import java.util.Arrays;
*/
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 final DataSource dataSource;
......@@ -46,7 +49,7 @@ public final class DefaultExtractorInput implements ExtractorInput {
this.dataSource = dataSource;
this.position = position;
this.streamLength = length;
peekBuffer = new byte[8 * 1024];
peekBuffer = new byte[PEEK_MIN_FREE_SPACE_AFTER_RESIZE];
}
@Override
......@@ -176,7 +179,9 @@ public final class DefaultExtractorInput implements ExtractorInput {
private void ensureSpaceForPeek(int length) {
int requiredLength = peekBufferPosition + 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 {
private void updatePeekBuffer(int bytesConsumed) {
peekBufferLength -= bytesConsumed;
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 {
private Format downstreamFormat;
// 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 totalBytesWritten;
private Allocation lastAllocation;
......@@ -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 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
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. May be null if the
* caller requires that the format of the stream be read even if it's not changing.
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.
* @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 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.
* @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
* {@link C#RESULT_BUFFER_READ}.
*/
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean loadingFinished,
long decodeOnlyUntilUs) {
switch (infoQueue.readData(formatHolder, buffer, downstreamFormat, extrasHolder)) {
case C.RESULT_NOTHING_READ:
if (loadingFinished) {
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
return C.RESULT_BUFFER_READ;
}
return C.RESULT_NOTHING_READ;
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired,
boolean loadingFinished, long decodeOnlyUntilUs) {
int result = infoQueue.readData(formatHolder, buffer, formatRequired, loadingFinished,
downstreamFormat, extrasHolder);
switch (result) {
case C.RESULT_FORMAT_READ:
downstreamFormat = formatHolder.format;
return C.RESULT_FORMAT_READ;
case C.RESULT_BUFFER_READ:
if (buffer.timeUs < decodeOnlyUntilUs) {
buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
if (!buffer.isEndOfStream()) {
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;
case C.RESULT_NOTHING_READ:
return C.RESULT_NOTHING_READ;
default:
throw new IllegalStateException();
}
......@@ -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
* samples subsequently queued to the buffer. The offset is also used to adjust
* {@link Format#subsampleOffsetUs} for both the {@link Format} passed and those subsequently
* passed to {@link #format(Format)}.
* Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples
* subsequently queued to the buffer.
*
* @param format The format.
* @param sampleOffsetUs The timestamp offset in microseconds.
*/
public void formatWithOffset(Format format, long sampleOffsetUs) {
this.sampleOffsetUs = sampleOffsetUs;
format(format);
public void setSampleOffsetUs(long sampleOffsetUs) {
if (this.sampleOffsetUs != sampleOffsetUs) {
this.sampleOffsetUs = sampleOffsetUs;
pendingFormatAdjustment = true;
}
}
@Override
public void format(Format format) {
Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs);
boolean formatChanged = infoQueue.format(adjustedFormat);
lastUnadjustedFormat = format;
pendingFormatAdjustment = false;
if (upstreamFormatChangeListener != null && formatChanged) {
upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat);
}
......@@ -518,6 +523,9 @@ public final class DefaultTrackOutput implements TrackOutput {
@Override
public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
byte[] encryptionKey) {
if (pendingFormatAdjustment) {
format(lastUnadjustedFormat);
}
if (!startWriteOperation()) {
infoQueue.commitSampleTimestamp(timeUs);
return;
......@@ -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
* 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.
* @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
* 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.
* @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ}
* or {@link C#RESULT_BUFFER_READ}.
*/
@SuppressWarnings("ReferenceEquality")
public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
Format downstreamFormat, BufferExtrasHolder extrasHolder) {
boolean formatRequired, boolean loadingFinished, Format downstreamFormat,
BufferExtrasHolder extrasHolder) {
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;
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];
return C.RESULT_FORMAT_READ;
}
......
......@@ -102,4 +102,5 @@ public interface Extractor {
* Releases all kept resources.
*/
void release();
}
......@@ -23,17 +23,18 @@ public interface ExtractorOutput {
/**
* Called by the {@link Extractor} to get the {@link TrackOutput} for a specific track.
* <p>
* The same {@link TrackOutput} is returned if multiple calls are made with the same
* {@code trackId}.
* The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}.
*
* @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.
*/
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
* passed to {@link #track(int)}.
* passed to {@link #track(int, int)}.
*/
void endTracks();
......
......@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.Metadata;
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.Pattern;
......@@ -26,6 +27,18 @@ import java.util.regex.Pattern;
*/
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 Pattern GAPLESS_COMMENT_PATTERN =
Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})");
......
......@@ -15,6 +15,7 @@
*/
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.ExtractorInput;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
......@@ -183,10 +184,12 @@ public final class FlvExtractor implements Extractor, SeekMap {
boolean hasAudio = (flags & 0x04) != 0;
boolean hasVideo = (flags & 0x01) != 0;
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) {
videoReader = new VideoTagPayloadReader(extractorOutput.track(TAG_TYPE_VIDEO));
videoReader = new VideoTagPayloadReader(
extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO));
}
if (metadataReader == null) {
metadataReader = new ScriptTagPayloadReader(null);
......
......@@ -673,6 +673,9 @@ public final class MatroskaExtractor implements Extractor {
case 3:
currentTrack.stereoMode = C.STEREO_MODE_TOP_BOTTOM;
break;
case 15:
currentTrack.stereoMode = C.STEREO_MODE_STEREO_MESH;
break;
default:
break;
}
......@@ -1462,6 +1465,7 @@ public final class MatroskaExtractor implements Extractor {
throw new ParserException("Unrecognized codec identifier.");
}
int type;
Format format;
@C.SelectionFlags int selectionFlags = 0;
selectionFlags |= flagDefault ? C.SELECTION_FLAG_DEFAULT : 0;
......@@ -1469,10 +1473,12 @@ public final class MatroskaExtractor implements Extractor {
// TODO: Consider reading the name elements of the tracks and, if present, incorporating them
// into the trackId passed when creating the formats.
if (MimeTypes.isAudio(mimeType)) {
type = C.TRACK_TYPE_AUDIO;
format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
Format.NO_VALUE, maxInputSize, channelCount, sampleRate, pcmEncoding,
initializationData, drmInitData, selectionFlags, language);
} else if (MimeTypes.isVideo(mimeType)) {
type = C.TRACK_TYPE_VIDEO;
if (displayUnit == Track.DISPLAY_UNIT_PIXELS) {
displayWidth = displayWidth == Format.NO_VALUE ? width : displayWidth;
displayHeight = displayHeight == Format.NO_VALUE ? height : displayHeight;
......@@ -1485,17 +1491,19 @@ public final class MatroskaExtractor implements Extractor {
Format.NO_VALUE, maxInputSize, width, height, Format.NO_VALUE, initializationData,
Format.NO_VALUE, pixelWidthHeightRatio, projectionData, stereoMode, drmInitData);
} else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) {
type = C.TRACK_TYPE_TEXT;
format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null,
Format.NO_VALUE, selectionFlags, language, drmInitData);
} else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType)
|| MimeTypes.APPLICATION_PGS.equals(mimeType)) {
type = C.TRACK_TYPE_TEXT;
format = Format.createImageSampleFormat(Integer.toString(trackId), mimeType, null,
Format.NO_VALUE, initializationData, language, drmInitData);
} else {
throw new ParserException("Unexpected MIME type.");
}
this.output = output.track(number);
this.output = output.track(number, type);
this.output.format(format);
}
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.extractor.mp3;
import android.support.annotation.IntDef;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
......@@ -33,6 +34,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.io.EOFException;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Extracts data from an MP3 file.
......@@ -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.
*/
private static final int MAX_SYNC_BYTES = 128 * 1024;
......@@ -72,6 +92,7 @@ public final class Mp3Extractor implements Extractor {
private static final int INFO_HEADER = Util.getIntegerCodeForString("Info");
private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI");
@Flags private final int flags;
private final long forcedFirstSampleTimestampUs;
private final ParsableByteArray scratch;
private final MpegAudioHeader synchronizedHeader;
......@@ -93,16 +114,27 @@ public final class Mp3Extractor implements Extractor {
* Constructs a new {@link Mp3Extractor}.
*/
public Mp3Extractor() {
this(C.TIME_UNSET);
this(0);
}
/**
* 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
* {@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;
scratch = new ParsableByteArray(SCRATCH_LENGTH);
synchronizedHeader = new MpegAudioHeader();
......@@ -118,7 +150,7 @@ public final class Mp3Extractor implements Extractor {
@Override
public void init(ExtractorOutput output) {
extractorOutput = output;
trackOutput = extractorOutput.track(0);
trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO);
extractorOutput.endTracks();
}
......@@ -151,7 +183,8 @@ public final class Mp3Extractor implements Extractor {
trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null,
Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels,
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);
}
......@@ -284,7 +317,11 @@ public final class Mp3Extractor implements Extractor {
byte[] id3Data = new byte[tagLength];
System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH);
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) {
gaplessInfoHolder.setFromMetadata(metadata);
}
......@@ -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
// would give an invalid CBR bitrate.
input.resetPeekPosition();
......
......@@ -332,6 +332,9 @@ import java.util.List;
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.
int editedSampleCount = 0;
int nextSampleIndex = 0;
......@@ -342,7 +345,8 @@ import java.util.List;
long duration = Util.scaleLargeTimestamp(track.editListDurations[i], track.timescale,
track.movieTimescale);
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;
copyMetadata |= nextSampleIndex != startIndex;
nextSampleIndex = endIndex;
......@@ -365,7 +369,7 @@ import java.util.List;
long endMediaTime = mediaTime + Util.scaleLargeTimestamp(duration, track.timescale,
track.movieTimescale);
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) {
int count = endIndex - startIndex;
System.arraycopy(offsets, startIndex, editedOffsets, sampleIndex, count);
......@@ -716,6 +720,9 @@ import java.util.List;
case 2:
stereoMode = C.STEREO_MODE_LEFT_RIGHT;
break;
case 3:
stereoMode = C.STEREO_MODE_STEREO_MESH;
break;
default:
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