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

This commit is contained in:
Julian Cable 2017-01-22 15:07:03 +00:00
commit 6ec840cc80
183 changed files with 7150 additions and 1999 deletions

View File

@ -58,7 +58,7 @@ this version.
* Fix issues that could cause ExtractorMediaSource based playbacks to get stuck * Fix issues that could cause ExtractorMediaSource based playbacks to get stuck
buffering ([#1962](https://github.com/google/ExoPlayer/issues/1962)). buffering ([#1962](https://github.com/google/ExoPlayer/issues/1962)).
* Correctly set SimpleExoPlayerView surface aspect ratio when an active player * 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 * OGG: Fix playback of short OGG files
([#1976](https://github.com/google/ExoPlayer/issues/1976)). ([#1976](https://github.com/google/ExoPlayer/issues/1976)).
* MP4: Support `.mp3` tracks * MP4: Support `.mp3` tracks

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 31 KiB

View File

@ -24,24 +24,19 @@ android {
buildTypes { buildTypes {
release { release {
minifyEnabled false shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt')
} }
debug { debug {
jniDebuggable = true jniDebuggable = true
debuggable = true
} }
} }
lintOptions {
abortOnError false
}
productFlavors { productFlavors {
noExtensions noExtensions
withExtensions withExtensions
} }
} }
dependencies { dependencies {

View File

@ -229,30 +229,6 @@
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd",
"drm_scheme": "widevine", "drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" "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"
} }
] ]
}, },

View File

@ -26,16 +26,17 @@ import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters; 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.Metadata;
import com.google.android.exoplayer2.metadata.MetadataRenderer; 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.ApicFrame;
import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.id3.CommentFrame;
import com.google.android.exoplayer2.metadata.id3.GeobFrame; import com.google.android.exoplayer2.metadata.id3.GeobFrame;
import com.google.android.exoplayer2.metadata.id3.Id3Frame; import com.google.android.exoplayer2.metadata.id3.Id3Frame;
import com.google.android.exoplayer2.metadata.id3.PrivFrame; import com.google.android.exoplayer2.metadata.id3.PrivFrame;
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; 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.AdaptiveMediaSourceEventListener;
import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
@ -55,7 +56,7 @@ import java.util.Locale;
*/ */
/* package */ final class EventLogger implements ExoPlayer.EventListener, /* package */ final class EventLogger implements ExoPlayer.EventListener,
AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener,
ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener, ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener,
MetadataRenderer.Output { MetadataRenderer.Output {
private static final String TAG = "EventLogger"; private static final String TAG = "EventLogger";
@ -153,7 +154,7 @@ import java.util.Locale;
String formatSupport = getFormatSupportString( String formatSupport = getFormatSupportString(
mappedTrackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex)); mappedTrackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex));
Log.d(TAG, " " + status + " Track:" + trackIndex + ", " Log.d(TAG, " " + status + " Track:" + trackIndex + ", "
+ getFormatString(trackGroup.getFormat(trackIndex)) + Format.toLogString(trackGroup.getFormat(trackIndex))
+ ", supported=" + formatSupport); + ", supported=" + formatSupport);
} }
Log.d(TAG, " ]"); Log.d(TAG, " ]");
@ -185,7 +186,7 @@ import java.util.Locale;
String formatSupport = getFormatSupportString( String formatSupport = getFormatSupportString(
RendererCapabilities.FORMAT_UNSUPPORTED_TYPE); RendererCapabilities.FORMAT_UNSUPPORTED_TYPE);
Log.d(TAG, " " + status + " Track:" + trackIndex + ", " Log.d(TAG, " " + status + " Track:" + trackIndex + ", "
+ getFormatString(trackGroup.getFormat(trackIndex)) + Format.toLogString(trackGroup.getFormat(trackIndex))
+ ", supported=" + formatSupport); + ", supported=" + formatSupport);
} }
Log.d(TAG, " ]"); Log.d(TAG, " ]");
@ -224,7 +225,7 @@ import java.util.Locale;
@Override @Override
public void onAudioInputFormatChanged(Format format) { 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 @Override
public void onVideoInputFormatChanged(Format format) { public void onVideoInputFormatChanged(Format format) {
Log.d(TAG, "videoFormatChanged [" + getSessionTimeString() + ", " + getFormatString(format) Log.d(TAG, "videoFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format)
+ "]"); + "]");
} }
@ -279,13 +280,23 @@ import java.util.Locale;
// Do nothing. // Do nothing.
} }
// StreamingDrmSessionManager.EventListener // DefaultDrmSessionManager.EventListener
@Override @Override
public void onDrmSessionManagerError(Exception e) { public void onDrmSessionManagerError(Exception e) {
printInternalError("drmSessionManagerError", e); printInternalError("drmSessionManagerError", e);
} }
@Override
public void onDrmKeysRestored() {
Log.d(TAG, "drmKeysRestored [" + getSessionTimeString() + "]");
}
@Override
public void onDrmKeysRemoved() {
Log.d(TAG, "drmKeysRemoved [" + getSessionTimeString() + "]");
}
@Override @Override
public void onDrmKeysLoaded() { public void onDrmKeysLoaded() {
Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]"); Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]");
@ -349,10 +360,13 @@ import java.util.Locale;
private void printMetadata(Metadata metadata, String prefix) { private void printMetadata(Metadata metadata, String prefix) {
for (int i = 0; i < metadata.length(); i++) { for (int i = 0; i < metadata.length(); i++) {
Metadata.Entry entry = metadata.get(i); Metadata.Entry entry = metadata.get(i);
if (entry instanceof TxxxFrame) { if (entry instanceof TextInformationFrame) {
TxxxFrame txxxFrame = (TxxxFrame) entry; TextInformationFrame textInformationFrame = (TextInformationFrame) entry;
Log.d(TAG, prefix + String.format("%s: description=%s, value=%s", txxxFrame.id, Log.d(TAG, prefix + String.format("%s: value=%s", textInformationFrame.id,
txxxFrame.description, txxxFrame.value)); 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) { } else if (entry instanceof PrivFrame) {
PrivFrame privFrame = (PrivFrame) entry; PrivFrame privFrame = (PrivFrame) entry;
Log.d(TAG, prefix + String.format("%s: owner=%s", privFrame.id, privFrame.owner)); 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; ApicFrame apicFrame = (ApicFrame) entry;
Log.d(TAG, prefix + String.format("%s: mimeType=%s, description=%s", Log.d(TAG, prefix + String.format("%s: mimeType=%s, description=%s",
apicFrame.id, apicFrame.mimeType, apicFrame.description)); 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) { } else if (entry instanceof CommentFrame) {
CommentFrame commentFrame = (CommentFrame) entry; 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)); commentFrame.language, commentFrame.description));
} else if (entry instanceof Id3Frame) { } else if (entry instanceof Id3Frame) {
Id3Frame id3Frame = (Id3Frame) entry; Id3Frame id3Frame = (Id3Frame) entry;
Log.d(TAG, prefix + String.format("%s", id3Frame.id)); 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, private static String getTrackStatusString(TrackSelection selection, TrackGroup group,
int trackIndex) { int trackIndex) {
return getTrackStatusString(selection != null && selection.getTrackGroup() == group return getTrackStatusString(selection != null && selection.getTrackGroup() == group

View File

@ -36,15 +36,16 @@ import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline; 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.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; 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.drm.UnsupportedDrmException;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; 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.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
@ -100,7 +101,6 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
} }
private Handler mainHandler; private Handler mainHandler;
private Timeline.Window window;
private EventLogger eventLogger; private EventLogger eventLogger;
private SimpleExoPlayerView simpleExoPlayerView; private SimpleExoPlayerView simpleExoPlayerView;
private LinearLayout debugRootView; private LinearLayout debugRootView;
@ -115,9 +115,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
private boolean playerNeedsSource; private boolean playerNeedsSource;
private boolean shouldAutoPlay; private boolean shouldAutoPlay;
private boolean isTimelineStatic; private int resumeWindow;
private int playerWindow; private long resumePosition;
private long playerPosition;
// Activity lifecycle // Activity lifecycle
@ -125,9 +124,9 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
shouldAutoPlay = true; shouldAutoPlay = true;
clearResumePosition();
mediaDataSourceFactory = buildDataSourceFactory(true); mediaDataSourceFactory = buildDataSourceFactory(true);
mainHandler = new Handler(); mainHandler = new Handler();
window = new Timeline.Window();
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) { if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER); CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
} }
@ -148,7 +147,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
@Override @Override
public void onNewIntent(Intent intent) { public void onNewIntent(Intent intent) {
releasePlayer(); releasePlayer();
isTimelineStatic = false; shouldAutoPlay = true;
clearResumePosition();
setIntent(intent); setIntent(intent);
} }
@ -264,7 +264,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
@SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode = @SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode =
((DemoApplication) getApplication()).useExtensionRenderers() ((DemoApplication) getApplication()).useExtensionRenderers()
? (preferExtensionDecoders ? SimpleExoPlayer.EXTENSION_RENDERER_MODE_PREFER ? (preferExtensionDecoders ? SimpleExoPlayer.EXTENSION_RENDERER_MODE_PREFER
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_ON) : SimpleExoPlayer.EXTENSION_RENDERER_MODE_ON)
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF; : SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF;
TrackSelection.Factory videoTrackSelectionFactory = TrackSelection.Factory videoTrackSelectionFactory =
new AdaptiveVideoTrackSelection.Factory(BANDWIDTH_METER); new AdaptiveVideoTrackSelection.Factory(BANDWIDTH_METER);
@ -278,16 +278,9 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
player.addListener(eventLogger); player.addListener(eventLogger);
player.setAudioDebugListener(eventLogger); player.setAudioDebugListener(eventLogger);
player.setVideoDebugListener(eventLogger); player.setVideoDebugListener(eventLogger);
player.setId3Output(eventLogger); player.setMetadataOutput(eventLogger);
simpleExoPlayerView.setPlayer(player); simpleExoPlayerView.setPlayer(player);
if (isTimelineStatic) {
if (playerPosition == C.TIME_UNSET) {
player.seekToDefaultPosition(playerWindow);
} else {
player.seekTo(playerWindow, playerPosition);
}
}
player.setPlayWhenReady(shouldAutoPlay); player.setPlayWhenReady(shouldAutoPlay);
debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper = new DebugTextViewHelper(player, debugTextView);
debugViewHelper.start(); debugViewHelper.start();
@ -324,7 +317,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
} }
MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0] MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
: new ConcatenatingMediaSource(mediaSources); : new ConcatenatingMediaSource(mediaSources);
player.prepare(mediaSource, !isTimelineStatic, !isTimelineStatic); player.seekTo(resumeWindow, resumePosition);
player.prepare(mediaSource, false, false);
playerNeedsSource = false; playerNeedsSource = false;
updateButtonVisibilities(); updateButtonVisibilities();
} }
@ -358,7 +352,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
} }
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl,
buildHttpDataSourceFactory(false), keyRequestProperties); buildHttpDataSourceFactory(false), keyRequestProperties);
return new StreamingDrmSessionManager<>(uuid, return new DefaultDrmSessionManager<>(uuid,
FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger); FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger);
} }
@ -367,12 +361,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
debugViewHelper.stop(); debugViewHelper.stop();
debugViewHelper = null; debugViewHelper = null;
shouldAutoPlay = player.getPlayWhenReady(); shouldAutoPlay = player.getPlayWhenReady();
playerWindow = player.getCurrentWindowIndex(); updateResumePosition();
playerPosition = C.TIME_UNSET;
Timeline timeline = player.getCurrentTimeline();
if (!timeline.isEmpty() && timeline.getWindow(playerWindow, window).isSeekable) {
playerPosition = player.getCurrentPosition();
}
player.release(); player.release();
player = null; player = null;
trackSelector = 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. * Returns a new DataSource factory.
* *
@ -427,8 +427,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest) { public void onTimelineChanged(Timeline timeline, Object manifest) {
isTimelineStatic = !timeline.isEmpty() // Do nothing.
&& !timeline.getWindow(timeline.getWindowCount() - 1, window).isDynamic;
} }
@Override @Override
@ -460,6 +459,11 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
showToast(errorString); showToast(errorString);
} }
playerNeedsSource = true; playerNeedsSource = true;
if (isBehindLiveWindow(e)) {
clearResumePosition();
} else {
updateResumePosition();
}
updateButtonVisibilities(); updateButtonVisibilities();
showControls(); showControls();
} }
@ -535,4 +539,18 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show(); 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;
}
} }

View File

@ -23,17 +23,6 @@ android {
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
lintOptions {
abortOnError false
}
sourceSets.main { sourceSets.main {
jniLibs.srcDirs = ['jniLibs'] jniLibs.srcDirs = ['jniLibs']
} }

View File

@ -57,8 +57,8 @@ import java.util.Map;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import org.chromium.net.CronetEngine; import org.chromium.net.CronetEngine;
import org.chromium.net.NetworkException;
import org.chromium.net.UrlRequest; import org.chromium.net.UrlRequest;
import org.chromium.net.UrlRequestException;
import org.chromium.net.UrlResponseInfo; import org.chromium.net.UrlResponseInfo;
import org.chromium.net.impl.UrlResponseInfoImpl; import org.chromium.net.impl.UrlResponseInfoImpl;
import org.junit.Before; import org.junit.Before;
@ -99,7 +99,7 @@ public final class CronetDataSourceTest {
@Mock @Mock
private Executor mockExecutor; private Executor mockExecutor;
@Mock @Mock
private UrlRequestException mockUrlRequestException; private NetworkException mockNetworkException;
@Mock private CronetEngine mockCronetEngine; @Mock private CronetEngine mockCronetEngine;
private CronetDataSource dataSourceUnderTest; private CronetDataSource dataSourceUnderTest;
@ -172,7 +172,7 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.onFailed( dataSourceUnderTest.onFailed(
mockUrlRequest, mockUrlRequest,
testUrlResponseInfo, testUrlResponseInfo,
mockUrlRequestException); mockNetworkException);
dataSourceUnderTest.onResponseStarted( dataSourceUnderTest.onResponseStarted(
mockUrlRequest2, mockUrlRequest2,
testUrlResponseInfo); testUrlResponseInfo);
@ -245,8 +245,8 @@ public final class CronetDataSourceTest {
@Test @Test
public void testRequestOpenFailDueToDnsFailure() { public void testRequestOpenFailDueToDnsFailure() {
mockResponseStartFailure(); mockResponseStartFailure();
when(mockUrlRequestException.getErrorCode()).thenReturn( when(mockNetworkException.getErrorCode()).thenReturn(
UrlRequestException.ERROR_HOSTNAME_NOT_RESOLVED); NetworkException.ERROR_HOSTNAME_NOT_RESOLVED);
try { try {
dataSourceUnderTest.open(testDataSpec); dataSourceUnderTest.open(testDataSpec);
@ -728,7 +728,7 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.onFailed( dataSourceUnderTest.onFailed(
mockUrlRequest, mockUrlRequest,
createUrlResponseInfo(500), // statusCode createUrlResponseInfo(500), // statusCode
mockUrlRequestException); mockNetworkException);
return null; return null;
} }
}).when(mockUrlRequest).start(); }).when(mockUrlRequest).start();
@ -764,7 +764,7 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.onFailed( dataSourceUnderTest.onFailed(
mockUrlRequest, mockUrlRequest,
createUrlResponseInfo(500), // statusCode createUrlResponseInfo(500), // statusCode
mockUrlRequestException); mockNetworkException);
return null; return null;
} }
}).when(mockUrlRequest).read(any(ByteBuffer.class)); }).when(mockUrlRequest).read(any(ByteBuffer.class));

View File

@ -40,9 +40,10 @@ import java.util.concurrent.Executor;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.chromium.net.CronetEngine; 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;
import org.chromium.net.UrlRequest.Status; import org.chromium.net.UrlRequest.Status;
import org.chromium.net.UrlRequestException;
import org.chromium.net.UrlResponseInfo; import org.chromium.net.UrlResponseInfo;
/** /**
@ -400,12 +401,17 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
@Override @Override
public synchronized void onFailed(UrlRequest request, UrlResponseInfo info, public synchronized void onFailed(UrlRequest request, UrlResponseInfo info,
UrlRequestException error) { CronetException error) {
if (request != currentUrlRequest) { if (request != currentUrlRequest) {
return; return;
} }
exception = error.getErrorCode() == UrlRequestException.ERROR_HOSTNAME_NOT_RESOLVED if (error instanceof NetworkException
? new UnknownHostException() : error; && ((NetworkException) error).getErrorCode()
== NetworkException.ERROR_HOSTNAME_NOT_RESOLVED) {
exception = new UnknownHostException();
} else {
exception = error;
}
operation.open(); operation.open();
} }

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.cronet; package com.google.android.exoplayer2.ext.cronet;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Predicate; import com.google.android.exoplayer2.util.Predicate;
@ -25,7 +26,7 @@ import org.chromium.net.CronetEngine;
/** /**
* A {@link Factory} that produces {@link CronetDataSource}. * 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. * The default connection timeout, in milliseconds.
@ -67,7 +68,7 @@ public final class CronetDataSourceFactory implements Factory {
} }
@Override @Override
public CronetDataSource createDataSource() { protected CronetDataSource createDataSourceInternal() {
return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener, return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener,
connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects); connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects);
} }

View File

@ -20,17 +20,7 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 9 minSdkVersion 9
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} consumerProguardFiles 'proguard-rules.txt'
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
lintOptions {
abortOnError false
} }
sourceSets.main { sourceSets.main {

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.ffmpeg;
import android.os.Handler; import android.os.Handler;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioCapabilities;
import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener;
@ -60,7 +61,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
} }
@Override @Override
public int supportsFormat(Format format) { protected int supportsFormatInternal(Format format) {
if (!FfmpegLibrary.isAvailable()) { if (!FfmpegLibrary.isAvailable()) {
return FORMAT_UNSUPPORTED_TYPE; return FORMAT_UNSUPPORTED_TYPE;
} }
@ -69,6 +70,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
: MimeTypes.isAudio(mimeType) ? FORMAT_UNSUPPORTED_SUBTYPE : FORMAT_UNSUPPORTED_TYPE; : MimeTypes.isAudio(mimeType) ? FORMAT_UNSUPPORTED_SUBTYPE : FORMAT_UNSUPPORTED_TYPE;
} }
@Override
public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {
return ADAPTIVE_NOT_SEAMLESS;
}
@Override @Override
protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
throws FfmpegDecoderException { throws FfmpegDecoderException {

View File

@ -20,17 +20,7 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 9 minSdkVersion 9
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} consumerProguardFiles 'proguard-rules.txt'
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
lintOptions {
abortOnError false
} }
sourceSets.main { sourceSets.main {

View File

@ -56,7 +56,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
} }
@Override @Override
public int supportsFormat(Format format) { protected int supportsFormatInternal(Format format) {
return FlacLibrary.isAvailable() && MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType) return FlacLibrary.isAvailable() && MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)
? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE; ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE;
} }

View File

@ -22,17 +22,6 @@ android {
minSdkVersion 9 minSdkVersion 9
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} }
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
lintOptions {
abortOnError false
}
} }
dependencies { dependencies {

View File

@ -261,7 +261,7 @@ public class OkHttpDataSource implements HttpDataSource {
private Request makeRequest(DataSpec dataSpec) { private Request makeRequest(DataSpec dataSpec) {
long position = dataSpec.position; long position = dataSpec.position;
long length = dataSpec.length; 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()); HttpUrl url = HttpUrl.parse(dataSpec.uri.toString());
Request.Builder builder = new Request.Builder().url(url); Request.Builder builder = new Request.Builder().url(url);

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.ext.okhttp; package com.google.android.exoplayer2.ext.okhttp;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import okhttp3.CacheControl; import okhttp3.CacheControl;
@ -24,7 +25,7 @@ import okhttp3.Call;
/** /**
* A {@link Factory} that produces {@link OkHttpDataSource}. * 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 Call.Factory callFactory;
private final String userAgent; private final String userAgent;
@ -58,7 +59,7 @@ public final class OkHttpDataSourceFactory implements Factory {
} }
@Override @Override
public OkHttpDataSource createDataSource() { protected OkHttpDataSource createDataSourceInternal() {
return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl); return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl);
} }

View File

@ -20,17 +20,7 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 9 minSdkVersion 9
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} consumerProguardFiles 'proguard-rules.txt'
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
lintOptions {
abortOnError false
} }
sourceSets.main { sourceSets.main {

View File

@ -72,7 +72,7 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
} }
@Override @Override
public int supportsFormat(Format format) { protected int supportsFormatInternal(Format format) {
return OpusLibrary.isAvailable() && MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType) return OpusLibrary.isAvailable() && MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)
? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE; ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE;
} }

View File

@ -20,17 +20,7 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 9 minSdkVersion 9
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
} consumerProguardFiles 'proguard-rules.txt'
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
lintOptions {
abortOnError false
} }
sourceSets.main { sourceSets.main {

View File

@ -1,5 +1,3 @@
import com.android.builder.core.BuilderConstants
// Copyright (C) 2016 The Android Open Source Project // Copyright (C) 2016 The Android Open Source Project
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // 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. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import com.android.builder.core.BuilderConstants
apply plugin: 'com.android.library' apply plugin: 'com.android.library'
apply plugin: 'bintray-release' apply plugin: 'bintray-release'
@ -28,13 +28,10 @@ android {
// greater. // greater.
minSdkVersion 9 minSdkVersion 9
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
} }
buildTypes { buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
// Re-enable test coverage when the following issue is fixed: // Re-enable test coverage when the following issue is fixed:
// https://code.google.com/p/android/issues/detail?id=226070 // https://code.google.com/p/android/issues/detail?id=226070
// debug { // debug {
@ -42,10 +39,6 @@ android {
// } // }
} }
lintOptions {
abortOnError false
}
sourceSets { sourceSets {
androidTest { androidTest {
java.srcDirs += "../testutils/src/main/java/" java.srcDirs += "../testutils/src/main/java/"

View File

@ -0,0 +1,7 @@
# 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);
}

View File

@ -21,7 +21,6 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.SinglePeriodTimeline;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
@ -29,6 +28,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MediaClock;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -49,16 +49,11 @@ public final class ExoPlayerTest extends TestCase {
*/ */
private static final int TIMEOUT_MS = 10000; private static final int TIMEOUT_MS = 10000;
/** private static final Format TEST_VIDEO_FORMAT = Format.createVideoSampleFormat(null,
* Tests playback of a source that exposes a single period. MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, 1280, 720, Format.NO_VALUE,
*/ null, null);
public void testPlayToEnd() throws Exception { private static final Format TEST_AUDIO_FORMAT = Format.createAudioSampleFormat(null,
PlayerWrapper playerWrapper = new PlayerWrapper(); MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null);
Format format = Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H264, null,
Format.NO_VALUE, Format.NO_VALUE, 1280, 720, Format.NO_VALUE, null, null);
playerWrapper.setup(new SinglePeriodTimeline(0, false), null, format);
playerWrapper.blockUntilEnded(TIMEOUT_MS);
}
/** /**
* Tests playback of a source that exposes an empty timeline. Playback is expected to end without * Tests playback of a source that exposes an empty timeline. Playback is expected to end without
@ -66,8 +61,100 @@ public final class ExoPlayerTest extends TestCase {
*/ */
public void testPlayEmptyTimeline() throws Exception { public void testPlayEmptyTimeline() throws Exception {
PlayerWrapper playerWrapper = new PlayerWrapper(); PlayerWrapper playerWrapper = new PlayerWrapper();
playerWrapper.setup(Timeline.EMPTY, null, null); Timeline timeline = Timeline.EMPTY;
MediaSource mediaSource = new FakeMediaSource(timeline, null);
FakeRenderer renderer = new FakeRenderer(null);
playerWrapper.setup(mediaSource, renderer);
playerWrapper.blockUntilEnded(TIMEOUT_MS); playerWrapper.blockUntilEnded(TIMEOUT_MS);
assertEquals(0, playerWrapper.positionDiscontinuityCount);
assertEquals(0, renderer.formatReadCount);
assertEquals(0, renderer.bufferReadCount);
assertFalse(renderer.isEnded);
assertEquals(timeline, playerWrapper.timeline);
assertNull(playerWrapper.manifest);
}
/**
* Tests playback of a source that exposes a single period.
*/
public void testPlaySinglePeriodTimeline() throws Exception {
PlayerWrapper playerWrapper = new PlayerWrapper();
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
Object manifest = new Object();
MediaSource mediaSource = new FakeMediaSource(timeline, manifest, TEST_VIDEO_FORMAT);
FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
playerWrapper.setup(mediaSource, renderer);
playerWrapper.blockUntilEnded(TIMEOUT_MS);
assertEquals(0, playerWrapper.positionDiscontinuityCount);
assertEquals(1, renderer.formatReadCount);
assertEquals(1, renderer.bufferReadCount);
assertTrue(renderer.isEnded);
assertEquals(timeline, playerWrapper.timeline);
assertEquals(manifest, playerWrapper.manifest);
assertEquals(new TrackGroupArray(new TrackGroup(TEST_VIDEO_FORMAT)), playerWrapper.trackGroups);
}
/**
* Tests playback of a source that exposes three periods.
*/
public void testPlayMultiPeriodTimeline() throws Exception {
PlayerWrapper playerWrapper = new PlayerWrapper();
Timeline timeline = new FakeTimeline(
new TimelineWindowDefinition(false, false, 0),
new TimelineWindowDefinition(false, false, 0),
new TimelineWindowDefinition(false, false, 0));
MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT);
FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
playerWrapper.setup(mediaSource, renderer);
playerWrapper.blockUntilEnded(TIMEOUT_MS);
assertEquals(2, playerWrapper.positionDiscontinuityCount);
assertEquals(3, renderer.formatReadCount);
assertEquals(1, renderer.bufferReadCount);
assertTrue(renderer.isEnded);
assertEquals(timeline, playerWrapper.timeline);
assertNull(playerWrapper.manifest);
}
/**
* Tests that the player does not unnecessarily reset renderers when playing a multi-period
* source.
*/
public void testReadAheadToEndDoesNotResetRenderer() throws Exception {
final PlayerWrapper playerWrapper = new PlayerWrapper();
Timeline timeline = new FakeTimeline(
new TimelineWindowDefinition(false, false, 10),
new TimelineWindowDefinition(false, false, 10),
new TimelineWindowDefinition(false, false, 10));
MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT,
TEST_AUDIO_FORMAT);
FakeRenderer videoRenderer = new FakeRenderer(TEST_VIDEO_FORMAT);
FakeMediaClockRenderer audioRenderer = new FakeMediaClockRenderer(TEST_AUDIO_FORMAT) {
@Override
public long getPositionUs() {
// Simulate the playback position lagging behind the reading position: the renderer media
// clock position will be the start of the timeline until the stream is set to be final, at
// which point it jumps to the end of the timeline allowing the playing period to advance.
// TODO: Avoid hard-coding ExoPlayerImplInternal.RENDERER_TIMESTAMP_OFFSET_US.
return isCurrentStreamFinal() ? 60000030 : 60000000;
}
@Override
public boolean isEnded() {
// Allow playback to end once the final period is playing.
return playerWrapper.positionDiscontinuityCount == 2;
}
};
playerWrapper.setup(mediaSource, videoRenderer, audioRenderer);
playerWrapper.blockUntilEnded(TIMEOUT_MS);
assertEquals(2, playerWrapper.positionDiscontinuityCount);
assertEquals(1, audioRenderer.positionResetCount);
assertTrue(videoRenderer.isEnded);
assertTrue(audioRenderer.isEnded);
assertEquals(timeline, playerWrapper.timeline);
assertNull(playerWrapper.manifest);
} }
/** /**
@ -79,12 +166,15 @@ public final class ExoPlayerTest extends TestCase {
private final HandlerThread playerThread; private final HandlerThread playerThread;
private final Handler handler; private final Handler handler;
private Timeline expectedTimeline;
private Object expectedManifest;
private Format expectedFormat;
private ExoPlayer player; private ExoPlayer player;
private Timeline timeline;
private Object manifest;
private TrackGroupArray trackGroups;
private Exception exception; private Exception exception;
// Written only on the main thread.
private volatile int positionDiscontinuityCount;
public PlayerWrapper() { public PlayerWrapper() {
endedCountDownLatch = new CountDownLatch(1); endedCountDownLatch = new CountDownLatch(1);
playerThread = new HandlerThread("ExoPlayerTest thread"); playerThread = new HandlerThread("ExoPlayerTest thread");
@ -105,20 +195,15 @@ public final class ExoPlayerTest extends TestCase {
} }
} }
public void setup(final Timeline timeline, final Object manifest, final Format format) { public void setup(final MediaSource mediaSource, final Renderer... renderers) {
expectedTimeline = timeline;
expectedManifest = manifest;
expectedFormat = format;
handler.post(new Runnable() { handler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
try { try {
Renderer fakeRenderer = new FakeVideoRenderer(expectedFormat); player = ExoPlayerFactory.newInstance(renderers, new DefaultTrackSelector());
player = ExoPlayerFactory.newInstance(new Renderer[] {fakeRenderer},
new DefaultTrackSelector());
player.addListener(PlayerWrapper.this); player.addListener(PlayerWrapper.this);
player.setPlayWhenReady(true); player.setPlayWhenReady(true);
player.prepare(new FakeMediaSource(timeline, manifest, format)); player.prepare(mediaSource);
} catch (Exception e) { } catch (Exception e) {
handleError(e); handleError(e);
} }
@ -167,14 +252,13 @@ public final class ExoPlayerTest extends TestCase {
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest) { public void onTimelineChanged(Timeline timeline, Object manifest) {
assertEquals(expectedTimeline, timeline); this.timeline = timeline;
assertEquals(expectedManifest, manifest); this.manifest = manifest;
} }
@Override @Override
public void onTracksChanged(TrackGroupArray trackGroups, public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
TrackSelectionArray trackSelections) { this.trackGroups = trackGroups;
assertEquals(new TrackGroupArray(new TrackGroup(expectedFormat)), trackGroups);
} }
@Override @Override
@ -182,10 +266,69 @@ public final class ExoPlayerTest extends TestCase {
handleError(exception); handleError(exception);
} }
@SuppressWarnings("NonAtomicVolatileUpdate")
@Override @Override
public void onPositionDiscontinuity() { public void onPositionDiscontinuity() {
// Should never happen. positionDiscontinuityCount++;
handleError(new IllegalStateException("Received position discontinuity")); }
}
private static final class TimelineWindowDefinition {
public final boolean isSeekable;
public final boolean isDynamic;
public final long durationUs;
public TimelineWindowDefinition(boolean isSeekable, boolean isDynamic, long durationUs) {
this.isSeekable = isSeekable;
this.isDynamic = isDynamic;
this.durationUs = durationUs;
}
}
private static final class FakeTimeline extends Timeline {
private final TimelineWindowDefinition[] windowDefinitions;
public FakeTimeline(TimelineWindowDefinition... windowDefinitions) {
this.windowDefinitions = windowDefinitions;
}
@Override
public int getWindowCount() {
return windowDefinitions.length;
}
@Override
public Window getWindow(int windowIndex, Window window, boolean setIds,
long defaultPositionProjectionUs) {
TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex];
Object id = setIds ? windowIndex : null;
return window.set(id, C.TIME_UNSET, C.TIME_UNSET, windowDefinition.isSeekable,
windowDefinition.isDynamic, 0, windowDefinition.durationUs, windowIndex, windowIndex, 0);
}
@Override
public int getPeriodCount() {
return windowDefinitions.length;
}
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
TimelineWindowDefinition windowDefinition = windowDefinitions[periodIndex];
Object id = setIds ? periodIndex : null;
return period.set(id, id, periodIndex, windowDefinition.durationUs, 0);
}
@Override
public int getIndexOfPeriod(Object uid) {
if (!(uid instanceof Integer)) {
return C.INDEX_UNSET;
}
int index = (Integer) uid;
return index >= 0 && index < windowDefinitions.length ? index : C.INDEX_UNSET;
} }
} }
@ -198,16 +341,20 @@ public final class ExoPlayerTest extends TestCase {
private final Timeline timeline; private final Timeline timeline;
private final Object manifest; private final Object manifest;
private final Format format; private final TrackGroupArray trackGroupArray;
private final ArrayList<FakeMediaPeriod> activeMediaPeriods; private final ArrayList<FakeMediaPeriod> activeMediaPeriods;
private boolean preparedSource; private boolean preparedSource;
private boolean releasedSource; private boolean releasedSource;
public FakeMediaSource(Timeline timeline, Object manifest, Format format) { public FakeMediaSource(Timeline timeline, Object manifest, Format... formats) {
this.timeline = timeline; this.timeline = timeline;
this.manifest = manifest; this.manifest = manifest;
this.format = format; TrackGroup[] trackGroups = new TrackGroup[formats.length];
for (int i = 0; i < formats.length; i++) {
trackGroups[i] = new TrackGroup(formats[i]);
}
trackGroupArray = new TrackGroupArray(trackGroups);
activeMediaPeriods = new ArrayList<>(); activeMediaPeriods = new ArrayList<>();
} }
@ -228,9 +375,8 @@ public final class ExoPlayerTest extends TestCase {
Assertions.checkIndex(index, 0, timeline.getPeriodCount()); Assertions.checkIndex(index, 0, timeline.getPeriodCount());
assertTrue(preparedSource); assertTrue(preparedSource);
assertFalse(releasedSource); assertFalse(releasedSource);
assertEquals(0, index);
assertEquals(0, positionUs); assertEquals(0, positionUs);
FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(format); FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray);
activeMediaPeriods.add(mediaPeriod); activeMediaPeriods.add(mediaPeriod);
return mediaPeriod; return mediaPeriod;
} }
@ -239,8 +385,9 @@ public final class ExoPlayerTest extends TestCase {
public void releasePeriod(MediaPeriod mediaPeriod) { public void releasePeriod(MediaPeriod mediaPeriod) {
assertTrue(preparedSource); assertTrue(preparedSource);
assertFalse(releasedSource); assertFalse(releasedSource);
assertTrue(activeMediaPeriods.remove(mediaPeriod)); FakeMediaPeriod fakeMediaPeriod = (FakeMediaPeriod) mediaPeriod;
((FakeMediaPeriod) mediaPeriod).release(); assertTrue(activeMediaPeriods.remove(fakeMediaPeriod));
fakeMediaPeriod.release();
} }
@Override @Override
@ -259,12 +406,12 @@ public final class ExoPlayerTest extends TestCase {
*/ */
private static final class FakeMediaPeriod implements MediaPeriod { private static final class FakeMediaPeriod implements MediaPeriod {
private final TrackGroup trackGroup; private final TrackGroupArray trackGroupArray;
private boolean preparedPeriod; private boolean preparedPeriod;
public FakeMediaPeriod(Format format) { public FakeMediaPeriod(TrackGroupArray trackGroupArray) {
trackGroup = new TrackGroup(format); this.trackGroupArray = trackGroupArray;
} }
public void release() { public void release() {
@ -286,26 +433,29 @@ public final class ExoPlayerTest extends TestCase {
@Override @Override
public TrackGroupArray getTrackGroups() { public TrackGroupArray getTrackGroups() {
assertTrue(preparedPeriod); assertTrue(preparedPeriod);
return new TrackGroupArray(trackGroup); return trackGroupArray;
} }
@Override @Override
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
assertTrue(preparedPeriod); assertTrue(preparedPeriod);
assertEquals(1, selections.length); int rendererCount = selections.length;
assertEquals(1, mayRetainStreamFlags.length); for (int i = 0; i < rendererCount; i++) {
assertEquals(1, streams.length); if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
assertEquals(1, streamResetFlags.length); streams[i] = null;
assertEquals(0, positionUs); }
if (streams[0] != null && (selections[0] == null || !mayRetainStreamFlags[0])) {
streams[0] = null;
} }
if (streams[0] == null && selections[0] != null) { for (int i = 0; i < rendererCount; i++) {
FakeSampleStream stream = new FakeSampleStream(trackGroup.getFormat(0)); if (streams[i] == null && selections[i] != null) {
assertEquals(trackGroup, selections[0].getTrackGroup()); TrackSelection selection = selections[i];
streams[0] = stream; assertEquals(1, selection.length());
streamResetFlags[0] = true; assertEquals(0, selection.getIndexInTrackGroup(0));
TrackGroup trackGroup = selection.getTrackGroup();
assertTrue(trackGroupArray.indexOf(trackGroup) != C.INDEX_UNSET);
streams[i] = new FakeSampleStream(trackGroup.getFormat(0));
streamResetFlags[i] = true;
}
} }
return 0; return 0;
} }
@ -332,7 +482,7 @@ public final class ExoPlayerTest extends TestCase {
@Override @Override
public long getNextLoadPositionUs() { public long getNextLoadPositionUs() {
assertTrue(preparedPeriod); assertTrue(preparedPeriod);
return 0; return C.TIME_END_OF_SOURCE;
} }
@Override @Override
@ -352,7 +502,6 @@ public final class ExoPlayerTest extends TestCase {
private final Format format; private final Format format;
private boolean readFormat; private boolean readFormat;
private boolean readEndOfStream;
public FakeSampleStream(Format format) { public FakeSampleStream(Format format) {
this.format = format; this.format = format;
@ -365,15 +514,14 @@ public final class ExoPlayerTest extends TestCase {
@Override @Override
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) {
Assertions.checkState(!readEndOfStream); if (buffer == null || !readFormat) {
if (readFormat) { formatHolder.format = format;
readFormat = true;
return C.RESULT_FORMAT_READ;
} else {
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
readEndOfStream = true;
return C.RESULT_BUFFER_READ; return C.RESULT_BUFFER_READ;
} }
formatHolder.format = format;
readFormat = true;
return C.RESULT_FORMAT_READ;
} }
@Override @Override
@ -389,20 +537,30 @@ public final class ExoPlayerTest extends TestCase {
} }
/** /**
* Fake {@link Renderer} that supports any video format. The renderer verifies that it reads a * Fake {@link Renderer} that supports any format with the matching MIME type. The renderer
* given {@link Format} then a buffer with the end of stream flag set. * verifies that it reads a given {@link Format}.
*/ */
private static final class FakeVideoRenderer extends BaseRenderer { private static class FakeRenderer extends BaseRenderer {
private final Format expectedFormat; private final Format expectedFormat;
private boolean isEnded; public int positionResetCount;
public int formatReadCount;
public int bufferReadCount;
public boolean isEnded;
public FakeVideoRenderer(Format expectedFormat) { public FakeRenderer(Format expectedFormat) {
super(C.TRACK_TYPE_VIDEO); super(expectedFormat == null ? C.TRACK_TYPE_UNKNOWN
: MimeTypes.getTrackType(expectedFormat.sampleMimeType));
this.expectedFormat = expectedFormat; this.expectedFormat = expectedFormat;
} }
@Override
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
positionResetCount++;
isEnded = false;
}
@Override @Override
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
if (isEnded) { if (isEnded) {
@ -411,20 +569,23 @@ public final class ExoPlayerTest extends TestCase {
// Verify the format matches the expected format. // Verify the format matches the expected format.
FormatHolder formatHolder = new FormatHolder(); FormatHolder formatHolder = new FormatHolder();
readSource(formatHolder, null);
assertEquals(expectedFormat, formatHolder.format);
// Verify that we get an end-of-stream buffer.
DecoderInputBuffer buffer = DecoderInputBuffer buffer =
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
readSource(null, buffer); int result = readSource(formatHolder, buffer);
assertTrue(buffer.isEndOfStream()); if (result == C.RESULT_FORMAT_READ) {
isEnded = true; formatReadCount++;
assertEquals(expectedFormat, formatHolder.format);
} else if (result == C.RESULT_BUFFER_READ) {
bufferReadCount++;
if (buffer.isEndOfStream()) {
isEnded = true;
}
}
} }
@Override @Override
public boolean isReady() { public boolean isReady() {
return isEnded; return isSourceReady();
} }
@Override @Override
@ -434,7 +595,21 @@ public final class ExoPlayerTest extends TestCase {
@Override @Override
public int supportsFormat(Format format) throws ExoPlaybackException { public int supportsFormat(Format format) throws ExoPlaybackException {
return MimeTypes.isVideo(format.sampleMimeType) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE; return getTrackType() == MimeTypes.getTrackType(format.sampleMimeType) ? FORMAT_HANDLED
: FORMAT_UNSUPPORTED_TYPE;
}
}
private abstract static class FakeMediaClockRenderer extends FakeRenderer implements MediaClock {
public FakeMediaClockRenderer(Format expectedFormat) {
super(expectedFormat);
}
@Override
public MediaClock getMediaClock() {
return this;
} }
} }

View File

@ -59,8 +59,8 @@ public final class FormatTest extends TestCase {
DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2); DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2);
byte[] projectionData = new byte[] {1, 2, 3}; byte[] projectionData = new byte[] {1, 2, 3};
Metadata metadata = new Metadata( Metadata metadata = new Metadata(
new TextInformationFrame("id1", "description1"), new TextInformationFrame("id1", "description1", "value1"),
new TextInformationFrame("id2", "description2")); new TextInformationFrame("id2", "description2", "value2"));
Format formatToParcel = new Format("id", MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null, Format formatToParcel = new Format("id", MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null,
1024, 2048, 1920, 1080, 24, 90, 2, projectionData, C.STEREO_MODE_TOP_BOTTOM, 6, 44100, 1024, 2048, 1920, 1080, 24, 90, 2, projectionData, C.STEREO_MODE_TOP_BOTTOM, 6, 44100,

View File

@ -0,0 +1,229 @@
/*
* 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}));
}
}

View File

@ -0,0 +1,51 @@
/*
* 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);
}
}

View File

@ -0,0 +1,39 @@
/*
* 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);
}
}

View File

@ -0,0 +1,43 @@
/*
* 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();
}
}

View File

@ -0,0 +1,45 @@
/*
* 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();
}
}

View File

@ -21,9 +21,9 @@ import com.google.android.exoplayer2.metadata.MetadataDecoderException;
import junit.framework.TestCase; 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 { 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, 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(); Id3Decoder decoder = new Id3Decoder();
Metadata metadata = decoder.decode(rawId3, rawId3.length); Metadata metadata = decoder.decode(rawId3, rawId3.length);
assertEquals(1, metadata.length()); assertEquals(1, metadata.length());
TxxxFrame txxxFrame = (TxxxFrame) metadata.get(0); TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0);
assertEquals("", txxxFrame.description); assertEquals("TXXX", textInformationFrame.id);
assertEquals("mdialog_VINDICO1527664_start", txxxFrame.value); assertEquals("", textInformationFrame.description);
assertEquals("mdialog_VINDICO1527664_start", textInformationFrame.value);
} }
public void testDecodeApicFrame() throws MetadataDecoderException { public void testDecodeApicFrame() throws MetadataDecoderException {
@ -60,7 +61,19 @@ public class Id3DecoderTest extends TestCase {
assertEquals(1, metadata.length()); assertEquals(1, metadata.length());
TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0);
assertEquals("TIT2", textInformationFrame.id); 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);
} }
} }

View File

@ -29,13 +29,13 @@ public class RepresentationTest extends TestCase {
String uri = "http://www.google.com"; String uri = "http://www.google.com";
SegmentBase base = new SingleSegmentBase(new RangedUri(null, 0, 1), 1, 0, 1, 1); SegmentBase base = new SingleSegmentBase(new RangedUri(null, 0, 1), 1, 0, 1, 1);
Format format = Format.createVideoContainerFormat("0", MimeTypes.APPLICATION_MP4, null, Format format = Format.createVideoContainerFormat("0", MimeTypes.APPLICATION_MP4, null,
MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null); MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null, 0);
Representation representation = Representation.newInstance("test_stream_1", 3, format, uri, Representation representation = Representation.newInstance("test_stream_1", 3, format, uri,
base); base);
assertEquals("test_stream_1.0.3", representation.getCacheKey()); assertEquals("test_stream_1.0.3", representation.getCacheKey());
format = Format.createVideoContainerFormat("150", MimeTypes.APPLICATION_MP4, null, format = Format.createVideoContainerFormat("150", MimeTypes.APPLICATION_MP4, null,
MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null); MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null, 0);
representation = Representation.newInstance("test_stream_1", Representation.REVISION_ID_DEFAULT, representation = Representation.newInstance("test_stream_1", Representation.REVISION_ID_DEFAULT,
format, uri, base); format, uri, base);
assertEquals("test_stream_1.150.-1", representation.getCacheKey()); assertEquals("test_stream_1.150.-1", representation.getCacheKey());

View File

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.hls.playlist;
import android.net.Uri; import android.net.Uri;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
@ -29,70 +30,86 @@ import junit.framework.TestCase;
*/ */
public class HlsMasterPlaylistParserTest extends TestCase { public class HlsMasterPlaylistParserTest extends TestCase {
public void testParseMasterPlaylist() { private static final String PLAYLIST_URI = "https://example.com/test.m3u8";
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
String playlistString = "#EXTM3U\n" private static final String MASTER_PLAYLIST = " #EXTM3U \n"
+ "\n" + "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+ "http://example.com/low.m3u8\n" + "http://example.com/low.m3u8\n"
+ "\n" + "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n" + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n"
+ "http://example.com/spaces_in_codecs.m3u8\n" + "http://example.com/spaces_in_codecs.m3u8\n"
+ "\n" + "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=384x160\n" + "#EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=384x160\n"
+ "http://example.com/mid.m3u8\n" + "http://example.com/mid.m3u8\n"
+ "\n" + "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=7680000\n" + "#EXT-X-STREAM-INF:BANDWIDTH=7680000\n"
+ "http://example.com/hi.m3u8\n" + "http://example.com/hi.m3u8\n"
+ "\n" + "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n" + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n"
+ "http://example.com/audio-only.m3u8"; + "http://example.com/audio-only.m3u8";
ByteArrayInputStream inputStream = new ByteArrayInputStream(
playlistString.getBytes(Charset.forName(C.UTF8_NAME))); 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 { try {
HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream); parsePlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER);
assertNotNull(playlist); fail("Expected exception not thrown.");
assertEquals(HlsPlaylist.TYPE_MASTER, playlist.type); } catch (ParserException e) {
// Expected due to invalid header.
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());
} }
} }
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);
}
} }

View File

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls.playlist;
import android.net.Uri; import android.net.Uri;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -73,59 +74,64 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals(2679, mediaPlaylist.mediaSequence); assertEquals(2679, mediaPlaylist.mediaSequence);
assertEquals(3, mediaPlaylist.version); assertEquals(3, mediaPlaylist.version);
assertEquals(true, mediaPlaylist.hasEndTag); assertTrue(mediaPlaylist.hasEndTag);
List<HlsMediaPlaylist.Segment> segments = mediaPlaylist.segments; List<Segment> segments = mediaPlaylist.segments;
assertNotNull(segments); assertNotNull(segments);
assertEquals(5, segments.size()); assertEquals(5, segments.size());
assertEquals(4, segments.get(0).discontinuitySequenceNumber); Segment segment = segments.get(0);
assertEquals(7975000, segments.get(0).durationUs); assertEquals(4, mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence);
assertEquals(false, segments.get(0).isEncrypted); assertEquals(7975000, segment.durationUs);
assertEquals(null, segments.get(0).encryptionKeyUri); assertFalse(segment.isEncrypted);
assertEquals(null, segments.get(0).encryptionIV); assertEquals(null, segment.encryptionKeyUri);
assertEquals(51370, segments.get(0).byterangeLength); assertEquals(null, segment.encryptionIV);
assertEquals(0, segments.get(0).byterangeOffset); assertEquals(51370, segment.byterangeLength);
assertEquals("https://priv.example.com/fileSequence2679.ts", segments.get(0).url); assertEquals(0, segment.byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2679.ts", segment.url);
assertEquals(4, segments.get(1).discontinuitySequenceNumber); segment = segments.get(1);
assertEquals(7975000, segments.get(1).durationUs); assertEquals(0, segment.relativeDiscontinuitySequence);
assertEquals(true, segments.get(1).isEncrypted); assertEquals(7975000, segment.durationUs);
assertEquals("https://priv.example.com/key.php?r=2680", segments.get(1).encryptionKeyUri); assertTrue(segment.isEncrypted);
assertEquals("0x1566B", segments.get(1).encryptionIV); assertEquals("https://priv.example.com/key.php?r=2680", segment.encryptionKeyUri);
assertEquals(51501, segments.get(1).byterangeLength); assertEquals("0x1566B", segment.encryptionIV);
assertEquals(2147483648L, segments.get(1).byterangeOffset); assertEquals(51501, segment.byterangeLength);
assertEquals("https://priv.example.com/fileSequence2680.ts", segments.get(1).url); assertEquals(2147483648L, segment.byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2680.ts", segment.url);
assertEquals(4, segments.get(2).discontinuitySequenceNumber); segment = segments.get(2);
assertEquals(7941000, segments.get(2).durationUs); assertEquals(0, segment.relativeDiscontinuitySequence);
assertEquals(false, segments.get(2).isEncrypted); assertEquals(7941000, segment.durationUs);
assertEquals(null, segments.get(2).encryptionKeyUri); assertFalse(segment.isEncrypted);
assertEquals(null, segments.get(2).encryptionIV); assertEquals(null, segment.encryptionKeyUri);
assertEquals(51501, segments.get(2).byterangeLength); assertEquals(null, segment.encryptionIV);
assertEquals(2147535149L, segments.get(2).byterangeOffset); assertEquals(51501, segment.byterangeLength);
assertEquals("https://priv.example.com/fileSequence2681.ts", segments.get(2).url); assertEquals(2147535149L, segment.byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2681.ts", segment.url);
assertEquals(5, segments.get(3).discontinuitySequenceNumber); segment = segments.get(3);
assertEquals(7975000, segments.get(3).durationUs); assertEquals(1, segment.relativeDiscontinuitySequence);
assertEquals(true, segments.get(3).isEncrypted); assertEquals(7975000, segment.durationUs);
assertEquals("https://priv.example.com/key.php?r=2682", segments.get(3).encryptionKeyUri); assertTrue(segment.isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2682", segment.encryptionKeyUri);
// 0xA7A == 2682. // 0xA7A == 2682.
assertNotNull(segments.get(3).encryptionIV); assertNotNull(segment.encryptionIV);
assertEquals("A7A", segments.get(3).encryptionIV.toUpperCase(Locale.getDefault())); assertEquals("A7A", segment.encryptionIV.toUpperCase(Locale.getDefault()));
assertEquals(51740, segments.get(3).byterangeLength); assertEquals(51740, segment.byterangeLength);
assertEquals(2147586650L, segments.get(3).byterangeOffset); assertEquals(2147586650L, segment.byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2682.ts", segments.get(3).url); assertEquals("https://priv.example.com/fileSequence2682.ts", segment.url);
assertEquals(5, segments.get(4).discontinuitySequenceNumber); segment = segments.get(4);
assertEquals(7975000, segments.get(4).durationUs); assertEquals(1, segment.relativeDiscontinuitySequence);
assertEquals(true, segments.get(4).isEncrypted); assertEquals(7975000, segment.durationUs);
assertEquals("https://priv.example.com/key.php?r=2682", segments.get(4).encryptionKeyUri); assertTrue(segment.isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2682", segment.encryptionKeyUri);
// 0xA7B == 2683. // 0xA7B == 2683.
assertNotNull(segments.get(4).encryptionIV); assertNotNull(segment.encryptionIV);
assertEquals("A7B", segments.get(4).encryptionIV.toUpperCase(Locale.getDefault())); assertEquals("A7B", segment.encryptionIV.toUpperCase(Locale.getDefault()));
assertEquals(C.LENGTH_UNSET, segments.get(4).byterangeLength); assertEquals(C.LENGTH_UNSET, segment.byterangeLength);
assertEquals(0, segments.get(4).byterangeOffset); assertEquals(0, segment.byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2683.ts", segments.get(4).url); assertEquals("https://priv.example.com/fileSequence2683.ts", segment.url);
} catch (IOException exception) { } catch (IOException exception) {
fail(exception.getMessage()); fail(exception.getMessage());
} }

View File

@ -27,7 +27,9 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
/** Unit tests for {@link CacheDataSource}. */ /**
* Unit tests for {@link CacheDataSource}.
*/
public class CacheDataSourceTest extends InstrumentationTestCase { public class CacheDataSourceTest extends InstrumentationTestCase {
private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

View File

@ -0,0 +1,181 @@
/*
* 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());
}
}

View File

@ -163,7 +163,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
public void testEncryption() throws Exception { public void testEncryption() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key 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), assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
new CachedContentIndex(cacheDir, key)); new CachedContentIndex(cacheDir, key));
@ -181,7 +181,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
// Assert file content is different // Assert file content is different
FileInputStream fis1 = new FileInputStream(file1); FileInputStream fis1 = new FileInputStream(file1);
FileInputStream fis2 = new FileInputStream(file2); FileInputStream fis2 = new FileInputStream(file2);
for (int b; (b = fis1.read()) == fis2.read();) { for (int b; (b = fis1.read()) == fis2.read(); ) {
assertTrue(b != -1); assertTrue(b != -1);
} }
@ -205,6 +205,12 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
// Non encrypted index file can be read even when encryption key provided. // Non encrypted index file can be read even when encryption key provided.
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir), assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir),
new CachedContentIndex(cacheDir, key)); 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) private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentIndex index2)

View File

@ -0,0 +1,126 @@
/*
* 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);
}
}

View File

@ -16,12 +16,16 @@
package com.google.android.exoplayer2.upstream.cache; package com.google.android.exoplayer2.upstream.cache;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import android.test.MoreAsserts;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.NavigableSet; import java.util.NavigableSet;
import java.util.Random;
import java.util.Set; import java.util.Set;
/** /**
@ -46,9 +50,9 @@ public class SimpleCacheTest extends InstrumentationTestCase {
public void testCommittingOneFile() throws Exception { public void testCommittingOneFile() throws Exception {
SimpleCache simpleCache = getSimpleCache(); SimpleCache simpleCache = getSimpleCache();
CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0); CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
assertFalse(cacheSpan.isCached); assertFalse(cacheSpan1.isCached);
assertTrue(cacheSpan.isOpenEnded()); assertTrue(cacheSpan1.isOpenEnded());
assertNull(simpleCache.startReadWriteNonBlocking(KEY_1, 0)); assertNull(simpleCache.startReadWriteNonBlocking(KEY_1, 0));
@ -58,20 +62,33 @@ public class SimpleCacheTest extends InstrumentationTestCase {
assertEquals(0, simpleCache.getCacheSpace()); assertEquals(0, simpleCache.getCacheSpace());
assertEquals(0, cacheDir.listFiles().length); assertEquals(0, cacheDir.listFiles().length);
addCache(simpleCache, 0, 15); addCache(simpleCache, KEY_1, 0, 15);
Set<String> cachedKeys = simpleCache.getKeys(); Set<String> cachedKeys = simpleCache.getKeys();
assertEquals(1, cachedKeys.size()); assertEquals(1, cachedKeys.size());
assertTrue(cachedKeys.contains(KEY_1)); assertTrue(cachedKeys.contains(KEY_1));
cachedSpans = simpleCache.getCachedSpans(KEY_1); cachedSpans = simpleCache.getCachedSpans(KEY_1);
assertEquals(1, cachedSpans.size()); assertEquals(1, cachedSpans.size());
assertTrue(cachedSpans.contains(cacheSpan)); assertTrue(cachedSpans.contains(cacheSpan1));
assertEquals(15, simpleCache.getCacheSpace()); assertEquals(15, simpleCache.getCacheSpace());
cacheSpan = simpleCache.startReadWrite(KEY_1, 0); simpleCache.releaseHoleSpan(cacheSpan1);
assertTrue(cacheSpan.isCached);
assertFalse(cacheSpan.isOpenEnded()); CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);
assertEquals(15, cacheSpan.length); 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 { public void testSetGetLength() throws Exception {
@ -83,12 +100,12 @@ public class SimpleCacheTest extends InstrumentationTestCase {
simpleCache.startReadWrite(KEY_1, 0); simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, 0, 15); addCache(simpleCache, KEY_1, 0, 15);
simpleCache.setContentLength(KEY_1, 150); simpleCache.setContentLength(KEY_1, 150);
assertEquals(150, simpleCache.getContentLength(KEY_1)); assertEquals(150, simpleCache.getContentLength(KEY_1));
addCache(simpleCache, 140, 10); addCache(simpleCache, KEY_1, 140, 10);
// Check if values are kept after cache is reloaded. // Check if values are kept after cache is reloaded.
SimpleCache simpleCache2 = getSimpleCache(); SimpleCache simpleCache2 = getSimpleCache();
@ -107,16 +124,109 @@ public class SimpleCacheTest extends InstrumentationTestCase {
assertEquals(150, simpleCache2.getContentLength(KEY_1)); 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() { private SimpleCache getSimpleCache() {
return new SimpleCache(cacheDir, new NoOpCacheEvictor()); return new SimpleCache(cacheDir, new NoOpCacheEvictor());
} }
private void addCache(SimpleCache simpleCache, int position, int length) throws IOException { private SimpleCache getEncryptedSimpleCache(byte[] secretKey) {
File file = simpleCache.startFile(KEY_1, position, length); 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); FileOutputStream fos = new FileOutputStream(file);
fos.write(new byte[length]); try {
fos.close(); fos.write(generateData(key, position, length));
} finally {
fos.close();
}
simpleCache.commitFile(file); 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;
}
} }

View File

@ -0,0 +1,186 @@
/*
* 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);
}
}

View File

@ -371,6 +371,73 @@ public class ParsableByteArrayTest extends TestCase {
assertNull(parser.readLine()); 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() { public void testReadSingleLineWithoutEndingTrail() {
byte[] bytes = new byte[] { byte[] bytes = new byte[] {
'f', 'o', 'o' 'f', 'o', 'o'

View File

@ -28,6 +28,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
private final int trackType; private final int trackType;
private RendererConfiguration configuration;
private int index; private int index;
private int state; private int state;
private SampleStream stream; private SampleStream stream;
@ -70,9 +71,11 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
} }
@Override @Override
public final void enable(Format[] formats, SampleStream stream, long positionUs, boolean joining, public final void enable(RendererConfiguration configuration, Format[] formats,
long offsetUs) throws ExoPlaybackException { SampleStream stream, long positionUs, boolean joining, long offsetUs)
throws ExoPlaybackException {
Assertions.checkState(state == STATE_DISABLED); Assertions.checkState(state == STATE_DISABLED);
this.configuration = configuration;
state = STATE_ENABLED; state = STATE_ENABLED;
onEnabled(joining); onEnabled(joining);
replaceStream(formats, stream, offsetUs); replaceStream(formats, stream, offsetUs);
@ -237,10 +240,15 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
// Methods to be called by subclasses. // 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. * Returns the index of the renderer within the player.
*
* @return The index of the renderer within the player.
*/ */
protected final int getIndex() { protected final int getIndex() {
return index; 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 * {@link C#RESULT_BUFFER_READ} is only returned if {@link #setCurrentStreamFinal()} has been
* called. {@link C#RESULT_NOTHING_READ} is returned otherwise. * called. {@link C#RESULT_NOTHING_READ} is returned otherwise.
* *
* @see SampleStream#readData(FormatHolder, DecoderInputBuffer)
* @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
* @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
* end of the stream. If the end of the stream has been reached, the * end of the stream. If the end of the stream has been reached, the
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. * {@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 * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
* {@link C#RESULT_BUFFER_READ}. * {@link C#RESULT_BUFFER_READ}.
*/ */

View File

@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import android.annotation.TargetApi;
import android.content.Context;
import android.media.AudioFormat; import android.media.AudioFormat;
import android.media.AudioManager; import android.media.AudioManager;
import android.media.MediaCodec; import android.media.MediaCodec;
@ -550,4 +552,13 @@ public final class C {
return timeMs == TIME_UNSET ? TIME_UNSET : (timeMs * 1000); 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();
}
} }

View File

@ -19,6 +19,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.upstream.DefaultAllocator;
import com.google.android.exoplayer2.util.PriorityTaskManager;
import com.google.android.exoplayer2.util.Util; 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; public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000;
/**
* Priority for media loading.
*/
public static final int LOADING_PRIORITY = 0;
private static final int ABOVE_HIGH_WATERMARK = 0; private static final int ABOVE_HIGH_WATERMARK = 0;
private static final int BETWEEN_WATERMARKS = 1; private static final int BETWEEN_WATERMARKS = 1;
private static final int BELOW_LOW_WATERMARK = 2; private static final int BELOW_LOW_WATERMARK = 2;
@ -60,6 +66,7 @@ public final class DefaultLoadControl implements LoadControl {
private final long maxBufferUs; private final long maxBufferUs;
private final long bufferForPlaybackUs; private final long bufferForPlaybackUs;
private final long bufferForPlaybackAfterRebufferUs; private final long bufferForPlaybackAfterRebufferUs;
private final PriorityTaskManager priorityTaskManager;
private int targetBufferSize; private int targetBufferSize;
private boolean isBuffering; private boolean isBuffering;
@ -97,11 +104,36 @@ public final class DefaultLoadControl implements LoadControl {
*/ */
public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs) { 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; this.allocator = allocator;
minBufferUs = minBufferMs * 1000L; minBufferUs = minBufferMs * 1000L;
maxBufferUs = maxBufferMs * 1000L; maxBufferUs = maxBufferMs * 1000L;
bufferForPlaybackUs = bufferForPlaybackMs * 1000L; bufferForPlaybackUs = bufferForPlaybackMs * 1000L;
bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L; bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L;
this.priorityTaskManager = priorityTaskManager;
} }
@Override @Override
@ -146,8 +178,16 @@ public final class DefaultLoadControl implements LoadControl {
public boolean shouldContinueLoading(long bufferedDurationUs) { public boolean shouldContinueLoading(long bufferedDurationUs) {
int bufferTimeState = getBufferTimeState(bufferedDurationUs); int bufferTimeState = getBufferTimeState(bufferedDurationUs);
boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize;
boolean wasBuffering = isBuffering;
isBuffering = bufferTimeState == BELOW_LOW_WATERMARK isBuffering = bufferTimeState == BELOW_LOW_WATERMARK
|| (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached); || (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached);
if (priorityTaskManager != null && isBuffering != wasBuffering) {
if (isBuffering) {
priorityTaskManager.add(LOADING_PRIORITY);
} else {
priorityTaskManager.remove(LOADING_PRIORITY);
}
}
return isBuffering; return isBuffering;
} }
@ -158,6 +198,9 @@ public final class DefaultLoadControl implements LoadControl {
private void reset(boolean resetAllocator) { private void reset(boolean resetAllocator) {
targetBufferSize = 0; targetBufferSize = 0;
if (priorityTaskManager != null && isBuffering) {
priorityTaskManager.remove(LOADING_PRIORITY);
}
isBuffering = false; isBuffering = false;
if (resetAllocator) { if (resetAllocator) {
allocator.reset(); allocator.reset();

View File

@ -447,4 +447,20 @@ public interface ExoPlayer {
*/ */
int getBufferedPercentage(); 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();
} }

View File

@ -22,12 +22,12 @@ import android.os.Message;
import android.util.Log; import android.util.Log;
import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo; import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo;
import com.google.android.exoplayer2.ExoPlayerImplInternal.SourceInfo; 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.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
@ -271,6 +271,22 @@ import java.util.concurrent.CopyOnWriteArraySet;
: (int) (duration == 0 ? 100 : (bufferedPosition * 100) / duration); : (int) (duration == 0 ? 100 : (bufferedPosition * 100) / duration);
} }
@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 @Override
public int getRendererCount() { public int getRendererCount() {
return renderers.length; return renderers.length;
@ -319,11 +335,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
break; break;
} }
case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: { case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: {
TrackInfo trackInfo = (TrackInfo) msg.obj; TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj;
tracksSelected = true; tracksSelected = true;
trackGroups = trackInfo.groups; trackGroups = trackSelectorResult.groups;
trackSelections = trackInfo.selections; trackSelections = trackSelectorResult.selections;
trackSelector.onSelectionActivated(trackInfo.info); trackSelector.onSelectionActivated(trackSelectorResult.info);
for (EventListener listener : listeners) { for (EventListener listener : listeners) {
listener.onTracksChanged(trackGroups, trackSelections); listener.onTracksChanged(trackGroups, trackSelections);
} }

View File

@ -26,16 +26,15 @@ import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage;
import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MediaClock; import com.google.android.exoplayer2.util.MediaClock;
import com.google.android.exoplayer2.util.PriorityHandlerThread; import com.google.android.exoplayer2.util.PriorityHandlerThread;
import com.google.android.exoplayer2.util.StandaloneMediaClock; import com.google.android.exoplayer2.util.StandaloneMediaClock;
import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
/** /**
@ -72,20 +71,6 @@ import java.io.IOException;
} }
public static final class TrackInfo {
public final TrackGroupArray groups;
public final TrackSelectionArray selections;
public final Object info;
public TrackInfo(TrackGroupArray groups, TrackSelectionArray selections, Object info) {
this.groups = groups;
this.selections = selections;
this.info = info;
}
}
public static final class SourceInfo { public static final class SourceInfo {
public final Timeline timeline; public final Timeline timeline;
@ -624,6 +609,7 @@ import java.io.IOException;
enabledRenderers = new Renderer[0]; enabledRenderers = new Renderer[0];
rendererMediaClock = null; rendererMediaClock = null;
rendererMediaClockSource = null; rendererMediaClockSource = null;
playingPeriodHolder = null;
} }
// Update the holders. // Update the holders.
@ -799,7 +785,8 @@ import java.io.IOException;
} }
} }
} }
eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.getTrackInfo()).sendToTarget(); eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.trackSelectorResult)
.sendToTarget();
enableRenderers(rendererWasEnabledFlags, enabledRendererCount); enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
} else { } else {
// Release and re-prepare/buffer periods after the one whose selection changed. // Release and re-prepare/buffer periods after the one whose selection changed.
@ -1138,33 +1125,38 @@ import java.io.IOException;
} }
if (readingPeriodHolder.isLast) { if (readingPeriodHolder.isLast) {
for (Renderer renderer : enabledRenderers) { for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[i];
SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
// Defer setting the stream as final until the renderer has actually consumed the whole // Defer setting the stream as final until the renderer has actually consumed the whole
// stream in case of playlist changes that cause the stream to be no longer final. // stream in case of playlist changes that cause the stream to be no longer final.
if (renderer.hasReadStreamToEnd()) { if (sampleStream != null && renderer.getStream() == sampleStream
&& renderer.hasReadStreamToEnd()) {
renderer.setCurrentStreamFinal(); renderer.setCurrentStreamFinal();
} }
} }
return; return;
} }
for (Renderer renderer : enabledRenderers) { for (int i = 0; i < renderers.length; i++) {
if (!renderer.hasReadStreamToEnd()) { Renderer renderer = renderers[i];
SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
if (renderer.getStream() != sampleStream
|| (sampleStream != null && !renderer.hasReadStreamToEnd())) {
return; return;
} }
} }
if (readingPeriodHolder.next != null && readingPeriodHolder.next.prepared) { if (readingPeriodHolder.next != null && readingPeriodHolder.next.prepared) {
TrackSelectionArray oldTrackSelections = readingPeriodHolder.trackSelections; TrackSelectorResult oldTrackSelectorResult = readingPeriodHolder.trackSelectorResult;
readingPeriodHolder = readingPeriodHolder.next; readingPeriodHolder = readingPeriodHolder.next;
TrackSelectionArray newTrackSelections = readingPeriodHolder.trackSelections; TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.trackSelectorResult;
boolean initialDiscontinuity = boolean initialDiscontinuity =
readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET; readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET;
for (int i = 0; i < renderers.length; i++) { for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[i]; Renderer renderer = renderers[i];
TrackSelection oldSelection = oldTrackSelections.get(i); TrackSelection oldSelection = oldTrackSelectorResult.selections.get(i);
TrackSelection newSelection = newTrackSelections.get(i);
if (oldSelection == null) { if (oldSelection == null) {
// The renderer has no current stream and will be enabled when we play the next period. // The renderer has no current stream and will be enabled when we play the next period.
} else if (initialDiscontinuity) { } else if (initialDiscontinuity) {
@ -1172,9 +1164,12 @@ import java.io.IOException;
// be disabled and re-enabled when it starts playing the next period. // be disabled and re-enabled when it starts playing the next period.
renderer.setCurrentStreamFinal(); renderer.setCurrentStreamFinal();
} else if (!renderer.isCurrentStreamFinal()) { } else if (!renderer.isCurrentStreamFinal()) {
if (newSelection != null) { TrackSelection newSelection = newTrackSelectorResult.selections.get(i);
// Replace the renderer's SampleStream so the transition to playing the next period RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[i];
// can be seamless. RendererConfiguration newConfig = newTrackSelectorResult.rendererConfigurations[i];
if (newSelection != null && newConfig.equals(oldConfig)) {
// Replace the renderer's SampleStream so the transition to playing the next period can
// be seamless.
Format[] formats = new Format[newSelection.length()]; Format[] formats = new Format[newSelection.length()];
for (int j = 0; j < formats.length; j++) { for (int j = 0; j < formats.length; j++) {
formats[j] = newSelection.getFormat(j); formats[j] = newSelection.getFormat(j);
@ -1182,8 +1177,9 @@ import java.io.IOException;
renderer.replaceStream(formats, readingPeriodHolder.sampleStreams[i], renderer.replaceStream(formats, readingPeriodHolder.sampleStreams[i],
readingPeriodHolder.getRendererOffset()); readingPeriodHolder.getRendererOffset());
} else { } else {
// The renderer will be disabled when transitioning to playing the next period. Mark the // The renderer will be disabled when transitioning to playing the next period, either
// SampleStream as final to play out any remaining data. // because there's no new selection or because a configuration change is required. Mark
// the SampleStream as final to play out any remaining data.
renderer.setCurrentStreamFinal(); renderer.setCurrentStreamFinal();
} }
} }
@ -1319,20 +1315,21 @@ import java.io.IOException;
return; return;
} }
playingPeriodHolder = periodHolder;
int enabledRendererCount = 0; int enabledRendererCount = 0;
boolean[] rendererWasEnabledFlags = new boolean[renderers.length]; boolean[] rendererWasEnabledFlags = new boolean[renderers.length];
for (int i = 0; i < renderers.length; i++) { for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[i]; Renderer renderer = renderers[i];
rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED; rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED;
TrackSelection newSelection = periodHolder.trackSelections.get(i); TrackSelection newSelection = periodHolder.trackSelectorResult.selections.get(i);
if (newSelection != null) { if (newSelection != null) {
enabledRendererCount++; enabledRendererCount++;
} }
if (rendererWasEnabledFlags[i] && (newSelection == null || renderer.isCurrentStreamFinal())) { if (rendererWasEnabledFlags[i] && (newSelection == null
|| (renderer.isCurrentStreamFinal()
&& renderer.getStream() == playingPeriodHolder.sampleStreams[i]))) {
// The renderer should be disabled before playing the next period, either because it's not // The renderer should be disabled before playing the next period, either because it's not
// needed to play the next period, or because we need to disable and re-enable it because // needed to play the next period, or because we need to re-enable it as its current stream
// the renderer thinks that its current stream is final. // is final and it's not reading ahead.
if (renderer == rendererMediaClockSource) { if (renderer == rendererMediaClockSource) {
// Sync standaloneMediaClock so that it can take over timing responsibilities. // Sync standaloneMediaClock so that it can take over timing responsibilities.
standaloneMediaClock.setPositionUs(rendererMediaClock.getPositionUs()); standaloneMediaClock.setPositionUs(rendererMediaClock.getPositionUs());
@ -1344,7 +1341,8 @@ import java.io.IOException;
} }
} }
eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.getTrackInfo()).sendToTarget(); playingPeriodHolder = periodHolder;
eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.trackSelectorResult).sendToTarget();
enableRenderers(rendererWasEnabledFlags, enabledRendererCount); enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
} }
@ -1354,10 +1352,12 @@ import java.io.IOException;
enabledRendererCount = 0; enabledRendererCount = 0;
for (int i = 0; i < renderers.length; i++) { for (int i = 0; i < renderers.length; i++) {
Renderer renderer = renderers[i]; Renderer renderer = renderers[i];
TrackSelection newSelection = playingPeriodHolder.trackSelections.get(i); TrackSelection newSelection = playingPeriodHolder.trackSelectorResult.selections.get(i);
if (newSelection != null) { if (newSelection != null) {
enabledRenderers[enabledRendererCount++] = renderer; enabledRenderers[enabledRendererCount++] = renderer;
if (renderer.getState() == Renderer.STATE_DISABLED) { if (renderer.getState() == Renderer.STATE_DISABLED) {
RendererConfiguration rendererConfiguration =
playingPeriodHolder.trackSelectorResult.rendererConfigurations[i];
// The renderer needs enabling with its new track selection. // The renderer needs enabling with its new track selection.
boolean playing = playWhenReady && state == ExoPlayer.STATE_READY; boolean playing = playWhenReady && state == ExoPlayer.STATE_READY;
// Consider as joining only if the renderer was previously disabled. // Consider as joining only if the renderer was previously disabled.
@ -1368,8 +1368,8 @@ import java.io.IOException;
formats[j] = newSelection.getFormat(j); formats[j] = newSelection.getFormat(j);
} }
// Enable the renderer. // Enable the renderer.
renderer.enable(formats, playingPeriodHolder.sampleStreams[i], rendererPositionUs, renderer.enable(rendererConfiguration, formats, playingPeriodHolder.sampleStreams[i],
joining, playingPeriodHolder.getRendererOffset()); rendererPositionUs, joining, playingPeriodHolder.getRendererOffset());
MediaClock mediaClock = renderer.getMediaClock(); MediaClock mediaClock = renderer.getMediaClock();
if (mediaClock != null) { if (mediaClock != null) {
if (rendererMediaClock != null) { if (rendererMediaClock != null) {
@ -1406,6 +1406,7 @@ import java.io.IOException;
public boolean hasEnabledTracks; public boolean hasEnabledTracks;
public MediaPeriodHolder next; public MediaPeriodHolder next;
public boolean needsContinueLoading; public boolean needsContinueLoading;
public TrackSelectorResult trackSelectorResult;
private final Renderer[] renderers; private final Renderer[] renderers;
private final RendererCapabilities[] rendererCapabilities; private final RendererCapabilities[] rendererCapabilities;
@ -1413,10 +1414,7 @@ import java.io.IOException;
private final LoadControl loadControl; private final LoadControl loadControl;
private final MediaSource mediaSource; private final MediaSource mediaSource;
private Object trackSelectionsInfo; private TrackSelectorResult periodTrackSelectorResult;
private TrackGroupArray trackGroups;
private TrackSelectionArray trackSelections;
private TrackSelectionArray periodTrackSelections;
public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities, public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities,
long rendererPositionOffsetUs, TrackSelector trackSelector, LoadControl loadControl, long rendererPositionOffsetUs, TrackSelector trackSelector, LoadControl loadControl,
@ -1462,20 +1460,17 @@ import java.io.IOException;
public void handlePrepared() throws ExoPlaybackException { public void handlePrepared() throws ExoPlaybackException {
prepared = true; prepared = true;
trackGroups = mediaPeriod.getTrackGroups();
selectTracks(); selectTracks();
startPositionUs = updatePeriodTrackSelection(startPositionUs, false); startPositionUs = updatePeriodTrackSelection(startPositionUs, false);
} }
public boolean selectTracks() throws ExoPlaybackException { public boolean selectTracks() throws ExoPlaybackException {
Pair<TrackSelectionArray, Object> selectorResult = trackSelector.selectTracks( TrackSelectorResult selectorResult = trackSelector.selectTracks(rendererCapabilities,
rendererCapabilities, trackGroups); mediaPeriod.getTrackGroups());
TrackSelectionArray newTrackSelections = selectorResult.first; if (selectorResult.isEquivalent(periodTrackSelectorResult)) {
if (newTrackSelections.equals(periodTrackSelections)) {
return false; return false;
} }
trackSelections = newTrackSelections; trackSelectorResult = selectorResult;
trackSelectionsInfo = selectorResult.second;
return true; return true;
} }
@ -1486,16 +1481,16 @@ import java.io.IOException;
public long updatePeriodTrackSelection(long positionUs, boolean forceRecreateStreams, public long updatePeriodTrackSelection(long positionUs, boolean forceRecreateStreams,
boolean[] streamResetFlags) { boolean[] streamResetFlags) {
TrackSelectionArray trackSelections = trackSelectorResult.selections;
for (int i = 0; i < trackSelections.length; i++) { for (int i = 0; i < trackSelections.length; i++) {
mayRetainStreamFlags[i] = !forceRecreateStreams mayRetainStreamFlags[i] = !forceRecreateStreams
&& Util.areEqual(periodTrackSelections == null ? null : periodTrackSelections.get(i), && trackSelectorResult.isEquivalent(periodTrackSelectorResult, i);
trackSelections.get(i));
} }
// Disable streams on the period and get new streams for updated/newly-enabled tracks. // Disable streams on the period and get new streams for updated/newly-enabled tracks.
positionUs = mediaPeriod.selectTracks(trackSelections.getAll(), mayRetainStreamFlags, positionUs = mediaPeriod.selectTracks(trackSelections.getAll(), mayRetainStreamFlags,
sampleStreams, streamResetFlags, positionUs); sampleStreams, streamResetFlags, positionUs);
periodTrackSelections = trackSelections; periodTrackSelectorResult = trackSelectorResult;
// Update whether we have enabled tracks and sanity check the expected streams are non-null. // Update whether we have enabled tracks and sanity check the expected streams are non-null.
hasEnabledTracks = false; hasEnabledTracks = false;
@ -1509,14 +1504,10 @@ import java.io.IOException;
} }
// The track selection has changed. // The track selection has changed.
loadControl.onTracksSelected(renderers, trackGroups, trackSelections); loadControl.onTracksSelected(renderers, trackSelectorResult.groups, trackSelections);
return positionUs; return positionUs;
} }
public TrackInfo getTrackInfo() {
return new TrackInfo(trackGroups, trackSelections, trackSelectionsInfo);
}
public void release() { public void release() {
try { try {
mediaSource.releasePeriod(mediaPeriod); mediaSource.releasePeriod(mediaPeriod);

View File

@ -183,20 +183,18 @@ public final class Format implements Parcelable {
*/ */
public final int accessibilityChannel; public final int accessibilityChannel;
// Lazily initialized hashcode and framework media format. // Lazily initialized hashcode.
private int hashCode; private int hashCode;
private MediaFormat frameworkMediaFormat;
// Video. // Video.
public static Format createVideoContainerFormat(String id, String containerMimeType, public static Format createVideoContainerFormat(String id, String containerMimeType,
String sampleMimeType, String codecs, int bitrate, int width, int height, 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, 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, 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, NO_VALUE, NO_VALUE, selectionFlags, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
null); initializationData, null, null);
} }
public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs, 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, public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
int bitrate, @C.SelectionFlags int selectionFlags, String language, int bitrate, @C.SelectionFlags int selectionFlags, String language, int accessibilityChannel,
int accessibilityChannel, DrmInitData drmInitData) { DrmInitData drmInitData) {
return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language, return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
accessibilityChannel, drmInitData, OFFSET_SAMPLE_RELATIVE); accessibilityChannel, drmInitData, OFFSET_SAMPLE_RELATIVE);
} }
@ -332,11 +330,20 @@ public final class Format implements Parcelable {
// Generic. // Generic.
public static Format createContainerFormat(String id, String containerMimeType, String codecs, public static Format createContainerFormat(String id, String containerMimeType,
String sampleMimeType, int bitrate) { String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags,
String language) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, 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, 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, public static Format createSampleFormat(String id, String sampleMimeType, String codecs,
@ -495,31 +502,28 @@ public final class Format implements Parcelable {
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
@TargetApi(16) @TargetApi(16)
public final MediaFormat getFrameworkMediaFormatV16() { public final MediaFormat getFrameworkMediaFormatV16() {
if (frameworkMediaFormat == null) { MediaFormat format = new MediaFormat();
MediaFormat format = new MediaFormat(); format.setString(MediaFormat.KEY_MIME, sampleMimeType);
format.setString(MediaFormat.KEY_MIME, sampleMimeType); maybeSetStringV16(format, MediaFormat.KEY_LANGUAGE, language);
maybeSetStringV16(format, MediaFormat.KEY_LANGUAGE, language); maybeSetIntegerV16(format, MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
maybeSetIntegerV16(format, MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize); maybeSetIntegerV16(format, MediaFormat.KEY_WIDTH, width);
maybeSetIntegerV16(format, MediaFormat.KEY_WIDTH, width); maybeSetIntegerV16(format, MediaFormat.KEY_HEIGHT, height);
maybeSetIntegerV16(format, MediaFormat.KEY_HEIGHT, height); maybeSetFloatV16(format, MediaFormat.KEY_FRAME_RATE, frameRate);
maybeSetFloatV16(format, MediaFormat.KEY_FRAME_RATE, frameRate); maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees);
maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees); maybeSetIntegerV16(format, MediaFormat.KEY_CHANNEL_COUNT, channelCount);
maybeSetIntegerV16(format, MediaFormat.KEY_CHANNEL_COUNT, channelCount); maybeSetIntegerV16(format, MediaFormat.KEY_SAMPLE_RATE, sampleRate);
maybeSetIntegerV16(format, MediaFormat.KEY_SAMPLE_RATE, sampleRate); maybeSetIntegerV16(format, "encoder-delay", encoderDelay);
maybeSetIntegerV16(format, "encoder-delay", encoderDelay); maybeSetIntegerV16(format, "encoder-padding", encoderPadding);
maybeSetIntegerV16(format, "encoder-padding", encoderPadding); for (int i = 0; i < initializationData.size(); i++) {
for (int i = 0; i < initializationData.size(); i++) { format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
}
frameworkMediaFormat = format;
} }
return frameworkMediaFormat; return format;
} }
@Override @Override
public String toString() { public String toString() {
return "Format(" + id + ", " + containerMimeType + ", " + sampleMimeType + ", " + bitrate + ", " return "Format(" + id + ", " + containerMimeType + ", " + sampleMimeType + ", " + bitrate + ", "
+ ", " + language + ", [" + width + ", " + height + ", " + frameRate + "]" + language + ", [" + width + ", " + height + ", " + frameRate + "]"
+ ", [" + channelCount + ", " + sampleRate + "])"; + ", [" + 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. // Parcelable implementation.
@Override @Override

View File

@ -92,6 +92,7 @@ public interface Renderer extends ExoPlayerComponent {
* This method may be called when the renderer is in the following states: * This method may be called when the renderer is in the following states:
* {@link #STATE_DISABLED}. * {@link #STATE_DISABLED}.
* *
* @param configuration The renderer configuration.
* @param formats The enabled formats. * @param formats The enabled formats.
* @param stream The {@link SampleStream} from which the renderer should consume. * @param stream The {@link SampleStream} from which the renderer should consume.
* @param positionUs The player's current position. * @param positionUs The player's current position.
@ -100,8 +101,8 @@ public interface Renderer extends ExoPlayerComponent {
* before they are rendered. * before they are rendered.
* @throws ExoPlaybackException If an error occurs. * @throws ExoPlaybackException If an error occurs.
*/ */
void enable(Format[] formats, SampleStream stream, long positionUs, boolean joining, void enable(RendererConfiguration configuration, Format[] formats, SampleStream stream,
long offsetUs) throws ExoPlaybackException; long positionUs, boolean joining, long offsetUs) throws ExoPlaybackException;
/** /**
* Starts the renderer, meaning that calls to {@link #render(long, long)} will cause media to be * Starts the renderer, meaning that calls to {@link #render(long, long)} will cause media to be

View File

@ -79,6 +79,20 @@ public interface RendererCapabilities {
*/ */
int ADAPTIVE_NOT_SUPPORTED = 0b0000; 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 * 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 * return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will return {@link C#TRACK_TYPE_AUDIO}, a
@ -91,7 +105,7 @@ public interface RendererCapabilities {
/** /**
* Returns the extent to which the {@link Renderer} supports a given format. The returned value is * 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> * <ul>
* <li>The level of support for the format itself. One of {@link #FORMAT_HANDLED}, * <li>The level of support for the format itself. One of {@link #FORMAT_HANDLED},
* {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_SUBTYPE} and * {@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. * <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 * One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and
* {@link #ADAPTIVE_NOT_SUPPORTED}.</li> * {@link #ADAPTIVE_NOT_SUPPORTED}.</li>
* <li>The level of support for tunneling. One of {@link #TUNNELING_SUPPORTED} and
* {@link #TUNNELING_NOT_SUPPORTED}.</li>
* </ul> * </ul>
* The individual properties can be retrieved by performing a bitwise AND with * 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. * @param format The format.
* @return The extent to which the renderer is capable of supporting the given format. * @return The extent to which the renderer is capable of supporting the given format.

View File

@ -0,0 +1,60 @@
/*
* 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;
}
}

View File

@ -36,7 +36,6 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataRenderer; 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.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
@ -448,15 +447,6 @@ public class SimpleExoPlayer implements ExoPlayer {
textOutput = output; textOutput = output;
} }
/**
* @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. * Sets a listener to receive metadata events.
* *
@ -555,6 +545,36 @@ public class SimpleExoPlayer implements ExoPlayer {
player.blockingSendMessages(messages); player.blockingSendMessages(messages);
} }
@Override
public int getRendererCount() {
return player.getRendererCount();
}
@Override
public int getRendererType(int index) {
return player.getRendererType(index);
}
@Override
public TrackGroupArray getCurrentTrackGroups() {
return player.getCurrentTrackGroups();
}
@Override
public TrackSelectionArray getCurrentTrackSelections() {
return player.getCurrentTrackSelections();
}
@Override
public Timeline getCurrentTimeline() {
return player.getCurrentTimeline();
}
@Override
public Object getCurrentManifest() {
return player.getCurrentManifest();
}
@Override @Override
public int getCurrentPeriodIndex() { public int getCurrentPeriodIndex() {
return player.getCurrentPeriodIndex(); return player.getCurrentPeriodIndex();
@ -586,33 +606,13 @@ public class SimpleExoPlayer implements ExoPlayer {
} }
@Override @Override
public int getRendererCount() { public boolean isCurrentWindowDynamic() {
return player.getRendererCount(); return player.isCurrentWindowDynamic();
} }
@Override @Override
public int getRendererType(int index) { public boolean isCurrentWindowSeekable() {
return player.getRendererType(index); return player.isCurrentWindowSeekable();
}
@Override
public TrackGroupArray getCurrentTrackGroups() {
return player.getCurrentTrackGroups();
}
@Override
public TrackSelectionArray getCurrentTrackSelections() {
return player.getCurrentTrackSelections();
}
@Override
public Timeline getCurrentTimeline() {
return player.getCurrentTimeline();
}
@Override
public Object getCurrentManifest() {
return player.getCurrentManifest();
} }
// Renderer building. // Renderer building.
@ -771,7 +771,7 @@ public class SimpleExoPlayer implements ExoPlayer {
protected void buildMetadataRenderers(Context context, Handler mainHandler, protected void buildMetadataRenderers(Context context, Handler mainHandler,
@ExtensionRendererMode int extensionRendererMode, MetadataRenderer.Output output, @ExtensionRendererMode int extensionRendererMode, MetadataRenderer.Output output,
ArrayList<Renderer> out) { ArrayList<Renderer> out) {
out.add(new MetadataRenderer(output, mainHandler.getLooper(), new Id3Decoder())); out.add(new MetadataRenderer(output, mainHandler.getLooper()));
} }
/** /**

View File

@ -38,21 +38,21 @@ import java.nio.ByteOrder;
* playback position smoothing, non-blocking writes and reconfiguration. * playback position smoothing, non-blocking writes and reconfiguration.
* <p> * <p>
* Before starting playback, specify the input format by calling * Before starting playback, specify the input format by calling
* {@link #configure(String, int, int, int, int)}. Next call {@link #initialize(int)} or * {@link #configure(String, int, int, int, int)}. Optionally call {@link #setAudioSessionId(int)},
* {@link #initializeV21(int, boolean)}, optionally specifying an audio session and whether the * {@link #setStreamType(int)}, {@link #enableTunnelingV21(int)} and {@link #disableTunneling()}
* track is to be used with tunneling video playback. * to configure audio playback. These methods may be called after writing data to the track, in
* which case it will be reinitialized as required.
* <p> * <p>
* Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()} * Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()}
* when the data being fed is discontinuous. Call {@link #play()} to start playing the written data. * when the data being fed is discontinuous. Call {@link #play()} to start playing the written data.
* <p> * <p>
* Call {@link #configure(String, int, int, int, int)} whenever the input format changes. If * Call {@link #configure(String, int, int, int, int)} whenever the input format changes. The track
* {@link #isInitialized()} returns {@code false} after the call, it is necessary to call * will be reinitialized on the next call to {@link #handleBuffer(ByteBuffer, long)}.
* {@link #initialize(int)} or {@link #initializeV21(int, boolean)} before writing more data.
* <p> * <p>
* The underlying {@link android.media.AudioTrack} is created by {@link #initialize(int)} and * Calling {@link #reset()} releases the underlying {@link android.media.AudioTrack} (and so does
* released by {@link #reset()} (and {@link #configure(String, int, int, int, int)} unless the input * calling {@link #configure(String, int, int, int, int)} unless the format is unchanged). It is
* format is unchanged). It is safe to call {@link #initialize(int)} or * safe to call {@link #handleBuffer(ByteBuffer, long)} after {@link #reset()} without calling
* {@link #initializeV21(int, boolean)} after calling {@link #reset()} without reconfiguration. * {@link #configure(String, int, int, int, int)}.
* <p> * <p>
* Call {@link #release()} when the instance is no longer required. * Call {@link #release()} when the instance is no longer required.
*/ */
@ -63,6 +63,19 @@ public final class AudioTrack {
*/ */
public interface Listener { public interface Listener {
/**
* Called when the audio track has been initialized with a newly generated audio session id.
*
* @param audioSessionId The newly generated audio session id.
*/
void onAudioSessionId(int audioSessionId);
/**
* Called when the audio track handles a buffer whose timestamp is discontinuous with the last
* buffer handled since it was reset.
*/
void onPositionDiscontinuity();
/** /**
* Called when the audio track underruns. * Called when the audio track underruns.
* *
@ -137,15 +150,6 @@ public final class AudioTrack {
} }
/**
* Returned in the result of {@link #handleBuffer} if the buffer was discontinuous.
*/
public static final int RESULT_POSITION_DISCONTINUITY = 1;
/**
* Returned in the result of {@link #handleBuffer} if the buffer can be released.
*/
public static final int RESULT_BUFFER_CONSUMED = 2;
/** /**
* Returned by {@link #getCurrentPositionUs} when the position is not set. * Returned by {@link #getCurrentPositionUs} when the position is not set.
*/ */
@ -253,7 +257,7 @@ public final class AudioTrack {
private final AudioTrackUtil audioTrackUtil; private final AudioTrackUtil audioTrackUtil;
/** /**
* Used to keep the audio session active on pre-V21 builds (see {@link #initialize(int)}). * Used to keep the audio session active on pre-V21 builds (see {@link #initialize()}).
*/ */
private android.media.AudioTrack keepSessionIdAudioTrack; private android.media.AudioTrack keepSessionIdAudioTrack;
@ -271,7 +275,6 @@ public final class AudioTrack {
private int bufferSize; private int bufferSize;
private long bufferSizeUs; private long bufferSizeUs;
private boolean useHwAvSync;
private ByteBuffer avSyncHeader; private ByteBuffer avSyncHeader;
private int bytesUntilNextAvSync; private int bytesUntilNextAvSync;
@ -299,6 +302,9 @@ public final class AudioTrack {
private ByteBuffer resampledBuffer; private ByteBuffer resampledBuffer;
private boolean useResampledBuffer; private boolean useResampledBuffer;
private boolean playing;
private int audioSessionId;
private boolean tunneling;
private boolean hasData; private boolean hasData;
private long lastFeedElapsedRealtimeMs; private long lastFeedElapsedRealtimeMs;
@ -329,6 +335,7 @@ public final class AudioTrack {
volume = 1.0f; volume = 1.0f;
startMediaTimeState = START_NOT_SET; startMediaTimeState = START_NOT_SET;
streamType = C.STREAM_TYPE_DEFAULT; streamType = C.STREAM_TYPE_DEFAULT;
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
} }
/** /**
@ -342,14 +349,6 @@ public final class AudioTrack {
&& audioCapabilities.supportsEncoding(getEncodingForMimeType(mimeType)); && audioCapabilities.supportsEncoding(getEncodingForMimeType(mimeType));
} }
/**
* Returns whether the audio track has been successfully initialized via {@link #initialize} or
* {@link #initializeV21(int, boolean)}, and has not yet been {@link #reset}.
*/
public boolean isInitialized() {
return audioTrack != null;
}
/** /**
* Returns the playback position in the stream starting at zero, in microseconds, or * Returns the playback position in the stream starting at zero, in microseconds, or
* {@link #CURRENT_POSITION_NOT_SET} if it is not yet available. * {@link #CURRENT_POSITION_NOT_SET} if it is not yet available.
@ -446,7 +445,7 @@ public final class AudioTrack {
// Workaround for overly strict channel configuration checks on nVidia Shield. // Workaround for overly strict channel configuration checks on nVidia Shield.
if (Util.SDK_INT <= 23 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER)) { if (Util.SDK_INT <= 23 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER)) {
switch(channelCount) { switch (channelCount) {
case 7: case 7:
channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND; channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND;
break; break;
@ -460,6 +459,13 @@ public final class AudioTrack {
} }
boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType);
// Workaround for Nexus Player not reporting support for mono passthrough.
// (See [Internal: b/34268671].)
if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && passthrough && channelCount == 1) {
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
}
@C.Encoding int sourceEncoding; @C.Encoding int sourceEncoding;
if (passthrough) { if (passthrough) {
sourceEncoding = getEncodingForMimeType(mimeType); sourceEncoding = getEncodingForMimeType(mimeType);
@ -512,31 +518,7 @@ public final class AudioTrack {
bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(pcmBytesToFrames(bufferSize)); bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(pcmBytesToFrames(bufferSize));
} }
/** private void initialize() throws InitializationException {
* Initializes the audio track for writing new buffers using {@link #handleBuffer}.
*
* @param sessionId Audio track session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} to create
* one.
* @return The audio track session identifier.
*/
public int initialize(int sessionId) throws InitializationException {
return initializeInternal(sessionId, false);
}
/**
* Initializes the audio track for writing new buffers using {@link #handleBuffer}.
*
* @param sessionId Audio track session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} to create
* one.
* @param tunneling Whether the audio track is to be used with tunneling video playback.
* @return The audio track session identifier.
*/
public int initializeV21(int sessionId, boolean tunneling) throws InitializationException {
Assertions.checkState(Util.SDK_INT >= 21);
return initializeInternal(sessionId, tunneling);
}
private int initializeInternal(int sessionId, boolean tunneling) throws InitializationException {
// If we're asynchronously releasing a previous audio track then we block until it has been // If we're asynchronously releasing a previous audio track then we block until it has been
// released. This guarantees that we cannot end up in a state where we have multiple audio // released. This guarantees that we cannot end up in a state where we have multiple audio
// track instances. Without this guarantee it would be possible, in extreme cases, to exhaust // track instances. Without this guarantee it would be possible, in extreme cases, to exhaust
@ -544,27 +526,26 @@ public final class AudioTrack {
// initialization of the audio track to fail. // initialization of the audio track to fail.
releasingConditionVariable.block(); releasingConditionVariable.block();
useHwAvSync = tunneling; if (tunneling) {
if (useHwAvSync) {
audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, targetEncoding, audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, targetEncoding,
bufferSize, sessionId); bufferSize, audioSessionId);
} else if (sessionId == C.AUDIO_SESSION_ID_UNSET) { } else if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
targetEncoding, bufferSize, MODE_STREAM); targetEncoding, bufferSize, MODE_STREAM);
} else { } else {
// Re-attach to the same audio session. // Re-attach to the same audio session.
audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
targetEncoding, bufferSize, MODE_STREAM, sessionId); targetEncoding, bufferSize, MODE_STREAM, audioSessionId);
} }
checkAudioTrackInitialized(); checkAudioTrackInitialized();
sessionId = audioTrack.getAudioSessionId(); int audioSessionId = audioTrack.getAudioSessionId();
if (enablePreV21AudioSessionWorkaround) { if (enablePreV21AudioSessionWorkaround) {
if (Util.SDK_INT < 21) { if (Util.SDK_INT < 21) {
// The workaround creates an audio track with a two byte buffer on the same session, and // The workaround creates an audio track with a two byte buffer on the same session, and
// does not release it until this object is released, which keeps the session active. // does not release it until this object is released, which keeps the session active.
if (keepSessionIdAudioTrack != null if (keepSessionIdAudioTrack != null
&& sessionId != keepSessionIdAudioTrack.getAudioSessionId()) { && audioSessionId != keepSessionIdAudioTrack.getAudioSessionId()) {
releaseKeepSessionIdAudioTrack(); releaseKeepSessionIdAudioTrack();
} }
if (keepSessionIdAudioTrack == null) { if (keepSessionIdAudioTrack == null) {
@ -573,21 +554,25 @@ public final class AudioTrack {
@C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT; @C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT;
int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback. int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback.
keepSessionIdAudioTrack = new android.media.AudioTrack(streamType, sampleRate, keepSessionIdAudioTrack = new android.media.AudioTrack(streamType, sampleRate,
channelConfig, encoding, bufferSize, MODE_STATIC, sessionId); channelConfig, encoding, bufferSize, MODE_STATIC, audioSessionId);
} }
} }
} }
if (this.audioSessionId != audioSessionId) {
this.audioSessionId = audioSessionId;
listener.onAudioSessionId(audioSessionId);
}
audioTrackUtil.reconfigure(audioTrack, needsPassthroughWorkarounds()); audioTrackUtil.reconfigure(audioTrack, needsPassthroughWorkarounds());
setAudioTrackVolume(); setVolumeInternal();
hasData = false; hasData = false;
return sessionId;
} }
/** /**
* Starts or resumes playing audio if the audio track has been initialized. * Starts or resumes playing audio if the audio track has been initialized.
*/ */
public void play() { public void play() {
playing = true;
if (isInitialized()) { if (isInitialized()) {
resumeSystemTimeUs = System.nanoTime() / 1000; resumeSystemTimeUs = System.nanoTime() / 1000;
audioTrack.play(); audioTrack.play();
@ -608,35 +593,41 @@ public final class AudioTrack {
* Attempts to write data from a {@link ByteBuffer} to the audio track, starting from its current * Attempts to write data from a {@link ByteBuffer} to the audio track, starting from its current
* position and ending at its limit (exclusive). The position of the {@link ByteBuffer} is * position and ending at its limit (exclusive). The position of the {@link ByteBuffer} is
* advanced by the number of bytes that were successfully written. * advanced by the number of bytes that were successfully written.
* {@link Listener#onPositionDiscontinuity()} will be called if {@code presentationTimeUs} is
* discontinuous with the last buffer handled since the track was reset.
* <p> * <p>
* Returns a bit field containing {@link #RESULT_BUFFER_CONSUMED} if the data was written in full, * Returns whether the data was written in full. If the data was not written in full then the same
* and {@link #RESULT_POSITION_DISCONTINUITY} if the buffer was discontinuous with previously * {@link ByteBuffer} must be provided to subsequent calls until it has been fully consumed,
* written data. * except in the case of an interleaving call to {@link #reset()} (or an interleaving call to
* <p> * {@link #configure(String, int, int, int, int)} that caused the track to be reset).
* If the data was not written in full then the same {@link ByteBuffer} must be provided to
* subsequent calls until it has been fully consumed, except in the case of an interleaving call
* to {@link #configure} or {@link #reset}.
* *
* @param buffer The buffer containing audio data to play back. * @param buffer The buffer containing audio data to play back.
* @param presentationTimeUs Presentation timestamp of the next buffer in microseconds. * @param presentationTimeUs Presentation timestamp of the next buffer in microseconds.
* @return A bit field with {@link #RESULT_BUFFER_CONSUMED} if the buffer can be released, and * @return Whether the buffer was consumed fully.
* {@link #RESULT_POSITION_DISCONTINUITY} if the buffer was not contiguous with previously * @throws InitializationException If an error occurs initializing the track.
* written data.
* @throws WriteException If an error occurs writing the audio data. * @throws WriteException If an error occurs writing the audio data.
*/ */
public int handleBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException { public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs)
throws InitializationException, WriteException {
if (!isInitialized()) {
initialize();
if (playing) {
play();
}
}
boolean hadData = hasData; boolean hadData = hasData;
hasData = hasPendingData(); hasData = hasPendingData();
if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) { if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) {
long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs;
listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs); listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs);
} }
int result = writeBuffer(buffer, presentationTimeUs); boolean result = writeBuffer(buffer, presentationTimeUs);
lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime();
return result; return result;
} }
private int writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException { private boolean writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException {
boolean isNewSourceBuffer = currentSourceBuffer == null; boolean isNewSourceBuffer = currentSourceBuffer == null;
Assertions.checkState(isNewSourceBuffer || currentSourceBuffer == buffer); Assertions.checkState(isNewSourceBuffer || currentSourceBuffer == buffer);
currentSourceBuffer = buffer; currentSourceBuffer = buffer;
@ -645,7 +636,7 @@ public final class AudioTrack {
// An AC-3 audio track continues to play data written while it is paused. Stop writing so its // An AC-3 audio track continues to play data written while it is paused. Stop writing so its
// buffer empties. See [Internal: b/18899620]. // buffer empties. See [Internal: b/18899620].
if (audioTrack.getPlayState() == PLAYSTATE_PAUSED) { if (audioTrack.getPlayState() == PLAYSTATE_PAUSED) {
return 0; return false;
} }
// A new AC-3 audio track's playback position continues to increase from the old track's // A new AC-3 audio track's playback position continues to increase from the old track's
@ -653,18 +644,17 @@ public final class AudioTrack {
// head position actually returns to zero. // head position actually returns to zero.
if (audioTrack.getPlayState() == PLAYSTATE_STOPPED if (audioTrack.getPlayState() == PLAYSTATE_STOPPED
&& audioTrackUtil.getPlaybackHeadPosition() != 0) { && audioTrackUtil.getPlaybackHeadPosition() != 0) {
return 0; return false;
} }
} }
int result = 0;
if (isNewSourceBuffer) { if (isNewSourceBuffer) {
// We're seeing this buffer for the first time. // We're seeing this buffer for the first time.
if (!currentSourceBuffer.hasRemaining()) { if (!currentSourceBuffer.hasRemaining()) {
// The buffer is empty. // The buffer is empty.
currentSourceBuffer = null; currentSourceBuffer = null;
return RESULT_BUFFER_CONSUMED; return true;
} }
useResampledBuffer = targetEncoding != sourceEncoding; useResampledBuffer = targetEncoding != sourceEncoding;
@ -697,7 +687,7 @@ public final class AudioTrack {
// number of bytes submitted. // number of bytes submitted.
startMediaTimeUs += (presentationTimeUs - expectedPresentationTimeUs); startMediaTimeUs += (presentationTimeUs - expectedPresentationTimeUs);
startMediaTimeState = START_IN_SYNC; startMediaTimeState = START_IN_SYNC;
result |= RESULT_POSITION_DISCONTINUITY; listener.onPositionDiscontinuity();
} }
} }
if (Util.SDK_INT < 21) { if (Util.SDK_INT < 21) {
@ -730,7 +720,7 @@ public final class AudioTrack {
buffer.position(buffer.position() + bytesWritten); buffer.position(buffer.position() + bytesWritten);
} }
} else { } else {
bytesWritten = useHwAvSync bytesWritten = tunneling
? writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, presentationTimeUs) ? writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, presentationTimeUs)
: writeNonBlockingV21(audioTrack, buffer, bytesRemaining); : writeNonBlockingV21(audioTrack, buffer, bytesRemaining);
} }
@ -747,9 +737,9 @@ public final class AudioTrack {
submittedEncodedFrames += framesPerEncodedSample; submittedEncodedFrames += framesPerEncodedSample;
} }
currentSourceBuffer = null; currentSourceBuffer = null;
result |= RESULT_BUFFER_CONSUMED; return true;
} }
return result; return false;
} }
/** /**
@ -785,28 +775,64 @@ public final class AudioTrack {
/** /**
* Sets the stream type for audio track. If the stream type has changed and if the audio track * Sets the stream type for audio track. If the stream type has changed and if the audio track
* is not configured for use with video tunneling, then the audio track is reset and the caller * is not configured for use with tunneling, then the audio track is reset and the audio session
* must re-initialize the audio track before writing more data. The caller must not reuse the * id is cleared.
* audio session identifier when re-initializing with a new stream type.
* <p> * <p>
* If the audio track is configured for use with video tunneling then the stream type is ignored * If the audio track is configured for use with tunneling then the stream type is ignored, the
* and the audio track is not reset. The passed stream type will be used if the audio track is * audio track is not reset and the audio session id is not cleared. The passed stream type will
* later re-configured into non-tunneled mode. * be used if the audio track is later re-configured into non-tunneled mode.
* *
* @param streamType The {@link C.StreamType} to use for audio output. * @param streamType The {@link C.StreamType} to use for audio output.
* @return Whether the audio track was reset as a result of this call.
*/ */
public boolean setStreamType(@C.StreamType int streamType) { public void setStreamType(@C.StreamType int streamType) {
if (this.streamType == streamType) { if (this.streamType == streamType) {
return false; return;
} }
this.streamType = streamType; this.streamType = streamType;
if (useHwAvSync) { if (tunneling) {
// The stream type is ignored in tunneling mode, so no need to reset. // The stream type is ignored in tunneling mode, so no need to reset.
return false; return;
} }
reset(); reset();
return true; audioSessionId = C.AUDIO_SESSION_ID_UNSET;
}
/**
* Sets the audio session id. The audio track is reset if the audio session id has changed.
*/
public void setAudioSessionId(int audioSessionId) {
if (this.audioSessionId != audioSessionId) {
this.audioSessionId = audioSessionId;
reset();
}
}
/**
* Enables tunneling. The audio track is reset if tunneling was previously disabled or if the
* audio session id has changed. Enabling tunneling requires platform API version 21 onwards.
*
* @param tunnelingAudioSessionId The audio session id to use.
* @throws IllegalStateException Thrown if enabling tunneling on platform API version < 21.
*/
public void enableTunnelingV21(int tunnelingAudioSessionId) {
Assertions.checkState(Util.SDK_INT >= 21);
if (!tunneling || audioSessionId != tunnelingAudioSessionId) {
tunneling = true;
audioSessionId = tunnelingAudioSessionId;
reset();
}
}
/**
* Disables tunneling. If tunneling was previously enabled then the audio track is reset and the
* audio session id is cleared.
*/
public void disableTunneling() {
if (tunneling) {
tunneling = false;
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
reset();
}
} }
/** /**
@ -817,17 +843,17 @@ public final class AudioTrack {
public void setVolume(float volume) { public void setVolume(float volume) {
if (this.volume != volume) { if (this.volume != volume) {
this.volume = volume; this.volume = volume;
setAudioTrackVolume(); setVolumeInternal();
} }
} }
private void setAudioTrackVolume() { private void setVolumeInternal() {
if (!isInitialized()) { if (!isInitialized()) {
// Do nothing. // Do nothing.
} else if (Util.SDK_INT >= 21) { } else if (Util.SDK_INT >= 21) {
setAudioTrackVolumeV21(audioTrack, volume); setVolumeInternalV21(audioTrack, volume);
} else { } else {
setAudioTrackVolumeV3(audioTrack, volume); setVolumeInternalV3(audioTrack, volume);
} }
} }
@ -835,6 +861,7 @@ public final class AudioTrack {
* Pauses playback. * Pauses playback.
*/ */
public void pause() { public void pause() {
playing = false;
if (isInitialized()) { if (isInitialized()) {
resetSyncParams(); resetSyncParams();
audioTrackUtil.pause(); audioTrackUtil.pause();
@ -844,9 +871,9 @@ public final class AudioTrack {
/** /**
* Releases the underlying audio track asynchronously. * Releases the underlying audio track asynchronously.
* <p> * <p>
* Calling {@link #initialize(int)} or {@link #initializeV21(int, boolean)} will block until the * Calling {@link #handleBuffer(ByteBuffer, long)} will block until the audio track has been
* audio track has been released, so it is safe to initialize immediately after a reset. The audio * released, so it is safe to use the audio track immediately after a reset. The audio session may
* session may remain active until {@link #release()} is called. * remain active until {@link #release()} is called.
*/ */
public void reset() { public void reset() {
if (isInitialized()) { if (isInitialized()) {
@ -855,6 +882,7 @@ public final class AudioTrack {
framesPerEncodedSample = 0; framesPerEncodedSample = 0;
currentSourceBuffer = null; currentSourceBuffer = null;
avSyncHeader = null; avSyncHeader = null;
bytesUntilNextAvSync = 0;
startMediaTimeState = START_NOT_SET; startMediaTimeState = START_NOT_SET;
latencyUs = 0; latencyUs = 0;
resetSyncParams(); resetSyncParams();
@ -887,6 +915,8 @@ public final class AudioTrack {
public void release() { public void release() {
reset(); reset();
releaseKeepSessionIdAudioTrack(); releaseKeepSessionIdAudioTrack();
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
playing = false;
} }
/** /**
@ -1024,6 +1054,10 @@ public final class AudioTrack {
throw new InitializationException(state, sampleRate, channelConfig, bufferSize); throw new InitializationException(state, sampleRate, channelConfig, bufferSize);
} }
private boolean isInitialized() {
return audioTrack != null;
}
private long pcmBytesToFrames(long byteCount) { private long pcmBytesToFrames(long byteCount) {
return byteCount / pcmFrameSize; return byteCount / pcmFrameSize;
} }
@ -1240,12 +1274,12 @@ public final class AudioTrack {
} }
@TargetApi(21) @TargetApi(21)
private static void setAudioTrackVolumeV21(android.media.AudioTrack audioTrack, float volume) { private static void setVolumeInternalV21(android.media.AudioTrack audioTrack, float volume) {
audioTrack.setVolume(volume); audioTrack.setVolume(volume);
} }
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
private static void setAudioTrackVolumeV3(android.media.AudioTrack audioTrack, float volume) { private static void setVolumeInternalV3(android.media.AudioTrack audioTrack, float volume) {
audioTrack.setStereoVolume(volume, volume); audioTrack.setStereoVolume(volume, volume);
} }
@ -1494,7 +1528,7 @@ public final class AudioTrack {
playbackParams = (playbackParams != null ? playbackParams : new PlaybackParams()) playbackParams = (playbackParams != null ? playbackParams : new PlaybackParams())
.allowDefaults(); .allowDefaults();
this.playbackParams = playbackParams; this.playbackParams = playbackParams;
this.playbackSpeed = playbackParams.getSpeed(); playbackSpeed = playbackParams.getSpeed();
maybeApplyPlaybackParams(); maybeApplyPlaybackParams();
} }

View File

@ -41,8 +41,7 @@ import java.nio.ByteBuffer;
* Decodes and renders audio using {@link MediaCodec} and {@link AudioTrack}. * Decodes and renders audio using {@link MediaCodec} and {@link AudioTrack}.
*/ */
@TargetApi(16) @TargetApi(16)
public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock, public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock {
AudioTrack.Listener {
private final EventDispatcher eventDispatcher; private final EventDispatcher eventDispatcher;
private final AudioTrack audioTrack; private final AudioTrack audioTrack;
@ -50,7 +49,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
private boolean passthroughEnabled; private boolean passthroughEnabled;
private android.media.MediaFormat passthroughMediaFormat; private android.media.MediaFormat passthroughMediaFormat;
private int pcmEncoding; private int pcmEncoding;
private int audioSessionId;
private long currentPositionUs; private long currentPositionUs;
private boolean allowPositionDiscontinuity; private boolean allowPositionDiscontinuity;
@ -129,8 +127,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
boolean playClearSamplesWithoutKeys, Handler eventHandler, boolean playClearSamplesWithoutKeys, Handler eventHandler,
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) { AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) {
super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys);
audioSessionId = C.AUDIO_SESSION_ID_UNSET; audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener());
audioTrack = new AudioTrack(audioCapabilities, this);
eventDispatcher = new EventDispatcher(eventHandler, eventListener); eventDispatcher = new EventDispatcher(eventHandler, eventListener);
} }
@ -141,10 +138,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
if (!MimeTypes.isAudio(mimeType)) { if (!MimeTypes.isAudio(mimeType)) {
return FORMAT_UNSUPPORTED_TYPE; return FORMAT_UNSUPPORTED_TYPE;
} }
int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;
if (allowPassthrough(mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null) { 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) { if (decoderInfo == null) {
return FORMAT_UNSUPPORTED_SUBTYPE; return FORMAT_UNSUPPORTED_SUBTYPE;
} }
@ -155,7 +153,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
&& (format.channelCount == Format.NO_VALUE && (format.channelCount == Format.NO_VALUE
|| decoderInfo.isAudioChannelCountSupportedV21(format.channelCount))); || decoderInfo.isAudioChannelCountSupportedV21(format.channelCount)));
int formatSupport = decoderCapable ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES; int formatSupport = decoderCapable ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES;
return ADAPTIVE_NOT_SEAMLESS | formatSupport; return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport;
} }
@Override @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 * Called when the audio session id becomes known. The default implementation is a no-op. One
* hence this method will not be called again) unless the renderer is disabled and then * reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in
* subsequently re-enabled. * order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances
* <p> * should be released in {@link #onDisabled()} (if not before).
* 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) { protected void onAudioSessionId(int audioSessionId) {
// Do nothing. // 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 @Override
protected void onEnabled(boolean joining) throws ExoPlaybackException { protected void onEnabled(boolean joining) throws ExoPlaybackException {
super.onEnabled(joining); super.onEnabled(joining);
eventDispatcher.enabled(decoderCounters); eventDispatcher.enabled(decoderCounters);
int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;
if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {
audioTrack.enableTunnelingV21(tunnelingAudioSessionId);
} else {
audioTrack.disableTunneling();
}
} }
@Override @Override
@ -274,7 +289,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Override @Override
protected void onDisabled() { protected void onDisabled() {
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
try { try {
audioTrack.release(); audioTrack.release();
} finally { } finally {
@ -325,44 +339,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
return true; 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 { try {
handleBufferResult = audioTrack.handleBuffer(buffer, bufferPresentationTimeUs); if (audioTrack.handleBuffer(buffer, bufferPresentationTimeUs)) {
} catch (AudioTrack.WriteException e) { codec.releaseOutputBuffer(bufferIndex, false);
decoderCounters.renderedOutputBufferCount++;
return true;
}
} catch (AudioTrack.InitializationException | AudioTrack.WriteException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex()); 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; return false;
} }
@ -371,10 +356,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
audioTrack.handleEndOfStream(); audioTrack.handleEndOfStream();
} }
protected void handleAudioTrackDiscontinuity() {
// Do nothing
}
@Override @Override
public void handleMessage(int messageType, Object message) throws ExoPlaybackException { public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
switch (messageType) { switch (messageType) {
@ -386,9 +367,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
break; break;
case C.MSG_SET_STREAM_TYPE: case C.MSG_SET_STREAM_TYPE:
@C.StreamType int streamType = (Integer) message; @C.StreamType int streamType = (Integer) message;
if (audioTrack.setStreamType(streamType)) { audioTrack.setStreamType(streamType);
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
}
break; break;
default: default:
super.handleMessage(messageType, message); 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);
} }
} }

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.audio; package com.google.android.exoplayer2.audio;
import android.media.PlaybackParams; import android.media.PlaybackParams;
import android.media.audiofx.Virtualizer;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.SystemClock; import android.os.SystemClock;
@ -43,8 +44,7 @@ import java.lang.annotation.RetentionPolicy;
/** /**
* Decodes and renders audio using a {@link SimpleDecoder}. * Decodes and renders audio using a {@link SimpleDecoder}.
*/ */
public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock, public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock {
AudioTrack.Listener {
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef({REINITIALIZATION_STATE_NONE, REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM, @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 outputStreamEnded;
private boolean waitingForKeys; private boolean waitingForKeys;
private int audioSessionId;
public SimpleDecoderAudioRenderer() { public SimpleDecoderAudioRenderer() {
this(null, null); this(null, null);
} }
@ -141,11 +139,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys) { DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys) {
super(C.TRACK_TYPE_AUDIO); super(C.TRACK_TYPE_AUDIO);
eventDispatcher = new EventDispatcher(eventHandler, eventListener); eventDispatcher = new EventDispatcher(eventHandler, eventListener);
audioTrack = new AudioTrack(audioCapabilities, this); audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener());
this.drmSessionManager = drmSessionManager; this.drmSessionManager = drmSessionManager;
formatHolder = new FormatHolder(); formatHolder = new FormatHolder();
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
decoderReinitializationState = REINITIALIZATION_STATE_NONE; decoderReinitializationState = REINITIALIZATION_STATE_NONE;
audioTrackNeedsConfigure = true; audioTrackNeedsConfigure = true;
} }
@ -155,6 +152,25 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
return this; return this;
} }
@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 @Override
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
if (outputStreamEnded) { if (outputStreamEnded) {
@ -185,6 +201,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. * Creates a decoder for the given format.
* *
@ -244,28 +287,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
audioTrackNeedsConfigure = false; audioTrackNeedsConfigure = false;
} }
if (!audioTrack.isInitialized()) { if (audioTrack.handleBuffer(outputBuffer.data, outputBuffer.timeUs)) {
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) {
decoderCounters.renderedOutputBufferCount++; decoderCounters.renderedOutputBufferCount++;
outputBuffer.release(); outputBuffer.release();
outputBuffer = null; outputBuffer = null;
@ -381,23 +403,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
return currentPositionUs; 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 @Override
protected void onEnabled(boolean joining) throws ExoPlaybackException { protected void onEnabled(boolean joining) throws ExoPlaybackException {
decoderCounters = new DecoderCounters(); decoderCounters = new DecoderCounters();
eventDispatcher.enabled(decoderCounters); eventDispatcher.enabled(decoderCounters);
int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;
if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {
audioTrack.enableTunnelingV21(tunnelingAudioSessionId);
} else {
audioTrack.disableTunneling();
}
} }
@Override @Override
@ -425,7 +440,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
@Override @Override
protected void onDisabled() { protected void onDisabled() {
inputFormat = null; inputFormat = null;
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
audioTrackNeedsConfigure = true; audioTrackNeedsConfigure = true;
waitingForKeys = false; waitingForKeys = false;
try { try {
@ -537,6 +551,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
// There aren't any final output buffers, so release the decoder immediately. // There aren't any final output buffers, so release the decoder immediately.
releaseDecoder(); releaseDecoder();
maybeInitDecoder(); maybeInitDecoder();
audioTrackNeedsConfigure = true;
} }
eventDispatcher.inputFormatChanged(newFormat); eventDispatcher.inputFormatChanged(newFormat);
@ -553,9 +568,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
break; break;
case C.MSG_SET_STREAM_TYPE: case C.MSG_SET_STREAM_TYPE:
@C.StreamType int streamType = (Integer) message; @C.StreamType int streamType = (Integer) message;
if (audioTrack.setStreamType(streamType)) { audioTrack.setStreamType(streamType);
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
}
break; break;
default: default:
super.handleMessage(messageType, message); 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);
} }
} }

View File

@ -24,7 +24,10 @@ import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.Looper; import android.os.Looper;
import android.os.Message; import android.os.Message;
import android.support.annotation.IntDef;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest; import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
@ -33,18 +36,21 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
/** /**
* A {@link DrmSessionManager} that supports streaming playbacks using {@link MediaDrm}. * A {@link DrmSessionManager} that supports playbacks using {@link MediaDrm}.
*/ */
@TargetApi(18) @TargetApi(18)
public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T>, public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T>,
DrmSession<T> { DrmSession<T> {
/** /**
* Listener of {@link StreamingDrmSessionManager} events. * Listener of {@link DefaultDrmSessionManager} events.
*/ */
public interface EventListener { public interface EventListener {
@ -60,6 +66,16 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
*/ */
void onDrmSessionManagerError(Exception e); void onDrmSessionManagerError(Exception e);
/**
* Called each time offline keys are restored.
*/
void onDrmKeysRestored();
/**
* Called each time offline keys are removed.
*/
void onDrmKeysRemoved();
} }
/** /**
@ -67,9 +83,32 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
*/ */
public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData"; public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData";
/** Determines the action to be done after a session acquired. */
@Retention(RetentionPolicy.SOURCE)
@IntDef({MODE_PLAYBACK, MODE_QUERY, MODE_DOWNLOAD, MODE_RELEASE})
public @interface Mode {}
/**
* Loads and refreshes (if necessary) a license for playback. Supports streaming and offline
* licenses.
*/
public static final int MODE_PLAYBACK = 0;
/**
* Restores an offline license to allow its status to be queried. If the offline license is
* expired sets state to {@link #STATE_ERROR}.
*/
public static final int MODE_QUERY = 1;
/** Downloads an offline license or renews an existing one. */
public static final int MODE_DOWNLOAD = 2;
/** Releases an existing offline license. */
public static final int MODE_RELEASE = 3;
private static final String TAG = "OfflineDrmSessionMngr";
private static final int MSG_PROVISION = 0; private static final int MSG_PROVISION = 0;
private static final int MSG_KEYS = 1; private static final int MSG_KEYS = 1;
private static final int MAX_LICENSE_DURATION_TO_RENEW = 60;
private final Handler eventHandler; private final Handler eventHandler;
private final EventListener eventListener; private final EventListener eventListener;
private final ExoMediaDrm<T> mediaDrm; private final ExoMediaDrm<T> mediaDrm;
@ -85,14 +124,17 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
private HandlerThread requestHandlerThread; private HandlerThread requestHandlerThread;
private Handler postRequestHandler; private Handler postRequestHandler;
private int mode;
private int openCount; private int openCount;
private boolean provisioningInProgress; private boolean provisioningInProgress;
@DrmSession.State @DrmSession.State
private int state; private int state;
private T mediaCrypto; private T mediaCrypto;
private Exception lastException; private DrmSessionException lastException;
private SchemeData schemeData; private byte[] schemeInitData;
private String schemeMimeType;
private byte[] sessionId; private byte[] sessionId;
private byte[] offlineLicenseKeySetId;
/** /**
* Instantiates a new instance using the Widevine scheme. * Instantiates a new instance using the Widevine scheme.
@ -105,7 +147,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
* @param eventListener A listener of events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required.
* @throws UnsupportedDrmException If the specified DRM scheme is not supported. * @throws UnsupportedDrmException If the specified DRM scheme is not supported.
*/ */
public static StreamingDrmSessionManager<FrameworkMediaCrypto> newWidevineInstance( public static DefaultDrmSessionManager<FrameworkMediaCrypto> newWidevineInstance(
MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters, MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters,
Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException { Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException {
return newFrameworkInstance(C.WIDEVINE_UUID, callback, optionalKeyRequestParameters, return newFrameworkInstance(C.WIDEVINE_UUID, callback, optionalKeyRequestParameters,
@ -125,7 +167,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
* @param eventListener A listener of events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required.
* @throws UnsupportedDrmException If the specified DRM scheme is not supported. * @throws UnsupportedDrmException If the specified DRM scheme is not supported.
*/ */
public static StreamingDrmSessionManager<FrameworkMediaCrypto> newPlayReadyInstance( public static DefaultDrmSessionManager<FrameworkMediaCrypto> newPlayReadyInstance(
MediaDrmCallback callback, String customData, Handler eventHandler, MediaDrmCallback callback, String customData, Handler eventHandler,
EventListener eventListener) throws UnsupportedDrmException { EventListener eventListener) throws UnsupportedDrmException {
HashMap<String, String> optionalKeyRequestParameters; HashMap<String, String> optionalKeyRequestParameters;
@ -151,10 +193,10 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
* @param eventListener A listener of events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required.
* @throws UnsupportedDrmException If the specified DRM scheme is not supported. * @throws UnsupportedDrmException If the specified DRM scheme is not supported.
*/ */
public static StreamingDrmSessionManager<FrameworkMediaCrypto> newFrameworkInstance( public static DefaultDrmSessionManager<FrameworkMediaCrypto> newFrameworkInstance(
UUID uuid, MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters, UUID uuid, MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters,
Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException { Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException {
return new StreamingDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), callback, return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), callback,
optionalKeyRequestParameters, eventHandler, eventListener); optionalKeyRequestParameters, eventHandler, eventListener);
} }
@ -168,7 +210,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
* null if delivery of events is not required. * null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required.
*/ */
public StreamingDrmSessionManager(UUID uuid, ExoMediaDrm<T> mediaDrm, MediaDrmCallback callback, public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm<T> mediaDrm, MediaDrmCallback callback,
HashMap<String, String> optionalKeyRequestParameters, Handler eventHandler, HashMap<String, String> optionalKeyRequestParameters, Handler eventHandler,
EventListener eventListener) { EventListener eventListener) {
this.uuid = uuid; this.uuid = uuid;
@ -179,6 +221,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
this.eventListener = eventListener; this.eventListener = eventListener;
mediaDrm.setOnEventListener(new MediaDrmEventListener()); mediaDrm.setOnEventListener(new MediaDrmEventListener());
state = STATE_CLOSED; state = STATE_CLOSED;
mode = MODE_PLAYBACK;
} }
/** /**
@ -229,6 +272,35 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
mediaDrm.setPropertyByteArray(key, value); mediaDrm.setPropertyByteArray(key, value);
} }
/**
* Sets the mode, which determines the role of sessions acquired from the instance. This must be
* called before {@link #acquireSession(Looper, DrmInitData)} is called.
*
* <p>By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when
* required.
*
* <p>{@code mode} must be one of these:
* <li>{@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is
* requested otherwise the offline license is restored.
* <li>{@link #MODE_QUERY}: {@code offlineLicenseKeySetId} can not be null. The offline license
* is restored.
* <li>{@link #MODE_DOWNLOAD}: If {@code offlineLicenseKeySetId} is null, an offline license is
* requested otherwise the offline license is renewed.
* <li>{@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline license
* is released.
*
* @param mode The mode to be set.
* @param offlineLicenseKeySetId The key set id of the license to be used with the given mode.
*/
public void setMode(@Mode int mode, byte[] offlineLicenseKeySetId) {
Assertions.checkState(openCount == 0);
if (mode == MODE_QUERY || mode == MODE_RELEASE) {
Assertions.checkNotNull(offlineLicenseKeySetId);
}
this.mode = mode;
this.offlineLicenseKeySetId = offlineLicenseKeySetId;
}
// DrmSessionManager implementation. // DrmSessionManager implementation.
@Override @Override
@ -248,18 +320,22 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
requestHandlerThread.start(); requestHandlerThread.start();
postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper()); postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
schemeData = drmInitData.get(uuid); if (offlineLicenseKeySetId == null) {
if (schemeData == null) { SchemeData schemeData = drmInitData.get(uuid);
onError(new IllegalStateException("Media does not support uuid: " + uuid)); if (schemeData == null) {
return this; onError(new IllegalStateException("Media does not support uuid: " + uuid));
} return this;
if (Util.SDK_INT < 21) { }
// Prior to L the Widevine CDM required data to be extracted from the PSSH atom. schemeInitData = schemeData.data;
byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeData.data, C.WIDEVINE_UUID); schemeMimeType = schemeData.mimeType;
if (psshData == null) { if (Util.SDK_INT < 21) {
// Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged. // Prior to L the Widevine CDM required data to be extracted from the PSSH atom.
} else { byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, C.WIDEVINE_UUID);
schemeData = new SchemeData(C.WIDEVINE_UUID, schemeData.mimeType, psshData); if (psshData == null) {
// Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged.
} else {
schemeInitData = psshData;
}
} }
} }
state = STATE_OPENING; state = STATE_OPENING;
@ -280,7 +356,8 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
postRequestHandler = null; postRequestHandler = null;
requestHandlerThread.quit(); requestHandlerThread.quit();
requestHandlerThread = null; requestHandlerThread = null;
schemeData = null; schemeInitData = null;
schemeMimeType = null;
mediaCrypto = null; mediaCrypto = null;
lastException = null; lastException = null;
if (sessionId != null) { if (sessionId != null) {
@ -314,10 +391,25 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
} }
@Override @Override
public final Exception getError() { public final DrmSessionException getError() {
return state == STATE_ERROR ? lastException : null; return state == STATE_ERROR ? lastException : null;
} }
@Override
public Map<String, String> queryKeyStatus() {
// User may call this method rightfully even if state == STATE_ERROR. So only check if there is
// a sessionId
if (sessionId == null) {
throw new IllegalStateException();
}
return mediaDrm.queryKeyStatus(sessionId);
}
@Override
public byte[] getOfflineLicenseKeySetId() {
return offlineLicenseKeySetId;
}
// Internal methods. // Internal methods.
private void openInternal(boolean allowProvisioning) { private void openInternal(boolean allowProvisioning) {
@ -325,7 +417,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
sessionId = mediaDrm.openSession(); sessionId = mediaDrm.openSession();
mediaCrypto = mediaDrm.createMediaCrypto(uuid, sessionId); mediaCrypto = mediaDrm.createMediaCrypto(uuid, sessionId);
state = STATE_OPENED; state = STATE_OPENED;
postKeyRequest(); doLicense();
} catch (NotProvisionedException e) { } catch (NotProvisionedException e) {
if (allowProvisioning) { if (allowProvisioning) {
postProvisionRequest(); postProvisionRequest();
@ -363,20 +455,87 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
if (state == STATE_OPENING) { if (state == STATE_OPENING) {
openInternal(false); openInternal(false);
} else { } else {
postKeyRequest(); doLicense();
} }
} catch (DeniedByServerException e) { } catch (DeniedByServerException e) {
onError(e); onError(e);
} }
} }
private void postKeyRequest() { private void doLicense() {
switch (mode) {
case MODE_PLAYBACK:
case MODE_QUERY:
if (offlineLicenseKeySetId == null) {
postKeyRequest(sessionId, MediaDrm.KEY_TYPE_STREAMING);
} else {
if (restoreKeys()) {
long licenseDurationRemainingSec = getLicenseDurationRemainingSec();
if (mode == MODE_PLAYBACK
&& licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW) {
Log.d(TAG, "Offline license has expired or will expire soon. "
+ "Remaining seconds: " + licenseDurationRemainingSec);
postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE);
} else if (licenseDurationRemainingSec <= 0) {
onError(new KeysExpiredException());
} else {
state = STATE_OPENED_WITH_KEYS;
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onDrmKeysRestored();
}
});
}
}
}
}
break;
case MODE_DOWNLOAD:
if (offlineLicenseKeySetId == null) {
postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE);
} else {
// Renew
if (restoreKeys()) {
postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE);
}
}
break;
case MODE_RELEASE:
if (restoreKeys()) {
postKeyRequest(offlineLicenseKeySetId, MediaDrm.KEY_TYPE_RELEASE);
}
break;
}
}
private boolean restoreKeys() {
try {
mediaDrm.restoreKeys(sessionId, offlineLicenseKeySetId);
return true;
} catch (Exception e) {
Log.e(TAG, "Error trying to restore Widevine keys.", e);
onError(e);
}
return false;
}
private long getLicenseDurationRemainingSec() {
if (!C.WIDEVINE_UUID.equals(uuid)) {
return Long.MAX_VALUE;
}
Pair<Long, Long> pair = WidevineUtil.getLicenseDurationRemainingSec(this);
return Math.min(pair.first, pair.second);
}
private void postKeyRequest(byte[] scope, int keyType) {
KeyRequest keyRequest; KeyRequest keyRequest;
try { try {
keyRequest = mediaDrm.getKeyRequest(sessionId, schemeData.data, schemeData.mimeType, keyRequest = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, keyType,
MediaDrm.KEY_TYPE_STREAMING, optionalKeyRequestParameters); optionalKeyRequestParameters);
postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget(); postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
} catch (NotProvisionedException e) { } catch (Exception e) {
onKeysError(e); onKeysError(e);
} }
} }
@ -393,15 +552,30 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
} }
try { try {
mediaDrm.provideKeyResponse(sessionId, (byte[]) response); if (mode == MODE_RELEASE) {
state = STATE_OPENED_WITH_KEYS; mediaDrm.provideKeyResponse(offlineLicenseKeySetId, (byte[]) response);
if (eventHandler != null && eventListener != null) { if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() { eventHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
eventListener.onDrmKeysLoaded(); eventListener.onDrmKeysRemoved();
} }
}); });
}
} else {
byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
if (keySetId != null && keySetId.length != 0) {
offlineLicenseKeySetId = keySetId;
}
state = STATE_OPENED_WITH_KEYS;
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
@Override
public void run() {
eventListener.onDrmKeysLoaded();
}
});
}
} }
} catch (Exception e) { } catch (Exception e) {
onKeysError(e); onKeysError(e);
@ -417,7 +591,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
} }
private void onError(final Exception e) { private void onError(final Exception e) {
lastException = e; lastException = new DrmSessionException(e);
if (eventHandler != null && eventListener != null) { if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() { eventHandler.post(new Runnable() {
@Override @Override
@ -446,11 +620,16 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
} }
switch (msg.what) { switch (msg.what) {
case MediaDrm.EVENT_KEY_REQUIRED: case MediaDrm.EVENT_KEY_REQUIRED:
postKeyRequest(); doLicense();
break; break;
case MediaDrm.EVENT_KEY_EXPIRED: case MediaDrm.EVENT_KEY_EXPIRED:
state = STATE_OPENED; // When an already expired key is loaded MediaDrm sends this event immediately. Ignore
onError(new KeysExpiredException()); // this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still
// waiting for key response.
if (state == STATE_OPENED_WITH_KEYS) {
state = STATE_OPENED;
onError(new KeysExpiredException());
}
break; break;
case MediaDrm.EVENT_PROVISION_REQUIRED: case MediaDrm.EVENT_PROVISION_REQUIRED:
state = STATE_OPENED; state = STATE_OPENED;
@ -466,7 +645,9 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
@Override @Override
public void onEvent(ExoMediaDrm<? extends T> md, byte[] sessionId, int event, int extra, public void onEvent(ExoMediaDrm<? extends T> md, byte[] sessionId, int event, int extra,
byte[] data) { byte[] data) {
mediaDrmHandler.sendEmptyMessage(event); if (mode == MODE_PLAYBACK) {
mediaDrmHandler.sendEmptyMessage(event);
}
} }
} }

View File

@ -16,9 +16,11 @@
package com.google.android.exoplayer2.drm; package com.google.android.exoplayer2.drm;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.media.MediaDrm;
import android.support.annotation.IntDef; import android.support.annotation.IntDef;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.Map;
/** /**
* A DRM session. * A DRM session.
@ -26,6 +28,15 @@ import java.lang.annotation.RetentionPolicy;
@TargetApi(16) @TargetApi(16)
public interface DrmSession<T extends ExoMediaCrypto> { 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. * 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. * @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();
} }

View File

@ -105,7 +105,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
try { try {
return Util.toByteArray(inputStream); return Util.toByteArray(inputStream);
} finally { } finally {
inputStream.close(); Util.closeQuietly(inputStream);
} }
} }

View File

@ -0,0 +1,315 @@
/*
* 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 android.media.MediaDrm;
import android.net.Uri;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.EventListener;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.Mode;
import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper;
import com.google.android.exoplayer2.source.chunk.InitializationChunk;
import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
import com.google.android.exoplayer2.source.dash.manifest.Period;
import com.google.android.exoplayer2.source.dash.manifest.RangedUri;
import com.google.android.exoplayer2.source.dash.manifest.Representation;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import java.io.IOException;
import java.util.HashMap;
/**
* Helper class to download, renew and release offline licenses. It utilizes {@link
* DefaultDrmSessionManager}.
*/
public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
private final ConditionVariable conditionVariable;
private final DefaultDrmSessionManager<T> drmSessionManager;
private final HandlerThread handlerThread;
/**
* Helper method to download a DASH manifest.
*
* @param dataSource The {@link HttpDataSource} from which the manifest should be read.
* @param manifestUriString The URI of the manifest to be read.
* @return An instance of {@link DashManifest}.
* @throws IOException If an error occurs reading data from the stream.
* @see DashManifestParser
*/
public static DashManifest downloadManifest(HttpDataSource dataSource, String manifestUriString)
throws IOException {
DataSourceInputStream inputStream = new DataSourceInputStream(
dataSource, new DataSpec(Uri.parse(manifestUriString)));
try {
inputStream.open();
DashManifestParser parser = new DashManifestParser();
return parser.parse(dataSource.getUri(), inputStream);
} finally {
inputStream.close();
}
}
/**
* Instantiates a new instance which uses Widevine CDM. Call {@link #releaseResources()} when
* you're done with the helper instance.
*
* @param licenseUrl The default license URL.
* @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
* @return A new instance which uses Widevine CDM.
* @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be
* instantiated.
*/
public static OfflineLicenseHelper<FrameworkMediaCrypto> newWidevineInstance(
String licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException {
return newWidevineInstance(
new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory, null), null);
}
/**
* Instantiates a new instance which uses Widevine CDM. Call {@link #releaseResources()} when
* you're done with the helper instance.
*
* @param callback Performs key and provisioning requests.
* @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
* to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
* @return A new instance which uses Widevine CDM.
* @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be
* instantiated.
* @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm,
* MediaDrmCallback, HashMap, Handler, EventListener)
*/
public static OfflineLicenseHelper<FrameworkMediaCrypto> newWidevineInstance(
MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters)
throws UnsupportedDrmException {
return new OfflineLicenseHelper<>(FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), callback,
optionalKeyRequestParameters);
}
/**
* Constructs an instance. Call {@link #releaseResources()} when you're done with it.
*
* @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager.
* @param callback Performs key and provisioning requests.
* @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
* to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
* @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm,
* MediaDrmCallback, HashMap, Handler, EventListener)
*/
public OfflineLicenseHelper(ExoMediaDrm<T> mediaDrm, MediaDrmCallback callback,
HashMap<String, String> optionalKeyRequestParameters) {
handlerThread = new HandlerThread("OfflineLicenseHelper");
handlerThread.start();
conditionVariable = new ConditionVariable();
EventListener eventListener = new EventListener() {
@Override
public void onDrmKeysLoaded() {
conditionVariable.open();
}
@Override
public void onDrmSessionManagerError(Exception e) {
conditionVariable.open();
}
@Override
public void onDrmKeysRestored() {
conditionVariable.open();
}
@Override
public void onDrmKeysRemoved() {
conditionVariable.open();
}
};
drmSessionManager = new DefaultDrmSessionManager<>(C.WIDEVINE_UUID, mediaDrm, callback,
optionalKeyRequestParameters, new Handler(handlerThread.getLooper()), eventListener);
}
/** Releases the used resources. */
public void releaseResources() {
handlerThread.quit();
}
/**
* Downloads an offline license.
*
* @param dataSource The {@link HttpDataSource} to be used for download.
* @param manifestUriString The URI of the manifest to be read.
* @return The downloaded offline license key set id.
* @throws IOException If an error occurs reading data from the stream.
* @throws InterruptedException If the thread has been interrupted.
* @throws DrmSessionException Thrown when there is an error during DRM session.
*/
public byte[] download(HttpDataSource dataSource, String manifestUriString)
throws IOException, InterruptedException, DrmSessionException {
return download(dataSource, downloadManifest(dataSource, manifestUriString));
}
/**
* Downloads an offline license.
*
* @param dataSource The {@link HttpDataSource} to be used for download.
* @param dashManifest The {@link DashManifest} of the DASH content.
* @return The downloaded offline license key set id.
* @throws IOException If an error occurs reading data from the stream.
* @throws InterruptedException If the thread has been interrupted.
* @throws DrmSessionException Thrown when there is an error during DRM session.
*/
public byte[] download(HttpDataSource dataSource, DashManifest dashManifest)
throws IOException, InterruptedException, DrmSessionException {
// Get DrmInitData
// Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream,
// as per DASH IF Interoperability Recommendations V3.0, 7.5.3.
if (dashManifest.getPeriodCount() < 1) {
return null;
}
Period period = dashManifest.getPeriod(0);
int adaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_VIDEO);
if (adaptationSetIndex == C.INDEX_UNSET) {
adaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_AUDIO);
if (adaptationSetIndex == C.INDEX_UNSET) {
return null;
}
}
AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex);
if (adaptationSet.representations.isEmpty()) {
return null;
}
Representation representation = adaptationSet.representations.get(0);
DrmInitData drmInitData = representation.format.drmInitData;
if (drmInitData == null) {
InitializationChunk initializationChunk = loadInitializationChunk(dataSource, representation);
if (initializationChunk == null) {
return null;
}
Format sampleFormat = initializationChunk.getSampleFormat();
if (sampleFormat != null) {
drmInitData = sampleFormat.drmInitData;
}
if (drmInitData == null) {
return null;
}
}
blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, null, drmInitData);
return drmSessionManager.getOfflineLicenseKeySetId();
}
/**
* Renews an offline license.
*
* @param offlineLicenseKeySetId The key set id of the license to be renewed.
* @return Renewed offline license key set id.
* @throws DrmSessionException Thrown when there is an error during DRM session.
*/
public byte[] renew(byte[] offlineLicenseKeySetId) throws DrmSessionException {
Assertions.checkNotNull(offlineLicenseKeySetId);
blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, offlineLicenseKeySetId, null);
return drmSessionManager.getOfflineLicenseKeySetId();
}
/**
* Releases an offline license.
*
* @param offlineLicenseKeySetId The key set id of the license to be released.
* @throws DrmSessionException Thrown when there is an error during DRM session.
*/
public void release(byte[] offlineLicenseKeySetId) throws DrmSessionException {
Assertions.checkNotNull(offlineLicenseKeySetId);
blockingKeyRequest(DefaultDrmSessionManager.MODE_RELEASE, offlineLicenseKeySetId, null);
}
/**
* Returns license and playback durations remaining in seconds of the given offline license.
*
* @param offlineLicenseKeySetId The key set id of the license.
*/
public Pair<Long, Long> getLicenseDurationRemainingSec(byte[] offlineLicenseKeySetId)
throws DrmSessionException {
Assertions.checkNotNull(offlineLicenseKeySetId);
DrmSession<T> session = openBlockingKeyRequest(DefaultDrmSessionManager.MODE_QUERY,
offlineLicenseKeySetId, null);
Pair<Long, Long> licenseDurationRemainingSec =
WidevineUtil.getLicenseDurationRemainingSec(drmSessionManager);
drmSessionManager.releaseSession(session);
return licenseDurationRemainingSec;
}
private void blockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId,
DrmInitData drmInitData) throws DrmSessionException {
DrmSession<T> session = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId,
drmInitData);
DrmSessionException error = session.getError();
if (error != null) {
throw error;
}
drmSessionManager.releaseSession(session);
}
private DrmSession<T> openBlockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId,
DrmInitData drmInitData) {
drmSessionManager.setMode(licenseMode, offlineLicenseKeySetId);
conditionVariable.close();
DrmSession<T> session = drmSessionManager.acquireSession(handlerThread.getLooper(),
drmInitData);
// Block current thread until key loading is finished
conditionVariable.block();
return session;
}
private static InitializationChunk loadInitializationChunk(final DataSource dataSource,
final Representation representation) throws IOException, InterruptedException {
RangedUri rangedUri = representation.getInitializationUri();
if (rangedUri == null) {
return null;
}
DataSpec dataSpec = new DataSpec(rangedUri.resolveUri(representation.baseUrl), rangedUri.start,
rangedUri.length, representation.getCacheKey());
InitializationChunk initializationChunk = new InitializationChunk(dataSource, dataSpec,
representation.format, C.SELECTION_REASON_UNKNOWN, null /* trackSelectionData */,
newWrappedExtractor(representation.format));
initializationChunk.load();
return initializationChunk;
}
private static ChunkExtractorWrapper newWrappedExtractor(final Format format) {
final String mimeType = format.containerMimeType;
final boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM)
|| mimeType.startsWith(MimeTypes.AUDIO_WEBM);
final Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor();
return new ChunkExtractorWrapper(extractor, format, false /* preferManifestDrmInitData */,
false /* resendFormatOnInit */);
}
}

View File

@ -0,0 +1,62 @@
/*
* 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;
}
}

View File

@ -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. * @param timeUs The seek time.
* @return Whether the skip was successful. * @return Whether the skip was successful.
*/ */
public boolean skipToKeyframeBefore(long timeUs) { 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) { if (nextOffset == C.POSITION_UNSET) {
return false; 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 formatHolder A {@link FormatHolder} to populate in the case of reading a format.
* @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
* end of the stream. If the end of the stream has been reached, the * end of the stream. If the end of the stream has been reached, the
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. * {@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 loadingFinished True if an empty queue should be considered the end of the stream.
* @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will * @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will
* be set if the buffer's timestamp is less than this value. * be set if the buffer's timestamp is less than this value.
@ -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 * 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 * 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 * 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 * @param downstreamFormat The current downstream {@link Format}. If the format of the next
* sample is different to the current downstream format then a format will be read. * sample is different to the current downstream format then a format will be read.
* @param extrasHolder The holder into which extra sample information should be written. * @param extrasHolder The holder into which extra sample information should be written.
@ -742,14 +763,14 @@ public final class DefaultTrackOutput implements TrackOutput {
public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
Format downstreamFormat, BufferExtrasHolder extrasHolder) { Format downstreamFormat, BufferExtrasHolder extrasHolder) {
if (queueSize == 0) { if (queueSize == 0) {
if (upstreamFormat != null && upstreamFormat != downstreamFormat) { if (upstreamFormat != null && (buffer == null || upstreamFormat != downstreamFormat)) {
formatHolder.format = upstreamFormat; formatHolder.format = upstreamFormat;
return C.RESULT_FORMAT_READ; return C.RESULT_FORMAT_READ;
} }
return C.RESULT_NOTHING_READ; return C.RESULT_NOTHING_READ;
} }
if (formats[relativeReadIndex] != downstreamFormat) { if (buffer == null || formats[relativeReadIndex] != downstreamFormat) {
formatHolder.format = formats[relativeReadIndex]; formatHolder.format = formats[relativeReadIndex];
return C.RESULT_FORMAT_READ; 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 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. * @return The offset of the keyframe's data if the keyframe was present.
* {@link C#POSITION_UNSET} otherwise. * {@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]) { if (queueSize == 0 || timeUs < timesUs[relativeReadIndex]) {
return C.POSITION_UNSET; return C.POSITION_UNSET;
} }
if (timeUs > largestQueuedTimestampUs) { if (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer) {
return C.POSITION_UNSET; return C.POSITION_UNSET;
} }

View File

@ -51,6 +51,34 @@ public final class TimestampAdjuster {
lastSampleTimestamp = C.TIME_UNSET; lastSampleTimestamp = C.TIME_UNSET;
} }
/**
* 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. * Resets the instance to its initial state.
*/ */

View File

@ -127,6 +127,7 @@ import java.util.List;
public static final int TYPE_mean = Util.getIntegerCodeForString("mean"); public static final int TYPE_mean = Util.getIntegerCodeForString("mean");
public static final int TYPE_name = Util.getIntegerCodeForString("name"); public static final int TYPE_name = Util.getIntegerCodeForString("name");
public static final int TYPE_data = Util.getIntegerCodeForString("data"); 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_st3d = Util.getIntegerCodeForString("st3d");
public static final int TYPE_sv3d = Util.getIntegerCodeForString("sv3d"); public static final int TYPE_sv3d = Util.getIntegerCodeForString("sv3d");
public static final int TYPE_proj = Util.getIntegerCodeForString("proj"); public static final int TYPE_proj = Util.getIntegerCodeForString("proj");

View File

@ -20,6 +20,7 @@ import android.util.Log;
import android.util.Pair; import android.util.Pair;
import android.util.SparseArray; import android.util.SparseArray;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
@ -44,6 +45,7 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Stack; import java.util.Stack;
import java.util.UUID; import java.util.UUID;
@ -73,7 +75,7 @@ public final class FragmentedMp4Extractor implements Extractor {
*/ */
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME, @IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME,
FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_SIDELOADED}) FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_SIDELOADED})
public @interface Flags {} public @interface Flags {}
/** /**
* Flag to work around an issue in some video streams where every frame is marked as a sync frame. * Flag to work around an issue in some video streams where every frame is marked as a sync frame.
@ -87,11 +89,16 @@ public final class FragmentedMp4Extractor implements Extractor {
* Flag to ignore any tfdt boxes in the stream. * Flag to ignore any tfdt boxes in the stream.
*/ */
public static final int FLAG_WORKAROUND_IGNORE_TFDT_BOX = 2; public static final int FLAG_WORKAROUND_IGNORE_TFDT_BOX = 2;
/**
* Flag to indicate that the extractor should output an event message metadata track. Any event
* messages in the stream will be delivered as samples to this track.
*/
public static final int FLAG_ENABLE_EMSG_TRACK = 4;
/** /**
* Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4 * Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4
* container. * container.
*/ */
private static final int FLAG_SIDELOADED = 4; private static final int FLAG_SIDELOADED = 8;
private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE = private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE =
new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12}; new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12};
@ -123,6 +130,7 @@ public final class FragmentedMp4Extractor implements Extractor {
private final ParsableByteArray atomHeader; private final ParsableByteArray atomHeader;
private final byte[] extendedTypeScratch; private final byte[] extendedTypeScratch;
private final Stack<ContainerAtom> containerAtoms; private final Stack<ContainerAtom> containerAtoms;
private final LinkedList<MetadataSampleInfo> pendingMetadataSampleInfos;
private int parserState; private int parserState;
private int atomType; private int atomType;
@ -130,8 +138,10 @@ public final class FragmentedMp4Extractor implements Extractor {
private int atomHeaderBytesRead; private int atomHeaderBytesRead;
private ParsableByteArray atomData; private ParsableByteArray atomData;
private long endOfMdatPosition; private long endOfMdatPosition;
private int pendingMetadataSampleBytes;
private long durationUs; private long durationUs;
private long segmentIndexEarliestPresentationTimeUs;
private TrackBundle currentTrackBundle; private TrackBundle currentTrackBundle;
private int sampleSize; private int sampleSize;
private int sampleBytesWritten; private int sampleBytesWritten;
@ -139,6 +149,7 @@ public final class FragmentedMp4Extractor implements Extractor {
// Extractor output. // Extractor output.
private ExtractorOutput extractorOutput; private ExtractorOutput extractorOutput;
private TrackOutput eventMessageTrackOutput;
// Whether extractorOutput.seekMap has been called. // Whether extractorOutput.seekMap has been called.
private boolean haveOutputSeekMap; private boolean haveOutputSeekMap;
@ -172,8 +183,10 @@ public final class FragmentedMp4Extractor implements Extractor {
encryptionSignalByte = new ParsableByteArray(1); encryptionSignalByte = new ParsableByteArray(1);
extendedTypeScratch = new byte[16]; extendedTypeScratch = new byte[16];
containerAtoms = new Stack<>(); containerAtoms = new Stack<>();
pendingMetadataSampleInfos = new LinkedList<>();
trackBundles = new SparseArray<>(); trackBundles = new SparseArray<>();
durationUs = C.TIME_UNSET; durationUs = C.TIME_UNSET;
segmentIndexEarliestPresentationTimeUs = C.TIME_UNSET;
enterReadingAtomHeaderState(); enterReadingAtomHeaderState();
} }
@ -189,6 +202,7 @@ public final class FragmentedMp4Extractor implements Extractor {
TrackBundle bundle = new TrackBundle(output.track(0)); TrackBundle bundle = new TrackBundle(output.track(0));
bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0)); bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0));
trackBundles.put(0, bundle); trackBundles.put(0, bundle);
maybeInitEventMessageTrack();
extractorOutput.endTracks(); extractorOutput.endTracks();
} }
} }
@ -199,6 +213,8 @@ public final class FragmentedMp4Extractor implements Extractor {
for (int i = 0; i < trackCount; i++) { for (int i = 0; i < trackCount; i++) {
trackBundles.valueAt(i).reset(); trackBundles.valueAt(i).reset();
} }
pendingMetadataSampleInfos.clear();
pendingMetadataSampleBytes = 0;
containerAtoms.clear(); containerAtoms.clear();
enterReadingAtomHeaderState(); enterReadingAtomHeaderState();
} }
@ -336,9 +352,12 @@ public final class FragmentedMp4Extractor implements Extractor {
if (!containerAtoms.isEmpty()) { if (!containerAtoms.isEmpty()) {
containerAtoms.peek().add(leaf); containerAtoms.peek().add(leaf);
} else if (leaf.type == Atom.TYPE_sidx) { } else if (leaf.type == Atom.TYPE_sidx) {
ChunkIndex segmentIndex = parseSidx(leaf.data, inputPosition); Pair<Long, ChunkIndex> result = parseSidx(leaf.data, inputPosition);
extractorOutput.seekMap(segmentIndex); segmentIndexEarliestPresentationTimeUs = result.first;
extractorOutput.seekMap(result.second);
haveOutputSeekMap = true; haveOutputSeekMap = true;
} else if (leaf.type == Atom.TYPE_emsg) {
onEmsgLeafAtomRead(leaf.data);
} }
} }
@ -394,6 +413,7 @@ public final class FragmentedMp4Extractor implements Extractor {
trackBundles.put(track.id, new TrackBundle(extractorOutput.track(i))); trackBundles.put(track.id, new TrackBundle(extractorOutput.track(i)));
durationUs = Math.max(durationUs, track.durationUs); durationUs = Math.max(durationUs, track.durationUs);
} }
maybeInitEventMessageTrack();
extractorOutput.endTracks(); extractorOutput.endTracks();
} else { } else {
Assertions.checkState(trackBundles.size() == trackCount); Assertions.checkState(trackBundles.size() == trackCount);
@ -417,6 +437,47 @@ public final class FragmentedMp4Extractor implements Extractor {
} }
} }
private void maybeInitEventMessageTrack() {
if ((flags & FLAG_ENABLE_EMSG_TRACK) == 0) {
return;
}
eventMessageTrackOutput = extractorOutput.track(trackBundles.size());
eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG,
Format.OFFSET_SAMPLE_RELATIVE));
}
/**
* Handles an emsg atom (defined in 23009-1).
*/
private void onEmsgLeafAtomRead(ParsableByteArray atom) {
if (eventMessageTrackOutput == null) {
return;
}
// Parse the event's presentation time delta.
atom.setPosition(Atom.FULL_HEADER_SIZE);
atom.readNullTerminatedString(); // schemeIdUri
atom.readNullTerminatedString(); // value
long timescale = atom.readUnsignedInt();
long presentationTimeDeltaUs =
Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale);
// Output the sample data.
atom.setPosition(Atom.FULL_HEADER_SIZE);
int sampleSize = atom.bytesLeft();
eventMessageTrackOutput.sampleData(atom, sampleSize);
// Output the sample metadata.
if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) {
// We can output the sample metadata immediately.
eventMessageTrackOutput.sampleMetadata(
segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs,
C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0 /* offset */, null);
} else {
// We need the first sample timestamp in the segment before we can output the metadata.
pendingMetadataSampleInfos.addLast(
new MetadataSampleInfo(presentationTimeDeltaUs, sampleSize));
pendingMetadataSampleBytes += sampleSize;
}
}
/** /**
* Parses a trex atom (defined in 14496-12). * Parses a trex atom (defined in 14496-12).
*/ */
@ -628,7 +689,7 @@ public final class FragmentedMp4Extractor implements Extractor {
DefaultSampleValues defaultSampleValues = trackBundle.defaultSampleValues; DefaultSampleValues defaultSampleValues = trackBundle.defaultSampleValues;
int defaultSampleDescriptionIndex = int defaultSampleDescriptionIndex =
((atomFlags & 0x02 /* default_sample_description_index_present */) != 0) ((atomFlags & 0x02 /* default_sample_description_index_present */) != 0)
? tfhd.readUnsignedIntToInt() - 1 : defaultSampleValues.sampleDescriptionIndex; ? tfhd.readUnsignedIntToInt() - 1 : defaultSampleValues.sampleDescriptionIndex;
int defaultSampleDuration = ((atomFlags & 0x08 /* default_sample_duration_present */) != 0) int defaultSampleDuration = ((atomFlags & 0x08 /* default_sample_duration_present */) != 0)
? tfhd.readUnsignedIntToInt() : defaultSampleValues.duration; ? tfhd.readUnsignedIntToInt() : defaultSampleValues.duration;
int defaultSampleSize = ((atomFlags & 0x10 /* default_sample_size_present */) != 0) int defaultSampleSize = ((atomFlags & 0x10 /* default_sample_size_present */) != 0)
@ -832,8 +893,13 @@ public final class FragmentedMp4Extractor implements Extractor {
/** /**
* Parses a sidx atom (defined in 14496-12). * Parses a sidx atom (defined in 14496-12).
*
* @param atom The atom data.
* @param inputPosition The input position of the first byte after the atom.
* @return A pair consisting of the earliest presentation time in microseconds, and the parsed
* {@link ChunkIndex}.
*/ */
private static ChunkIndex parseSidx(ParsableByteArray atom, long inputPosition) private static Pair<Long, ChunkIndex> parseSidx(ParsableByteArray atom, long inputPosition)
throws ParserException { throws ParserException {
atom.setPosition(Atom.HEADER_SIZE); atom.setPosition(Atom.HEADER_SIZE);
int fullAtom = atom.readInt(); int fullAtom = atom.readInt();
@ -850,6 +916,8 @@ public final class FragmentedMp4Extractor implements Extractor {
earliestPresentationTime = atom.readUnsignedLongToLong(); earliestPresentationTime = atom.readUnsignedLongToLong();
offset += atom.readUnsignedLongToLong(); offset += atom.readUnsignedLongToLong();
} }
long earliestPresentationTimeUs = Util.scaleLargeTimestamp(earliestPresentationTime,
C.MICROS_PER_SECOND, timescale);
atom.skipBytes(2); atom.skipBytes(2);
@ -860,7 +928,7 @@ public final class FragmentedMp4Extractor implements Extractor {
long[] timesUs = new long[referenceCount]; long[] timesUs = new long[referenceCount];
long time = earliestPresentationTime; long time = earliestPresentationTime;
long timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale); long timeUs = earliestPresentationTimeUs;
for (int i = 0; i < referenceCount; i++) { for (int i = 0; i < referenceCount; i++) {
int firstInt = atom.readInt(); int firstInt = atom.readInt();
@ -884,7 +952,8 @@ public final class FragmentedMp4Extractor implements Extractor {
offset += sizes[i]; offset += sizes[i];
} }
return new ChunkIndex(sizes, offsets, durationsUs, timesUs); return Pair.create(earliestPresentationTimeUs,
new ChunkIndex(sizes, offsets, durationsUs, timesUs));
} }
private void readEncryptionData(ExtractorInput input) throws IOException, InterruptedException { private void readEncryptionData(ExtractorInput input) throws IOException, InterruptedException {
@ -946,13 +1015,9 @@ public final class FragmentedMp4Extractor implements Extractor {
// We skip bytes preceding the next sample to read. // We skip bytes preceding the next sample to read.
int bytesToSkip = (int) (nextDataPosition - input.getPosition()); int bytesToSkip = (int) (nextDataPosition - input.getPosition());
if (bytesToSkip < 0) { if (bytesToSkip < 0) {
if (nextDataPosition == currentTrackBundle.fragment.atomPosition) { // Assume the sample data must be contiguous in the mdat with no preceding data.
// Assume the sample data must be contiguous in the mdat with no preceeding data. Log.w(TAG, "Ignoring negative offset to sample data.");
Log.w(TAG, "Offset to sample data was missing."); bytesToSkip = 0;
bytesToSkip = 0;
} else {
throw new ParserException("Offset to sample data was negative.");
}
} }
input.skipFully(bytesToSkip); input.skipFully(bytesToSkip);
this.currentTrackBundle = currentTrackBundle; this.currentTrackBundle = currentTrackBundle;
@ -1029,6 +1094,14 @@ public final class FragmentedMp4Extractor implements Extractor {
} }
output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, encryptionKey); output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, encryptionKey);
while (!pendingMetadataSampleInfos.isEmpty()) {
MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst();
pendingMetadataSampleBytes -= sampleInfo.size;
eventMessageTrackOutput.sampleMetadata(
sampleTimeUs + sampleInfo.presentationTimeDeltaUs,
C.BUFFER_FLAG_KEY_FRAME, sampleInfo.size, pendingMetadataSampleBytes, null);
}
currentTrackBundle.currentSampleIndex++; currentTrackBundle.currentSampleIndex++;
currentTrackBundle.currentSampleInTrackRun++; currentTrackBundle.currentSampleInTrackRun++;
if (currentTrackBundle.currentSampleInTrackRun if (currentTrackBundle.currentSampleInTrackRun
@ -1134,7 +1207,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|| atom == Atom.TYPE_trun || atom == Atom.TYPE_pssh || atom == Atom.TYPE_saiz || atom == Atom.TYPE_trun || atom == Atom.TYPE_pssh || atom == Atom.TYPE_saiz
|| atom == Atom.TYPE_saio || atom == Atom.TYPE_senc || atom == Atom.TYPE_uuid || atom == Atom.TYPE_saio || atom == Atom.TYPE_senc || atom == Atom.TYPE_uuid
|| atom == Atom.TYPE_sbgp || atom == Atom.TYPE_sgpd || atom == Atom.TYPE_elst || atom == Atom.TYPE_sbgp || atom == Atom.TYPE_sgpd || atom == Atom.TYPE_elst
|| atom == Atom.TYPE_mehd; || atom == Atom.TYPE_mehd || atom == Atom.TYPE_emsg;
} }
/** Returns whether the extractor should decode a container atom with type {@code atom}. */ /** Returns whether the extractor should decode a container atom with type {@code atom}. */
@ -1144,6 +1217,21 @@ public final class FragmentedMp4Extractor implements Extractor {
|| atom == Atom.TYPE_traf || atom == Atom.TYPE_mvex || atom == Atom.TYPE_edts; || atom == Atom.TYPE_traf || atom == Atom.TYPE_mvex || atom == Atom.TYPE_edts;
} }
/**
* Holds data corresponding to a metadata sample.
*/
private static final class MetadataSampleInfo {
public final long presentationTimeDeltaUs;
public final int size;
public MetadataSampleInfo(long presentationTimeDeltaUs, int size) {
this.presentationTimeDeltaUs = presentationTimeDeltaUs;
this.size = size;
}
}
/** /**
* Holds data corresponding to a single track. * Holds data corresponding to a single track.
*/ */

View File

@ -188,7 +188,7 @@ import com.google.android.exoplayer2.util.Util;
if (atomType == Atom.TYPE_data) { if (atomType == Atom.TYPE_data) {
data.skipBytes(8); // version (1), flags (3), empty (4) data.skipBytes(8); // version (1), flags (3), empty (4)
String value = data.readNullTerminatedString(atomSize - 16); 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)); Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type));
return null; return null;
@ -213,7 +213,7 @@ import com.google.android.exoplayer2.util.Util;
value = Math.min(1, value); value = Math.min(1, value);
} }
if (value >= 0) { 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)); : new CommentFrame(LANGUAGE_UNDEFINED, id, Integer.toString(value));
} }
Log.w(TAG, "Failed to parse uint8 attribute: " + Atom.getAtomTypeString(type)); 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) data.skipBytes(10); // version (1), flags (3), empty (4), empty (2)
int index = data.readUnsignedShort(); int index = data.readUnsignedShort();
if (index > 0) { if (index > 0) {
String description = "" + index; String value = "" + index;
int count = data.readUnsignedShort(); int count = data.readUnsignedShort();
if (count > 0) { 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)); 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) String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length)
? STANDARD_GENRES[genreCode - 1] : null; ? STANDARD_GENRES[genreCode - 1] : null;
if (genreString != null) { if (genreString != null) {
return new TextInformationFrame("TCON", genreString); return new TextInformationFrame("TCON", null, genreString);
} }
Log.w(TAG, "Failed to parse standard genre code"); Log.w(TAG, "Failed to parse standard genre code");
return null; return null;

View File

@ -83,8 +83,11 @@ public final class RawCcExtractor implements Extractor {
while (true) { while (true) {
switch (parserState) { switch (parserState) {
case STATE_READING_HEADER: case STATE_READING_HEADER:
parseHeader(input); if (parseHeader(input)) {
parserState = STATE_READING_TIMESTAMP_AND_COUNT; parserState = STATE_READING_TIMESTAMP_AND_COUNT;
} else {
return RESULT_END_OF_INPUT;
}
break; break;
case STATE_READING_TIMESTAMP_AND_COUNT: case STATE_READING_TIMESTAMP_AND_COUNT:
if (parseTimestampAndSampleCount(input)) { if (parseTimestampAndSampleCount(input)) {
@ -114,14 +117,18 @@ public final class RawCcExtractor implements Extractor {
// Do nothing // Do nothing
} }
private void parseHeader(ExtractorInput input) throws IOException, InterruptedException { private boolean parseHeader(ExtractorInput input) throws IOException, InterruptedException {
dataScratch.reset(); dataScratch.reset();
input.readFully(dataScratch.data, 0, HEADER_SIZE); if (input.readFully(dataScratch.data, 0, HEADER_SIZE, true)) {
if (dataScratch.readInt() != HEADER_ID) { if (dataScratch.readInt() != HEADER_ID) {
throw new IOException("Input not RawCC"); 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, private boolean parseTimestampAndSampleCount(ExtractorInput input) throws IOException,

View File

@ -28,11 +28,14 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
*/ */
public final class SpliceInfoSectionReader implements SectionPayloadReader { public final class SpliceInfoSectionReader implements SectionPayloadReader {
private TimestampAdjuster timestampAdjuster;
private TrackOutput output; private TrackOutput output;
private boolean formatDeclared;
@Override @Override
public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput, public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
TsPayloadReader.TrackIdGenerator idGenerator) { TsPayloadReader.TrackIdGenerator idGenerator) {
this.timestampAdjuster = timestampAdjuster;
output = extractorOutput.track(idGenerator.getNextId()); output = extractorOutput.track(idGenerator.getNextId());
output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35, null, output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35, null,
Format.NO_VALUE, null)); Format.NO_VALUE, null));
@ -40,9 +43,19 @@ public final class SpliceInfoSectionReader implements SectionPayloadReader {
@Override @Override
public void consume(ParsableByteArray sectionData) { 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(); int sampleSize = sectionData.bytesLeft();
output.sampleData(sectionData, sampleSize); 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);
} }
} }

View File

@ -270,7 +270,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
*/ */
protected MediaCodecInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector, protected MediaCodecInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector,
Format format, boolean requiresSecureDecoder) throws DecoderQueryException { Format format, boolean requiresSecureDecoder) throws DecoderQueryException {
return mediaCodecSelector.getDecoderInfo(format.sampleMimeType, requiresSecureDecoder, false); return mediaCodecSelector.getDecoderInfo(format.sampleMimeType, requiresSecureDecoder);
} }
/** /**

View File

@ -29,9 +29,9 @@ public interface MediaCodecSelector {
MediaCodecSelector DEFAULT = new MediaCodecSelector() { MediaCodecSelector DEFAULT = new MediaCodecSelector() {
@Override @Override
public MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder, public MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder)
boolean requiresTunneling) throws DecoderQueryException { throws DecoderQueryException {
return MediaCodecUtil.getDecoderInfo(mimeType, requiresSecureDecoder, requiresTunneling); return MediaCodecUtil.getDecoderInfo(mimeType, requiresSecureDecoder);
} }
@Override @Override
@ -46,13 +46,11 @@ public interface MediaCodecSelector {
* *
* @param mimeType The mime type for which a decoder is required. * @param mimeType The mime type for which a decoder is required.
* @param requiresSecureDecoder Whether a secure 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. * @throws DecoderQueryException Thrown if there was an error querying decoders.
*/ */
MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder, MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder)
boolean requiresTunneling) throws DecoderQueryException; throws DecoderQueryException;
/** /**
* Selects a decoder to instantiate for audio passthrough. * Selects a decoder to instantiate for audio passthrough.

View File

@ -81,9 +81,8 @@ public final class MediaCodecUtil {
/** /**
* Optional call to warm the codec cache for a given mime type. * Optional call to warm the codec cache for a given mime type.
* <p> * <p>
* Calling this method may speed up subsequent calls to * Calling this method may speed up subsequent calls to {@link #getDecoderInfo(String, boolean)}
* {@link #getDecoderInfo(String, boolean, boolean)} and * and {@link #getDecoderInfos(String, boolean)}.
* {@link #getDecoderInfos(String, boolean)}.
* *
* @param mimeType The mime type. * @param mimeType The mime type.
* @param secure Whether the decoder is required to support secure decryption. Always pass false * @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 mimeType The mime type.
* @param secure Whether the decoder is required to support secure decryption. Always pass false * @param secure Whether the decoder is required to support secure decryption. Always pass false
* unless secure decryption really is required. * 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 * @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder
* exists. * exists.
* @throws DecoderQueryException If there was an error querying the available decoders. * @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 { throws DecoderQueryException {
List<MediaCodecInfo> decoderInfos = getDecoderInfos(mimeType, secure); List<MediaCodecInfo> decoderInfos = getDecoderInfos(mimeType, secure);
if (tunneling) { return decoderInfos.isEmpty() ? null : decoderInfos.get(0);
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);
}
} }
/** /**
@ -305,7 +292,7 @@ public final class MediaCodecUtil {
public static int maxH264DecodableFrameSize() throws DecoderQueryException { public static int maxH264DecodableFrameSize() throws DecoderQueryException {
if (maxH264DecodableFrameSize == -1) { if (maxH264DecodableFrameSize == -1) {
int result = 0; int result = 0;
MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false, false); MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false);
if (decoderInfo != null) { if (decoderInfo != null) {
for (CodecProfileLevel profileLevel : decoderInfo.getProfileLevels()) { for (CodecProfileLevel profileLevel : decoderInfo.getProfileLevels()) {
result = Math.max(avcLevelToMaxFrameSize(profileLevel.level), result); result = Math.max(avcLevelToMaxFrameSize(profileLevel.level), result);

View File

@ -21,21 +21,12 @@ package com.google.android.exoplayer2.metadata;
public interface MetadataDecoder { 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. * @param inputBuffer The input buffer to decode.
* @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.
* @return The decoded metadata object. * @return The decoded metadata object.
* @throws MetadataDecoderException If a problem occurred decoding the data. * @throws MetadataDecoderException If a problem occurred decoding the data.
*/ */
Metadata decode(byte[] data, int size) throws MetadataDecoderException; Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException;
} }

View File

@ -0,0 +1,99 @@
/*
* 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;
}
}
};
}

View File

@ -0,0 +1,36 @@
/*
* 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);
}
}

View File

@ -24,9 +24,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.nio.ByteBuffer;
/** /**
* A renderer for metadata. * 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 static final int MSG_INVOKE_RENDERER = 0;
private final MetadataDecoder metadataDecoder; private final MetadataDecoderFactory decoderFactory;
private final Output output; private final Output output;
private final Handler outputHandler; private final Handler outputHandler;
private final FormatHolder formatHolder; private final FormatHolder formatHolder;
private final DecoderInputBuffer buffer; private final MetadataInputBuffer buffer;
private MetadataDecoder decoder;
private boolean inputStreamEnded; private boolean inputStreamEnded;
private long pendingMetadataTimestamp; private long pendingMetadataTimestamp;
private Metadata pendingMetadata; 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 * 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 * {@link android.app.Activity#getMainLooper()}. Null may be passed if the output should be
* called directly on the player's internal rendering thread. * 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); super(C.TRACK_TYPE_METADATA);
this.output = Assertions.checkNotNull(output); this.output = Assertions.checkNotNull(output);
this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this); this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this);
this.metadataDecoder = Assertions.checkNotNull(metadataDecoder); this.decoderFactory = Assertions.checkNotNull(decoderFactory);
formatHolder = new FormatHolder(); formatHolder = new FormatHolder();
buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL); buffer = new MetadataInputBuffer();
} }
@Override @Override
public int supportsFormat(Format format) { public int supportsFormat(Format format) {
return metadataDecoder.canDecode(format.sampleMimeType) ? FORMAT_HANDLED return decoderFactory.supportsFormat(format) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE;
: FORMAT_UNSUPPORTED_TYPE; }
@Override
protected void onStreamChanged(Format[] formats) throws ExoPlaybackException {
decoder = decoderFactory.createDecoder(formats[0]);
} }
@Override @Override
@ -97,12 +113,16 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
if (result == C.RESULT_BUFFER_READ) { if (result == C.RESULT_BUFFER_READ) {
if (buffer.isEndOfStream()) { if (buffer.isEndOfStream()) {
inputStreamEnded = true; 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 { } else {
pendingMetadataTimestamp = buffer.timeUs; pendingMetadataTimestamp = buffer.timeUs;
buffer.subsampleOffsetUs = formatHolder.format.subsampleOffsetUs;
buffer.flip();
try { try {
buffer.flip(); pendingMetadata = decoder.decode(buffer);
ByteBuffer bufferData = buffer.data;
pendingMetadata = metadataDecoder.decode(bufferData.array(), bufferData.limit());
} catch (MetadataDecoderException e) { } catch (MetadataDecoderException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex()); throw ExoPlaybackException.createForRenderer(e, getIndex());
} }
@ -119,6 +139,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
@Override @Override
protected void onDisabled() { protected void onDisabled() {
pendingMetadata = null; pendingMetadata = null;
decoder = null;
super.onDisabled(); super.onDisabled();
} }

View File

@ -0,0 +1,142 @@
/*
* 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];
}
};
}

View File

@ -0,0 +1,49 @@
/*
* 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));
}
}

View File

@ -0,0 +1,142 @@
/*
* 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];
}
};
}

View File

@ -0,0 +1,123 @@
/*
* 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];
}
};
}

View File

@ -16,12 +16,14 @@
package com.google.android.exoplayer2.metadata.id3; package com.google.android.exoplayer2.metadata.id3;
import android.util.Log; import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataDecoder;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -49,11 +51,18 @@ public final class Id3Decoder implements MetadataDecoder {
private static final int ID3_TEXT_ENCODING_UTF_8 = 3; private static final int ID3_TEXT_ENCODING_UTF_8 = 3;
@Override @Override
public boolean canDecode(String mimeType) { public Metadata decode(MetadataInputBuffer inputBuffer) {
return mimeType.equals(MimeTypes.APPLICATION_ID3); ByteBuffer buffer = inputBuffer.data;
return decode(buffer.array(), buffer.limit());
} }
@Override /**
* Decodes ID3 tags.
*
* @param data The bytes to decode ID3 tags from.
* @param size Amount of bytes in {@code data} to read.
* @return A {@link Metadata} object containing the decoded ID3 tags.
*/
public Metadata decode(byte[] data, int size) { public Metadata decode(byte[] data, int size) {
List<Id3Frame> id3Frames = new ArrayList<>(); List<Id3Frame> id3Frames = new ArrayList<>();
ParsableByteArray id3Data = new ParsableByteArray(data, size); ParsableByteArray id3Data = new ParsableByteArray(data, size);
@ -84,7 +93,8 @@ public final class Id3Decoder implements MetadataDecoder {
int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10; int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10;
while (id3Data.bytesLeft() >= frameHeaderSize) { while (id3Data.bytesLeft() >= frameHeaderSize) {
Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack); Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack,
frameHeaderSize);
if (frame != null) { if (frame != null) {
id3Frames.add(frame); id3Frames.add(frame);
} }
@ -190,7 +200,7 @@ public final class Id3Decoder implements MetadataDecoder {
} }
private static Id3Frame decodeFrame(int majorVersion, ParsableByteArray id3Data, private static Id3Frame decodeFrame(int majorVersion, ParsableByteArray id3Data,
boolean unsignedIntFrameSizeHack) { boolean unsignedIntFrameSizeHack, int frameHeaderSize) {
int frameId0 = id3Data.readUnsignedByte(); int frameId0 = id3Data.readUnsignedByte();
int frameId1 = id3Data.readUnsignedByte(); int frameId1 = id3Data.readUnsignedByte();
int frameId2 = id3Data.readUnsignedByte(); int frameId2 = id3Data.readUnsignedByte();
@ -266,6 +276,19 @@ public final class Id3Decoder implements MetadataDecoder {
if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X' if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X'
&& (majorVersion == 2 || frameId3 == 'X')) { && (majorVersion == 2 || frameId3 == 'X')) {
frame = decodeTxxxFrame(id3Data, frameSize); frame = decodeTxxxFrame(id3Data, frameSize);
} else if (frameId0 == 'T') {
String id = majorVersion == 2
? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2)
: String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
frame = decodeTextInformationFrame(id3Data, frameSize, id);
} else if (frameId0 == 'W' && frameId1 == 'X' && frameId2 == 'X'
&& (majorVersion == 2 || frameId3 == 'X')) {
frame = decodeWxxxFrame(id3Data, frameSize);
} else if (frameId0 == 'W') {
String id = majorVersion == 2
? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2)
: String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
frame = decodeUrlLinkFrame(id3Data, frameSize, id);
} else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') { } else if (frameId0 == 'P' && frameId1 == 'R' && frameId2 == 'I' && frameId3 == 'V') {
frame = decodePrivFrame(id3Data, frameSize); frame = decodePrivFrame(id3Data, frameSize);
} else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O' } else if (frameId0 == 'G' && frameId1 == 'E' && frameId2 == 'O'
@ -274,14 +297,15 @@ public final class Id3Decoder implements MetadataDecoder {
} else if (majorVersion == 2 ? (frameId0 == 'P' && frameId1 == 'I' && frameId2 == 'C') } else if (majorVersion == 2 ? (frameId0 == 'P' && frameId1 == 'I' && frameId2 == 'C')
: (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C')) { : (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C')) {
frame = decodeApicFrame(id3Data, frameSize, majorVersion); frame = decodeApicFrame(id3Data, frameSize, majorVersion);
} else if (frameId0 == 'T') {
String id = majorVersion == 2
? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2)
: String.format(Locale.US, "%c%c%c%c", frameId0, frameId1, frameId2, frameId3);
frame = decodeTextInformationFrame(id3Data, frameSize, id);
} else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M' } else if (frameId0 == 'C' && frameId1 == 'O' && frameId2 == 'M'
&& (frameId3 == 'M' || majorVersion == 2)) { && (frameId3 == 'M' || majorVersion == 2)) {
frame = decodeCommentFrame(id3Data, frameSize); frame = decodeCommentFrame(id3Data, frameSize);
} else if (frameId0 == 'C' && frameId1 == 'H' && frameId2 == 'A' && frameId3 == 'P') {
frame = decodeChapterFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack,
frameHeaderSize);
} else if (frameId0 == 'C' && frameId1 == 'T' && frameId2 == 'O' && frameId3 == 'C') {
frame = decodeChapterTOCFrame(id3Data, frameSize, majorVersion, unsignedIntFrameSizeHack,
frameHeaderSize);
} else { } else {
String id = majorVersion == 2 String id = majorVersion == 2
? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2) ? String.format(Locale.US, "%c%c%c", frameId0, frameId1, frameId2)
@ -297,7 +321,7 @@ public final class Id3Decoder implements MetadataDecoder {
} }
} }
private static TxxxFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize) private static TextInformationFrame decodeTxxxFrame(ParsableByteArray id3Data, int frameSize)
throws UnsupportedEncodingException { throws UnsupportedEncodingException {
int encoding = id3Data.readUnsignedByte(); int encoding = id3Data.readUnsignedByte();
String charset = getCharsetName(encoding); String charset = getCharsetName(encoding);
@ -308,11 +332,74 @@ public final class Id3Decoder implements MetadataDecoder {
int descriptionEndIndex = indexOfEos(data, 0, encoding); int descriptionEndIndex = indexOfEos(data, 0, encoding);
String description = new String(data, 0, descriptionEndIndex, charset); String description = new String(data, 0, descriptionEndIndex, charset);
String value;
int valueStartIndex = descriptionEndIndex + delimiterLength(encoding); int valueStartIndex = descriptionEndIndex + delimiterLength(encoding);
int valueEndIndex = indexOfEos(data, valueStartIndex, encoding); if (valueStartIndex < data.length) {
String value = new String(data, valueStartIndex, valueEndIndex - valueStartIndex, charset); int valueEndIndex = indexOfEos(data, valueStartIndex, encoding);
value = new String(data, valueStartIndex, valueEndIndex - valueStartIndex, charset);
} else {
value = "";
}
return new TxxxFrame(description, value); return new TextInformationFrame("TXXX", description, value);
}
private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data,
int frameSize, String id) throws UnsupportedEncodingException {
if (frameSize <= 1) {
// Frame is empty or contains only the text encoding byte.
return new TextInformationFrame(id, null, "");
}
int encoding = id3Data.readUnsignedByte();
String charset = getCharsetName(encoding);
byte[] data = new byte[frameSize - 1];
id3Data.readBytes(data, 0, frameSize - 1);
int valueEndIndex = indexOfEos(data, 0, encoding);
String value = new String(data, 0, valueEndIndex, charset);
return new TextInformationFrame(id, null, value);
}
private static UrlLinkFrame decodeWxxxFrame(ParsableByteArray id3Data, int frameSize)
throws UnsupportedEncodingException {
int encoding = id3Data.readUnsignedByte();
String charset = getCharsetName(encoding);
byte[] data = new byte[frameSize - 1];
id3Data.readBytes(data, 0, frameSize - 1);
int descriptionEndIndex = indexOfEos(data, 0, encoding);
String description = new String(data, 0, descriptionEndIndex, charset);
String url;
int urlStartIndex = descriptionEndIndex + delimiterLength(encoding);
if (urlStartIndex < data.length) {
int urlEndIndex = indexOfZeroByte(data, urlStartIndex);
url = new String(data, urlStartIndex, urlEndIndex - urlStartIndex, "ISO-8859-1");
} else {
url = "";
}
return new UrlLinkFrame("WXXX", description, url);
}
private static UrlLinkFrame decodeUrlLinkFrame(ParsableByteArray id3Data, int frameSize,
String id) throws UnsupportedEncodingException {
if (frameSize == 0) {
// Frame is empty.
return new UrlLinkFrame(id, null, "");
}
byte[] data = new byte[frameSize];
id3Data.readBytes(data, 0, frameSize);
int urlEndIndex = indexOfZeroByte(data, 0);
String url = new String(data, 0, urlEndIndex, "ISO-8859-1");
return new UrlLinkFrame(id, null, url);
} }
private static PrivFrame decodePrivFrame(ParsableByteArray id3Data, int frameSize) private static PrivFrame decodePrivFrame(ParsableByteArray id3Data, int frameSize)
@ -408,25 +495,88 @@ public final class Id3Decoder implements MetadataDecoder {
int descriptionEndIndex = indexOfEos(data, 0, encoding); int descriptionEndIndex = indexOfEos(data, 0, encoding);
String description = new String(data, 0, descriptionEndIndex, charset); String description = new String(data, 0, descriptionEndIndex, charset);
String text;
int textStartIndex = descriptionEndIndex + delimiterLength(encoding); int textStartIndex = descriptionEndIndex + delimiterLength(encoding);
int textEndIndex = indexOfEos(data, textStartIndex, encoding); if (textStartIndex < data.length) {
String text = new String(data, textStartIndex, textEndIndex - textStartIndex, charset); int textEndIndex = indexOfEos(data, textStartIndex, encoding);
text = new String(data, textStartIndex, textEndIndex - textStartIndex, charset);
} else {
text = "";
}
return new CommentFrame(language, description, text); return new CommentFrame(language, description, text);
} }
private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data, private static ChapterFrame decodeChapterFrame(ParsableByteArray id3Data, int frameSize,
int frameSize, String id) throws UnsupportedEncodingException { int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize)
int encoding = id3Data.readUnsignedByte(); throws UnsupportedEncodingException {
String charset = getCharsetName(encoding); int framePosition = id3Data.getPosition();
int chapterIdEndIndex = indexOfZeroByte(id3Data.data, framePosition);
String chapterId = new String(id3Data.data, framePosition, chapterIdEndIndex - framePosition,
"ISO-8859-1");
id3Data.setPosition(chapterIdEndIndex + 1);
byte[] data = new byte[frameSize - 1]; int startTime = id3Data.readInt();
id3Data.readBytes(data, 0, frameSize - 1); int endTime = id3Data.readInt();
long startOffset = id3Data.readUnsignedInt();
if (startOffset == 0xFFFFFFFFL) {
startOffset = C.POSITION_UNSET;
}
long endOffset = id3Data.readUnsignedInt();
if (endOffset == 0xFFFFFFFFL) {
endOffset = C.POSITION_UNSET;
}
int descriptionEndIndex = indexOfEos(data, 0, encoding); ArrayList<Id3Frame> subFrames = new ArrayList<>();
String description = new String(data, 0, descriptionEndIndex, charset); int limit = framePosition + frameSize;
while (id3Data.getPosition() < limit) {
Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack,
frameHeaderSize);
if (frame != null) {
subFrames.add(frame);
}
}
return new TextInformationFrame(id, description); Id3Frame[] subFrameArray = new Id3Frame[subFrames.size()];
subFrames.toArray(subFrameArray);
return new ChapterFrame(chapterId, startTime, endTime, startOffset, endOffset, subFrameArray);
}
private static ChapterTocFrame decodeChapterTOCFrame(ParsableByteArray id3Data, int frameSize,
int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize)
throws UnsupportedEncodingException {
int framePosition = id3Data.getPosition();
int elementIdEndIndex = indexOfZeroByte(id3Data.data, framePosition);
String elementId = new String(id3Data.data, framePosition, elementIdEndIndex - framePosition,
"ISO-8859-1");
id3Data.setPosition(elementIdEndIndex + 1);
int ctocFlags = id3Data.readUnsignedByte();
boolean isRoot = (ctocFlags & 0x0002) != 0;
boolean isOrdered = (ctocFlags & 0x0001) != 0;
int childCount = id3Data.readUnsignedByte();
String[] children = new String[childCount];
for (int i = 0; i < childCount; i++) {
int startIndex = id3Data.getPosition();
int endIndex = indexOfZeroByte(id3Data.data, startIndex);
children[i] = new String(id3Data.data, startIndex, endIndex - startIndex, "ISO-8859-1");
id3Data.setPosition(endIndex + 1);
}
ArrayList<Id3Frame> subFrames = new ArrayList<>();
int limit = framePosition + frameSize;
while (id3Data.getPosition() < limit) {
Id3Frame frame = decodeFrame(majorVersion, id3Data, unsignedIntFrameSizeHack,
frameHeaderSize);
if (frame != null) {
subFrames.add(frame);
}
}
Id3Frame[] subFrameArray = new Id3Frame[subFrames.size()];
subFrames.toArray(subFrameArray);
return new ChapterTocFrame(elementId, isRoot, isOrdered, children, subFrameArray);
} }
private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize, private static BinaryFrame decodeBinaryFrame(ParsableByteArray id3Data, int frameSize,
@ -458,6 +608,7 @@ public final class Id3Decoder implements MetadataDecoder {
/** /**
* Maps encoding byte from ID3v2 frame to a Charset. * Maps encoding byte from ID3v2 frame to a Charset.
*
* @param encodingByte The value of encoding byte from ID3v2 frame. * @param encodingByte The value of encoding byte from ID3v2 frame.
* @return Charset name. * @return Charset name.
*/ */

View File

@ -20,20 +20,23 @@ import android.os.Parcelable;
import com.google.android.exoplayer2.util.Util; 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 class TextInformationFrame extends Id3Frame {
public final String description; public final String description;
public final String value;
public TextInformationFrame(String id, String description) { public TextInformationFrame(String id, String description, String value) {
super(id); super(id);
this.description = description; this.description = description;
this.value = value;
} }
/* package */ TextInformationFrame(Parcel in) { /* package */ TextInformationFrame(Parcel in) {
super(in.readString()); super(in.readString());
description = in.readString(); description = in.readString();
value = in.readString();
} }
@Override @Override
@ -45,7 +48,8 @@ public final class TextInformationFrame extends Id3Frame {
return false; return false;
} }
TextInformationFrame other = (TextInformationFrame) obj; 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 @Override
@ -53,6 +57,7 @@ public final class TextInformationFrame extends Id3Frame {
int result = 17; int result = 17;
result = 31 * result + id.hashCode(); result = 31 * result + id.hashCode();
result = 31 * result + (description != null ? description.hashCode() : 0); result = 31 * result + (description != null ? description.hashCode() : 0);
result = 31 * result + (value != null ? value.hashCode() : 0);
return result; return result;
} }
@ -60,6 +65,7 @@ public final class TextInformationFrame extends Id3Frame {
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id); dest.writeString(id);
dest.writeString(description); dest.writeString(description);
dest.writeString(value);
} }
public static final Parcelable.Creator<TextInformationFrame> CREATOR = public static final Parcelable.Creator<TextInformationFrame> CREATOR =

View File

@ -1,5 +1,5 @@
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; 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 final class UrlLinkFrame extends Id3Frame {
public static final String ID = "TXXX";
public final String description; public final String description;
public final String value; public final String url;
public TxxxFrame(String description, String value) { public UrlLinkFrame(String id, String description, String url) {
super(ID); super(id);
this.description = description; this.description = description;
this.value = value; this.url = url;
} }
/* package */ TxxxFrame(Parcel in) { /* package */ UrlLinkFrame(Parcel in) {
super(ID); super(in.readString());
description = in.readString(); description = in.readString();
value = in.readString(); url = in.readString();
} }
@Override @Override
@ -49,36 +47,40 @@ public final class TxxxFrame extends Id3Frame {
if (obj == null || getClass() != obj.getClass()) { if (obj == null || getClass() != obj.getClass()) {
return false; return false;
} }
TxxxFrame other = (TxxxFrame) obj; UrlLinkFrame other = (UrlLinkFrame) obj;
return Util.areEqual(description, other.description) && Util.areEqual(value, other.value); return id.equals(other.id) && Util.areEqual(description, other.description)
&& Util.areEqual(url, other.url);
} }
@Override @Override
public int hashCode() { public int hashCode() {
int result = 17; int result = 17;
result = 31 * result + id.hashCode();
result = 31 * result + (description != null ? description.hashCode() : 0); 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; return result;
} }
@Override @Override
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(Parcel dest, int flags) {
dest.writeString(id);
dest.writeString(description); 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 @Override
public TxxxFrame createFromParcel(Parcel in) { public UrlLinkFrame createFromParcel(Parcel in) {
return new TxxxFrame(in); return new UrlLinkFrame(in);
} }
@Override @Override
public TxxxFrame[] newArray(int size) { public UrlLinkFrame[] newArray(int size) {
return new TxxxFrame[size]; return new UrlLinkFrame[size];
} }
}; };
} }

View File

@ -15,13 +15,13 @@
*/ */
package com.google.android.exoplayer2.metadata.scte35; package com.google.android.exoplayer2.metadata.scte35;
import android.text.TextUtils;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoder; import com.google.android.exoplayer2.metadata.MetadataDecoder;
import com.google.android.exoplayer2.metadata.MetadataDecoderException; 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.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import java.nio.ByteBuffer;
/** /**
* Decodes splice info sections and produces splice commands. * Decodes splice info sections and produces splice commands.
@ -43,12 +43,10 @@ public final class SpliceInfoDecoder implements MetadataDecoder {
} }
@Override @Override
public boolean canDecode(String mimeType) { public Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException {
return TextUtils.equals(mimeType, MimeTypes.APPLICATION_SCTE35); ByteBuffer buffer = inputBuffer.data;
} byte[] data = buffer.array();
int size = buffer.limit();
@Override
public Metadata decode(byte[] data, int size) throws MetadataDecoderException {
sectionData.reset(data, size); sectionData.reset(data, size);
sectionHeader.reset(data, size); sectionHeader.reset(data, size);
// table_id(8), section_syntax_indicator(1), private_indicator(1), reserved(2), // table_id(8), section_syntax_indicator(1), private_indicator(1), reserved(2),

View File

@ -26,10 +26,12 @@ import java.io.IOException;
* Wraps a {@link MediaPeriod} and clips its {@link SampleStream}s to provide a subsequence of their * Wraps a {@link MediaPeriod} and clips its {@link SampleStream}s to provide a subsequence of their
* samples. * 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; public final MediaPeriod mediaPeriod;
private final ClippingMediaSource mediaSource;
private MediaPeriod.Callback callback; private MediaPeriod.Callback callback;
private long startUs; 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 * Creates a new clipping media period that provides a clipped view of the specified
* {@link MediaPeriod}'s sample streams. * {@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 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.mediaPeriod = mediaPeriod;
this.mediaSource = mediaSource;
startUs = C.TIME_UNSET; startUs = C.TIME_UNSET;
endUs = C.TIME_UNSET; endUs = C.TIME_UNSET;
sampleStreams = new ClippingSampleStream[0]; 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 @Override
public void prepare(MediaPeriod.Callback callback) { public void prepare(MediaPeriod.Callback callback) {
this.callback = callback; this.callback = callback;
@ -80,7 +95,8 @@ import java.io.IOException;
long enablePositionUs = mediaPeriod.selectTracks(selections, mayRetainStreamFlags, long enablePositionUs = mediaPeriod.selectTracks(selections, mayRetainStreamFlags,
internalStreams, streamResetFlags, positionUs + startUs); internalStreams, streamResetFlags, positionUs + startUs);
Assertions.checkState(enablePositionUs == 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++) { for (int i = 0; i < streams.length; i++) {
if (internalStreams[i] == null) { if (internalStreams[i] == null) {
sampleStreams[i] = null; sampleStreams[i] = null;
@ -110,14 +126,16 @@ import java.io.IOException;
if (discontinuityUs == C.TIME_UNSET) { if (discontinuityUs == C.TIME_UNSET) {
return 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; return discontinuityUs - startUs;
} }
@Override @Override
public long getBufferedPositionUs() { public long getBufferedPositionUs() {
long bufferedPositionUs = mediaPeriod.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 C.TIME_END_OF_SOURCE;
} }
return Math.max(0, bufferedPositionUs - startUs); return Math.max(0, bufferedPositionUs - startUs);
@ -131,14 +149,16 @@ import java.io.IOException;
} }
} }
long seekUs = mediaPeriod.seekToUs(positionUs + startUs); 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; return seekUs - startUs;
} }
@Override @Override
public long getNextLoadPositionUs() { public long getNextLoadPositionUs() {
long nextLoadPositionUs = mediaPeriod.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 C.TIME_END_OF_SOURCE;
} }
return nextLoadPositionUs - startUs; return nextLoadPositionUs - startUs;
@ -153,8 +173,6 @@ import java.io.IOException;
@Override @Override
public void onPrepared(MediaPeriod mediaPeriod) { public void onPrepared(MediaPeriod mediaPeriod) {
startUs = mediaSource.getStartUs();
endUs = mediaSource.getEndUs();
Assertions.checkState(startUs != C.TIME_UNSET && endUs != C.TIME_UNSET); Assertions.checkState(startUs != C.TIME_UNSET && endUs != C.TIME_UNSET);
// If the clipping start position is non-zero, the clipping sample streams will adjust // 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 // timestamps on buffers they read from the unclipped sample streams. These adjusted buffer
@ -217,21 +235,24 @@ import java.io.IOException;
if (pendingDiscontinuity) { if (pendingDiscontinuity) {
return C.RESULT_NOTHING_READ; return C.RESULT_NOTHING_READ;
} }
if (buffer == null) {
return stream.readData(formatHolder, null);
}
if (sentEos) { if (sentEos) {
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
return C.RESULT_BUFFER_READ; return C.RESULT_BUFFER_READ;
} }
int result = stream.readData(formatHolder, buffer); int result = stream.readData(formatHolder, buffer);
// TODO: Clear gapless playback metadata if a format was read (if applicable). // TODO: Clear gapless playback metadata if a format was read (if applicable).
if ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs) if (endUs != C.TIME_END_OF_SOURCE && ((result == C.RESULT_BUFFER_READ
|| (result == C.RESULT_NOTHING_READ && buffer.timeUs >= endUs) || (result == C.RESULT_NOTHING_READ
&& mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE)) { && mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) {
buffer.clear(); buffer.clear();
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
sentEos = true; sentEos = true;
return C.RESULT_BUFFER_READ; return C.RESULT_BUFFER_READ;
} }
if (result == C.RESULT_BUFFER_READ) { if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream()) {
buffer.timeUs -= startUs; buffer.timeUs -= startUs;
} }
return result; return result;

View File

@ -21,17 +21,19 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
/** /**
* {@link MediaSource} that wraps a source and clips its timeline based on specified start/end * {@link MediaSource} that wraps a source and clips its timeline based on specified start/end
* positions. The wrapped source may only have a single period/window and it must not be dynamic * positions. The wrapped source may only have a single period/window and it must not be dynamic
* (live). The specified start position must correspond to a synchronization sample in the period. * (live).
*/ */
public final class ClippingMediaSource implements MediaSource, MediaSource.Listener { public final class ClippingMediaSource implements MediaSource, MediaSource.Listener {
private final MediaSource mediaSource; private final MediaSource mediaSource;
private final long startUs; private final long startUs;
private final long endUs; private final long endUs;
private final ArrayList<ClippingMediaPeriod> mediaPeriods;
private MediaSource.Listener sourceListener; private MediaSource.Listener sourceListener;
private ClippingTimeline clippingTimeline; private ClippingTimeline clippingTimeline;
@ -51,20 +53,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
this.mediaSource = Assertions.checkNotNull(mediaSource); this.mediaSource = Assertions.checkNotNull(mediaSource);
startUs = startPositionUs; startUs = startPositionUs;
endUs = endPositionUs; endUs = endPositionUs;
} mediaPeriods = new ArrayList<>();
/**
* Returns the start position of the clipping source's timeline in microseconds.
*/
/* package */ long getStartUs() {
return clippingTimeline.startUs;
}
/**
* Returns the end position of the clipping source's timeline in microseconds.
*/
/* package */ long getEndUs() {
return clippingTimeline.endUs;
} }
@Override @Override
@ -80,12 +69,16 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
@Override @Override
public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) { public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
return new ClippingMediaPeriod( ClippingMediaPeriod mediaPeriod = new ClippingMediaPeriod(
mediaSource.createPeriod(index, allocator, startUs + positionUs), this); mediaSource.createPeriod(index, allocator, startUs + positionUs));
mediaPeriods.add(mediaPeriod);
mediaPeriod.setClipping(clippingTimeline.startUs, clippingTimeline.endUs);
return mediaPeriod;
} }
@Override @Override
public void releasePeriod(MediaPeriod mediaPeriod) { public void releasePeriod(MediaPeriod mediaPeriod) {
Assertions.checkState(mediaPeriods.remove(mediaPeriod));
mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod);
} }
@ -100,6 +93,13 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) { public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
clippingTimeline = new ClippingTimeline(timeline, startUs, endUs); clippingTimeline = new ClippingTimeline(timeline, startUs, endUs);
sourceListener.onSourceInfoRefreshed(clippingTimeline, manifest); sourceListener.onSourceInfoRefreshed(clippingTimeline, manifest);
long startUs = clippingTimeline.startUs;
long endUs = clippingTimeline.endUs == C.TIME_UNSET ? C.TIME_END_OF_SOURCE
: clippingTimeline.endUs;
int count = mediaPeriods.size();
for (int i = 0; i < count; i++) {
mediaPeriods.get(i).setClipping(startUs, endUs);
}
} }
/** /**
@ -112,7 +112,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
private final long endUs; private final long endUs;
/** /**
* Creates a new timeline that wraps the specified timeline. * Creates a new clipping timeline that wraps the specified timeline.
* *
* @param timeline The timeline to clip. * @param timeline The timeline to clip.
* @param startUs The number of microseconds to clip from the start of {@code timeline}. * @param startUs The number of microseconds to clip from the start of {@code timeline}.

View File

@ -40,6 +40,7 @@ import com.google.android.exoplayer2.upstream.Loader.Loadable;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ConditionVariable; import com.google.android.exoplayer2.util.ConditionVariable;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
@ -62,6 +63,7 @@ import java.io.IOException;
private final ExtractorMediaSource.EventListener eventListener; private final ExtractorMediaSource.EventListener eventListener;
private final MediaSource.Listener sourceListener; private final MediaSource.Listener sourceListener;
private final Allocator allocator; private final Allocator allocator;
private final String customCacheKey;
private final Loader loader; private final Loader loader;
private final ExtractorHolder extractorHolder; private final ExtractorHolder extractorHolder;
private final ConditionVariable loadCondition; private final ConditionVariable loadCondition;
@ -101,11 +103,13 @@ import java.io.IOException;
* @param eventListener A listener of events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required.
* @param sourceListener A listener to notify when the timeline has been loaded. * @param sourceListener A listener to notify when the timeline has been loaded.
* @param allocator An {@link Allocator} from which to obtain media buffer allocations. * @param allocator An {@link Allocator} from which to obtain media buffer allocations.
* @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
* indexing. May be null.
*/ */
public ExtractorMediaPeriod(Uri uri, DataSource dataSource, Extractor[] extractors, public ExtractorMediaPeriod(Uri uri, DataSource dataSource, Extractor[] extractors,
int minLoadableRetryCount, Handler eventHandler, int minLoadableRetryCount, Handler eventHandler,
ExtractorMediaSource.EventListener eventListener, MediaSource.Listener sourceListener, ExtractorMediaSource.EventListener eventListener, MediaSource.Listener sourceListener,
Allocator allocator) { Allocator allocator, String customCacheKey) {
this.uri = uri; this.uri = uri;
this.dataSource = dataSource; this.dataSource = dataSource;
this.minLoadableRetryCount = minLoadableRetryCount; this.minLoadableRetryCount = minLoadableRetryCount;
@ -113,6 +117,7 @@ import java.io.IOException;
this.eventListener = eventListener; this.eventListener = eventListener;
this.sourceListener = sourceListener; this.sourceListener = sourceListener;
this.allocator = allocator; this.allocator = allocator;
this.customCacheKey = customCacheKey;
loader = new Loader("Loader:ExtractorMediaPeriod"); loader = new Loader("Loader:ExtractorMediaPeriod");
extractorHolder = new ExtractorHolder(extractors, this); extractorHolder = new ExtractorHolder(extractors, this);
loadCondition = new ConditionVariable(); loadCondition = new ConditionVariable();
@ -615,7 +620,7 @@ import java.io.IOException;
ExtractorInput input = null; ExtractorInput input = null;
try { try {
long position = positionHolder.position; long position = positionHolder.position;
length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNSET, null)); length = dataSource.open(new DataSpec(uri, position, C.LENGTH_UNSET, customCacheKey));
if (length != C.LENGTH_UNSET) { if (length != C.LENGTH_UNSET) {
length += position; length += position;
} }
@ -640,7 +645,7 @@ import java.io.IOException;
} else if (input != null) { } else if (input != null) {
positionHolder.position = input.getPosition(); positionHolder.position = input.getPosition();
} }
dataSource.close(); Util.closeQuietly(dataSource);
} }
} }
} }

View File

@ -93,6 +93,7 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
private final Handler eventHandler; private final Handler eventHandler;
private final EventListener eventListener; private final EventListener eventListener;
private final Timeline.Period period; private final Timeline.Period period;
private final String customCacheKey;
private MediaSource.Listener sourceListener; private MediaSource.Listener sourceListener;
private Timeline timeline; private Timeline timeline;
@ -110,7 +111,25 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory,
ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener) { ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener) {
this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler, this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler,
eventListener); eventListener, null);
}
/**
* @param uri The {@link Uri} of the media stream.
* @param dataSourceFactory A factory for {@link DataSource}s to read the media.
* @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the
* possible formats are known, pass a factory that instantiates extractors for those formats.
* Otherwise, pass a {@link DefaultExtractorsFactory} to use default extractors.
* @param eventHandler A handler for events. May be null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
* indexing. May be null.
*/
public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory,
ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener,
String customCacheKey) {
this(uri, dataSourceFactory, extractorsFactory, MIN_RETRY_COUNT_DEFAULT_FOR_MEDIA, eventHandler,
eventListener, customCacheKey);
} }
/** /**
@ -122,16 +141,19 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
* @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs. * @param minLoadableRetryCount The minimum number of times to retry if a loading error occurs.
* @param eventHandler A handler for events. May be null if delivery of events is not required. * @param eventHandler A handler for events. May be null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required.
* @param customCacheKey A custom key that uniquely identifies the original stream. Used for cache
* indexing. May be null.
*/ */
public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory, public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory,
ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler, ExtractorsFactory extractorsFactory, int minLoadableRetryCount, Handler eventHandler,
EventListener eventListener) { EventListener eventListener, String customCacheKey) {
this.uri = uri; this.uri = uri;
this.dataSourceFactory = dataSourceFactory; this.dataSourceFactory = dataSourceFactory;
this.extractorsFactory = extractorsFactory; this.extractorsFactory = extractorsFactory;
this.minLoadableRetryCount = minLoadableRetryCount; this.minLoadableRetryCount = minLoadableRetryCount;
this.eventHandler = eventHandler; this.eventHandler = eventHandler;
this.eventListener = eventListener; this.eventListener = eventListener;
this.customCacheKey = customCacheKey;
period = new Timeline.Period(); period = new Timeline.Period();
} }
@ -152,7 +174,7 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
Assertions.checkArgument(index == 0); Assertions.checkArgument(index == 0);
return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(), return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(),
extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener, extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener,
this, allocator); this, allocator, customCacheKey);
} }
@Override @Override

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source; package com.google.android.exoplayer2.source;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import java.io.IOException; import java.io.IOException;
@ -47,6 +48,10 @@ public interface MediaPeriod extends SequenceableLoader {
* <p> * <p>
* {@code callback.onPrepared} is called when preparation completes. If preparation fails, * {@code callback.onPrepared} is called when preparation completes. If preparation fails,
* {@link #maybeThrowPrepareError()} will throw an {@link IOException}. * {@link #maybeThrowPrepareError()} will throw an {@link IOException}.
* <p>
* If preparation succeeds and results in a source timeline change (e.g. the period duration
* becoming known), {@link MediaSource.Listener#onSourceInfoRefreshed(Timeline, Object)} will be
* called before {@code callback.onPrepared}.
* *
* @param callback Callback to receive updates from this period, including being notified when * @param callback Callback to receive updates from this period, including being notified when
* preparation completes. * preparation completes.

View File

@ -44,11 +44,17 @@ public interface SampleStream {
/** /**
* Attempts to read from the stream. * Attempts to read from the stream.
* <p>
* If no data is available then {@link C#RESULT_NOTHING_READ} is returned. If the format of the
* media is changing or if {@code buffer == null} then {@code formatHolder} is populated and
* {@link C#RESULT_FORMAT_READ} is returned. Else {@code buffer} is populated and
* {@link C#RESULT_BUFFER_READ} is returned.
* *
* @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
* @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
* end of the stream. If the end of the stream has been reached, the * end of the stream. If the end of the stream has been reached, the
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. * {@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 * @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
* {@link C#RESULT_BUFFER_READ}. * {@link C#RESULT_BUFFER_READ}.
*/ */

View File

@ -28,6 +28,7 @@ import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.upstream.Loader.Loadable; import com.google.android.exoplayer2.upstream.Loader.Loadable;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -205,13 +206,13 @@ import java.util.Arrays;
@Override @Override
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) { public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) {
if (streamState == STREAM_STATE_END_OF_STREAM) { if (buffer == null || streamState == STREAM_STATE_SEND_FORMAT) {
buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
return C.RESULT_BUFFER_READ;
} else if (streamState == STREAM_STATE_SEND_FORMAT) {
formatHolder.format = format; formatHolder.format = format;
streamState = STREAM_STATE_SEND_SAMPLE; streamState = STREAM_STATE_SEND_SAMPLE;
return C.RESULT_FORMAT_READ; return C.RESULT_FORMAT_READ;
} else if (streamState == STREAM_STATE_END_OF_STREAM) {
buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
return C.RESULT_BUFFER_READ;
} }
Assertions.checkState(streamState == STREAM_STATE_SEND_SAMPLE); Assertions.checkState(streamState == STREAM_STATE_SEND_SAMPLE);
@ -276,7 +277,7 @@ import java.util.Arrays;
result = dataSource.read(sampleData, sampleSize, sampleData.length - sampleSize); result = dataSource.read(sampleData, sampleSize, sampleData.length - sampleSize);
} }
} finally { } finally {
dataSource.close(); Util.closeQuietly(dataSource);
} }
} }

View File

@ -30,15 +30,15 @@ import java.io.IOException;
/** /**
* An {@link Extractor} wrapper for loading chunks containing a single track. * An {@link Extractor} wrapper for loading chunks containing a single track.
* <p> * <p>
* The wrapper allows switching of the {@link SingleTrackMetadataOutput} and {@link TrackOutput} * The wrapper allows switching of the {@link SeekMapOutput} and {@link TrackOutput} that receive
* which receive parsed data. * parsed data.
*/ */
public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput { public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput {
/** /**
* Receives metadata associated with the track as extracted by the wrapped {@link Extractor}. * Receives {@link SeekMap}s extracted by the wrapped {@link Extractor}.
*/ */
public interface SingleTrackMetadataOutput { public interface SeekMapOutput {
/** /**
* @see ExtractorOutput#seekMap(SeekMap) * @see ExtractorOutput#seekMap(SeekMap)
@ -47,13 +47,14 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput
} }
private final Extractor extractor; public final Extractor extractor;
private final Format manifestFormat; private final Format manifestFormat;
private final boolean preferManifestDrmInitData; private final boolean preferManifestDrmInitData;
private final boolean resendFormatOnInit; private final boolean resendFormatOnInit;
private boolean extractorInitialized; private boolean extractorInitialized;
private SingleTrackMetadataOutput metadataOutput; private SeekMapOutput seekMapOutput;
private TrackOutput trackOutput; private TrackOutput trackOutput;
private Format sentFormat; private Format sentFormat;
@ -68,7 +69,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput
* @param preferManifestDrmInitData Whether {@link DrmInitData} defined in {@code manifestFormat} * @param preferManifestDrmInitData Whether {@link DrmInitData} defined in {@code manifestFormat}
* should be preferred when the sample and manifest {@link Format}s are merged. * should be preferred when the sample and manifest {@link Format}s are merged.
* @param resendFormatOnInit Whether the extractor should resend the previous {@link Format} when * @param resendFormatOnInit Whether the extractor should resend the previous {@link Format} when
* it is initialized via {@link #init(SingleTrackMetadataOutput, TrackOutput)}. * it is initialized via {@link #init(SeekMapOutput, TrackOutput)}.
*/ */
public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat, public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat,
boolean preferManifestDrmInitData, boolean resendFormatOnInit) { boolean preferManifestDrmInitData, boolean resendFormatOnInit) {
@ -79,14 +80,14 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput
} }
/** /**
* Initializes the extractor to output to the provided {@link SingleTrackMetadataOutput} and * Initializes the extractor to output to the provided {@link SeekMapOutput} and
* {@link TrackOutput} instances, and configures it to receive data from a new chunk. * {@link TrackOutput} instances, and configures it to receive data from a new chunk.
* *
* @param metadataOutput The {@link SingleTrackMetadataOutput} that will receive metadata. * @param seekMapOutput The {@link SeekMapOutput} that will receive extracted {@link SeekMap}s.
* @param trackOutput The {@link TrackOutput} that will receive sample data. * @param trackOutput The {@link TrackOutput} that will receive sample data.
*/ */
public void init(SingleTrackMetadataOutput metadataOutput, TrackOutput trackOutput) { public void init(SeekMapOutput seekMapOutput, TrackOutput trackOutput) {
this.metadataOutput = metadataOutput; this.seekMapOutput = seekMapOutput;
this.trackOutput = trackOutput; this.trackOutput = trackOutput;
if (!extractorInitialized) { if (!extractorInitialized) {
extractor.init(this); extractor.init(this);
@ -99,20 +100,6 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput
} }
} }
/**
* Reads from the provided {@link ExtractorInput}.
*
* @param input The {@link ExtractorInput} from which to read.
* @return One of {@link Extractor#RESULT_CONTINUE} and {@link Extractor#RESULT_END_OF_INPUT}.
* @throws IOException If an error occurred reading from the source.
* @throws InterruptedException If the thread was interrupted.
*/
public int read(ExtractorInput input) throws IOException, InterruptedException {
int result = extractor.read(input, null);
Assertions.checkState(result != Extractor.RESULT_SEEK);
return result;
}
// ExtractorOutput implementation. // ExtractorOutput implementation.
@Override @Override
@ -130,7 +117,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput
@Override @Override
public void seekMap(SeekMap seekMap) { public void seekMap(SeekMap seekMap) {
metadataOutput.seekMap(seekMap); seekMapOutput.seekMap(seekMap);
} }
// TrackOutput implementation. // TrackOutput implementation.

View File

@ -122,7 +122,8 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
public void seekToUs(long positionUs) { public void seekToUs(long positionUs) {
lastSeekPositionUs = positionUs; lastSeekPositionUs = positionUs;
// If we're not pending a reset, see if we can seek within the sample queue. // If we're not pending a reset, see if we can seek within the sample queue.
boolean seekInsideBuffer = !isPendingReset() && sampleQueue.skipToKeyframeBefore(positionUs); boolean seekInsideBuffer = !isPendingReset()
&& sampleQueue.skipToKeyframeBefore(positionUs, positionUs < getNextLoadPositionUs());
if (seekInsideBuffer) { if (seekInsideBuffer) {
// We succeeded. All we need to do is discard any chunks that we've moved past. // We succeeded. All we need to do is discard any chunks that we've moved past.
while (mediaChunks.size() > 1 while (mediaChunks.size() > 1

View File

@ -21,16 +21,17 @@ import com.google.android.exoplayer2.extractor.DefaultTrackOutput;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.SingleTrackMetadataOutput; import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.SeekMapOutput;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
/** /**
* A {@link BaseMediaChunk} that uses an {@link Extractor} to decode sample data. * A {@link BaseMediaChunk} that uses an {@link Extractor} to decode sample data.
*/ */
public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackMetadataOutput { public class ContainerMediaChunk extends BaseMediaChunk implements SeekMapOutput {
private final int chunkCount; private final int chunkCount;
private final long sampleOffsetUs; private final long sampleOffsetUs;
@ -85,7 +86,7 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackMe
return bytesLoaded; return bytesLoaded;
} }
// SingleTrackMetadataOutput implementation. // SeekMapOutput implementation.
@Override @Override
public final void seekMap(SeekMap seekMap) { public final void seekMap(SeekMap seekMap) {
@ -120,15 +121,17 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackMe
} }
// Load and decode the sample data. // Load and decode the sample data.
try { try {
Extractor extractor = extractorWrapper.extractor;
int result = Extractor.RESULT_CONTINUE; int result = Extractor.RESULT_CONTINUE;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
result = extractorWrapper.read(input); result = extractor.read(input, null);
} }
Assertions.checkState(result != Extractor.RESULT_SEEK);
} finally { } finally {
bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
} }
} finally { } finally {
dataSource.close(); Util.closeQuietly(dataSource);
} }
loadCompleted = true; loadCompleted = true;
} }

View File

@ -19,6 +19,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
@ -96,7 +97,7 @@ public abstract class DataChunk extends Chunk {
consume(data, limit); consume(data, limit);
} }
} finally { } finally {
dataSource.close(); Util.closeQuietly(dataSource);
} }
} }

View File

@ -22,9 +22,10 @@ import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.SingleTrackMetadataOutput; import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper.SeekMapOutput;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
@ -32,7 +33,7 @@ import java.io.IOException;
/** /**
* A {@link Chunk} that uses an {@link Extractor} to decode initialization data for single track. * A {@link Chunk} that uses an {@link Extractor} to decode initialization data for single track.
*/ */
public final class InitializationChunk extends Chunk implements SingleTrackMetadataOutput, public final class InitializationChunk extends Chunk implements SeekMapOutput,
TrackOutput { TrackOutput {
private final ChunkExtractorWrapper extractorWrapper; private final ChunkExtractorWrapper extractorWrapper;
@ -85,7 +86,7 @@ public final class InitializationChunk extends Chunk implements SingleTrackMetad
return seekMap; return seekMap;
} }
// SingleTrackMetadataOutput implementation. // SeekMapOutput implementation.
@Override @Override
public void seekMap(SeekMap seekMap) { public void seekMap(SeekMap seekMap) {
@ -142,15 +143,17 @@ public final class InitializationChunk extends Chunk implements SingleTrackMetad
} }
// Load and decode the initialization data. // Load and decode the initialization data.
try { try {
Extractor extractor = extractorWrapper.extractor;
int result = Extractor.RESULT_CONTINUE; int result = Extractor.RESULT_CONTINUE;
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) { while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
result = extractorWrapper.read(input); result = extractor.read(input, null);
} }
Assertions.checkState(result != Extractor.RESULT_SEEK);
} finally { } finally {
bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition); bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
} }
} finally { } finally {
dataSource.close(); Util.closeQuietly(dataSource);
} }
} }

View File

@ -98,7 +98,7 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk {
int sampleSize = bytesLoaded; int sampleSize = bytesLoaded;
trackOutput.sampleMetadata(startTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null); trackOutput.sampleMetadata(startTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
} finally { } finally {
dataSource.close(); Util.closeQuietly(dataSource);
} }
loadCompleted = true; loadCompleted = true;
} }

View File

@ -28,9 +28,8 @@ import com.google.android.exoplayer2.source.dash.manifest.RangedUri;
/** /**
* @param chunkIndex The {@link ChunkIndex} to wrap. * @param chunkIndex The {@link ChunkIndex} to wrap.
* @param uri The URI where the data is located.
*/ */
public DashWrappingSegmentIndex(ChunkIndex chunkIndex, String uri) { public DashWrappingSegmentIndex(ChunkIndex chunkIndex) {
this.chunkIndex = chunkIndex; this.chunkIndex = chunkIndex;
} }

View File

@ -185,10 +185,9 @@ public class DefaultDashChunkSource implements DashChunkSource {
} }
if (pendingInitializationUri != null || pendingIndexUri != null) { if (pendingInitializationUri != null || pendingIndexUri != null) {
// We have initialization and/or index requests to make. // We have initialization and/or index requests to make.
Chunk initializationChunk = newInitializationChunk(representationHolder, dataSource, out.chunk = newInitializationChunk(representationHolder, dataSource,
trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(), trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(),
trackSelection.getSelectionData(), pendingInitializationUri, pendingIndexUri); trackSelection.getSelectionData(), pendingInitializationUri, pendingIndexUri);
out.chunk = initializationChunk;
return; return;
} }
@ -233,10 +232,9 @@ public class DefaultDashChunkSource implements DashChunkSource {
} }
int maxSegmentCount = Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1); int maxSegmentCount = Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1);
Chunk nextMediaChunk = newMediaChunk(representationHolder, dataSource, out.chunk = newMediaChunk(representationHolder, dataSource, trackSelection.getSelectedFormat(),
trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(), trackSelection.getSelectionReason(), trackSelection.getSelectionData(), sampleFormat,
trackSelection.getSelectionData(), sampleFormat, segmentNum, maxSegmentCount); segmentNum, maxSegmentCount);
out.chunk = nextMediaChunk;
} }
@Override @Override
@ -255,8 +253,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
if (representationHolder.segmentIndex == null) { if (representationHolder.segmentIndex == null) {
SeekMap seekMap = initializationChunk.getSeekMap(); SeekMap seekMap = initializationChunk.getSeekMap();
if (seekMap != null) { if (seekMap != null) {
representationHolder.segmentIndex = new DashWrappingSegmentIndex((ChunkIndex) seekMap, representationHolder.segmentIndex = new DashWrappingSegmentIndex((ChunkIndex) seekMap);
initializationChunk.dataSpec.uri.toString());
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More