mirror of
https://github.com/androidx/media.git
synced 2025-05-10 00:59:51 +08:00
Merge remote-tracking branch 'upstream/dev-v2' into dev-v2
This commit is contained in:
commit
6ec840cc80
@ -58,7 +58,7 @@ this version.
|
||||
* Fix issues that could cause ExtractorMediaSource based playbacks to get stuck
|
||||
buffering ([#1962](https://github.com/google/ExoPlayer/issues/1962)).
|
||||
* Correctly set SimpleExoPlayerView surface aspect ratio when an active player
|
||||
is attached ([#2077](https://github.com/google/ExoPlayer/issues/1976)).
|
||||
is attached ([#2077](https://github.com/google/ExoPlayer/issues/2077)).
|
||||
* OGG: Fix playback of short OGG files
|
||||
([#1976](https://github.com/google/ExoPlayer/issues/1976)).
|
||||
* MP4: Support `.mp3` tracks
|
||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 31 KiB |
@ -24,24 +24,19 @@ android {
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
shrinkResources true
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||
}
|
||||
debug {
|
||||
jniDebuggable = true
|
||||
debuggable = true
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
productFlavors {
|
||||
noExtensions
|
||||
withExtensions
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -229,30 +229,6 @@
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure Subsample (WebM, VP9 with altref)",
|
||||
"uri": "https://storage.googleapis.com/widevine_test/vp9/sintel_1080p_vp9_altref_subsample/sintel_1080p_vp9_altref_subsample.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://widevine-proxy.appspot.com/proxy"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure Fullsample (WebM, VP9 with altref)",
|
||||
"uri": "https://storage.googleapis.com/widevine_test/vp9/sintel_1080p_vp9_altref_fullsample/sintel_1080p_vp9_altref_fullsample.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://widevine-proxy.appspot.com/proxy"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure Subsample (WebM, VP9 without altref)",
|
||||
"uri": "https://storage.googleapis.com/widevine_test/vp9/sintel_1080p_vp9_noaltref_subsample/sintel_1080p_vp9_noaltref_subsample.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://widevine-proxy.appspot.com/proxy"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure Fullsample (WebM, VP9 without altref)",
|
||||
"uri": "https://storage.googleapis.com/widevine_test/vp9/sintel_1080p_vp9_noaltref_fullsample/sintel_1080p_vp9_noaltref_fullsample.mpd",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://widevine-proxy.appspot.com/proxy"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -26,16 +26,17 @@ import com.google.android.exoplayer2.RendererCapabilities;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
import com.google.android.exoplayer2.drm.StreamingDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.MetadataRenderer;
|
||||
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
|
||||
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.GeobFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
|
||||
import com.google.android.exoplayer2.metadata.id3.PrivFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.TxxxFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame;
|
||||
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
@ -55,7 +56,7 @@ import java.util.Locale;
|
||||
*/
|
||||
/* package */ final class EventLogger implements ExoPlayer.EventListener,
|
||||
AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener,
|
||||
ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener,
|
||||
ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener,
|
||||
MetadataRenderer.Output {
|
||||
|
||||
private static final String TAG = "EventLogger";
|
||||
@ -153,7 +154,7 @@ import java.util.Locale;
|
||||
String formatSupport = getFormatSupportString(
|
||||
mappedTrackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex));
|
||||
Log.d(TAG, " " + status + " Track:" + trackIndex + ", "
|
||||
+ getFormatString(trackGroup.getFormat(trackIndex))
|
||||
+ Format.toLogString(trackGroup.getFormat(trackIndex))
|
||||
+ ", supported=" + formatSupport);
|
||||
}
|
||||
Log.d(TAG, " ]");
|
||||
@ -185,7 +186,7 @@ import java.util.Locale;
|
||||
String formatSupport = getFormatSupportString(
|
||||
RendererCapabilities.FORMAT_UNSUPPORTED_TYPE);
|
||||
Log.d(TAG, " " + status + " Track:" + trackIndex + ", "
|
||||
+ getFormatString(trackGroup.getFormat(trackIndex))
|
||||
+ Format.toLogString(trackGroup.getFormat(trackIndex))
|
||||
+ ", supported=" + formatSupport);
|
||||
}
|
||||
Log.d(TAG, " ]");
|
||||
@ -224,7 +225,7 @@ import java.util.Locale;
|
||||
|
||||
@Override
|
||||
public void onAudioInputFormatChanged(Format format) {
|
||||
Log.d(TAG, "audioFormatChanged [" + getSessionTimeString() + ", " + getFormatString(format)
|
||||
Log.d(TAG, "audioFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format)
|
||||
+ "]");
|
||||
}
|
||||
|
||||
@ -254,7 +255,7 @@ import java.util.Locale;
|
||||
|
||||
@Override
|
||||
public void onVideoInputFormatChanged(Format format) {
|
||||
Log.d(TAG, "videoFormatChanged [" + getSessionTimeString() + ", " + getFormatString(format)
|
||||
Log.d(TAG, "videoFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format)
|
||||
+ "]");
|
||||
}
|
||||
|
||||
@ -279,13 +280,23 @@ import java.util.Locale;
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
// StreamingDrmSessionManager.EventListener
|
||||
// DefaultDrmSessionManager.EventListener
|
||||
|
||||
@Override
|
||||
public void onDrmSessionManagerError(Exception e) {
|
||||
printInternalError("drmSessionManagerError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmKeysRestored() {
|
||||
Log.d(TAG, "drmKeysRestored [" + getSessionTimeString() + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmKeysRemoved() {
|
||||
Log.d(TAG, "drmKeysRemoved [" + getSessionTimeString() + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmKeysLoaded() {
|
||||
Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]");
|
||||
@ -349,10 +360,13 @@ import java.util.Locale;
|
||||
private void printMetadata(Metadata metadata, String prefix) {
|
||||
for (int i = 0; i < metadata.length(); i++) {
|
||||
Metadata.Entry entry = metadata.get(i);
|
||||
if (entry instanceof TxxxFrame) {
|
||||
TxxxFrame txxxFrame = (TxxxFrame) entry;
|
||||
Log.d(TAG, prefix + String.format("%s: description=%s, value=%s", txxxFrame.id,
|
||||
txxxFrame.description, txxxFrame.value));
|
||||
if (entry instanceof TextInformationFrame) {
|
||||
TextInformationFrame textInformationFrame = (TextInformationFrame) entry;
|
||||
Log.d(TAG, prefix + String.format("%s: value=%s", textInformationFrame.id,
|
||||
textInformationFrame.value));
|
||||
} else if (entry instanceof UrlLinkFrame) {
|
||||
UrlLinkFrame urlLinkFrame = (UrlLinkFrame) entry;
|
||||
Log.d(TAG, prefix + String.format("%s: url=%s", urlLinkFrame.id, urlLinkFrame.url));
|
||||
} else if (entry instanceof PrivFrame) {
|
||||
PrivFrame privFrame = (PrivFrame) entry;
|
||||
Log.d(TAG, prefix + String.format("%s: owner=%s", privFrame.id, privFrame.owner));
|
||||
@ -364,17 +378,17 @@ import java.util.Locale;
|
||||
ApicFrame apicFrame = (ApicFrame) entry;
|
||||
Log.d(TAG, prefix + String.format("%s: mimeType=%s, description=%s",
|
||||
apicFrame.id, apicFrame.mimeType, apicFrame.description));
|
||||
} else if (entry instanceof TextInformationFrame) {
|
||||
TextInformationFrame textInformationFrame = (TextInformationFrame) entry;
|
||||
Log.d(TAG, prefix + String.format("%s: description=%s", textInformationFrame.id,
|
||||
textInformationFrame.description));
|
||||
} else if (entry instanceof CommentFrame) {
|
||||
CommentFrame commentFrame = (CommentFrame) entry;
|
||||
Log.d(TAG, prefix + String.format("%s: language=%s description=%s", commentFrame.id,
|
||||
Log.d(TAG, prefix + String.format("%s: language=%s, description=%s", commentFrame.id,
|
||||
commentFrame.language, commentFrame.description));
|
||||
} else if (entry instanceof Id3Frame) {
|
||||
Id3Frame id3Frame = (Id3Frame) entry;
|
||||
Log.d(TAG, prefix + String.format("%s", id3Frame.id));
|
||||
} else if (entry instanceof EventMessage) {
|
||||
EventMessage eventMessage = (EventMessage) entry;
|
||||
Log.d(TAG, prefix + String.format("EMSG: scheme=%s, id=%d, value=%s",
|
||||
eventMessage.schemeIdUri, eventMessage.id, eventMessage.value));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -433,33 +447,6 @@ import java.util.Locale;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getFormatString(Format format) {
|
||||
if (format == null) {
|
||||
return "null";
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("id=").append(format.id).append(", mimeType=").append(format.sampleMimeType);
|
||||
if (format.bitrate != Format.NO_VALUE) {
|
||||
builder.append(", bitrate=").append(format.bitrate);
|
||||
}
|
||||
if (format.width != Format.NO_VALUE && format.height != Format.NO_VALUE) {
|
||||
builder.append(", res=").append(format.width).append("x").append(format.height);
|
||||
}
|
||||
if (format.frameRate != Format.NO_VALUE) {
|
||||
builder.append(", fps=").append(format.frameRate);
|
||||
}
|
||||
if (format.channelCount != Format.NO_VALUE) {
|
||||
builder.append(", channels=").append(format.channelCount);
|
||||
}
|
||||
if (format.sampleRate != Format.NO_VALUE) {
|
||||
builder.append(", sample_rate=").append(format.sampleRate);
|
||||
}
|
||||
if (format.language != null) {
|
||||
builder.append(", language=").append(format.language);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static String getTrackStatusString(TrackSelection selection, TrackGroup group,
|
||||
int trackIndex) {
|
||||
return getTrackStatusString(selection != null && selection.getTrackGroup() == group
|
||||
|
@ -36,15 +36,16 @@ import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
||||
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
||||
import com.google.android.exoplayer2.drm.StreamingDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
@ -100,7 +101,6 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
}
|
||||
|
||||
private Handler mainHandler;
|
||||
private Timeline.Window window;
|
||||
private EventLogger eventLogger;
|
||||
private SimpleExoPlayerView simpleExoPlayerView;
|
||||
private LinearLayout debugRootView;
|
||||
@ -115,9 +115,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
private boolean playerNeedsSource;
|
||||
|
||||
private boolean shouldAutoPlay;
|
||||
private boolean isTimelineStatic;
|
||||
private int playerWindow;
|
||||
private long playerPosition;
|
||||
private int resumeWindow;
|
||||
private long resumePosition;
|
||||
|
||||
// Activity lifecycle
|
||||
|
||||
@ -125,9 +124,9 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
shouldAutoPlay = true;
|
||||
clearResumePosition();
|
||||
mediaDataSourceFactory = buildDataSourceFactory(true);
|
||||
mainHandler = new Handler();
|
||||
window = new Timeline.Window();
|
||||
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
|
||||
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
|
||||
}
|
||||
@ -148,7 +147,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
releasePlayer();
|
||||
isTimelineStatic = false;
|
||||
shouldAutoPlay = true;
|
||||
clearResumePosition();
|
||||
setIntent(intent);
|
||||
}
|
||||
|
||||
@ -264,7 +264,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
@SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode =
|
||||
((DemoApplication) getApplication()).useExtensionRenderers()
|
||||
? (preferExtensionDecoders ? SimpleExoPlayer.EXTENSION_RENDERER_MODE_PREFER
|
||||
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_ON)
|
||||
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_ON)
|
||||
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF;
|
||||
TrackSelection.Factory videoTrackSelectionFactory =
|
||||
new AdaptiveVideoTrackSelection.Factory(BANDWIDTH_METER);
|
||||
@ -278,16 +278,9 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
player.addListener(eventLogger);
|
||||
player.setAudioDebugListener(eventLogger);
|
||||
player.setVideoDebugListener(eventLogger);
|
||||
player.setId3Output(eventLogger);
|
||||
player.setMetadataOutput(eventLogger);
|
||||
|
||||
simpleExoPlayerView.setPlayer(player);
|
||||
if (isTimelineStatic) {
|
||||
if (playerPosition == C.TIME_UNSET) {
|
||||
player.seekToDefaultPosition(playerWindow);
|
||||
} else {
|
||||
player.seekTo(playerWindow, playerPosition);
|
||||
}
|
||||
}
|
||||
player.setPlayWhenReady(shouldAutoPlay);
|
||||
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
|
||||
debugViewHelper.start();
|
||||
@ -324,7 +317,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
}
|
||||
MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
|
||||
: new ConcatenatingMediaSource(mediaSources);
|
||||
player.prepare(mediaSource, !isTimelineStatic, !isTimelineStatic);
|
||||
player.seekTo(resumeWindow, resumePosition);
|
||||
player.prepare(mediaSource, false, false);
|
||||
playerNeedsSource = false;
|
||||
updateButtonVisibilities();
|
||||
}
|
||||
@ -358,7 +352,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
}
|
||||
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl,
|
||||
buildHttpDataSourceFactory(false), keyRequestProperties);
|
||||
return new StreamingDrmSessionManager<>(uuid,
|
||||
return new DefaultDrmSessionManager<>(uuid,
|
||||
FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger);
|
||||
}
|
||||
|
||||
@ -367,12 +361,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
debugViewHelper.stop();
|
||||
debugViewHelper = null;
|
||||
shouldAutoPlay = player.getPlayWhenReady();
|
||||
playerWindow = player.getCurrentWindowIndex();
|
||||
playerPosition = C.TIME_UNSET;
|
||||
Timeline timeline = player.getCurrentTimeline();
|
||||
if (!timeline.isEmpty() && timeline.getWindow(playerWindow, window).isSeekable) {
|
||||
playerPosition = player.getCurrentPosition();
|
||||
}
|
||||
updateResumePosition();
|
||||
player.release();
|
||||
player = null;
|
||||
trackSelector = null;
|
||||
@ -381,6 +370,17 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
}
|
||||
}
|
||||
|
||||
private void updateResumePosition() {
|
||||
resumeWindow = player.getCurrentWindowIndex();
|
||||
resumePosition = player.isCurrentWindowSeekable() ? Math.max(0, player.getCurrentPosition())
|
||||
: C.TIME_UNSET;
|
||||
}
|
||||
|
||||
private void clearResumePosition() {
|
||||
resumeWindow = 0;
|
||||
resumePosition = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new DataSource factory.
|
||||
*
|
||||
@ -427,8 +427,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||
isTimelineStatic = !timeline.isEmpty()
|
||||
&& !timeline.getWindow(timeline.getWindowCount() - 1, window).isDynamic;
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -460,6 +459,11 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
showToast(errorString);
|
||||
}
|
||||
playerNeedsSource = true;
|
||||
if (isBehindLiveWindow(e)) {
|
||||
clearResumePosition();
|
||||
} else {
|
||||
updateResumePosition();
|
||||
}
|
||||
updateButtonVisibilities();
|
||||
showControls();
|
||||
}
|
||||
@ -535,4 +539,18 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
||||
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private static boolean isBehindLiveWindow(ExoPlaybackException e) {
|
||||
if (e.type != ExoPlaybackException.TYPE_SOURCE) {
|
||||
return false;
|
||||
}
|
||||
Throwable cause = e.getSourceException();
|
||||
while (cause != null) {
|
||||
if (cause instanceof BehindLiveWindowException) {
|
||||
return true;
|
||||
}
|
||||
cause = cause.getCause();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -23,17 +23,6 @@ android {
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
sourceSets.main {
|
||||
jniLibs.srcDirs = ['jniLibs']
|
||||
}
|
||||
|
@ -57,8 +57,8 @@ import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.chromium.net.CronetEngine;
|
||||
import org.chromium.net.NetworkException;
|
||||
import org.chromium.net.UrlRequest;
|
||||
import org.chromium.net.UrlRequestException;
|
||||
import org.chromium.net.UrlResponseInfo;
|
||||
import org.chromium.net.impl.UrlResponseInfoImpl;
|
||||
import org.junit.Before;
|
||||
@ -99,7 +99,7 @@ public final class CronetDataSourceTest {
|
||||
@Mock
|
||||
private Executor mockExecutor;
|
||||
@Mock
|
||||
private UrlRequestException mockUrlRequestException;
|
||||
private NetworkException mockNetworkException;
|
||||
@Mock private CronetEngine mockCronetEngine;
|
||||
|
||||
private CronetDataSource dataSourceUnderTest;
|
||||
@ -172,7 +172,7 @@ public final class CronetDataSourceTest {
|
||||
dataSourceUnderTest.onFailed(
|
||||
mockUrlRequest,
|
||||
testUrlResponseInfo,
|
||||
mockUrlRequestException);
|
||||
mockNetworkException);
|
||||
dataSourceUnderTest.onResponseStarted(
|
||||
mockUrlRequest2,
|
||||
testUrlResponseInfo);
|
||||
@ -245,8 +245,8 @@ public final class CronetDataSourceTest {
|
||||
@Test
|
||||
public void testRequestOpenFailDueToDnsFailure() {
|
||||
mockResponseStartFailure();
|
||||
when(mockUrlRequestException.getErrorCode()).thenReturn(
|
||||
UrlRequestException.ERROR_HOSTNAME_NOT_RESOLVED);
|
||||
when(mockNetworkException.getErrorCode()).thenReturn(
|
||||
NetworkException.ERROR_HOSTNAME_NOT_RESOLVED);
|
||||
|
||||
try {
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
@ -728,7 +728,7 @@ public final class CronetDataSourceTest {
|
||||
dataSourceUnderTest.onFailed(
|
||||
mockUrlRequest,
|
||||
createUrlResponseInfo(500), // statusCode
|
||||
mockUrlRequestException);
|
||||
mockNetworkException);
|
||||
return null;
|
||||
}
|
||||
}).when(mockUrlRequest).start();
|
||||
@ -764,7 +764,7 @@ public final class CronetDataSourceTest {
|
||||
dataSourceUnderTest.onFailed(
|
||||
mockUrlRequest,
|
||||
createUrlResponseInfo(500), // statusCode
|
||||
mockUrlRequestException);
|
||||
mockNetworkException);
|
||||
return null;
|
||||
}
|
||||
}).when(mockUrlRequest).read(any(ByteBuffer.class));
|
||||
|
@ -40,9 +40,10 @@ import java.util.concurrent.Executor;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.chromium.net.CronetEngine;
|
||||
import org.chromium.net.CronetException;
|
||||
import org.chromium.net.NetworkException;
|
||||
import org.chromium.net.UrlRequest;
|
||||
import org.chromium.net.UrlRequest.Status;
|
||||
import org.chromium.net.UrlRequestException;
|
||||
import org.chromium.net.UrlResponseInfo;
|
||||
|
||||
/**
|
||||
@ -400,12 +401,17 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
||||
|
||||
@Override
|
||||
public synchronized void onFailed(UrlRequest request, UrlResponseInfo info,
|
||||
UrlRequestException error) {
|
||||
CronetException error) {
|
||||
if (request != currentUrlRequest) {
|
||||
return;
|
||||
}
|
||||
exception = error.getErrorCode() == UrlRequestException.ERROR_HOSTNAME_NOT_RESOLVED
|
||||
? new UnknownHostException() : error;
|
||||
if (error instanceof NetworkException
|
||||
&& ((NetworkException) error).getErrorCode()
|
||||
== NetworkException.ERROR_HOSTNAME_NOT_RESOLVED) {
|
||||
exception = new UnknownHostException();
|
||||
} else {
|
||||
exception = error;
|
||||
}
|
||||
operation.open();
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.util.Predicate;
|
||||
@ -25,7 +26,7 @@ import org.chromium.net.CronetEngine;
|
||||
/**
|
||||
* A {@link Factory} that produces {@link CronetDataSource}.
|
||||
*/
|
||||
public final class CronetDataSourceFactory implements Factory {
|
||||
public final class CronetDataSourceFactory extends BaseFactory {
|
||||
|
||||
/**
|
||||
* The default connection timeout, in milliseconds.
|
||||
@ -67,7 +68,7 @@ public final class CronetDataSourceFactory implements Factory {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CronetDataSource createDataSource() {
|
||||
protected CronetDataSource createDataSourceInternal() {
|
||||
return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener,
|
||||
connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects);
|
||||
}
|
||||
|
@ -20,17 +20,7 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion 9
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
|
||||
sourceSets.main {
|
||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.ffmpeg;
|
||||
|
||||
import android.os.Handler;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
@ -60,7 +61,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int supportsFormat(Format format) {
|
||||
protected int supportsFormatInternal(Format format) {
|
||||
if (!FfmpegLibrary.isAvailable()) {
|
||||
return FORMAT_UNSUPPORTED_TYPE;
|
||||
}
|
||||
@ -69,6 +70,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
: MimeTypes.isAudio(mimeType) ? FORMAT_UNSUPPORTED_SUBTYPE : FORMAT_UNSUPPORTED_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {
|
||||
return ADAPTIVE_NOT_SEAMLESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
|
||||
throws FfmpegDecoderException {
|
||||
|
@ -20,17 +20,7 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion 9
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
|
||||
sourceSets.main {
|
||||
|
@ -56,7 +56,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int supportsFormat(Format format) {
|
||||
protected int supportsFormatInternal(Format format) {
|
||||
return FlacLibrary.isAvailable() && MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)
|
||||
? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE;
|
||||
}
|
||||
|
@ -22,17 +22,6 @@ android {
|
||||
minSdkVersion 9
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -261,7 +261,7 @@ public class OkHttpDataSource implements HttpDataSource {
|
||||
private Request makeRequest(DataSpec dataSpec) {
|
||||
long position = dataSpec.position;
|
||||
long length = dataSpec.length;
|
||||
boolean allowGzip = (dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) != 0;
|
||||
boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
|
||||
|
||||
HttpUrl url = HttpUrl.parse(dataSpec.uri.toString());
|
||||
Request.Builder builder = new Request.Builder().url(url);
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2.ext.okhttp;
|
||||
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import okhttp3.CacheControl;
|
||||
@ -24,7 +25,7 @@ import okhttp3.Call;
|
||||
/**
|
||||
* A {@link Factory} that produces {@link OkHttpDataSource}.
|
||||
*/
|
||||
public final class OkHttpDataSourceFactory implements Factory {
|
||||
public final class OkHttpDataSourceFactory extends BaseFactory {
|
||||
|
||||
private final Call.Factory callFactory;
|
||||
private final String userAgent;
|
||||
@ -58,7 +59,7 @@ public final class OkHttpDataSourceFactory implements Factory {
|
||||
}
|
||||
|
||||
@Override
|
||||
public OkHttpDataSource createDataSource() {
|
||||
protected OkHttpDataSource createDataSourceInternal() {
|
||||
return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl);
|
||||
}
|
||||
|
||||
|
@ -20,17 +20,7 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion 9
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
|
||||
sourceSets.main {
|
||||
|
@ -72,7 +72,7 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int supportsFormat(Format format) {
|
||||
protected int supportsFormatInternal(Format format) {
|
||||
return OpusLibrary.isAvailable() && MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)
|
||||
? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE;
|
||||
}
|
||||
|
@ -20,17 +20,7 @@ android {
|
||||
defaultConfig {
|
||||
minSdkVersion 9
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
|
||||
sourceSets.main {
|
||||
|
@ -1,5 +1,3 @@
|
||||
import com.android.builder.core.BuilderConstants
|
||||
|
||||
// Copyright (C) 2016 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -13,6 +11,8 @@ import com.android.builder.core.BuilderConstants
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import com.android.builder.core.BuilderConstants
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'bintray-release'
|
||||
|
||||
@ -28,13 +28,10 @@ android {
|
||||
// greater.
|
||||
minSdkVersion 9
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
}
|
||||
// Re-enable test coverage when the following issue is fixed:
|
||||
// https://code.google.com/p/android/issues/detail?id=226070
|
||||
// debug {
|
||||
@ -42,10 +39,6 @@ android {
|
||||
// }
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
androidTest {
|
||||
java.srcDirs += "../testutils/src/main/java/"
|
||||
|
7
library/proguard-rules.txt
Normal file
7
library/proguard-rules.txt
Normal 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);
|
||||
}
|
@ -21,7 +21,6 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
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.TrackGroupArray;
|
||||
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.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MediaClock;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
@ -49,16 +49,11 @@ public final class ExoPlayerTest extends TestCase {
|
||||
*/
|
||||
private static final int TIMEOUT_MS = 10000;
|
||||
|
||||
/**
|
||||
* Tests playback of a source that exposes a single period.
|
||||
*/
|
||||
public void testPlayToEnd() throws Exception {
|
||||
PlayerWrapper playerWrapper = new PlayerWrapper();
|
||||
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);
|
||||
}
|
||||
private static final Format TEST_VIDEO_FORMAT = Format.createVideoSampleFormat(null,
|
||||
MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, 1280, 720, Format.NO_VALUE,
|
||||
null, null);
|
||||
private static final Format TEST_AUDIO_FORMAT = Format.createAudioSampleFormat(null,
|
||||
MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null);
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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);
|
||||
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 Handler handler;
|
||||
|
||||
private Timeline expectedTimeline;
|
||||
private Object expectedManifest;
|
||||
private Format expectedFormat;
|
||||
private ExoPlayer player;
|
||||
private Timeline timeline;
|
||||
private Object manifest;
|
||||
private TrackGroupArray trackGroups;
|
||||
private Exception exception;
|
||||
|
||||
// Written only on the main thread.
|
||||
private volatile int positionDiscontinuityCount;
|
||||
|
||||
public PlayerWrapper() {
|
||||
endedCountDownLatch = new CountDownLatch(1);
|
||||
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) {
|
||||
expectedTimeline = timeline;
|
||||
expectedManifest = manifest;
|
||||
expectedFormat = format;
|
||||
public void setup(final MediaSource mediaSource, final Renderer... renderers) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Renderer fakeRenderer = new FakeVideoRenderer(expectedFormat);
|
||||
player = ExoPlayerFactory.newInstance(new Renderer[] {fakeRenderer},
|
||||
new DefaultTrackSelector());
|
||||
player = ExoPlayerFactory.newInstance(renderers, new DefaultTrackSelector());
|
||||
player.addListener(PlayerWrapper.this);
|
||||
player.setPlayWhenReady(true);
|
||||
player.prepare(new FakeMediaSource(timeline, manifest, format));
|
||||
player.prepare(mediaSource);
|
||||
} catch (Exception e) {
|
||||
handleError(e);
|
||||
}
|
||||
@ -167,14 +252,13 @@ public final class ExoPlayerTest extends TestCase {
|
||||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||
assertEquals(expectedTimeline, timeline);
|
||||
assertEquals(expectedManifest, manifest);
|
||||
this.timeline = timeline;
|
||||
this.manifest = manifest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(TrackGroupArray trackGroups,
|
||||
TrackSelectionArray trackSelections) {
|
||||
assertEquals(new TrackGroupArray(new TrackGroup(expectedFormat)), trackGroups);
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
this.trackGroups = trackGroups;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -182,10 +266,69 @@ public final class ExoPlayerTest extends TestCase {
|
||||
handleError(exception);
|
||||
}
|
||||
|
||||
@SuppressWarnings("NonAtomicVolatileUpdate")
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
// Should never happen.
|
||||
handleError(new IllegalStateException("Received position discontinuity"));
|
||||
positionDiscontinuityCount++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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 Object manifest;
|
||||
private final Format format;
|
||||
private final TrackGroupArray trackGroupArray;
|
||||
private final ArrayList<FakeMediaPeriod> activeMediaPeriods;
|
||||
|
||||
private boolean preparedSource;
|
||||
private boolean releasedSource;
|
||||
|
||||
public FakeMediaSource(Timeline timeline, Object manifest, Format format) {
|
||||
public FakeMediaSource(Timeline timeline, Object manifest, Format... formats) {
|
||||
this.timeline = timeline;
|
||||
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<>();
|
||||
}
|
||||
|
||||
@ -228,9 +375,8 @@ public final class ExoPlayerTest extends TestCase {
|
||||
Assertions.checkIndex(index, 0, timeline.getPeriodCount());
|
||||
assertTrue(preparedSource);
|
||||
assertFalse(releasedSource);
|
||||
assertEquals(0, index);
|
||||
assertEquals(0, positionUs);
|
||||
FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(format);
|
||||
FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray);
|
||||
activeMediaPeriods.add(mediaPeriod);
|
||||
return mediaPeriod;
|
||||
}
|
||||
@ -239,8 +385,9 @@ public final class ExoPlayerTest extends TestCase {
|
||||
public void releasePeriod(MediaPeriod mediaPeriod) {
|
||||
assertTrue(preparedSource);
|
||||
assertFalse(releasedSource);
|
||||
assertTrue(activeMediaPeriods.remove(mediaPeriod));
|
||||
((FakeMediaPeriod) mediaPeriod).release();
|
||||
FakeMediaPeriod fakeMediaPeriod = (FakeMediaPeriod) mediaPeriod;
|
||||
assertTrue(activeMediaPeriods.remove(fakeMediaPeriod));
|
||||
fakeMediaPeriod.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -259,12 +406,12 @@ public final class ExoPlayerTest extends TestCase {
|
||||
*/
|
||||
private static final class FakeMediaPeriod implements MediaPeriod {
|
||||
|
||||
private final TrackGroup trackGroup;
|
||||
private final TrackGroupArray trackGroupArray;
|
||||
|
||||
private boolean preparedPeriod;
|
||||
|
||||
public FakeMediaPeriod(Format format) {
|
||||
trackGroup = new TrackGroup(format);
|
||||
public FakeMediaPeriod(TrackGroupArray trackGroupArray) {
|
||||
this.trackGroupArray = trackGroupArray;
|
||||
}
|
||||
|
||||
public void release() {
|
||||
@ -286,26 +433,29 @@ public final class ExoPlayerTest extends TestCase {
|
||||
@Override
|
||||
public TrackGroupArray getTrackGroups() {
|
||||
assertTrue(preparedPeriod);
|
||||
return new TrackGroupArray(trackGroup);
|
||||
return trackGroupArray;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
|
||||
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
|
||||
assertTrue(preparedPeriod);
|
||||
assertEquals(1, selections.length);
|
||||
assertEquals(1, mayRetainStreamFlags.length);
|
||||
assertEquals(1, streams.length);
|
||||
assertEquals(1, streamResetFlags.length);
|
||||
assertEquals(0, positionUs);
|
||||
if (streams[0] != null && (selections[0] == null || !mayRetainStreamFlags[0])) {
|
||||
streams[0] = null;
|
||||
int rendererCount = selections.length;
|
||||
for (int i = 0; i < rendererCount; i++) {
|
||||
if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
|
||||
streams[i] = null;
|
||||
}
|
||||
}
|
||||
if (streams[0] == null && selections[0] != null) {
|
||||
FakeSampleStream stream = new FakeSampleStream(trackGroup.getFormat(0));
|
||||
assertEquals(trackGroup, selections[0].getTrackGroup());
|
||||
streams[0] = stream;
|
||||
streamResetFlags[0] = true;
|
||||
for (int i = 0; i < rendererCount; i++) {
|
||||
if (streams[i] == null && selections[i] != null) {
|
||||
TrackSelection selection = selections[i];
|
||||
assertEquals(1, selection.length());
|
||||
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;
|
||||
}
|
||||
@ -332,7 +482,7 @@ public final class ExoPlayerTest extends TestCase {
|
||||
@Override
|
||||
public long getNextLoadPositionUs() {
|
||||
assertTrue(preparedPeriod);
|
||||
return 0;
|
||||
return C.TIME_END_OF_SOURCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -352,7 +502,6 @@ public final class ExoPlayerTest extends TestCase {
|
||||
private final Format format;
|
||||
|
||||
private boolean readFormat;
|
||||
private boolean readEndOfStream;
|
||||
|
||||
public FakeSampleStream(Format format) {
|
||||
this.format = format;
|
||||
@ -365,15 +514,14 @@ public final class ExoPlayerTest extends TestCase {
|
||||
|
||||
@Override
|
||||
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) {
|
||||
Assertions.checkState(!readEndOfStream);
|
||||
if (readFormat) {
|
||||
if (buffer == null || !readFormat) {
|
||||
formatHolder.format = format;
|
||||
readFormat = true;
|
||||
return C.RESULT_FORMAT_READ;
|
||||
} else {
|
||||
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
|
||||
readEndOfStream = true;
|
||||
return C.RESULT_BUFFER_READ;
|
||||
}
|
||||
formatHolder.format = format;
|
||||
readFormat = true;
|
||||
return C.RESULT_FORMAT_READ;
|
||||
}
|
||||
|
||||
@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
|
||||
* given {@link Format} then a buffer with the end of stream flag set.
|
||||
* Fake {@link Renderer} that supports any format with the matching MIME type. The renderer
|
||||
* 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 boolean isEnded;
|
||||
public int positionResetCount;
|
||||
public int formatReadCount;
|
||||
public int bufferReadCount;
|
||||
public boolean isEnded;
|
||||
|
||||
public FakeVideoRenderer(Format expectedFormat) {
|
||||
super(C.TRACK_TYPE_VIDEO);
|
||||
public FakeRenderer(Format expectedFormat) {
|
||||
super(expectedFormat == null ? C.TRACK_TYPE_UNKNOWN
|
||||
: MimeTypes.getTrackType(expectedFormat.sampleMimeType));
|
||||
this.expectedFormat = expectedFormat;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
|
||||
positionResetCount++;
|
||||
isEnded = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
|
||||
if (isEnded) {
|
||||
@ -411,20 +569,23 @@ public final class ExoPlayerTest extends TestCase {
|
||||
|
||||
// Verify the format matches the expected format.
|
||||
FormatHolder formatHolder = new FormatHolder();
|
||||
readSource(formatHolder, null);
|
||||
assertEquals(expectedFormat, formatHolder.format);
|
||||
|
||||
// Verify that we get an end-of-stream buffer.
|
||||
DecoderInputBuffer buffer =
|
||||
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
|
||||
readSource(null, buffer);
|
||||
assertTrue(buffer.isEndOfStream());
|
||||
isEnded = true;
|
||||
int result = readSource(formatHolder, buffer);
|
||||
if (result == C.RESULT_FORMAT_READ) {
|
||||
formatReadCount++;
|
||||
assertEquals(expectedFormat, formatHolder.format);
|
||||
} else if (result == C.RESULT_BUFFER_READ) {
|
||||
bufferReadCount++;
|
||||
if (buffer.isEndOfStream()) {
|
||||
isEnded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return isEnded;
|
||||
return isSourceReady();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -434,7 +595,21 @@ public final class ExoPlayerTest extends TestCase {
|
||||
|
||||
@Override
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -59,8 +59,8 @@ public final class FormatTest extends TestCase {
|
||||
DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2);
|
||||
byte[] projectionData = new byte[] {1, 2, 3};
|
||||
Metadata metadata = new Metadata(
|
||||
new TextInformationFrame("id1", "description1"),
|
||||
new TextInformationFrame("id2", "description2"));
|
||||
new TextInformationFrame("id1", "description1", "value1"),
|
||||
new TextInformationFrame("id2", "description2", "value2"));
|
||||
|
||||
Format formatToParcel = new Format("id", MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null,
|
||||
1024, 2048, 1920, 1080, 24, 90, 2, projectionData, C.STEREO_MODE_TOP_BOTTOM, 6, 44100,
|
||||
|
@ -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}));
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -21,9 +21,9 @@ import com.google.android.exoplayer2.metadata.MetadataDecoderException;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* Test for {@link Id3Decoder}
|
||||
* Test for {@link Id3Decoder}.
|
||||
*/
|
||||
public class Id3DecoderTest extends TestCase {
|
||||
public final class Id3DecoderTest extends TestCase {
|
||||
|
||||
public void testDecodeTxxxFrame() throws MetadataDecoderException {
|
||||
byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 41, 84, 88, 88, 88, 0, 0, 0, 31, 0, 0,
|
||||
@ -32,9 +32,10 @@ public class Id3DecoderTest extends TestCase {
|
||||
Id3Decoder decoder = new Id3Decoder();
|
||||
Metadata metadata = decoder.decode(rawId3, rawId3.length);
|
||||
assertEquals(1, metadata.length());
|
||||
TxxxFrame txxxFrame = (TxxxFrame) metadata.get(0);
|
||||
assertEquals("", txxxFrame.description);
|
||||
assertEquals("mdialog_VINDICO1527664_start", txxxFrame.value);
|
||||
TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0);
|
||||
assertEquals("TXXX", textInformationFrame.id);
|
||||
assertEquals("", textInformationFrame.description);
|
||||
assertEquals("mdialog_VINDICO1527664_start", textInformationFrame.value);
|
||||
}
|
||||
|
||||
public void testDecodeApicFrame() throws MetadataDecoderException {
|
||||
@ -60,7 +61,19 @@ public class Id3DecoderTest extends TestCase {
|
||||
assertEquals(1, metadata.length());
|
||||
TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0);
|
||||
assertEquals("TIT2", textInformationFrame.id);
|
||||
assertEquals("Hello World", textInformationFrame.description);
|
||||
assertNull(textInformationFrame.description);
|
||||
assertEquals("Hello World", textInformationFrame.value);
|
||||
}
|
||||
|
||||
public void testDecodePrivFrame() throws MetadataDecoderException {
|
||||
byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 19, 80, 82, 73, 86, 0, 0, 0, 9, 0, 0,
|
||||
116, 101, 115, 116, 0, 1, 2, 3, 4};
|
||||
Id3Decoder decoder = new Id3Decoder();
|
||||
Metadata metadata = decoder.decode(rawId3, rawId3.length);
|
||||
assertEquals(1, metadata.length());
|
||||
PrivFrame privFrame = (PrivFrame) metadata.get(0);
|
||||
assertEquals("test", privFrame.owner);
|
||||
MoreAsserts.assertEquals(new byte[] {1, 2, 3, 4}, privFrame.privateData);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -29,13 +29,13 @@ public class RepresentationTest extends TestCase {
|
||||
String uri = "http://www.google.com";
|
||||
SegmentBase base = new SingleSegmentBase(new RangedUri(null, 0, 1), 1, 0, 1, 1);
|
||||
Format format = Format.createVideoContainerFormat("0", MimeTypes.APPLICATION_MP4, null,
|
||||
MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null);
|
||||
MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null, 0);
|
||||
Representation representation = Representation.newInstance("test_stream_1", 3, format, uri,
|
||||
base);
|
||||
assertEquals("test_stream_1.0.3", representation.getCacheKey());
|
||||
|
||||
format = Format.createVideoContainerFormat("150", MimeTypes.APPLICATION_MP4, null,
|
||||
MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null);
|
||||
MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null, 0);
|
||||
representation = Representation.newInstance("test_stream_1", Representation.REVISION_ID_DEFAULT,
|
||||
format, uri, base);
|
||||
assertEquals("test_stream_1.150.-1", representation.getCacheKey());
|
||||
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.hls.playlist;
|
||||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
@ -29,70 +30,86 @@ import junit.framework.TestCase;
|
||||
*/
|
||||
public class HlsMasterPlaylistParserTest extends TestCase {
|
||||
|
||||
public void testParseMasterPlaylist() {
|
||||
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
|
||||
String playlistString = "#EXTM3U\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
|
||||
+ "http://example.com/low.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n"
|
||||
+ "http://example.com/spaces_in_codecs.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=384x160\n"
|
||||
+ "http://example.com/mid.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=7680000\n"
|
||||
+ "http://example.com/hi.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n"
|
||||
+ "http://example.com/audio-only.m3u8";
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(
|
||||
playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
|
||||
private static final String PLAYLIST_URI = "https://example.com/test.m3u8";
|
||||
|
||||
private static final String MASTER_PLAYLIST = " #EXTM3U \n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
|
||||
+ "http://example.com/low.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n"
|
||||
+ "http://example.com/spaces_in_codecs.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=384x160\n"
|
||||
+ "http://example.com/mid.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=7680000\n"
|
||||
+ "http://example.com/hi.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n"
|
||||
+ "http://example.com/audio-only.m3u8";
|
||||
|
||||
private static final String PLAYLIST_WITH_INVALID_HEADER = "#EXTMU3\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
|
||||
+ "http://example.com/low.m3u8\n";
|
||||
|
||||
public void testParseMasterPlaylist() throws IOException{
|
||||
HlsPlaylist playlist = parsePlaylist(PLAYLIST_URI, MASTER_PLAYLIST);
|
||||
assertNotNull(playlist);
|
||||
assertEquals(HlsPlaylist.TYPE_MASTER, playlist.type);
|
||||
|
||||
HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist;
|
||||
|
||||
List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
|
||||
assertNotNull(variants);
|
||||
assertEquals(5, variants.size());
|
||||
|
||||
assertEquals(1280000, variants.get(0).format.bitrate);
|
||||
assertNotNull(variants.get(0).format.codecs);
|
||||
assertEquals("mp4a.40.2,avc1.66.30", variants.get(0).format.codecs);
|
||||
assertEquals(304, variants.get(0).format.width);
|
||||
assertEquals(128, variants.get(0).format.height);
|
||||
assertEquals("http://example.com/low.m3u8", variants.get(0).url);
|
||||
|
||||
assertEquals(1280000, variants.get(1).format.bitrate);
|
||||
assertNotNull(variants.get(1).format.codecs);
|
||||
assertEquals("mp4a.40.2 , avc1.66.30 ", variants.get(1).format.codecs);
|
||||
assertEquals("http://example.com/spaces_in_codecs.m3u8", variants.get(1).url);
|
||||
|
||||
assertEquals(2560000, variants.get(2).format.bitrate);
|
||||
assertEquals(null, variants.get(2).format.codecs);
|
||||
assertEquals(384, variants.get(2).format.width);
|
||||
assertEquals(160, variants.get(2).format.height);
|
||||
assertEquals("http://example.com/mid.m3u8", variants.get(2).url);
|
||||
|
||||
assertEquals(7680000, variants.get(3).format.bitrate);
|
||||
assertEquals(null, variants.get(3).format.codecs);
|
||||
assertEquals(Format.NO_VALUE, variants.get(3).format.width);
|
||||
assertEquals(Format.NO_VALUE, variants.get(3).format.height);
|
||||
assertEquals("http://example.com/hi.m3u8", variants.get(3).url);
|
||||
|
||||
assertEquals(65000, variants.get(4).format.bitrate);
|
||||
assertNotNull(variants.get(4).format.codecs);
|
||||
assertEquals("mp4a.40.5", variants.get(4).format.codecs);
|
||||
assertEquals(Format.NO_VALUE, variants.get(4).format.width);
|
||||
assertEquals(Format.NO_VALUE, variants.get(4).format.height);
|
||||
assertEquals("http://example.com/audio-only.m3u8", variants.get(4).url);
|
||||
}
|
||||
|
||||
public void testPlaylistWithInvalidHeader() throws IOException {
|
||||
try {
|
||||
HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream);
|
||||
assertNotNull(playlist);
|
||||
assertEquals(HlsPlaylist.TYPE_MASTER, playlist.type);
|
||||
|
||||
HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist;
|
||||
|
||||
List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
|
||||
assertNotNull(variants);
|
||||
assertEquals(5, variants.size());
|
||||
|
||||
assertEquals(1280000, variants.get(0).format.bitrate);
|
||||
assertNotNull(variants.get(0).format.codecs);
|
||||
assertEquals("mp4a.40.2,avc1.66.30", variants.get(0).format.codecs);
|
||||
assertEquals(304, variants.get(0).format.width);
|
||||
assertEquals(128, variants.get(0).format.height);
|
||||
assertEquals("http://example.com/low.m3u8", variants.get(0).url);
|
||||
|
||||
assertEquals(1280000, variants.get(1).format.bitrate);
|
||||
assertNotNull(variants.get(1).format.codecs);
|
||||
assertEquals("mp4a.40.2 , avc1.66.30 ", variants.get(1).format.codecs);
|
||||
assertEquals("http://example.com/spaces_in_codecs.m3u8", variants.get(1).url);
|
||||
|
||||
assertEquals(2560000, variants.get(2).format.bitrate);
|
||||
assertEquals(null, variants.get(2).format.codecs);
|
||||
assertEquals(384, variants.get(2).format.width);
|
||||
assertEquals(160, variants.get(2).format.height);
|
||||
assertEquals("http://example.com/mid.m3u8", variants.get(2).url);
|
||||
|
||||
assertEquals(7680000, variants.get(3).format.bitrate);
|
||||
assertEquals(null, variants.get(3).format.codecs);
|
||||
assertEquals(Format.NO_VALUE, variants.get(3).format.width);
|
||||
assertEquals(Format.NO_VALUE, variants.get(3).format.height);
|
||||
assertEquals("http://example.com/hi.m3u8", variants.get(3).url);
|
||||
|
||||
assertEquals(65000, variants.get(4).format.bitrate);
|
||||
assertNotNull(variants.get(4).format.codecs);
|
||||
assertEquals("mp4a.40.5", variants.get(4).format.codecs);
|
||||
assertEquals(Format.NO_VALUE, variants.get(4).format.width);
|
||||
assertEquals(Format.NO_VALUE, variants.get(4).format.height);
|
||||
assertEquals("http://example.com/audio-only.m3u8", variants.get(4).url);
|
||||
} catch (IOException exception) {
|
||||
fail(exception.getMessage());
|
||||
parsePlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER);
|
||||
fail("Expected exception not thrown.");
|
||||
} catch (ParserException e) {
|
||||
// Expected due to invalid header.
|
||||
}
|
||||
}
|
||||
|
||||
private static HlsPlaylist parsePlaylist(String uri, String playlistString) throws IOException {
|
||||
Uri playlistUri = Uri.parse(uri);
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(
|
||||
playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
|
||||
return new HlsPlaylistParser().parse(playlistUri, inputStream);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls.playlist;
|
||||
|
||||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@ -73,59 +74,64 @@ public class HlsMediaPlaylistParserTest extends TestCase {
|
||||
|
||||
assertEquals(2679, mediaPlaylist.mediaSequence);
|
||||
assertEquals(3, mediaPlaylist.version);
|
||||
assertEquals(true, mediaPlaylist.hasEndTag);
|
||||
List<HlsMediaPlaylist.Segment> segments = mediaPlaylist.segments;
|
||||
assertTrue(mediaPlaylist.hasEndTag);
|
||||
List<Segment> segments = mediaPlaylist.segments;
|
||||
assertNotNull(segments);
|
||||
assertEquals(5, segments.size());
|
||||
|
||||
assertEquals(4, segments.get(0).discontinuitySequenceNumber);
|
||||
assertEquals(7975000, segments.get(0).durationUs);
|
||||
assertEquals(false, segments.get(0).isEncrypted);
|
||||
assertEquals(null, segments.get(0).encryptionKeyUri);
|
||||
assertEquals(null, segments.get(0).encryptionIV);
|
||||
assertEquals(51370, segments.get(0).byterangeLength);
|
||||
assertEquals(0, segments.get(0).byterangeOffset);
|
||||
assertEquals("https://priv.example.com/fileSequence2679.ts", segments.get(0).url);
|
||||
Segment segment = segments.get(0);
|
||||
assertEquals(4, mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence);
|
||||
assertEquals(7975000, segment.durationUs);
|
||||
assertFalse(segment.isEncrypted);
|
||||
assertEquals(null, segment.encryptionKeyUri);
|
||||
assertEquals(null, segment.encryptionIV);
|
||||
assertEquals(51370, segment.byterangeLength);
|
||||
assertEquals(0, segment.byterangeOffset);
|
||||
assertEquals("https://priv.example.com/fileSequence2679.ts", segment.url);
|
||||
|
||||
assertEquals(4, segments.get(1).discontinuitySequenceNumber);
|
||||
assertEquals(7975000, segments.get(1).durationUs);
|
||||
assertEquals(true, segments.get(1).isEncrypted);
|
||||
assertEquals("https://priv.example.com/key.php?r=2680", segments.get(1).encryptionKeyUri);
|
||||
assertEquals("0x1566B", segments.get(1).encryptionIV);
|
||||
assertEquals(51501, segments.get(1).byterangeLength);
|
||||
assertEquals(2147483648L, segments.get(1).byterangeOffset);
|
||||
assertEquals("https://priv.example.com/fileSequence2680.ts", segments.get(1).url);
|
||||
segment = segments.get(1);
|
||||
assertEquals(0, segment.relativeDiscontinuitySequence);
|
||||
assertEquals(7975000, segment.durationUs);
|
||||
assertTrue(segment.isEncrypted);
|
||||
assertEquals("https://priv.example.com/key.php?r=2680", segment.encryptionKeyUri);
|
||||
assertEquals("0x1566B", segment.encryptionIV);
|
||||
assertEquals(51501, segment.byterangeLength);
|
||||
assertEquals(2147483648L, segment.byterangeOffset);
|
||||
assertEquals("https://priv.example.com/fileSequence2680.ts", segment.url);
|
||||
|
||||
assertEquals(4, segments.get(2).discontinuitySequenceNumber);
|
||||
assertEquals(7941000, segments.get(2).durationUs);
|
||||
assertEquals(false, segments.get(2).isEncrypted);
|
||||
assertEquals(null, segments.get(2).encryptionKeyUri);
|
||||
assertEquals(null, segments.get(2).encryptionIV);
|
||||
assertEquals(51501, segments.get(2).byterangeLength);
|
||||
assertEquals(2147535149L, segments.get(2).byterangeOffset);
|
||||
assertEquals("https://priv.example.com/fileSequence2681.ts", segments.get(2).url);
|
||||
segment = segments.get(2);
|
||||
assertEquals(0, segment.relativeDiscontinuitySequence);
|
||||
assertEquals(7941000, segment.durationUs);
|
||||
assertFalse(segment.isEncrypted);
|
||||
assertEquals(null, segment.encryptionKeyUri);
|
||||
assertEquals(null, segment.encryptionIV);
|
||||
assertEquals(51501, segment.byterangeLength);
|
||||
assertEquals(2147535149L, segment.byterangeOffset);
|
||||
assertEquals("https://priv.example.com/fileSequence2681.ts", segment.url);
|
||||
|
||||
assertEquals(5, segments.get(3).discontinuitySequenceNumber);
|
||||
assertEquals(7975000, segments.get(3).durationUs);
|
||||
assertEquals(true, segments.get(3).isEncrypted);
|
||||
assertEquals("https://priv.example.com/key.php?r=2682", segments.get(3).encryptionKeyUri);
|
||||
segment = segments.get(3);
|
||||
assertEquals(1, segment.relativeDiscontinuitySequence);
|
||||
assertEquals(7975000, segment.durationUs);
|
||||
assertTrue(segment.isEncrypted);
|
||||
assertEquals("https://priv.example.com/key.php?r=2682", segment.encryptionKeyUri);
|
||||
// 0xA7A == 2682.
|
||||
assertNotNull(segments.get(3).encryptionIV);
|
||||
assertEquals("A7A", segments.get(3).encryptionIV.toUpperCase(Locale.getDefault()));
|
||||
assertEquals(51740, segments.get(3).byterangeLength);
|
||||
assertEquals(2147586650L, segments.get(3).byterangeOffset);
|
||||
assertEquals("https://priv.example.com/fileSequence2682.ts", segments.get(3).url);
|
||||
assertNotNull(segment.encryptionIV);
|
||||
assertEquals("A7A", segment.encryptionIV.toUpperCase(Locale.getDefault()));
|
||||
assertEquals(51740, segment.byterangeLength);
|
||||
assertEquals(2147586650L, segment.byterangeOffset);
|
||||
assertEquals("https://priv.example.com/fileSequence2682.ts", segment.url);
|
||||
|
||||
assertEquals(5, segments.get(4).discontinuitySequenceNumber);
|
||||
assertEquals(7975000, segments.get(4).durationUs);
|
||||
assertEquals(true, segments.get(4).isEncrypted);
|
||||
assertEquals("https://priv.example.com/key.php?r=2682", segments.get(4).encryptionKeyUri);
|
||||
segment = segments.get(4);
|
||||
assertEquals(1, segment.relativeDiscontinuitySequence);
|
||||
assertEquals(7975000, segment.durationUs);
|
||||
assertTrue(segment.isEncrypted);
|
||||
assertEquals("https://priv.example.com/key.php?r=2682", segment.encryptionKeyUri);
|
||||
// 0xA7B == 2683.
|
||||
assertNotNull(segments.get(4).encryptionIV);
|
||||
assertEquals("A7B", segments.get(4).encryptionIV.toUpperCase(Locale.getDefault()));
|
||||
assertEquals(C.LENGTH_UNSET, segments.get(4).byterangeLength);
|
||||
assertEquals(0, segments.get(4).byterangeOffset);
|
||||
assertEquals("https://priv.example.com/fileSequence2683.ts", segments.get(4).url);
|
||||
assertNotNull(segment.encryptionIV);
|
||||
assertEquals("A7B", segment.encryptionIV.toUpperCase(Locale.getDefault()));
|
||||
assertEquals(C.LENGTH_UNSET, segment.byterangeLength);
|
||||
assertEquals(0, segment.byterangeOffset);
|
||||
assertEquals("https://priv.example.com/fileSequence2683.ts", segment.url);
|
||||
} catch (IOException exception) {
|
||||
fail(exception.getMessage());
|
||||
}
|
||||
|
@ -27,7 +27,9 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
/** Unit tests for {@link CacheDataSource}. */
|
||||
/**
|
||||
* Unit tests for {@link CacheDataSource}.
|
||||
*/
|
||||
public class CacheDataSourceTest extends InstrumentationTestCase {
|
||||
|
||||
private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
|
||||
|
181
library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java
vendored
Normal file
181
library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java
vendored
Normal 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());
|
||||
}
|
||||
|
||||
}
|
@ -163,7 +163,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||
|
||||
public void testEncryption() throws Exception {
|
||||
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
|
||||
byte[] key2 = "bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
|
||||
byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key
|
||||
|
||||
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
|
||||
new CachedContentIndex(cacheDir, key));
|
||||
@ -181,7 +181,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||
// Assert file content is different
|
||||
FileInputStream fis1 = new FileInputStream(file1);
|
||||
FileInputStream fis2 = new FileInputStream(file2);
|
||||
for (int b; (b = fis1.read()) == fis2.read();) {
|
||||
for (int b; (b = fis1.read()) == fis2.read(); ) {
|
||||
assertTrue(b != -1);
|
||||
}
|
||||
|
||||
@ -205,6 +205,12 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
||||
// Non encrypted index file can be read even when encryption key provided.
|
||||
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir),
|
||||
new CachedContentIndex(cacheDir, key));
|
||||
|
||||
// Test multiple store() calls
|
||||
CachedContentIndex index = new CachedContentIndex(cacheDir, key);
|
||||
index.addNew(new CachedContent(15, "key3", 110));
|
||||
index.store();
|
||||
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir, key));
|
||||
}
|
||||
|
||||
private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentIndex index2)
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -16,12 +16,16 @@
|
||||
package com.google.android.exoplayer2.upstream.cache;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import android.test.MoreAsserts;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.NavigableSet;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@ -46,9 +50,9 @@ public class SimpleCacheTest extends InstrumentationTestCase {
|
||||
public void testCommittingOneFile() throws Exception {
|
||||
SimpleCache simpleCache = getSimpleCache();
|
||||
|
||||
CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0);
|
||||
assertFalse(cacheSpan.isCached);
|
||||
assertTrue(cacheSpan.isOpenEnded());
|
||||
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
|
||||
assertFalse(cacheSpan1.isCached);
|
||||
assertTrue(cacheSpan1.isOpenEnded());
|
||||
|
||||
assertNull(simpleCache.startReadWriteNonBlocking(KEY_1, 0));
|
||||
|
||||
@ -58,20 +62,33 @@ public class SimpleCacheTest extends InstrumentationTestCase {
|
||||
assertEquals(0, simpleCache.getCacheSpace());
|
||||
assertEquals(0, cacheDir.listFiles().length);
|
||||
|
||||
addCache(simpleCache, 0, 15);
|
||||
addCache(simpleCache, KEY_1, 0, 15);
|
||||
|
||||
Set<String> cachedKeys = simpleCache.getKeys();
|
||||
assertEquals(1, cachedKeys.size());
|
||||
assertTrue(cachedKeys.contains(KEY_1));
|
||||
cachedSpans = simpleCache.getCachedSpans(KEY_1);
|
||||
assertEquals(1, cachedSpans.size());
|
||||
assertTrue(cachedSpans.contains(cacheSpan));
|
||||
assertTrue(cachedSpans.contains(cacheSpan1));
|
||||
assertEquals(15, simpleCache.getCacheSpace());
|
||||
|
||||
cacheSpan = simpleCache.startReadWrite(KEY_1, 0);
|
||||
assertTrue(cacheSpan.isCached);
|
||||
assertFalse(cacheSpan.isOpenEnded());
|
||||
assertEquals(15, cacheSpan.length);
|
||||
simpleCache.releaseHoleSpan(cacheSpan1);
|
||||
|
||||
CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);
|
||||
assertTrue(cacheSpan2.isCached);
|
||||
assertFalse(cacheSpan2.isOpenEnded());
|
||||
assertEquals(15, cacheSpan2.length);
|
||||
assertCachedDataReadCorrect(cacheSpan2);
|
||||
}
|
||||
|
||||
public void testReadCacheWithoutReleasingWriteCacheSpan() throws Exception {
|
||||
SimpleCache simpleCache = getSimpleCache();
|
||||
|
||||
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
|
||||
addCache(simpleCache, KEY_1, 0, 15);
|
||||
CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);
|
||||
assertCachedDataReadCorrect(cacheSpan2);
|
||||
simpleCache.releaseHoleSpan(cacheSpan1);
|
||||
}
|
||||
|
||||
public void testSetGetLength() throws Exception {
|
||||
@ -83,12 +100,12 @@ public class SimpleCacheTest extends InstrumentationTestCase {
|
||||
|
||||
simpleCache.startReadWrite(KEY_1, 0);
|
||||
|
||||
addCache(simpleCache, 0, 15);
|
||||
addCache(simpleCache, KEY_1, 0, 15);
|
||||
|
||||
simpleCache.setContentLength(KEY_1, 150);
|
||||
assertEquals(150, simpleCache.getContentLength(KEY_1));
|
||||
|
||||
addCache(simpleCache, 140, 10);
|
||||
addCache(simpleCache, KEY_1, 140, 10);
|
||||
|
||||
// Check if values are kept after cache is reloaded.
|
||||
SimpleCache simpleCache2 = getSimpleCache();
|
||||
@ -107,16 +124,109 @@ public class SimpleCacheTest extends InstrumentationTestCase {
|
||||
assertEquals(150, simpleCache2.getContentLength(KEY_1));
|
||||
}
|
||||
|
||||
public void testReloadCache() throws Exception {
|
||||
SimpleCache simpleCache = getSimpleCache();
|
||||
|
||||
// write data
|
||||
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
|
||||
addCache(simpleCache, KEY_1, 0, 15);
|
||||
simpleCache.releaseHoleSpan(cacheSpan1);
|
||||
|
||||
// Reload cache
|
||||
simpleCache = getSimpleCache();
|
||||
|
||||
// read data back
|
||||
CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);
|
||||
assertCachedDataReadCorrect(cacheSpan2);
|
||||
}
|
||||
|
||||
public void testEncryptedIndex() throws Exception {
|
||||
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
|
||||
SimpleCache simpleCache = getEncryptedSimpleCache(key);
|
||||
|
||||
// write data
|
||||
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
|
||||
addCache(simpleCache, KEY_1, 0, 15);
|
||||
simpleCache.releaseHoleSpan(cacheSpan1);
|
||||
|
||||
// Reload cache
|
||||
simpleCache = getEncryptedSimpleCache(key);
|
||||
|
||||
// read data back
|
||||
CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);
|
||||
assertCachedDataReadCorrect(cacheSpan2);
|
||||
}
|
||||
|
||||
public void testEncryptedIndexWrongKey() throws Exception {
|
||||
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
|
||||
SimpleCache simpleCache = getEncryptedSimpleCache(key);
|
||||
|
||||
// write data
|
||||
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
|
||||
addCache(simpleCache, KEY_1, 0, 15);
|
||||
simpleCache.releaseHoleSpan(cacheSpan1);
|
||||
|
||||
// Reload cache
|
||||
byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key
|
||||
simpleCache = getEncryptedSimpleCache(key2);
|
||||
|
||||
// Cache should be cleared
|
||||
assertEquals(0, simpleCache.getKeys().size());
|
||||
assertEquals(0, cacheDir.listFiles().length);
|
||||
}
|
||||
|
||||
public void testEncryptedIndexLostKey() throws Exception {
|
||||
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
|
||||
SimpleCache simpleCache = getEncryptedSimpleCache(key);
|
||||
|
||||
// write data
|
||||
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
|
||||
addCache(simpleCache, KEY_1, 0, 15);
|
||||
simpleCache.releaseHoleSpan(cacheSpan1);
|
||||
|
||||
// Reload cache
|
||||
simpleCache = getSimpleCache();
|
||||
|
||||
// Cache should be cleared
|
||||
assertEquals(0, simpleCache.getKeys().size());
|
||||
assertEquals(0, cacheDir.listFiles().length);
|
||||
}
|
||||
|
||||
private SimpleCache getSimpleCache() {
|
||||
return new SimpleCache(cacheDir, new NoOpCacheEvictor());
|
||||
}
|
||||
|
||||
private void addCache(SimpleCache simpleCache, int position, int length) throws IOException {
|
||||
File file = simpleCache.startFile(KEY_1, position, length);
|
||||
private SimpleCache getEncryptedSimpleCache(byte[] secretKey) {
|
||||
return new SimpleCache(cacheDir, new NoOpCacheEvictor(), secretKey);
|
||||
}
|
||||
|
||||
private static void addCache(SimpleCache simpleCache, String key, int position, int length)
|
||||
throws IOException {
|
||||
File file = simpleCache.startFile(key, position, length);
|
||||
FileOutputStream fos = new FileOutputStream(file);
|
||||
fos.write(new byte[length]);
|
||||
fos.close();
|
||||
try {
|
||||
fos.write(generateData(key, position, length));
|
||||
} finally {
|
||||
fos.close();
|
||||
}
|
||||
simpleCache.commitFile(file);
|
||||
}
|
||||
|
||||
private static void assertCachedDataReadCorrect(CacheSpan cacheSpan) throws IOException {
|
||||
assertTrue(cacheSpan.isCached);
|
||||
byte[] expected = generateData(cacheSpan.key, (int) cacheSpan.position, (int) cacheSpan.length);
|
||||
FileInputStream inputStream = new FileInputStream(cacheSpan.file);
|
||||
try {
|
||||
MoreAsserts.assertEquals(expected, Util.toByteArray(inputStream));
|
||||
} finally {
|
||||
inputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] generateData(String key, int position, int length) {
|
||||
byte[] bytes = new byte[length];
|
||||
new Random((long) (key.hashCode() ^ position)).nextBytes(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -371,6 +371,73 @@ public class ParsableByteArrayTest extends TestCase {
|
||||
assertNull(parser.readLine());
|
||||
}
|
||||
|
||||
public void testReadNullTerminatedStringWithLengths() {
|
||||
byte[] bytes = new byte[] {
|
||||
'f', 'o', 'o', 0, 'b', 'a', 'r', 0
|
||||
};
|
||||
// Test with lengths that match NUL byte positions.
|
||||
ParsableByteArray parser = new ParsableByteArray(bytes);
|
||||
assertEquals("foo", parser.readNullTerminatedString(4));
|
||||
assertEquals(4, parser.getPosition());
|
||||
assertEquals("bar", parser.readNullTerminatedString(4));
|
||||
assertEquals(8, parser.getPosition());
|
||||
assertNull(parser.readNullTerminatedString());
|
||||
// Test with lengths that do not match NUL byte positions.
|
||||
parser = new ParsableByteArray(bytes);
|
||||
assertEquals("fo", parser.readNullTerminatedString(2));
|
||||
assertEquals(2, parser.getPosition());
|
||||
assertEquals("o", parser.readNullTerminatedString(2));
|
||||
assertEquals(4, parser.getPosition());
|
||||
assertEquals("bar", parser.readNullTerminatedString(3));
|
||||
assertEquals(7, parser.getPosition());
|
||||
assertEquals("", parser.readNullTerminatedString(1));
|
||||
assertEquals(8, parser.getPosition());
|
||||
assertNull(parser.readNullTerminatedString());
|
||||
// Test with limit at NUL
|
||||
parser = new ParsableByteArray(bytes, 4);
|
||||
assertEquals("foo", parser.readNullTerminatedString(4));
|
||||
assertEquals(4, parser.getPosition());
|
||||
assertNull(parser.readNullTerminatedString());
|
||||
// Test with limit before NUL
|
||||
parser = new ParsableByteArray(bytes, 3);
|
||||
assertEquals("foo", parser.readNullTerminatedString(3));
|
||||
assertEquals(3, parser.getPosition());
|
||||
assertNull(parser.readNullTerminatedString());
|
||||
}
|
||||
|
||||
public void testReadNullTerminatedString() {
|
||||
byte[] bytes = new byte[] {
|
||||
'f', 'o', 'o', 0, 'b', 'a', 'r', 0
|
||||
};
|
||||
// Test normal case.
|
||||
ParsableByteArray parser = new ParsableByteArray(bytes);
|
||||
assertEquals("foo", parser.readNullTerminatedString());
|
||||
assertEquals(4, parser.getPosition());
|
||||
assertEquals("bar", parser.readNullTerminatedString());
|
||||
assertEquals(8, parser.getPosition());
|
||||
assertNull(parser.readNullTerminatedString());
|
||||
// Test with limit at NUL.
|
||||
parser = new ParsableByteArray(bytes, 4);
|
||||
assertEquals("foo", parser.readNullTerminatedString());
|
||||
assertEquals(4, parser.getPosition());
|
||||
assertNull(parser.readNullTerminatedString());
|
||||
// Test with limit before NUL.
|
||||
parser = new ParsableByteArray(bytes, 3);
|
||||
assertEquals("foo", parser.readNullTerminatedString());
|
||||
assertEquals(3, parser.getPosition());
|
||||
assertNull(parser.readNullTerminatedString());
|
||||
}
|
||||
|
||||
public void testReadNullTerminatedStringWithoutEndingNull() {
|
||||
byte[] bytes = new byte[] {
|
||||
'f', 'o', 'o', 0, 'b', 'a', 'r'
|
||||
};
|
||||
ParsableByteArray parser = new ParsableByteArray(bytes);
|
||||
assertEquals("foo", parser.readNullTerminatedString());
|
||||
assertEquals("bar", parser.readNullTerminatedString());
|
||||
assertNull(parser.readNullTerminatedString());
|
||||
}
|
||||
|
||||
public void testReadSingleLineWithoutEndingTrail() {
|
||||
byte[] bytes = new byte[] {
|
||||
'f', 'o', 'o'
|
||||
|
@ -28,6 +28,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
||||
|
||||
private final int trackType;
|
||||
|
||||
private RendererConfiguration configuration;
|
||||
private int index;
|
||||
private int state;
|
||||
private SampleStream stream;
|
||||
@ -70,9 +71,11 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void enable(Format[] formats, SampleStream stream, long positionUs, boolean joining,
|
||||
long offsetUs) throws ExoPlaybackException {
|
||||
public final void enable(RendererConfiguration configuration, Format[] formats,
|
||||
SampleStream stream, long positionUs, boolean joining, long offsetUs)
|
||||
throws ExoPlaybackException {
|
||||
Assertions.checkState(state == STATE_DISABLED);
|
||||
this.configuration = configuration;
|
||||
state = STATE_ENABLED;
|
||||
onEnabled(joining);
|
||||
replaceStream(formats, stream, offsetUs);
|
||||
@ -237,10 +240,15 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
||||
|
||||
// Methods to be called by subclasses.
|
||||
|
||||
/**
|
||||
* Returns the configuration set when the renderer was most recently enabled.
|
||||
*/
|
||||
protected final RendererConfiguration getConfiguration() {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the renderer within the player.
|
||||
*
|
||||
* @return The index of the renderer within the player.
|
||||
*/
|
||||
protected final int getIndex() {
|
||||
return index;
|
||||
@ -251,11 +259,11 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
||||
* {@link C#RESULT_BUFFER_READ} is only returned if {@link #setCurrentStreamFinal()} has been
|
||||
* called. {@link C#RESULT_NOTHING_READ} is returned otherwise.
|
||||
*
|
||||
* @see SampleStream#readData(FormatHolder, DecoderInputBuffer)
|
||||
* @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
|
||||
* @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
|
||||
* end of the stream. If the end of the stream has been reached, the
|
||||
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.
|
||||
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. May be null if the
|
||||
* caller requires that the format of the stream be read even if it's not changing.
|
||||
* @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
|
||||
* {@link C#RESULT_BUFFER_READ}.
|
||||
*/
|
||||
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaCodec;
|
||||
@ -550,4 +552,13 @@ public final class C {
|
||||
return timeMs == TIME_UNSET ? TIME_UNSET : (timeMs * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a newly generated {@link android.media.AudioTrack} session identifier.
|
||||
*/
|
||||
@TargetApi(21)
|
||||
public static int generateAudioSessionIdV21(Context context) {
|
||||
return ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE))
|
||||
.generateAudioSessionId();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.DefaultAllocator;
|
||||
import com.google.android.exoplayer2.util.PriorityTaskManager;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/**
|
||||
@ -50,6 +51,11 @@ public final class DefaultLoadControl implements LoadControl {
|
||||
*/
|
||||
public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000;
|
||||
|
||||
/**
|
||||
* Priority for media loading.
|
||||
*/
|
||||
public static final int LOADING_PRIORITY = 0;
|
||||
|
||||
private static final int ABOVE_HIGH_WATERMARK = 0;
|
||||
private static final int BETWEEN_WATERMARKS = 1;
|
||||
private static final int BELOW_LOW_WATERMARK = 2;
|
||||
@ -60,6 +66,7 @@ public final class DefaultLoadControl implements LoadControl {
|
||||
private final long maxBufferUs;
|
||||
private final long bufferForPlaybackUs;
|
||||
private final long bufferForPlaybackAfterRebufferUs;
|
||||
private final PriorityTaskManager priorityTaskManager;
|
||||
|
||||
private int targetBufferSize;
|
||||
private boolean isBuffering;
|
||||
@ -97,11 +104,36 @@ public final class DefaultLoadControl implements LoadControl {
|
||||
*/
|
||||
public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
|
||||
long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs) {
|
||||
this(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs,
|
||||
null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param allocator The {@link DefaultAllocator} used by the loader.
|
||||
* @param minBufferMs The minimum duration of media that the player will attempt to ensure is
|
||||
* buffered at all times, in milliseconds.
|
||||
* @param maxBufferMs The maximum duration of media that the player will attempt buffer, in
|
||||
* milliseconds.
|
||||
* @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or
|
||||
* resume following a user action such as a seek, in milliseconds.
|
||||
* @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for
|
||||
* playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by
|
||||
* buffer depletion rather than a user action.
|
||||
* @param priorityTaskManager If not null, registers itself as a task with priority
|
||||
* {@link #LOADING_PRIORITY} during loading periods, and unregisters itself during draining
|
||||
* periods.
|
||||
*/
|
||||
public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
|
||||
long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs,
|
||||
PriorityTaskManager priorityTaskManager) {
|
||||
this.allocator = allocator;
|
||||
minBufferUs = minBufferMs * 1000L;
|
||||
maxBufferUs = maxBufferMs * 1000L;
|
||||
bufferForPlaybackUs = bufferForPlaybackMs * 1000L;
|
||||
bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L;
|
||||
this.priorityTaskManager = priorityTaskManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -146,8 +178,16 @@ public final class DefaultLoadControl implements LoadControl {
|
||||
public boolean shouldContinueLoading(long bufferedDurationUs) {
|
||||
int bufferTimeState = getBufferTimeState(bufferedDurationUs);
|
||||
boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize;
|
||||
boolean wasBuffering = isBuffering;
|
||||
isBuffering = bufferTimeState == BELOW_LOW_WATERMARK
|
||||
|| (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached);
|
||||
if (priorityTaskManager != null && isBuffering != wasBuffering) {
|
||||
if (isBuffering) {
|
||||
priorityTaskManager.add(LOADING_PRIORITY);
|
||||
} else {
|
||||
priorityTaskManager.remove(LOADING_PRIORITY);
|
||||
}
|
||||
}
|
||||
return isBuffering;
|
||||
}
|
||||
|
||||
@ -158,6 +198,9 @@ public final class DefaultLoadControl implements LoadControl {
|
||||
|
||||
private void reset(boolean resetAllocator) {
|
||||
targetBufferSize = 0;
|
||||
if (priorityTaskManager != null && isBuffering) {
|
||||
priorityTaskManager.remove(LOADING_PRIORITY);
|
||||
}
|
||||
isBuffering = false;
|
||||
if (resetAllocator) {
|
||||
allocator.reset();
|
||||
|
@ -447,4 +447,20 @@ public interface ExoPlayer {
|
||||
*/
|
||||
int getBufferedPercentage();
|
||||
|
||||
/**
|
||||
* Returns whether the current window is dynamic, or {@code false} if the {@link Timeline} is
|
||||
* empty.
|
||||
*
|
||||
* @see Timeline.Window#isDynamic
|
||||
*/
|
||||
boolean isCurrentWindowDynamic();
|
||||
|
||||
/**
|
||||
* Returns whether the current window is seekable, or {@code false} if the {@link Timeline} is
|
||||
* empty.
|
||||
*
|
||||
* @see Timeline.Window#isSeekable
|
||||
*/
|
||||
boolean isCurrentWindowSeekable();
|
||||
|
||||
}
|
||||
|
@ -22,12 +22,12 @@ import android.os.Message;
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo;
|
||||
import com.google.android.exoplayer2.ExoPlayerImplInternal.SourceInfo;
|
||||
import com.google.android.exoplayer2.ExoPlayerImplInternal.TrackInfo;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
@ -271,6 +271,22 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
||||
: (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
|
||||
public int getRendererCount() {
|
||||
return renderers.length;
|
||||
@ -319,11 +335,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
||||
break;
|
||||
}
|
||||
case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: {
|
||||
TrackInfo trackInfo = (TrackInfo) msg.obj;
|
||||
TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj;
|
||||
tracksSelected = true;
|
||||
trackGroups = trackInfo.groups;
|
||||
trackSelections = trackInfo.selections;
|
||||
trackSelector.onSelectionActivated(trackInfo.info);
|
||||
trackGroups = trackSelectorResult.groups;
|
||||
trackSelections = trackSelectorResult.selections;
|
||||
trackSelector.onSelectionActivated(trackSelectorResult.info);
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onTracksChanged(trackGroups, trackSelections);
|
||||
}
|
||||
|
@ -26,16 +26,15 @@ import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
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.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MediaClock;
|
||||
import com.google.android.exoplayer2.util.PriorityHandlerThread;
|
||||
import com.google.android.exoplayer2.util.StandaloneMediaClock;
|
||||
import com.google.android.exoplayer2.util.TraceUtil;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
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 final Timeline timeline;
|
||||
@ -624,6 +609,7 @@ import java.io.IOException;
|
||||
enabledRenderers = new Renderer[0];
|
||||
rendererMediaClock = null;
|
||||
rendererMediaClockSource = null;
|
||||
playingPeriodHolder = null;
|
||||
}
|
||||
|
||||
// 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);
|
||||
} else {
|
||||
// Release and re-prepare/buffer periods after the one whose selection changed.
|
||||
@ -1138,33 +1125,38 @@ import java.io.IOException;
|
||||
}
|
||||
|
||||
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
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (Renderer renderer : enabledRenderers) {
|
||||
if (!renderer.hasReadStreamToEnd()) {
|
||||
for (int i = 0; i < renderers.length; i++) {
|
||||
Renderer renderer = renderers[i];
|
||||
SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
|
||||
if (renderer.getStream() != sampleStream
|
||||
|| (sampleStream != null && !renderer.hasReadStreamToEnd())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (readingPeriodHolder.next != null && readingPeriodHolder.next.prepared) {
|
||||
TrackSelectionArray oldTrackSelections = readingPeriodHolder.trackSelections;
|
||||
TrackSelectorResult oldTrackSelectorResult = readingPeriodHolder.trackSelectorResult;
|
||||
readingPeriodHolder = readingPeriodHolder.next;
|
||||
TrackSelectionArray newTrackSelections = readingPeriodHolder.trackSelections;
|
||||
TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.trackSelectorResult;
|
||||
|
||||
boolean initialDiscontinuity =
|
||||
readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET;
|
||||
for (int i = 0; i < renderers.length; i++) {
|
||||
Renderer renderer = renderers[i];
|
||||
TrackSelection oldSelection = oldTrackSelections.get(i);
|
||||
TrackSelection newSelection = newTrackSelections.get(i);
|
||||
TrackSelection oldSelection = oldTrackSelectorResult.selections.get(i);
|
||||
if (oldSelection == null) {
|
||||
// The renderer has no current stream and will be enabled when we play the next period.
|
||||
} else if (initialDiscontinuity) {
|
||||
@ -1172,9 +1164,12 @@ import java.io.IOException;
|
||||
// be disabled and re-enabled when it starts playing the next period.
|
||||
renderer.setCurrentStreamFinal();
|
||||
} else if (!renderer.isCurrentStreamFinal()) {
|
||||
if (newSelection != null) {
|
||||
// Replace the renderer's SampleStream so the transition to playing the next period
|
||||
// can be seamless.
|
||||
TrackSelection newSelection = newTrackSelectorResult.selections.get(i);
|
||||
RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[i];
|
||||
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()];
|
||||
for (int j = 0; j < formats.length; j++) {
|
||||
formats[j] = newSelection.getFormat(j);
|
||||
@ -1182,8 +1177,9 @@ import java.io.IOException;
|
||||
renderer.replaceStream(formats, readingPeriodHolder.sampleStreams[i],
|
||||
readingPeriodHolder.getRendererOffset());
|
||||
} else {
|
||||
// The renderer will be disabled when transitioning to playing the next period. Mark the
|
||||
// SampleStream as final to play out any remaining data.
|
||||
// The renderer will be disabled when transitioning to playing the next period, either
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
@ -1319,20 +1315,21 @@ import java.io.IOException;
|
||||
return;
|
||||
}
|
||||
|
||||
playingPeriodHolder = periodHolder;
|
||||
int enabledRendererCount = 0;
|
||||
boolean[] rendererWasEnabledFlags = new boolean[renderers.length];
|
||||
for (int i = 0; i < renderers.length; i++) {
|
||||
Renderer renderer = renderers[i];
|
||||
rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED;
|
||||
TrackSelection newSelection = periodHolder.trackSelections.get(i);
|
||||
TrackSelection newSelection = periodHolder.trackSelectorResult.selections.get(i);
|
||||
if (newSelection != null) {
|
||||
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
|
||||
// needed to play the next period, or because we need to disable and re-enable it because
|
||||
// the renderer thinks that its current stream is final.
|
||||
// needed to play the next period, or because we need to re-enable it as its current stream
|
||||
// is final and it's not reading ahead.
|
||||
if (renderer == rendererMediaClockSource) {
|
||||
// Sync standaloneMediaClock so that it can take over timing responsibilities.
|
||||
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);
|
||||
}
|
||||
|
||||
@ -1354,10 +1352,12 @@ import java.io.IOException;
|
||||
enabledRendererCount = 0;
|
||||
for (int i = 0; i < renderers.length; i++) {
|
||||
Renderer renderer = renderers[i];
|
||||
TrackSelection newSelection = playingPeriodHolder.trackSelections.get(i);
|
||||
TrackSelection newSelection = playingPeriodHolder.trackSelectorResult.selections.get(i);
|
||||
if (newSelection != null) {
|
||||
enabledRenderers[enabledRendererCount++] = renderer;
|
||||
if (renderer.getState() == Renderer.STATE_DISABLED) {
|
||||
RendererConfiguration rendererConfiguration =
|
||||
playingPeriodHolder.trackSelectorResult.rendererConfigurations[i];
|
||||
// The renderer needs enabling with its new track selection.
|
||||
boolean playing = playWhenReady && state == ExoPlayer.STATE_READY;
|
||||
// Consider as joining only if the renderer was previously disabled.
|
||||
@ -1368,8 +1368,8 @@ import java.io.IOException;
|
||||
formats[j] = newSelection.getFormat(j);
|
||||
}
|
||||
// Enable the renderer.
|
||||
renderer.enable(formats, playingPeriodHolder.sampleStreams[i], rendererPositionUs,
|
||||
joining, playingPeriodHolder.getRendererOffset());
|
||||
renderer.enable(rendererConfiguration, formats, playingPeriodHolder.sampleStreams[i],
|
||||
rendererPositionUs, joining, playingPeriodHolder.getRendererOffset());
|
||||
MediaClock mediaClock = renderer.getMediaClock();
|
||||
if (mediaClock != null) {
|
||||
if (rendererMediaClock != null) {
|
||||
@ -1406,6 +1406,7 @@ import java.io.IOException;
|
||||
public boolean hasEnabledTracks;
|
||||
public MediaPeriodHolder next;
|
||||
public boolean needsContinueLoading;
|
||||
public TrackSelectorResult trackSelectorResult;
|
||||
|
||||
private final Renderer[] renderers;
|
||||
private final RendererCapabilities[] rendererCapabilities;
|
||||
@ -1413,10 +1414,7 @@ import java.io.IOException;
|
||||
private final LoadControl loadControl;
|
||||
private final MediaSource mediaSource;
|
||||
|
||||
private Object trackSelectionsInfo;
|
||||
private TrackGroupArray trackGroups;
|
||||
private TrackSelectionArray trackSelections;
|
||||
private TrackSelectionArray periodTrackSelections;
|
||||
private TrackSelectorResult periodTrackSelectorResult;
|
||||
|
||||
public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities,
|
||||
long rendererPositionOffsetUs, TrackSelector trackSelector, LoadControl loadControl,
|
||||
@ -1462,20 +1460,17 @@ import java.io.IOException;
|
||||
|
||||
public void handlePrepared() throws ExoPlaybackException {
|
||||
prepared = true;
|
||||
trackGroups = mediaPeriod.getTrackGroups();
|
||||
selectTracks();
|
||||
startPositionUs = updatePeriodTrackSelection(startPositionUs, false);
|
||||
}
|
||||
|
||||
public boolean selectTracks() throws ExoPlaybackException {
|
||||
Pair<TrackSelectionArray, Object> selectorResult = trackSelector.selectTracks(
|
||||
rendererCapabilities, trackGroups);
|
||||
TrackSelectionArray newTrackSelections = selectorResult.first;
|
||||
if (newTrackSelections.equals(periodTrackSelections)) {
|
||||
TrackSelectorResult selectorResult = trackSelector.selectTracks(rendererCapabilities,
|
||||
mediaPeriod.getTrackGroups());
|
||||
if (selectorResult.isEquivalent(periodTrackSelectorResult)) {
|
||||
return false;
|
||||
}
|
||||
trackSelections = newTrackSelections;
|
||||
trackSelectionsInfo = selectorResult.second;
|
||||
trackSelectorResult = selectorResult;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1486,16 +1481,16 @@ import java.io.IOException;
|
||||
|
||||
public long updatePeriodTrackSelection(long positionUs, boolean forceRecreateStreams,
|
||||
boolean[] streamResetFlags) {
|
||||
TrackSelectionArray trackSelections = trackSelectorResult.selections;
|
||||
for (int i = 0; i < trackSelections.length; i++) {
|
||||
mayRetainStreamFlags[i] = !forceRecreateStreams
|
||||
&& Util.areEqual(periodTrackSelections == null ? null : periodTrackSelections.get(i),
|
||||
trackSelections.get(i));
|
||||
&& trackSelectorResult.isEquivalent(periodTrackSelectorResult, i);
|
||||
}
|
||||
|
||||
// Disable streams on the period and get new streams for updated/newly-enabled tracks.
|
||||
positionUs = mediaPeriod.selectTracks(trackSelections.getAll(), mayRetainStreamFlags,
|
||||
sampleStreams, streamResetFlags, positionUs);
|
||||
periodTrackSelections = trackSelections;
|
||||
periodTrackSelectorResult = trackSelectorResult;
|
||||
|
||||
// Update whether we have enabled tracks and sanity check the expected streams are non-null.
|
||||
hasEnabledTracks = false;
|
||||
@ -1509,14 +1504,10 @@ import java.io.IOException;
|
||||
}
|
||||
|
||||
// The track selection has changed.
|
||||
loadControl.onTracksSelected(renderers, trackGroups, trackSelections);
|
||||
loadControl.onTracksSelected(renderers, trackSelectorResult.groups, trackSelections);
|
||||
return positionUs;
|
||||
}
|
||||
|
||||
public TrackInfo getTrackInfo() {
|
||||
return new TrackInfo(trackGroups, trackSelections, trackSelectionsInfo);
|
||||
}
|
||||
|
||||
public void release() {
|
||||
try {
|
||||
mediaSource.releasePeriod(mediaPeriod);
|
||||
|
@ -183,20 +183,18 @@ public final class Format implements Parcelable {
|
||||
*/
|
||||
public final int accessibilityChannel;
|
||||
|
||||
// Lazily initialized hashcode and framework media format.
|
||||
|
||||
// Lazily initialized hashcode.
|
||||
private int hashCode;
|
||||
private MediaFormat frameworkMediaFormat;
|
||||
|
||||
// Video.
|
||||
|
||||
public static Format createVideoContainerFormat(String id, String containerMimeType,
|
||||
String sampleMimeType, String codecs, int bitrate, int width, int height,
|
||||
float frameRate, List<byte[]> initializationData) {
|
||||
float frameRate, List<byte[]> initializationData, @C.SelectionFlags int selectionFlags) {
|
||||
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, width,
|
||||
height, frameRate, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
|
||||
NO_VALUE, NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, null,
|
||||
null);
|
||||
NO_VALUE, NO_VALUE, selectionFlags, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
|
||||
initializationData, null, null);
|
||||
}
|
||||
|
||||
public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs,
|
||||
@ -289,8 +287,8 @@ public final class Format implements Parcelable {
|
||||
}
|
||||
|
||||
public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
|
||||
int bitrate, @C.SelectionFlags int selectionFlags, String language,
|
||||
int accessibilityChannel, DrmInitData drmInitData) {
|
||||
int bitrate, @C.SelectionFlags int selectionFlags, String language, int accessibilityChannel,
|
||||
DrmInitData drmInitData) {
|
||||
return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
|
||||
accessibilityChannel, drmInitData, OFFSET_SAMPLE_RELATIVE);
|
||||
}
|
||||
@ -332,11 +330,20 @@ public final class Format implements Parcelable {
|
||||
|
||||
// Generic.
|
||||
|
||||
public static Format createContainerFormat(String id, String containerMimeType, String codecs,
|
||||
String sampleMimeType, int bitrate) {
|
||||
public static Format createContainerFormat(String id, String containerMimeType,
|
||||
String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags,
|
||||
String language) {
|
||||
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
|
||||
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
|
||||
NO_VALUE, NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, null, null);
|
||||
NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, null,
|
||||
null);
|
||||
}
|
||||
|
||||
public static Format createSampleFormat(String id, String sampleMimeType,
|
||||
long subsampleOffsetUs) {
|
||||
return new Format(id, null, sampleMimeType, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
|
||||
NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
|
||||
NO_VALUE, 0, null, NO_VALUE, subsampleOffsetUs, null, null, null);
|
||||
}
|
||||
|
||||
public static Format createSampleFormat(String id, String sampleMimeType, String codecs,
|
||||
@ -495,31 +502,28 @@ public final class Format implements Parcelable {
|
||||
@SuppressLint("InlinedApi")
|
||||
@TargetApi(16)
|
||||
public final MediaFormat getFrameworkMediaFormatV16() {
|
||||
if (frameworkMediaFormat == null) {
|
||||
MediaFormat format = new MediaFormat();
|
||||
format.setString(MediaFormat.KEY_MIME, sampleMimeType);
|
||||
maybeSetStringV16(format, MediaFormat.KEY_LANGUAGE, language);
|
||||
maybeSetIntegerV16(format, MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
|
||||
maybeSetIntegerV16(format, MediaFormat.KEY_WIDTH, width);
|
||||
maybeSetIntegerV16(format, MediaFormat.KEY_HEIGHT, height);
|
||||
maybeSetFloatV16(format, MediaFormat.KEY_FRAME_RATE, frameRate);
|
||||
maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees);
|
||||
maybeSetIntegerV16(format, MediaFormat.KEY_CHANNEL_COUNT, channelCount);
|
||||
maybeSetIntegerV16(format, MediaFormat.KEY_SAMPLE_RATE, sampleRate);
|
||||
maybeSetIntegerV16(format, "encoder-delay", encoderDelay);
|
||||
maybeSetIntegerV16(format, "encoder-padding", encoderPadding);
|
||||
for (int i = 0; i < initializationData.size(); i++) {
|
||||
format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
|
||||
}
|
||||
frameworkMediaFormat = format;
|
||||
MediaFormat format = new MediaFormat();
|
||||
format.setString(MediaFormat.KEY_MIME, sampleMimeType);
|
||||
maybeSetStringV16(format, MediaFormat.KEY_LANGUAGE, language);
|
||||
maybeSetIntegerV16(format, MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
|
||||
maybeSetIntegerV16(format, MediaFormat.KEY_WIDTH, width);
|
||||
maybeSetIntegerV16(format, MediaFormat.KEY_HEIGHT, height);
|
||||
maybeSetFloatV16(format, MediaFormat.KEY_FRAME_RATE, frameRate);
|
||||
maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees);
|
||||
maybeSetIntegerV16(format, MediaFormat.KEY_CHANNEL_COUNT, channelCount);
|
||||
maybeSetIntegerV16(format, MediaFormat.KEY_SAMPLE_RATE, sampleRate);
|
||||
maybeSetIntegerV16(format, "encoder-delay", encoderDelay);
|
||||
maybeSetIntegerV16(format, "encoder-padding", encoderPadding);
|
||||
for (int i = 0; i < initializationData.size(); i++) {
|
||||
format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
|
||||
}
|
||||
return frameworkMediaFormat;
|
||||
return format;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Format(" + id + ", " + containerMimeType + ", " + sampleMimeType + ", " + bitrate + ", "
|
||||
+ ", " + language + ", [" + width + ", " + height + ", " + frameRate + "]"
|
||||
+ language + ", [" + width + ", " + height + ", " + frameRate + "]"
|
||||
+ ", [" + channelCount + ", " + sampleRate + "])";
|
||||
}
|
||||
|
||||
@ -602,6 +606,38 @@ public final class Format implements Parcelable {
|
||||
}
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
|
||||
/**
|
||||
* Returns a prettier {@link String} than {@link #toString()}, intended for logging.
|
||||
*/
|
||||
public static String toLogString(Format format) {
|
||||
if (format == null) {
|
||||
return "null";
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("id=").append(format.id).append(", mimeType=").append(format.sampleMimeType);
|
||||
if (format.bitrate != Format.NO_VALUE) {
|
||||
builder.append(", bitrate=").append(format.bitrate);
|
||||
}
|
||||
if (format.width != Format.NO_VALUE && format.height != Format.NO_VALUE) {
|
||||
builder.append(", res=").append(format.width).append("x").append(format.height);
|
||||
}
|
||||
if (format.frameRate != Format.NO_VALUE) {
|
||||
builder.append(", fps=").append(format.frameRate);
|
||||
}
|
||||
if (format.channelCount != Format.NO_VALUE) {
|
||||
builder.append(", channels=").append(format.channelCount);
|
||||
}
|
||||
if (format.sampleRate != Format.NO_VALUE) {
|
||||
builder.append(", sample_rate=").append(format.sampleRate);
|
||||
}
|
||||
if (format.language != null) {
|
||||
builder.append(", language=").append(format.language);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
// Parcelable implementation.
|
||||
|
||||
@Override
|
||||
|
@ -92,6 +92,7 @@ public interface Renderer extends ExoPlayerComponent {
|
||||
* This method may be called when the renderer is in the following states:
|
||||
* {@link #STATE_DISABLED}.
|
||||
*
|
||||
* @param configuration The renderer configuration.
|
||||
* @param formats The enabled formats.
|
||||
* @param stream The {@link SampleStream} from which the renderer should consume.
|
||||
* @param positionUs The player's current position.
|
||||
@ -100,8 +101,8 @@ public interface Renderer extends ExoPlayerComponent {
|
||||
* before they are rendered.
|
||||
* @throws ExoPlaybackException If an error occurs.
|
||||
*/
|
||||
void enable(Format[] formats, SampleStream stream, long positionUs, boolean joining,
|
||||
long offsetUs) throws ExoPlaybackException;
|
||||
void enable(RendererConfiguration configuration, Format[] formats, SampleStream stream,
|
||||
long positionUs, boolean joining, long offsetUs) throws ExoPlaybackException;
|
||||
|
||||
/**
|
||||
* Starts the renderer, meaning that calls to {@link #render(long, long)} will cause media to be
|
||||
|
@ -79,6 +79,20 @@ public interface RendererCapabilities {
|
||||
*/
|
||||
int ADAPTIVE_NOT_SUPPORTED = 0b0000;
|
||||
|
||||
/**
|
||||
* A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of
|
||||
* {@link #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}.
|
||||
*/
|
||||
int TUNNELING_SUPPORT_MASK = 0b10000;
|
||||
/**
|
||||
* The {@link Renderer} supports tunneled output.
|
||||
*/
|
||||
int TUNNELING_SUPPORTED = 0b10000;
|
||||
/**
|
||||
* The {@link Renderer} does not support tunneled output.
|
||||
*/
|
||||
int TUNNELING_NOT_SUPPORTED = 0b00000;
|
||||
|
||||
/**
|
||||
* Returns the track type that the {@link Renderer} handles. For example, a video renderer will
|
||||
* return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will return {@link C#TRACK_TYPE_AUDIO}, a
|
||||
@ -91,7 +105,7 @@ public interface RendererCapabilities {
|
||||
|
||||
/**
|
||||
* Returns the extent to which the {@link Renderer} supports a given format. The returned value is
|
||||
* the bitwise OR of two properties:
|
||||
* the bitwise OR of three properties:
|
||||
* <ul>
|
||||
* <li>The level of support for the format itself. One of {@link #FORMAT_HANDLED},
|
||||
* {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_SUBTYPE} and
|
||||
@ -99,9 +113,12 @@ public interface RendererCapabilities {
|
||||
* <li>The level of support for adapting from the format to another format of the same mime type.
|
||||
* One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and
|
||||
* {@link #ADAPTIVE_NOT_SUPPORTED}.</li>
|
||||
* <li>The level of support for tunneling. One of {@link #TUNNELING_SUPPORTED} and
|
||||
* {@link #TUNNELING_NOT_SUPPORTED}.</li>
|
||||
* </ul>
|
||||
* The individual properties can be retrieved by performing a bitwise AND with
|
||||
* {@link #FORMAT_SUPPORT_MASK} and {@link #ADAPTIVE_SUPPORT_MASK} respectively.
|
||||
* {@link #FORMAT_SUPPORT_MASK}, {@link #ADAPTIVE_SUPPORT_MASK} and
|
||||
* {@link #TUNNELING_SUPPORT_MASK} respectively.
|
||||
*
|
||||
* @param format The format.
|
||||
* @return The extent to which the renderer is capable of supporting the given format.
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -36,7 +36,6 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.MetadataRenderer;
|
||||
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
@ -448,15 +447,6 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
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.
|
||||
*
|
||||
@ -555,6 +545,36 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
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
|
||||
public int getCurrentPeriodIndex() {
|
||||
return player.getCurrentPeriodIndex();
|
||||
@ -586,33 +606,13 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRendererCount() {
|
||||
return player.getRendererCount();
|
||||
public boolean isCurrentWindowDynamic() {
|
||||
return player.isCurrentWindowDynamic();
|
||||
}
|
||||
|
||||
@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();
|
||||
public boolean isCurrentWindowSeekable() {
|
||||
return player.isCurrentWindowSeekable();
|
||||
}
|
||||
|
||||
// Renderer building.
|
||||
@ -771,7 +771,7 @@ public class SimpleExoPlayer implements ExoPlayer {
|
||||
protected void buildMetadataRenderers(Context context, Handler mainHandler,
|
||||
@ExtensionRendererMode int extensionRendererMode, MetadataRenderer.Output output,
|
||||
ArrayList<Renderer> out) {
|
||||
out.add(new MetadataRenderer(output, mainHandler.getLooper(), new Id3Decoder()));
|
||||
out.add(new MetadataRenderer(output, mainHandler.getLooper()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -38,21 +38,21 @@ import java.nio.ByteOrder;
|
||||
* playback position smoothing, non-blocking writes and reconfiguration.
|
||||
* <p>
|
||||
* Before starting playback, specify the input format by calling
|
||||
* {@link #configure(String, int, int, int, int)}. Next call {@link #initialize(int)} or
|
||||
* {@link #initializeV21(int, boolean)}, optionally specifying an audio session and whether the
|
||||
* track is to be used with tunneling video playback.
|
||||
* {@link #configure(String, int, int, int, int)}. Optionally call {@link #setAudioSessionId(int)},
|
||||
* {@link #setStreamType(int)}, {@link #enableTunnelingV21(int)} and {@link #disableTunneling()}
|
||||
* 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>
|
||||
* 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.
|
||||
* <p>
|
||||
* Call {@link #configure(String, int, int, int, int)} whenever the input format changes. If
|
||||
* {@link #isInitialized()} returns {@code false} after the call, it is necessary to call
|
||||
* {@link #initialize(int)} or {@link #initializeV21(int, boolean)} before writing more data.
|
||||
* Call {@link #configure(String, int, int, int, int)} whenever the input format changes. The track
|
||||
* will be reinitialized on the next call to {@link #handleBuffer(ByteBuffer, long)}.
|
||||
* <p>
|
||||
* The underlying {@link android.media.AudioTrack} is created by {@link #initialize(int)} and
|
||||
* released by {@link #reset()} (and {@link #configure(String, int, int, int, int)} unless the input
|
||||
* format is unchanged). It is safe to call {@link #initialize(int)} or
|
||||
* {@link #initializeV21(int, boolean)} after calling {@link #reset()} without reconfiguration.
|
||||
* Calling {@link #reset()} releases the underlying {@link android.media.AudioTrack} (and so does
|
||||
* calling {@link #configure(String, int, int, int, int)} unless the format is unchanged). It is
|
||||
* safe to call {@link #handleBuffer(ByteBuffer, long)} after {@link #reset()} without calling
|
||||
* {@link #configure(String, int, int, int, int)}.
|
||||
* <p>
|
||||
* Call {@link #release()} when the instance is no longer required.
|
||||
*/
|
||||
@ -63,6 +63,19 @@ public final class AudioTrack {
|
||||
*/
|
||||
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.
|
||||
*
|
||||
@ -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.
|
||||
*/
|
||||
@ -253,7 +257,7 @@ public final class AudioTrack {
|
||||
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;
|
||||
|
||||
@ -271,7 +275,6 @@ public final class AudioTrack {
|
||||
private int bufferSize;
|
||||
private long bufferSizeUs;
|
||||
|
||||
private boolean useHwAvSync;
|
||||
private ByteBuffer avSyncHeader;
|
||||
private int bytesUntilNextAvSync;
|
||||
|
||||
@ -299,6 +302,9 @@ public final class AudioTrack {
|
||||
private ByteBuffer resampledBuffer;
|
||||
private boolean useResampledBuffer;
|
||||
|
||||
private boolean playing;
|
||||
private int audioSessionId;
|
||||
private boolean tunneling;
|
||||
private boolean hasData;
|
||||
private long lastFeedElapsedRealtimeMs;
|
||||
|
||||
@ -329,6 +335,7 @@ public final class AudioTrack {
|
||||
volume = 1.0f;
|
||||
startMediaTimeState = START_NOT_SET;
|
||||
streamType = C.STREAM_TYPE_DEFAULT;
|
||||
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -342,14 +349,6 @@ public final class AudioTrack {
|
||||
&& 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
|
||||
* {@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.
|
||||
if (Util.SDK_INT <= 23 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER)) {
|
||||
switch(channelCount) {
|
||||
switch (channelCount) {
|
||||
case 7:
|
||||
channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND;
|
||||
break;
|
||||
@ -460,6 +459,13 @@ public final class AudioTrack {
|
||||
}
|
||||
|
||||
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;
|
||||
if (passthrough) {
|
||||
sourceEncoding = getEncodingForMimeType(mimeType);
|
||||
@ -512,31 +518,7 @@ public final class AudioTrack {
|
||||
bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(pcmBytesToFrames(bufferSize));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
private void initialize() throws InitializationException {
|
||||
// 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
|
||||
// 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.
|
||||
releasingConditionVariable.block();
|
||||
|
||||
useHwAvSync = tunneling;
|
||||
if (useHwAvSync) {
|
||||
if (tunneling) {
|
||||
audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, targetEncoding,
|
||||
bufferSize, sessionId);
|
||||
} else if (sessionId == C.AUDIO_SESSION_ID_UNSET) {
|
||||
bufferSize, audioSessionId);
|
||||
} else if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
|
||||
audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
|
||||
targetEncoding, bufferSize, MODE_STREAM);
|
||||
} else {
|
||||
// Re-attach to the same audio session.
|
||||
audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
|
||||
targetEncoding, bufferSize, MODE_STREAM, sessionId);
|
||||
targetEncoding, bufferSize, MODE_STREAM, audioSessionId);
|
||||
}
|
||||
checkAudioTrackInitialized();
|
||||
|
||||
sessionId = audioTrack.getAudioSessionId();
|
||||
int audioSessionId = audioTrack.getAudioSessionId();
|
||||
if (enablePreV21AudioSessionWorkaround) {
|
||||
if (Util.SDK_INT < 21) {
|
||||
// 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.
|
||||
if (keepSessionIdAudioTrack != null
|
||||
&& sessionId != keepSessionIdAudioTrack.getAudioSessionId()) {
|
||||
&& audioSessionId != keepSessionIdAudioTrack.getAudioSessionId()) {
|
||||
releaseKeepSessionIdAudioTrack();
|
||||
}
|
||||
if (keepSessionIdAudioTrack == null) {
|
||||
@ -573,21 +554,25 @@ public final class AudioTrack {
|
||||
@C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT;
|
||||
int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback.
|
||||
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());
|
||||
setAudioTrackVolume();
|
||||
setVolumeInternal();
|
||||
hasData = false;
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts or resumes playing audio if the audio track has been initialized.
|
||||
*/
|
||||
public void play() {
|
||||
playing = true;
|
||||
if (isInitialized()) {
|
||||
resumeSystemTimeUs = System.nanoTime() / 1000;
|
||||
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
|
||||
* position and ending at its limit (exclusive). The position of the {@link ByteBuffer} is
|
||||
* 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>
|
||||
* Returns a bit field containing {@link #RESULT_BUFFER_CONSUMED} if the data was written in full,
|
||||
* and {@link #RESULT_POSITION_DISCONTINUITY} if the buffer was discontinuous with previously
|
||||
* written data.
|
||||
* <p>
|
||||
* 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}.
|
||||
* Returns whether the data was written in full. 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 #reset()} (or an interleaving call to
|
||||
* {@link #configure(String, int, int, int, int)} that caused the track to be reset).
|
||||
*
|
||||
* @param buffer The buffer containing audio data to play back.
|
||||
* @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
|
||||
* {@link #RESULT_POSITION_DISCONTINUITY} if the buffer was not contiguous with previously
|
||||
* written data.
|
||||
* @return Whether the buffer was consumed fully.
|
||||
* @throws InitializationException If an error occurs initializing the track.
|
||||
* @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;
|
||||
hasData = hasPendingData();
|
||||
if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) {
|
||||
long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs;
|
||||
listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs);
|
||||
}
|
||||
int result = writeBuffer(buffer, presentationTimeUs);
|
||||
boolean result = writeBuffer(buffer, presentationTimeUs);
|
||||
lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime();
|
||||
return result;
|
||||
}
|
||||
|
||||
private int writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException {
|
||||
private boolean writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException {
|
||||
boolean isNewSourceBuffer = currentSourceBuffer == null;
|
||||
Assertions.checkState(isNewSourceBuffer || 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
|
||||
// buffer empties. See [Internal: b/18899620].
|
||||
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
|
||||
@ -653,18 +644,17 @@ public final class AudioTrack {
|
||||
// head position actually returns to zero.
|
||||
if (audioTrack.getPlayState() == PLAYSTATE_STOPPED
|
||||
&& audioTrackUtil.getPlaybackHeadPosition() != 0) {
|
||||
return 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int result = 0;
|
||||
if (isNewSourceBuffer) {
|
||||
// We're seeing this buffer for the first time.
|
||||
|
||||
if (!currentSourceBuffer.hasRemaining()) {
|
||||
// The buffer is empty.
|
||||
currentSourceBuffer = null;
|
||||
return RESULT_BUFFER_CONSUMED;
|
||||
return true;
|
||||
}
|
||||
|
||||
useResampledBuffer = targetEncoding != sourceEncoding;
|
||||
@ -697,7 +687,7 @@ public final class AudioTrack {
|
||||
// number of bytes submitted.
|
||||
startMediaTimeUs += (presentationTimeUs - expectedPresentationTimeUs);
|
||||
startMediaTimeState = START_IN_SYNC;
|
||||
result |= RESULT_POSITION_DISCONTINUITY;
|
||||
listener.onPositionDiscontinuity();
|
||||
}
|
||||
}
|
||||
if (Util.SDK_INT < 21) {
|
||||
@ -730,7 +720,7 @@ public final class AudioTrack {
|
||||
buffer.position(buffer.position() + bytesWritten);
|
||||
}
|
||||
} else {
|
||||
bytesWritten = useHwAvSync
|
||||
bytesWritten = tunneling
|
||||
? writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, presentationTimeUs)
|
||||
: writeNonBlockingV21(audioTrack, buffer, bytesRemaining);
|
||||
}
|
||||
@ -747,9 +737,9 @@ public final class AudioTrack {
|
||||
submittedEncodedFrames += framesPerEncodedSample;
|
||||
}
|
||||
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
|
||||
* is not configured for use with video tunneling, then the audio track is reset and the caller
|
||||
* must re-initialize the audio track before writing more data. The caller must not reuse the
|
||||
* audio session identifier when re-initializing with a new stream type.
|
||||
* is not configured for use with tunneling, then the audio track is reset and the audio session
|
||||
* id is cleared.
|
||||
* <p>
|
||||
* If the audio track is configured for use with video tunneling then the stream type is ignored
|
||||
* and the audio track is not reset. The passed stream type will be used if the audio track is
|
||||
* later re-configured into non-tunneled mode.
|
||||
* If the audio track is configured for use with tunneling then the stream type is ignored, the
|
||||
* audio track is not reset and the audio session id is not cleared. The passed stream type will
|
||||
* 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.
|
||||
* @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) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
this.streamType = streamType;
|
||||
if (useHwAvSync) {
|
||||
if (tunneling) {
|
||||
// The stream type is ignored in tunneling mode, so no need to reset.
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
if (this.volume != volume) {
|
||||
this.volume = volume;
|
||||
setAudioTrackVolume();
|
||||
setVolumeInternal();
|
||||
}
|
||||
}
|
||||
|
||||
private void setAudioTrackVolume() {
|
||||
private void setVolumeInternal() {
|
||||
if (!isInitialized()) {
|
||||
// Do nothing.
|
||||
} else if (Util.SDK_INT >= 21) {
|
||||
setAudioTrackVolumeV21(audioTrack, volume);
|
||||
setVolumeInternalV21(audioTrack, volume);
|
||||
} else {
|
||||
setAudioTrackVolumeV3(audioTrack, volume);
|
||||
setVolumeInternalV3(audioTrack, volume);
|
||||
}
|
||||
}
|
||||
|
||||
@ -835,6 +861,7 @@ public final class AudioTrack {
|
||||
* Pauses playback.
|
||||
*/
|
||||
public void pause() {
|
||||
playing = false;
|
||||
if (isInitialized()) {
|
||||
resetSyncParams();
|
||||
audioTrackUtil.pause();
|
||||
@ -844,9 +871,9 @@ public final class AudioTrack {
|
||||
/**
|
||||
* Releases the underlying audio track asynchronously.
|
||||
* <p>
|
||||
* Calling {@link #initialize(int)} or {@link #initializeV21(int, boolean)} will block until the
|
||||
* audio track has been released, so it is safe to initialize immediately after a reset. The audio
|
||||
* session may remain active until {@link #release()} is called.
|
||||
* Calling {@link #handleBuffer(ByteBuffer, long)} will block until the audio track has been
|
||||
* released, so it is safe to use the audio track immediately after a reset. The audio session may
|
||||
* remain active until {@link #release()} is called.
|
||||
*/
|
||||
public void reset() {
|
||||
if (isInitialized()) {
|
||||
@ -855,6 +882,7 @@ public final class AudioTrack {
|
||||
framesPerEncodedSample = 0;
|
||||
currentSourceBuffer = null;
|
||||
avSyncHeader = null;
|
||||
bytesUntilNextAvSync = 0;
|
||||
startMediaTimeState = START_NOT_SET;
|
||||
latencyUs = 0;
|
||||
resetSyncParams();
|
||||
@ -887,6 +915,8 @@ public final class AudioTrack {
|
||||
public void release() {
|
||||
reset();
|
||||
releaseKeepSessionIdAudioTrack();
|
||||
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
|
||||
playing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1024,6 +1054,10 @@ public final class AudioTrack {
|
||||
throw new InitializationException(state, sampleRate, channelConfig, bufferSize);
|
||||
}
|
||||
|
||||
private boolean isInitialized() {
|
||||
return audioTrack != null;
|
||||
}
|
||||
|
||||
private long pcmBytesToFrames(long byteCount) {
|
||||
return byteCount / pcmFrameSize;
|
||||
}
|
||||
@ -1240,12 +1274,12 @@ public final class AudioTrack {
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@ -1494,7 +1528,7 @@ public final class AudioTrack {
|
||||
playbackParams = (playbackParams != null ? playbackParams : new PlaybackParams())
|
||||
.allowDefaults();
|
||||
this.playbackParams = playbackParams;
|
||||
this.playbackSpeed = playbackParams.getSpeed();
|
||||
playbackSpeed = playbackParams.getSpeed();
|
||||
maybeApplyPlaybackParams();
|
||||
}
|
||||
|
||||
|
@ -41,8 +41,7 @@ import java.nio.ByteBuffer;
|
||||
* Decodes and renders audio using {@link MediaCodec} and {@link AudioTrack}.
|
||||
*/
|
||||
@TargetApi(16)
|
||||
public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock,
|
||||
AudioTrack.Listener {
|
||||
public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock {
|
||||
|
||||
private final EventDispatcher eventDispatcher;
|
||||
private final AudioTrack audioTrack;
|
||||
@ -50,7 +49,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
private boolean passthroughEnabled;
|
||||
private android.media.MediaFormat passthroughMediaFormat;
|
||||
private int pcmEncoding;
|
||||
private int audioSessionId;
|
||||
private long currentPositionUs;
|
||||
private boolean allowPositionDiscontinuity;
|
||||
|
||||
@ -129,8 +127,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
boolean playClearSamplesWithoutKeys, Handler eventHandler,
|
||||
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) {
|
||||
super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys);
|
||||
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
|
||||
audioTrack = new AudioTrack(audioCapabilities, this);
|
||||
audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener());
|
||||
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
||||
}
|
||||
|
||||
@ -141,10 +138,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
if (!MimeTypes.isAudio(mimeType)) {
|
||||
return FORMAT_UNSUPPORTED_TYPE;
|
||||
}
|
||||
int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;
|
||||
if (allowPassthrough(mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null) {
|
||||
return ADAPTIVE_NOT_SEAMLESS | FORMAT_HANDLED;
|
||||
return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED;
|
||||
}
|
||||
MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, false, false);
|
||||
MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, false);
|
||||
if (decoderInfo == null) {
|
||||
return FORMAT_UNSUPPORTED_SUBTYPE;
|
||||
}
|
||||
@ -155,7 +153,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
&& (format.channelCount == Format.NO_VALUE
|
||||
|| decoderInfo.isAudioChannelCountSupportedV21(format.channelCount)));
|
||||
int formatSupport = decoderCapable ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES;
|
||||
return ADAPTIVE_NOT_SEAMLESS | formatSupport;
|
||||
return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -231,25 +229,42 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the audio session id becomes known. Once the id is known it will not change (and
|
||||
* hence this method will not be called again) unless the renderer is disabled and then
|
||||
* subsequently re-enabled.
|
||||
* <p>
|
||||
* The default implementation is a no-op. One reason for overriding this method would be to
|
||||
* instantiate and enable a {@link Virtualizer} in order to spatialize the audio channels. For
|
||||
* this use case, any {@link Virtualizer} instances should be released in {@link #onDisabled()}
|
||||
* (if not before).
|
||||
* Called when the audio session id becomes known. The default implementation is a no-op. One
|
||||
* reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in
|
||||
* order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances
|
||||
* should be released in {@link #onDisabled()} (if not before).
|
||||
*
|
||||
* @param audioSessionId The audio session id.
|
||||
* @see AudioTrack.Listener#onAudioSessionId(int)
|
||||
*/
|
||||
protected void onAudioSessionId(int audioSessionId) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* @see AudioTrack.Listener#onPositionDiscontinuity()
|
||||
*/
|
||||
protected void onAudioTrackPositionDiscontinuity() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* @see AudioTrack.Listener#onUnderrun(int, long, long)
|
||||
*/
|
||||
protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs,
|
||||
long elapsedSinceLastFeedMs) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnabled(boolean joining) throws ExoPlaybackException {
|
||||
super.onEnabled(joining);
|
||||
eventDispatcher.enabled(decoderCounters);
|
||||
int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;
|
||||
if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {
|
||||
audioTrack.enableTunnelingV21(tunnelingAudioSessionId);
|
||||
} else {
|
||||
audioTrack.disableTunneling();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -274,7 +289,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
|
||||
@Override
|
||||
protected void onDisabled() {
|
||||
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
|
||||
try {
|
||||
audioTrack.release();
|
||||
} finally {
|
||||
@ -325,44 +339,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!audioTrack.isInitialized()) {
|
||||
// Initialize the AudioTrack now.
|
||||
try {
|
||||
if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
|
||||
audioSessionId = audioTrack.initialize(C.AUDIO_SESSION_ID_UNSET);
|
||||
eventDispatcher.audioSessionId(audioSessionId);
|
||||
onAudioSessionId(audioSessionId);
|
||||
} else {
|
||||
audioTrack.initialize(audioSessionId);
|
||||
}
|
||||
} catch (AudioTrack.InitializationException e) {
|
||||
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
||||
}
|
||||
if (getState() == STATE_STARTED) {
|
||||
audioTrack.play();
|
||||
}
|
||||
}
|
||||
|
||||
int handleBufferResult;
|
||||
try {
|
||||
handleBufferResult = audioTrack.handleBuffer(buffer, bufferPresentationTimeUs);
|
||||
} catch (AudioTrack.WriteException e) {
|
||||
if (audioTrack.handleBuffer(buffer, bufferPresentationTimeUs)) {
|
||||
codec.releaseOutputBuffer(bufferIndex, false);
|
||||
decoderCounters.renderedOutputBufferCount++;
|
||||
return true;
|
||||
}
|
||||
} catch (AudioTrack.InitializationException | AudioTrack.WriteException e) {
|
||||
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
||||
}
|
||||
|
||||
// If we are out of sync, allow currentPositionUs to jump backwards.
|
||||
if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) {
|
||||
handleAudioTrackDiscontinuity();
|
||||
allowPositionDiscontinuity = true;
|
||||
}
|
||||
|
||||
// Release the buffer if it was consumed.
|
||||
if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) {
|
||||
codec.releaseOutputBuffer(bufferIndex, false);
|
||||
decoderCounters.renderedOutputBufferCount++;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -371,10 +356,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
audioTrack.handleEndOfStream();
|
||||
}
|
||||
|
||||
protected void handleAudioTrackDiscontinuity() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
|
||||
switch (messageType) {
|
||||
@ -386,9 +367,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
break;
|
||||
case C.MSG_SET_STREAM_TYPE:
|
||||
@C.StreamType int streamType = (Integer) message;
|
||||
if (audioTrack.setStreamType(streamType)) {
|
||||
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
|
||||
}
|
||||
audioTrack.setStreamType(streamType);
|
||||
break;
|
||||
default:
|
||||
super.handleMessage(messageType, message);
|
||||
@ -396,11 +375,27 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
}
|
||||
}
|
||||
|
||||
// AudioTrack.Listener implementation.
|
||||
private final class AudioTrackListener implements AudioTrack.Listener {
|
||||
|
||||
@Override
|
||||
public void onAudioSessionId(int audioSessionId) {
|
||||
eventDispatcher.audioSessionId(audioSessionId);
|
||||
MediaCodecAudioRenderer.this.onAudioSessionId(audioSessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
onAudioTrackPositionDiscontinuity();
|
||||
// We are out of sync so allow currentPositionUs to jump backwards.
|
||||
MediaCodecAudioRenderer.this.allowPositionDiscontinuity = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||
onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2.audio;
|
||||
|
||||
import android.media.PlaybackParams;
|
||||
import android.media.audiofx.Virtualizer;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
@ -43,8 +44,7 @@ import java.lang.annotation.RetentionPolicy;
|
||||
/**
|
||||
* Decodes and renders audio using a {@link SimpleDecoder}.
|
||||
*/
|
||||
public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock,
|
||||
AudioTrack.Listener {
|
||||
public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock {
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({REINITIALIZATION_STATE_NONE, REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM,
|
||||
@ -94,8 +94,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||
private boolean outputStreamEnded;
|
||||
private boolean waitingForKeys;
|
||||
|
||||
private int audioSessionId;
|
||||
|
||||
public SimpleDecoderAudioRenderer() {
|
||||
this(null, null);
|
||||
}
|
||||
@ -141,11 +139,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||
DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys) {
|
||||
super(C.TRACK_TYPE_AUDIO);
|
||||
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
||||
audioTrack = new AudioTrack(audioCapabilities, this);
|
||||
audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener());
|
||||
this.drmSessionManager = drmSessionManager;
|
||||
formatHolder = new FormatHolder();
|
||||
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
|
||||
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
|
||||
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
|
||||
audioTrackNeedsConfigure = true;
|
||||
}
|
||||
@ -155,6 +152,25 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||
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
|
||||
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
|
||||
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.
|
||||
*
|
||||
@ -244,28 +287,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||
audioTrackNeedsConfigure = false;
|
||||
}
|
||||
|
||||
if (!audioTrack.isInitialized()) {
|
||||
if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
|
||||
audioSessionId = audioTrack.initialize(C.AUDIO_SESSION_ID_UNSET);
|
||||
eventDispatcher.audioSessionId(audioSessionId);
|
||||
onAudioSessionId(audioSessionId);
|
||||
} else {
|
||||
audioTrack.initialize(audioSessionId);
|
||||
}
|
||||
if (getState() == STATE_STARTED) {
|
||||
audioTrack.play();
|
||||
}
|
||||
}
|
||||
|
||||
int handleBufferResult = audioTrack.handleBuffer(outputBuffer.data, outputBuffer.timeUs);
|
||||
|
||||
// If we are out of sync, allow currentPositionUs to jump backwards.
|
||||
if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) {
|
||||
allowPositionDiscontinuity = true;
|
||||
}
|
||||
|
||||
// Release the buffer if it was consumed.
|
||||
if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) {
|
||||
if (audioTrack.handleBuffer(outputBuffer.data, outputBuffer.timeUs)) {
|
||||
decoderCounters.renderedOutputBufferCount++;
|
||||
outputBuffer.release();
|
||||
outputBuffer = null;
|
||||
@ -381,23 +403,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||
return currentPositionUs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the audio session id becomes known. Once the id is known it will not change (and
|
||||
* hence this method will not be called again) unless the renderer is disabled and then
|
||||
* subsequently re-enabled.
|
||||
* <p>
|
||||
* The default implementation is a no-op.
|
||||
*
|
||||
* @param audioSessionId The audio session id.
|
||||
*/
|
||||
protected void onAudioSessionId(int audioSessionId) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnabled(boolean joining) throws ExoPlaybackException {
|
||||
decoderCounters = new DecoderCounters();
|
||||
eventDispatcher.enabled(decoderCounters);
|
||||
int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;
|
||||
if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {
|
||||
audioTrack.enableTunnelingV21(tunnelingAudioSessionId);
|
||||
} else {
|
||||
audioTrack.disableTunneling();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -425,7 +440,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||
@Override
|
||||
protected void onDisabled() {
|
||||
inputFormat = null;
|
||||
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
|
||||
audioTrackNeedsConfigure = true;
|
||||
waitingForKeys = false;
|
||||
try {
|
||||
@ -537,6 +551,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||
// There aren't any final output buffers, so release the decoder immediately.
|
||||
releaseDecoder();
|
||||
maybeInitDecoder();
|
||||
audioTrackNeedsConfigure = true;
|
||||
}
|
||||
|
||||
eventDispatcher.inputFormatChanged(newFormat);
|
||||
@ -553,9 +568,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||
break;
|
||||
case C.MSG_SET_STREAM_TYPE:
|
||||
@C.StreamType int streamType = (Integer) message;
|
||||
if (audioTrack.setStreamType(streamType)) {
|
||||
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
|
||||
}
|
||||
audioTrack.setStreamType(streamType);
|
||||
break;
|
||||
default:
|
||||
super.handleMessage(messageType, message);
|
||||
@ -563,11 +576,27 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
||||
}
|
||||
}
|
||||
|
||||
// AudioTrack.Listener implementation.
|
||||
private final class AudioTrackListener implements AudioTrack.Listener {
|
||||
|
||||
@Override
|
||||
public void onAudioSessionId(int audioSessionId) {
|
||||
eventDispatcher.audioSessionId(audioSessionId);
|
||||
SimpleDecoderAudioRenderer.this.onAudioSessionId(audioSessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
onAudioTrackPositionDiscontinuity();
|
||||
// We are out of sync so allow currentPositionUs to jump backwards.
|
||||
SimpleDecoderAudioRenderer.this.allowPositionDiscontinuity = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||
onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -24,7 +24,10 @@ import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.support.annotation.IntDef;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
|
||||
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.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
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)
|
||||
public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T>,
|
||||
public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T>,
|
||||
DrmSession<T> {
|
||||
|
||||
/**
|
||||
* Listener of {@link StreamingDrmSessionManager} events.
|
||||
* Listener of {@link DefaultDrmSessionManager} events.
|
||||
*/
|
||||
public interface EventListener {
|
||||
|
||||
@ -60,6 +66,16 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
||||
*/
|
||||
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";
|
||||
|
||||
/** 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_KEYS = 1;
|
||||
|
||||
private static final int MAX_LICENSE_DURATION_TO_RENEW = 60;
|
||||
|
||||
private final Handler eventHandler;
|
||||
private final EventListener eventListener;
|
||||
private final ExoMediaDrm<T> mediaDrm;
|
||||
@ -85,14 +124,17 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
||||
private HandlerThread requestHandlerThread;
|
||||
private Handler postRequestHandler;
|
||||
|
||||
private int mode;
|
||||
private int openCount;
|
||||
private boolean provisioningInProgress;
|
||||
@DrmSession.State
|
||||
private int state;
|
||||
private T mediaCrypto;
|
||||
private Exception lastException;
|
||||
private SchemeData schemeData;
|
||||
private DrmSessionException lastException;
|
||||
private byte[] schemeInitData;
|
||||
private String schemeMimeType;
|
||||
private byte[] sessionId;
|
||||
private byte[] offlineLicenseKeySetId;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @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,
|
||||
Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException {
|
||||
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.
|
||||
* @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,
|
||||
EventListener eventListener) throws UnsupportedDrmException {
|
||||
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.
|
||||
* @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,
|
||||
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);
|
||||
}
|
||||
|
||||
@ -168,7 +210,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
||||
* 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,
|
||||
EventListener eventListener) {
|
||||
this.uuid = uuid;
|
||||
@ -179,6 +221,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
||||
this.eventListener = eventListener;
|
||||
mediaDrm.setOnEventListener(new MediaDrmEventListener());
|
||||
state = STATE_CLOSED;
|
||||
mode = MODE_PLAYBACK;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -229,6 +272,35 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
||||
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.
|
||||
|
||||
@Override
|
||||
@ -248,18 +320,22 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
||||
requestHandlerThread.start();
|
||||
postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
|
||||
|
||||
schemeData = drmInitData.get(uuid);
|
||||
if (schemeData == null) {
|
||||
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.
|
||||
byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeData.data, C.WIDEVINE_UUID);
|
||||
if (psshData == null) {
|
||||
// Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged.
|
||||
} else {
|
||||
schemeData = new SchemeData(C.WIDEVINE_UUID, schemeData.mimeType, psshData);
|
||||
if (offlineLicenseKeySetId == null) {
|
||||
SchemeData schemeData = drmInitData.get(uuid);
|
||||
if (schemeData == null) {
|
||||
onError(new IllegalStateException("Media does not support uuid: " + uuid));
|
||||
return this;
|
||||
}
|
||||
schemeInitData = schemeData.data;
|
||||
schemeMimeType = schemeData.mimeType;
|
||||
if (Util.SDK_INT < 21) {
|
||||
// Prior to L the Widevine CDM required data to be extracted from the PSSH atom.
|
||||
byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, C.WIDEVINE_UUID);
|
||||
if (psshData == null) {
|
||||
// Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged.
|
||||
} else {
|
||||
schemeInitData = psshData;
|
||||
}
|
||||
}
|
||||
}
|
||||
state = STATE_OPENING;
|
||||
@ -280,7 +356,8 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
||||
postRequestHandler = null;
|
||||
requestHandlerThread.quit();
|
||||
requestHandlerThread = null;
|
||||
schemeData = null;
|
||||
schemeInitData = null;
|
||||
schemeMimeType = null;
|
||||
mediaCrypto = null;
|
||||
lastException = null;
|
||||
if (sessionId != null) {
|
||||
@ -314,10 +391,25 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Exception getError() {
|
||||
public final DrmSessionException getError() {
|
||||
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.
|
||||
|
||||
private void openInternal(boolean allowProvisioning) {
|
||||
@ -325,7 +417,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
||||
sessionId = mediaDrm.openSession();
|
||||
mediaCrypto = mediaDrm.createMediaCrypto(uuid, sessionId);
|
||||
state = STATE_OPENED;
|
||||
postKeyRequest();
|
||||
doLicense();
|
||||
} catch (NotProvisionedException e) {
|
||||
if (allowProvisioning) {
|
||||
postProvisionRequest();
|
||||
@ -363,20 +455,87 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
||||
if (state == STATE_OPENING) {
|
||||
openInternal(false);
|
||||
} else {
|
||||
postKeyRequest();
|
||||
doLicense();
|
||||
}
|
||||
} catch (DeniedByServerException 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;
|
||||
try {
|
||||
keyRequest = mediaDrm.getKeyRequest(sessionId, schemeData.data, schemeData.mimeType,
|
||||
MediaDrm.KEY_TYPE_STREAMING, optionalKeyRequestParameters);
|
||||
keyRequest = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, keyType,
|
||||
optionalKeyRequestParameters);
|
||||
postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
|
||||
} catch (NotProvisionedException e) {
|
||||
} catch (Exception e) {
|
||||
onKeysError(e);
|
||||
}
|
||||
}
|
||||
@ -393,15 +552,30 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
||||
}
|
||||
|
||||
try {
|
||||
mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
|
||||
state = STATE_OPENED_WITH_KEYS;
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onDrmKeysLoaded();
|
||||
}
|
||||
});
|
||||
if (mode == MODE_RELEASE) {
|
||||
mediaDrm.provideKeyResponse(offlineLicenseKeySetId, (byte[]) response);
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
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) {
|
||||
onKeysError(e);
|
||||
@ -417,7 +591,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
||||
}
|
||||
|
||||
private void onError(final Exception e) {
|
||||
lastException = e;
|
||||
lastException = new DrmSessionException(e);
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
@ -446,11 +620,16 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
||||
}
|
||||
switch (msg.what) {
|
||||
case MediaDrm.EVENT_KEY_REQUIRED:
|
||||
postKeyRequest();
|
||||
doLicense();
|
||||
break;
|
||||
case MediaDrm.EVENT_KEY_EXPIRED:
|
||||
state = STATE_OPENED;
|
||||
onError(new KeysExpiredException());
|
||||
// When an already expired key is loaded MediaDrm sends this event immediately. Ignore
|
||||
// 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;
|
||||
case MediaDrm.EVENT_PROVISION_REQUIRED:
|
||||
state = STATE_OPENED;
|
||||
@ -466,7 +645,9 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
||||
@Override
|
||||
public void onEvent(ExoMediaDrm<? extends T> md, byte[] sessionId, int event, int extra,
|
||||
byte[] data) {
|
||||
mediaDrmHandler.sendEmptyMessage(event);
|
||||
if (mode == MODE_PLAYBACK) {
|
||||
mediaDrmHandler.sendEmptyMessage(event);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -16,9 +16,11 @@
|
||||
package com.google.android.exoplayer2.drm;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.MediaDrm;
|
||||
import android.support.annotation.IntDef;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A DRM session.
|
||||
@ -26,6 +28,15 @@ import java.lang.annotation.RetentionPolicy;
|
||||
@TargetApi(16)
|
||||
public interface DrmSession<T extends ExoMediaCrypto> {
|
||||
|
||||
/** Wraps the exception which is the cause of the error state. */
|
||||
class DrmSessionException extends Exception {
|
||||
|
||||
DrmSessionException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The state of the DRM session.
|
||||
*/
|
||||
@ -96,6 +107,26 @@ public interface DrmSession<T extends ExoMediaCrypto> {
|
||||
*
|
||||
* @return An exception if the state is {@link #STATE_ERROR}. Null otherwise.
|
||||
*/
|
||||
Exception getError();
|
||||
DrmSessionException getError();
|
||||
|
||||
/**
|
||||
* Returns an informative description of the key status for the session. The status is in the form
|
||||
* of {name, value} pairs.
|
||||
*
|
||||
* <p>Since DRM license policies vary by vendor, the specific status field names are determined by
|
||||
* each DRM vendor. Refer to your DRM provider documentation for definitions of the field names
|
||||
* for a particular DRM engine plugin.
|
||||
*
|
||||
* @return A map of key status.
|
||||
* @throws IllegalStateException If called when the session isn't opened.
|
||||
* @see MediaDrm#queryKeyStatus(byte[])
|
||||
*/
|
||||
Map<String, String> queryKeyStatus();
|
||||
|
||||
/**
|
||||
* Returns the key set id of the offline license loaded into this session, if there is one. Null
|
||||
* otherwise.
|
||||
*/
|
||||
byte[] getOfflineLicenseKeySetId();
|
||||
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
|
||||
try {
|
||||
return Util.toByteArray(inputStream);
|
||||
} finally {
|
||||
inputStream.close();
|
||||
Util.closeQuietly(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 */);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -226,13 +226,32 @@ public final class DefaultTrackOutput implements TrackOutput {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to skip to the keyframe before the specified time, if it's present in the buffer.
|
||||
* Attempts to skip to the keyframe before or at the specified time. Succeeds only if the buffer
|
||||
* contains a keyframe with a timestamp of {@code timeUs} or earlier, and if {@code timeUs} falls
|
||||
* within the currently buffered media.
|
||||
* <p>
|
||||
* This method is equivalent to {@code skipToKeyframeBefore(timeUs, false)}.
|
||||
*
|
||||
* @param timeUs The seek time.
|
||||
* @return Whether the skip was successful.
|
||||
*/
|
||||
public boolean skipToKeyframeBefore(long timeUs) {
|
||||
long nextOffset = infoQueue.skipToKeyframeBefore(timeUs);
|
||||
return skipToKeyframeBefore(timeUs, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to skip to the keyframe before or at the specified time. Succeeds only if the buffer
|
||||
* contains a keyframe with a timestamp of {@code timeUs} or earlier. If
|
||||
* {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs}
|
||||
* falls within the buffer.
|
||||
*
|
||||
* @param timeUs The seek time.
|
||||
* @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end
|
||||
* of the buffer.
|
||||
* @return Whether the skip was successful.
|
||||
*/
|
||||
public boolean skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) {
|
||||
long nextOffset = infoQueue.skipToKeyframeBefore(timeUs, allowTimeBeyondBuffer);
|
||||
if (nextOffset == C.POSITION_UNSET) {
|
||||
return false;
|
||||
}
|
||||
@ -246,7 +265,8 @@ public final class DefaultTrackOutput implements TrackOutput {
|
||||
* @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
|
||||
* @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
|
||||
* end of the stream. If the end of the stream has been reached, the
|
||||
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.
|
||||
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. May be null if the
|
||||
* caller requires that the format of the stream be read even if it's not changing.
|
||||
* @param loadingFinished True if an empty queue should be considered the end of the stream.
|
||||
* @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will
|
||||
* be set if the buffer's timestamp is less than this value.
|
||||
@ -732,7 +752,8 @@ public final class DefaultTrackOutput implements TrackOutput {
|
||||
* about the sample, but not its data. The size and absolute position of the data in the
|
||||
* rolling buffer is stored in {@code extrasHolder}, along with an encryption id if present
|
||||
* and the absolute position of the first byte that may still be required after the current
|
||||
* sample has been read.
|
||||
* sample has been read. May be null if the caller requires that the format of the stream be
|
||||
* read even if it's not changing.
|
||||
* @param downstreamFormat The current downstream {@link Format}. If the format of the next
|
||||
* sample is different to the current downstream format then a format will be read.
|
||||
* @param extrasHolder The holder into which extra sample information should be written.
|
||||
@ -742,14 +763,14 @@ public final class DefaultTrackOutput implements TrackOutput {
|
||||
public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
|
||||
Format downstreamFormat, BufferExtrasHolder extrasHolder) {
|
||||
if (queueSize == 0) {
|
||||
if (upstreamFormat != null && upstreamFormat != downstreamFormat) {
|
||||
if (upstreamFormat != null && (buffer == null || upstreamFormat != downstreamFormat)) {
|
||||
formatHolder.format = upstreamFormat;
|
||||
return C.RESULT_FORMAT_READ;
|
||||
}
|
||||
return C.RESULT_NOTHING_READ;
|
||||
}
|
||||
|
||||
if (formats[relativeReadIndex] != downstreamFormat) {
|
||||
if (buffer == null || formats[relativeReadIndex] != downstreamFormat) {
|
||||
formatHolder.format = formats[relativeReadIndex];
|
||||
return C.RESULT_FORMAT_READ;
|
||||
}
|
||||
@ -775,18 +796,22 @@ public final class DefaultTrackOutput implements TrackOutput {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to locate the keyframe before the specified time, if it's present in the buffer.
|
||||
* Attempts to locate the keyframe before or at the specified time. If
|
||||
* {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs}
|
||||
* falls within the buffer.
|
||||
*
|
||||
* @param timeUs The seek time.
|
||||
* @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end
|
||||
* of the buffer.
|
||||
* @return The offset of the keyframe's data if the keyframe was present.
|
||||
* {@link C#POSITION_UNSET} otherwise.
|
||||
*/
|
||||
public synchronized long skipToKeyframeBefore(long timeUs) {
|
||||
public synchronized long skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) {
|
||||
if (queueSize == 0 || timeUs < timesUs[relativeReadIndex]) {
|
||||
return C.POSITION_UNSET;
|
||||
}
|
||||
|
||||
if (timeUs > largestQueuedTimestampUs) {
|
||||
if (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer) {
|
||||
return C.POSITION_UNSET;
|
||||
}
|
||||
|
||||
|
@ -51,6 +51,34 @@ public final class TimestampAdjuster {
|
||||
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.
|
||||
*/
|
||||
|
@ -127,6 +127,7 @@ import java.util.List;
|
||||
public static final int TYPE_mean = Util.getIntegerCodeForString("mean");
|
||||
public static final int TYPE_name = Util.getIntegerCodeForString("name");
|
||||
public static final int TYPE_data = Util.getIntegerCodeForString("data");
|
||||
public static final int TYPE_emsg = Util.getIntegerCodeForString("emsg");
|
||||
public static final int TYPE_st3d = Util.getIntegerCodeForString("st3d");
|
||||
public static final int TYPE_sv3d = Util.getIntegerCodeForString("sv3d");
|
||||
public static final int TYPE_proj = Util.getIntegerCodeForString("proj");
|
||||
|
@ -20,6 +20,7 @@ import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.util.SparseArray;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
|
||||
@ -44,6 +45,7 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Stack;
|
||||
import java.util.UUID;
|
||||
@ -73,7 +75,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
*/
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@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 {}
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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
|
||||
* 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 =
|
||||
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 byte[] extendedTypeScratch;
|
||||
private final Stack<ContainerAtom> containerAtoms;
|
||||
private final LinkedList<MetadataSampleInfo> pendingMetadataSampleInfos;
|
||||
|
||||
private int parserState;
|
||||
private int atomType;
|
||||
@ -130,8 +138,10 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
private int atomHeaderBytesRead;
|
||||
private ParsableByteArray atomData;
|
||||
private long endOfMdatPosition;
|
||||
private int pendingMetadataSampleBytes;
|
||||
|
||||
private long durationUs;
|
||||
private long segmentIndexEarliestPresentationTimeUs;
|
||||
private TrackBundle currentTrackBundle;
|
||||
private int sampleSize;
|
||||
private int sampleBytesWritten;
|
||||
@ -139,6 +149,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
|
||||
// Extractor output.
|
||||
private ExtractorOutput extractorOutput;
|
||||
private TrackOutput eventMessageTrackOutput;
|
||||
|
||||
// Whether extractorOutput.seekMap has been called.
|
||||
private boolean haveOutputSeekMap;
|
||||
@ -172,8 +183,10 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
encryptionSignalByte = new ParsableByteArray(1);
|
||||
extendedTypeScratch = new byte[16];
|
||||
containerAtoms = new Stack<>();
|
||||
pendingMetadataSampleInfos = new LinkedList<>();
|
||||
trackBundles = new SparseArray<>();
|
||||
durationUs = C.TIME_UNSET;
|
||||
segmentIndexEarliestPresentationTimeUs = C.TIME_UNSET;
|
||||
enterReadingAtomHeaderState();
|
||||
}
|
||||
|
||||
@ -189,6 +202,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
TrackBundle bundle = new TrackBundle(output.track(0));
|
||||
bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0));
|
||||
trackBundles.put(0, bundle);
|
||||
maybeInitEventMessageTrack();
|
||||
extractorOutput.endTracks();
|
||||
}
|
||||
}
|
||||
@ -199,6 +213,8 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
trackBundles.valueAt(i).reset();
|
||||
}
|
||||
pendingMetadataSampleInfos.clear();
|
||||
pendingMetadataSampleBytes = 0;
|
||||
containerAtoms.clear();
|
||||
enterReadingAtomHeaderState();
|
||||
}
|
||||
@ -336,9 +352,12 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
if (!containerAtoms.isEmpty()) {
|
||||
containerAtoms.peek().add(leaf);
|
||||
} else if (leaf.type == Atom.TYPE_sidx) {
|
||||
ChunkIndex segmentIndex = parseSidx(leaf.data, inputPosition);
|
||||
extractorOutput.seekMap(segmentIndex);
|
||||
Pair<Long, ChunkIndex> result = parseSidx(leaf.data, inputPosition);
|
||||
segmentIndexEarliestPresentationTimeUs = result.first;
|
||||
extractorOutput.seekMap(result.second);
|
||||
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)));
|
||||
durationUs = Math.max(durationUs, track.durationUs);
|
||||
}
|
||||
maybeInitEventMessageTrack();
|
||||
extractorOutput.endTracks();
|
||||
} else {
|
||||
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).
|
||||
*/
|
||||
@ -628,7 +689,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
DefaultSampleValues defaultSampleValues = trackBundle.defaultSampleValues;
|
||||
int defaultSampleDescriptionIndex =
|
||||
((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)
|
||||
? tfhd.readUnsignedIntToInt() : defaultSampleValues.duration;
|
||||
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).
|
||||
*
|
||||
* @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 {
|
||||
atom.setPosition(Atom.HEADER_SIZE);
|
||||
int fullAtom = atom.readInt();
|
||||
@ -850,6 +916,8 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
earliestPresentationTime = atom.readUnsignedLongToLong();
|
||||
offset += atom.readUnsignedLongToLong();
|
||||
}
|
||||
long earliestPresentationTimeUs = Util.scaleLargeTimestamp(earliestPresentationTime,
|
||||
C.MICROS_PER_SECOND, timescale);
|
||||
|
||||
atom.skipBytes(2);
|
||||
|
||||
@ -860,7 +928,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
long[] timesUs = new long[referenceCount];
|
||||
|
||||
long time = earliestPresentationTime;
|
||||
long timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale);
|
||||
long timeUs = earliestPresentationTimeUs;
|
||||
for (int i = 0; i < referenceCount; i++) {
|
||||
int firstInt = atom.readInt();
|
||||
|
||||
@ -884,7 +952,8 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
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 {
|
||||
@ -946,13 +1015,9 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
// We skip bytes preceding the next sample to read.
|
||||
int bytesToSkip = (int) (nextDataPosition - input.getPosition());
|
||||
if (bytesToSkip < 0) {
|
||||
if (nextDataPosition == currentTrackBundle.fragment.atomPosition) {
|
||||
// Assume the sample data must be contiguous in the mdat with no preceeding data.
|
||||
Log.w(TAG, "Offset to sample data was missing.");
|
||||
bytesToSkip = 0;
|
||||
} else {
|
||||
throw new ParserException("Offset to sample data was negative.");
|
||||
}
|
||||
// Assume the sample data must be contiguous in the mdat with no preceding data.
|
||||
Log.w(TAG, "Ignoring negative offset to sample data.");
|
||||
bytesToSkip = 0;
|
||||
}
|
||||
input.skipFully(bytesToSkip);
|
||||
this.currentTrackBundle = currentTrackBundle;
|
||||
@ -1029,6 +1094,14 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
}
|
||||
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.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_saio || atom == Atom.TYPE_senc || atom == Atom.TYPE_uuid
|
||||
|| 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}. */
|
||||
@ -1144,6 +1217,21 @@ public final class FragmentedMp4Extractor implements Extractor {
|
||||
|| 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.
|
||||
*/
|
||||
|
@ -188,7 +188,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||
if (atomType == Atom.TYPE_data) {
|
||||
data.skipBytes(8); // version (1), flags (3), empty (4)
|
||||
String value = data.readNullTerminatedString(atomSize - 16);
|
||||
return new TextInformationFrame(id, value);
|
||||
return new TextInformationFrame(id, null, value);
|
||||
}
|
||||
Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type));
|
||||
return null;
|
||||
@ -213,7 +213,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||
value = Math.min(1, value);
|
||||
}
|
||||
if (value >= 0) {
|
||||
return isTextInformationFrame ? new TextInformationFrame(id, Integer.toString(value))
|
||||
return isTextInformationFrame ? new TextInformationFrame(id, null, Integer.toString(value))
|
||||
: new CommentFrame(LANGUAGE_UNDEFINED, id, Integer.toString(value));
|
||||
}
|
||||
Log.w(TAG, "Failed to parse uint8 attribute: " + Atom.getAtomTypeString(type));
|
||||
@ -228,12 +228,12 @@ import com.google.android.exoplayer2.util.Util;
|
||||
data.skipBytes(10); // version (1), flags (3), empty (4), empty (2)
|
||||
int index = data.readUnsignedShort();
|
||||
if (index > 0) {
|
||||
String description = "" + index;
|
||||
String value = "" + index;
|
||||
int count = data.readUnsignedShort();
|
||||
if (count > 0) {
|
||||
description += "/" + count;
|
||||
value += "/" + count;
|
||||
}
|
||||
return new TextInformationFrame(attributeName, description);
|
||||
return new TextInformationFrame(attributeName, null, value);
|
||||
}
|
||||
}
|
||||
Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type));
|
||||
@ -245,7 +245,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||
String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length)
|
||||
? STANDARD_GENRES[genreCode - 1] : null;
|
||||
if (genreString != null) {
|
||||
return new TextInformationFrame("TCON", genreString);
|
||||
return new TextInformationFrame("TCON", null, genreString);
|
||||
}
|
||||
Log.w(TAG, "Failed to parse standard genre code");
|
||||
return null;
|
||||
|
@ -83,8 +83,11 @@ public final class RawCcExtractor implements Extractor {
|
||||
while (true) {
|
||||
switch (parserState) {
|
||||
case STATE_READING_HEADER:
|
||||
parseHeader(input);
|
||||
parserState = STATE_READING_TIMESTAMP_AND_COUNT;
|
||||
if (parseHeader(input)) {
|
||||
parserState = STATE_READING_TIMESTAMP_AND_COUNT;
|
||||
} else {
|
||||
return RESULT_END_OF_INPUT;
|
||||
}
|
||||
break;
|
||||
case STATE_READING_TIMESTAMP_AND_COUNT:
|
||||
if (parseTimestampAndSampleCount(input)) {
|
||||
@ -114,14 +117,18 @@ public final class RawCcExtractor implements Extractor {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
private void parseHeader(ExtractorInput input) throws IOException, InterruptedException {
|
||||
private boolean parseHeader(ExtractorInput input) throws IOException, InterruptedException {
|
||||
dataScratch.reset();
|
||||
input.readFully(dataScratch.data, 0, HEADER_SIZE);
|
||||
if (dataScratch.readInt() != HEADER_ID) {
|
||||
throw new IOException("Input not RawCC");
|
||||
if (input.readFully(dataScratch.data, 0, HEADER_SIZE, true)) {
|
||||
if (dataScratch.readInt() != HEADER_ID) {
|
||||
throw new IOException("Input not RawCC");
|
||||
}
|
||||
version = dataScratch.readUnsignedByte();
|
||||
// no versions use the flag fields yet
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
version = dataScratch.readUnsignedByte();
|
||||
// no versions use the flag fields yet
|
||||
}
|
||||
|
||||
private boolean parseTimestampAndSampleCount(ExtractorInput input) throws IOException,
|
||||
|
@ -28,11 +28,14 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
*/
|
||||
public final class SpliceInfoSectionReader implements SectionPayloadReader {
|
||||
|
||||
private TimestampAdjuster timestampAdjuster;
|
||||
private TrackOutput output;
|
||||
private boolean formatDeclared;
|
||||
|
||||
@Override
|
||||
public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
|
||||
TsPayloadReader.TrackIdGenerator idGenerator) {
|
||||
this.timestampAdjuster = timestampAdjuster;
|
||||
output = extractorOutput.track(idGenerator.getNextId());
|
||||
output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35, null,
|
||||
Format.NO_VALUE, null));
|
||||
@ -40,9 +43,19 @@ public final class SpliceInfoSectionReader implements SectionPayloadReader {
|
||||
|
||||
@Override
|
||||
public void consume(ParsableByteArray sectionData) {
|
||||
if (!formatDeclared) {
|
||||
if (timestampAdjuster.getTimestampOffsetUs() == C.TIME_UNSET) {
|
||||
// There is not enough information to initialize the timestamp adjuster.
|
||||
return;
|
||||
}
|
||||
output.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_SCTE35,
|
||||
timestampAdjuster.getTimestampOffsetUs()));
|
||||
formatDeclared = true;
|
||||
}
|
||||
int sampleSize = sectionData.bytesLeft();
|
||||
output.sampleData(sectionData, sampleSize);
|
||||
output.sampleMetadata(0, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
|
||||
output.sampleMetadata(timestampAdjuster.getLastAdjustedTimestampUs(), C.BUFFER_FLAG_KEY_FRAME,
|
||||
sampleSize, 0, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -270,7 +270,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
||||
*/
|
||||
protected MediaCodecInfo getDecoderInfo(MediaCodecSelector mediaCodecSelector,
|
||||
Format format, boolean requiresSecureDecoder) throws DecoderQueryException {
|
||||
return mediaCodecSelector.getDecoderInfo(format.sampleMimeType, requiresSecureDecoder, false);
|
||||
return mediaCodecSelector.getDecoderInfo(format.sampleMimeType, requiresSecureDecoder);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -29,9 +29,9 @@ public interface MediaCodecSelector {
|
||||
MediaCodecSelector DEFAULT = new MediaCodecSelector() {
|
||||
|
||||
@Override
|
||||
public MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder,
|
||||
boolean requiresTunneling) throws DecoderQueryException {
|
||||
return MediaCodecUtil.getDecoderInfo(mimeType, requiresSecureDecoder, requiresTunneling);
|
||||
public MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder)
|
||||
throws DecoderQueryException {
|
||||
return MediaCodecUtil.getDecoderInfo(mimeType, requiresSecureDecoder);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -46,13 +46,11 @@ public interface MediaCodecSelector {
|
||||
*
|
||||
* @param mimeType The mime type for which a decoder is required.
|
||||
* @param requiresSecureDecoder Whether a secure decoder is required.
|
||||
* @param requiresTunneling Whether a decoder that supports tunneling is required.
|
||||
* @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder
|
||||
* exists.
|
||||
* @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder exists.
|
||||
* @throws DecoderQueryException Thrown if there was an error querying decoders.
|
||||
*/
|
||||
MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder,
|
||||
boolean requiresTunneling) throws DecoderQueryException;
|
||||
MediaCodecInfo getDecoderInfo(String mimeType, boolean requiresSecureDecoder)
|
||||
throws DecoderQueryException;
|
||||
|
||||
/**
|
||||
* Selects a decoder to instantiate for audio passthrough.
|
||||
|
@ -81,9 +81,8 @@ public final class MediaCodecUtil {
|
||||
/**
|
||||
* Optional call to warm the codec cache for a given mime type.
|
||||
* <p>
|
||||
* Calling this method may speed up subsequent calls to
|
||||
* {@link #getDecoderInfo(String, boolean, boolean)} and
|
||||
* {@link #getDecoderInfos(String, boolean)}.
|
||||
* Calling this method may speed up subsequent calls to {@link #getDecoderInfo(String, boolean)}
|
||||
* and {@link #getDecoderInfos(String, boolean)}.
|
||||
*
|
||||
* @param mimeType The mime type.
|
||||
* @param secure Whether the decoder is required to support secure decryption. Always pass false
|
||||
@ -115,26 +114,14 @@ public final class MediaCodecUtil {
|
||||
* @param mimeType The mime type.
|
||||
* @param secure Whether the decoder is required to support secure decryption. Always pass false
|
||||
* unless secure decryption really is required.
|
||||
* @param tunneling Whether the decoder is required to support tunneling. Always pass false unless
|
||||
* tunneling really is required.
|
||||
* @return A {@link MediaCodecInfo} describing the decoder, or null if no suitable decoder
|
||||
* exists.
|
||||
* @throws DecoderQueryException If there was an error querying the available decoders.
|
||||
*/
|
||||
public static MediaCodecInfo getDecoderInfo(String mimeType, boolean secure, boolean tunneling)
|
||||
public static MediaCodecInfo getDecoderInfo(String mimeType, boolean secure)
|
||||
throws DecoderQueryException {
|
||||
List<MediaCodecInfo> decoderInfos = getDecoderInfos(mimeType, secure);
|
||||
if (tunneling) {
|
||||
for (int i = 0; i < decoderInfos.size(); i++) {
|
||||
MediaCodecInfo decoderInfo = decoderInfos.get(i);
|
||||
if (decoderInfo.tunneling) {
|
||||
return decoderInfo;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
return decoderInfos.isEmpty() ? null : decoderInfos.get(0);
|
||||
}
|
||||
return decoderInfos.isEmpty() ? null : decoderInfos.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -305,7 +292,7 @@ public final class MediaCodecUtil {
|
||||
public static int maxH264DecodableFrameSize() throws DecoderQueryException {
|
||||
if (maxH264DecodableFrameSize == -1) {
|
||||
int result = 0;
|
||||
MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false, false);
|
||||
MediaCodecInfo decoderInfo = getDecoderInfo(MimeTypes.VIDEO_H264, false);
|
||||
if (decoderInfo != null) {
|
||||
for (CodecProfileLevel profileLevel : decoderInfo.getProfileLevels()) {
|
||||
result = Math.max(avcLevelToMaxFrameSize(profileLevel.level), result);
|
||||
|
@ -21,21 +21,12 @@ package com.google.android.exoplayer2.metadata;
|
||||
public interface MetadataDecoder {
|
||||
|
||||
/**
|
||||
* Checks whether the decoder supports a given mime type.
|
||||
* Decodes a {@link Metadata} element from the provided input buffer.
|
||||
*
|
||||
* @param mimeType A metadata mime type.
|
||||
* @return Whether the mime type is supported.
|
||||
*/
|
||||
boolean canDecode(String mimeType);
|
||||
|
||||
/**
|
||||
* Decodes a metadata object from the provided binary data.
|
||||
*
|
||||
* @param data The raw binary data from which to decode the metadata.
|
||||
* @param size The size of the input data.
|
||||
* @param inputBuffer The input buffer to decode.
|
||||
* @return The decoded metadata object.
|
||||
* @throws MetadataDecoderException If a problem occurred decoding the data.
|
||||
*/
|
||||
Metadata decode(byte[] data, int size) throws MetadataDecoderException;
|
||||
Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException;
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -24,9 +24,7 @@ import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.FormatHolder;
|
||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* A renderer for metadata.
|
||||
@ -49,12 +47,13 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
|
||||
|
||||
private static final int MSG_INVOKE_RENDERER = 0;
|
||||
|
||||
private final MetadataDecoder metadataDecoder;
|
||||
private final MetadataDecoderFactory decoderFactory;
|
||||
private final Output output;
|
||||
private final Handler outputHandler;
|
||||
private final FormatHolder formatHolder;
|
||||
private final DecoderInputBuffer buffer;
|
||||
private final MetadataInputBuffer buffer;
|
||||
|
||||
private MetadataDecoder decoder;
|
||||
private boolean inputStreamEnded;
|
||||
private long pendingMetadataTimestamp;
|
||||
private Metadata pendingMetadata;
|
||||
@ -66,21 +65,38 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
|
||||
* looper associated with the application's main thread, which can be obtained using
|
||||
* {@link android.app.Activity#getMainLooper()}. Null may be passed if the output should be
|
||||
* called directly on the player's internal rendering thread.
|
||||
* @param metadataDecoder A decoder for the metadata.
|
||||
*/
|
||||
public MetadataRenderer(Output output, Looper outputLooper, MetadataDecoder metadataDecoder) {
|
||||
public MetadataRenderer(Output output, Looper outputLooper) {
|
||||
this(output, outputLooper, MetadataDecoderFactory.DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param output The output.
|
||||
* @param outputLooper The looper associated with the thread on which the output should be called.
|
||||
* If the output makes use of standard Android UI components, then this should normally be the
|
||||
* looper associated with the application's main thread, which can be obtained using
|
||||
* {@link android.app.Activity#getMainLooper()}. Null may be passed if the output should be
|
||||
* called directly on the player's internal rendering thread.
|
||||
* @param decoderFactory A factory from which to obtain {@link MetadataDecoder} instances.
|
||||
*/
|
||||
public MetadataRenderer(Output output, Looper outputLooper,
|
||||
MetadataDecoderFactory decoderFactory) {
|
||||
super(C.TRACK_TYPE_METADATA);
|
||||
this.output = Assertions.checkNotNull(output);
|
||||
this.outputHandler = outputLooper == null ? null : new Handler(outputLooper, this);
|
||||
this.metadataDecoder = Assertions.checkNotNull(metadataDecoder);
|
||||
this.decoderFactory = Assertions.checkNotNull(decoderFactory);
|
||||
formatHolder = new FormatHolder();
|
||||
buffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
|
||||
buffer = new MetadataInputBuffer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int supportsFormat(Format format) {
|
||||
return metadataDecoder.canDecode(format.sampleMimeType) ? FORMAT_HANDLED
|
||||
: FORMAT_UNSUPPORTED_TYPE;
|
||||
return decoderFactory.supportsFormat(format) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStreamChanged(Format[] formats) throws ExoPlaybackException {
|
||||
decoder = decoderFactory.createDecoder(formats[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -97,12 +113,16 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
|
||||
if (result == C.RESULT_BUFFER_READ) {
|
||||
if (buffer.isEndOfStream()) {
|
||||
inputStreamEnded = true;
|
||||
} else if (buffer.isDecodeOnly()) {
|
||||
// Do nothing. Note this assumes that all metadata buffers can be decoded independently.
|
||||
// If we ever need to support a metadata format where this is not the case, we'll need to
|
||||
// pass the buffer to the decoder and discard the output.
|
||||
} else {
|
||||
pendingMetadataTimestamp = buffer.timeUs;
|
||||
buffer.subsampleOffsetUs = formatHolder.format.subsampleOffsetUs;
|
||||
buffer.flip();
|
||||
try {
|
||||
buffer.flip();
|
||||
ByteBuffer bufferData = buffer.data;
|
||||
pendingMetadata = metadataDecoder.decode(bufferData.array(), bufferData.limit());
|
||||
pendingMetadata = decoder.decode(buffer);
|
||||
} catch (MetadataDecoderException e) {
|
||||
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
||||
}
|
||||
@ -119,6 +139,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
|
||||
@Override
|
||||
protected void onDisabled() {
|
||||
pendingMetadata = null;
|
||||
decoder = null;
|
||||
super.onDisabled();
|
||||
}
|
||||
|
||||
|
@ -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];
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
@ -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];
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
@ -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];
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
@ -16,12 +16,14 @@
|
||||
package com.google.android.exoplayer2.metadata.id3;
|
||||
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
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.Util;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@ -49,11 +51,18 @@ public final class Id3Decoder implements MetadataDecoder {
|
||||
private static final int ID3_TEXT_ENCODING_UTF_8 = 3;
|
||||
|
||||
@Override
|
||||
public boolean canDecode(String mimeType) {
|
||||
return mimeType.equals(MimeTypes.APPLICATION_ID3);
|
||||
public Metadata decode(MetadataInputBuffer inputBuffer) {
|
||||
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) {
|
||||
List<Id3Frame> id3Frames = new ArrayList<>();
|
||||
ParsableByteArray id3Data = new ParsableByteArray(data, size);
|
||||
@ -84,7 +93,8 @@ public final class Id3Decoder implements MetadataDecoder {
|
||||
|
||||
int frameHeaderSize = id3Header.majorVersion == 2 ? 6 : 10;
|
||||
while (id3Data.bytesLeft() >= frameHeaderSize) {
|
||||
Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack);
|
||||
Id3Frame frame = decodeFrame(id3Header.majorVersion, id3Data, unsignedIntFrameSizeHack,
|
||||
frameHeaderSize);
|
||||
if (frame != null) {
|
||||
id3Frames.add(frame);
|
||||
}
|
||||
@ -190,7 +200,7 @@ public final class Id3Decoder implements MetadataDecoder {
|
||||
}
|
||||
|
||||
private static Id3Frame decodeFrame(int majorVersion, ParsableByteArray id3Data,
|
||||
boolean unsignedIntFrameSizeHack) {
|
||||
boolean unsignedIntFrameSizeHack, int frameHeaderSize) {
|
||||
int frameId0 = id3Data.readUnsignedByte();
|
||||
int frameId1 = id3Data.readUnsignedByte();
|
||||
int frameId2 = id3Data.readUnsignedByte();
|
||||
@ -266,6 +276,19 @@ public final class Id3Decoder implements MetadataDecoder {
|
||||
if (frameId0 == 'T' && frameId1 == 'X' && frameId2 == 'X'
|
||||
&& (majorVersion == 2 || frameId3 == 'X')) {
|
||||
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') {
|
||||
frame = decodePrivFrame(id3Data, frameSize);
|
||||
} 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')
|
||||
: (frameId0 == 'A' && frameId1 == 'P' && frameId2 == 'I' && frameId3 == 'C')) {
|
||||
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'
|
||||
&& (frameId3 == 'M' || majorVersion == 2)) {
|
||||
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 {
|
||||
String id = majorVersion == 2
|
||||
? 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 {
|
||||
int encoding = id3Data.readUnsignedByte();
|
||||
String charset = getCharsetName(encoding);
|
||||
@ -308,11 +332,74 @@ public final class Id3Decoder implements MetadataDecoder {
|
||||
int descriptionEndIndex = indexOfEos(data, 0, encoding);
|
||||
String description = new String(data, 0, descriptionEndIndex, charset);
|
||||
|
||||
String value;
|
||||
int valueStartIndex = descriptionEndIndex + delimiterLength(encoding);
|
||||
int valueEndIndex = indexOfEos(data, valueStartIndex, encoding);
|
||||
String value = new String(data, valueStartIndex, valueEndIndex - valueStartIndex, charset);
|
||||
if (valueStartIndex < data.length) {
|
||||
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)
|
||||
@ -408,25 +495,88 @@ public final class Id3Decoder implements MetadataDecoder {
|
||||
int descriptionEndIndex = indexOfEos(data, 0, encoding);
|
||||
String description = new String(data, 0, descriptionEndIndex, charset);
|
||||
|
||||
String text;
|
||||
int textStartIndex = descriptionEndIndex + delimiterLength(encoding);
|
||||
int textEndIndex = indexOfEos(data, textStartIndex, encoding);
|
||||
String text = new String(data, textStartIndex, textEndIndex - textStartIndex, charset);
|
||||
if (textStartIndex < data.length) {
|
||||
int textEndIndex = indexOfEos(data, textStartIndex, encoding);
|
||||
text = new String(data, textStartIndex, textEndIndex - textStartIndex, charset);
|
||||
} else {
|
||||
text = "";
|
||||
}
|
||||
|
||||
return new CommentFrame(language, description, text);
|
||||
}
|
||||
|
||||
private static TextInformationFrame decodeTextInformationFrame(ParsableByteArray id3Data,
|
||||
int frameSize, String id) throws UnsupportedEncodingException {
|
||||
int encoding = id3Data.readUnsignedByte();
|
||||
String charset = getCharsetName(encoding);
|
||||
private static ChapterFrame decodeChapterFrame(ParsableByteArray id3Data, int frameSize,
|
||||
int majorVersion, boolean unsignedIntFrameSizeHack, int frameHeaderSize)
|
||||
throws UnsupportedEncodingException {
|
||||
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];
|
||||
id3Data.readBytes(data, 0, frameSize - 1);
|
||||
int startTime = id3Data.readInt();
|
||||
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);
|
||||
String description = new String(data, 0, descriptionEndIndex, charset);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@ -458,6 +608,7 @@ public final class Id3Decoder implements MetadataDecoder {
|
||||
|
||||
/**
|
||||
* Maps encoding byte from ID3v2 frame to a Charset.
|
||||
*
|
||||
* @param encodingByte The value of encoding byte from ID3v2 frame.
|
||||
* @return Charset name.
|
||||
*/
|
||||
|
@ -20,20 +20,23 @@ import android.os.Parcelable;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/**
|
||||
* Text information ("T000" - "TZZZ", excluding "TXXX") ID3 frame.
|
||||
* Text information ID3 frame.
|
||||
*/
|
||||
public final class TextInformationFrame extends Id3Frame {
|
||||
|
||||
public final String description;
|
||||
public final String value;
|
||||
|
||||
public TextInformationFrame(String id, String description) {
|
||||
public TextInformationFrame(String id, String description, String value) {
|
||||
super(id);
|
||||
this.description = description;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/* package */ TextInformationFrame(Parcel in) {
|
||||
super(in.readString());
|
||||
description = in.readString();
|
||||
value = in.readString();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -45,7 +48,8 @@ public final class TextInformationFrame extends Id3Frame {
|
||||
return false;
|
||||
}
|
||||
TextInformationFrame other = (TextInformationFrame) obj;
|
||||
return id.equals(other.id) && Util.areEqual(description, other.description);
|
||||
return id.equals(other.id) && Util.areEqual(description, other.description)
|
||||
&& Util.areEqual(value, other.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -53,6 +57,7 @@ public final class TextInformationFrame extends Id3Frame {
|
||||
int result = 17;
|
||||
result = 31 * result + id.hashCode();
|
||||
result = 31 * result + (description != null ? description.hashCode() : 0);
|
||||
result = 31 * result + (value != null ? value.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -60,6 +65,7 @@ public final class TextInformationFrame extends Id3Frame {
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(id);
|
||||
dest.writeString(description);
|
||||
dest.writeString(value);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<TextInformationFrame> CREATOR =
|
||||
|
@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -20,25 +20,23 @@ import android.os.Parcelable;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/**
|
||||
* TXXX (User defined text information) ID3 frame.
|
||||
* Url link ID3 frame.
|
||||
*/
|
||||
public final class TxxxFrame extends Id3Frame {
|
||||
|
||||
public static final String ID = "TXXX";
|
||||
public final class UrlLinkFrame extends Id3Frame {
|
||||
|
||||
public final String description;
|
||||
public final String value;
|
||||
public final String url;
|
||||
|
||||
public TxxxFrame(String description, String value) {
|
||||
super(ID);
|
||||
public UrlLinkFrame(String id, String description, String url) {
|
||||
super(id);
|
||||
this.description = description;
|
||||
this.value = value;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
/* package */ TxxxFrame(Parcel in) {
|
||||
super(ID);
|
||||
/* package */ UrlLinkFrame(Parcel in) {
|
||||
super(in.readString());
|
||||
description = in.readString();
|
||||
value = in.readString();
|
||||
url = in.readString();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -49,36 +47,40 @@ public final class TxxxFrame extends Id3Frame {
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
TxxxFrame other = (TxxxFrame) obj;
|
||||
return Util.areEqual(description, other.description) && Util.areEqual(value, other.value);
|
||||
UrlLinkFrame other = (UrlLinkFrame) obj;
|
||||
return id.equals(other.id) && Util.areEqual(description, other.description)
|
||||
&& Util.areEqual(url, other.url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = 17;
|
||||
result = 31 * result + id.hashCode();
|
||||
result = 31 * result + (description != null ? description.hashCode() : 0);
|
||||
result = 31 * result + (value != null ? value.hashCode() : 0);
|
||||
result = 31 * result + (url != null ? url.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(id);
|
||||
dest.writeString(description);
|
||||
dest.writeString(value);
|
||||
dest.writeString(url);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<TxxxFrame> CREATOR = new Parcelable.Creator<TxxxFrame>() {
|
||||
public static final Parcelable.Creator<UrlLinkFrame> CREATOR =
|
||||
new Parcelable.Creator<UrlLinkFrame>() {
|
||||
|
||||
@Override
|
||||
public TxxxFrame createFromParcel(Parcel in) {
|
||||
return new TxxxFrame(in);
|
||||
}
|
||||
@Override
|
||||
public UrlLinkFrame createFromParcel(Parcel in) {
|
||||
return new UrlLinkFrame(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TxxxFrame[] newArray(int size) {
|
||||
return new TxxxFrame[size];
|
||||
}
|
||||
@Override
|
||||
public UrlLinkFrame[] newArray(int size) {
|
||||
return new UrlLinkFrame[size];
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
}
|
@ -15,13 +15,13 @@
|
||||
*/
|
||||
package com.google.android.exoplayer2.metadata.scte35;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.MetadataDecoder;
|
||||
import com.google.android.exoplayer2.metadata.MetadataDecoderException;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
|
||||
import com.google.android.exoplayer2.util.ParsableBitArray;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Decodes splice info sections and produces splice commands.
|
||||
@ -43,12 +43,10 @@ public final class SpliceInfoDecoder implements MetadataDecoder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canDecode(String mimeType) {
|
||||
return TextUtils.equals(mimeType, MimeTypes.APPLICATION_SCTE35);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Metadata decode(byte[] data, int size) throws MetadataDecoderException {
|
||||
public Metadata decode(MetadataInputBuffer inputBuffer) throws MetadataDecoderException {
|
||||
ByteBuffer buffer = inputBuffer.data;
|
||||
byte[] data = buffer.array();
|
||||
int size = buffer.limit();
|
||||
sectionData.reset(data, size);
|
||||
sectionHeader.reset(data, size);
|
||||
// table_id(8), section_syntax_indicator(1), private_indicator(1), reserved(2),
|
||||
|
@ -26,10 +26,12 @@ import java.io.IOException;
|
||||
* Wraps a {@link MediaPeriod} and clips its {@link SampleStream}s to provide a subsequence of their
|
||||
* samples.
|
||||
*/
|
||||
/* package */ final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
|
||||
public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
|
||||
|
||||
/**
|
||||
* The {@link MediaPeriod} wrapped by this clipping media period.
|
||||
*/
|
||||
public final MediaPeriod mediaPeriod;
|
||||
private final ClippingMediaSource mediaSource;
|
||||
|
||||
private MediaPeriod.Callback callback;
|
||||
private long startUs;
|
||||
@ -40,18 +42,31 @@ import java.io.IOException;
|
||||
/**
|
||||
* Creates a new clipping media period that provides a clipped view of the specified
|
||||
* {@link MediaPeriod}'s sample streams.
|
||||
* <p>
|
||||
* The clipping start/end positions must be specified by calling {@link #setClipping(long, long)}
|
||||
* on the playback thread before preparation completes.
|
||||
*
|
||||
* @param mediaPeriod The media period to clip.
|
||||
* @param mediaSource The {@link ClippingMediaSource} to which this period belongs.
|
||||
*/
|
||||
public ClippingMediaPeriod(MediaPeriod mediaPeriod, ClippingMediaSource mediaSource) {
|
||||
public ClippingMediaPeriod(MediaPeriod mediaPeriod) {
|
||||
this.mediaPeriod = mediaPeriod;
|
||||
this.mediaSource = mediaSource;
|
||||
startUs = C.TIME_UNSET;
|
||||
endUs = C.TIME_UNSET;
|
||||
sampleStreams = new ClippingSampleStream[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the clipping start/end times for this period, in microseconds.
|
||||
*
|
||||
* @param startUs The clipping start time, in microseconds.
|
||||
* @param endUs The clipping end time, in microseconds, or {@link C#TIME_END_OF_SOURCE} to
|
||||
* indicate the end of the period.
|
||||
*/
|
||||
public void setClipping(long startUs, long endUs) {
|
||||
this.startUs = startUs;
|
||||
this.endUs = endUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare(MediaPeriod.Callback callback) {
|
||||
this.callback = callback;
|
||||
@ -80,7 +95,8 @@ import java.io.IOException;
|
||||
long enablePositionUs = mediaPeriod.selectTracks(selections, mayRetainStreamFlags,
|
||||
internalStreams, streamResetFlags, positionUs + startUs);
|
||||
Assertions.checkState(enablePositionUs == positionUs + startUs
|
||||
|| (enablePositionUs >= startUs && enablePositionUs <= endUs));
|
||||
|| (enablePositionUs >= startUs
|
||||
&& (endUs == C.TIME_END_OF_SOURCE || enablePositionUs <= endUs)));
|
||||
for (int i = 0; i < streams.length; i++) {
|
||||
if (internalStreams[i] == null) {
|
||||
sampleStreams[i] = null;
|
||||
@ -110,14 +126,16 @@ import java.io.IOException;
|
||||
if (discontinuityUs == C.TIME_UNSET) {
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
Assertions.checkState(discontinuityUs >= startUs && discontinuityUs <= endUs);
|
||||
Assertions.checkState(discontinuityUs >= startUs);
|
||||
Assertions.checkState(endUs == C.TIME_END_OF_SOURCE || discontinuityUs <= endUs);
|
||||
return discontinuityUs - startUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getBufferedPositionUs() {
|
||||
long bufferedPositionUs = mediaPeriod.getBufferedPositionUs();
|
||||
if (bufferedPositionUs == C.TIME_END_OF_SOURCE || bufferedPositionUs >= endUs) {
|
||||
if (bufferedPositionUs == C.TIME_END_OF_SOURCE
|
||||
|| (endUs != C.TIME_END_OF_SOURCE && bufferedPositionUs >= endUs)) {
|
||||
return C.TIME_END_OF_SOURCE;
|
||||
}
|
||||
return Math.max(0, bufferedPositionUs - startUs);
|
||||
@ -131,14 +149,16 @@ import java.io.IOException;
|
||||
}
|
||||
}
|
||||
long seekUs = mediaPeriod.seekToUs(positionUs + startUs);
|
||||
Assertions.checkState(seekUs == positionUs + startUs || (seekUs >= startUs && seekUs <= endUs));
|
||||
Assertions.checkState(seekUs == positionUs + startUs
|
||||
|| (seekUs >= startUs && (endUs == C.TIME_END_OF_SOURCE || seekUs <= endUs)));
|
||||
return seekUs - startUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getNextLoadPositionUs() {
|
||||
long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs();
|
||||
if (nextLoadPositionUs == C.TIME_END_OF_SOURCE || nextLoadPositionUs >= endUs) {
|
||||
if (nextLoadPositionUs == C.TIME_END_OF_SOURCE
|
||||
|| (endUs != C.TIME_END_OF_SOURCE && nextLoadPositionUs >= endUs)) {
|
||||
return C.TIME_END_OF_SOURCE;
|
||||
}
|
||||
return nextLoadPositionUs - startUs;
|
||||
@ -153,8 +173,6 @@ import java.io.IOException;
|
||||
|
||||
@Override
|
||||
public void onPrepared(MediaPeriod mediaPeriod) {
|
||||
startUs = mediaSource.getStartUs();
|
||||
endUs = mediaSource.getEndUs();
|
||||
Assertions.checkState(startUs != C.TIME_UNSET && endUs != C.TIME_UNSET);
|
||||
// If the clipping start position is non-zero, the clipping sample streams will adjust
|
||||
// timestamps on buffers they read from the unclipped sample streams. These adjusted buffer
|
||||
@ -217,21 +235,24 @@ import java.io.IOException;
|
||||
if (pendingDiscontinuity) {
|
||||
return C.RESULT_NOTHING_READ;
|
||||
}
|
||||
if (buffer == null) {
|
||||
return stream.readData(formatHolder, null);
|
||||
}
|
||||
if (sentEos) {
|
||||
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
|
||||
return C.RESULT_BUFFER_READ;
|
||||
}
|
||||
int result = stream.readData(formatHolder, buffer);
|
||||
// TODO: Clear gapless playback metadata if a format was read (if applicable).
|
||||
if ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs)
|
||||
|| (result == C.RESULT_NOTHING_READ
|
||||
&& mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE)) {
|
||||
if (endUs != C.TIME_END_OF_SOURCE && ((result == C.RESULT_BUFFER_READ
|
||||
&& buffer.timeUs >= endUs) || (result == C.RESULT_NOTHING_READ
|
||||
&& mediaPeriod.getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) {
|
||||
buffer.clear();
|
||||
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
|
||||
sentEos = true;
|
||||
return C.RESULT_BUFFER_READ;
|
||||
}
|
||||
if (result == C.RESULT_BUFFER_READ) {
|
||||
if (result == C.RESULT_BUFFER_READ && !buffer.isEndOfStream()) {
|
||||
buffer.timeUs -= startUs;
|
||||
}
|
||||
return result;
|
||||
|
@ -21,17 +21,19 @@ import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* {@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
|
||||
* (live). The specified start position must correspond to a synchronization sample in the period.
|
||||
* (live).
|
||||
*/
|
||||
public final class ClippingMediaSource implements MediaSource, MediaSource.Listener {
|
||||
|
||||
private final MediaSource mediaSource;
|
||||
private final long startUs;
|
||||
private final long endUs;
|
||||
private final ArrayList<ClippingMediaPeriod> mediaPeriods;
|
||||
|
||||
private MediaSource.Listener sourceListener;
|
||||
private ClippingTimeline clippingTimeline;
|
||||
@ -51,20 +53,7 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
|
||||
this.mediaSource = Assertions.checkNotNull(mediaSource);
|
||||
startUs = startPositionUs;
|
||||
endUs = endPositionUs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
mediaPeriods = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -80,12 +69,16 @@ public final class ClippingMediaSource implements MediaSource, MediaSource.Liste
|
||||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
|
||||
return new ClippingMediaPeriod(
|
||||
mediaSource.createPeriod(index, allocator, startUs + positionUs), this);
|
||||
ClippingMediaPeriod mediaPeriod = new ClippingMediaPeriod(
|
||||
mediaSource.createPeriod(index, allocator, startUs + positionUs));
|
||||
mediaPeriods.add(mediaPeriod);
|
||||
mediaPeriod.setClipping(clippingTimeline.startUs, clippingTimeline.endUs);
|
||||
return mediaPeriod;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releasePeriod(MediaPeriod mediaPeriod) {
|
||||
Assertions.checkState(mediaPeriods.remove(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) {
|
||||
clippingTimeline = new ClippingTimeline(timeline, startUs, endUs);
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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 startUs The number of microseconds to clip from the start of {@code timeline}.
|
||||
|
@ -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.ConditionVariable;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
|
||||
@ -62,6 +63,7 @@ import java.io.IOException;
|
||||
private final ExtractorMediaSource.EventListener eventListener;
|
||||
private final MediaSource.Listener sourceListener;
|
||||
private final Allocator allocator;
|
||||
private final String customCacheKey;
|
||||
private final Loader loader;
|
||||
private final ExtractorHolder extractorHolder;
|
||||
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 sourceListener A listener to notify when the timeline has been loaded.
|
||||
* @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,
|
||||
int minLoadableRetryCount, Handler eventHandler,
|
||||
ExtractorMediaSource.EventListener eventListener, MediaSource.Listener sourceListener,
|
||||
Allocator allocator) {
|
||||
Allocator allocator, String customCacheKey) {
|
||||
this.uri = uri;
|
||||
this.dataSource = dataSource;
|
||||
this.minLoadableRetryCount = minLoadableRetryCount;
|
||||
@ -113,6 +117,7 @@ import java.io.IOException;
|
||||
this.eventListener = eventListener;
|
||||
this.sourceListener = sourceListener;
|
||||
this.allocator = allocator;
|
||||
this.customCacheKey = customCacheKey;
|
||||
loader = new Loader("Loader:ExtractorMediaPeriod");
|
||||
extractorHolder = new ExtractorHolder(extractors, this);
|
||||
loadCondition = new ConditionVariable();
|
||||
@ -615,7 +620,7 @@ import java.io.IOException;
|
||||
ExtractorInput input = null;
|
||||
try {
|
||||
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) {
|
||||
length += position;
|
||||
}
|
||||
@ -640,7 +645,7 @@ import java.io.IOException;
|
||||
} else if (input != null) {
|
||||
positionHolder.position = input.getPosition();
|
||||
}
|
||||
dataSource.close();
|
||||
Util.closeQuietly(dataSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -93,6 +93,7 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
|
||||
private final Handler eventHandler;
|
||||
private final EventListener eventListener;
|
||||
private final Timeline.Period period;
|
||||
private final String customCacheKey;
|
||||
|
||||
private MediaSource.Listener sourceListener;
|
||||
private Timeline timeline;
|
||||
@ -110,7 +111,25 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
|
||||
public ExtractorMediaSource(Uri uri, DataSource.Factory dataSourceFactory,
|
||||
ExtractorsFactory extractorsFactory, Handler eventHandler, EventListener eventListener) {
|
||||
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 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, int minLoadableRetryCount, Handler eventHandler,
|
||||
EventListener eventListener) {
|
||||
EventListener eventListener, String customCacheKey) {
|
||||
this.uri = uri;
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.extractorsFactory = extractorsFactory;
|
||||
this.minLoadableRetryCount = minLoadableRetryCount;
|
||||
this.eventHandler = eventHandler;
|
||||
this.eventListener = eventListener;
|
||||
this.customCacheKey = customCacheKey;
|
||||
period = new Timeline.Period();
|
||||
}
|
||||
|
||||
@ -152,7 +174,7 @@ public final class ExtractorMediaSource implements MediaSource, MediaSource.List
|
||||
Assertions.checkArgument(index == 0);
|
||||
return new ExtractorMediaPeriod(uri, dataSourceFactory.createDataSource(),
|
||||
extractorsFactory.createExtractors(), minLoadableRetryCount, eventHandler, eventListener,
|
||||
this, allocator);
|
||||
this, allocator, customCacheKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -16,6 +16,7 @@
|
||||
package com.google.android.exoplayer2.source;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import java.io.IOException;
|
||||
|
||||
@ -47,6 +48,10 @@ public interface MediaPeriod extends SequenceableLoader {
|
||||
* <p>
|
||||
* {@code callback.onPrepared} is called when preparation completes. If preparation fails,
|
||||
* {@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
|
||||
* preparation completes.
|
||||
|
@ -44,11 +44,17 @@ public interface SampleStream {
|
||||
|
||||
/**
|
||||
* 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 buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
|
||||
* end of the stream. If the end of the stream has been reached, the
|
||||
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.
|
||||
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. May be null if the
|
||||
* caller requires that the format of the stream be read even if it's not changing.
|
||||
* @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
|
||||
* {@link C#RESULT_BUFFER_READ}.
|
||||
*/
|
||||
|
@ -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.Loadable;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@ -205,13 +206,13 @@ import java.util.Arrays;
|
||||
|
||||
@Override
|
||||
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) {
|
||||
if (streamState == STREAM_STATE_END_OF_STREAM) {
|
||||
buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
|
||||
return C.RESULT_BUFFER_READ;
|
||||
} else if (streamState == STREAM_STATE_SEND_FORMAT) {
|
||||
if (buffer == null || streamState == STREAM_STATE_SEND_FORMAT) {
|
||||
formatHolder.format = format;
|
||||
streamState = STREAM_STATE_SEND_SAMPLE;
|
||||
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);
|
||||
@ -276,7 +277,7 @@ import java.util.Arrays;
|
||||
result = dataSource.read(sampleData, sampleSize, sampleData.length - sampleSize);
|
||||
}
|
||||
} finally {
|
||||
dataSource.close();
|
||||
Util.closeQuietly(dataSource);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,15 +30,15 @@ import java.io.IOException;
|
||||
/**
|
||||
* An {@link Extractor} wrapper for loading chunks containing a single track.
|
||||
* <p>
|
||||
* The wrapper allows switching of the {@link SingleTrackMetadataOutput} and {@link TrackOutput}
|
||||
* which receive parsed data.
|
||||
* The wrapper allows switching of the {@link SeekMapOutput} and {@link TrackOutput} that receive
|
||||
* parsed data.
|
||||
*/
|
||||
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)
|
||||
@ -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 boolean preferManifestDrmInitData;
|
||||
private final boolean resendFormatOnInit;
|
||||
|
||||
private boolean extractorInitialized;
|
||||
private SingleTrackMetadataOutput metadataOutput;
|
||||
private SeekMapOutput seekMapOutput;
|
||||
private TrackOutput trackOutput;
|
||||
private Format sentFormat;
|
||||
|
||||
@ -68,7 +69,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput
|
||||
* @param preferManifestDrmInitData Whether {@link DrmInitData} defined in {@code manifestFormat}
|
||||
* 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
|
||||
* it is initialized via {@link #init(SingleTrackMetadataOutput, TrackOutput)}.
|
||||
* it is initialized via {@link #init(SeekMapOutput, TrackOutput)}.
|
||||
*/
|
||||
public ChunkExtractorWrapper(Extractor extractor, Format manifestFormat,
|
||||
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.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
public void init(SingleTrackMetadataOutput metadataOutput, TrackOutput trackOutput) {
|
||||
this.metadataOutput = metadataOutput;
|
||||
public void init(SeekMapOutput seekMapOutput, TrackOutput trackOutput) {
|
||||
this.seekMapOutput = seekMapOutput;
|
||||
this.trackOutput = trackOutput;
|
||||
if (!extractorInitialized) {
|
||||
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.
|
||||
|
||||
@Override
|
||||
@ -130,7 +117,7 @@ public final class ChunkExtractorWrapper implements ExtractorOutput, TrackOutput
|
||||
|
||||
@Override
|
||||
public void seekMap(SeekMap seekMap) {
|
||||
metadataOutput.seekMap(seekMap);
|
||||
seekMapOutput.seekMap(seekMap);
|
||||
}
|
||||
|
||||
// TrackOutput implementation.
|
||||
|
@ -122,7 +122,8 @@ public class ChunkSampleStream<T extends ChunkSource> implements SampleStream, S
|
||||
public void seekToUs(long positionUs) {
|
||||
lastSeekPositionUs = positionUs;
|
||||
// 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) {
|
||||
// We succeeded. All we need to do is discard any chunks that we've moved past.
|
||||
while (mediaChunks.size() > 1
|
||||
|
@ -21,16 +21,17 @@ import com.google.android.exoplayer2.extractor.DefaultTrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
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.DataSpec;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 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 long sampleOffsetUs;
|
||||
@ -85,7 +86,7 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackMe
|
||||
return bytesLoaded;
|
||||
}
|
||||
|
||||
// SingleTrackMetadataOutput implementation.
|
||||
// SeekMapOutput implementation.
|
||||
|
||||
@Override
|
||||
public final void seekMap(SeekMap seekMap) {
|
||||
@ -120,15 +121,17 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackMe
|
||||
}
|
||||
// Load and decode the sample data.
|
||||
try {
|
||||
Extractor extractor = extractorWrapper.extractor;
|
||||
int result = Extractor.RESULT_CONTINUE;
|
||||
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
|
||||
result = extractorWrapper.read(input);
|
||||
result = extractor.read(input, null);
|
||||
}
|
||||
Assertions.checkState(result != Extractor.RESULT_SEEK);
|
||||
} finally {
|
||||
bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
|
||||
}
|
||||
} finally {
|
||||
dataSource.close();
|
||||
Util.closeQuietly(dataSource);
|
||||
}
|
||||
loadCompleted = true;
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
@ -96,7 +97,7 @@ public abstract class DataChunk extends Chunk {
|
||||
consume(data, limit);
|
||||
}
|
||||
} finally {
|
||||
dataSource.close();
|
||||
Util.closeQuietly(dataSource);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,9 +22,10 @@ import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
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.DataSpec;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
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.
|
||||
*/
|
||||
public final class InitializationChunk extends Chunk implements SingleTrackMetadataOutput,
|
||||
public final class InitializationChunk extends Chunk implements SeekMapOutput,
|
||||
TrackOutput {
|
||||
|
||||
private final ChunkExtractorWrapper extractorWrapper;
|
||||
@ -85,7 +86,7 @@ public final class InitializationChunk extends Chunk implements SingleTrackMetad
|
||||
return seekMap;
|
||||
}
|
||||
|
||||
// SingleTrackMetadataOutput implementation.
|
||||
// SeekMapOutput implementation.
|
||||
|
||||
@Override
|
||||
public void seekMap(SeekMap seekMap) {
|
||||
@ -142,15 +143,17 @@ public final class InitializationChunk extends Chunk implements SingleTrackMetad
|
||||
}
|
||||
// Load and decode the initialization data.
|
||||
try {
|
||||
Extractor extractor = extractorWrapper.extractor;
|
||||
int result = Extractor.RESULT_CONTINUE;
|
||||
while (result == Extractor.RESULT_CONTINUE && !loadCanceled) {
|
||||
result = extractorWrapper.read(input);
|
||||
result = extractor.read(input, null);
|
||||
}
|
||||
Assertions.checkState(result != Extractor.RESULT_SEEK);
|
||||
} finally {
|
||||
bytesLoaded = (int) (input.getPosition() - dataSpec.absoluteStreamPosition);
|
||||
}
|
||||
} finally {
|
||||
dataSource.close();
|
||||
Util.closeQuietly(dataSource);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,7 +98,7 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk {
|
||||
int sampleSize = bytesLoaded;
|
||||
trackOutput.sampleMetadata(startTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
|
||||
} finally {
|
||||
dataSource.close();
|
||||
Util.closeQuietly(dataSource);
|
||||
}
|
||||
loadCompleted = true;
|
||||
}
|
||||
|
@ -28,9 +28,8 @@ import com.google.android.exoplayer2.source.dash.manifest.RangedUri;
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
|
@ -185,10 +185,9 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
}
|
||||
if (pendingInitializationUri != null || pendingIndexUri != null) {
|
||||
// We have initialization and/or index requests to make.
|
||||
Chunk initializationChunk = newInitializationChunk(representationHolder, dataSource,
|
||||
out.chunk = newInitializationChunk(representationHolder, dataSource,
|
||||
trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(),
|
||||
trackSelection.getSelectionData(), pendingInitializationUri, pendingIndexUri);
|
||||
out.chunk = initializationChunk;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -233,10 +232,9 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
}
|
||||
|
||||
int maxSegmentCount = Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1);
|
||||
Chunk nextMediaChunk = newMediaChunk(representationHolder, dataSource,
|
||||
trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(),
|
||||
trackSelection.getSelectionData(), sampleFormat, segmentNum, maxSegmentCount);
|
||||
out.chunk = nextMediaChunk;
|
||||
out.chunk = newMediaChunk(representationHolder, dataSource, trackSelection.getSelectedFormat(),
|
||||
trackSelection.getSelectionReason(), trackSelection.getSelectionData(), sampleFormat,
|
||||
segmentNum, maxSegmentCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -255,8 +253,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
if (representationHolder.segmentIndex == null) {
|
||||
SeekMap seekMap = initializationChunk.getSeekMap();
|
||||
if (seekMap != null) {
|
||||
representationHolder.segmentIndex = new DashWrappingSegmentIndex((ChunkIndex) seekMap,
|
||||
initializationChunk.dataSpec.uri.toString());
|
||||
representationHolder.segmentIndex = new DashWrappingSegmentIndex((ChunkIndex) seekMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user