Commit 6ec840cc by Julian Cable

Merge remote-tracking branch 'upstream/dev-v2' into dev-v2

parents 94a36402 55ca323c
Showing with 2719 additions and 656 deletions
......@@ -58,7 +58,7 @@ this version.
* Fix issues that could cause ExtractorMediaSource based playbacks to get stuck
buffering ([#1962](https://github.com/google/ExoPlayer/issues/1962)).
* Correctly set SimpleExoPlayerView surface aspect ratio when an active player
is attached ([#2077](https://github.com/google/ExoPlayer/issues/1976)).
is attached ([#2077](https://github.com/google/ExoPlayer/issues/2077)).
* OGG: Fix playback of short OGG files
([#1976](https://github.com/google/ExoPlayer/issues/1976)).
* MP4: Support `.mp3` tracks
......
......@@ -24,24 +24,19 @@ android {
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt')
}
debug {
jniDebuggable = true
debuggable = true
}
}
lintOptions {
abortOnError false
}
productFlavors {
noExtensions
withExtensions
}
}
dependencies {
......
......@@ -229,30 +229,6 @@
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
},
{
"name": "WV: Secure Subsample (WebM, VP9 with altref)",
"uri": "https://storage.googleapis.com/widevine_test/vp9/sintel_1080p_vp9_altref_subsample/sintel_1080p_vp9_altref_subsample.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://widevine-proxy.appspot.com/proxy"
},
{
"name": "WV: Secure Fullsample (WebM, VP9 with altref)",
"uri": "https://storage.googleapis.com/widevine_test/vp9/sintel_1080p_vp9_altref_fullsample/sintel_1080p_vp9_altref_fullsample.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://widevine-proxy.appspot.com/proxy"
},
{
"name": "WV: Secure Subsample (WebM, VP9 without altref)",
"uri": "https://storage.googleapis.com/widevine_test/vp9/sintel_1080p_vp9_noaltref_subsample/sintel_1080p_vp9_noaltref_subsample.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://widevine-proxy.appspot.com/proxy"
},
{
"name": "WV: Secure Fullsample (WebM, VP9 without altref)",
"uri": "https://storage.googleapis.com/widevine_test/vp9/sintel_1080p_vp9_noaltref_fullsample/sintel_1080p_vp9_noaltref_fullsample.mpd",
"drm_scheme": "widevine",
"drm_license_url": "https://widevine-proxy.appspot.com/proxy"
}
]
},
......
......@@ -26,16 +26,17 @@ import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.StreamingDrmSessionManager;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
import com.google.android.exoplayer2.metadata.id3.GeobFrame;
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
import com.google.android.exoplayer2.metadata.id3.PrivFrame;
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.metadata.id3.TxxxFrame;
import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.TrackGroup;
......@@ -55,7 +56,7 @@ import java.util.Locale;
*/
/* package */ final class EventLogger implements ExoPlayer.EventListener,
AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener,
ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener,
ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener,
MetadataRenderer.Output {
private static final String TAG = "EventLogger";
......@@ -153,7 +154,7 @@ import java.util.Locale;
String formatSupport = getFormatSupportString(
mappedTrackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex));
Log.d(TAG, " " + status + " Track:" + trackIndex + ", "
+ getFormatString(trackGroup.getFormat(trackIndex))
+ Format.toLogString(trackGroup.getFormat(trackIndex))
+ ", supported=" + formatSupport);
}
Log.d(TAG, " ]");
......@@ -185,7 +186,7 @@ import java.util.Locale;
String formatSupport = getFormatSupportString(
RendererCapabilities.FORMAT_UNSUPPORTED_TYPE);
Log.d(TAG, " " + status + " Track:" + trackIndex + ", "
+ getFormatString(trackGroup.getFormat(trackIndex))
+ Format.toLogString(trackGroup.getFormat(trackIndex))
+ ", supported=" + formatSupport);
}
Log.d(TAG, " ]");
......@@ -224,7 +225,7 @@ import java.util.Locale;
@Override
public void onAudioInputFormatChanged(Format format) {
Log.d(TAG, "audioFormatChanged [" + getSessionTimeString() + ", " + getFormatString(format)
Log.d(TAG, "audioFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format)
+ "]");
}
......@@ -254,7 +255,7 @@ import java.util.Locale;
@Override
public void onVideoInputFormatChanged(Format format) {
Log.d(TAG, "videoFormatChanged [" + getSessionTimeString() + ", " + getFormatString(format)
Log.d(TAG, "videoFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format)
+ "]");
}
......@@ -279,7 +280,7 @@ import java.util.Locale;
// Do nothing.
}
// StreamingDrmSessionManager.EventListener
// DefaultDrmSessionManager.EventListener
@Override
public void onDrmSessionManagerError(Exception e) {
......@@ -287,6 +288,16 @@ import java.util.Locale;
}
@Override
public void onDrmKeysRestored() {
Log.d(TAG, "drmKeysRestored [" + getSessionTimeString() + "]");
}
@Override
public void onDrmKeysRemoved() {
Log.d(TAG, "drmKeysRemoved [" + getSessionTimeString() + "]");
}
@Override
public void onDrmKeysLoaded() {
Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]");
}
......@@ -349,10 +360,13 @@ import java.util.Locale;
private void printMetadata(Metadata metadata, String prefix) {
for (int i = 0; i < metadata.length(); i++) {
Metadata.Entry entry = metadata.get(i);
if (entry instanceof TxxxFrame) {
TxxxFrame txxxFrame = (TxxxFrame) entry;
Log.d(TAG, prefix + String.format("%s: description=%s, value=%s", txxxFrame.id,
txxxFrame.description, txxxFrame.value));
if (entry instanceof TextInformationFrame) {
TextInformationFrame textInformationFrame = (TextInformationFrame) entry;
Log.d(TAG, prefix + String.format("%s: value=%s", textInformationFrame.id,
textInformationFrame.value));
} else if (entry instanceof UrlLinkFrame) {
UrlLinkFrame urlLinkFrame = (UrlLinkFrame) entry;
Log.d(TAG, prefix + String.format("%s: url=%s", urlLinkFrame.id, urlLinkFrame.url));
} else if (entry instanceof PrivFrame) {
PrivFrame privFrame = (PrivFrame) entry;
Log.d(TAG, prefix + String.format("%s: owner=%s", privFrame.id, privFrame.owner));
......@@ -364,17 +378,17 @@ import java.util.Locale;
ApicFrame apicFrame = (ApicFrame) entry;
Log.d(TAG, prefix + String.format("%s: mimeType=%s, description=%s",
apicFrame.id, apicFrame.mimeType, apicFrame.description));
} else if (entry instanceof TextInformationFrame) {
TextInformationFrame textInformationFrame = (TextInformationFrame) entry;
Log.d(TAG, prefix + String.format("%s: description=%s", textInformationFrame.id,
textInformationFrame.description));
} else if (entry instanceof CommentFrame) {
CommentFrame commentFrame = (CommentFrame) entry;
Log.d(TAG, prefix + String.format("%s: language=%s description=%s", commentFrame.id,
Log.d(TAG, prefix + String.format("%s: language=%s, description=%s", commentFrame.id,
commentFrame.language, commentFrame.description));
} else if (entry instanceof Id3Frame) {
Id3Frame id3Frame = (Id3Frame) entry;
Log.d(TAG, prefix + String.format("%s", id3Frame.id));
} else if (entry instanceof EventMessage) {
EventMessage eventMessage = (EventMessage) entry;
Log.d(TAG, prefix + String.format("EMSG: scheme=%s, id=%d, value=%s",
eventMessage.schemeIdUri, eventMessage.id, eventMessage.value));
}
}
}
......@@ -433,33 +447,6 @@ import java.util.Locale;
}
}
private static String getFormatString(Format format) {
if (format == null) {
return "null";
}
StringBuilder builder = new StringBuilder();
builder.append("id=").append(format.id).append(", mimeType=").append(format.sampleMimeType);
if (format.bitrate != Format.NO_VALUE) {
builder.append(", bitrate=").append(format.bitrate);
}
if (format.width != Format.NO_VALUE && format.height != Format.NO_VALUE) {
builder.append(", res=").append(format.width).append("x").append(format.height);
}
if (format.frameRate != Format.NO_VALUE) {
builder.append(", fps=").append(format.frameRate);
}
if (format.channelCount != Format.NO_VALUE) {
builder.append(", channels=").append(format.channelCount);
}
if (format.sampleRate != Format.NO_VALUE) {
builder.append(", sample_rate=").append(format.sampleRate);
}
if (format.language != null) {
builder.append(", language=").append(format.language);
}
return builder.toString();
}
private static String getTrackStatusString(TrackSelection selection, TrackGroup group,
int trackIndex) {
return getTrackStatusString(selection != null && selection.getTrackGroup() == group
......
......@@ -36,15 +36,16 @@ import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.drm.StreamingDrmSessionManager;
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
import com.google.android.exoplayer2.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
......@@ -100,7 +101,6 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
}
private Handler mainHandler;
private Timeline.Window window;
private EventLogger eventLogger;
private SimpleExoPlayerView simpleExoPlayerView;
private LinearLayout debugRootView;
......@@ -115,9 +115,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
private boolean playerNeedsSource;
private boolean shouldAutoPlay;
private boolean isTimelineStatic;
private int playerWindow;
private long playerPosition;
private int resumeWindow;
private long resumePosition;
// Activity lifecycle
......@@ -125,9 +124,9 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
shouldAutoPlay = true;
clearResumePosition();
mediaDataSourceFactory = buildDataSourceFactory(true);
mainHandler = new Handler();
window = new Timeline.Window();
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
}
......@@ -148,7 +147,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
@Override
public void onNewIntent(Intent intent) {
releasePlayer();
isTimelineStatic = false;
shouldAutoPlay = true;
clearResumePosition();
setIntent(intent);
}
......@@ -264,7 +264,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
@SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode =
((DemoApplication) getApplication()).useExtensionRenderers()
? (preferExtensionDecoders ? SimpleExoPlayer.EXTENSION_RENDERER_MODE_PREFER
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_ON)
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_ON)
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF;
TrackSelection.Factory videoTrackSelectionFactory =
new AdaptiveVideoTrackSelection.Factory(BANDWIDTH_METER);
......@@ -278,16 +278,9 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
player.addListener(eventLogger);
player.setAudioDebugListener(eventLogger);
player.setVideoDebugListener(eventLogger);
player.setId3Output(eventLogger);
player.setMetadataOutput(eventLogger);
simpleExoPlayerView.setPlayer(player);
if (isTimelineStatic) {
if (playerPosition == C.TIME_UNSET) {
player.seekToDefaultPosition(playerWindow);
} else {
player.seekTo(playerWindow, playerPosition);
}
}
player.setPlayWhenReady(shouldAutoPlay);
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
debugViewHelper.start();
......@@ -324,7 +317,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
}
MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
: new ConcatenatingMediaSource(mediaSources);
player.prepare(mediaSource, !isTimelineStatic, !isTimelineStatic);
player.seekTo(resumeWindow, resumePosition);
player.prepare(mediaSource, false, false);
playerNeedsSource = false;
updateButtonVisibilities();
}
......@@ -358,7 +352,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
}
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl,
buildHttpDataSourceFactory(false), keyRequestProperties);
return new StreamingDrmSessionManager<>(uuid,
return new DefaultDrmSessionManager<>(uuid,
FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger);
}
......@@ -367,12 +361,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
debugViewHelper.stop();
debugViewHelper = null;
shouldAutoPlay = player.getPlayWhenReady();
playerWindow = player.getCurrentWindowIndex();
playerPosition = C.TIME_UNSET;
Timeline timeline = player.getCurrentTimeline();
if (!timeline.isEmpty() && timeline.getWindow(playerWindow, window).isSeekable) {
playerPosition = player.getCurrentPosition();
}
updateResumePosition();
player.release();
player = null;
trackSelector = null;
......@@ -381,6 +370,17 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
}
}
private void updateResumePosition() {
resumeWindow = player.getCurrentWindowIndex();
resumePosition = player.isCurrentWindowSeekable() ? Math.max(0, player.getCurrentPosition())
: C.TIME_UNSET;
}
private void clearResumePosition() {
resumeWindow = 0;
resumePosition = C.TIME_UNSET;
}
/**
* Returns a new DataSource factory.
*
......@@ -427,8 +427,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
isTimelineStatic = !timeline.isEmpty()
&& !timeline.getWindow(timeline.getWindowCount() - 1, window).isDynamic;
// Do nothing.
}
@Override
......@@ -460,6 +459,11 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
showToast(errorString);
}
playerNeedsSource = true;
if (isBehindLiveWindow(e)) {
clearResumePosition();
} else {
updateResumePosition();
}
updateButtonVisibilities();
showControls();
}
......@@ -535,4 +539,18 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
}
private static boolean isBehindLiveWindow(ExoPlaybackException e) {
if (e.type != ExoPlaybackException.TYPE_SOURCE) {
return false;
}
Throwable cause = e.getSourceException();
while (cause != null) {
if (cause instanceof BehindLiveWindowException) {
return true;
}
cause = cause.getCause();
}
return false;
}
}
......@@ -23,17 +23,6 @@ android {
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
lintOptions {
abortOnError false
}
sourceSets.main {
jniLibs.srcDirs = ['jniLibs']
}
......
......@@ -57,8 +57,8 @@ import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import org.chromium.net.CronetEngine;
import org.chromium.net.NetworkException;
import org.chromium.net.UrlRequest;
import org.chromium.net.UrlRequestException;
import org.chromium.net.UrlResponseInfo;
import org.chromium.net.impl.UrlResponseInfoImpl;
import org.junit.Before;
......@@ -99,7 +99,7 @@ public final class CronetDataSourceTest {
@Mock
private Executor mockExecutor;
@Mock
private UrlRequestException mockUrlRequestException;
private NetworkException mockNetworkException;
@Mock private CronetEngine mockCronetEngine;
private CronetDataSource dataSourceUnderTest;
......@@ -172,7 +172,7 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.onFailed(
mockUrlRequest,
testUrlResponseInfo,
mockUrlRequestException);
mockNetworkException);
dataSourceUnderTest.onResponseStarted(
mockUrlRequest2,
testUrlResponseInfo);
......@@ -245,8 +245,8 @@ public final class CronetDataSourceTest {
@Test
public void testRequestOpenFailDueToDnsFailure() {
mockResponseStartFailure();
when(mockUrlRequestException.getErrorCode()).thenReturn(
UrlRequestException.ERROR_HOSTNAME_NOT_RESOLVED);
when(mockNetworkException.getErrorCode()).thenReturn(
NetworkException.ERROR_HOSTNAME_NOT_RESOLVED);
try {
dataSourceUnderTest.open(testDataSpec);
......@@ -728,7 +728,7 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.onFailed(
mockUrlRequest,
createUrlResponseInfo(500), // statusCode
mockUrlRequestException);
mockNetworkException);
return null;
}
}).when(mockUrlRequest).start();
......@@ -764,7 +764,7 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.onFailed(
mockUrlRequest,
createUrlResponseInfo(500), // statusCode
mockUrlRequestException);
mockNetworkException);
return null;
}
}).when(mockUrlRequest).read(any(ByteBuffer.class));
......
......@@ -40,9 +40,10 @@ import java.util.concurrent.Executor;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.chromium.net.CronetEngine;
import org.chromium.net.CronetException;
import org.chromium.net.NetworkException;
import org.chromium.net.UrlRequest;
import org.chromium.net.UrlRequest.Status;
import org.chromium.net.UrlRequestException;
import org.chromium.net.UrlResponseInfo;
/**
......@@ -400,12 +401,17 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
@Override
public synchronized void onFailed(UrlRequest request, UrlResponseInfo info,
UrlRequestException error) {
CronetException error) {
if (request != currentUrlRequest) {
return;
}
exception = error.getErrorCode() == UrlRequestException.ERROR_HOSTNAME_NOT_RESOLVED
? new UnknownHostException() : error;
if (error instanceof NetworkException
&& ((NetworkException) error).getErrorCode()
== NetworkException.ERROR_HOSTNAME_NOT_RESOLVED) {
exception = new UnknownHostException();
} else {
exception = error;
}
operation.open();
}
......
......@@ -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.BaseFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Predicate;
......@@ -25,7 +26,7 @@ import org.chromium.net.CronetEngine;
/**
* A {@link Factory} that produces {@link CronetDataSource}.
*/
public final class CronetDataSourceFactory implements Factory {
public final class CronetDataSourceFactory extends BaseFactory {
/**
* The default connection timeout, in milliseconds.
......@@ -67,7 +68,7 @@ public final class CronetDataSourceFactory implements Factory {
}
@Override
public CronetDataSource createDataSource() {
protected CronetDataSource createDataSourceInternal() {
return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener,
connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects);
}
......
......@@ -20,17 +20,7 @@ android {
defaultConfig {
minSdkVersion 9
targetSdkVersion project.ext.targetSdkVersion
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
lintOptions {
abortOnError false
consumerProguardFiles 'proguard-rules.txt'
}
sourceSets.main {
......
......@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.ffmpeg;
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.AudioRendererEventListener;
......@@ -60,7 +61,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
}
@Override
public int supportsFormat(Format format) {
protected int supportsFormatInternal(Format format) {
if (!FfmpegLibrary.isAvailable()) {
return FORMAT_UNSUPPORTED_TYPE;
}
......@@ -70,6 +71,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
}
@Override
public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {
return ADAPTIVE_NOT_SEAMLESS;
}
@Override
protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
throws FfmpegDecoderException {
decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
......
......@@ -20,17 +20,7 @@ android {
defaultConfig {
minSdkVersion 9
targetSdkVersion project.ext.targetSdkVersion
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
lintOptions {
abortOnError false
consumerProguardFiles 'proguard-rules.txt'
}
sourceSets.main {
......
......@@ -56,7 +56,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
}
@Override
public int supportsFormat(Format format) {
protected int supportsFormatInternal(Format format) {
return FlacLibrary.isAvailable() && MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)
? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE;
}
......
......@@ -22,17 +22,6 @@ android {
minSdkVersion 9
targetSdkVersion project.ext.targetSdkVersion
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
lintOptions {
abortOnError false
}
}
dependencies {
......
......@@ -261,7 +261,7 @@ public class OkHttpDataSource implements HttpDataSource {
private Request makeRequest(DataSpec dataSpec) {
long position = dataSpec.position;
long length = dataSpec.length;
boolean allowGzip = (dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) != 0;
boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
HttpUrl url = HttpUrl.parse(dataSpec.uri.toString());
Request.Builder builder = new Request.Builder().url(url);
......
......@@ -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.BaseFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
import com.google.android.exoplayer2.upstream.TransferListener;
import okhttp3.CacheControl;
......@@ -24,7 +25,7 @@ import okhttp3.Call;
/**
* A {@link Factory} that produces {@link OkHttpDataSource}.
*/
public final class OkHttpDataSourceFactory implements Factory {
public final class OkHttpDataSourceFactory extends BaseFactory {
private final Call.Factory callFactory;
private final String userAgent;
......@@ -58,7 +59,7 @@ public final class OkHttpDataSourceFactory implements Factory {
}
@Override
public OkHttpDataSource createDataSource() {
protected OkHttpDataSource createDataSourceInternal() {
return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl);
}
......
......@@ -20,17 +20,7 @@ android {
defaultConfig {
minSdkVersion 9
targetSdkVersion project.ext.targetSdkVersion
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
lintOptions {
abortOnError false
consumerProguardFiles 'proguard-rules.txt'
}
sourceSets.main {
......
......@@ -72,7 +72,7 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
}
@Override
public int supportsFormat(Format format) {
protected int supportsFormatInternal(Format format) {
return OpusLibrary.isAvailable() && MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)
? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE;
}
......
......@@ -20,17 +20,7 @@ android {
defaultConfig {
minSdkVersion 9
targetSdkVersion project.ext.targetSdkVersion
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
lintOptions {
abortOnError false
consumerProguardFiles 'proguard-rules.txt'
}
sourceSets.main {
......
import com.android.builder.core.BuilderConstants
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
......@@ -13,6 +11,8 @@ import com.android.builder.core.BuilderConstants
// 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.
import com.android.builder.core.BuilderConstants
apply plugin: 'com.android.library'
apply plugin: 'bintray-release'
......@@ -28,13 +28,10 @@ android {
// greater.
minSdkVersion 9
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
// Re-enable test coverage when the following issue is fixed:
// https://code.google.com/p/android/issues/detail?id=226070
// debug {
......@@ -42,10 +39,6 @@ android {
// }
}
lintOptions {
abortOnError false
}
sourceSets {
androidTest {
java.srcDirs += "../testutils/src/main/java/"
......
# Accessed via reflection in SubtitleDecoderFactory.DEFAULT
-keepclassmembers class com.google.android.exoplayer2.text.cea.Cea608Decoder {
public <init>(java.lang.String, int);
}
-keepclassmembers class com.google.android.exoplayer2.text.cea.Cea708Decoder {
public <init>(int);
}
......@@ -59,8 +59,8 @@ public final class FormatTest extends TestCase {
DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2);
byte[] projectionData = new byte[] {1, 2, 3};
Metadata metadata = new Metadata(
new TextInformationFrame("id1", "description1"),
new TextInformationFrame("id2", "description2"));
new TextInformationFrame("id1", "description1", "value1"),
new TextInformationFrame("id2", "description2", "value2"));
Format formatToParcel = new Format("id", MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null,
1024, 2048, 1920, 1080, 24, 90, 2, projectionData, C.STEREO_MODE_TOP_BOTTOM, 6, 44100,
......
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.drm;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.when;
import android.test.InstrumentationTestCase;
import android.test.MoreAsserts;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
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.Period;
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 java.util.Arrays;
import java.util.HashMap;
import org.mockito.Mock;
/**
* Tests {@link OfflineLicenseHelper}.
*/
public class OfflineLicenseHelperTest extends InstrumentationTestCase {
private OfflineLicenseHelper<?> offlineLicenseHelper;
@Mock private HttpDataSource httpDataSource;
@Mock private MediaDrmCallback mediaDrmCallback;
@Mock private ExoMediaDrm<ExoMediaCrypto> mediaDrm;
@Override
protected void setUp() throws Exception {
TestUtil.setUpMockito(this);
when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3});
offlineLicenseHelper = new OfflineLicenseHelper<>(mediaDrm, mediaDrmCallback, null);
}
@Override
protected void tearDown() throws Exception {
offlineLicenseHelper.releaseResources();
}
public void testDownloadRenewReleaseKey() throws Exception {
DashManifest manifest = newDashManifestWithAllElements();
setStubLicenseAndPlaybackDurationValues(1000, 200);
byte[] keySetId = {2, 5, 8};
setStubKeySetId(keySetId);
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
assertOfflineLicenseKeySetIdEqual(keySetId, offlineLicenseKeySetId);
byte[] keySetId2 = {6, 7, 0, 1, 4};
setStubKeySetId(keySetId2);
byte[] offlineLicenseKeySetId2 = offlineLicenseHelper.renew(offlineLicenseKeySetId);
assertOfflineLicenseKeySetIdEqual(keySetId2, offlineLicenseKeySetId2);
offlineLicenseHelper.release(offlineLicenseKeySetId2);
}
public void testDownloadFailsIfThereIsNoInitData() throws Exception {
setDefaultStubValues();
DashManifest manifest =
newDashManifest(newPeriods(newAdaptationSets(newRepresentations(null /*no init data*/))));
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
assertNull(offlineLicenseKeySetId);
}
public void testDownloadFailsIfThereIsNoRepresentation() throws Exception {
setDefaultStubValues();
DashManifest manifest = newDashManifest(newPeriods(newAdaptationSets(/*no representation*/)));
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
assertNull(offlineLicenseKeySetId);
}
public void testDownloadFailsIfThereIsNoAdaptationSet() throws Exception {
setDefaultStubValues();
DashManifest manifest = newDashManifest(newPeriods(/*no adaptation set*/));
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
assertNull(offlineLicenseKeySetId);
}
public void testDownloadFailsIfThereIsNoPeriod() throws Exception {
setDefaultStubValues();
DashManifest manifest = newDashManifest(/*no periods*/);
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
assertNull(offlineLicenseKeySetId);
}
public void testDownloadFailsIfNoKeySetIdIsReturned() throws Exception {
setStubLicenseAndPlaybackDurationValues(1000, 200);
DashManifest manifest = newDashManifestWithAllElements();
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
assertNull(offlineLicenseKeySetId);
}
public void testDownloadDoesNotFailIfDurationNotAvailable() throws Exception {
setDefaultStubKeySetId();
DashManifest manifest = newDashManifestWithAllElements();
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
assertNotNull(offlineLicenseKeySetId);
}
public void testGetLicenseDurationRemainingSec() throws Exception {
long licenseDuration = 1000;
int playbackDuration = 200;
setStubLicenseAndPlaybackDurationValues(licenseDuration, playbackDuration);
setDefaultStubKeySetId();
DashManifest manifest = newDashManifestWithAllElements();
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
Pair<Long, Long> licenseDurationRemainingSec = offlineLicenseHelper
.getLicenseDurationRemainingSec(offlineLicenseKeySetId);
assertEquals(licenseDuration, (long) licenseDurationRemainingSec.first);
assertEquals(playbackDuration, (long) licenseDurationRemainingSec.second);
}
public void testGetLicenseDurationRemainingSecExpiredLicense() throws Exception {
long licenseDuration = 0;
int playbackDuration = 0;
setStubLicenseAndPlaybackDurationValues(licenseDuration, playbackDuration);
setDefaultStubKeySetId();
DashManifest manifest = newDashManifestWithAllElements();
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
Pair<Long, Long> licenseDurationRemainingSec = offlineLicenseHelper
.getLicenseDurationRemainingSec(offlineLicenseKeySetId);
assertEquals(licenseDuration, (long) licenseDurationRemainingSec.first);
assertEquals(playbackDuration, (long) licenseDurationRemainingSec.second);
}
private void setDefaultStubValues()
throws android.media.NotProvisionedException, android.media.DeniedByServerException {
setDefaultStubKeySetId();
setStubLicenseAndPlaybackDurationValues(1000, 200);
}
private void setDefaultStubKeySetId()
throws android.media.NotProvisionedException, android.media.DeniedByServerException {
setStubKeySetId(new byte[] {2, 5, 8});
}
private void setStubKeySetId(byte[] keySetId)
throws android.media.NotProvisionedException, android.media.DeniedByServerException {
when(mediaDrm.provideKeyResponse(any(byte[].class), any(byte[].class))).thenReturn(keySetId);
}
private static void assertOfflineLicenseKeySetIdEqual(
byte[] expectedKeySetId, byte[] actualKeySetId) throws Exception {
assertNotNull(actualKeySetId);
MoreAsserts.assertEquals(expectedKeySetId, actualKeySetId);
}
private void setStubLicenseAndPlaybackDurationValues(long licenseDuration,
long playbackDuration) {
HashMap<String, String> keyStatus = new HashMap<>();
keyStatus.put(WidevineUtil.PROPERTY_LICENSE_DURATION_REMAINING,
String.valueOf(licenseDuration));
keyStatus.put(WidevineUtil.PROPERTY_PLAYBACK_DURATION_REMAINING,
String.valueOf(playbackDuration));
when(mediaDrm.queryKeyStatus(any(byte[].class))).thenReturn(keyStatus);
}
private static DashManifest newDashManifestWithAllElements() {
return newDashManifest(newPeriods(newAdaptationSets(newRepresentations(newDrmInitData()))));
}
private static DashManifest newDashManifest(Period... periods) {
return new DashManifest(0, 0, 0, false, 0, 0, 0, null, null, Arrays.asList(periods));
}
private static Period newPeriods(AdaptationSet... adaptationSets) {
return new Period("", 0, Arrays.asList(adaptationSets));
}
private static AdaptationSet newAdaptationSets(Representation... representations) {
return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations));
}
private static Representation newRepresentations(DrmInitData drmInitData) {
Format format = Format.createVideoSampleFormat("", "", "", 0, 0, 0, 0, 0, null, drmInitData);
return Representation.newInstance("", 0, format, "", new SingleSegmentBase());
}
private static DrmInitData newDrmInitData() {
return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType",
new byte[]{1, 4, 7, 0, 3, 6}));
}
}
/*
* 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.metadata.emsg;
import android.test.MoreAsserts;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import java.nio.ByteBuffer;
import junit.framework.TestCase;
/**
* Test for {@link EventMessageDecoder}.
*/
public final class EventMessageDecoderTest extends TestCase {
public void testDecodeEventMessage() {
byte[] rawEmsgBody = new byte[] {
117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test"
49, 50, 51, 0, // value = "123"
0, 0, -69, -128, // timescale = 48000
0, 0, 0, 0, // presentation_time_delta (ignored) = 0
0, 2, 50, -128, // event_duration = 144000
0, 15, 67, -45, // id = 1000403
0, 1, 2, 3, 4}; // message_data = {0, 1, 2, 3, 4}
EventMessageDecoder decoder = new EventMessageDecoder();
MetadataInputBuffer buffer = new MetadataInputBuffer();
buffer.data = ByteBuffer.allocate(rawEmsgBody.length).put(rawEmsgBody);
Metadata metadata = decoder.decode(buffer);
assertEquals(1, metadata.length());
EventMessage eventMessage = (EventMessage) metadata.get(0);
assertEquals("urn:test", eventMessage.schemeIdUri);
assertEquals("123", eventMessage.value);
assertEquals(3000, eventMessage.durationMs);
assertEquals(1000403, eventMessage.id);
MoreAsserts.assertEquals(new byte[] {0, 1, 2, 3, 4}, eventMessage.messageData);
}
}
/*
* 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.metadata.emsg;
import android.os.Parcel;
import junit.framework.TestCase;
/**
* Test for {@link EventMessage}.
*/
public final class EventMessageTest extends TestCase {
public void testEventMessageParcelable() {
EventMessage eventMessage = new EventMessage("urn:test", "123", 3000, 1000403,
new byte[] {0, 1, 2, 3, 4});
// Write to parcel.
Parcel parcel = Parcel.obtain();
eventMessage.writeToParcel(parcel, 0);
// Create from parcel.
parcel.setDataPosition(0);
EventMessage fromParcelEventMessage = EventMessage.CREATOR.createFromParcel(parcel);
// Assert equals.
assertEquals(eventMessage, fromParcelEventMessage);
}
}
/*
* 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.metadata.id3;
import android.os.Parcel;
import junit.framework.TestCase;
/**
* Test for {@link ChapterFrame}.
*/
public final class ChapterFrameTest extends TestCase {
public void testParcelable() {
Id3Frame[] subFrames = new Id3Frame[] {
new TextInformationFrame("TIT2", null, "title"),
new UrlLinkFrame("WXXX", "description", "url")
};
ChapterFrame chapterFrameToParcel = new ChapterFrame("id", 0, 1, 2, 3, subFrames);
Parcel parcel = Parcel.obtain();
chapterFrameToParcel.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
ChapterFrame chapterFrameFromParcel = ChapterFrame.CREATOR.createFromParcel(parcel);
assertEquals(chapterFrameToParcel, chapterFrameFromParcel);
parcel.recycle();
}
}
/*
* 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.metadata.id3;
import android.os.Parcel;
import junit.framework.TestCase;
/**
* Test for {@link ChapterTocFrame}.
*/
public final class ChapterTocFrameTest extends TestCase {
public void testParcelable() {
String[] children = new String[] {"child0", "child1"};
Id3Frame[] subFrames = new Id3Frame[] {
new TextInformationFrame("TIT2", null, "title"),
new UrlLinkFrame("WXXX", "description", "url")
};
ChapterTocFrame chapterTocFrameToParcel = new ChapterTocFrame("id", false, true, children,
subFrames);
Parcel parcel = Parcel.obtain();
chapterTocFrameToParcel.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
ChapterTocFrame chapterTocFrameFromParcel = ChapterTocFrame.CREATOR.createFromParcel(parcel);
assertEquals(chapterTocFrameToParcel, chapterTocFrameFromParcel);
parcel.recycle();
}
}
......@@ -21,9 +21,9 @@ import com.google.android.exoplayer2.metadata.MetadataDecoderException;
import junit.framework.TestCase;
/**
* Test for {@link Id3Decoder}
* Test for {@link Id3Decoder}.
*/
public class Id3DecoderTest extends TestCase {
public final class Id3DecoderTest extends TestCase {
public void testDecodeTxxxFrame() throws MetadataDecoderException {
byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 41, 84, 88, 88, 88, 0, 0, 0, 31, 0, 0,
......@@ -32,9 +32,10 @@ public class Id3DecoderTest extends TestCase {
Id3Decoder decoder = new Id3Decoder();
Metadata metadata = decoder.decode(rawId3, rawId3.length);
assertEquals(1, metadata.length());
TxxxFrame txxxFrame = (TxxxFrame) metadata.get(0);
assertEquals("", txxxFrame.description);
assertEquals("mdialog_VINDICO1527664_start", txxxFrame.value);
TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0);
assertEquals("TXXX", textInformationFrame.id);
assertEquals("", textInformationFrame.description);
assertEquals("mdialog_VINDICO1527664_start", textInformationFrame.value);
}
public void testDecodeApicFrame() throws MetadataDecoderException {
......@@ -60,7 +61,19 @@ public class Id3DecoderTest extends TestCase {
assertEquals(1, metadata.length());
TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0);
assertEquals("TIT2", textInformationFrame.id);
assertEquals("Hello World", textInformationFrame.description);
assertNull(textInformationFrame.description);
assertEquals("Hello World", textInformationFrame.value);
}
public void testDecodePrivFrame() throws MetadataDecoderException {
byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 19, 80, 82, 73, 86, 0, 0, 0, 9, 0, 0,
116, 101, 115, 116, 0, 1, 2, 3, 4};
Id3Decoder decoder = new Id3Decoder();
Metadata metadata = decoder.decode(rawId3, rawId3.length);
assertEquals(1, metadata.length());
PrivFrame privFrame = (PrivFrame) metadata.get(0);
assertEquals("test", privFrame.owner);
MoreAsserts.assertEquals(new byte[] {1, 2, 3, 4}, privFrame.privateData);
}
}
......@@ -29,13 +29,13 @@ public class RepresentationTest extends TestCase {
String uri = "http://www.google.com";
SegmentBase base = new SingleSegmentBase(new RangedUri(null, 0, 1), 1, 0, 1, 1);
Format format = Format.createVideoContainerFormat("0", MimeTypes.APPLICATION_MP4, null,
MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null);
MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null, 0);
Representation representation = Representation.newInstance("test_stream_1", 3, format, uri,
base);
assertEquals("test_stream_1.0.3", representation.getCacheKey());
format = Format.createVideoContainerFormat("150", MimeTypes.APPLICATION_MP4, null,
MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null);
MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null, 0);
representation = Representation.newInstance("test_stream_1", Representation.REVISION_ID_DEFAULT,
format, uri, base);
assertEquals("test_stream_1.150.-1", representation.getCacheKey());
......
......@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.hls.playlist;
import android.net.Uri;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.Charset;
......@@ -29,70 +30,86 @@ import junit.framework.TestCase;
*/
public class HlsMasterPlaylistParserTest extends TestCase {
public void testParseMasterPlaylist() {
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
String playlistString = "#EXTM3U\n"
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+ "http://example.com/low.m3u8\n"
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n"
+ "http://example.com/spaces_in_codecs.m3u8\n"
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=384x160\n"
+ "http://example.com/mid.m3u8\n"
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=7680000\n"
+ "http://example.com/hi.m3u8\n"
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n"
+ "http://example.com/audio-only.m3u8";
ByteArrayInputStream inputStream = new ByteArrayInputStream(
playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
private static final String PLAYLIST_URI = "https://example.com/test.m3u8";
private static final String MASTER_PLAYLIST = " #EXTM3U \n"
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+ "http://example.com/low.m3u8\n"
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n"
+ "http://example.com/spaces_in_codecs.m3u8\n"
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=384x160\n"
+ "http://example.com/mid.m3u8\n"
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=7680000\n"
+ "http://example.com/hi.m3u8\n"
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n"
+ "http://example.com/audio-only.m3u8";
private static final String PLAYLIST_WITH_INVALID_HEADER = "#EXTMU3\n"
+ "#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);
HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist;
List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
assertNotNull(variants);
assertEquals(5, variants.size());
assertEquals(1280000, variants.get(0).format.bitrate);
assertNotNull(variants.get(0).format.codecs);
assertEquals("mp4a.40.2,avc1.66.30", variants.get(0).format.codecs);
assertEquals(304, variants.get(0).format.width);
assertEquals(128, variants.get(0).format.height);
assertEquals("http://example.com/low.m3u8", variants.get(0).url);
assertEquals(1280000, variants.get(1).format.bitrate);
assertNotNull(variants.get(1).format.codecs);
assertEquals("mp4a.40.2 , avc1.66.30 ", variants.get(1).format.codecs);
assertEquals("http://example.com/spaces_in_codecs.m3u8", variants.get(1).url);
assertEquals(2560000, variants.get(2).format.bitrate);
assertEquals(null, variants.get(2).format.codecs);
assertEquals(384, variants.get(2).format.width);
assertEquals(160, variants.get(2).format.height);
assertEquals("http://example.com/mid.m3u8", variants.get(2).url);
assertEquals(7680000, variants.get(3).format.bitrate);
assertEquals(null, variants.get(3).format.codecs);
assertEquals(Format.NO_VALUE, variants.get(3).format.width);
assertEquals(Format.NO_VALUE, variants.get(3).format.height);
assertEquals("http://example.com/hi.m3u8", variants.get(3).url);
assertEquals(65000, variants.get(4).format.bitrate);
assertNotNull(variants.get(4).format.codecs);
assertEquals("mp4a.40.5", variants.get(4).format.codecs);
assertEquals(Format.NO_VALUE, variants.get(4).format.width);
assertEquals(Format.NO_VALUE, variants.get(4).format.height);
assertEquals("http://example.com/audio-only.m3u8", variants.get(4).url);
}
public void testPlaylistWithInvalidHeader() throws IOException {
try {
HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream);
assertNotNull(playlist);
assertEquals(HlsPlaylist.TYPE_MASTER, playlist.type);
HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist;
List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
assertNotNull(variants);
assertEquals(5, variants.size());
assertEquals(1280000, variants.get(0).format.bitrate);
assertNotNull(variants.get(0).format.codecs);
assertEquals("mp4a.40.2,avc1.66.30", variants.get(0).format.codecs);
assertEquals(304, variants.get(0).format.width);
assertEquals(128, variants.get(0).format.height);
assertEquals("http://example.com/low.m3u8", variants.get(0).url);
assertEquals(1280000, variants.get(1).format.bitrate);
assertNotNull(variants.get(1).format.codecs);
assertEquals("mp4a.40.2 , avc1.66.30 ", variants.get(1).format.codecs);
assertEquals("http://example.com/spaces_in_codecs.m3u8", variants.get(1).url);
assertEquals(2560000, variants.get(2).format.bitrate);
assertEquals(null, variants.get(2).format.codecs);
assertEquals(384, variants.get(2).format.width);
assertEquals(160, variants.get(2).format.height);
assertEquals("http://example.com/mid.m3u8", variants.get(2).url);
assertEquals(7680000, variants.get(3).format.bitrate);
assertEquals(null, variants.get(3).format.codecs);
assertEquals(Format.NO_VALUE, variants.get(3).format.width);
assertEquals(Format.NO_VALUE, variants.get(3).format.height);
assertEquals("http://example.com/hi.m3u8", variants.get(3).url);
assertEquals(65000, variants.get(4).format.bitrate);
assertNotNull(variants.get(4).format.codecs);
assertEquals("mp4a.40.5", variants.get(4).format.codecs);
assertEquals(Format.NO_VALUE, variants.get(4).format.width);
assertEquals(Format.NO_VALUE, variants.get(4).format.height);
assertEquals("http://example.com/audio-only.m3u8", variants.get(4).url);
} catch (IOException exception) {
fail(exception.getMessage());
parsePlaylist(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 {
Uri playlistUri = Uri.parse(uri);
ByteArrayInputStream inputStream = new ByteArrayInputStream(
playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
return new HlsPlaylistParser().parse(playlistUri, inputStream);
}
}
......@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls.playlist;
import android.net.Uri;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
......@@ -73,59 +74,64 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals(2679, mediaPlaylist.mediaSequence);
assertEquals(3, mediaPlaylist.version);
assertEquals(true, mediaPlaylist.hasEndTag);
List<HlsMediaPlaylist.Segment> segments = mediaPlaylist.segments;
assertTrue(mediaPlaylist.hasEndTag);
List<Segment> segments = mediaPlaylist.segments;
assertNotNull(segments);
assertEquals(5, segments.size());
assertEquals(4, segments.get(0).discontinuitySequenceNumber);
assertEquals(7975000, segments.get(0).durationUs);
assertEquals(false, segments.get(0).isEncrypted);
assertEquals(null, segments.get(0).encryptionKeyUri);
assertEquals(null, segments.get(0).encryptionIV);
assertEquals(51370, segments.get(0).byterangeLength);
assertEquals(0, segments.get(0).byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2679.ts", segments.get(0).url);
Segment segment = segments.get(0);
assertEquals(4, mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence);
assertEquals(7975000, segment.durationUs);
assertFalse(segment.isEncrypted);
assertEquals(null, segment.encryptionKeyUri);
assertEquals(null, segment.encryptionIV);
assertEquals(51370, segment.byterangeLength);
assertEquals(0, segment.byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2679.ts", segment.url);
assertEquals(4, segments.get(1).discontinuitySequenceNumber);
assertEquals(7975000, segments.get(1).durationUs);
assertEquals(true, segments.get(1).isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2680", segments.get(1).encryptionKeyUri);
assertEquals("0x1566B", segments.get(1).encryptionIV);
assertEquals(51501, segments.get(1).byterangeLength);
assertEquals(2147483648L, segments.get(1).byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2680.ts", segments.get(1).url);
segment = segments.get(1);
assertEquals(0, segment.relativeDiscontinuitySequence);
assertEquals(7975000, segment.durationUs);
assertTrue(segment.isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2680", segment.encryptionKeyUri);
assertEquals("0x1566B", segment.encryptionIV);
assertEquals(51501, segment.byterangeLength);
assertEquals(2147483648L, segment.byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2680.ts", segment.url);
assertEquals(4, segments.get(2).discontinuitySequenceNumber);
assertEquals(7941000, segments.get(2).durationUs);
assertEquals(false, segments.get(2).isEncrypted);
assertEquals(null, segments.get(2).encryptionKeyUri);
assertEquals(null, segments.get(2).encryptionIV);
assertEquals(51501, segments.get(2).byterangeLength);
assertEquals(2147535149L, segments.get(2).byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2681.ts", segments.get(2).url);
segment = segments.get(2);
assertEquals(0, segment.relativeDiscontinuitySequence);
assertEquals(7941000, segment.durationUs);
assertFalse(segment.isEncrypted);
assertEquals(null, segment.encryptionKeyUri);
assertEquals(null, segment.encryptionIV);
assertEquals(51501, segment.byterangeLength);
assertEquals(2147535149L, segment.byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2681.ts", segment.url);
assertEquals(5, segments.get(3).discontinuitySequenceNumber);
assertEquals(7975000, segments.get(3).durationUs);
assertEquals(true, segments.get(3).isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2682", segments.get(3).encryptionKeyUri);
segment = segments.get(3);
assertEquals(1, segment.relativeDiscontinuitySequence);
assertEquals(7975000, segment.durationUs);
assertTrue(segment.isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2682", segment.encryptionKeyUri);
// 0xA7A == 2682.
assertNotNull(segments.get(3).encryptionIV);
assertEquals("A7A", segments.get(3).encryptionIV.toUpperCase(Locale.getDefault()));
assertEquals(51740, segments.get(3).byterangeLength);
assertEquals(2147586650L, segments.get(3).byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2682.ts", segments.get(3).url);
assertNotNull(segment.encryptionIV);
assertEquals("A7A", segment.encryptionIV.toUpperCase(Locale.getDefault()));
assertEquals(51740, segment.byterangeLength);
assertEquals(2147586650L, segment.byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2682.ts", segment.url);
assertEquals(5, segments.get(4).discontinuitySequenceNumber);
assertEquals(7975000, segments.get(4).durationUs);
assertEquals(true, segments.get(4).isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2682", segments.get(4).encryptionKeyUri);
segment = segments.get(4);
assertEquals(1, segment.relativeDiscontinuitySequence);
assertEquals(7975000, segment.durationUs);
assertTrue(segment.isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2682", segment.encryptionKeyUri);
// 0xA7B == 2683.
assertNotNull(segments.get(4).encryptionIV);
assertEquals("A7B", segments.get(4).encryptionIV.toUpperCase(Locale.getDefault()));
assertEquals(C.LENGTH_UNSET, segments.get(4).byterangeLength);
assertEquals(0, segments.get(4).byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2683.ts", segments.get(4).url);
assertNotNull(segment.encryptionIV);
assertEquals("A7B", segment.encryptionIV.toUpperCase(Locale.getDefault()));
assertEquals(C.LENGTH_UNSET, segment.byterangeLength);
assertEquals(0, segment.byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2683.ts", segment.url);
} catch (IOException exception) {
fail(exception.getMessage());
}
......
......@@ -27,7 +27,9 @@ import java.io.File;
import java.io.IOException;
import java.util.Arrays;
/** Unit tests for {@link CacheDataSource}. */
/**
* Unit tests for {@link CacheDataSource}.
*/
public class CacheDataSourceTest extends InstrumentationTestCase {
private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
......
/*
* 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.upstream.cache;
import android.content.Context;
import android.net.Uri;
import android.test.AndroidTestCase;
import android.test.MoreAsserts;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DataSink;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.upstream.crypto.AesCipherDataSink;
import com.google.android.exoplayer2.upstream.crypto.AesCipherDataSource;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Random;
/**
* Additional tests for {@link CacheDataSource}.
*/
public class CacheDataSourceTest2 extends AndroidTestCase {
private static final String EXO_CACHE_DIR = "exo";
private static final int EXO_CACHE_MAX_FILESIZE = 128;
private static final Uri URI = Uri.parse("http://test.com/content");
private static final String KEY = "key";
private static final byte[] DATA = TestUtil.buildTestData(8 * EXO_CACHE_MAX_FILESIZE + 1);
// A DataSpec that covers the full file.
private static final DataSpec FULL = new DataSpec(URI, 0, DATA.length, KEY);
private static final int OFFSET_ON_BOUNDARY = EXO_CACHE_MAX_FILESIZE;
// A DataSpec that starts at 0 and extends to a cache file boundary.
private static final DataSpec END_ON_BOUNDARY = new DataSpec(URI, 0, OFFSET_ON_BOUNDARY, KEY);
// A DataSpec that starts on the same boundary and extends to the end of the file.
private static final DataSpec START_ON_BOUNDARY = new DataSpec(URI, OFFSET_ON_BOUNDARY,
DATA.length - OFFSET_ON_BOUNDARY, KEY);
private static final int OFFSET_OFF_BOUNDARY = EXO_CACHE_MAX_FILESIZE * 2 + 1;
// A DataSpec that starts at 0 and extends to just past a cache file boundary.
private static final DataSpec END_OFF_BOUNDARY = new DataSpec(URI, 0, OFFSET_OFF_BOUNDARY, KEY);
// A DataSpec that starts on the same boundary and extends to the end of the file.
private static final DataSpec START_OFF_BOUNDARY = new DataSpec(URI, OFFSET_OFF_BOUNDARY,
DATA.length - OFFSET_OFF_BOUNDARY, KEY);
public void testWithoutEncryption() throws IOException {
testReads(false);
}
public void testWithEncryption() throws IOException {
testReads(true);
}
private void testReads(boolean useEncryption) throws IOException {
FakeDataSource upstreamSource = buildFakeUpstreamSource();
CacheDataSource source = buildCacheDataSource(getContext(), upstreamSource, useEncryption);
// First read, should arrive from upstream.
testRead(END_ON_BOUNDARY, source);
assertSingleOpen(upstreamSource, 0, OFFSET_ON_BOUNDARY);
// Second read, should arrive from upstream.
testRead(START_OFF_BOUNDARY, source);
assertSingleOpen(upstreamSource, OFFSET_OFF_BOUNDARY, DATA.length);
// Second read, should arrive part from cache and part from upstream.
testRead(END_OFF_BOUNDARY, source);
assertSingleOpen(upstreamSource, OFFSET_ON_BOUNDARY, OFFSET_OFF_BOUNDARY);
// Third read, should arrive from cache.
testRead(FULL, source);
assertNoOpen(upstreamSource);
// Various reads, should all arrive from cache.
testRead(FULL, source);
assertNoOpen(upstreamSource);
testRead(START_ON_BOUNDARY, source);
assertNoOpen(upstreamSource);
testRead(END_ON_BOUNDARY, source);
assertNoOpen(upstreamSource);
testRead(START_OFF_BOUNDARY, source);
assertNoOpen(upstreamSource);
testRead(END_OFF_BOUNDARY, source);
assertNoOpen(upstreamSource);
}
private void testRead(DataSpec dataSpec, CacheDataSource source) throws IOException {
byte[] scratch = new byte[4096];
Random random = new Random(0);
source.open(dataSpec);
int position = (int) dataSpec.absoluteStreamPosition;
int bytesRead = 0;
while (bytesRead != C.RESULT_END_OF_INPUT) {
int maxBytesToRead = random.nextInt(scratch.length) + 1;
bytesRead = source.read(scratch, 0, maxBytesToRead);
if (bytesRead != C.RESULT_END_OF_INPUT) {
MoreAsserts.assertEquals(Arrays.copyOfRange(DATA, position, position + bytesRead),
Arrays.copyOf(scratch, bytesRead));
position += bytesRead;
}
}
source.close();
}
/**
* Asserts that a single {@link DataSource#open(DataSpec)} call has been made to the upstream
* source, with the specified start (inclusive) and end (exclusive) positions.
*/
private void assertSingleOpen(FakeDataSource upstreamSource, int start, int end) {
DataSpec[] openedDataSpecs = upstreamSource.getAndClearOpenedDataSpecs();
assertEquals(1, openedDataSpecs.length);
assertEquals(start, openedDataSpecs[0].position);
assertEquals(start, openedDataSpecs[0].absoluteStreamPosition);
assertEquals(end - start, openedDataSpecs[0].length);
}
/**
* Asserts that the upstream source was not opened.
*/
private void assertNoOpen(FakeDataSource upstreamSource) {
DataSpec[] openedDataSpecs = upstreamSource.getAndClearOpenedDataSpecs();
assertEquals(0, openedDataSpecs.length);
}
private static FakeDataSource buildFakeUpstreamSource() {
return new FakeDataSource.Builder().appendReadData(DATA).build();
}
private static CacheDataSource buildCacheDataSource(Context context, DataSource upstreamSource,
boolean useAesEncryption) throws CacheException {
File cacheDir = context.getExternalCacheDir();
Cache cache = new SimpleCache(new File(cacheDir, EXO_CACHE_DIR), new NoOpCacheEvictor());
emptyCache(cache);
// Source and cipher
final String secretKey = "testKey:12345678";
DataSource file = new FileDataSource();
DataSource cacheReadDataSource = useAesEncryption
? new AesCipherDataSource(Util.getUtf8Bytes(secretKey), file) : file;
// Sink and cipher
CacheDataSink cacheSink = new CacheDataSink(cache, EXO_CACHE_MAX_FILESIZE);
byte[] scratch = new byte[3897];
DataSink cacheWriteDataSink = useAesEncryption
? new AesCipherDataSink(Util.getUtf8Bytes(secretKey), cacheSink, scratch) : cacheSink;
return new CacheDataSource(cache,
upstreamSource,
cacheReadDataSource,
cacheWriteDataSink,
CacheDataSource.FLAG_BLOCK_ON_CACHE,
null); // eventListener
}
private static void emptyCache(Cache cache) throws CacheException {
for (String key : cache.getKeys()) {
for (CacheSpan span : cache.getCachedSpans(key)) {
cache.removeSpan(span);
}
}
// Sanity check that the cache really is empty now.
assertTrue(cache.getKeys().isEmpty());
}
}
......@@ -163,7 +163,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
public void testEncryption() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
byte[] key2 = "bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
new CachedContentIndex(cacheDir, key));
......@@ -181,7 +181,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
// Assert file content is different
FileInputStream fis1 = new FileInputStream(file1);
FileInputStream fis2 = new FileInputStream(file2);
for (int b; (b = fis1.read()) == fis2.read();) {
for (int b; (b = fis1.read()) == fis2.read(); ) {
assertTrue(b != -1);
}
......@@ -205,6 +205,12 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
// Non encrypted index file can be read even when encryption key provided.
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir),
new CachedContentIndex(cacheDir, key));
// Test multiple store() calls
CachedContentIndex index = new CachedContentIndex(cacheDir, key);
index.addNew(new CachedContent(15, "key3", 110));
index.store();
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir, key));
}
private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentIndex index2)
......
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.upstream.cache;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.ChunkIndex;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.File;
import java.io.IOException;
import org.mockito.Mock;
/**
* Tests for {@link CachedRegionTracker}.
*/
public final class CachedRegionTrackerTest extends InstrumentationTestCase {
private static final String CACHE_KEY = "abc";
private static final long MS_IN_US = 1000;
// 5 chunks, each 20 bytes long and 100 ms long.
private static final ChunkIndex CHUNK_INDEX = new ChunkIndex(
new int[] {20, 20, 20, 20, 20},
new long[] {100, 120, 140, 160, 180},
new long[] {100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US},
new long[] {0, 100 * MS_IN_US, 200 * MS_IN_US, 300 * MS_IN_US, 400 * MS_IN_US});
@Mock private Cache cache;
private CachedRegionTracker tracker;
private CachedContentIndex index;
private File cacheDir;
@Override
protected void setUp() throws Exception {
TestUtil.setUpMockito(this);
tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX);
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
index = new CachedContentIndex(cacheDir);
}
@Override
protected void tearDown() throws Exception {
TestUtil.recursiveDelete(cacheDir);
}
public void testGetRegion_noSpansInCache() {
assertEquals(CachedRegionTracker.NOT_CACHED, tracker.getRegionEndTimeMs(100));
assertEquals(CachedRegionTracker.NOT_CACHED, tracker.getRegionEndTimeMs(150));
}
public void testGetRegion_fullyCached() throws Exception {
tracker.onSpanAdded(
cache,
newCacheSpan(100, 100));
assertEquals(CachedRegionTracker.CACHED_TO_END, tracker.getRegionEndTimeMs(101));
assertEquals(CachedRegionTracker.CACHED_TO_END, tracker.getRegionEndTimeMs(121));
}
public void testGetRegion_partiallyCached() throws Exception {
tracker.onSpanAdded(
cache,
newCacheSpan(100, 40));
assertEquals(200, tracker.getRegionEndTimeMs(101));
assertEquals(200, tracker.getRegionEndTimeMs(121));
}
public void testGetRegion_multipleSpanAddsJoinedCorrectly() throws Exception {
tracker.onSpanAdded(
cache,
newCacheSpan(100, 20));
tracker.onSpanAdded(
cache,
newCacheSpan(120, 20));
assertEquals(200, tracker.getRegionEndTimeMs(101));
assertEquals(200, tracker.getRegionEndTimeMs(121));
}
public void testGetRegion_fullyCachedThenPartiallyRemoved() throws Exception {
// Start with the full stream in cache.
tracker.onSpanAdded(
cache,
newCacheSpan(100, 100));
// Remove the middle bit.
tracker.onSpanRemoved(
cache,
newCacheSpan(140, 40));
assertEquals(200, tracker.getRegionEndTimeMs(101));
assertEquals(200, tracker.getRegionEndTimeMs(121));
assertEquals(CachedRegionTracker.CACHED_TO_END, tracker.getRegionEndTimeMs(181));
}
public void testGetRegion_subchunkEstimation() throws Exception {
tracker.onSpanAdded(
cache,
newCacheSpan(100, 10));
assertEquals(50, tracker.getRegionEndTimeMs(101));
assertEquals(CachedRegionTracker.NOT_CACHED, tracker.getRegionEndTimeMs(111));
}
private CacheSpan newCacheSpan(int position, int length) throws IOException {
return SimpleCacheSpanTest.createCacheSpan(index, cacheDir, CACHE_KEY, position, length, 0);
}
}
......@@ -16,12 +16,16 @@
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;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.NavigableSet;
import java.util.Random;
import java.util.Set;
/**
......@@ -46,9 +50,9 @@ public class SimpleCacheTest extends InstrumentationTestCase {
public void testCommittingOneFile() throws Exception {
SimpleCache simpleCache = getSimpleCache();
CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0);
assertFalse(cacheSpan.isCached);
assertTrue(cacheSpan.isOpenEnded());
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
assertFalse(cacheSpan1.isCached);
assertTrue(cacheSpan1.isOpenEnded());
assertNull(simpleCache.startReadWriteNonBlocking(KEY_1, 0));
......@@ -58,20 +62,33 @@ public class SimpleCacheTest extends InstrumentationTestCase {
assertEquals(0, simpleCache.getCacheSpace());
assertEquals(0, cacheDir.listFiles().length);
addCache(simpleCache, 0, 15);
addCache(simpleCache, KEY_1, 0, 15);
Set<String> cachedKeys = simpleCache.getKeys();
assertEquals(1, cachedKeys.size());
assertTrue(cachedKeys.contains(KEY_1));
cachedSpans = simpleCache.getCachedSpans(KEY_1);
assertEquals(1, cachedSpans.size());
assertTrue(cachedSpans.contains(cacheSpan));
assertTrue(cachedSpans.contains(cacheSpan1));
assertEquals(15, simpleCache.getCacheSpace());
cacheSpan = simpleCache.startReadWrite(KEY_1, 0);
assertTrue(cacheSpan.isCached);
assertFalse(cacheSpan.isOpenEnded());
assertEquals(15, cacheSpan.length);
simpleCache.releaseHoleSpan(cacheSpan1);
CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);
assertTrue(cacheSpan2.isCached);
assertFalse(cacheSpan2.isOpenEnded());
assertEquals(15, cacheSpan2.length);
assertCachedDataReadCorrect(cacheSpan2);
}
public void testReadCacheWithoutReleasingWriteCacheSpan() throws Exception {
SimpleCache simpleCache = getSimpleCache();
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15);
CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);
assertCachedDataReadCorrect(cacheSpan2);
simpleCache.releaseHoleSpan(cacheSpan1);
}
public void testSetGetLength() throws Exception {
......@@ -83,12 +100,12 @@ public class SimpleCacheTest extends InstrumentationTestCase {
simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, 0, 15);
addCache(simpleCache, KEY_1, 0, 15);
simpleCache.setContentLength(KEY_1, 150);
assertEquals(150, simpleCache.getContentLength(KEY_1));
addCache(simpleCache, 140, 10);
addCache(simpleCache, KEY_1, 140, 10);
// Check if values are kept after cache is reloaded.
SimpleCache simpleCache2 = getSimpleCache();
......@@ -107,16 +124,109 @@ public class SimpleCacheTest extends InstrumentationTestCase {
assertEquals(150, simpleCache2.getContentLength(KEY_1));
}
public void testReloadCache() throws Exception {
SimpleCache simpleCache = getSimpleCache();
// write data
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15);
simpleCache.releaseHoleSpan(cacheSpan1);
// Reload cache
simpleCache = getSimpleCache();
// read data back
CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);
assertCachedDataReadCorrect(cacheSpan2);
}
public void testEncryptedIndex() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
SimpleCache simpleCache = getEncryptedSimpleCache(key);
// write data
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15);
simpleCache.releaseHoleSpan(cacheSpan1);
// Reload cache
simpleCache = getEncryptedSimpleCache(key);
// read data back
CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);
assertCachedDataReadCorrect(cacheSpan2);
}
public void testEncryptedIndexWrongKey() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
SimpleCache simpleCache = getEncryptedSimpleCache(key);
// write data
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15);
simpleCache.releaseHoleSpan(cacheSpan1);
// Reload cache
byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key
simpleCache = getEncryptedSimpleCache(key2);
// Cache should be cleared
assertEquals(0, simpleCache.getKeys().size());
assertEquals(0, cacheDir.listFiles().length);
}
public void testEncryptedIndexLostKey() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
SimpleCache simpleCache = getEncryptedSimpleCache(key);
// write data
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15);
simpleCache.releaseHoleSpan(cacheSpan1);
// Reload cache
simpleCache = getSimpleCache();
// Cache should be cleared
assertEquals(0, simpleCache.getKeys().size());
assertEquals(0, cacheDir.listFiles().length);
}
private SimpleCache getSimpleCache() {
return new SimpleCache(cacheDir, new NoOpCacheEvictor());
}
private void addCache(SimpleCache simpleCache, int position, int length) throws IOException {
File file = simpleCache.startFile(KEY_1, position, length);
private SimpleCache getEncryptedSimpleCache(byte[] secretKey) {
return new SimpleCache(cacheDir, new NoOpCacheEvictor(), secretKey);
}
private static void addCache(SimpleCache simpleCache, String key, int position, int length)
throws IOException {
File file = simpleCache.startFile(key, position, length);
FileOutputStream fos = new FileOutputStream(file);
fos.write(new byte[length]);
fos.close();
try {
fos.write(generateData(key, position, length));
} finally {
fos.close();
}
simpleCache.commitFile(file);
}
private static void assertCachedDataReadCorrect(CacheSpan cacheSpan) throws IOException {
assertTrue(cacheSpan.isCached);
byte[] expected = generateData(cacheSpan.key, (int) cacheSpan.position, (int) cacheSpan.length);
FileInputStream inputStream = new FileInputStream(cacheSpan.file);
try {
MoreAsserts.assertEquals(expected, Util.toByteArray(inputStream));
} finally {
inputStream.close();
}
}
private static byte[] generateData(String key, int position, int length) {
byte[] bytes = new byte[length];
new Random((long) (key.hashCode() ^ position)).nextBytes(bytes);
return bytes;
}
}
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.upstream.crypto;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util;
import java.util.Random;
import javax.crypto.Cipher;
import junit.framework.TestCase;
/**
* Unit tests for {@link AesFlushingCipher}.
*/
public class AesFlushingCipherTest extends TestCase {
private static final int DATA_LENGTH = 65536;
private static final byte[] KEY = Util.getUtf8Bytes("testKey:12345678");
private static final long NONCE = 0;
private static final long START_OFFSET = 11;
private static final long RANDOM_SEED = 0x12345678;
private AesFlushingCipher encryptCipher;
private AesFlushingCipher decryptCipher;
@Override
protected void setUp() {
encryptCipher = new AesFlushingCipher(Cipher.ENCRYPT_MODE, KEY, NONCE, START_OFFSET);
decryptCipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, KEY, NONCE, START_OFFSET);
}
@Override
protected void tearDown() {
encryptCipher = null;
decryptCipher = null;
}
private long getMaxUnchangedBytesAllowedPostEncryption(long length) {
// Assuming that not more than 10% of the resultant bytes should be identical.
// The value of 10% is arbitrary, ciphers standards do not name a value.
return length / 10;
}
// Count the number of bytes that do not match.
private int getDifferingByteCount(byte[] data1, byte[] data2, int startOffset) {
int count = 0;
for (int i = startOffset; i < data1.length; i++) {
if (data1[i] != data2[i]) {
count++;
}
}
return count;
}
// Count the number of bytes that do not match.
private int getDifferingByteCount(byte[] data1, byte[] data2) {
return getDifferingByteCount(data1, data2, 0);
}
// Test a single encrypt and decrypt call
public void testSingle() {
byte[] reference = TestUtil.buildTestData(DATA_LENGTH);
byte[] data = reference.clone();
encryptCipher.updateInPlace(data, 0, data.length);
int unchangedByteCount = data.length - getDifferingByteCount(reference, data);
assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length));
decryptCipher.updateInPlace(data, 0, data.length);
int differingByteCount = getDifferingByteCount(reference, data);
assertEquals(0, differingByteCount);
}
// Test several encrypt and decrypt calls, each aligned on a 16 byte block size
public void testAligned() {
byte[] reference = TestUtil.buildTestData(DATA_LENGTH);
byte[] data = reference.clone();
Random random = new Random(RANDOM_SEED);
int offset = 0;
while (offset < data.length) {
int bytes = (1 + random.nextInt(50)) * 16;
bytes = Math.min(bytes, data.length - offset);
assertEquals(0, bytes % 16);
encryptCipher.updateInPlace(data, offset, bytes);
offset += bytes;
}
int unchangedByteCount = data.length - getDifferingByteCount(reference, data);
assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length));
offset = 0;
while (offset < data.length) {
int bytes = (1 + random.nextInt(50)) * 16;
bytes = Math.min(bytes, data.length - offset);
assertEquals(0, bytes % 16);
decryptCipher.updateInPlace(data, offset, bytes);
offset += bytes;
}
int differingByteCount = getDifferingByteCount(reference, data);
assertEquals(0, differingByteCount);
}
// Test several encrypt and decrypt calls, not aligned on block boundary
public void testUnAligned() {
byte[] reference = TestUtil.buildTestData(DATA_LENGTH);
byte[] data = reference.clone();
Random random = new Random(RANDOM_SEED);
// Encrypt
int offset = 0;
while (offset < data.length) {
int bytes = 1 + random.nextInt(4095);
bytes = Math.min(bytes, data.length - offset);
encryptCipher.updateInPlace(data, offset, bytes);
offset += bytes;
}
int unchangedByteCount = data.length - getDifferingByteCount(reference, data);
assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length));
offset = 0;
while (offset < data.length) {
int bytes = 1 + random.nextInt(4095);
bytes = Math.min(bytes, data.length - offset);
decryptCipher.updateInPlace(data, offset, bytes);
offset += bytes;
}
int differingByteCount = getDifferingByteCount(reference, data);
assertEquals(0, differingByteCount);
}
// Test decryption starting from the middle of an encrypted block
public void testMidJoin() {
byte[] reference = TestUtil.buildTestData(DATA_LENGTH);
byte[] data = reference.clone();
Random random = new Random(RANDOM_SEED);
// Encrypt
int offset = 0;
while (offset < data.length) {
int bytes = 1 + random.nextInt(4095);
bytes = Math.min(bytes, data.length - offset);
encryptCipher.updateInPlace(data, offset, bytes);
offset += bytes;
}
// Verify
int unchangedByteCount = data.length - getDifferingByteCount(reference, data);
assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length));
// Setup decryption from random location
offset = random.nextInt(4096);
decryptCipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, KEY, NONCE, offset + START_OFFSET);
int remainingLength = data.length - offset;
int originalOffset = offset;
// Decrypt
while (remainingLength > 0) {
int bytes = 1 + random.nextInt(4095);
bytes = Math.min(bytes, remainingLength);
decryptCipher.updateInPlace(data, offset, bytes);
offset += bytes;
remainingLength -= bytes;
}
// Verify
int differingByteCount = getDifferingByteCount(reference, data, originalOffset);
assertEquals(0, differingByteCount);
}
}
......@@ -371,6 +371,73 @@ public class ParsableByteArrayTest extends TestCase {
assertNull(parser.readLine());
}
public void testReadNullTerminatedStringWithLengths() {
byte[] bytes = new byte[] {
'f', 'o', 'o', 0, 'b', 'a', 'r', 0
};
// Test with lengths that match NUL byte positions.
ParsableByteArray parser = new ParsableByteArray(bytes);
assertEquals("foo", parser.readNullTerminatedString(4));
assertEquals(4, parser.getPosition());
assertEquals("bar", parser.readNullTerminatedString(4));
assertEquals(8, parser.getPosition());
assertNull(parser.readNullTerminatedString());
// Test with lengths that do not match NUL byte positions.
parser = new ParsableByteArray(bytes);
assertEquals("fo", parser.readNullTerminatedString(2));
assertEquals(2, parser.getPosition());
assertEquals("o", parser.readNullTerminatedString(2));
assertEquals(4, parser.getPosition());
assertEquals("bar", parser.readNullTerminatedString(3));
assertEquals(7, parser.getPosition());
assertEquals("", parser.readNullTerminatedString(1));
assertEquals(8, parser.getPosition());
assertNull(parser.readNullTerminatedString());
// Test with limit at NUL
parser = new ParsableByteArray(bytes, 4);
assertEquals("foo", parser.readNullTerminatedString(4));
assertEquals(4, parser.getPosition());
assertNull(parser.readNullTerminatedString());
// Test with limit before NUL
parser = new ParsableByteArray(bytes, 3);
assertEquals("foo", parser.readNullTerminatedString(3));
assertEquals(3, parser.getPosition());
assertNull(parser.readNullTerminatedString());
}
public void testReadNullTerminatedString() {
byte[] bytes = new byte[] {
'f', 'o', 'o', 0, 'b', 'a', 'r', 0
};
// Test normal case.
ParsableByteArray parser = new ParsableByteArray(bytes);
assertEquals("foo", parser.readNullTerminatedString());
assertEquals(4, parser.getPosition());
assertEquals("bar", parser.readNullTerminatedString());
assertEquals(8, parser.getPosition());
assertNull(parser.readNullTerminatedString());
// Test with limit at NUL.
parser = new ParsableByteArray(bytes, 4);
assertEquals("foo", parser.readNullTerminatedString());
assertEquals(4, parser.getPosition());
assertNull(parser.readNullTerminatedString());
// Test with limit before NUL.
parser = new ParsableByteArray(bytes, 3);
assertEquals("foo", parser.readNullTerminatedString());
assertEquals(3, parser.getPosition());
assertNull(parser.readNullTerminatedString());
}
public void testReadNullTerminatedStringWithoutEndingNull() {
byte[] bytes = new byte[] {
'f', 'o', 'o', 0, 'b', 'a', 'r'
};
ParsableByteArray parser = new ParsableByteArray(bytes);
assertEquals("foo", parser.readNullTerminatedString());
assertEquals("bar", parser.readNullTerminatedString());
assertNull(parser.readNullTerminatedString());
}
public void testReadSingleLineWithoutEndingTrail() {
byte[] bytes = new byte[] {
'f', 'o', 'o'
......
......@@ -28,6 +28,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
private final int trackType;
private RendererConfiguration configuration;
private int index;
private int state;
private SampleStream stream;
......@@ -70,9 +71,11 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
}
@Override
public final void enable(Format[] formats, SampleStream stream, long positionUs, boolean joining,
long offsetUs) throws ExoPlaybackException {
public final void enable(RendererConfiguration configuration, Format[] formats,
SampleStream stream, long positionUs, boolean joining, long offsetUs)
throws ExoPlaybackException {
Assertions.checkState(state == STATE_DISABLED);
this.configuration = configuration;
state = STATE_ENABLED;
onEnabled(joining);
replaceStream(formats, stream, offsetUs);
......@@ -238,9 +241,14 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
// Methods to be called by subclasses.
/**
* Returns the configuration set when the renderer was most recently enabled.
*/
protected final RendererConfiguration getConfiguration() {
return configuration;
}
/**
* Returns the index of the renderer within the player.
*
* @return The index of the renderer within the player.
*/
protected final int getIndex() {
return index;
......@@ -251,11 +259,11 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
* {@link C#RESULT_BUFFER_READ} is only returned if {@link #setCurrentStreamFinal()} has been
* called. {@link C#RESULT_NOTHING_READ} is returned otherwise.
*
* @see SampleStream#readData(FormatHolder, DecoderInputBuffer)
* @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.
* {@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.
* @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
* {@link C#RESULT_BUFFER_READ}.
*/
......
......@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2;
import android.annotation.TargetApi;
import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.MediaCodec;
......@@ -550,4 +552,13 @@ public final class C {
return timeMs == TIME_UNSET ? TIME_UNSET : (timeMs * 1000);
}
/**
* Returns a newly generated {@link android.media.AudioTrack} session identifier.
*/
@TargetApi(21)
public static int generateAudioSessionIdV21(Context context) {
return ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE))
.generateAudioSessionId();
}
}
......@@ -19,6 +19,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
import com.google.android.exoplayer2.util.PriorityTaskManager;
import com.google.android.exoplayer2.util.Util;
/**
......@@ -50,6 +51,11 @@ 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;
......@@ -60,6 +66,7 @@ public final class DefaultLoadControl implements LoadControl {
private final long maxBufferUs;
private final long bufferForPlaybackUs;
private final long bufferForPlaybackAfterRebufferUs;
private final PriorityTaskManager priorityTaskManager;
private int targetBufferSize;
private boolean isBuffering;
......@@ -97,11 +104,36 @@ public final class DefaultLoadControl implements LoadControl {
*/
public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs) {
this(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs,
null);
}
/**
* Constructs a new instance.
*
* @param allocator The {@link DefaultAllocator} used by the loader.
* @param minBufferMs The minimum duration of media that the player will attempt to ensure is
* buffered at all times, in milliseconds.
* @param maxBufferMs The maximum duration of media that the player will attempt buffer, in
* milliseconds.
* @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or
* resume following a user action such as a seek, in milliseconds.
* @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for
* 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
* periods.
*/
public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs,
PriorityTaskManager priorityTaskManager) {
this.allocator = allocator;
minBufferUs = minBufferMs * 1000L;
maxBufferUs = maxBufferMs * 1000L;
bufferForPlaybackUs = bufferForPlaybackMs * 1000L;
bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L;
this.priorityTaskManager = priorityTaskManager;
}
@Override
......@@ -146,8 +178,16 @@ public final class DefaultLoadControl implements LoadControl {
public boolean shouldContinueLoading(long bufferedDurationUs) {
int bufferTimeState = getBufferTimeState(bufferedDurationUs);
boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize;
boolean wasBuffering = isBuffering;
isBuffering = bufferTimeState == BELOW_LOW_WATERMARK
|| (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached);
if (priorityTaskManager != null && isBuffering != wasBuffering) {
if (isBuffering) {
priorityTaskManager.add(LOADING_PRIORITY);
} else {
priorityTaskManager.remove(LOADING_PRIORITY);
}
}
return isBuffering;
}
......@@ -158,6 +198,9 @@ public final class DefaultLoadControl implements LoadControl {
private void reset(boolean resetAllocator) {
targetBufferSize = 0;
if (priorityTaskManager != null && isBuffering) {
priorityTaskManager.remove(LOADING_PRIORITY);
}
isBuffering = false;
if (resetAllocator) {
allocator.reset();
......
......@@ -447,4 +447,20 @@ public interface ExoPlayer {
*/
int getBufferedPercentage();
/**
* Returns whether the current window is dynamic, or {@code false} if the {@link Timeline} is
* empty.
*
* @see Timeline.Window#isDynamic
*/
boolean isCurrentWindowDynamic();
/**
* Returns whether the current window is seekable, or {@code false} if the {@link Timeline} is
* empty.
*
* @see Timeline.Window#isSeekable
*/
boolean isCurrentWindowSeekable();
}
......@@ -22,12 +22,12 @@ import android.os.Message;
import android.util.Log;
import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo;
import com.google.android.exoplayer2.ExoPlayerImplInternal.SourceInfo;
import com.google.android.exoplayer2.ExoPlayerImplInternal.TrackInfo;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.concurrent.CopyOnWriteArraySet;
......@@ -272,6 +272,22 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
@Override
public boolean isCurrentWindowDynamic() {
if (timeline.isEmpty()) {
return false;
}
return timeline.getWindow(getCurrentWindowIndex(), window).isDynamic;
}
@Override
public boolean isCurrentWindowSeekable() {
if (timeline.isEmpty()) {
return false;
}
return timeline.getWindow(getCurrentWindowIndex(), window).isSeekable;
}
@Override
public int getRendererCount() {
return renderers.length;
}
......@@ -319,11 +335,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
break;
}
case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: {
TrackInfo trackInfo = (TrackInfo) msg.obj;
TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj;
tracksSelected = true;
trackGroups = trackInfo.groups;
trackSelections = trackInfo.selections;
trackSelector.onSelectionActivated(trackInfo.info);
trackGroups = trackSelectorResult.groups;
trackSelections = trackSelectorResult.selections;
trackSelector.onSelectionActivated(trackSelectorResult.info);
for (EventListener listener : listeners) {
listener.onTracksChanged(trackGroups, trackSelections);
}
......
......@@ -183,20 +183,18 @@ public final class Format implements Parcelable {
*/
public final int accessibilityChannel;
// Lazily initialized hashcode and framework media format.
// Lazily initialized hashcode.
private int hashCode;
private MediaFormat frameworkMediaFormat;
// Video.
public static Format createVideoContainerFormat(String id, String containerMimeType,
String sampleMimeType, String codecs, int bitrate, int width, int height,
float frameRate, List<byte[]> initializationData) {
float frameRate, List<byte[]> initializationData, @C.SelectionFlags int selectionFlags) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, width,
height, frameRate, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, null,
null);
NO_VALUE, NO_VALUE, selectionFlags, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
initializationData, null, null);
}
public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs,
......@@ -289,8 +287,8 @@ public final class Format implements Parcelable {
}
public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
int bitrate, @C.SelectionFlags int selectionFlags, String language,
int accessibilityChannel, DrmInitData drmInitData) {
int bitrate, @C.SelectionFlags int selectionFlags, String language, int accessibilityChannel,
DrmInitData drmInitData) {
return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
accessibilityChannel, drmInitData, OFFSET_SAMPLE_RELATIVE);
}
......@@ -332,11 +330,20 @@ public final class Format implements Parcelable {
// Generic.
public static Format createContainerFormat(String id, String containerMimeType, String codecs,
String sampleMimeType, int bitrate) {
public static Format createContainerFormat(String id, String containerMimeType,
String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags,
String language) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, null, null);
NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, null,
null);
}
public static Format createSampleFormat(String id, String sampleMimeType,
long subsampleOffsetUs) {
return new Format(id, null, sampleMimeType, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, 0, null, NO_VALUE, subsampleOffsetUs, null, null, null);
}
public static Format createSampleFormat(String id, String sampleMimeType, String codecs,
......@@ -495,31 +502,28 @@ public final class Format implements Parcelable {
@SuppressLint("InlinedApi")
@TargetApi(16)
public final MediaFormat getFrameworkMediaFormatV16() {
if (frameworkMediaFormat == null) {
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, sampleMimeType);
maybeSetStringV16(format, MediaFormat.KEY_LANGUAGE, language);
maybeSetIntegerV16(format, MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
maybeSetIntegerV16(format, MediaFormat.KEY_WIDTH, width);
maybeSetIntegerV16(format, MediaFormat.KEY_HEIGHT, height);
maybeSetFloatV16(format, MediaFormat.KEY_FRAME_RATE, frameRate);
maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees);
maybeSetIntegerV16(format, MediaFormat.KEY_CHANNEL_COUNT, channelCount);
maybeSetIntegerV16(format, MediaFormat.KEY_SAMPLE_RATE, sampleRate);
maybeSetIntegerV16(format, "encoder-delay", encoderDelay);
maybeSetIntegerV16(format, "encoder-padding", encoderPadding);
for (int i = 0; i < initializationData.size(); i++) {
format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
}
frameworkMediaFormat = format;
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, sampleMimeType);
maybeSetStringV16(format, MediaFormat.KEY_LANGUAGE, language);
maybeSetIntegerV16(format, MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
maybeSetIntegerV16(format, MediaFormat.KEY_WIDTH, width);
maybeSetIntegerV16(format, MediaFormat.KEY_HEIGHT, height);
maybeSetFloatV16(format, MediaFormat.KEY_FRAME_RATE, frameRate);
maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees);
maybeSetIntegerV16(format, MediaFormat.KEY_CHANNEL_COUNT, channelCount);
maybeSetIntegerV16(format, MediaFormat.KEY_SAMPLE_RATE, sampleRate);
maybeSetIntegerV16(format, "encoder-delay", encoderDelay);
maybeSetIntegerV16(format, "encoder-padding", encoderPadding);
for (int i = 0; i < initializationData.size(); i++) {
format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
}
return frameworkMediaFormat;
return format;
}
@Override
public String toString() {
return "Format(" + id + ", " + containerMimeType + ", " + sampleMimeType + ", " + bitrate + ", "
+ ", " + language + ", [" + width + ", " + height + ", " + frameRate + "]"
+ language + ", [" + width + ", " + height + ", " + frameRate + "]"
+ ", [" + channelCount + ", " + sampleRate + "])";
}
......@@ -602,6 +606,38 @@ public final class Format implements Parcelable {
}
}
// Utility methods
/**
* Returns a prettier {@link String} than {@link #toString()}, intended for logging.
*/
public static String toLogString(Format format) {
if (format == null) {
return "null";
}
StringBuilder builder = new StringBuilder();
builder.append("id=").append(format.id).append(", mimeType=").append(format.sampleMimeType);
if (format.bitrate != Format.NO_VALUE) {
builder.append(", bitrate=").append(format.bitrate);
}
if (format.width != Format.NO_VALUE && format.height != Format.NO_VALUE) {
builder.append(", res=").append(format.width).append("x").append(format.height);
}
if (format.frameRate != Format.NO_VALUE) {
builder.append(", fps=").append(format.frameRate);
}
if (format.channelCount != Format.NO_VALUE) {
builder.append(", channels=").append(format.channelCount);
}
if (format.sampleRate != Format.NO_VALUE) {
builder.append(", sample_rate=").append(format.sampleRate);
}
if (format.language != null) {
builder.append(", language=").append(format.language);
}
return builder.toString();
}
// Parcelable implementation.
@Override
......
......@@ -92,6 +92,7 @@ public interface Renderer extends ExoPlayerComponent {
* This method may be called when the renderer is in the following states:
* {@link #STATE_DISABLED}.
*
* @param configuration The renderer configuration.
* @param formats The enabled formats.
* @param stream The {@link SampleStream} from which the renderer should consume.
* @param positionUs The player's current position.
......@@ -100,8 +101,8 @@ public interface Renderer extends ExoPlayerComponent {
* before they are rendered.
* @throws ExoPlaybackException If an error occurs.
*/
void enable(Format[] formats, SampleStream stream, long positionUs, boolean joining,
long offsetUs) throws ExoPlaybackException;
void enable(RendererConfiguration configuration, Format[] formats, SampleStream stream,
long positionUs, boolean joining, long offsetUs) throws ExoPlaybackException;
/**
* Starts the renderer, meaning that calls to {@link #render(long, long)} will cause media to be
......
......@@ -80,6 +80,20 @@ public interface RendererCapabilities {
int ADAPTIVE_NOT_SUPPORTED = 0b0000;
/**
* A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of
* {@link #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}.
*/
int TUNNELING_SUPPORT_MASK = 0b10000;
/**
* The {@link Renderer} supports tunneled output.
*/
int TUNNELING_SUPPORTED = 0b10000;
/**
* The {@link Renderer} does not support tunneled output.
*/
int TUNNELING_NOT_SUPPORTED = 0b00000;
/**
* Returns the track type that the {@link Renderer} handles. For example, a video renderer will
* return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will return {@link C#TRACK_TYPE_AUDIO}, a
* text renderer will return {@link C#TRACK_TYPE_TEXT}, and so on.
......@@ -91,7 +105,7 @@ public interface RendererCapabilities {
/**
* Returns the extent to which the {@link Renderer} supports a given format. The returned value is
* the bitwise OR of two properties:
* the bitwise OR of three properties:
* <ul>
* <li>The level of support for the format itself. One of {@link #FORMAT_HANDLED},
* {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_SUBTYPE} and
......@@ -99,9 +113,12 @@ public interface RendererCapabilities {
* <li>The level of support for adapting from the format to another format of the same mime type.
* One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and
* {@link #ADAPTIVE_NOT_SUPPORTED}.</li>
* <li>The level of support for tunneling. One of {@link #TUNNELING_SUPPORTED} and
* {@link #TUNNELING_NOT_SUPPORTED}.</li>
* </ul>
* The individual properties can be retrieved by performing a bitwise AND with
* {@link #FORMAT_SUPPORT_MASK} and {@link #ADAPTIVE_SUPPORT_MASK} respectively.
* {@link #FORMAT_SUPPORT_MASK}, {@link #ADAPTIVE_SUPPORT_MASK} and
* {@link #TUNNELING_SUPPORT_MASK} respectively.
*
* @param format The format.
* @return The extent to which the renderer is capable of supporting the given format.
......
/*
* 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;
/**
* The configuration of a {@link Renderer}.
*/
public final class RendererConfiguration {
/**
* The default configuration.
*/
public static final RendererConfiguration DEFAULT =
new RendererConfiguration(C.AUDIO_SESSION_ID_UNSET);
/**
* The audio session id to use for tunneling, or {@link C#AUDIO_SESSION_ID_UNSET} if tunneling
* should not be enabled.
*/
public final int tunnelingAudioSessionId;
/**
* @param tunnelingAudioSessionId The audio session id to use for tunneling, or
* {@link C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled.
*/
public RendererConfiguration(int tunnelingAudioSessionId) {
this.tunnelingAudioSessionId = tunnelingAudioSessionId;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
RendererConfiguration other = (RendererConfiguration) obj;
return tunnelingAudioSessionId == other.tunnelingAudioSessionId;
}
@Override
public int hashCode() {
return tunnelingAudioSessionId;
}
}
......@@ -36,7 +36,6 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue;
......@@ -449,15 +448,6 @@ public class SimpleExoPlayer implements ExoPlayer {
}
/**
* @deprecated Use {@link #setMetadataOutput(MetadataRenderer.Output)} instead.
* @param output The output.
*/
@Deprecated
public void setId3Output(MetadataRenderer.Output output) {
setMetadataOutput(output);
}
/**
* Sets a listener to receive metadata events.
*
* @param output The output.
......@@ -556,63 +546,73 @@ public class SimpleExoPlayer implements ExoPlayer {
}
@Override
public int getCurrentPeriodIndex() {
return player.getCurrentPeriodIndex();
public int getRendererCount() {
return player.getRendererCount();
}
@Override
public int getCurrentWindowIndex() {
return player.getCurrentWindowIndex();
public int getRendererType(int index) {
return player.getRendererType(index);
}
@Override
public long getDuration() {
return player.getDuration();
public TrackGroupArray getCurrentTrackGroups() {
return player.getCurrentTrackGroups();
}
@Override
public long getCurrentPosition() {
return player.getCurrentPosition();
public TrackSelectionArray getCurrentTrackSelections() {
return player.getCurrentTrackSelections();
}
@Override
public long getBufferedPosition() {
return player.getBufferedPosition();
public Timeline getCurrentTimeline() {
return player.getCurrentTimeline();
}
@Override
public int getBufferedPercentage() {
return player.getBufferedPercentage();
public Object getCurrentManifest() {
return player.getCurrentManifest();
}
@Override
public int getRendererCount() {
return player.getRendererCount();
public int getCurrentPeriodIndex() {
return player.getCurrentPeriodIndex();
}
@Override
public int getRendererType(int index) {
return player.getRendererType(index);
public int getCurrentWindowIndex() {
return player.getCurrentWindowIndex();
}
@Override
public TrackGroupArray getCurrentTrackGroups() {
return player.getCurrentTrackGroups();
public long getDuration() {
return player.getDuration();
}
@Override
public TrackSelectionArray getCurrentTrackSelections() {
return player.getCurrentTrackSelections();
public long getCurrentPosition() {
return player.getCurrentPosition();
}
@Override
public Timeline getCurrentTimeline() {
return player.getCurrentTimeline();
public long getBufferedPosition() {
return player.getBufferedPosition();
}
@Override
public Object getCurrentManifest() {
return player.getCurrentManifest();
public int getBufferedPercentage() {
return player.getBufferedPercentage();
}
@Override
public boolean isCurrentWindowDynamic() {
return player.isCurrentWindowDynamic();
}
@Override
public boolean isCurrentWindowSeekable() {
return player.isCurrentWindowSeekable();
}
// Renderer building.
......@@ -771,7 +771,7 @@ public class SimpleExoPlayer implements ExoPlayer {
protected void buildMetadataRenderers(Context context, Handler mainHandler,
@ExtensionRendererMode int extensionRendererMode, MetadataRenderer.Output output,
ArrayList<Renderer> out) {
out.add(new MetadataRenderer(output, mainHandler.getLooper(), new Id3Decoder()));
out.add(new MetadataRenderer(output, mainHandler.getLooper()));
}
/**
......
......@@ -41,8 +41,7 @@ import java.nio.ByteBuffer;
* Decodes and renders audio using {@link MediaCodec} and {@link AudioTrack}.
*/
@TargetApi(16)
public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock,
AudioTrack.Listener {
public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock {
private final EventDispatcher eventDispatcher;
private final AudioTrack audioTrack;
......@@ -50,7 +49,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
private boolean passthroughEnabled;
private android.media.MediaFormat passthroughMediaFormat;
private int pcmEncoding;
private int audioSessionId;
private long currentPositionUs;
private boolean allowPositionDiscontinuity;
......@@ -129,8 +127,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
boolean playClearSamplesWithoutKeys, Handler eventHandler,
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) {
super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys);
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
audioTrack = new AudioTrack(audioCapabilities, this);
audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener());
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
}
......@@ -141,10 +138,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
if (!MimeTypes.isAudio(mimeType)) {
return FORMAT_UNSUPPORTED_TYPE;
}
int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;
if (allowPassthrough(mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null) {
return ADAPTIVE_NOT_SEAMLESS | FORMAT_HANDLED;
return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED;
}
MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, false, false);
MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, false);
if (decoderInfo == null) {
return FORMAT_UNSUPPORTED_SUBTYPE;
}
......@@ -155,7 +153,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
&& (format.channelCount == Format.NO_VALUE
|| decoderInfo.isAudioChannelCountSupportedV21(format.channelCount)));
int formatSupport = decoderCapable ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES;
return ADAPTIVE_NOT_SEAMLESS | formatSupport;
return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport;
}
@Override
......@@ -231,25 +229,42 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
}
/**
* Called when the audio session id becomes known. Once the id is known it will not change (and
* hence this method will not be called again) unless the renderer is disabled and then
* subsequently re-enabled.
* <p>
* The default implementation is a no-op. One reason for overriding this method would be to
* instantiate and enable a {@link Virtualizer} in order to spatialize the audio channels. For
* this use case, any {@link Virtualizer} instances should be released in {@link #onDisabled()}
* (if not before).
* Called when the audio session id becomes known. The default implementation is a no-op. One
* reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in
* order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances
* should be released in {@link #onDisabled()} (if not before).
*
* @param audioSessionId The audio session id.
* @see AudioTrack.Listener#onAudioSessionId(int)
*/
protected void onAudioSessionId(int audioSessionId) {
// Do nothing.
}
/**
* @see AudioTrack.Listener#onPositionDiscontinuity()
*/
protected void onAudioTrackPositionDiscontinuity() {
// Do nothing.
}
/**
* @see AudioTrack.Listener#onUnderrun(int, long, long)
*/
protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs,
long elapsedSinceLastFeedMs) {
// Do nothing.
}
@Override
protected void onEnabled(boolean joining) throws ExoPlaybackException {
super.onEnabled(joining);
eventDispatcher.enabled(decoderCounters);
int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;
if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {
audioTrack.enableTunnelingV21(tunnelingAudioSessionId);
} else {
audioTrack.disableTunneling();
}
}
@Override
......@@ -274,7 +289,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Override
protected void onDisabled() {
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
try {
audioTrack.release();
} finally {
......@@ -325,44 +339,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
return true;
}
if (!audioTrack.isInitialized()) {
// Initialize the AudioTrack now.
try {
if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
audioSessionId = audioTrack.initialize(C.AUDIO_SESSION_ID_UNSET);
eventDispatcher.audioSessionId(audioSessionId);
onAudioSessionId(audioSessionId);
} else {
audioTrack.initialize(audioSessionId);
}
} catch (AudioTrack.InitializationException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
if (getState() == STATE_STARTED) {
audioTrack.play();
}
}
int handleBufferResult;
try {
handleBufferResult = audioTrack.handleBuffer(buffer, bufferPresentationTimeUs);
} catch (AudioTrack.WriteException e) {
if (audioTrack.handleBuffer(buffer, bufferPresentationTimeUs)) {
codec.releaseOutputBuffer(bufferIndex, false);
decoderCounters.renderedOutputBufferCount++;
return true;
}
} catch (AudioTrack.InitializationException | AudioTrack.WriteException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
// If we are out of sync, allow currentPositionUs to jump backwards.
if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) {
handleAudioTrackDiscontinuity();
allowPositionDiscontinuity = true;
}
// Release the buffer if it was consumed.
if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) {
codec.releaseOutputBuffer(bufferIndex, false);
decoderCounters.renderedOutputBufferCount++;
return true;
}
return false;
}
......@@ -371,10 +356,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
audioTrack.handleEndOfStream();
}
protected void handleAudioTrackDiscontinuity() {
// Do nothing
}
@Override
public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
switch (messageType) {
......@@ -386,9 +367,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
break;
case C.MSG_SET_STREAM_TYPE:
@C.StreamType int streamType = (Integer) message;
if (audioTrack.setStreamType(streamType)) {
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
}
audioTrack.setStreamType(streamType);
break;
default:
super.handleMessage(messageType, message);
......@@ -396,11 +375,27 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
}
}
// AudioTrack.Listener implementation.
private final class AudioTrackListener implements AudioTrack.Listener {
@Override
public void onAudioSessionId(int audioSessionId) {
eventDispatcher.audioSessionId(audioSessionId);
MediaCodecAudioRenderer.this.onAudioSessionId(audioSessionId);
}
@Override
public void onPositionDiscontinuity() {
onAudioTrackPositionDiscontinuity();
// We are out of sync so allow currentPositionUs to jump backwards.
MediaCodecAudioRenderer.this.allowPositionDiscontinuity = true;
}
@Override
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
}
@Override
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
}
}
......@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.audio;
import android.media.PlaybackParams;
import android.media.audiofx.Virtualizer;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
......@@ -43,8 +44,7 @@ import java.lang.annotation.RetentionPolicy;
/**
* Decodes and renders audio using a {@link SimpleDecoder}.
*/
public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock,
AudioTrack.Listener {
public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock {
@Retention(RetentionPolicy.SOURCE)
@IntDef({REINITIALIZATION_STATE_NONE, REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM,
......@@ -94,8 +94,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
private boolean outputStreamEnded;
private boolean waitingForKeys;
private int audioSessionId;
public SimpleDecoderAudioRenderer() {
this(null, null);
}
......@@ -141,11 +139,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys) {
super(C.TRACK_TYPE_AUDIO);
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
audioTrack = new AudioTrack(audioCapabilities, this);
audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener());
this.drmSessionManager = drmSessionManager;
formatHolder = new FormatHolder();
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
audioTrackNeedsConfigure = true;
}
......@@ -156,6 +153,25 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
}
@Override
public final int supportsFormat(Format format) {
int formatSupport = supportsFormatInternal(format);
if (formatSupport == FORMAT_UNSUPPORTED_TYPE || formatSupport == FORMAT_UNSUPPORTED_SUBTYPE) {
return formatSupport;
}
int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;
return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport;
}
/**
* Returns the {@link #FORMAT_SUPPORT_MASK} component of the return value for
* {@link #supportsFormat(Format)}.
*
* @param format The format.
* @return The extent to which the renderer supports the format itself.
*/
protected abstract int supportsFormatInternal(Format format);
@Override
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
if (outputStreamEnded) {
return;
......@@ -186,6 +202,33 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
}
/**
* Called when the audio session id becomes known. The default implementation is a no-op. One
* reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in
* order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances
* should be released in {@link #onDisabled()} (if not before).
*
* @see AudioTrack.Listener#onAudioSessionId(int)
*/
protected void onAudioSessionId(int audioSessionId) {
// Do nothing.
}
/**
* @see AudioTrack.Listener#onPositionDiscontinuity()
*/
protected void onAudioTrackPositionDiscontinuity() {
// Do nothing.
}
/**
* @see AudioTrack.Listener#onUnderrun(int, long, long)
*/
protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs,
long elapsedSinceLastFeedMs) {
// Do nothing.
}
/**
* Creates a decoder for the given format.
*
* @param format The format for which a decoder is required.
......@@ -244,28 +287,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
audioTrackNeedsConfigure = false;
}
if (!audioTrack.isInitialized()) {
if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
audioSessionId = audioTrack.initialize(C.AUDIO_SESSION_ID_UNSET);
eventDispatcher.audioSessionId(audioSessionId);
onAudioSessionId(audioSessionId);
} else {
audioTrack.initialize(audioSessionId);
}
if (getState() == STATE_STARTED) {
audioTrack.play();
}
}
int handleBufferResult = audioTrack.handleBuffer(outputBuffer.data, outputBuffer.timeUs);
// If we are out of sync, allow currentPositionUs to jump backwards.
if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) {
allowPositionDiscontinuity = true;
}
// Release the buffer if it was consumed.
if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) {
if (audioTrack.handleBuffer(outputBuffer.data, outputBuffer.timeUs)) {
decoderCounters.renderedOutputBufferCount++;
outputBuffer.release();
outputBuffer = null;
......@@ -381,23 +403,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
return currentPositionUs;
}
/**
* Called when the audio session id becomes known. Once the id is known it will not change (and
* hence this method will not be called again) unless the renderer is disabled and then
* subsequently re-enabled.
* <p>
* The default implementation is a no-op.
*
* @param audioSessionId The audio session id.
*/
protected void onAudioSessionId(int audioSessionId) {
// Do nothing.
}
@Override
protected void onEnabled(boolean joining) throws ExoPlaybackException {
decoderCounters = new DecoderCounters();
eventDispatcher.enabled(decoderCounters);
int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;
if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {
audioTrack.enableTunnelingV21(tunnelingAudioSessionId);
} else {
audioTrack.disableTunneling();
}
}
@Override
......@@ -425,7 +440,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
@Override
protected void onDisabled() {
inputFormat = null;
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
audioTrackNeedsConfigure = true;
waitingForKeys = false;
try {
......@@ -537,6 +551,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
// There aren't any final output buffers, so release the decoder immediately.
releaseDecoder();
maybeInitDecoder();
audioTrackNeedsConfigure = true;
}
eventDispatcher.inputFormatChanged(newFormat);
......@@ -553,9 +568,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
break;
case C.MSG_SET_STREAM_TYPE:
@C.StreamType int streamType = (Integer) message;
if (audioTrack.setStreamType(streamType)) {
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
}
audioTrack.setStreamType(streamType);
break;
default:
super.handleMessage(messageType, message);
......@@ -563,11 +576,27 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
}
}
// AudioTrack.Listener implementation.
private final class AudioTrackListener implements AudioTrack.Listener {
@Override
public void onAudioSessionId(int audioSessionId) {
eventDispatcher.audioSessionId(audioSessionId);
SimpleDecoderAudioRenderer.this.onAudioSessionId(audioSessionId);
}
@Override
public void onPositionDiscontinuity() {
onAudioTrackPositionDiscontinuity();
// We are out of sync so allow currentPositionUs to jump backwards.
SimpleDecoderAudioRenderer.this.allowPositionDiscontinuity = true;
}
@Override
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
}
@Override
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
}
}
......@@ -16,9 +16,11 @@
package com.google.android.exoplayer2.drm;
import android.annotation.TargetApi;
import android.media.MediaDrm;
import android.support.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Map;
/**
* A DRM session.
......@@ -26,6 +28,15 @@ import java.lang.annotation.RetentionPolicy;
@TargetApi(16)
public interface DrmSession<T extends ExoMediaCrypto> {
/** Wraps the exception which is the cause of the error state. */
class DrmSessionException extends Exception {
DrmSessionException(Exception e) {
super(e);
}
}
/**
* The state of the DRM session.
*/
......@@ -96,6 +107,26 @@ public interface DrmSession<T extends ExoMediaCrypto> {
*
* @return An exception if the state is {@link #STATE_ERROR}. Null otherwise.
*/
Exception getError();
DrmSessionException getError();
/**
* Returns an informative description of the key status for the session. The status is in the form
* of {name, value} pairs.
*
* <p>Since DRM license policies vary by vendor, the specific status field names are determined by
* each DRM vendor. Refer to your DRM provider documentation for definitions of the field names
* for a particular DRM engine plugin.
*
* @return A map of key status.
* @throws IllegalStateException If called when the session isn't opened.
* @see MediaDrm#queryKeyStatus(byte[])
*/
Map<String, String> queryKeyStatus();
/**
* Returns the key set id of the offline license loaded into this session, if there is one. Null
* otherwise.
*/
byte[] getOfflineLicenseKeySetId();
}
......@@ -105,7 +105,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
try {
return Util.toByteArray(inputStream);
} finally {
inputStream.close();
Util.closeQuietly(inputStream);
}
}
......
/*
* 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.drm;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import java.util.Map;
/**
* Utility methods for Widevine.
*/
public final class WidevineUtil {
/** Widevine specific key status field name for the remaining license duration, in seconds. */
public static final String PROPERTY_LICENSE_DURATION_REMAINING = "LicenseDurationRemaining";
/** Widevine specific key status field name for the remaining playback duration, in seconds. */
public static final String PROPERTY_PLAYBACK_DURATION_REMAINING = "PlaybackDurationRemaining";
private WidevineUtil() {}
/**
* Returns license and playback durations remaining in seconds.
*
* @return A {@link Pair} consisting of the remaining license and playback durations in seconds.
* @throws IllegalStateException If called when a session isn't opened.
* @param drmSession
*/
public static Pair<Long, Long> getLicenseDurationRemainingSec(DrmSession drmSession) {
Map<String, String> keyStatus = drmSession.queryKeyStatus();
return new Pair<>(
getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING),
getDurationRemainingSec(keyStatus, PROPERTY_PLAYBACK_DURATION_REMAINING));
}
private static long getDurationRemainingSec(Map<String, String> keyStatus, String property) {
if (keyStatus != null) {
try {
String value = keyStatus.get(property);
if (value != null) {
return Long.parseLong(value);
}
} catch (NumberFormatException e) {
// do nothing.
}
}
return C.TIME_UNSET;
}
}
......@@ -226,13 +226,32 @@ public final class DefaultTrackOutput implements TrackOutput {
}
/**
* Attempts to skip to the keyframe before the specified time, if it's present in the buffer.
* Attempts to skip to the keyframe before or at the specified time. Succeeds only if the buffer
* contains a keyframe with a timestamp of {@code timeUs} or earlier, and if {@code timeUs} falls
* within the currently buffered media.
* <p>
* This method is equivalent to {@code skipToKeyframeBefore(timeUs, false)}.
*
* @param timeUs The seek time.
* @return Whether the skip was successful.
*/
public boolean skipToKeyframeBefore(long timeUs) {
long nextOffset = infoQueue.skipToKeyframeBefore(timeUs);
return skipToKeyframeBefore(timeUs, false);
}
/**
* Attempts to skip to the keyframe before or at the specified time. Succeeds only if the buffer
* contains a keyframe with a timestamp of {@code timeUs} or earlier. If
* {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs}
* falls within the buffer.
*
* @param timeUs The seek time.
* @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end
* of the buffer.
* @return Whether the skip was successful.
*/
public boolean skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) {
long nextOffset = infoQueue.skipToKeyframeBefore(timeUs, allowTimeBeyondBuffer);
if (nextOffset == C.POSITION_UNSET) {
return false;
}
......@@ -246,7 +265,8 @@ 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.
* {@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.
* @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.
......@@ -732,7 +752,8 @@ public final class DefaultTrackOutput implements TrackOutput {
* about the sample, but not its data. The size and absolute position of the data in the
* rolling buffer is stored in {@code extrasHolder}, along with an encryption id if present
* and the absolute position of the first byte that may still be required after the current
* sample has been read.
* 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 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.
......@@ -742,14 +763,14 @@ public final class DefaultTrackOutput implements TrackOutput {
public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
Format downstreamFormat, BufferExtrasHolder extrasHolder) {
if (queueSize == 0) {
if (upstreamFormat != null && upstreamFormat != downstreamFormat) {
if (upstreamFormat != null && (buffer == null || upstreamFormat != downstreamFormat)) {
formatHolder.format = upstreamFormat;
return C.RESULT_FORMAT_READ;
}
return C.RESULT_NOTHING_READ;
}
if (formats[relativeReadIndex] != downstreamFormat) {
if (buffer == null || formats[relativeReadIndex] != downstreamFormat) {
formatHolder.format = formats[relativeReadIndex];
return C.RESULT_FORMAT_READ;
}
......@@ -775,18 +796,22 @@ public final class DefaultTrackOutput implements TrackOutput {
}
/**
* Attempts to locate the keyframe before the specified time, if it's present in the buffer.
* Attempts to locate the keyframe before or at the specified time. If
* {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs}
* falls within the buffer.
*
* @param timeUs The seek time.
* @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end
* of the buffer.
* @return The offset of the keyframe's data if the keyframe was present.
* {@link C#POSITION_UNSET} otherwise.
*/
public synchronized long skipToKeyframeBefore(long timeUs) {
public synchronized long skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) {
if (queueSize == 0 || timeUs < timesUs[relativeReadIndex]) {
return C.POSITION_UNSET;
}
if (timeUs > largestQueuedTimestampUs) {
if (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer) {
return C.POSITION_UNSET;
}
......
......@@ -52,6 +52,34 @@ public final class TimestampAdjuster {
}
/**
* Returns the last adjusted timestamp. If no timestamp has been adjusted, returns
* {@code firstSampleTimestampUs} as provided to the constructor. If this value is
* {@link #DO_NOT_OFFSET}, returns {@link C#TIME_UNSET}.
*
* @return The last adjusted timestamp. If not present, {@code firstSampleTimestampUs} is
* returned unless equal to {@link #DO_NOT_OFFSET}, in which case {@link C#TIME_UNSET} is
* returned.
*/
public long getLastAdjustedTimestampUs() {
return lastSampleTimestamp != C.TIME_UNSET ? lastSampleTimestamp
: firstSampleTimestampUs != DO_NOT_OFFSET ? firstSampleTimestampUs : C.TIME_UNSET;
}
/**
* Returns the offset between the input of {@link #adjustSampleTimestamp(long)} and its output.
* If {@link #DO_NOT_OFFSET} was provided to the constructor, 0 is returned. If the timestamp
* adjuster is yet not initialized, {@link C#TIME_UNSET} is returned.
*
* @return The offset between {@link #adjustSampleTimestamp(long)}'s input and output.
* {@link C#TIME_UNSET} if the adjuster is not yet initialized and 0 if timestamps should not
* be offset.
*/
public long getTimestampOffsetUs() {
return firstSampleTimestampUs == DO_NOT_OFFSET ? 0
: lastSampleTimestamp == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs;
}
/**
* Resets the instance to its initial state.
*/
public void reset() {
......
......@@ -127,6 +127,7 @@ import java.util.List;
public static final int TYPE_mean = Util.getIntegerCodeForString("mean");
public static final int TYPE_name = Util.getIntegerCodeForString("name");
public static final int TYPE_data = Util.getIntegerCodeForString("data");
public static final int TYPE_emsg = Util.getIntegerCodeForString("emsg");
public static final int TYPE_st3d = Util.getIntegerCodeForString("st3d");
public static final int TYPE_sv3d = Util.getIntegerCodeForString("sv3d");
public static final int TYPE_proj = Util.getIntegerCodeForString("proj");
......
......@@ -188,7 +188,7 @@ import com.google.android.exoplayer2.util.Util;
if (atomType == Atom.TYPE_data) {
data.skipBytes(8); // version (1), flags (3), empty (4)
String value = data.readNullTerminatedString(atomSize - 16);
return new TextInformationFrame(id, value);
return new TextInformationFrame(id, null, value);
}
Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type));
return null;
......@@ -213,7 +213,7 @@ import com.google.android.exoplayer2.util.Util;
value = Math.min(1, value);
}
if (value >= 0) {
return isTextInformationFrame ? new TextInformationFrame(id, Integer.toString(value))
return isTextInformationFrame ? new TextInformationFrame(id, null, Integer.toString(value))
: new CommentFrame(LANGUAGE_UNDEFINED, id, Integer.toString(value));
}
Log.w(TAG, "Failed to parse uint8 attribute: " + Atom.getAtomTypeString(type));
......@@ -228,12 +228,12 @@ import com.google.android.exoplayer2.util.Util;
data.skipBytes(10); // version (1), flags (3), empty (4), empty (2)
int index = data.readUnsignedShort();
if (index > 0) {
String description = "" + index;
String value = "" + index;
int count = data.readUnsignedShort();
if (count > 0) {
description += "/" + count;
value += "/" + count;
}
return new TextInformationFrame(attributeName, description);
return new TextInformationFrame(attributeName, null, value);
}
}
Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type));
......@@ -245,7 +245,7 @@ import com.google.android.exoplayer2.util.Util;
String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length)
? STANDARD_GENRES[genreCode - 1] : null;
if (genreString != null) {
return new TextInformationFrame("TCON", genreString);
return new TextInformationFrame("TCON", null, genreString);
}
Log.w(TAG, "Failed to parse standard genre code");
return null;
......
......@@ -83,8 +83,11 @@ public final class RawCcExtractor implements Extractor {
while (true) {
switch (parserState) {
case STATE_READING_HEADER:
parseHeader(input);
parserState = STATE_READING_TIMESTAMP_AND_COUNT;
if (parseHeader(input)) {
parserState = STATE_READING_TIMESTAMP_AND_COUNT;
} else {
return RESULT_END_OF_INPUT;
}
break;
case STATE_READING_TIMESTAMP_AND_COUNT:
if (parseTimestampAndSampleCount(input)) {
......@@ -114,14 +117,18 @@ public final class RawCcExtractor implements Extractor {
// Do nothing
}
private void parseHeader(ExtractorInput input) throws IOException, InterruptedException {
private boolean parseHeader(ExtractorInput input) throws IOException, InterruptedException {
dataScratch.reset();
input.readFully(dataScratch.data, 0, HEADER_SIZE);
if (dataScratch.readInt() != HEADER_ID) {
throw new IOException("Input not RawCC");
if (input.readFully(dataScratch.data, 0, HEADER_SIZE, true)) {
if (dataScratch.readInt() != HEADER_ID) {
throw new IOException("Input not RawCC");
}
version = dataScratch.readUnsignedByte();
// no versions use the flag fields yet
return true;
} else {
return false;
}
version = dataScratch.readUnsignedByte();
// no versions use the flag fields yet
}
private boolean parseTimestampAndSampleCount(ExtractorInput input) throws IOException,
......
......@@ -28,11 +28,14 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
*/
public final class SpliceInfoSectionReader implements SectionPayloadReader {
private TimestampAdjuster timestampAdjuster;
private TrackOutput output;
private boolean formatDeclared;
@Override
public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
TsPayloadReader.TrackIdGenerator idGenerator) {
this.timestampAdjuster = timestampAdjuster;
output = extractorOutput.track(idGenerator.getNextId());
output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35, null,
Format.NO_VALUE, null));
......@@ -40,9 +43,19 @@ public final class SpliceInfoSectionReader implements SectionPayloadReader {
@Override
public void consume(ParsableByteArray sectionData) {
if (!formatDeclared) {
if (timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET) {
// There is not enough information to initialize the timestamp adjuster.
return;
}
output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35,
timestampAdjuster.getTimestampOffsetUs()));
formatDeclared = true;
}
int sampleSize = sectionData.bytesLeft();
output.sampleData(sectionData, sampleSize);
output.sampleMetadata(0, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
output.sampleMetadata(timestampAdjuster.getLastAdjustedTimestampUs(), C.BUFFER_FLAG_KEY_FRAME,
sampleSize, 0, null);
}
}
......@@ -270,7 +270,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
*/
protected MediaCodecInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector,
Format format, boolean requiresSecureDecoder) throws DecoderQueryException {
return mediaCodecSelector.getDecoderInfo(format.sampleMimeType, requiresSecureDecoder, false);
return mediaCodecSelector.getDecoderInfo(format.sampleMimeType, requiresSecureDecoder);
}
/**
......
......@@ -29,9 +29,9 @@ public interface MediaCodecSelector {
MediaCodecSelector DEFAULT = new MediaCodecSelector() {
@Override
public MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder,
boolean requiresTunneling) throws DecoderQueryException {
return MediaCodecUtil.getDecoderInfo(mimeType, requiresSecureDecoder, requiresTunneling);
public MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder)
throws DecoderQueryException {
return MediaCodecUtil.getDecoderInfo(mimeType, requiresSecureDecoder);
}
@Override
......@@ -46,13 +46,11 @@ public interface MediaCodecSelector {
*
* @param mimeType The mime type for which a decoder is required.
* @param requiresSecureDecoder Whether a secure decoder is required.
* @param requiresTunneling Whether a decoder that supports tunneling is required.
* @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder
* exists.
* @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists.
* @throws DecoderQueryException Thrown if there was an error querying decoders.
*/
MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder,
boolean requiresTunneling) throws DecoderQueryException;
MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder)
throws DecoderQueryException;
/**
* Selects a decoder to instantiate for audio passthrough.
......
......@@ -81,9 +81,8 @@ public final class MediaCodecUtil {
/**
* Optional call to warm the codec cache for a given mime type.
* <p>
* Calling this method may speed up subsequent calls to
* {@link #getDecoderInfo(String, boolean, boolean)} and
* {@link #getDecoderInfos(String, boolean)}.
* Calling this method may speed up subsequent calls to {@link #getDecoderInfo(String, boolean)}
* and {@link #getDecoderInfos(String, boolean)}.
*
* @param mimeType The mime type.
* @param secure Whether the decoder is required to support secure decryption. Always pass false
......@@ -115,26 +114,14 @@ public final class MediaCodecUtil {
* @param mimeType The mime type.
* @param secure Whether the decoder is required to support secure decryption. Always pass false
* unless secure decryption really is required.
* @param tunneling Whether the decoder is required to support tunneling. Always pass false unless
* tunneling really is required.
* @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder
* exists.
* @throws DecoderQueryException If there was an error querying the available decoders.
*/
public static MediaCodecInfo getDecoderInfo(String mimeType, boolean secure, boolean tunneling)
public static MediaCodecInfo getDecoderInfo(String mimeType, boolean secure)
throws DecoderQueryException {
List<MediaCodecInfo> decoderInfos = getDecoderInfos(mimeType, secure);
if (tunneling) {
for (int i = 0; i < decoderInfos.size(); i++) {
MediaCodecInfo decoderInfo = decoderInfos.get(i);
if (decoderInfo.tunneling) {
return decoderInfo;
}
}
return null;
} else {
return decoderInfos.isEmpty() ? null : decoderInfos.get(0);
}
return decoderInfos.isEmpty() ? null : decoderInfos.get(0);
}
/**
......@@ -305,7 +292,7 @@ public final class MediaCodecUtil {
public static int maxH264DecodableFrameSize() throws DecoderQueryException {
if (maxH264DecodableFrameSize == -1) {
int result = 0;
MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false, false);
MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false);
if (decoderInfo != null) {
for (CodecProfileLevel profileLevel : decoderInfo.getProfileLevels()) {
result = Math.max(avcLevelToMaxFrameSize(profileLevel.level), result);
......
......@@ -21,21 +21,12 @@ package com.google.android.exoplayer2.metadata;
public interface MetadataDecoder {
/**
* Checks whether the decoder supports a given mime type.
* Decodes a {@link Metadata} element from the provided input buffer.
*
* @param mimeType A metadata mime type.
* @return Whether the mime type is supported.
*/
boolean canDecode(String mimeType);
/**
* Decodes a metadata object from the provided binary data.
*
* @param data The raw binary data from which to decode the metadata.
* @param size The size of the input data.
* @param inputBuffer The input buffer to decode.
* @return The decoded metadata object.
* @throws MetadataDecoderException If a problem occurred decoding the data.
*/
Metadata decode(byte[] data, int size) throws MetadataDecoderException;
Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException;
}
/*
* 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.metadata;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.metadata.scte35.SpliceInfoDecoder;
import com.google.android.exoplayer2.util.MimeTypes;
/**
* A factory for {@link MetadataDecoder} instances.
*/
public interface MetadataDecoderFactory {
/**
* Returns whether the factory is able to instantiate a {@link MetadataDecoder} for the given
* {@link Format}.
*
* @param format The {@link Format}.
* @return Whether the factory can instantiate a suitable {@link MetadataDecoder}.
*/
boolean supportsFormat(Format format);
/**
* Creates a {@link MetadataDecoder} for the given {@link Format}.
*
* @param format The {@link Format}.
* @return A new {@link MetadataDecoder}.
* @throws IllegalArgumentException If the {@link Format} is not supported.
*/
MetadataDecoder createDecoder(Format format);
/**
* Default {@link MetadataDecoder} implementation.
* <p>
* The formats supported by this factory are:
* <ul>
* <li>ID3 ({@link Id3Decoder})</li>
* <li>EMSG ({@link EventMessageDecoder})</li>
* <li>SCTE-35 ({@link SpliceInfoDecoder})</li>
* </ul>
*/
MetadataDecoderFactory DEFAULT = new MetadataDecoderFactory() {
@Override
public boolean supportsFormat(Format format) {
return getDecoderClass(format.sampleMimeType) != null;
}
@Override
public MetadataDecoder createDecoder(Format format) {
try {
Class<?> clazz = getDecoderClass(format.sampleMimeType);
if (clazz == null) {
throw new IllegalArgumentException("Attempted to create decoder for unsupported format");
}
return clazz.asSubclass(MetadataDecoder.class).getConstructor().newInstance();
} catch (Exception e) {
throw new IllegalStateException("Unexpected error instantiating decoder", e);
}
}
private Class<?> getDecoderClass(String mimeType) {
if (mimeType == null) {
return null;
}
try {
switch (mimeType) {
case MimeTypes.APPLICATION_ID3:
return Class.forName("com.google.android.exoplayer2.metadata.id3.Id3Decoder");
case MimeTypes.APPLICATION_EMSG:
return Class.forName("com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder");
case MimeTypes.APPLICATION_SCTE35:
return Class.forName("com.google.android.exoplayer2.metadata.scte35.SpliceInfoDecoder");
default:
return null;
}
} catch (ClassNotFoundException e) {
return null;
}
}
};
}
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
/**
* A {@link DecoderInputBuffer} for a {@link MetadataDecoder}.
*/
public final class MetadataInputBuffer extends DecoderInputBuffer {
/**
* An offset that must be added to the metadata's timestamps after it's been decoded, or
* {@link Format#OFFSET_SAMPLE_RELATIVE} if {@link #timeUs} should be added.
*/
public long subsampleOffsetUs;
public MetadataInputBuffer() {
super(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
}
}
......@@ -24,9 +24,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.util.Assertions;
import java.nio.ByteBuffer;
/**
* A renderer for metadata.
......@@ -49,12 +47,13 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
private static final int MSG_INVOKE_RENDERER = 0;
private final MetadataDecoder metadataDecoder;
private final MetadataDecoderFactory decoderFactory;
private final Output output;
private final Handler outputHandler;
private final FormatHolder formatHolder;
private final DecoderInputBuffer buffer;
private final MetadataInputBuffer buffer;
private MetadataDecoder decoder;
private boolean inputStreamEnded;
private long pendingMetadataTimestamp;
private Metadata pendingMetadata;
......@@ -66,21 +65,38 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
* looper associated with the application's main thread, which can be obtained using
* {@link android.app.Activity#getMainLooper()}. Null may be passed if the output should be
* called directly on the player's internal rendering thread.
* @param metadataDecoder A decoder for the metadata.
*/
public MetadataRenderer(Output output, Looper outputLooper, MetadataDecoder metadataDecoder) {
public MetadataRenderer(Output output, Looper outputLooper) {
this(output, outputLooper, MetadataDecoderFactory.DEFAULT);
}
/**
* @param output The output.
* @param outputLooper The looper associated with the thread on which the output should be called.
* If the output makes use of standard Android UI components, then this should normally be the
* looper associated with the application's main thread, which can be obtained using
* {@link android.app.Activity#getMainLooper()}. Null may be passed if the output should be
* called directly on the player's internal rendering thread.
* @param decoderFactory A factory from which to obtain {@link MetadataDecoder} instances.
*/
public MetadataRenderer(Output output, Looper outputLooper,
MetadataDecoderFactory decoderFactory) {
super(C.TRACK_TYPE_METADATA);
this.output = Assertions.checkNotNull(output);
this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this);
this.metadataDecoder = Assertions.checkNotNull(metadataDecoder);
this.decoderFactory = Assertions.checkNotNull(decoderFactory);
formatHolder = new FormatHolder();
buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
buffer = new MetadataInputBuffer();
}
@Override
public int supportsFormat(Format format) {
return metadataDecoder.canDecode(format.sampleMimeType) ? FORMAT_HANDLED
: FORMAT_UNSUPPORTED_TYPE;
return decoderFactory.supportsFormat(format) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE;
}
@Override
protected void onStreamChanged(Format[] formats) throws ExoPlaybackException {
decoder = decoderFactory.createDecoder(formats[0]);
}
@Override
......@@ -97,12 +113,16 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
if (result == C.RESULT_BUFFER_READ) {
if (buffer.isEndOfStream()) {
inputStreamEnded = true;
} else if (buffer.isDecodeOnly()) {
// Do nothing. Note this assumes that all metadata buffers can be decoded independently.
// If we ever need to support a metadata format where this is not the case, we'll need to
// pass the buffer to the decoder and discard the output.
} else {
pendingMetadataTimestamp = buffer.timeUs;
buffer.subsampleOffsetUs = formatHolder.format.subsampleOffsetUs;
buffer.flip();
try {
buffer.flip();
ByteBuffer bufferData = buffer.data;
pendingMetadata = metadataDecoder.decode(bufferData.array(), bufferData.limit());
pendingMetadata = decoder.decode(buffer);
} catch (MetadataDecoderException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
......@@ -119,6 +139,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
@Override
protected void onDisabled() {
pendingMetadata = null;
decoder = null;
super.onDisabled();
}
......
/*
* 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.metadata.emsg;
import android.os.Parcel;
import android.os.Parcelable;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
/**
* An Event Message (emsg) as defined in ISO 23009-1.
*/
public final class EventMessage implements Metadata.Entry {
/**
* The message scheme.
*/
public final String schemeIdUri;
/**
* The value for the event.
*/
public final String value;
/**
* The duration of the event in milliseconds.
*/
public final long durationMs;
/**
* The instance identifier.
*/
public final long id;
/**
* The body of the message.
*/
public final byte[] messageData;
// Lazily initialized hashcode.
private int hashCode;
/**
*
* @param schemeIdUri The message scheme.
* @param value The value for the event.
* @param durationMs The duration of the event in milliseconds.
* @param id The instance identifier.
* @param messageData The body of the message.
*/
public EventMessage(String schemeIdUri, String value, long durationMs, long id,
byte[] messageData) {
this.schemeIdUri = schemeIdUri;
this.value = value;
this.durationMs = durationMs;
this.id = id;
this.messageData = messageData;
}
/* package */ EventMessage(Parcel in) {
schemeIdUri = in.readString();
value = in.readString();
durationMs = in.readLong();
id = in.readLong();
messageData = in.createByteArray();
}
@Override
public int hashCode() {
if (hashCode == 0) {
int result = 17;
result = 31 * result + (schemeIdUri != null ? schemeIdUri.hashCode() : 0);
result = 31 * result + (value != null ? value.hashCode() : 0);
result = 31 * result + (int) (durationMs ^ (durationMs >>> 32));
result = 31 * result + (int) (id ^ (id >>> 32));
result = 31 * result + Arrays.hashCode(messageData);
hashCode = result;
}
return hashCode;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
EventMessage other = (EventMessage) obj;
return durationMs == other.durationMs && id == other.id
&& Util.areEqual(schemeIdUri, other.schemeIdUri) && Util.areEqual(value, other.value)
&& Arrays.equals(messageData, other.messageData);
}
// Parcelable implementation.
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(schemeIdUri);
dest.writeString(value);
dest.writeLong(durationMs);
dest.writeLong(id);
dest.writeByteArray(messageData);
}
public static final Parcelable.Creator<EventMessage> CREATOR =
new Parcelable.Creator<EventMessage>() {
@Override
public EventMessage createFromParcel(Parcel in) {
return new EventMessage(in);
}
@Override
public EventMessage[] newArray(int size) {
return new EventMessage[size];
}
};
}
/*
* 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.metadata.emsg;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoder;
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
* Decodes Event Message (emsg) atoms, as defined in ISO 23009-1.
* <p>
* Atom data should be provided to the decoder without the full atom header (i.e. starting from the
* first byte of the scheme_id_uri field).
*/
public final class EventMessageDecoder implements MetadataDecoder {
@Override
public Metadata decode(MetadataInputBuffer inputBuffer) {
ByteBuffer buffer = inputBuffer.data;
byte[] data = buffer.array();
int size = buffer.limit();
ParsableByteArray emsgData = new ParsableByteArray(data, size);
String schemeIdUri = emsgData.readNullTerminatedString();
String value = emsgData.readNullTerminatedString();
long timescale = emsgData.readUnsignedInt();
emsgData.skipBytes(4); // presentation_time_delta
long durationMs = (emsgData.readUnsignedInt() * 1000) / timescale;
long id = emsgData.readUnsignedInt();
byte[] messageData = Arrays.copyOfRange(data, emsgData.getPosition(), size);
return new Metadata(new EventMessage(schemeIdUri, value, durationMs, id, messageData));
}
}
/*
* 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.metadata.id3;
import android.os.Parcel;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
/**
* Chapter information ID3 frame.
*/
public final class ChapterFrame extends Id3Frame {
public static final String ID = "CHAP";
public final String chapterId;
public final int startTimeMs;
public final int endTimeMs;
/**
* The byte offset of the start of the chapter, or {@link C#POSITION_UNSET} if not set.
*/
public final long startOffset;
/**
* The byte offset of the end of the chapter, or {@link C#POSITION_UNSET} if not set.
*/
public final long endOffset;
private final Id3Frame[] subFrames;
public ChapterFrame(String chapterId, int startTimeMs, int endTimeMs, long startOffset,
long endOffset, Id3Frame[] subFrames) {
super(ID);
this.chapterId = chapterId;
this.startTimeMs = startTimeMs;
this.endTimeMs = endTimeMs;
this.startOffset = startOffset;
this.endOffset = endOffset;
this.subFrames = subFrames;
}
/* package */ ChapterFrame(Parcel in) {
super(ID);
this.chapterId = in.readString();
this.startTimeMs = in.readInt();
this.endTimeMs = in.readInt();
this.startOffset = in.readLong();
this.endOffset = in.readLong();
int subFrameCount = in.readInt();
subFrames = new Id3Frame[subFrameCount];
for (int i = 0; i < subFrameCount; i++) {
subFrames[i] = in.readParcelable(Id3Frame.class.getClassLoader());
}
}
/**
* Returns the number of sub-frames.
*/
public int getSubFrameCount() {
return subFrames.length;
}
/**
* Returns the sub-frame at {@code index}.
*/
public Id3Frame getSubFrame(int index) {
return subFrames[index];
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
ChapterFrame other = (ChapterFrame) obj;
return startTimeMs == other.startTimeMs
&& endTimeMs == other.endTimeMs
&& startOffset == other.startOffset
&& endOffset == other.endOffset
&& Util.areEqual(chapterId, other.chapterId)
&& Arrays.equals(subFrames, other.subFrames);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + startTimeMs;
result = 31 * result + endTimeMs;
result = 31 * result + (int) startOffset;
result = 31 * result + (int) endOffset;
result = 31 * result + (chapterId != null ? chapterId.hashCode() : 0);
return result;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(chapterId);
dest.writeInt(startTimeMs);
dest.writeInt(endTimeMs);
dest.writeLong(startOffset);
dest.writeLong(endOffset);
dest.writeInt(subFrames.length);
for (Id3Frame subFrame : subFrames) {
dest.writeParcelable(subFrame, 0);
}
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<ChapterFrame> CREATOR = new Creator<ChapterFrame>() {
@Override
public ChapterFrame createFromParcel(Parcel in) {
return new ChapterFrame(in);
}
@Override
public ChapterFrame[] newArray(int size) {
return new ChapterFrame[size];
}
};
}
/*
* 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.metadata.id3;
import android.os.Parcel;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
/**
* Chapter table of contents ID3 frame.
*/
public final class ChapterTocFrame extends Id3Frame {
public static final String ID = "CTOC";
public final String elementId;
public final boolean isRoot;
public final boolean isOrdered;
public final String[] children;
private final Id3Frame[] subFrames;
public ChapterTocFrame(String elementId, boolean isRoot, boolean isOrdered, String[] children,
Id3Frame[] subFrames) {
super(ID);
this.elementId = elementId;
this.isRoot = isRoot;
this.isOrdered = isOrdered;
this.children = children;
this.subFrames = subFrames;
}
/* package */ ChapterTocFrame(Parcel in) {
super(ID);
this.elementId = in.readString();
this.isRoot = in.readByte() != 0;
this.isOrdered = in.readByte() != 0;
this.children = in.createStringArray();
int subFrameCount = in.readInt();
subFrames = new Id3Frame[subFrameCount];
for (int i = 0; i < subFrameCount; i++) {
subFrames[i] = in.readParcelable(Id3Frame.class.getClassLoader());
}
}
/**
* Returns the number of sub-frames.
*/
public int getSubFrameCount() {
return subFrames.length;
}
/**
* Returns the sub-frame at {@code index}.
*/
public Id3Frame getSubFrame(int index) {
return subFrames[index];
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
ChapterTocFrame other = (ChapterTocFrame) obj;
return isRoot == other.isRoot
&& isOrdered == other.isOrdered
&& Util.areEqual(elementId, other.elementId)
&& Arrays.equals(children, other.children)
&& Arrays.equals(subFrames, other.subFrames);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (isRoot ? 1 : 0);
result = 31 * result + (isOrdered ? 1 : 0);
result = 31 * result + (elementId != null ? elementId.hashCode() : 0);
return result;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(elementId);
dest.writeByte((byte) (isRoot ? 1 : 0));
dest.writeByte((byte) (isOrdered ? 1 : 0));
dest.writeStringArray(children);
dest.writeInt(subFrames.length);
for (int i = 0; i < subFrames.length; i++) {
dest.writeParcelable(subFrames[i], 0);
}
}
public static final Creator<ChapterTocFrame> CREATOR = new Creator<ChapterTocFrame>() {
@Override
public ChapterTocFrame createFromParcel(Parcel in) {
return new ChapterTocFrame(in);
}
@Override
public ChapterTocFrame[] newArray(int size) {
return new ChapterTocFrame[size];
}
};
}
......@@ -20,20 +20,23 @@ import android.os.Parcelable;
import com.google.android.exoplayer2.util.Util;
/**
* Text information ("T000" - "TZZZ", excluding "TXXX") ID3 frame.
* Text information ID3 frame.
*/
public final class TextInformationFrame extends Id3Frame {
public final String description;
public final String value;
public TextInformationFrame(String id, String description) {
public TextInformationFrame(String id, String description, String value) {
super(id);
this.description = description;
this.value = value;
}
/* package */ TextInformationFrame(Parcel in) {
super(in.readString());
description = in.readString();
value = in.readString();
}
@Override
......@@ -45,7 +48,8 @@ public final class TextInformationFrame extends Id3Frame {
return false;
}
TextInformationFrame other = (TextInformationFrame) obj;
return id.equals(other.id) && Util.areEqual(description, other.description);
return id.equals(other.id) && Util.areEqual(description, other.description)
&& Util.areEqual(value, other.value);
}
@Override
......@@ -53,6 +57,7 @@ public final class TextInformationFrame extends Id3Frame {
int result = 17;
result = 31 * result + id.hashCode();
result = 31 * result + (description != null ? description.hashCode() : 0);
result = 31 * result + (value != null ? value.hashCode() : 0);
return result;
}
......@@ -60,6 +65,7 @@ public final class TextInformationFrame extends Id3Frame {
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id);
dest.writeString(description);
dest.writeString(value);
}
public static final Parcelable.Creator<TextInformationFrame> CREATOR =
......
/*
* Copyright (C) 2016 The Android Open Source Project
* 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.
......@@ -20,25 +20,23 @@ import android.os.Parcelable;
import com.google.android.exoplayer2.util.Util;
/**
* TXXX (User defined text information) ID3 frame.
* Url link ID3 frame.
*/
public final class TxxxFrame extends Id3Frame {
public static final String ID = "TXXX";
public final class UrlLinkFrame extends Id3Frame {
public final String description;
public final String value;
public final String url;
public TxxxFrame(String description, String value) {
super(ID);
public UrlLinkFrame(String id, String description, String url) {
super(id);
this.description = description;
this.value = value;
this.url = url;
}
/* package */ TxxxFrame(Parcel in) {
super(ID);
/* package */ UrlLinkFrame(Parcel in) {
super(in.readString());
description = in.readString();
value = in.readString();
url = in.readString();
}
@Override
......@@ -49,36 +47,40 @@ public final class TxxxFrame extends Id3Frame {
if (obj == null || getClass() != obj.getClass()) {
return false;
}
TxxxFrame other = (TxxxFrame) obj;
return Util.areEqual(description, other.description) && Util.areEqual(value, other.value);
UrlLinkFrame other = (UrlLinkFrame) obj;
return id.equals(other.id) && Util.areEqual(description, other.description)
&& Util.areEqual(url, other.url);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + id.hashCode();
result = 31 * result + (description != null ? description.hashCode() : 0);
result = 31 * result + (value != null ? value.hashCode() : 0);
result = 31 * result + (url != null ? url.hashCode() : 0);
return result;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id);
dest.writeString(description);
dest.writeString(value);
dest.writeString(url);
}
public static final Parcelable.Creator<TxxxFrame> CREATOR = new Parcelable.Creator<TxxxFrame>() {
public static final Parcelable.Creator<UrlLinkFrame> CREATOR =
new Parcelable.Creator<UrlLinkFrame>() {
@Override
public TxxxFrame createFromParcel(Parcel in) {
return new TxxxFrame(in);
}
@Override
public UrlLinkFrame createFromParcel(Parcel in) {
return new UrlLinkFrame(in);
}
@Override
public TxxxFrame[] newArray(int size) {
return new TxxxFrame[size];
}
@Override
public UrlLinkFrame[] newArray(int size) {
return new UrlLinkFrame[size];
}
};
};
}
......@@ -15,13 +15,13 @@
*/
package com.google.android.exoplayer2.metadata.scte35;
import android.text.TextUtils;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoder;
import com.google.android.exoplayer2.metadata.MetadataDecoderException;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.nio.ByteBuffer;
/**
* Decodes splice info sections and produces splice commands.
......@@ -43,12 +43,10 @@ public final class SpliceInfoDecoder implements MetadataDecoder {
}
@Override
public boolean canDecode(String mimeType) {
return TextUtils.equals(mimeType, MimeTypes.APPLICATION_SCTE35);
}
@Override
public Metadata decode(byte[] data, int size) throws MetadataDecoderException {
public Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException {
ByteBuffer buffer = inputBuffer.data;
byte[] data = buffer.array();
int size = buffer.limit();
sectionData.reset(data, size);
sectionHeader.reset(data, size);
// table_id(8), section_syntax_indicator(1), private_indicator(1), reserved(2),
......
......@@ -26,10 +26,12 @@ import java.io.IOException;
* Wraps a {@link MediaPeriod} and clips its {@link SampleStream}s to provide a subsequence of their
* samples.
*/
/* package */ final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
/**
* The {@link MediaPeriod} wrapped by this clipping media period.
*/
public final MediaPeriod mediaPeriod;
private final ClippingMediaSource mediaSource;
private MediaPeriod.Callback callback;
private long startUs;
......@@ -40,18 +42,31 @@ import java.io.IOException;
/**
* Creates a new clipping media period that provides a clipped view of the specified
* {@link MediaPeriod}'s sample streams.
* <p>
* The clipping start/end positions must be specified by calling {@link #setClipping(long, long)}
* on the playback thread before preparation completes.
*
* @param mediaPeriod The media period to clip.
* @param mediaSource The {@link ClippingMediaSource} to which this period belongs.
*/
public ClippingMediaPeriod(MediaPeriod mediaPeriod, ClippingMediaSource mediaSource) {
public ClippingMediaPeriod(MediaPeriod mediaPeriod) {
this.mediaPeriod = mediaPeriod;
this.mediaSource = mediaSource;
startUs = C.TIME_UNSET;
endUs = C.TIME_UNSET;
sampleStreams = new ClippingSampleStream[0];
}
/**
* Sets the clipping start/end times for this period, in microseconds.
*
* @param startUs The clipping start time, in microseconds.
* @param endUs The clipping end time, in microseconds, or {@link C#TIME_END_OF_SOURCE} to
* indicate the end of the period.
*/
public void setClipping(long startUs, long endUs) {
this.startUs = startUs;
this.endUs = endUs;
}
@Override
public void prepare(MediaPeriod.Callback callback) {
this.callback = callback;
......@@ -80,7 +95,8 @@ import java.io.IOException;
long enablePositionUs = mediaPeriod.selectTracks(selections, mayRetainStreamFlags,
internalStreams, streamResetFlags, positionUs + startUs);
Assertions.checkState(enablePositionUs == positionUs + startUs
|| (enablePositionUs >= startUs && enablePositionUs <= endUs));
|| (enablePositionUs >= startUs
&& (endUs == C.TIME_END_OF_SOURCE || enablePositionUs <= endUs)));
for (int i = 0; i < streams.length; i++) {
if (internalStreams[i] == null) {
sampleStreams[i] = null;
......@@ -110,14 +126,16 @@ import java.io.IOException;
if (discontinuityUs == C.TIME_UNSET) {
return C.TIME_UNSET;
}
Assertions.checkState(discontinuityUs >= startUs && discontinuityUs <= endUs);
Assertions.checkState(discontinuityUs >= startUs);
Assertions.checkState(endUs == C.TIME_END_OF_SOURCE || discontinuityUs <= endUs);
return discontinuityUs - startUs;
}
@Override
public long getBufferedPositionUs() {
long bufferedPositionUs = mediaPeriod.getBufferedPositionUs();
if (bufferedPositionUs == C.TIME_END_OF_SOURCE || bufferedPositionUs >= endUs) {
if (bufferedPositionUs == C.TIME_END_OF_SOURCE
|| (endUs != C.TIME_END_OF_SOURCE && bufferedPositionUs >= endUs)) {
return C.TIME_END_OF_SOURCE;
}
return Math.max(0, bufferedPositionUs - startUs);
......@@ -131,14 +149,16 @@ import java.io.IOException;
}
}
long seekUs = mediaPeriod.seekToUs(positionUs + startUs);
Assertions.checkState(seekUs == positionUs + startUs || (seekUs >= startUs && seekUs <= endUs));
Assertions.checkState(seekUs == positionUs + startUs
|| (seekUs >= startUs && (endUs == C.TIME_END_OF_SOURCE || seekUs <= endUs)));
return seekUs - startUs;
}
@Override
public long getNextLoadPositionUs() {
long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs();
if (nextLoadPositionUs == C.TIME_END_OF_SOURCE || nextLoadPositionUs >= endUs) {
if (nextLoadPositionUs == C.TIME_END_OF_SOURCE
|| (endUs != C.TIME_END_OF_SOURCE && nextLoadPositionUs >= endUs)) {
return C.TIME_END_OF_SOURCE;
}
return nextLoadPositionUs - startUs;
......@@ -153,8 +173,6 @@ import java.io.IOException;
@Override
public void onPrepared(MediaPeriod mediaPeriod) {
startUs = mediaSource.getStartUs();
endUs = mediaSource.getEndUs();
Assertions.checkState(startUs != C.TIME_UNSET && endUs != C.TIME_UNSET);
// If the clipping start position is non-zero, the clipping sample streams will adjust
// timestamps on buffers they read from the unclipped sample streams. These adjusted buffer
......@@ -217,21 +235,24 @@ import java.io.IOException;
if (pendingDiscontinuity) {
return C.RESULT_NOTHING_READ;
}
if (buffer == null) {
return stream.readData(formatHolder, null);
}
if (sentEos) {
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
return C.RESULT_BUFFER_READ;
}
int result = stream.readData(formatHolder, buffer);
// TODO: Clear gapless playback metadata if a format was read (if applicable).
if ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs)
|| (result == C.RESULT_NOTHING_READ
&& mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE)) {
if (endUs != C.TIME_END_OF_SOURCE && ((result == C.RESULT_BUFFER_READ
&& buffer.timeUs >= endUs) || (result == C.RESULT_NOTHING_READ
&& mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) {
buffer.clear();
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
sentEos = true;
return C.RESULT_BUFFER_READ;
}
if (result == C.RESULT_BUFFER_READ) {
if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream()) {
buffer.timeUs -= startUs;
}
return result;
......

1.09 KB | W: | H:

354 Bytes | W: | H:

library/src/main/res/drawable-hdpi/exo_controls_fastforward.png
library/src/main/res/drawable-hdpi/exo_controls_fastforward.png
library/src/main/res/drawable-hdpi/exo_controls_fastforward.png
library/src/main/res/drawable-hdpi/exo_controls_fastforward.png
  • 2-up
  • Swipe
  • Onion skin

1.05 KB | W: | H:

323 Bytes | W: | H:

library/src/main/res/drawable-hdpi/exo_controls_next.png
library/src/main/res/drawable-hdpi/exo_controls_next.png
library/src/main/res/drawable-hdpi/exo_controls_next.png
library/src/main/res/drawable-hdpi/exo_controls_next.png
  • 2-up
  • Swipe
  • Onion skin

599 Bytes | W: | H:

108 Bytes | W: | H:

library/src/main/res/drawable-hdpi/exo_controls_pause.png
library/src/main/res/drawable-hdpi/exo_controls_pause.png
library/src/main/res/drawable-hdpi/exo_controls_pause.png
library/src/main/res/drawable-hdpi/exo_controls_pause.png
  • 2-up
  • Swipe
  • Onion skin

1.14 KB | W: | H:

286 Bytes | W: | H:

library/src/main/res/drawable-hdpi/exo_controls_play.png
library/src/main/res/drawable-hdpi/exo_controls_play.png
library/src/main/res/drawable-hdpi/exo_controls_play.png
library/src/main/res/drawable-hdpi/exo_controls_play.png
  • 2-up
  • Swipe
  • Onion skin

1.04 KB | W: | H:

292 Bytes | W: | H:

library/src/main/res/drawable-hdpi/exo_controls_previous.png
library/src/main/res/drawable-hdpi/exo_controls_previous.png
library/src/main/res/drawable-hdpi/exo_controls_previous.png
library/src/main/res/drawable-hdpi/exo_controls_previous.png
  • 2-up
  • Swipe
  • Onion skin

1.22 KB | W: | H:

347 Bytes | W: | H:

library/src/main/res/drawable-hdpi/exo_controls_rewind.png
library/src/main/res/drawable-hdpi/exo_controls_rewind.png
library/src/main/res/drawable-hdpi/exo_controls_rewind.png
library/src/main/res/drawable-hdpi/exo_controls_rewind.png
  • 2-up
  • Swipe
  • Onion skin

886 Bytes | W: | H:

192 Bytes | W: | H:

library/src/main/res/drawable-ldpi/exo_controls_fastforward.png
library/src/main/res/drawable-ldpi/exo_controls_fastforward.png
library/src/main/res/drawable-ldpi/exo_controls_fastforward.png
library/src/main/res/drawable-ldpi/exo_controls_fastforward.png
  • 2-up
  • Swipe
  • Onion skin

735 Bytes | W: | H:

167 Bytes | W: | H:

library/src/main/res/drawable-ldpi/exo_controls_next.png
library/src/main/res/drawable-ldpi/exo_controls_next.png
library/src/main/res/drawable-ldpi/exo_controls_next.png
library/src/main/res/drawable-ldpi/exo_controls_next.png
  • 2-up
  • Swipe
  • Onion skin

3.17 KB | W: | H:

91 Bytes | W: | H:

library/src/main/res/drawable-ldpi/exo_controls_pause.png
library/src/main/res/drawable-ldpi/exo_controls_pause.png
library/src/main/res/drawable-ldpi/exo_controls_pause.png
library/src/main/res/drawable-ldpi/exo_controls_pause.png
  • 2-up
  • Swipe
  • Onion skin

673 Bytes | W: | H:

182 Bytes | W: | H:

library/src/main/res/drawable-ldpi/exo_controls_play.png
library/src/main/res/drawable-ldpi/exo_controls_play.png
library/src/main/res/drawable-ldpi/exo_controls_play.png
library/src/main/res/drawable-ldpi/exo_controls_play.png
  • 2-up
  • Swipe
  • Onion skin

770 Bytes | W: | H:

187 Bytes | W: | H:

library/src/main/res/drawable-ldpi/exo_controls_previous.png
library/src/main/res/drawable-ldpi/exo_controls_previous.png
library/src/main/res/drawable-ldpi/exo_controls_previous.png
library/src/main/res/drawable-ldpi/exo_controls_previous.png
  • 2-up
  • Swipe
  • Onion skin

906 Bytes | W: | H:

214 Bytes | W: | H:

library/src/main/res/drawable-ldpi/exo_controls_rewind.png
library/src/main/res/drawable-ldpi/exo_controls_rewind.png
library/src/main/res/drawable-ldpi/exo_controls_rewind.png
library/src/main/res/drawable-ldpi/exo_controls_rewind.png
  • 2-up
  • Swipe
  • Onion skin

929 Bytes | W: | H:

255 Bytes | W: | H:

library/src/main/res/drawable-mdpi/exo_controls_fastforward.png
library/src/main/res/drawable-mdpi/exo_controls_fastforward.png
library/src/main/res/drawable-mdpi/exo_controls_fastforward.png
library/src/main/res/drawable-mdpi/exo_controls_fastforward.png
  • 2-up
  • Swipe
  • Onion skin

843 Bytes | W: | H:

276 Bytes | W: | H:

library/src/main/res/drawable-mdpi/exo_controls_next.png
library/src/main/res/drawable-mdpi/exo_controls_next.png
library/src/main/res/drawable-mdpi/exo_controls_next.png
library/src/main/res/drawable-mdpi/exo_controls_next.png
  • 2-up
  • Swipe
  • Onion skin

540 Bytes | W: | H:

153 Bytes | W: | H:

library/src/main/res/drawable-mdpi/exo_controls_pause.png
library/src/main/res/drawable-mdpi/exo_controls_pause.png
library/src/main/res/drawable-mdpi/exo_controls_pause.png
library/src/main/res/drawable-mdpi/exo_controls_pause.png
  • 2-up
  • Swipe
  • Onion skin

897 Bytes | W: | H:

228 Bytes | W: | H:

library/src/main/res/drawable-mdpi/exo_controls_play.png
library/src/main/res/drawable-mdpi/exo_controls_play.png
library/src/main/res/drawable-mdpi/exo_controls_play.png
library/src/main/res/drawable-mdpi/exo_controls_play.png
  • 2-up
  • Swipe
  • Onion skin

837 Bytes | W: | H:

227 Bytes | W: | H:

library/src/main/res/drawable-mdpi/exo_controls_previous.png
library/src/main/res/drawable-mdpi/exo_controls_previous.png
library/src/main/res/drawable-mdpi/exo_controls_previous.png
library/src/main/res/drawable-mdpi/exo_controls_previous.png
  • 2-up
  • Swipe
  • Onion skin

997 Bytes | W: | H:

273 Bytes | W: | H:

library/src/main/res/drawable-mdpi/exo_controls_rewind.png
library/src/main/res/drawable-mdpi/exo_controls_rewind.png
library/src/main/res/drawable-mdpi/exo_controls_rewind.png
library/src/main/res/drawable-mdpi/exo_controls_rewind.png
  • 2-up
  • Swipe
  • Onion skin

1.44 KB | W: | H:

392 Bytes | W: | H:

library/src/main/res/drawable-xhdpi/exo_controls_fastforward.png
library/src/main/res/drawable-xhdpi/exo_controls_fastforward.png
library/src/main/res/drawable-xhdpi/exo_controls_fastforward.png
library/src/main/res/drawable-xhdpi/exo_controls_fastforward.png
  • 2-up
  • Swipe
  • Onion skin

1.33 KB | W: | H:

334 Bytes | W: | H:

library/src/main/res/drawable-xhdpi/exo_controls_next.png
library/src/main/res/drawable-xhdpi/exo_controls_next.png
library/src/main/res/drawable-xhdpi/exo_controls_next.png
library/src/main/res/drawable-xhdpi/exo_controls_next.png
  • 2-up
  • Swipe
  • Onion skin

685 Bytes | W: | H:

164 Bytes | W: | H:

library/src/main/res/drawable-xhdpi/exo_controls_pause.png
library/src/main/res/drawable-xhdpi/exo_controls_pause.png
library/src/main/res/drawable-xhdpi/exo_controls_pause.png
library/src/main/res/drawable-xhdpi/exo_controls_pause.png
  • 2-up
  • Swipe
  • Onion skin

1.58 KB | W: | H:

343 Bytes | W: | H:

library/src/main/res/drawable-xhdpi/exo_controls_play.png
library/src/main/res/drawable-xhdpi/exo_controls_play.png
library/src/main/res/drawable-xhdpi/exo_controls_play.png
library/src/main/res/drawable-xhdpi/exo_controls_play.png
  • 2-up
  • Swipe
  • Onion skin

1.34 KB | W: | H:

339 Bytes | W: | H:

library/src/main/res/drawable-xhdpi/exo_controls_previous.png
library/src/main/res/drawable-xhdpi/exo_controls_previous.png
library/src/main/res/drawable-xhdpi/exo_controls_previous.png
library/src/main/res/drawable-xhdpi/exo_controls_previous.png
  • 2-up
  • Swipe
  • Onion skin

1.64 KB | W: | H:

400 Bytes | W: | H:

library/src/main/res/drawable-xhdpi/exo_controls_rewind.png
library/src/main/res/drawable-xhdpi/exo_controls_rewind.png
library/src/main/res/drawable-xhdpi/exo_controls_rewind.png
library/src/main/res/drawable-xhdpi/exo_controls_rewind.png
  • 2-up
  • Swipe
  • Onion skin

1.29 KB | W: | H:

391 Bytes | W: | H:

library/src/main/res/drawable-xxhdpi/exo_controls_next.png
library/src/main/res/drawable-xxhdpi/exo_controls_next.png
library/src/main/res/drawable-xxhdpi/exo_controls_next.png
library/src/main/res/drawable-xxhdpi/exo_controls_next.png
  • 2-up
  • Swipe
  • Onion skin

611 Bytes | W: | H:

113 Bytes | W: | H:

library/src/main/res/drawable-xxhdpi/exo_controls_pause.png
library/src/main/res/drawable-xxhdpi/exo_controls_pause.png
library/src/main/res/drawable-xxhdpi/exo_controls_pause.png
library/src/main/res/drawable-xxhdpi/exo_controls_pause.png
  • 2-up
  • Swipe
  • Onion skin

1.16 KB | W: | H:

384 Bytes | W: | H:

library/src/main/res/drawable-xxhdpi/exo_controls_play.png
library/src/main/res/drawable-xxhdpi/exo_controls_play.png
library/src/main/res/drawable-xxhdpi/exo_controls_play.png
library/src/main/res/drawable-xxhdpi/exo_controls_play.png
  • 2-up
  • Swipe
  • Onion skin

1.26 KB | W: | H:

464 Bytes | W: | H:

library/src/main/res/drawable-xxhdpi/exo_controls_previous.png
library/src/main/res/drawable-xxhdpi/exo_controls_previous.png
library/src/main/res/drawable-xxhdpi/exo_controls_previous.png
library/src/main/res/drawable-xxhdpi/exo_controls_previous.png
  • 2-up
  • Swipe
  • Onion skin

1.17 KB | W: | H:

571 Bytes | W: | H:

library/src/main/res/drawable-xxhdpi/exo_controls_rewind.png
library/src/main/res/drawable-xxhdpi/exo_controls_rewind.png
library/src/main/res/drawable-xxhdpi/exo_controls_rewind.png
library/src/main/res/drawable-xxhdpi/exo_controls_rewind.png
  • 2-up
  • Swipe
  • Onion skin
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