Merge branch 'dev-v2-r2.1.0' into release-v2

This commit is contained in:
Oliver Woodman 2016-12-14 17:08:03 +00:00
commit e6778c90a1
259 changed files with 12926 additions and 4645 deletions

View file

@ -1,5 +1,7 @@
*** PLEASE DO NOT IGNORE THIS ISSUE TEMPLATE ***
Please search the existing issues before filing a new one, including issues that
are closed. When filing a new issue please include all of the following, unless
are closed. When filing a new issue please include ALL of the following, unless
you're certain that they're not useful for the particular issue being reported.
- A description of the issue.

View file

@ -1,9 +1,68 @@
# Release notes #
### r2.1.0 ###
This release contains important bug fixes. Users of r2.0.x should proactively
update to this version.
* HLS: Support for seeking in live streams
([87](https://github.com/google/ExoPlayer/issues/87)).
* HLS: Improved support:
* Support for EXT-X-PROGRAM-DATE-TIME
([747](https://github.com/google/ExoPlayer/issues/747)).
* Improved handling of sample timestamps and their alignment across variants
and renditions.
* Fix issue that could cause playbacks to get stuck in an endless initial
buffering state.
* Correctly propagate BehindLiveWindowException instead of
IndexOutOfBoundsException exception
([1695](https://github.com/google/ExoPlayer/issues/1695)).
* MP3/MP4: Support for ID3 metadata, including embedded album art
([979](https://github.com/google/ExoPlayer/issues/979)).
* Improved customization of UI components. You can read about customization of
ExoPlayer's UI components
[here](https://medium.com/google-exoplayer/customizing-exoplayers-ui-components-728cf55ee07a#.9ewjg7avi).
* Robustness improvements when handling MediaSource timeline changes and
MediaPeriod transitions.
* EIA608: Support for caption styling and positioning.
* MPEG-TS: Improved support:
* Support injection of custom TS payload readers.
* Support injection of custom section payload readers.
* Support SCTE-35 splice information messages.
* Support multiple table sections in a single PSI section.
* Fix NullPointerException when an unsupported stream type is encountered
([2149](https://github.com/google/ExoPlayer/issues/2149)).
* Avoid failure when expected ID3 header not found
([1966](https://github.com/google/ExoPlayer/issues/1966)).
* Improvements to the upstream cache package.
* Support caching of media segments for DASH, HLS and SmoothStreaming. Note
that caching of manifest and playlist files is still not supported in the
(normal) case where the corresponding responses are compressed.
* Support caching for ExtractorMediaSource based playbacks.
* Improved flexibility of SimpleExoPlayer
([2102](https://github.com/google/ExoPlayer/issues/2102)).
* Fix issue where only the audio of a video would play due to capability
detection issues ([2007](https://github.com/google/ExoPlayer/issues/2007))
([2034](https://github.com/google/ExoPlayer/issues/2034))
([2157](https://github.com/google/ExoPlayer/issues/2157)).
* 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)).
* OGG: Fix playback of short OGG files
([1976](https://github.com/google/ExoPlayer/issues/1976)).
* MP4: Support `.mp3` tracks
([2066](https://github.com/google/ExoPlayer/issues/2066)).
* SubRip: Don't fail playbacks if SubRip file contains negative timestamps
([2145](https://github.com/google/ExoPlayer/issues/2145)).
* Misc bugfixes.
### r2.0.4 ###
This release contains important bug fixes. Users of earlier r2.0.x versions
should proactively update to this version.
* Fix crash on Jellybean devices when using playback controls
([#1965](https://github.com/google/ExoPlayer/issues/1965)).
### r2.0.3 ###
* Fix crash on Jellybean devices when using playback controls
([#1965](https://github.com/google/ExoPlayer/issues/1965)).
@ -113,6 +172,26 @@ some of the motivations behind ExoPlayer 2.x
* Suppressed "Sending message to a Handler on a dead thread" warnings
([#426](https://github.com/google/ExoPlayer/issues/426)).
# Legacy release notes #
Note: Since ExoPlayer V1 is still being maintained alongside V2, there is some
overlap between these notes and the notes above. r2.0.0 followed from r1.5.11,
and hence it can be assumed that all changes in r1.5.11 and earlier are included
in all V2 releases. This cannot be assumed for changes in r1.5.12 and later,
however it can be assumed that all such changes are included in the most recent
V2 release.
### r1.5.13 ###
* Improvements to the upstream cache package.
* MP4: Support `.mp3` tracks
([2066](https://github.com/google/ExoPlayer/issues/2066)).
* SubRip: Don't fail playbacks if SubRip file contains negative timestamps
([2145](https://github.com/google/ExoPlayer/issues/2145)).
* MPEG-TS: Avoid failure when expected ID3 header not found
([1966](https://github.com/google/ExoPlayer/issues/1966)).
* Misc bugfixes.
### r1.5.12 ###
* Improvements to Cronet network stack extension.

View file

@ -19,7 +19,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.2'
classpath 'com.android.tools.build:gradle:2.2.1'
classpath 'com.novoda:bintray-release:0.3.4'
}
}
@ -35,7 +35,7 @@ allprojects {
releaseRepoName = 'exoplayer'
releaseUserOrg = 'google'
releaseGroupId = 'com.google.android.exoplayer'
releaseVersion = 'r2.0.4'
releaseVersion = 'r2.1.0'
releaseWebsite = 'https://github.com/google/ExoPlayer'
}
}

View file

@ -41,6 +41,7 @@ android {
noExtensions
withExtensions
}
}
dependencies {

View file

@ -16,17 +16,19 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.demo"
android:versionCode="2004"
android:versionName="2.0.4">
android:versionCode="2100"
android:versionName="2.1.0">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-feature android:name="android.software.leanback" android:required="false"/>
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="24"/>
<application
android:label="@string/application_name"
android:icon="@drawable/ic_launcher"
android:banner="@drawable/ic_banner"
android:largeHeap="true"
android:allowBackup="false"
android:name="com.google.android.exoplayer2.demo.DemoApplication">
@ -37,6 +39,7 @@
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>

View file

@ -36,13 +36,17 @@ public class DemoApplication extends Application {
userAgent = Util.getUserAgent(this, "ExoPlayerDemo");
}
DataSource.Factory buildDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
public DataSource.Factory buildDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
return new DefaultDataSourceFactory(this, bandwidthMeter,
buildHttpDataSourceFactory(bandwidthMeter));
}
HttpDataSource.Factory buildHttpDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
public HttpDataSource.Factory buildHttpDataSourceFactory(DefaultBandwidthMeter bandwidthMeter) {
return new DefaultHttpDataSourceFactory(userAgent, bandwidthMeter);
}
public boolean useExtensionRenderers() {
return BuildConfig.FLAVOR.equals("withExtensions");
}
}

View file

@ -27,8 +27,10 @@ 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.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataRenderer;
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;
@ -38,15 +40,14 @@ import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelections;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import java.io.IOException;
import java.text.NumberFormat;
import java.util.List;
import java.util.Locale;
/**
@ -55,7 +56,7 @@ import java.util.Locale;
/* package */ final class EventLogger implements ExoPlayer.EventListener,
AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener,
ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener,
TrackSelector.EventListener<MappedTrackInfo>, MetadataRenderer.Output<List<Id3Frame>> {
MetadataRenderer.Output {
private static final String TAG = "EventLogger";
private static final int MAX_TIMELINE_ITEM_LINES = 3;
@ -67,11 +68,13 @@ import java.util.Locale;
TIME_FORMAT.setGroupingUsed(false);
}
private final MappingTrackSelector trackSelector;
private final Timeline.Window window;
private final Timeline.Period period;
private final long startTimeMs;
public EventLogger() {
public EventLogger(MappingTrackSelector trackSelector) {
this.trackSelector = trackSelector;
window = new Timeline.Window();
period = new Timeline.Period();
startTimeMs = SystemClock.elapsedRealtime();
@ -126,43 +129,57 @@ import java.util.Locale;
Log.e(TAG, "playerFailed [" + getSessionTimeString() + "]", e);
}
// MappingTrackSelector.EventListener
@Override
public void onTrackSelectionsChanged(TrackSelections<? extends MappedTrackInfo> trackSelections) {
public void onTracksChanged(TrackGroupArray ignored, TrackSelectionArray trackSelections) {
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo == null) {
Log.d(TAG, "Tracks []");
return;
}
Log.d(TAG, "Tracks [");
// Log tracks associated to renderers.
MappedTrackInfo info = trackSelections.info;
for (int rendererIndex = 0; rendererIndex < trackSelections.length; rendererIndex++) {
TrackGroupArray trackGroups = info.getTrackGroups(rendererIndex);
for (int rendererIndex = 0; rendererIndex < mappedTrackInfo.length; rendererIndex++) {
TrackGroupArray rendererTrackGroups = mappedTrackInfo.getTrackGroups(rendererIndex);
TrackSelection trackSelection = trackSelections.get(rendererIndex);
if (trackGroups.length > 0) {
if (rendererTrackGroups.length > 0) {
Log.d(TAG, " Renderer:" + rendererIndex + " [");
for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {
TrackGroup trackGroup = trackGroups.get(groupIndex);
String adaptiveSupport = getAdaptiveSupportString(
trackGroup.length, info.getAdaptiveSupport(rendererIndex, groupIndex, false));
for (int groupIndex = 0; groupIndex < rendererTrackGroups.length; groupIndex++) {
TrackGroup trackGroup = rendererTrackGroups.get(groupIndex);
String adaptiveSupport = getAdaptiveSupportString(trackGroup.length,
mappedTrackInfo.getAdaptiveSupport(rendererIndex, groupIndex, false));
Log.d(TAG, " Group:" + groupIndex + ", adaptive_supported=" + adaptiveSupport + " [");
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
String status = getTrackStatusString(trackSelection, trackGroup, trackIndex);
String formatSupport = getFormatSupportString(
info.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex));
mappedTrackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex));
Log.d(TAG, " " + status + " Track:" + trackIndex + ", "
+ getFormatString(trackGroup.getFormat(trackIndex))
+ ", supported=" + formatSupport);
}
Log.d(TAG, " ]");
}
// Log metadata for at most one of the tracks selected for the renderer.
if (trackSelection != null) {
for (int selectionIndex = 0; selectionIndex < trackSelection.length(); selectionIndex++) {
Metadata metadata = trackSelection.getFormat(selectionIndex).metadata;
if (metadata != null) {
Log.d(TAG, " Metadata [");
printMetadata(metadata, " ");
Log.d(TAG, " ]");
break;
}
}
}
Log.d(TAG, " ]");
}
}
// Log tracks not associated with a renderer.
TrackGroupArray trackGroups = info.getUnassociatedTrackGroups();
if (trackGroups.length > 0) {
TrackGroupArray unassociatedTrackGroups = mappedTrackInfo.getUnassociatedTrackGroups();
if (unassociatedTrackGroups.length > 0) {
Log.d(TAG, " Renderer:None [");
for (int groupIndex = 0; groupIndex < trackGroups.length; groupIndex++) {
for (int groupIndex = 0; groupIndex < unassociatedTrackGroups.length; groupIndex++) {
Log.d(TAG, " Group:" + groupIndex + " [");
TrackGroup trackGroup = trackGroups.get(groupIndex);
TrackGroup trackGroup = unassociatedTrackGroups.get(groupIndex);
for (int trackIndex = 0; trackIndex < trackGroup.length; trackIndex++) {
String status = getTrackStatusString(false);
String formatSupport = getFormatSupportString(
@ -178,34 +195,13 @@ import java.util.Locale;
Log.d(TAG, "]");
}
// MetadataRenderer.Output<List<Id3Frame>>
// MetadataRenderer.Output
@Override
public void onMetadata(List<Id3Frame> id3Frames) {
for (Id3Frame id3Frame : id3Frames) {
if (id3Frame instanceof TxxxFrame) {
TxxxFrame txxxFrame = (TxxxFrame) id3Frame;
Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s, value=%s", txxxFrame.id,
txxxFrame.description, txxxFrame.value));
} else if (id3Frame instanceof PrivFrame) {
PrivFrame privFrame = (PrivFrame) id3Frame;
Log.i(TAG, String.format("ID3 TimedMetadata %s: owner=%s", privFrame.id, privFrame.owner));
} else if (id3Frame instanceof GeobFrame) {
GeobFrame geobFrame = (GeobFrame) id3Frame;
Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, filename=%s, description=%s",
geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description));
} else if (id3Frame instanceof ApicFrame) {
ApicFrame apicFrame = (ApicFrame) id3Frame;
Log.i(TAG, String.format("ID3 TimedMetadata %s: mimeType=%s, description=%s",
apicFrame.id, apicFrame.mimeType, apicFrame.description));
} else if (id3Frame instanceof TextInformationFrame) {
TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frame;
Log.i(TAG, String.format("ID3 TimedMetadata %s: description=%s", textInformationFrame.id,
textInformationFrame.description));
} else {
Log.i(TAG, String.format("ID3 TimedMetadata %s", id3Frame.id));
}
}
public void onMetadata(Metadata metadata) {
Log.d(TAG, "onMetadata [");
printMetadata(metadata, " ");
Log.d(TAG, "]");
}
// AudioRendererEventListener
@ -350,6 +346,39 @@ import java.util.Locale;
Log.e(TAG, "internalError [" + getSessionTimeString() + ", " + type + "]", e);
}
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));
} else if (entry instanceof PrivFrame) {
PrivFrame privFrame = (PrivFrame) entry;
Log.d(TAG, prefix + String.format("%s: owner=%s", privFrame.id, privFrame.owner));
} else if (entry instanceof GeobFrame) {
GeobFrame geobFrame = (GeobFrame) entry;
Log.d(TAG, prefix + String.format("%s: mimeType=%s, filename=%s, description=%s",
geobFrame.id, geobFrame.mimeType, geobFrame.filename, geobFrame.description));
} else if (entry instanceof ApicFrame) {
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,
commentFrame.language, commentFrame.description));
} else if (entry instanceof Id3Frame) {
Id3Frame id3Frame = (Id3Frame) entry;
Log.d(TAG, prefix + String.format("%s", id3Frame.id));
}
}
}
private String getSessionTimeString() {
return getTimeString(SystemClock.elapsedRealtime() - startTimeMs);
}

View file

@ -22,6 +22,7 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
@ -55,11 +56,9 @@ import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveVideoTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelections;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.DebugTextViewHelper;
import com.google.android.exoplayer2.ui.PlaybackControlView;
import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
@ -78,7 +77,7 @@ import java.util.UUID;
* An activity that plays media using {@link SimpleExoPlayer}.
*/
public class PlayerActivity extends Activity implements OnClickListener, ExoPlayer.EventListener,
TrackSelector.EventListener<MappedTrackInfo>, PlaybackControlView.VisibilityListener {
PlaybackControlView.VisibilityListener {
public static final String DRM_SCHEME_UUID_EXTRA = "drm_scheme_uuid";
public static final String DRM_LICENSE_URL = "drm_license_url";
@ -110,7 +109,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
private DataSource.Factory mediaDataSourceFactory;
private SimpleExoPlayer player;
private MappingTrackSelector trackSelector;
private DefaultTrackSelector trackSelector;
private TrackSelectionHelper trackSelectionHelper;
private DebugTextViewHelper debugViewHelper;
private boolean playerNeedsSource;
@ -196,6 +195,16 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
}
}
// Activity input
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// Show the controls on any key event.
simpleExoPlayerView.showController();
// If the event was not handled then see if the player view can handle it as a media key event.
return super.dispatchKeyEvent(event) || simpleExoPlayerView.dispatchMediaKeyEvent(event);
}
// OnClickListener methods
@Override
@ -203,8 +212,11 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
if (view == retryButton) {
initializePlayer();
} else if (view.getParent() == debugRootView) {
trackSelectionHelper.showSelectionDialog(this, ((Button) view).getText(),
trackSelector.getCurrentSelections().info, (int) view.getTag());
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo != null) {
trackSelectionHelper.showSelectionDialog(this, ((Button) view).getText(),
trackSelector.getCurrentMappedTrackInfo(), (int) view.getTag());
}
}
}
@ -249,20 +261,25 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
}
}
eventLogger = new EventLogger();
@SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode =
((DemoApplication) getApplication()).useExtensionRenderers()
? (preferExtensionDecoders ? SimpleExoPlayer.EXTENSION_RENDERER_MODE_PREFER
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_ON)
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF;
TrackSelection.Factory videoTrackSelectionFactory =
new AdaptiveVideoTrackSelection.Factory(BANDWIDTH_METER);
trackSelector = new DefaultTrackSelector(mainHandler, videoTrackSelectionFactory);
trackSelector.addListener(this);
trackSelector.addListener(eventLogger);
trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
trackSelectionHelper = new TrackSelectionHelper(trackSelector, videoTrackSelectionFactory);
player = ExoPlayerFactory.newSimpleInstance(this, trackSelector, new DefaultLoadControl(),
drmSessionManager, preferExtensionDecoders);
drmSessionManager, extensionRendererMode);
player.addListener(this);
eventLogger = new EventLogger(trackSelector);
player.addListener(eventLogger);
player.setAudioDebugListener(eventLogger);
player.setVideoDebugListener(eventLogger);
player.setId3Output(eventLogger);
simpleExoPlayerView.setPlayer(player);
if (isTimelineStatic) {
if (playerPosition == C.TIME_UNSET) {
@ -353,7 +370,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
playerWindow = player.getCurrentWindowIndex();
playerPosition = C.TIME_UNSET;
Timeline timeline = player.getCurrentTimeline();
if (timeline != null && timeline.getWindow(playerWindow, window).isSeekable) {
if (!timeline.isEmpty() && timeline.getWindow(playerWindow, window).isSeekable) {
playerPosition = player.getCurrentPosition();
}
player.release();
@ -410,7 +427,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
isTimelineStatic = timeline != null && timeline.getWindowCount() > 0
isTimelineStatic = !timeline.isEmpty()
&& !timeline.getWindow(timeline.getWindowCount() - 1, window).isDynamic;
}
@ -447,17 +464,19 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
showControls();
}
// MappingTrackSelector.EventListener implementation
@Override
public void onTrackSelectionsChanged(TrackSelections<? extends MappedTrackInfo> trackSelections) {
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
updateButtonVisibilities();
MappedTrackInfo trackInfo = trackSelections.info;
if (trackInfo.hasOnlyUnplayableTracks(C.TRACK_TYPE_VIDEO)) {
showToast(R.string.error_unsupported_video);
}
if (trackInfo.hasOnlyUnplayableTracks(C.TRACK_TYPE_AUDIO)) {
showToast(R.string.error_unsupported_audio);
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo != null) {
if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_VIDEO)
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
showToast(R.string.error_unsupported_video);
}
if (mappedTrackInfo.getTrackTypeRendererSupport(C.TRACK_TYPE_AUDIO)
== MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
showToast(R.string.error_unsupported_audio);
}
}
}
@ -473,14 +492,13 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
return;
}
TrackSelections<MappedTrackInfo> trackSelections = trackSelector.getCurrentSelections();
if (trackSelections == null) {
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo == null) {
return;
}
int rendererCount = trackSelections.length;
for (int i = 0; i < rendererCount; i++) {
TrackGroupArray trackGroups = trackSelections.info.getTrackGroups(i);
for (int i = 0; i < mappedTrackInfo.length; i++) {
TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(i);
if (trackGroups.length != 0) {
Button button = new Button(this);
int label;

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.demo;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.AssetManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
@ -43,6 +44,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
@ -63,9 +65,21 @@ public class SampleChooserActivity extends Activity {
if (dataUri != null) {
uris = new String[] {dataUri};
} else {
uris = new String[] {
"asset:///media.exolist.json",
};
ArrayList<String> uriList = new ArrayList<>();
AssetManager assetManager = getAssets();
try {
for (String asset : assetManager.list("")) {
if (asset.endsWith(".exolist.json")) {
uriList.add("asset:///" + asset);
}
}
} catch (IOException e) {
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
.show();
}
uris = new String[uriList.size()];
uriList.toArray(uris);
Arrays.sort(uris);
}
SampleListLoader loaderTask = new SampleListLoader();
loaderTask.execute(uris);

View file

@ -18,7 +18,9 @@ package com.google.android.exoplayer2.demo;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.TypedArray;
import android.text.TextUtils;
import android.util.Pair;
import android.view.LayoutInflater;
@ -100,7 +102,7 @@ import java.util.Locale;
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(title)
.setView(buildView(LayoutInflater.from(builder.getContext())))
.setView(buildView(builder.getContext()))
.setPositiveButton(android.R.string.ok, this)
.setNegativeButton(android.R.string.cancel, null)
.create()
@ -108,13 +110,20 @@ import java.util.Locale;
}
@SuppressLint("InflateParams")
private View buildView(LayoutInflater inflater) {
private View buildView(Context context) {
LayoutInflater inflater = LayoutInflater.from(context);
View view = inflater.inflate(R.layout.track_selection_dialog, null);
ViewGroup root = (ViewGroup) view.findViewById(R.id.root);
TypedArray attributeArray = context.getTheme().obtainStyledAttributes(
new int[] {android.R.attr.selectableItemBackground});
int selectableItemBackgroundResourceId = attributeArray.getResourceId(0, 0);
attributeArray.recycle();
// View for disabling the renderer.
disableView = (CheckedTextView) inflater.inflate(
android.R.layout.simple_list_item_single_choice, root, false);
disableView.setBackgroundResource(selectableItemBackgroundResourceId);
disableView.setText(R.string.selection_disabled);
disableView.setFocusable(true);
disableView.setOnClickListener(this);
@ -123,6 +132,7 @@ import java.util.Locale;
// View for clearing the override to allow the selector to use its default selection logic.
defaultView = (CheckedTextView) inflater.inflate(
android.R.layout.simple_list_item_single_choice, root, false);
defaultView.setBackgroundResource(selectableItemBackgroundResourceId);
defaultView.setText(R.string.selection_default);
defaultView.setFocusable(true);
defaultView.setOnClickListener(this);
@ -146,6 +156,7 @@ import java.util.Locale;
: android.R.layout.simple_list_item_single_choice;
CheckedTextView trackView = (CheckedTextView) inflater.inflate(
trackViewLayoutId, root, false);
trackView.setBackgroundResource(selectableItemBackgroundResourceId);
trackView.setText(buildTrackName(group.getFormat(trackIndex)));
if (trackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex)
== RendererCapabilities.FORMAT_HANDLED) {
@ -169,6 +180,7 @@ import java.util.Locale;
// View for using random adaptation.
enableRandomAdaptationView = (CheckedTextView) inflater.inflate(
android.R.layout.simple_list_item_multiple_choice, root, false);
enableRandomAdaptationView.setBackgroundResource(selectableItemBackgroundResourceId);
enableRandomAdaptationView.setText(R.string.enable_random_adaptation);
enableRandomAdaptationView.setOnClickListener(this);
root.addView(inflater.inflate(R.layout.list_divider, root, false));

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View file

@ -16,13 +16,11 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root"
android:focusable="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
<com.google.android.exoplayer2.ui.SimpleExoPlayerView android:id="@+id/player_view"
android:focusable="true"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

View file

@ -21,8 +21,9 @@ git clone https://github.com/google/ExoPlayer.git
1. Find the latest Cronet release [here][] and navigate to its `Release/cronet`
directory
1. Download `cronet.jar`, `cronet_api.jar` and the `libs` directory
1. Copy the two jar files into the `libs` directory of this extension
1. Download `cronet_api.jar`, `cronet_impl_common_java.jar`,
`cronet_impl_native_java.jar` and the `libs` directory
1. Copy the three jar files into the `libs` directory of this extension
1. Copy the content of the downloaded `libs` directory into the `jniLibs`
directory of this extension

View file

@ -42,10 +42,11 @@ android {
dependencies {
compile project(':library')
compile files('libs/cronet_api.jar')
compile files('libs/cronet.jar')
compile files('libs/cronet_impl_common_java.jar')
compile files('libs/cronet_impl_native_java.jar')
androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
androidTestCompile 'org.mockito:mockito-core:1.9.5'
androidTestCompile project(':library')
androidTestCompile 'com.android.support.test:runner:0.4'
androidTestCompile 'com.android.support.test:runner:0.5'
}

View file

@ -22,7 +22,6 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
@ -52,7 +51,6 @@ import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@ -62,6 +60,7 @@ import org.chromium.net.CronetEngine;
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;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -88,20 +87,7 @@ public final class CronetDataSourceTest {
private Map<String, String> testResponseHeader;
private UrlResponseInfo testUrlResponseInfo;
/**
* MockableCronetEngine is an abstract class for helping creating new Requests.
*/
public abstract static class MockableCronetEngine extends CronetEngine {
@Override
public abstract UrlRequest createRequest(String url, UrlRequest.Callback callback,
Executor executor, int priority,
Collection<Object> connectionAnnotations,
boolean disableCache,
boolean disableConnectionMigration,
boolean allowDirectExecutor);
}
@Mock private UrlRequest.Builder mockUrlRequestBuilder;
@Mock
private UrlRequest mockUrlRequest;
@Mock
@ -114,8 +100,7 @@ public final class CronetDataSourceTest {
private Executor mockExecutor;
@Mock
private UrlRequestException mockUrlRequestException;
@Mock
private MockableCronetEngine mockCronetEngine;
@Mock private CronetEngine mockCronetEngine;
private CronetDataSource dataSourceUnderTest;
@ -135,15 +120,10 @@ public final class CronetDataSourceTest {
true, // resetTimeoutOnRedirects
mockClock));
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);
when(mockCronetEngine.createRequest(
anyString(),
any(UrlRequest.Callback.class),
any(Executor.class),
anyInt(),
eq(Collections.emptyList()),
any(Boolean.class),
any(Boolean.class),
any(Boolean.class))).thenReturn(mockUrlRequest);
when(mockCronetEngine.newUrlRequestBuilder(
anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
.thenReturn(mockUrlRequestBuilder);
when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest);
mockStatusResponse();
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, C.LENGTH_UNSET, null);
@ -159,7 +139,7 @@ public final class CronetDataSourceTest {
private UrlResponseInfo createUrlResponseInfo(int statusCode) {
ArrayList<Map.Entry<String, String>> responseHeaderList = new ArrayList<>();
responseHeaderList.addAll(testResponseHeader.entrySet());
return new UrlResponseInfo(
return new UrlResponseInfoImpl(
Collections.singletonList(TEST_URL),
statusCode,
null, // httpStatusText
@ -184,15 +164,7 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.close();
// Prepare a mock UrlRequest to be used in the second open() call.
final UrlRequest mockUrlRequest2 = mock(UrlRequest.class);
when(mockCronetEngine.createRequest(
anyString(),
any(UrlRequest.Callback.class),
any(Executor.class),
anyInt(),
eq(Collections.emptyList()),
any(Boolean.class),
any(Boolean.class),
any(Boolean.class))).thenReturn(mockUrlRequest2);
when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest2);
doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
@ -215,15 +187,8 @@ public final class CronetDataSourceTest {
mockResponseStartSuccess();
dataSourceUnderTest.open(testDataSpec);
verify(mockCronetEngine).createRequest(
eq(TEST_URL),
any(UrlRequest.Callback.class),
any(Executor.class),
anyInt(),
eq(Collections.emptyList()),
any(Boolean.class),
any(Boolean.class),
any(Boolean.class));
verify(mockCronetEngine)
.newUrlRequestBuilder(eq(TEST_URL), any(UrlRequest.Callback.class), any(Executor.class));
verify(mockUrlRequest).start();
}
@ -237,9 +202,9 @@ public final class CronetDataSourceTest {
dataSourceUnderTest.open(testDataSpec);
// The header value to add is current position to current position + length - 1.
verify(mockUrlRequest).addHeader("Range", "bytes=1000-5999");
verify(mockUrlRequest).addHeader("firstHeader", "firstValue");
verify(mockUrlRequest).addHeader("secondHeader", "secondValue");
verify(mockUrlRequestBuilder).addHeader("Range", "bytes=1000-5999");
verify(mockUrlRequestBuilder).addHeader("firstHeader", "firstValue");
verify(mockUrlRequestBuilder).addHeader("secondHeader", "secondValue");
verify(mockUrlRequest).start();
}

View file

@ -412,8 +412,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
// Internal methods.
private UrlRequest buildRequest(DataSpec dataSpec) throws OpenException {
UrlRequest.Builder requestBuilder = new UrlRequest.Builder(dataSpec.uri.toString(), this,
executor, cronetEngine);
UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(dataSpec.uri.toString(),
this, executor);
// Set the headers.
synchronized (requestProperties) {
if (dataSpec.postBody != null && dataSpec.postBody.length != 0

View file

@ -31,7 +31,7 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main"
NDK_PATH="<path to Android NDK>"
```
* Fetch and build ffmpeg.
* Fetch and build FFmpeg.
For example, to fetch and build for armv7a:
@ -75,7 +75,7 @@ cd "${FFMPEG_EXT_PATH}"/jni && \
${NDK_PATH}/ndk-build APP_ABI=armeabi-v7a -j4
```
TODO: Add instructions for other ABIs.
Repeat these steps for any other architectures you need to support.
* In your project, you can add a dependency on the extension by using a rule
like this:

View file

@ -20,8 +20,8 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioCapabilities;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.audio.AudioTrack;
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.MimeTypes;
/**
@ -53,11 +53,10 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
* @param streamType The type of audio stream for the {@link AudioTrack}.
*/
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioCapabilities audioCapabilities, int streamType) {
super(eventHandler, eventListener, audioCapabilities, streamType);
AudioCapabilities audioCapabilities) {
super(eventHandler, eventListener, audioCapabilities);
}
@Override
@ -71,7 +70,8 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
}
@Override
protected FfmpegDecoder createDecoder(Format format) throws FfmpegDecoderException {
protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
throws FfmpegDecoderException {
decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
format.sampleMimeType, format.initializationData);
return decoder;

View file

@ -267,7 +267,7 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
sampleFormat, 1);
AVAudioResampleContext *resampleContext;
if (context->opaque) {
resampleContext = (AVAudioResampleContext *)context->opaque;
resampleContext = (AVAudioResampleContext *) context->opaque;
} else {
resampleContext = avresample_alloc_context();
av_opt_set_int(resampleContext, "in_channel_layout", channelLayout, 0);
@ -326,7 +326,7 @@ void releaseContext(AVCodecContext *context) {
return;
}
AVAudioResampleContext *resampleContext;
if (resampleContext = (AVAudioResampleContext *)context->opaque) {
if ((resampleContext = (AVAudioResampleContext *) context->opaque)) {
avresample_free(&resampleContext);
context->opaque = NULL;
}

View file

@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.flac;
import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.ExoPlaybackException;
@ -27,7 +26,9 @@ import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
/**
@ -72,7 +73,7 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
public void run() {
Looper.prepare();
LibflacAudioRenderer audioRenderer = new LibflacAudioRenderer();
DefaultTrackSelector trackSelector = new DefaultTrackSelector(new Handler());
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this);
ExtractorMediaSource mediaSource = new ExtractorMediaSource(
@ -91,6 +92,11 @@ public class FlacPlaybackTest extends InstrumentationTestCase {
// Do nothing.
}
@Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
// Do nothing.
}
@Override
public void onPositionDiscontinuity() {
// Do nothing.

View file

@ -95,7 +95,7 @@ public final class FlacExtractor implements Extractor {
if (streamInfo == null) {
throw new IOException("Metadata decoding failed");
}
} catch (IOException e){
} catch (IOException e) {
decoderJni.reset(0);
input.setRetryPosition(0, e);
throw e; // never executes
@ -137,7 +137,7 @@ public final class FlacExtractor implements Extractor {
int size;
try {
size = decoderJni.decodeSample(outputByteBuffer);
} catch (IOException e){
} catch (IOException e) {
if (lastDecodePosition >= 0) {
decoderJni.reset(lastDecodePosition);
input.setRetryPosition(lastDecodePosition, e);
@ -155,7 +155,7 @@ public final class FlacExtractor implements Extractor {
}
@Override
public void seek(long position) {
public void seek(long position, long timeUs) {
if (position == 0) {
metadataParsed = false;
}

View file

@ -19,8 +19,8 @@ import android.os.Handler;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioCapabilities;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.audio.AudioTrack;
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.MimeTypes;
/**
@ -49,11 +49,10 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
* @param streamType The type of audio stream for the {@link AudioTrack}.
*/
public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioCapabilities audioCapabilities, int streamType) {
super(eventHandler, eventListener, audioCapabilities, streamType);
AudioCapabilities audioCapabilities) {
super(eventHandler, eventListener, audioCapabilities);
}
@Override
@ -63,7 +62,8 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
}
@Override
protected FlacDecoder createDecoder(Format format) throws FlacDecoderException {
protected FlacDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
throws FlacDecoderException {
return new FlacDecoder(NUM_BUFFERS, NUM_BUFFERS, format.initializationData);
}

View file

@ -37,7 +37,7 @@ android {
dependencies {
compile project(':library')
compile('com.squareup.okhttp3:okhttp:+') {
compile('com.squareup.okhttp3:okhttp:3.4.1') {
exclude group: 'org.json'
}
}

View file

@ -65,7 +65,8 @@ public class OkHttpDataSource implements HttpDataSource {
private long bytesRead;
/**
* @param callFactory An {@link Call.Factory} for use by the source.
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the source.
* @param userAgent The User-Agent string that should be used.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then a InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
@ -76,7 +77,8 @@ public class OkHttpDataSource implements HttpDataSource {
}
/**
* @param callFactory An {@link Call.Factory} for use by the source.
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the source.
* @param userAgent The User-Agent string that should be used.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then a {@link InvalidContentTypeException} is thrown from
@ -89,14 +91,14 @@ public class OkHttpDataSource implements HttpDataSource {
}
/**
* @param callFactory An {@link Call.Factory} for use by the source.
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the source.
* @param userAgent The User-Agent string that should be used.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
* predicate then a {@link InvalidContentTypeException} is thrown from
* {@link #open(DataSpec)}.
* @param listener An optional listener.
* @param cacheControl An optional {@link CacheControl} which sets all requests' Cache-Control
* header. For example, you could force the network response for all requests.
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
*/
public OkHttpDataSource(Call.Factory callFactory, String userAgent,
Predicate<String> contentTypePredicate, TransferListener<? super OkHttpDataSource> listener,

View file

@ -28,25 +28,38 @@ public final class OkHttpDataSourceFactory implements Factory {
private final Call.Factory callFactory;
private final String userAgent;
private final TransferListener<? super DataSource> transferListener;
private final TransferListener<? super DataSource> listener;
private final CacheControl cacheControl;
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the sources created by the factory.
* @param userAgent The User-Agent string that should be used.
* @param listener An optional listener.
*/
public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent,
TransferListener<? super DataSource> transferListener) {
this(callFactory, userAgent, transferListener, null);
TransferListener<? super DataSource> listener) {
this(callFactory, userAgent, listener, null);
}
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the sources created by the factory.
* @param userAgent The User-Agent string that should be used.
* @param listener An optional listener.
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
*/
public OkHttpDataSourceFactory(Call.Factory callFactory, String userAgent,
TransferListener<? super DataSource> transferListener, CacheControl cacheControl) {
TransferListener<? super DataSource> listener, CacheControl cacheControl) {
this.callFactory = callFactory;
this.userAgent = userAgent;
this.transferListener = transferListener;
this.listener = listener;
this.cacheControl = cacheControl;
}
@Override
public OkHttpDataSource createDataSource() {
return new OkHttpDataSource(callFactory, userAgent, null, transferListener, cacheControl);
return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl);
}
}

View file

@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.opus;
import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.ExoPlaybackException;
@ -27,7 +26,9 @@ import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
/**
@ -72,7 +73,7 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
public void run() {
Looper.prepare();
LibopusAudioRenderer audioRenderer = new LibopusAudioRenderer();
DefaultTrackSelector trackSelector = new DefaultTrackSelector(new Handler());
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
player = ExoPlayerFactory.newInstance(new Renderer[] {audioRenderer}, trackSelector);
player.addListener(this);
ExtractorMediaSource mediaSource = new ExtractorMediaSource(
@ -91,6 +92,11 @@ public class OpusPlaybackTest extends InstrumentationTestCase {
// Do nothing.
}
@Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
// Do nothing.
}
@Override
public void onPositionDiscontinuity() {
// Do nothing.

View file

@ -19,8 +19,9 @@ import android.os.Handler;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AudioCapabilities;
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.audio.AudioTrack;
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.MimeTypes;
/**
@ -50,11 +51,24 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
* @param streamType The type of audio stream for the {@link AudioTrack}.
*/
public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioCapabilities audioCapabilities, int streamType) {
super(eventHandler, eventListener, audioCapabilities, streamType);
AudioCapabilities audioCapabilities) {
super(eventHandler, eventListener, audioCapabilities);
}
/**
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
*/
public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
AudioCapabilities audioCapabilities, DrmSessionManager<ExoMediaCrypto> drmSessionManager,
boolean playClearSamplesWithoutKeys) {
super(eventHandler, eventListener, audioCapabilities, drmSessionManager,
playClearSamplesWithoutKeys);
}
@Override
@ -64,9 +78,10 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
}
@Override
protected OpusDecoder createDecoder(Format format) throws OpusDecoderException {
protected OpusDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
throws OpusDecoderException {
return new OpusDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
format.initializationData);
format.initializationData, mediaCrypto);
}
}

View file

@ -16,9 +16,12 @@
package com.google.android.exoplayer2.ext.opus;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
import com.google.android.exoplayer2.drm.DecryptionException;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.List;
@ -36,6 +39,12 @@ import java.util.List;
*/
private static final int SAMPLE_RATE = 48000;
private static final int NO_ERROR = 0;
private static final int DECODE_ERROR = -1;
private static final int DRM_ERROR = -2;
private final ExoMediaCrypto exoMediaCrypto;
private final int channelCount;
private final int headerSkipSamples;
private final int headerSeekPreRollSamples;
@ -52,14 +61,20 @@ import java.util.List;
* @param initializationData Codec-specific initialization data. The first element must contain an
* opus header. Optionally, the list may contain two additional buffers, which must contain
* the encoder delay and seek pre roll values in nanoseconds, encoded as longs.
* @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted
* content. Maybe null and can be ignored if decoder does not handle encrypted content.
* @throws OpusDecoderException Thrown if an exception occurs when initializing the decoder.
*/
public OpusDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
List<byte[]> initializationData) throws OpusDecoderException {
List<byte[]> initializationData, ExoMediaCrypto exoMediaCrypto) throws OpusDecoderException {
super(new DecoderInputBuffer[numInputBuffers], new SimpleOutputBuffer[numOutputBuffers]);
if (!OpusLibrary.isAvailable()) {
throw new OpusDecoderException("Failed to load decoder native libraries.");
}
this.exoMediaCrypto = exoMediaCrypto;
if (exoMediaCrypto != null && !OpusLibrary.opusIsSecureDecodeSupported()) {
throw new OpusDecoderException("Opus decoder does not support secure decode.");
}
byte[] headerBytes = initializationData.get(0);
if (headerBytes.length < 19) {
throw new OpusDecoderException("Header size is too small.");
@ -139,11 +154,25 @@ import java.util.List;
skipSamples = (inputBuffer.timeUs == 0) ? headerSkipSamples : headerSeekPreRollSamples;
}
ByteBuffer inputData = inputBuffer.data;
int result = opusDecode(nativeDecoderContext, inputBuffer.timeUs, inputData, inputData.limit(),
outputBuffer, SAMPLE_RATE);
CryptoInfo cryptoInfo = inputBuffer.cryptoInfo;
int result = inputBuffer.isEncrypted()
? opusSecureDecode(nativeDecoderContext, inputBuffer.timeUs, inputData, inputData.limit(),
outputBuffer, SAMPLE_RATE, exoMediaCrypto, cryptoInfo.mode,
cryptoInfo.key, cryptoInfo.iv, cryptoInfo.numSubSamples,
cryptoInfo.numBytesOfClearData, cryptoInfo.numBytesOfEncryptedData)
: opusDecode(nativeDecoderContext, inputBuffer.timeUs, inputData, inputData.limit(),
outputBuffer, SAMPLE_RATE);
if (result < 0) {
return new OpusDecoderException("Decode error: " + opusGetErrorMessage(result));
if (result == DRM_ERROR) {
String message = "Drm error: " + opusGetErrorMessage(nativeDecoderContext);
DecryptionException cause = new DecryptionException(
opusGetErrorCode(nativeDecoderContext), message);
return new OpusDecoderException(message, cause);
} else {
return new OpusDecoderException("Decode error: " + opusGetErrorMessage(result));
}
}
ByteBuffer outputData = outputBuffer.data;
outputData.position(0);
outputData.limit(result);
@ -182,8 +211,13 @@ import java.util.List;
int gain, byte[] streamMap);
private native int opusDecode(long decoder, long timeUs, ByteBuffer inputBuffer, int inputSize,
SimpleOutputBuffer outputBuffer, int sampleRate);
private native int opusSecureDecode(long decoder, long timeUs, ByteBuffer inputBuffer,
int inputSize, SimpleOutputBuffer outputBuffer, int sampleRate,
ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv,
int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
private native void opusClose(long decoder);
private native void opusReset(long decoder);
private native String opusGetErrorMessage(int errorCode);
private native int opusGetErrorCode(long decoder);
private native String opusGetErrorMessage(long decoder);
}

View file

@ -26,4 +26,8 @@ public final class OpusDecoderException extends AudioDecoderException {
super(message);
}
/* package */ OpusDecoderException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -50,5 +50,5 @@ public final class OpusLibrary {
}
public static native String opusGetVersion();
public static native boolean opusIsSecureDecodeSupported();
}

View file

@ -60,11 +60,13 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
static const int kBytesPerSample = 2; // opus fixed point uses 16 bit samples.
static int channelCount;
static int errorCode;
DECODER_FUNC(jlong, opusInit, jint sampleRate, jint channelCount,
jint numStreams, jint numCoupled, jint gain, jbyteArray jStreamMap) {
int status = OPUS_INVALID_STATE;
::channelCount = channelCount;
errorCode = 0;
jbyte* streamMapBytes = env->GetByteArrayElements(jStreamMap, 0);
uint8_t* streamMap = reinterpret_cast<uint8_t*>(streamMapBytes);
OpusMSDecoder* decoder = opus_multistream_decoder_create(
@ -109,10 +111,24 @@ DECODER_FUNC(jint, opusDecode, jlong jDecoder, jlong jTimeUs,
env->GetDirectBufferAddress(jOutputBufferData));
int sampleCount = opus_multistream_decode(decoder, inputBuffer, inputSize,
outputBufferData, outputSize, 0);
// record error code
errorCode = (sampleCount < 0) ? sampleCount : 0;
return (sampleCount < 0) ? sampleCount
: sampleCount * kBytesPerSample * channelCount;
}
DECODER_FUNC(jint, opusSecureDecode, jlong jDecoder, jlong jTimeUs,
jobject jInputBuffer, jint inputSize, jobject jOutputBuffer,
jint sampleRate, jobject mediaCrypto, jint inputMode, jbyteArray key,
jbyteArray javaIv, jint inputNumSubSamples, jintArray numBytesOfClearData,
jintArray numBytesOfEncryptedData) {
// Doesn't support
// Java client should have checked vpxSupportSecureDecode
// and avoid calling this
// return -2 (DRM Error)
return -2;
}
DECODER_FUNC(void, opusClose, jlong jDecoder) {
OpusMSDecoder* decoder = reinterpret_cast<OpusMSDecoder*>(jDecoder);
opus_multistream_decoder_destroy(decoder);
@ -123,10 +139,19 @@ DECODER_FUNC(void, opusReset, jlong jDecoder) {
opus_multistream_decoder_ctl(decoder, OPUS_RESET_STATE);
}
DECODER_FUNC(jstring, opusGetErrorMessage, jint errorCode) {
DECODER_FUNC(jstring, opusGetErrorMessage, jlong jContext) {
return env->NewStringUTF(opus_strerror(errorCode));
}
DECODER_FUNC(jint, opusGetErrorCode, jlong jContext) {
return errorCode;
}
LIBRARY_FUNC(jstring, opusIsSecureDecodeSupported) {
// Doesn't support
return 0;
}
LIBRARY_FUNC(jstring, opusGetVersion) {
return env->NewStringUTF(opus_get_version_string());
}

View file

@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ext.vp9;
import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.ExoPlaybackException;
@ -27,7 +26,9 @@ import com.google.android.exoplayer2.Renderer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
/**
@ -88,7 +89,7 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
public void run() {
Looper.prepare();
LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(true, 0);
DefaultTrackSelector trackSelector = new DefaultTrackSelector(new Handler());
DefaultTrackSelector trackSelector = new DefaultTrackSelector();
player = ExoPlayerFactory.newInstance(new Renderer[] {videoRenderer}, trackSelector);
player.addListener(this);
ExtractorMediaSource mediaSource = new ExtractorMediaSource(
@ -110,6 +111,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
// Do nothing.
}
@Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
// Do nothing.
}
@Override
public void onPositionDiscontinuity() {
// Do nothing.

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.ext.vp9;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.view.Surface;
import com.google.android.exoplayer2.BaseRenderer;
@ -28,8 +29,13 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import com.google.android.exoplayer2.video.VideoRendererEventListener.EventDispatcher;
@ -56,8 +62,10 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
private final boolean scaleToFit;
private final long allowedJoiningTimeMs;
private final int maxDroppedFramesToNotify;
private final boolean playClearSamplesWithoutKeys;
private final EventDispatcher eventDispatcher;
private final FormatHolder formatHolder;
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
private DecoderCounters decoderCounters;
private Format format;
@ -65,6 +73,8 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
private DecoderInputBuffer inputBuffer;
private VpxOutputBuffer outputBuffer;
private VpxOutputBuffer nextOutputBuffer;
private DrmSession<ExoMediaCrypto> drmSession;
private DrmSession<ExoMediaCrypto> pendingDrmSession;
private Bitmap bitmap;
private boolean renderedFirstFrame;
@ -72,11 +82,12 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
private Surface surface;
private VpxOutputBufferRenderer outputBufferRenderer;
private int outputMode;
private boolean waitingForKeys;
private boolean inputStreamEnded;
private boolean outputStreamEnded;
private int previousWidth;
private int previousHeight;
private int lastReportedWidth;
private int lastReportedHeight;
private long droppedFrameAccumulationStartTimeMs;
private int droppedFrames;
@ -104,13 +115,39 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs,
Handler eventHandler, VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify) {
this(scaleToFit, allowedJoiningTimeMs, eventHandler, eventListener, maxDroppedFramesToNotify,
null, false);
}
/**
* @param scaleToFit Whether video frames should be scaled to fit when rendering.
* @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer
* can attempt to seamlessly join an ongoing playback.
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between
* invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}.
* @param drmSessionManager For use with encrypted media. May be null if support for encrypted
* media is not required.
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
* For example a media file may start with a short clear region so as to allow playback to
* begin in parallel with key acquisition. This parameter specifies whether the renderer is
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media.
*/
public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs,
Handler eventHandler, VideoRendererEventListener eventListener,
int maxDroppedFramesToNotify, DrmSessionManager<ExoMediaCrypto> drmSessionManager,
boolean playClearSamplesWithoutKeys) {
super(C.TRACK_TYPE_VIDEO);
this.scaleToFit = scaleToFit;
this.allowedJoiningTimeMs = allowedJoiningTimeMs;
this.maxDroppedFramesToNotify = maxDroppedFramesToNotify;
this.drmSessionManager = drmSessionManager;
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
joiningDeadlineMs = -1;
previousWidth = -1;
previousHeight = -1;
clearLastReportedVideoSize();
formatHolder = new FormatHolder();
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
outputMode = VpxDecoder.OUTPUT_MODE_NONE;
@ -135,12 +172,27 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
if (isRendererAvailable()) {
drmSession = pendingDrmSession;
ExoMediaCrypto mediaCrypto = null;
if (drmSession != null) {
int drmSessionState = drmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
} else if (drmSessionState == DrmSession.STATE_OPENED
|| drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) {
mediaCrypto = drmSession.getMediaCrypto();
} else {
// The drm session isn't open yet.
return;
}
}
try {
if (decoder == null) {
// If we don't have a decoder yet, we need to instantiate one.
long codecInitializingTimestamp = SystemClock.elapsedRealtime();
TraceUtil.beginSection("createVpxDecoder");
decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE);
decoder = new VpxDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
mediaCrypto);
decoder.setOutputMode(outputMode);
TraceUtil.endSection();
long codecInitializedTimestamp = SystemClock.elapsedRealtime();
@ -258,7 +310,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
surface.unlockCanvasAndPost(canvas);
}
private boolean feedInputBuffer() throws VpxDecoderException {
private boolean feedInputBuffer() throws VpxDecoderException, ExoPlaybackException {
if (inputStreamEnded) {
return false;
}
@ -270,7 +322,14 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
}
}
int result = readSource(formatHolder, inputBuffer);
int result;
if (waitingForKeys) {
// We've already read an encrypted sample into buffer, and are waiting for keys.
result = C.RESULT_BUFFER_READ;
} else {
result = readSource(formatHolder, inputBuffer);
}
if (result == C.RESULT_NOTHING_READ) {
return false;
}
@ -284,6 +343,11 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
inputBuffer = null;
return false;
}
boolean bufferEncrypted = inputBuffer.isEncrypted();
waitingForKeys = shouldWaitForKeys(bufferEncrypted);
if (waitingForKeys) {
return false;
}
inputBuffer.flip();
decoder.queueInputBuffer(inputBuffer);
decoderCounters.inputBufferCount++;
@ -291,8 +355,21 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
return true;
}
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
if (drmSession == null) {
return false;
}
int drmSessionState = drmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
}
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS
&& (bufferEncrypted || !playClearSamplesWithoutKeys);
}
private void flushDecoder() {
inputBuffer = null;
waitingForKeys = false;
if (outputBuffer != null) {
outputBuffer.release();
outputBuffer = null;
@ -311,6 +388,9 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
@Override
public boolean isReady() {
if (waitingForKeys) {
return false;
}
if (format != null && (isSourceReady() || outputBuffer != null)
&& (renderedFirstFrame || !isRendererAvailable())) {
// Ready. If we were joining then we've now joined, so clear the joining deadline.
@ -365,11 +445,27 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
inputBuffer = null;
outputBuffer = null;
format = null;
waitingForKeys = false;
clearLastReportedVideoSize();
try {
releaseDecoder();
} finally {
decoderCounters.ensureUpdated();
eventDispatcher.disabled(decoderCounters);
try {
if (drmSession != null) {
drmSessionManager.releaseSession(drmSession);
}
} finally {
try {
if (pendingDrmSession != null && pendingDrmSession != drmSession) {
drmSessionManager.releaseSession(pendingDrmSession);
}
} finally {
drmSession = null;
pendingDrmSession = null;
decoderCounters.ensureUpdated();
eventDispatcher.disabled(decoderCounters);
}
}
}
}
@ -378,10 +474,18 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
decoder.release();
decoder = null;
decoderCounters.decoderReleaseCount++;
waitingForKeys = false;
if (drmSession != null && pendingDrmSession != drmSession) {
try {
drmSessionManager.releaseSession(drmSession);
} finally {
drmSession = null;
}
}
}
}
private boolean readFormat() {
private boolean readFormat() throws ExoPlaybackException {
int result = readSource(formatHolder, null);
if (result == C.RESULT_FORMAT_READ) {
onInputFormatChanged(formatHolder.format);
@ -390,42 +494,56 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
return false;
}
private void onInputFormatChanged(Format newFormat) {
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
Format oldFormat = format;
format = newFormat;
boolean drmInitDataChanged = !Util.areEqual(format.drmInitData, oldFormat == null ? null
: oldFormat.drmInitData);
if (drmInitDataChanged) {
if (format.drmInitData != null) {
if (drmSessionManager == null) {
throw ExoPlaybackException.createForRenderer(
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
}
pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(), format.drmInitData);
if (pendingDrmSession == drmSession) {
drmSessionManager.releaseSession(pendingDrmSession);
}
} else {
pendingDrmSession = null;
}
}
eventDispatcher.inputFormatChanged(format);
}
@Override
public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
if (messageType == C.MSG_SET_SURFACE) {
setSurface((Surface) message);
setOutput((Surface) message, null);
} else if (messageType == MSG_SET_OUTPUT_BUFFER_RENDERER) {
setOutputBufferRenderer((VpxOutputBufferRenderer) message);
setOutput(null, (VpxOutputBufferRenderer) message);
} else {
super.handleMessage(messageType, message);
}
}
private void setSurface(Surface surface) {
if (this.surface == surface) {
return;
}
private void setOutput(Surface surface, VpxOutputBufferRenderer outputBufferRenderer) {
// At most one output may be non-null. Both may be null if the output is being cleared.
Assertions.checkState(surface == null || outputBufferRenderer == null);
// Clear state so that we always call the event listener with the video size and when a frame
// is rendered, even if the output hasn't changed.
renderedFirstFrame = false;
this.surface = surface;
outputBufferRenderer = null;
outputMode = (surface != null) ? VpxDecoder.OUTPUT_MODE_RGB : VpxDecoder.OUTPUT_MODE_NONE;
updateDecoder();
}
private void setOutputBufferRenderer(VpxOutputBufferRenderer outputBufferRenderer) {
if (this.outputBufferRenderer == outputBufferRenderer) {
return;
clearLastReportedVideoSize();
// We only need to update the decoder if the output has changed.
if (this.surface != surface || this.outputBufferRenderer != outputBufferRenderer) {
this.surface = surface;
this.outputBufferRenderer = outputBufferRenderer;
outputMode = outputBufferRenderer != null ? VpxDecoder.OUTPUT_MODE_YUV
: surface != null ? VpxDecoder.OUTPUT_MODE_RGB : VpxDecoder.OUTPUT_MODE_NONE;
updateDecoder();
}
this.outputBufferRenderer = outputBufferRenderer;
surface = null;
outputMode = (outputBufferRenderer != null) ? VpxDecoder.OUTPUT_MODE_YUV
: VpxDecoder.OUTPUT_MODE_NONE;
updateDecoder();
}
private void updateDecoder() {
@ -442,10 +560,15 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
return surface != null || outputBufferRenderer != null;
}
private void maybeNotifyVideoSizeChanged(final int width, final int height) {
if (previousWidth != width || previousHeight != height) {
previousWidth = width;
previousHeight = height;
private void clearLastReportedVideoSize() {
lastReportedWidth = Format.NO_VALUE;
lastReportedHeight = Format.NO_VALUE;
}
private void maybeNotifyVideoSizeChanged(int width, int height) {
if (lastReportedWidth != width || lastReportedHeight != height) {
lastReportedWidth = width;
lastReportedHeight = height;
eventDispatcher.videoSizeChanged(width, height, 0, 1);
}
}

View file

@ -16,8 +16,11 @@
package com.google.android.exoplayer2.ext.vp9;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.decoder.CryptoInfo;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.drm.DecryptionException;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import java.nio.ByteBuffer;
/**
@ -30,6 +33,11 @@ import java.nio.ByteBuffer;
public static final int OUTPUT_MODE_YUV = 0;
public static final int OUTPUT_MODE_RGB = 1;
private static final int NO_ERROR = 0;
private static final int DECODE_ERROR = 1;
private static final int DRM_ERROR = 2;
private final ExoMediaCrypto exoMediaCrypto;
private final long vpxDecContext;
private volatile int outputMode;
@ -40,14 +48,20 @@ import java.nio.ByteBuffer;
* @param numInputBuffers The number of input buffers.
* @param numOutputBuffers The number of output buffers.
* @param initialInputBufferSize The initial size of each input buffer.
* @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted
* content. Maybe null and can be ignored if decoder does not handle encrypted content.
* @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder.
*/
public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize)
throws VpxDecoderException {
public VpxDecoder(int numInputBuffers, int numOutputBuffers, int initialInputBufferSize,
ExoMediaCrypto exoMediaCrypto) throws VpxDecoderException {
super(new DecoderInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]);
if (!VpxLibrary.isAvailable()) {
throw new VpxDecoderException("Failed to load decoder native libraries.");
}
this.exoMediaCrypto = exoMediaCrypto;
if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) {
throw new VpxDecoderException("Vpx decoder does not support secure decode.");
}
vpxDecContext = vpxInit();
if (vpxDecContext == 0) {
throw new VpxDecoderException("Failed to initialize decoder");
@ -90,12 +104,29 @@ import java.nio.ByteBuffer;
boolean reset) {
ByteBuffer inputData = inputBuffer.data;
int inputSize = inputData.limit();
if (vpxDecode(vpxDecContext, inputData, inputSize) != 0) {
return new VpxDecoderException("Decode error: " + vpxGetErrorMessage(vpxDecContext));
CryptoInfo cryptoInfo = inputBuffer.cryptoInfo;
final long result = inputBuffer.isEncrypted()
? vpxSecureDecode(vpxDecContext, inputData, inputSize, exoMediaCrypto,
cryptoInfo.mode, cryptoInfo.key, cryptoInfo.iv, cryptoInfo.numSubSamples,
cryptoInfo.numBytesOfClearData, cryptoInfo.numBytesOfEncryptedData)
: vpxDecode(vpxDecContext, inputData, inputSize);
if (result != NO_ERROR) {
if (result == DRM_ERROR) {
String message = "Drm error: " + vpxGetErrorMessage(vpxDecContext);
DecryptionException cause = new DecryptionException(
vpxGetErrorCode(vpxDecContext), message);
return new VpxDecoderException(message, cause);
} else {
return new VpxDecoderException("Decode error: " + vpxGetErrorMessage(vpxDecContext));
}
}
outputBuffer.init(inputBuffer.timeUs, outputMode);
if (vpxGetFrame(vpxDecContext, outputBuffer) != 0) {
int getFrameResult = vpxGetFrame(vpxDecContext, outputBuffer);
if (getFrameResult == 1) {
outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
} else if (getFrameResult == -1) {
return new VpxDecoderException("Buffer initialization failed.");
}
return null;
}
@ -109,7 +140,11 @@ import java.nio.ByteBuffer;
private native long vpxInit();
private native long vpxClose(long context);
private native long vpxDecode(long context, ByteBuffer encoded, int length);
private native long vpxSecureDecode(long context, ByteBuffer encoded, int length,
ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv,
int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer);
private native int vpxGetErrorCode(long context);
private native String vpxGetErrorMessage(long context);
}

View file

@ -20,8 +20,11 @@ package com.google.android.exoplayer2.ext.vp9;
*/
public class VpxDecoderException extends Exception {
/* package */ VpxDecoderException(String message) {
super(message);
}
/* package */ VpxDecoderException(String message) {
super(message);
}
/* package */ VpxDecoderException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -59,5 +59,5 @@ public final class VpxLibrary {
private static native String vpxGetVersion();
private static native String vpxGetBuildConfig();
public static native boolean vpxIsSecureDecodeSupported();
}

View file

@ -66,28 +66,39 @@ import java.nio.ByteBuffer;
/**
* Resizes the buffer based on the given dimensions. Called via JNI after decoding completes.
* @return Whether the buffer was resized successfully.
*/
public void initForRgbFrame(int width, int height) {
public boolean initForRgbFrame(int width, int height) {
this.width = width;
this.height = height;
this.yuvPlanes = null;
if (!isSafeToMultiply(width, height) || !isSafeToMultiply(width * height, 2)) {
return false;
}
int minimumRgbSize = width * height * 2;
initData(minimumRgbSize);
return true;
}
/**
* Resizes the buffer based on the given stride. Called via JNI after decoding completes.
* @return Whether the buffer was resized successfully.
*/
public void initForYuvFrame(int width, int height, int yStride, int uvStride,
public boolean initForYuvFrame(int width, int height, int yStride, int uvStride,
int colorspace) {
this.width = width;
this.height = height;
this.colorspace = colorspace;
int uvHeight = (int) (((long) height + 1) / 2);
if (!isSafeToMultiply(yStride, height) || !isSafeToMultiply(uvStride, uvHeight)) {
return false;
}
int yLength = yStride * height;
int uvLength = uvStride * ((height + 1) / 2);
int uvLength = uvStride * uvHeight;
int minimumYuvSize = yLength + (uvLength * 2);
if (!isSafeToMultiply(uvLength, 2) || minimumYuvSize < yLength) {
return false;
}
initData(minimumYuvSize);
if (yuvPlanes == null) {
@ -108,6 +119,7 @@ import java.nio.ByteBuffer;
yuvStrides[0] = yStride;
yuvStrides[1] = uvStride;
yuvStrides[2] = uvStride;
return true;
}
private void initData(int size) {
@ -119,4 +131,12 @@ import java.nio.ByteBuffer;
}
}
/**
* Ensures that the result of multiplying individual numbers can fit into the size limit of an
* integer.
*/
private boolean isSafeToMultiply(int a, int b) {
return a >= 0 && b >= 0 && !(b > 0 && a >= Integer.MAX_VALUE / b);
}
}

View file

@ -73,10 +73,13 @@ import javax.microedition.khronos.opengles.GL10;
private final int[] yuvTextures = new int[3];
private final AtomicReference<VpxOutputBuffer> pendingOutputBufferReference;
// Kept in a field rather than a local variable so that it doesn't get garbage collected before
// glDrawArrays uses it.
@SuppressWarnings("FieldCanBeLocal")
private FloatBuffer textureCoords;
private int program;
private int texLocation;
private int colorMatrixLocation;
private FloatBuffer textureCoords;
private int previousWidth;
private int previousStride;

View file

@ -59,6 +59,7 @@ static jmethodID initForRgbFrame;
static jmethodID initForYuvFrame;
static jfieldID dataField;
static jfieldID outputModeField;
static int errorCode;
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
@ -72,6 +73,7 @@ DECODER_FUNC(jlong, vpxInit) {
vpx_codec_ctx_t* context = new vpx_codec_ctx_t();
vpx_codec_dec_cfg_t cfg = {0, 0, 0};
cfg.threads = android_getCpuCount();
errorCode = 0;
if (vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo, &cfg, 0)) {
LOGE("ERROR: Fail to initialize libvpx decoder.");
return 0;
@ -81,9 +83,9 @@ DECODER_FUNC(jlong, vpxInit) {
const jclass outputBufferClass = env->FindClass(
"com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer");
initForYuvFrame = env->GetMethodID(outputBufferClass, "initForYuvFrame",
"(IIIII)V");
"(IIIII)Z");
initForRgbFrame = env->GetMethodID(outputBufferClass, "initForRgbFrame",
"(II)V");
"(II)Z");
dataField = env->GetFieldID(outputBufferClass, "data",
"Ljava/nio/ByteBuffer;");
outputModeField = env->GetFieldID(outputBufferClass, "mode", "I");
@ -97,13 +99,26 @@ DECODER_FUNC(jlong, vpxDecode, jlong jContext, jobject encoded, jint len) {
reinterpret_cast<const uint8_t*>(env->GetDirectBufferAddress(encoded));
const vpx_codec_err_t status =
vpx_codec_decode(context, buffer, len, NULL, 0);
errorCode = 0;
if (status != VPX_CODEC_OK) {
LOGE("ERROR: vpx_codec_decode() failed, status= %d", status);
errorCode = status;
return -1;
}
return 0;
}
DECODER_FUNC(jlong, vpxSecureDecode, jlong jContext, jobject encoded, jint len,
jobject mediaCrypto, jint inputMode, jbyteArray&, jbyteArray&,
jint inputNumSubSamples, jintArray numBytesOfClearData,
jintArray numBytesOfEncryptedData) {
// Doesn't support
// Java client should have checked vpxSupportSecureDecode
// and avoid calling this
// return -2 (DRM Error)
return -2;
}
DECODER_FUNC(jlong, vpxClose, jlong jContext) {
vpx_codec_ctx_t* const context = reinterpret_cast<vpx_codec_ctx_t*>(jContext);
vpx_codec_destroy(context);
@ -126,7 +141,11 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
int outputMode = env->GetIntField(jOutputBuffer, outputModeField);
if (outputMode == kOutputModeRgb) {
// resize buffer if required.
env->CallVoidMethod(jOutputBuffer, initForRgbFrame, img->d_w, img->d_h);
jboolean initResult = env->CallBooleanMethod(jOutputBuffer, initForRgbFrame,
img->d_w, img->d_h);
if (initResult == JNI_FALSE) {
return -1;
}
// get pointer to the data buffer.
const jobject dataObject = env->GetObjectField(jOutputBuffer, dataField);
@ -155,9 +174,12 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
}
// resize buffer if required.
env->CallVoidMethod(jOutputBuffer, initForYuvFrame, img->d_w, img->d_h,
img->stride[VPX_PLANE_Y], img->stride[VPX_PLANE_U],
colorspace);
jboolean initResult = env->CallBooleanMethod(
jOutputBuffer, initForYuvFrame, img->d_w, img->d_h,
img->stride[VPX_PLANE_Y], img->stride[VPX_PLANE_U], colorspace);
if (initResult == JNI_FALSE) {
return -1;
}
// get pointer to the data buffer.
const jobject dataObject = env->GetObjectField(jOutputBuffer, dataField);
@ -181,6 +203,15 @@ DECODER_FUNC(jstring, vpxGetErrorMessage, jlong jContext) {
return env->NewStringUTF(vpx_codec_error(context));
}
DECODER_FUNC(jint, vpxGetErrorCode, jlong jContext) {
return errorCode;
}
LIBRARY_FUNC(jstring, vpxIsSecureDecodeSupported) {
// Doesn't support
return 0;
}
LIBRARY_FUNC(jstring, vpxGetVersion) {
return env->NewStringUTF(vpx_codec_version_str());
}

View file

@ -1,6 +1,6 @@
#Thu Sep 01 11:39:15 BST 2016
#Mon Oct 24 14:40:37 BST 2016
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip

View file

@ -35,9 +35,11 @@ android {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
debug {
testCoverageEnabled = true
}
// Re-enable test coverage when the following issue is fixed:
// https://code.google.com/p/android/issues/detail?id=226070
// debug {
// testCoverageEnabled = true
// }
}
lintOptions {
@ -55,7 +57,7 @@ dependencies {
androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
androidTestCompile 'org.mockito:mockito-core:1.9.5'
compile 'com.android.support:support-annotations:24.2.0'
compile 'com.android.support:support-annotations:25.0.1'
}
android.libraryVariants.all { variant ->

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:mpeg:DASH:schema:MPD:2011" xmlns:yt="http://youtube.com/yt/2012/10/10" xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd" minBufferTime="PT1.500S" profiles="urn:mpeg:dash:profile:isoff-main:2011" type="dynamic" availabilityStartTime="2016-10-14T17:00:17" timeShiftBufferDepth="PT7200.000S" minimumUpdatePeriod="PT2.000S" yt:earliestMediaSequence="0" yt:mpdRequestTime="2016-10-14T18:29:17.082" yt:mpdResponseTime="2016-10-14T18:29:17.194">
<Period start="PT0.000S" yt:segmentIngestTime="2016-10-14T17:00:14.257">
<SegmentTemplate startNumber="0" timescale="1000" media="sq/$Number$">
<SegmentTimeline>
<S d="2002" t="6009" r="2"/>
<S d="1985"/>
<S d="2000"/>
</SegmentTimeline>
</SegmentTemplate>
<AdaptationSet id="0" mimeType="audio/mp4" subsegmentAlignment="true">
<Role schemeIdUri="urn:mpeg:DASH:role:2011" value="main"/>
<Representation id="140" codecs="mp4a.40.2" audioSamplingRate="48000" startWithSAP="1" bandwidth="144000">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
<BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/140/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/audio%2Fmp4/live/1/gir/yes/noclen/1/signature/B5137EA0CC278C07DD056D204E863CC81EDEB39E.1AD5D242EBC94922EDA7165353A89A5E08A4103A/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>
</Representation>
</AdaptationSet>
<AdaptationSet id="1" mimeType="video/mp4" subsegmentAlignment="true">
<Role schemeIdUri="urn:mpeg:DASH:role:2011" value="main"/>
<Representation id="133" codecs="avc1.4d4015" width="426" height="240" startWithSAP="1" maxPlayoutRate="1" bandwidth="258000" frameRate="30">
<BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/133/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/90154AE9C5C9D9D519CBF2E43AB0A1778375992D.40E2E855ADFB38FA7E95E168FEEEA6796B080BD7/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>
</Representation>
<Representation id="134" codecs="avc1.4d401e" width="640" height="360" startWithSAP="1" maxPlayoutRate="1" bandwidth="646000" frameRate="30">
<BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/134/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/5C094AEFDCEB1A4D2F3C05F8BD095C336EF0E1C3.7AE6B9951B0237AAE6F031927AACAC4974BAFFAA/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>
</Representation>
<Representation id="135" codecs="avc1.4d401f" width="854" height="480" startWithSAP="1" maxPlayoutRate="1" bandwidth="1171000" frameRate="30">
<BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/135/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/1F7660CA4E5B4AE4D60E18795680E34CDD2EF3C9.800B0A1D5F490DE142CCF4C88C64FD21D42129/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>
</Representation>
<Representation id="160" codecs="avc1.42c00b" width="256" height="144" startWithSAP="1" maxPlayoutRate="1" bandwidth="124000" frameRate="30">
<BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/160/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/94EB61673784DF0C4237A1A866F2E171C8A64ADB.AEC00AA06C2278FEA8702FB62693B70D8977F46C/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>
</Representation>
<Representation id="136" codecs="avc1.4d401f" width="1280" height="720" startWithSAP="1" maxPlayoutRate="1" bandwidth="2326000" frameRate="30">
<BaseURL>http://redirector.googlevideo.com/videoplayback/id/BktsoMO3OMs.0/itag/136/source/yt_live_broadcast/ratebypass/yes/cmbypass/yes/mime/video%2Fmp4/live/1/gir/yes/noclen/1/signature/6D8C34FC30A1F1A4F700B61180D1C4CCF6274844.29EBCB4A837DE626C52C66CF650519E61C2FF0BF/key/dg_test0/mpd_version/5/ip/0.0.0.0/ipbits/0/expire/1476490914/sparams/ip,ipbits,expire,id,itag,source,ratebypass,cmbypass,mime,live,gir,noclen/</BaseURL>
</Representation>
</AdaptationSet>
</Period>
</MPD>

View file

@ -22,7 +22,7 @@ track 0:
encoderPadding = -1
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = und
language = null
drmInitData = -
initializationData:
data = length 19, hash BFE794DB

View file

@ -22,7 +22,7 @@ track 0:
encoderPadding = -1
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = und
language = null
drmInitData = -
initializationData:
data = length 19, hash BFE794DB

View file

@ -22,7 +22,7 @@ track 0:
encoderPadding = -1
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = und
language = null
drmInitData = -
initializationData:
data = length 19, hash BFE794DB

View file

@ -22,7 +22,7 @@ track 0:
encoderPadding = -1
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = und
language = null
drmInitData = -
initializationData:
data = length 19, hash BFE794DB

View file

@ -22,7 +22,7 @@ track 0:
encoderPadding = -1
subsampleOffsetUs = 9223372036854775807
selectionFlags = 0
language = und
language = null
drmInitData = -
initializationData:
data = length 19, hash BFE794DB

View file

@ -0,0 +1,12 @@
1
-0:00:04,567 --> -0:00:03,456
This is the first subtitle.
2
-00:00:02,345 --> 00:00:01,234
This is the second subtitle.
Second subtitle with second line.
3
00:00:04,567 --> 00:00:08,901
This is the third subtitle.

Binary file not shown.

View file

@ -0,0 +1,443 @@
/*
* 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;
import android.os.Handler;
import android.os.HandlerThread;
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;
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.MimeTypes;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import junit.framework.TestCase;
/**
* Unit test for {@link ExoPlayer}.
*/
public final class ExoPlayerTest extends TestCase {
/**
* For tests that rely on the player transitioning to the ended state, the duration in
* milliseconds after starting the player before the test will time out. This is to catch cases
* where the player under test is not making progress, in which case the test should fail.
*/
private static final int TIMEOUT_MS = 10000;
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), new Object(), format);
playerWrapper.blockUntilEndedOrError(TIMEOUT_MS);
}
/**
* Wraps a player with its own handler thread.
*/
private static final class PlayerWrapper implements ExoPlayer.EventListener {
private final CountDownLatch endedCountDownLatch;
private final HandlerThread playerThread;
private final Handler handler;
private Timeline expectedTimeline;
private Object expectedManifest;
private Format expectedFormat;
private ExoPlayer player;
private Exception exception;
private boolean seenPositionDiscontinuity;
public PlayerWrapper() {
endedCountDownLatch = new CountDownLatch(1);
playerThread = new HandlerThread("ExoPlayerTest thread");
playerThread.start();
handler = new Handler(playerThread.getLooper());
}
// Called on the test thread.
public void blockUntilEndedOrError(long timeoutMs) throws Exception {
if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
exception = new TimeoutException("Test playback timed out.");
}
release();
// Throw any pending exception (from playback, timing out or releasing).
if (exception != null) {
throw exception;
}
}
public void setup(final Timeline timeline, final Object manifest, final Format format) {
expectedTimeline = timeline;
expectedManifest = manifest;
expectedFormat = format;
handler.post(new Runnable() {
@Override
public void run() {
try {
Renderer fakeRenderer = new FakeVideoRenderer(expectedFormat);
player = ExoPlayerFactory.newInstance(new Renderer[] {fakeRenderer},
new DefaultTrackSelector());
player.addListener(PlayerWrapper.this);
player.setPlayWhenReady(true);
player.prepare(new FakeMediaSource(timeline, manifest, format));
} catch (Exception e) {
handlePlayerException(e);
}
}
});
}
public void release() throws InterruptedException {
handler.post(new Runnable() {
@Override
public void run() {
try {
if (player != null) {
player.release();
}
} catch (Exception e) {
handlePlayerException(e);
} finally {
playerThread.quit();
}
}
});
playerThread.join();
}
private void handlePlayerException(Exception exception) {
if (this.exception == null) {
this.exception = exception;
}
endedCountDownLatch.countDown();
}
// ExoPlayer.EventListener implementation.
@Override
public void onLoadingChanged(boolean isLoading) {
// Do nothing.
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (playbackState == ExoPlayer.STATE_ENDED) {
endedCountDownLatch.countDown();
}
}
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
assertEquals(expectedTimeline, timeline);
assertEquals(expectedManifest, manifest);
}
@Override
public void onTracksChanged(TrackGroupArray trackGroups,
TrackSelectionArray trackSelections) {
assertEquals(new TrackGroupArray(new TrackGroup(expectedFormat)), trackGroups);
}
@Override
public void onPlayerError(ExoPlaybackException exception) {
this.exception = exception;
endedCountDownLatch.countDown();
}
@Override
public void onPositionDiscontinuity() {
assertFalse(seenPositionDiscontinuity);
assertEquals(0, player.getCurrentWindowIndex());
assertEquals(0, player.getCurrentPeriodIndex());
assertEquals(0, player.getCurrentPosition());
assertEquals(0, player.getBufferedPosition());
assertEquals(expectedTimeline, player.getCurrentTimeline());
assertEquals(expectedManifest, player.getCurrentManifest());
seenPositionDiscontinuity = true;
}
}
/**
* Fake {@link MediaSource} that provides a given timeline (which must have one period). Creating
* the period will return a {@link FakeMediaPeriod}.
*/
private static final class FakeMediaSource implements MediaSource {
private final Timeline timeline;
private final Object manifest;
private final Format format;
private FakeMediaPeriod mediaPeriod;
private boolean preparedSource;
private boolean releasedPeriod;
private boolean releasedSource;
public FakeMediaSource(Timeline timeline, Object manifest, Format format) {
Assertions.checkArgument(timeline.getPeriodCount() == 1);
this.timeline = timeline;
this.manifest = manifest;
this.format = format;
}
@Override
public void prepareSource(ExoPlayer player, boolean isTopLevelSource, Listener listener) {
assertFalse(preparedSource);
preparedSource = true;
listener.onSourceInfoRefreshed(timeline, manifest);
}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
assertTrue(preparedSource);
}
@Override
public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
assertTrue(preparedSource);
assertNull(mediaPeriod);
assertFalse(releasedPeriod);
assertFalse(releasedSource);
assertEquals(0, index);
assertEquals(0, positionUs);
mediaPeriod = new FakeMediaPeriod(format);
return mediaPeriod;
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
assertTrue(preparedSource);
assertNotNull(this.mediaPeriod);
assertFalse(releasedPeriod);
assertFalse(releasedSource);
assertEquals(this.mediaPeriod, mediaPeriod);
this.mediaPeriod.release();
releasedPeriod = true;
}
@Override
public void releaseSource() {
assertTrue(preparedSource);
assertNotNull(this.mediaPeriod);
assertTrue(releasedPeriod);
assertFalse(releasedSource);
releasedSource = true;
}
}
/**
* Fake {@link MediaPeriod} that provides one track with a given {@link Format}. Selecting that
* track will give the player a {@link FakeSampleStream}.
*/
private static final class FakeMediaPeriod implements MediaPeriod {
private final TrackGroup trackGroup;
private boolean preparedPeriod;
public FakeMediaPeriod(Format format) {
trackGroup = new TrackGroup(format);
}
public void release() {
preparedPeriod = false;
}
@Override
public void prepare(Callback callback) {
assertFalse(preparedPeriod);
preparedPeriod = true;
callback.onPrepared(this);
}
@Override
public void maybeThrowPrepareError() throws IOException {
assertTrue(preparedPeriod);
}
@Override
public TrackGroupArray getTrackGroups() {
assertTrue(preparedPeriod);
return new TrackGroupArray(trackGroup);
}
@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;
}
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;
}
return 0;
}
@Override
public long readDiscontinuity() {
assertTrue(preparedPeriod);
return C.TIME_UNSET;
}
@Override
public long getBufferedPositionUs() {
assertTrue(preparedPeriod);
return C.TIME_END_OF_SOURCE;
}
@Override
public long seekToUs(long positionUs) {
assertTrue(preparedPeriod);
assertEquals(0, positionUs);
return positionUs;
}
@Override
public long getNextLoadPositionUs() {
assertTrue(preparedPeriod);
return 0;
}
@Override
public boolean continueLoading(long positionUs) {
assertTrue(preparedPeriod);
return false;
}
}
/**
* Fake {@link SampleStream} that outputs a given {@link Format} then sets the end of stream flag
* on its input buffer.
*/
private static final class FakeSampleStream implements SampleStream {
private final Format format;
private boolean readFormat;
private boolean readEndOfStream;
public FakeSampleStream(Format format) {
this.format = format;
}
@Override
public boolean isReady() {
return true;
}
@Override
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) {
Assertions.checkState(!readEndOfStream);
if (readFormat) {
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
public void maybeThrowError() throws IOException {
// Do nothing.
}
@Override
public void skipToKeyframeBefore(long timeUs) {
// Do nothing.
}
}
/**
* 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.
*/
private static final class FakeVideoRenderer extends BaseRenderer {
private final Format expectedFormat;
private boolean isEnded;
public FakeVideoRenderer(Format expectedFormat) {
super(C.TRACK_TYPE_VIDEO);
Assertions.checkArgument(MimeTypes.isVideo(expectedFormat.sampleMimeType));
this.expectedFormat = expectedFormat;
}
@Override
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
if (isEnded) {
return;
}
// 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;
}
@Override
public boolean isReady() {
return isEnded;
}
@Override
public boolean isEnded() {
return isEnded;
}
@Override
public int supportsFormat(Format format) throws ExoPlaybackException {
return MimeTypes.isVideo(format.sampleMimeType) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE;
}
}
}

View file

@ -24,6 +24,8 @@ import android.annotation.TargetApi;
import android.media.MediaFormat;
import android.os.Parcel;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
@ -56,11 +58,14 @@ public final class FormatTest extends TestCase {
TestUtil.buildTestData(128, 1 /* data seed */));
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"));
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,
C.ENCODING_PCM_24BIT, 1001, 1002, 0, "und", Format.OFFSET_SAMPLE_RELATIVE, INIT_DATA,
drmInitData);
C.ENCODING_PCM_24BIT, 1001, 1002, 0, "und", Format.NO_VALUE, Format.OFFSET_SAMPLE_RELATIVE,
INIT_DATA, drmInitData, metadata);
Parcel parcel = Parcel.obtain();
formatToParcel.writeToParcel(parcel, 0);

View file

@ -27,9 +27,9 @@ import junit.framework.TestCase;
*/
public final class DefaultOggSeekerTest extends TestCase {
public void testSetupUnboundAudioLength() {
public void testSetupWithUnsetEndPositionFails() {
try {
new DefaultOggSeeker(0, C.LENGTH_UNSET, new TestStreamReader());
new DefaultOggSeeker(0, C.LENGTH_UNSET, new TestStreamReader(), 1, 1);
fail();
} catch (IllegalArgumentException e) {
// ignored
@ -43,11 +43,12 @@ public final class DefaultOggSeekerTest extends TestCase {
}
}
public void testSeeking(Random random) throws IOException, InterruptedException {
private void testSeeking(Random random) throws IOException, InterruptedException {
OggTestFile testFile = OggTestFile.generate(random, 1000);
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(testFile.data).build();
TestStreamReader streamReader = new TestStreamReader();
DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, testFile.data.length, streamReader);
DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, testFile.data.length, streamReader,
testFile.firstPayloadPageSize, testFile.firstPayloadPageGranulePosition);
OggPageHeader pageHeader = new OggPageHeader();
while (true) {
@ -109,8 +110,8 @@ public final class DefaultOggSeekerTest extends TestCase {
long granuleDiff = currentGranule - targetGranule;
if ((granuleDiff > DefaultOggSeeker.MATCH_RANGE || granuleDiff < 0)
&& positionDiff > DefaultOggSeeker.MATCH_BYTE_RANGE) {
fail(String.format("granuleDiff (%d) or positionDiff (%d) is more than allowed.",
granuleDiff, positionDiff));
fail("granuleDiff (" + granuleDiff + ") or positionDiff (" + positionDiff
+ ") is more than allowed.");
}
}
}

View file

@ -28,8 +28,8 @@ import junit.framework.TestCase;
*/
public class DefaultOggSeekerUtilMethodsTest extends TestCase {
private Random random = new Random(0);
private final Random random = new Random(0);
public void testSkipToNextPage() throws Exception {
FakeExtractorInput extractorInput = TestData.createInput(
TestUtil.joinByteArrays(
@ -75,7 +75,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
private static void skipToNextPage(ExtractorInput extractorInput)
throws IOException, InterruptedException {
DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, extractorInput.getLength(),
new FlacReader());
new FlacReader(), 1, 2);
while (true) {
try {
oggSeeker.skipToNextPage(extractorInput);
@ -143,7 +143,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
private void skipToPageOfGranule(ExtractorInput input, long granule,
long elapsedSamplesExpected) throws IOException, InterruptedException {
DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, input.getLength(), new FlacReader());
DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, input.getLength(), new FlacReader(), 1, 2);
while (true) {
try {
assertEquals(elapsedSamplesExpected, oggSeeker.skipToPageOfGranule(input, granule, -1));
@ -193,7 +193,7 @@ public class DefaultOggSeekerUtilMethodsTest extends TestCase {
private void assertReadGranuleOfLastPage(FakeExtractorInput input, int expected)
throws IOException, InterruptedException {
DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, input.getLength(), new FlacReader());
DefaultOggSeeker oggSeeker = new DefaultOggSeeker(0, input.getLength(), new FlacReader(), 1, 2);
while (true) {
try {
assertEquals(expected, oggSeeker.readGranuleOfLastPage(input));

View file

@ -25,20 +25,25 @@ import junit.framework.Assert;
*/
/* package */ final class OggTestFile {
public static final int MAX_PACKET_LENGTH = 2048;
public static final int MAX_SEGMENT_COUNT = 10;
public static final int MAX_GRANULES_IN_PAGE = 100000;
private static final int MAX_PACKET_LENGTH = 2048;
private static final int MAX_SEGMENT_COUNT = 10;
private static final int MAX_GRANULES_IN_PAGE = 100000;
byte[] data;
long lastGranule;
int packetCount;
int pageCount;
public final byte[] data;
public final long lastGranule;
public final int packetCount;
public final int pageCount;
public final int firstPayloadPageSize;
public final long firstPayloadPageGranulePosition;
private OggTestFile(byte[] data, long lastGranule, int packetCount, int pageCount) {
private OggTestFile(byte[] data, long lastGranule, int packetCount, int pageCount,
int firstPayloadPageSize, long firstPayloadPageGranulePosition) {
this.data = data;
this.lastGranule = lastGranule;
this.packetCount = packetCount;
this.pageCount = pageCount;
this.firstPayloadPageSize = firstPayloadPageSize;
this.firstPayloadPageGranulePosition = firstPayloadPageGranulePosition;
}
public static OggTestFile generate(Random random, int pageCount) {
@ -47,6 +52,8 @@ import junit.framework.Assert;
long granule = 0;
int packetLength = -1;
int packetCount = 0;
int firstPayloadPageSize = 0;
long firstPayloadPageGranulePosition = 0;
for (int i = 0; i < pageCount; i++) {
int headerType = 0x00;
@ -89,6 +96,10 @@ import junit.framework.Assert;
byte[] payload = TestUtil.buildTestData(bodySize, random);
fileData.add(payload);
fileSize += payload.length;
if (i == 0) {
firstPayloadPageSize = header.length + bodySize;
firstPayloadPageGranulePosition = granule;
}
}
byte[] file = new byte[fileSize];
@ -97,7 +108,8 @@ import junit.framework.Assert;
System.arraycopy(data, 0, file, position, data.length);
position += data.length;
}
return new OggTestFile(file, granule, packetCount, pageCount);
return new OggTestFile(file, granule, packetCount, pageCount, firstPayloadPageSize,
firstPayloadPageGranulePosition);
}
public int findPreviousPageStart(long position) {

View file

@ -17,8 +17,10 @@ package com.google.android.exoplayer2.extractor.rawcc;
import android.annotation.TargetApi;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.MimeTypes;
/**
* Tests for {@link RawCcExtractor}.
@ -27,12 +29,15 @@ import com.google.android.exoplayer2.testutil.TestUtil;
public final class RawCcExtractorTest extends InstrumentationTestCase {
public void testRawCcSample() throws Exception {
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
@Override
public Extractor create() {
return new RawCcExtractor();
}
}, "rawcc/sample.rawcc", getInstrumentation());
TestUtil.assertOutput(
new TestUtil.ExtractorFactory() {
@Override
public Extractor create() {
return new RawCcExtractor(
Format.createTextContainerFormat(null, null, MimeTypes.APPLICATION_CEA608,
"cea608", Format.NO_VALUE, 0, null, 1));
}
}, "rawcc/sample.rawcc", getInstrumentation());
}
}

View file

@ -16,7 +16,7 @@
package com.google.android.exoplayer2.extractor.ts;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.TrackIdGenerator;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
import com.google.android.exoplayer2.testutil.TestUtil;
@ -52,7 +52,7 @@ public class AdtsReaderTest extends TestCase {
public static final byte[] ADTS_CONTENT = TestUtil.createByteArray(
0x20, 0x00, 0x20, 0x00, 0x00, 0x80, 0x0e);
private static final byte TEST_DATA[] = TestUtil.joinByteArrays(
private static final byte[] TEST_DATA = TestUtil.joinByteArrays(
ID3_DATA_1,
ID3_DATA_2,
ADTS_HEADER,
@ -73,7 +73,7 @@ public class AdtsReaderTest extends TestCase {
id3Output = fakeExtractorOutput.track(1);
adtsReader = new AdtsReader(true);
TrackIdGenerator idGenerator = new TrackIdGenerator(0, 1);
adtsReader.init(fakeExtractorOutput, idGenerator);
adtsReader.createTracks(fakeExtractorOutput, idGenerator);
data = new ParsableByteArray(TEST_DATA);
firstFeed = true;
}

View file

@ -0,0 +1,195 @@
/*
* 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.extractor.ts;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import junit.framework.TestCase;
/**
* Test for {@link SectionReader}.
*/
public class SectionReaderTest extends TestCase {
private byte[] packetPayload;
private CustomSectionPayloadReader payloadReader;
private SectionReader reader;
@Override
public void setUp() {
packetPayload = new byte[512];
Arrays.fill(packetPayload, (byte) 0xFF);
payloadReader = new CustomSectionPayloadReader();
reader = new SectionReader(payloadReader);
reader.init(new TimestampAdjuster(0), new FakeExtractorOutput(),
new TsPayloadReader.TrackIdGenerator(0, 1));
}
public void testSingleOnePacketSection() {
packetPayload[0] = 3;
insertTableSection(4, (byte) 99, 3);
reader.consume(new ParsableByteArray(packetPayload), true);
assertEquals(Collections.singletonList(99), payloadReader.parsedTableIds);
}
public void testHeaderSplitAcrossPackets() {
packetPayload[0] = 3; // The first packet includes a pointer_field.
insertTableSection(4, (byte) 100, 3); // This section header spreads across both packets.
ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 5);
reader.consume(firstPacket, true);
assertEquals(Collections.emptyList(), payloadReader.parsedTableIds);
ParsableByteArray secondPacket = new ParsableByteArray(packetPayload);
secondPacket.setPosition(5);
reader.consume(secondPacket, false);
assertEquals(Collections.singletonList(100), payloadReader.parsedTableIds);
}
public void testFiveSectionsInTwoPackets() {
packetPayload[0] = 0; // The first packet includes a pointer_field.
insertTableSection(1, (byte) 101, 10);
insertTableSection(14, (byte) 102, 10);
insertTableSection(27, (byte) 103, 10);
packetPayload[40] = 0; // The second packet includes a pointer_field.
insertTableSection(41, (byte) 104, 10);
insertTableSection(54, (byte) 105, 10);
ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 40);
reader.consume(firstPacket, true);
assertEquals(Arrays.asList(101, 102, 103), payloadReader.parsedTableIds);
ParsableByteArray secondPacket = new ParsableByteArray(packetPayload);
secondPacket.setPosition(40);
reader.consume(secondPacket, true);
assertEquals(Arrays.asList(101, 102, 103, 104, 105), payloadReader.parsedTableIds);
}
public void testLongSectionAcrossFourPackets() {
packetPayload[0] = 13; // The first packet includes a pointer_field.
insertTableSection(1, (byte) 106, 10); // First section. Should be skipped.
// Second section spread across four packets. Should be consumed.
insertTableSection(14, (byte) 107, 300);
packetPayload[300] = 17; // The third packet includes a pointer_field.
// Third section, at the payload start of the fourth packet. Should be consumed.
insertTableSection(318, (byte) 108, 10);
ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 100);
reader.consume(firstPacket, true);
assertEquals(Collections.emptyList(), payloadReader.parsedTableIds);
ParsableByteArray secondPacket = new ParsableByteArray(packetPayload, 200);
secondPacket.setPosition(100);
reader.consume(secondPacket, false);
assertEquals(Collections.emptyList(), payloadReader.parsedTableIds);
ParsableByteArray thirdPacket = new ParsableByteArray(packetPayload, 300);
thirdPacket.setPosition(200);
reader.consume(thirdPacket, false);
assertEquals(Collections.emptyList(), payloadReader.parsedTableIds);
ParsableByteArray fourthPacket = new ParsableByteArray(packetPayload);
fourthPacket.setPosition(300);
reader.consume(fourthPacket, true);
assertEquals(Arrays.asList(107, 108), payloadReader.parsedTableIds);
}
public void testSeek() {
packetPayload[0] = 13; // The first packet includes a pointer_field.
insertTableSection(1, (byte) 109, 10); // First section. Should be skipped.
// Second section spread across four packets. Should be consumed.
insertTableSection(14, (byte) 110, 300);
packetPayload[300] = 17; // The third packet includes a pointer_field.
// Third section, at the payload start of the fourth packet. Should be consumed.
insertTableSection(318, (byte) 111, 10);
ParsableByteArray firstPacket = new ParsableByteArray(packetPayload, 100);
reader.consume(firstPacket, true);
assertEquals(Collections.emptyList(), payloadReader.parsedTableIds);
ParsableByteArray secondPacket = new ParsableByteArray(packetPayload, 200);
secondPacket.setPosition(100);
reader.consume(secondPacket, false);
assertEquals(Collections.emptyList(), payloadReader.parsedTableIds);
ParsableByteArray thirdPacket = new ParsableByteArray(packetPayload, 300);
thirdPacket.setPosition(200);
reader.consume(thirdPacket, false);
assertEquals(Collections.emptyList(), payloadReader.parsedTableIds);
reader.seek();
ParsableByteArray fourthPacket = new ParsableByteArray(packetPayload);
fourthPacket.setPosition(300);
reader.consume(fourthPacket, true);
assertEquals(Collections.singletonList(111), payloadReader.parsedTableIds);
}
public void testCrcChecks() {
byte[] correctCrcPat = new byte[] {
(byte) 0x0, (byte) 0x0, (byte) 0xb0, (byte) 0xd, (byte) 0x0, (byte) 0x1, (byte) 0xc1,
(byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x1, (byte) 0xe1, (byte) 0x0, (byte) 0xe8,
(byte) 0xf9, (byte) 0x5e, (byte) 0x7d};
byte[] incorrectCrcPat = Arrays.copyOf(correctCrcPat, correctCrcPat.length);
// Crc field is incorrect, and should not be passed to the payload reader.
incorrectCrcPat[16]--;
reader.consume(new ParsableByteArray(correctCrcPat), true);
assertEquals(Collections.singletonList(0), payloadReader.parsedTableIds);
reader.consume(new ParsableByteArray(incorrectCrcPat), true);
assertEquals(Collections.singletonList(0), payloadReader.parsedTableIds);
}
// Internal methods.
/**
* Inserts a private section header to {@link #packetPayload}.
*
* @param offset The position at which the header is inserted.
* @param tableId The table_id for the inserted section.
* @param sectionLength The value to use for private_section_length.
*/
private void insertTableSection(int offset, byte tableId, int sectionLength) {
packetPayload[offset++] = tableId;
packetPayload[offset++] = (byte) ((sectionLength >> 8) & 0x0F);
packetPayload[offset] = (byte) (sectionLength & 0xFF);
}
// Internal classes.
private static final class CustomSectionPayloadReader implements SectionPayloadReader {
List<Integer> parsedTableIds;
@Override
public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
TsPayloadReader.TrackIdGenerator idGenerator) {
parsedTableIds = new ArrayList<>();
}
@Override
public void consume(ParsableByteArray sectionData) {
parsedTableIds.add(sectionData.readUnsignedByte());
}
}
}

View file

@ -16,20 +16,21 @@
package com.google.android.exoplayer2.extractor.ts;
import android.test.InstrumentationTestCase;
import android.util.SparseArray;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.ts.ElementaryStreamReader.EsInfo;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Random;
/**
@ -72,7 +73,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
}
public void testCustomPesReader() throws Exception {
CustomEsReaderFactory factory = new CustomEsReaderFactory();
CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(true, false);
TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory, false);
FakeExtractorInput input = new FakeExtractorInput.Builder()
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts"))
@ -81,13 +82,12 @@ public final class TsExtractorTest extends InstrumentationTestCase {
.setSimulatePartialReads(false).build();
FakeExtractorOutput output = new FakeExtractorOutput();
tsExtractor.init(output);
tsExtractor.seek(input.getPosition());
PositionHolder seekPositionHolder = new PositionHolder();
int readResult = Extractor.RESULT_CONTINUE;
while (readResult != Extractor.RESULT_END_OF_INPUT) {
readResult = tsExtractor.read(input, seekPositionHolder);
}
CustomEsReader reader = factory.reader;
CustomEsReader reader = factory.esReader;
assertEquals(2, reader.packetsRead);
TrackOutput trackOutput = reader.getTrackOutput();
assertTrue(trackOutput == output.trackOutputs.get(257 /* PID of audio track. */));
@ -96,7 +96,24 @@ public final class TsExtractorTest extends InstrumentationTestCase {
((FakeTrackOutput) trackOutput).format);
}
private static void writeJunkData(ByteArrayOutputStream out, int length) throws IOException {
public void testCustomInitialSectionReader() throws Exception {
CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(false, true);
TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory, false);
FakeExtractorInput input = new FakeExtractorInput.Builder()
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample_with_sdt.ts"))
.setSimulateIOErrors(false)
.setSimulateUnknownLength(false)
.setSimulatePartialReads(false).build();
tsExtractor.init(new FakeExtractorOutput());
PositionHolder seekPositionHolder = new PositionHolder();
int readResult = Extractor.RESULT_CONTINUE;
while (readResult != Extractor.RESULT_END_OF_INPUT) {
readResult = tsExtractor.read(input, seekPositionHolder);
}
assertEquals(1, factory.sdtReader.consumedSdts);
}
private static void writeJunkData(ByteArrayOutputStream out, int length) {
for (int i = 0; i < length; i++) {
if (((byte) i) == TS_SYNC_BYTE) {
out.write(0);
@ -106,7 +123,46 @@ public final class TsExtractorTest extends InstrumentationTestCase {
}
}
private static final class CustomEsReader extends ElementaryStreamReader {
private static final class CustomTsPayloadReaderFactory implements TsPayloadReader.Factory {
private final boolean provideSdtReader;
private final boolean provideCustomEsReader;
private final TsPayloadReader.Factory defaultFactory;
private CustomEsReader esReader;
private SdtSectionReader sdtReader;
public CustomTsPayloadReaderFactory(boolean provideCustomEsReader, boolean provideSdtReader) {
this.provideCustomEsReader = provideCustomEsReader;
this.provideSdtReader = provideSdtReader;
defaultFactory = new DefaultTsPayloadReaderFactory();
}
@Override
public SparseArray<TsPayloadReader> createInitialPayloadReaders() {
if (provideSdtReader) {
assertNull(sdtReader);
SparseArray<TsPayloadReader> mapping = new SparseArray<>();
sdtReader = new SdtSectionReader();
mapping.put(17, new SectionReader(sdtReader));
return mapping;
} else {
return defaultFactory.createInitialPayloadReaders();
}
}
@Override
public TsPayloadReader createPayloadReader(int streamType, EsInfo esInfo) {
if (provideCustomEsReader && streamType == 3) {
esReader = new CustomEsReader(esInfo.language);
return new PesReader(esReader);
} else {
return defaultFactory.createPayloadReader(streamType, esInfo);
}
}
}
private static final class CustomEsReader implements ElementaryStreamReader {
private final String language;
private TrackOutput output;
@ -121,7 +177,7 @@ public final class TsExtractorTest extends InstrumentationTestCase {
}
@Override
public void init(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
output = extractorOutput.track(idGenerator.getNextId());
output.format(Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0,
language, null, 0));
@ -146,23 +202,44 @@ public final class TsExtractorTest extends InstrumentationTestCase {
}
private static final class CustomEsReaderFactory implements ElementaryStreamReader.Factory {
private static final class SdtSectionReader implements SectionPayloadReader {
private final ElementaryStreamReader.Factory defaultFactory;
private CustomEsReader reader;
private int consumedSdts;
public CustomEsReaderFactory() {
defaultFactory = new DefaultStreamReaderFactory();
@Override
public void init(TimestampAdjuster timestampAdjuster, ExtractorOutput extractorOutput,
TrackIdGenerator idGenerator) {
// Do nothing.
}
@Override
public ElementaryStreamReader createStreamReader(int streamType, EsInfo esInfo) {
if (streamType == 3) {
reader = new CustomEsReader(esInfo.language);
return reader;
} else {
return defaultFactory.createStreamReader(streamType, esInfo);
public void consume(ParsableByteArray sectionData) {
// table_id(8), section_syntax_indicator(1), reserved_future_use(1), reserved(2),
// section_length(12), transport_stream_id(16), reserved(2), version_number(5),
// current_next_indicator(1), section_number(8), last_section_number(8),
// original_network_id(16), reserved_future_use(8)
sectionData.skipBytes(11);
// Start of the service loop.
assertEquals(0x5566 /* arbitrary service id */, sectionData.readUnsignedShort());
// reserved_future_use(6), EIT_schedule_flag(1), EIT_present_following_flag(1)
sectionData.skipBytes(1);
// Assert there is only one service.
// Remove running_status(3), free_CA_mode(1) from the descriptors_loop_length with the mask.
assertEquals(sectionData.readUnsignedShort() & 0xFFF, sectionData.bytesLeft());
while (sectionData.bytesLeft() > 0) {
int descriptorTag = sectionData.readUnsignedByte();
int descriptorLength = sectionData.readUnsignedByte();
if (descriptorTag == 72 /* service descriptor */) {
assertEquals(1, sectionData.readUnsignedByte()); // Service type: Digital TV.
int serviceProviderNameLength = sectionData.readUnsignedByte();
assertEquals("Some provider", sectionData.readString(serviceProviderNameLength));
int serviceNameLength = sectionData.readUnsignedByte();
assertEquals("Some Channel", sectionData.readString(serviceNameLength));
} else {
sectionData.skipBytes(descriptorLength);
}
}
consumedSdts++;
}
}

View file

@ -16,8 +16,8 @@
package com.google.android.exoplayer2.metadata.id3;
import android.test.MoreAsserts;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoderException;
import java.util.List;
import junit.framework.TestCase;
/**
@ -30,9 +30,9 @@ public class Id3DecoderTest extends TestCase {
3, 0, 109, 100, 105, 97, 108, 111, 103, 95, 86, 73, 78, 68, 73, 67, 79, 49, 53, 50, 55, 54,
54, 52, 95, 115, 116, 97, 114, 116, 0};
Id3Decoder decoder = new Id3Decoder();
List<Id3Frame> id3Frames = decoder.decode(rawId3, rawId3.length);
assertEquals(1, id3Frames.size());
TxxxFrame txxxFrame = (TxxxFrame) id3Frames.get(0);
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);
}
@ -42,9 +42,9 @@ public class Id3DecoderTest extends TestCase {
3, 105, 109, 97, 103, 101, 47, 106, 112, 101, 103, 0, 16, 72, 101, 108, 108, 111, 32, 87,
111, 114, 108, 100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
Id3Decoder decoder = new Id3Decoder();
List<Id3Frame> id3Frames = decoder.decode(rawId3, rawId3.length);
assertEquals(1, id3Frames.size());
ApicFrame apicFrame = (ApicFrame) id3Frames.get(0);
Metadata metadata = decoder.decode(rawId3, rawId3.length);
assertEquals(1, metadata.length());
ApicFrame apicFrame = (ApicFrame) metadata.get(0);
assertEquals("image/jpeg", apicFrame.mimeType);
assertEquals(16, apicFrame.pictureType);
assertEquals("Hello World", apicFrame.description);
@ -56,9 +56,9 @@ public class Id3DecoderTest extends TestCase {
byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 23, 84, 73, 84, 50, 0, 0, 0, 13, 0, 0,
3, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0};
Id3Decoder decoder = new Id3Decoder();
List<Id3Frame> id3Frames = decoder.decode(rawId3, rawId3.length);
assertEquals(1, id3Frames.size());
TextInformationFrame textInformationFrame = (TextInformationFrame) id3Frames.get(0);
Metadata metadata = decoder.decode(rawId3, rawId3.length);
assertEquals(1, metadata.length());
TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0);
assertEquals("TIT2", textInformationFrame.id);
assertEquals("Hello World", textInformationFrame.description);
}

View file

@ -0,0 +1,143 @@
/*
* 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.source;
import static org.mockito.Mockito.doAnswer;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.Timeline.Window;
import com.google.android.exoplayer2.source.MediaSource.Listener;
import com.google.android.exoplayer2.testutil.TestUtil;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
/**
* Unit tests for {@link ClippingMediaSource}.
*/
public final class ClippingMediaSourceTest extends InstrumentationTestCase {
private static final long TEST_PERIOD_DURATION_US = 1000000;
private static final long TEST_CLIP_AMOUNT_US = 300000;
@Mock
private MediaSource mockMediaSource;
private Timeline clippedTimeline;
private Window window;
private Period period;
@Override
protected void setUp() throws Exception {
TestUtil.setUpMockito(this);
window = new Timeline.Window();
period = new Timeline.Period();
}
public void testNoClipping() {
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true);
Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US);
assertEquals(1, clippedTimeline.getWindowCount());
assertEquals(1, clippedTimeline.getPeriodCount());
assertEquals(TEST_PERIOD_DURATION_US, clippedTimeline.getWindow(0, window).getDurationUs());
assertEquals(TEST_PERIOD_DURATION_US, clippedTimeline.getPeriod(0, period).getDurationUs());
}
public void testClippingUnseekableWindowThrows() {
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false);
// If the unseekable window isn't clipped, clipping succeeds.
getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US);
try {
// If the unseekable window is clipped, clipping fails.
getClippedTimeline(timeline, 1, TEST_PERIOD_DURATION_US);
fail("Expected clipping to fail.");
} catch (IllegalArgumentException e) {
// Expected.
}
}
public void testClippingStart() {
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true);
Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US,
TEST_PERIOD_DURATION_US);
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
clippedTimeline.getWindow(0, window).getDurationUs());
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
clippedTimeline.getPeriod(0, period).getDurationUs());
}
public void testClippingEnd() {
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true);
Timeline clippedTimeline = getClippedTimeline(timeline, 0,
TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
clippedTimeline.getWindow(0, window).getDurationUs());
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
clippedTimeline.getPeriod(0, period).getDurationUs());
}
public void testClippingStartAndEnd() {
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true);
Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US,
TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2);
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3,
clippedTimeline.getWindow(0, window).getDurationUs());
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3,
clippedTimeline.getPeriod(0, period).getDurationUs());
}
/**
* Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline.
*/
private Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) {
mockMediaSourceSourceWithTimeline(timeline);
new ClippingMediaSource(mockMediaSource, startMs, endMs).prepareSource(null, true,
new Listener() {
@Override
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
clippedTimeline = timeline;
}
});
return clippedTimeline;
}
/**
* Returns a mock {@link MediaSource} with the specified {@link Timeline} in its source info.
*/
private MediaSource mockMediaSourceSourceWithTimeline(final Timeline timeline) {
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
MediaSource.Listener listener = (MediaSource.Listener) invocation.getArguments()[2];
listener.onSourceInfoRefreshed(timeline, null);
return null;
}
}).when(mockMediaSource).prepareSource(Mockito.any(ExoPlayer.class), Mockito.anyBoolean(),
Mockito.any(MediaSource.Listener.class));
return mockMediaSource;
}
}

View file

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.dash.manifest;
import android.net.Uri;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.IOException;
@ -28,6 +29,8 @@ public class DashManifestParserTest extends InstrumentationTestCase {
private static final String SAMPLE_MPD_1 = "dash/sample_mpd_1";
private static final String SAMPLE_MPD_2_UNKNOWN_MIME_TYPE =
"dash/sample_mpd_2_unknown_mime_type";
private static final String SAMPLE_MPD_3_SEGMENT_TEMPLATE =
"dash/sample_mpd_3_segment_template";
/**
* Simple test to ensure the sample manifests parse without any exceptions being thrown.
@ -40,4 +43,61 @@ public class DashManifestParserTest extends InstrumentationTestCase {
TestUtil.getInputStream(getInstrumentation(), SAMPLE_MPD_2_UNKNOWN_MIME_TYPE));
}
public void testParseMediaPresentationDescriptionWithSegmentTemplate() throws IOException {
DashManifestParser parser = new DashManifestParser();
DashManifest mpd = parser.parse(Uri.parse("https://example.com/test.mpd"),
TestUtil.getInputStream(getInstrumentation(), SAMPLE_MPD_3_SEGMENT_TEMPLATE));
assertEquals(1, mpd.getPeriodCount());
Period period = mpd.getPeriod(0);
assertNotNull(period);
assertEquals(2, period.adaptationSets.size());
for (AdaptationSet adaptationSet : period.adaptationSets) {
assertNotNull(adaptationSet);
for (Representation representation : adaptationSet.representations) {
if (representation instanceof Representation.MultiSegmentRepresentation) {
Representation.MultiSegmentRepresentation multiSegmentRepresentation =
(Representation.MultiSegmentRepresentation) representation;
int firstSegmentIndex = multiSegmentRepresentation.getFirstSegmentNum();
RangedUri uri = multiSegmentRepresentation.getSegmentUrl(firstSegmentIndex);
assertTrue(uri.resolveUriString(representation.baseUrl).contains(
"redirector.googlevideo.com"));
}
}
}
}
public void testParseCea608AccessibilityChannel() {
assertEquals(1, DashManifestParser.parseCea608AccessibilityChannel("CC1=eng"));
assertEquals(2, DashManifestParser.parseCea608AccessibilityChannel("CC2=eng"));
assertEquals(3, DashManifestParser.parseCea608AccessibilityChannel("CC3=eng"));
assertEquals(4, DashManifestParser.parseCea608AccessibilityChannel("CC4=eng"));
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(null));
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(""));
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel("CC0=eng"));
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel("CC5=eng"));
assertEquals(Format.NO_VALUE,
DashManifestParser.parseCea608AccessibilityChannel("Wrong format"));
}
public void testParseCea708AccessibilityChannel() {
assertEquals(1, DashManifestParser.parseCea708AccessibilityChannel("1=lang:eng"));
assertEquals(2, DashManifestParser.parseCea708AccessibilityChannel("2=lang:eng"));
assertEquals(3, DashManifestParser.parseCea708AccessibilityChannel("3=lang:eng"));
assertEquals(62, DashManifestParser.parseCea708AccessibilityChannel("62=lang:eng"));
assertEquals(63, DashManifestParser.parseCea708AccessibilityChannel("63=lang:eng"));
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(null));
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(""));
assertEquals(Format.NO_VALUE,
DashManifestParser.parseCea708AccessibilityChannel("0=lang:eng"));
assertEquals(Format.NO_VALUE,
DashManifestParser.parseCea708AccessibilityChannel("64=lang:eng"));
assertEquals(Format.NO_VALUE,
DashManifestParser.parseCea708AccessibilityChannel("Wrong format"));
}
}

View file

@ -23,56 +23,64 @@ import junit.framework.TestCase;
*/
public class RangedUriTest extends TestCase {
private static final String FULL_URI = "http://www.test.com/path/file.ext";
private static final String BASE_URI = "http://www.test.com/";
private static final String PARTIAL_URI = "path/file.ext";
private static final String FULL_URI = BASE_URI + PARTIAL_URI;
public void testMerge() {
RangedUri rangeA = new RangedUri(null, FULL_URI, 0, 10);
RangedUri rangeB = new RangedUri(null, FULL_URI, 10, 10);
RangedUri expected = new RangedUri(null, FULL_URI, 0, 20);
assertMerge(rangeA, rangeB, expected);
RangedUri rangeA = new RangedUri(FULL_URI, 0, 10);
RangedUri rangeB = new RangedUri(FULL_URI, 10, 10);
RangedUri expected = new RangedUri(FULL_URI, 0, 20);
assertMerge(rangeA, rangeB, expected, null);
}
public void testMergeUnbounded() {
RangedUri rangeA = new RangedUri(null, FULL_URI, 0, 10);
RangedUri rangeB = new RangedUri(null, FULL_URI, 10, C.LENGTH_UNSET);
RangedUri expected = new RangedUri(null, FULL_URI, 0, C.LENGTH_UNSET);
assertMerge(rangeA, rangeB, expected);
RangedUri rangeA = new RangedUri(FULL_URI, 0, 10);
RangedUri rangeB = new RangedUri(FULL_URI, 10, C.LENGTH_UNSET);
RangedUri expected = new RangedUri(FULL_URI, 0, C.LENGTH_UNSET);
assertMerge(rangeA, rangeB, expected, null);
}
public void testNonMerge() {
// A and B do not overlap, so should not merge
RangedUri rangeA = new RangedUri(null, FULL_URI, 0, 10);
RangedUri rangeB = new RangedUri(null, FULL_URI, 11, 10);
assertNonMerge(rangeA, rangeB);
RangedUri rangeA = new RangedUri(FULL_URI, 0, 10);
RangedUri rangeB = new RangedUri(FULL_URI, 11, 10);
assertNonMerge(rangeA, rangeB, null);
// A and B do not overlap, so should not merge
rangeA = new RangedUri(null, FULL_URI, 0, 10);
rangeB = new RangedUri(null, FULL_URI, 11, C.LENGTH_UNSET);
assertNonMerge(rangeA, rangeB);
rangeA = new RangedUri(FULL_URI, 0, 10);
rangeB = new RangedUri(FULL_URI, 11, C.LENGTH_UNSET);
assertNonMerge(rangeA, rangeB, null);
// A and B are bounded but overlap, so should not merge
rangeA = new RangedUri(null, FULL_URI, 0, 11);
rangeB = new RangedUri(null, FULL_URI, 10, 10);
assertNonMerge(rangeA, rangeB);
rangeA = new RangedUri(FULL_URI, 0, 11);
rangeB = new RangedUri(FULL_URI, 10, 10);
assertNonMerge(rangeA, rangeB, null);
// A and B overlap due to unboundedness, so should not merge
rangeA = new RangedUri(null, FULL_URI, 0, C.LENGTH_UNSET);
rangeB = new RangedUri(null, FULL_URI, 10, C.LENGTH_UNSET);
assertNonMerge(rangeA, rangeB);
rangeA = new RangedUri(FULL_URI, 0, C.LENGTH_UNSET);
rangeB = new RangedUri(FULL_URI, 10, C.LENGTH_UNSET);
assertNonMerge(rangeA, rangeB, null);
}
private void assertMerge(RangedUri rangeA, RangedUri rangeB, RangedUri expected) {
RangedUri merged = rangeA.attemptMerge(rangeB);
public void testMergeWithBaseUri() {
RangedUri rangeA = new RangedUri(PARTIAL_URI, 0, 10);
RangedUri rangeB = new RangedUri(FULL_URI, 10, 10);
RangedUri expected = new RangedUri(FULL_URI, 0, 20);
assertMerge(rangeA, rangeB, expected, BASE_URI);
}
private void assertMerge(RangedUri rangeA, RangedUri rangeB, RangedUri expected, String baseUrl) {
RangedUri merged = rangeA.attemptMerge(rangeB, baseUrl);
assertEquals(expected, merged);
merged = rangeB.attemptMerge(rangeA);
merged = rangeB.attemptMerge(rangeA, baseUrl);
assertEquals(expected, merged);
}
private void assertNonMerge(RangedUri rangeA, RangedUri rangeB) {
RangedUri merged = rangeA.attemptMerge(rangeB);
private void assertNonMerge(RangedUri rangeA, RangedUri rangeB, String baseUrl) {
RangedUri merged = rangeA.attemptMerge(rangeB, baseUrl);
assertNull(merged);
merged = rangeB.attemptMerge(rangeA);
merged = rangeB.attemptMerge(rangeA, baseUrl);
assertNull(merged);
}

View file

@ -27,16 +27,17 @@ public class RepresentationTest extends TestCase {
public void testGetCacheKey() {
String uri = "http://www.google.com";
SegmentBase base = new SingleSegmentBase(new RangedUri(uri, null, 0, 1), 1, 0, uri, 1, 1);
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);
Representation representation = Representation.newInstance("test_stream_1", 3, format, base);
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);
representation = Representation.newInstance("test_stream_1", Representation.REVISION_ID_DEFAULT,
format, base);
format, uri, base);
assertEquals("test_stream_1.150.-1", representation.getCacheKey());
}

View file

@ -72,15 +72,14 @@ public class HlsMediaPlaylistParserTest extends TestCase {
HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
assertEquals(2679, mediaPlaylist.mediaSequence);
assertEquals(8, mediaPlaylist.targetDurationSecs);
assertEquals(3, mediaPlaylist.version);
assertEquals(false, mediaPlaylist.live);
assertEquals(true, mediaPlaylist.hasEndTag);
List<HlsMediaPlaylist.Segment> segments = mediaPlaylist.segments;
assertNotNull(segments);
assertEquals(5, segments.size());
assertEquals(4, segments.get(0).discontinuitySequenceNumber);
assertEquals(7.975, segments.get(0).durationSecs);
assertEquals(7975000, segments.get(0).durationUs);
assertEquals(false, segments.get(0).isEncrypted);
assertEquals(null, segments.get(0).encryptionKeyUri);
assertEquals(null, segments.get(0).encryptionIV);
@ -89,7 +88,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals("https://priv.example.com/fileSequence2679.ts", segments.get(0).url);
assertEquals(4, segments.get(1).discontinuitySequenceNumber);
assertEquals(7.975, segments.get(1).durationSecs);
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);
@ -98,7 +97,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals("https://priv.example.com/fileSequence2680.ts", segments.get(1).url);
assertEquals(4, segments.get(2).discontinuitySequenceNumber);
assertEquals(7.941, segments.get(2).durationSecs);
assertEquals(7941000, segments.get(2).durationUs);
assertEquals(false, segments.get(2).isEncrypted);
assertEquals(null, segments.get(2).encryptionKeyUri);
assertEquals(null, segments.get(2).encryptionIV);
@ -107,7 +106,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals("https://priv.example.com/fileSequence2681.ts", segments.get(2).url);
assertEquals(5, segments.get(3).discontinuitySequenceNumber);
assertEquals(7.975, segments.get(3).durationSecs);
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);
// 0xA7A == 2682.
@ -118,7 +117,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals("https://priv.example.com/fileSequence2682.ts", segments.get(3).url);
assertEquals(5, segments.get(4).discontinuitySequenceNumber);
assertEquals(7.975, segments.get(4).durationSecs);
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);
// 0xA7B == 2683.

View file

@ -30,6 +30,7 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
private static final String TYPICAL_EXTRA_BLANK_LINE = "subrip/typical_extra_blank_line";
private static final String TYPICAL_MISSING_TIMECODE = "subrip/typical_missing_timecode";
private static final String TYPICAL_MISSING_SEQUENCE = "subrip/typical_missing_sequence";
private static final String TYPICAL_NEGATIVE_TIMESTAMPS = "subrip/typical_negative_timestamps";
private static final String NO_END_TIMECODES_FILE = "subrip/no_end_timecodes";
public void testDecodeEmpty() throws IOException {
@ -91,6 +92,15 @@ public final class SubripDecoderTest extends InstrumentationTestCase {
assertTypicalCue3(subtitle, 2);
}
public void testDecodeTypicalNegativeTimestamps() throws IOException {
// Parsing should succeed, parsing the third cue only.
SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), TYPICAL_NEGATIVE_TIMESTAMPS);
SubripSubtitle subtitle = decoder.decode(bytes, bytes.length);
assertEquals(2, subtitle.getEventTimeCount());
assertTypicalCue3(subtitle, 0);
}
public void testDecodeNoEndTimecodes() throws IOException {
SubripDecoder decoder = new SubripDecoder();
byte[] bytes = TestUtil.getByteArray(getInstrumentation(), NO_END_TIMECODES_FILE);

View file

@ -97,7 +97,7 @@ public final class Mp4WebvttDecoderTest extends TestCase {
public void testNoCueSample() throws SubtitleDecoderException {
Mp4WebvttDecoder decoder = new Mp4WebvttDecoder();
Subtitle result = decoder.decode(NO_CUE_SAMPLE, NO_CUE_SAMPLE.length);
assertMp4WebvttSubtitleEquals(result, new Cue[0]);
assertMp4WebvttSubtitleEquals(result);
}
// Negative tests.

View file

@ -116,7 +116,7 @@ public class WebvttDecoderTest extends InstrumentationTestCase {
Alignment.ALIGN_CENTER, 0.45f, Cue.LINE_TYPE_FRACTION, Cue.ANCHOR_TYPE_END, Cue.DIMEN_UNSET,
Cue.TYPE_UNSET, 0.35f);
assertCue(subtitle, 6, 6000000, 7000000, "This is the fourth subtitle.",
Alignment.ALIGN_CENTER, -10f, Cue.LINE_TYPE_NUMBER, Cue.TYPE_UNSET, Cue.DIMEN_UNSET,
Alignment.ALIGN_CENTER, -11f, Cue.LINE_TYPE_NUMBER, Cue.TYPE_UNSET, Cue.DIMEN_UNSET,
Cue.TYPE_UNSET, Cue.DIMEN_UNSET);
assertCue(subtitle, 8, 7000000, 8000000, "This is the fifth subtitle.",
Alignment.ALIGN_OPPOSITE, Cue.DIMEN_UNSET, Cue.TYPE_UNSET, Cue.TYPE_UNSET, 0.1f,

View file

@ -0,0 +1,99 @@
/*
* 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;
import android.test.MoreAsserts;
import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.IOException;
import java.util.Arrays;
import junit.framework.TestCase;
/**
* Unit tests for {@link DataSourceInputStream}.
*/
public class DataSourceInputStreamTest extends TestCase {
private static final byte[] TEST_DATA = TestUtil.buildTestData(16);
public void testReadSingleBytes() throws IOException {
DataSourceInputStream inputStream = buildTestInputStream();
// No bytes read yet.
assertEquals(0, inputStream.bytesRead());
// Read bytes.
for (int i = 0; i < TEST_DATA.length; i++) {
int readByte = inputStream.read();
assertTrue(0 <= readByte && readByte < 256);
assertEquals(TEST_DATA[i] & 0xFF, readByte);
assertEquals(i + 1, inputStream.bytesRead());
}
// Check end of stream.
assertEquals(-1, inputStream.read());
assertEquals(TEST_DATA.length, inputStream.bytesRead());
// Check close succeeds.
inputStream.close();
}
public void testRead() throws IOException {
DataSourceInputStream inputStream = buildTestInputStream();
// Read bytes.
byte[] readBytes = new byte[TEST_DATA.length];
int totalBytesRead = 0;
while (totalBytesRead < TEST_DATA.length) {
long bytesRead = inputStream.read(readBytes, totalBytesRead,
TEST_DATA.length - totalBytesRead);
assertTrue(bytesRead > 0);
totalBytesRead += bytesRead;
assertEquals(totalBytesRead, inputStream.bytesRead());
}
// Check the read data.
MoreAsserts.assertEquals(TEST_DATA, readBytes);
// Check end of stream.
assertEquals(TEST_DATA.length, inputStream.bytesRead());
assertEquals(TEST_DATA.length, totalBytesRead);
assertEquals(-1, inputStream.read());
// Check close succeeds.
inputStream.close();
}
public void testSkip() throws IOException {
DataSourceInputStream inputStream = buildTestInputStream();
// Skip bytes.
long totalBytesSkipped = 0;
while (totalBytesSkipped < TEST_DATA.length) {
long bytesSkipped = inputStream.skip(Long.MAX_VALUE);
assertTrue(bytesSkipped > 0);
totalBytesSkipped += bytesSkipped;
assertEquals(totalBytesSkipped, inputStream.bytesRead());
}
// Check end of stream.
assertEquals(TEST_DATA.length, inputStream.bytesRead());
assertEquals(TEST_DATA.length, totalBytesSkipped);
assertEquals(-1, inputStream.read());
// Check close succeeds.
inputStream.close();
}
private static DataSourceInputStream buildTestInputStream() {
FakeDataSource.Builder fakeDataSourceBuilder = new FakeDataSource.Builder()
.appendReadData(Arrays.copyOfRange(TEST_DATA, 0, 5))
.appendReadData(Arrays.copyOfRange(TEST_DATA, 5, 10))
.appendReadData(Arrays.copyOfRange(TEST_DATA, 10, 15))
.appendReadData(Arrays.copyOfRange(TEST_DATA, 15, TEST_DATA.length));
return new DataSourceInputStream(fakeDataSourceBuilder.build(), new DataSpec(null));
}
}

View file

@ -23,7 +23,6 @@ import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.testutil.FakeDataSource.Builder;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DataSpec;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
@ -41,11 +40,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
@Override
protected void setUp() throws Exception {
// Create a temporary folder
cacheDir = File.createTempFile("CacheDataSourceTest", null);
assertTrue(cacheDir.delete());
assertTrue(cacheDir.mkdir());
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
}
@ -57,8 +52,12 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
public void testMaxCacheFileSize() throws Exception {
CacheDataSource cacheDataSource = createCacheDataSource(false, false);
assertReadDataContentLength(cacheDataSource, false, false);
assertEquals((int) Math.ceil((double) TEST_DATA.length / MAX_CACHE_FILE_SIZE),
cacheDir.listFiles().length);
File[] files = cacheDir.listFiles();
for (File file : files) {
if (!file.getName().equals(CachedContentIndex.FILE_NAME)) {
assertTrue(file.length() <= MAX_CACHE_FILE_SIZE);
}
}
}
public void testCacheAndRead() throws Exception {
@ -177,8 +176,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
builder.setSimulateUnknownLength(simulateUnknownLength);
builder.appendReadData(TEST_DATA);
FakeDataSource upstream = builder.build();
return new CacheDataSource(simpleCache, upstream,
CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_CACHE_UNBOUNDED_REQUESTS,
return new CacheDataSource(simpleCache, upstream, CacheDataSource.FLAG_BLOCK_ON_CACHE,
MAX_CACHE_FILE_SIZE);
}

View file

@ -1,79 +0,0 @@
/*
* 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 com.google.android.exoplayer2.testutil.TestUtil;
import java.io.File;
import java.util.Random;
import junit.framework.TestCase;
/**
* Unit tests for {@link CacheSpan}.
*/
public class CacheSpanTest extends TestCase {
public void testCacheFile() throws Exception {
assertCacheSpan(new File("parent"), "key", 0, 0);
assertCacheSpan(new File("parent/"), "key", 1, 2);
assertCacheSpan(new File("parent"), "<>:\"/\\|?*%", 1, 2);
assertCacheSpan(new File("/"), "key", 1, 2);
assertNullCacheSpan(new File("parent"), "", 1, 2);
assertNullCacheSpan(new File("parent"), "key", -1, 2);
assertNullCacheSpan(new File("parent"), "key", 1, -2);
assertNotNull(CacheSpan.createCacheEntry(new File("/asd%aa.1.2.v2.exo")));
assertNull(CacheSpan.createCacheEntry(new File("/asd%za.1.2.v2.exo")));
assertCacheSpan(new File("parent"),
"A newline (line feed) character \n"
+ "A carriage-return character followed immediately by a newline character \r\n"
+ "A standalone carriage-return character \r"
+ "A next-line character \u0085"
+ "A line-separator character \u2028"
+ "A paragraph-separator character \u2029", 1, 2);
}
public void testCacheFileNameRandomData() throws Exception {
Random random = new Random(0);
File parent = new File("parent");
for (int i = 0; i < 1000; i++) {
String key = TestUtil.buildTestString(1000, random);
long offset = Math.abs(random.nextLong());
long lastAccessTimestamp = Math.abs(random.nextLong());
assertCacheSpan(parent, key, offset, lastAccessTimestamp);
}
}
private void assertCacheSpan(File parent, String key, long offset, long lastAccessTimestamp) {
File cacheFile = CacheSpan.getCacheFileName(parent, key, offset, lastAccessTimestamp);
CacheSpan cacheSpan = CacheSpan.createCacheEntry(cacheFile);
String message = cacheFile.toString();
assertNotNull(message, cacheSpan);
assertEquals(message, parent, cacheFile.getParentFile());
assertEquals(message, key, cacheSpan.key);
assertEquals(message, offset, cacheSpan.position);
assertEquals(message, lastAccessTimestamp, cacheSpan.lastAccessTimestamp);
}
private void assertNullCacheSpan(File parent, String key, long offset,
long lastAccessTimestamp) {
File cacheFile = CacheSpan.getCacheFileName(parent, key, offset, lastAccessTimestamp);
CacheSpan cacheSpan = CacheSpan.createCacheEntry(cacheFile);
assertNull(cacheFile.toString(), cacheSpan);
}
}

View file

@ -0,0 +1,226 @@
package com.google.android.exoplayer2.upstream.cache;
import android.test.InstrumentationTestCase;
import android.test.MoreAsserts;
import android.util.SparseArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Set;
import junit.framework.AssertionFailedError;
/**
* Tests {@link CachedContentIndex}.
*/
public class CachedContentIndexTest extends InstrumentationTestCase {
private final byte[] testIndexV1File = {
0, 0, 0, 1, // version
0, 0, 0, 0, // flags
0, 0, 0, 2, // number_of_CachedContent
0, 0, 0, 5, // cache_id
0, 5, 65, 66, 67, 68, 69, // cache_key
0, 0, 0, 0, 0, 0, 0, 10, // original_content_length
0, 0, 0, 2, // cache_id
0, 5, 75, 76, 77, 78, 79, // cache_key
0, 0, 0, 0, 0, 0, 10, 0, // original_content_length
(byte) 0xF6, (byte) 0xFB, 0x50, 0x41 // hashcode_of_CachedContent_array
};
private CachedContentIndex index;
private File cacheDir;
@Override
public void setUp() throws Exception {
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
index = new CachedContentIndex(cacheDir);
}
@Override
protected void tearDown() throws Exception {
TestUtil.recursiveDelete(cacheDir);
}
public void testAddGetRemove() throws Exception {
final String key1 = "key1";
final String key2 = "key2";
final String key3 = "key3";
// Add two CachedContents with add methods
CachedContent cachedContent1 = new CachedContent(5, key1, 10);
index.addNew(cachedContent1);
CachedContent cachedContent2 = index.add(key2);
assertTrue(cachedContent1.id != cachedContent2.id);
// add a span
File cacheSpanFile = SimpleCacheSpanTest
.createCacheSpanFile(cacheDir, cachedContent1.id, 10, 20, 30);
SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(cacheSpanFile, index);
assertNotNull(span);
cachedContent1.addSpan(span);
// Check if they are added and get method returns null if the key isn't found
assertEquals(cachedContent1, index.get(key1));
assertEquals(cachedContent2, index.get(key2));
assertNull(index.get(key3));
// test getAll()
Collection<CachedContent> cachedContents = index.getAll();
assertEquals(2, cachedContents.size());
assertTrue(Arrays.asList(cachedContent1, cachedContent2).containsAll(cachedContents));
// test getKeys()
Set<String> keys = index.getKeys();
assertEquals(2, keys.size());
assertTrue(Arrays.asList(key1, key2).containsAll(keys));
// test getKeyForId()
assertEquals(key1, index.getKeyForId(cachedContent1.id));
assertEquals(key2, index.getKeyForId(cachedContent2.id));
// test remove()
index.removeEmpty(key2);
index.removeEmpty(key3);
assertEquals(cachedContent1, index.get(key1));
assertNull(index.get(key2));
assertTrue(cacheSpanFile.exists());
// test removeEmpty()
index.addNew(cachedContent2);
index.removeEmpty();
assertEquals(cachedContent1, index.get(key1));
assertNull(index.get(key2));
assertTrue(cacheSpanFile.exists());
}
public void testStoreAndLoad() throws Exception {
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir));
}
public void testLoadV1() throws Exception {
FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
fos.write(testIndexV1File);
fos.close();
index.load();
assertEquals(2, index.getAll().size());
assertEquals(5, index.assignIdForKey("ABCDE"));
assertEquals(10, index.getContentLength("ABCDE"));
assertEquals(2, index.assignIdForKey("KLMNO"));
assertEquals(2560, index.getContentLength("KLMNO"));
}
public void testStoreV1() throws Exception {
index.addNew(new CachedContent(2, "KLMNO", 2560));
index.addNew(new CachedContent(5, "ABCDE", 10));
index.store();
byte[] buffer = new byte[testIndexV1File.length];
FileInputStream fos = new FileInputStream(new File(cacheDir, CachedContentIndex.FILE_NAME));
assertEquals(testIndexV1File.length, fos.read(buffer));
assertEquals(-1, fos.read());
fos.close();
// TODO: The order of the CachedContent stored in index file isn't defined so this test may fail
// on a different implementation of the underlying set
MoreAsserts.assertEquals(testIndexV1File, buffer);
}
public void testAssignIdForKeyAndGetKeyForId() throws Exception {
final String key1 = "key1";
final String key2 = "key2";
int id1 = index.assignIdForKey(key1);
int id2 = index.assignIdForKey(key2);
assertEquals(key1, index.getKeyForId(id1));
assertEquals(key2, index.getKeyForId(id2));
assertTrue(id1 != id2);
assertEquals(id1, index.assignIdForKey(key1));
assertEquals(id2, index.assignIdForKey(key2));
}
public void testSetGetContentLength() throws Exception {
final String key1 = "key1";
assertEquals(C.LENGTH_UNSET, index.getContentLength(key1));
index.setContentLength(key1, 10);
assertEquals(10, index.getContentLength(key1));
}
public void testGetNewId() throws Exception {
SparseArray<String> idToKey = new SparseArray<>();
assertEquals(0, CachedContentIndex.getNewId(idToKey));
idToKey.put(10, "");
assertEquals(11, CachedContentIndex.getNewId(idToKey));
idToKey.put(Integer.MAX_VALUE, "");
assertEquals(0, CachedContentIndex.getNewId(idToKey));
idToKey.put(0, "");
assertEquals(1, CachedContentIndex.getNewId(idToKey));
}
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
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
new CachedContentIndex(cacheDir, key));
// Rename the index file from the test above
File file1 = new File(cacheDir, CachedContentIndex.FILE_NAME);
File file2 = new File(cacheDir, "file2compare");
assertTrue(file1.renameTo(file2));
// Write a new index file
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
new CachedContentIndex(cacheDir, key));
assertEquals(file2.length(), file1.length());
// Assert file content is different
FileInputStream fis1 = new FileInputStream(file1);
FileInputStream fis2 = new FileInputStream(file2);
for (int b; (b = fis1.read()) == fis2.read();) {
assertTrue(b != -1);
}
boolean threw = false;
try {
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
new CachedContentIndex(cacheDir, key2));
} catch (AssertionFailedError e) {
threw = true;
}
assertTrue("Encrypted index file can not be read with different encryption key", threw);
try {
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
new CachedContentIndex(cacheDir));
} catch (AssertionFailedError e) {
threw = true;
}
assertTrue("Encrypted index file can not be read without encryption key", threw);
// Non encrypted index file can be read even when encryption key provided.
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir),
new CachedContentIndex(cacheDir, key));
}
private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentIndex index2)
throws IOException {
index.addNew(new CachedContent(5, "key1", 10));
index.add("key2");
index.store();
index2.load();
Set<String> keys = index.getKeys();
Set<String> keys2 = index2.getKeys();
assertEquals(keys, keys2);
for (String key : keys) {
assertEquals(index.getContentLength(key), index2.getContentLength(key));
assertEquals(index.get(key).getSpans(), index2.get(key).getSpans());
}
}
}

View file

@ -0,0 +1,162 @@
/*
* 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.testutil.TestUtil;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Set;
import java.util.TreeSet;
/**
* Unit tests for {@link SimpleCacheSpan}.
*/
public class SimpleCacheSpanTest extends InstrumentationTestCase {
private CachedContentIndex index;
private File cacheDir;
public static File createCacheSpanFile(File cacheDir, int id, long offset, int length,
long lastAccessTimestamp) throws IOException {
File cacheFile = SimpleCacheSpan.getCacheFile(cacheDir, id, offset, lastAccessTimestamp);
createTestFile(cacheFile, length);
return cacheFile;
}
public static CacheSpan createCacheSpan(CachedContentIndex index, File cacheDir, String key,
long offset, int length, long lastAccessTimestamp) throws IOException {
int id = index.assignIdForKey(key);
File cacheFile = createCacheSpanFile(cacheDir, id, offset, length, lastAccessTimestamp);
return SimpleCacheSpan.createCacheEntry(cacheFile, index);
}
@Override
protected void setUp() throws Exception {
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
index = new CachedContentIndex(cacheDir);
}
@Override
protected void tearDown() throws Exception {
TestUtil.recursiveDelete(cacheDir);
}
public void testCacheFile() throws Exception {
assertCacheSpan("key1", 0, 0);
assertCacheSpan("key2", 1, 2);
assertCacheSpan("<>:\"/\\|?*%", 1, 2);
assertCacheSpan("key3", 1, 2);
assertNullCacheSpan(new File("parent"), "key4", -1, 2);
assertNullCacheSpan(new File("parent"), "key5", 1, -2);
assertCacheSpan(
"A newline (line feed) character \n"
+ "A carriage-return character followed immediately by a newline character \r\n"
+ "A standalone carriage-return character \r"
+ "A next-line character \u0085"
+ "A line-separator character \u2028"
+ "A paragraph-separator character \u2029", 1, 2);
}
public void testUpgradeFileName() throws Exception {
String key = "asd\u00aa";
int id = index.assignIdForKey(key);
File v3file = createTestFile(id + ".0.1.v3.exo");
File v2file = createTestFile("asd%aa.1.2.v2.exo");
File wrongEscapedV2file = createTestFile("asd%za.3.4.v2.exo");
File v1File = createTestFile("asd\u00aa.5.6.v1.exo");
for (File file : cacheDir.listFiles()) {
SimpleCacheSpan cacheEntry = SimpleCacheSpan.createCacheEntry(file, index);
if (file.equals(wrongEscapedV2file)) {
assertNull(cacheEntry);
} else {
assertNotNull(cacheEntry);
}
}
assertTrue(v3file.exists());
assertFalse(v2file.exists());
assertTrue(wrongEscapedV2file.exists());
assertFalse(v1File.exists());
File[] files = cacheDir.listFiles();
assertEquals(4, files.length);
Set<String> keys = index.getKeys();
assertEquals("There should be only one key for all files.", 1, keys.size());
assertTrue(keys.contains(key));
TreeSet<SimpleCacheSpan> spans = index.get(key).getSpans();
assertTrue("upgradeOldFiles() shouldn't add any spans.", spans.isEmpty());
HashMap<Long, Long> cachedPositions = new HashMap<>();
for (File file : files) {
SimpleCacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(file, index);
if (cacheSpan != null) {
assertEquals(key, cacheSpan.key);
cachedPositions.put(cacheSpan.position, cacheSpan.lastAccessTimestamp);
}
}
assertEquals(1, (long) cachedPositions.get((long) 0));
assertEquals(2, (long) cachedPositions.get((long) 1));
assertEquals(6, (long) cachedPositions.get((long) 5));
}
private static void createTestFile(File file, int length) throws IOException {
FileOutputStream output = new FileOutputStream(file);
for (int i = 0; i < length; i++) {
output.write(i);
}
output.close();
}
private File createTestFile(String name) throws IOException {
File file = new File(cacheDir, name);
createTestFile(file, 1);
return file;
}
private void assertCacheSpan(String key, long offset, long lastAccessTimestamp)
throws IOException {
int id = index.assignIdForKey(key);
File cacheFile = createCacheSpanFile(cacheDir, id, offset, 1, lastAccessTimestamp);
SimpleCacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(cacheFile, index);
String message = cacheFile.toString();
assertNotNull(message, cacheSpan);
assertEquals(message, cacheDir, cacheFile.getParentFile());
assertEquals(message, key, cacheSpan.key);
assertEquals(message, offset, cacheSpan.position);
assertEquals(message, 1, cacheSpan.length);
assertTrue(message, cacheSpan.isCached);
assertEquals(message, cacheFile, cacheSpan.file);
assertEquals(message, lastAccessTimestamp, cacheSpan.lastAccessTimestamp);
}
private void assertNullCacheSpan(File parent, String key, long offset,
long lastAccessTimestamp) {
File cacheFile = SimpleCacheSpan.getCacheFile(parent, index.assignIdForKey(key), offset,
lastAccessTimestamp);
CacheSpan cacheSpan = SimpleCacheSpan.createCacheEntry(cacheFile, index);
assertNull(cacheFile.toString(), cacheSpan);
}
}

View file

@ -16,7 +16,6 @@
package com.google.android.exoplayer2.upstream.cache;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.File;
@ -36,10 +35,7 @@ public class SimpleCacheTest extends InstrumentationTestCase {
@Override
protected void setUp() throws Exception {
// Create a temporary folder
cacheDir = File.createTempFile("SimpleCacheTest", null);
assertTrue(cacheDir.delete());
assertTrue(cacheDir.mkdir());
this.cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
}
@Override
@ -48,7 +44,7 @@ public class SimpleCacheTest extends InstrumentationTestCase {
}
public void testCommittingOneFile() throws Exception {
SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
SimpleCache simpleCache = getSimpleCache();
CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0);
assertFalse(cacheSpan.isCached);
@ -79,37 +75,40 @@ public class SimpleCacheTest extends InstrumentationTestCase {
}
public void testSetGetLength() throws Exception {
SimpleCache simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
SimpleCache simpleCache = getSimpleCache();
assertEquals(C.LENGTH_UNSET, simpleCache.getContentLength(KEY_1));
assertTrue(simpleCache.setContentLength(KEY_1, 15));
simpleCache.setContentLength(KEY_1, 15);
assertEquals(15, simpleCache.getContentLength(KEY_1));
simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, 0, 15);
assertTrue(simpleCache.setContentLength(KEY_1, 150));
simpleCache.setContentLength(KEY_1, 150);
assertEquals(150, simpleCache.getContentLength(KEY_1));
addCache(simpleCache, 140, 10);
// Try to set length shorter then the content
assertFalse(simpleCache.setContentLength(KEY_1, 15));
assertEquals("Content length should be unchanged.",
150, simpleCache.getContentLength(KEY_1));
/* TODO Enable when the length persistance is fixed
// Check if values are kept after cache is reloaded.
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
assertEquals(150, simpleCache.getContentLength(KEY_1));
CacheSpan lastSpan = simpleCache.startReadWrite(KEY_1, 145);
SimpleCache simpleCache2 = getSimpleCache();
Set<String> keys = simpleCache.getKeys();
Set<String> keys2 = simpleCache2.getKeys();
assertEquals(keys, keys2);
for (String key : keys) {
assertEquals(simpleCache.getContentLength(key), simpleCache2.getContentLength(key));
assertEquals(simpleCache.getCachedSpans(key), simpleCache2.getCachedSpans(key));
}
// Removing the last span shouldn't cause the length be change next time cache loaded
simpleCache.removeSpan(lastSpan);
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
assertEquals(150, simpleCache.getContentLength(KEY_1));
*/
SimpleCacheSpan lastSpan = simpleCache2.startReadWrite(KEY_1, 145);
simpleCache2.removeSpan(lastSpan);
simpleCache2 = getSimpleCache();
assertEquals(150, simpleCache2.getContentLength(KEY_1));
}
private SimpleCache getSimpleCache() {
return new SimpleCache(cacheDir, new NoOpCacheEvictor());
}
private void addCache(SimpleCache simpleCache, int position, int length) throws IOException {

View file

@ -0,0 +1,87 @@
/*
* 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.util;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Tests {@link AtomicFile}.
*/
public class AtomicFileTest extends InstrumentationTestCase {
private File tempFolder;
private File file;
private AtomicFile atomicFile;
@Override
public void setUp() throws Exception {
tempFolder = TestUtil.createTempFolder(getInstrumentation().getContext());
file = new File(tempFolder, "atomicFile");
atomicFile = new AtomicFile(file);
}
@Override
protected void tearDown() throws Exception {
TestUtil.recursiveDelete(tempFolder);
}
public void testDelete() throws Exception {
assertTrue(file.createNewFile());
atomicFile.delete();
assertFalse(file.exists());
}
public void testWriteRead() throws Exception {
OutputStream output = atomicFile.startWrite();
output.write(5);
atomicFile.endWrite(output);
output.close();
assertRead();
output = atomicFile.startWrite();
output.write(5);
output.write(6);
output.close();
assertRead();
output = atomicFile.startWrite();
output.write(6);
assertRead();
output.close();
output = atomicFile.startWrite();
assertRead();
output.close();
}
private void assertRead() throws IOException {
InputStream input = atomicFile.openRead();
assertEquals(5, input.read());
assertEquals(-1, input.read());
input.close();
}
}

View file

@ -0,0 +1,46 @@
/*
* 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.util;
import android.test.MoreAsserts;
import java.io.ByteArrayOutputStream;
import junit.framework.TestCase;
/**
* Tests {@link ReusableBufferedOutputStream}.
*/
public class ReusableBufferedOutputStreamTest extends TestCase {
private static final byte[] TEST_DATA_1 = "test data 1".getBytes();
private static final byte[] TEST_DATA_2 = "2 test data".getBytes();
public void testReset() throws Exception {
ByteArrayOutputStream byteArrayOutputStream1 = new ByteArrayOutputStream(1000);
ReusableBufferedOutputStream outputStream = new ReusableBufferedOutputStream(
byteArrayOutputStream1, 1000);
outputStream.write(TEST_DATA_1);
outputStream.close();
ByteArrayOutputStream byteArrayOutputStream2 = new ByteArrayOutputStream(1000);
outputStream.reset(byteArrayOutputStream2);
outputStream.write(TEST_DATA_2);
outputStream.close();
MoreAsserts.assertEquals(TEST_DATA_1, byteArrayOutputStream1.toByteArray());
MoreAsserts.assertEquals(TEST_DATA_2, byteArrayOutputStream2.toByteArray());
}
}

View file

@ -15,9 +15,7 @@
*/
package com.google.android.exoplayer2.util;
import android.test.MoreAsserts;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
@ -141,23 +139,11 @@ public class UtilTest extends TestCase {
assertEquals(1500L, Util.parseXsDuration("PT1.500S"));
}
public void testParseXsDateTime() throws ParseException {
public void testParseXsDateTime() throws Exception {
assertEquals(1403219262000L, Util.parseXsDateTime("2014-06-19T23:07:42"));
assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00Z"));
}
public void testGetHexStringByteArray() throws Exception {
assertHexStringByteArray("", new byte[] {});
assertHexStringByteArray("01", new byte[] {1});
assertHexStringByteArray("FF", new byte[] {(byte) 255});
assertHexStringByteArray("01020304", new byte[] {1, 2, 3, 4});
assertHexStringByteArray("0123456789ABCDEF",
new byte[] {1, 0x23, 0x45, 0x67, (byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF});
}
private void assertHexStringByteArray(String hex, byte[] array) {
assertEquals(hex, Util.getHexString(array));
MoreAsserts.assertEquals(array, Util.getBytesFromHexString(hex));
assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-08:00"));
assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-0800"));
}
public void testUnescapeInvalidFileName() {

View file

@ -70,8 +70,8 @@ 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(Format[] formats, SampleStream stream, long positionUs, boolean joining,
long offsetUs) throws ExoPlaybackException {
Assertions.checkState(state == STATE_DISABLED);
state = STATE_ENABLED;
onEnabled(joining);
@ -107,10 +107,15 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
}
@Override
public final void setCurrentStreamIsFinal() {
public final void setCurrentStreamFinal() {
streamIsFinal = true;
}
@Override
public final boolean isCurrentStreamFinal() {
return streamIsFinal;
}
@Override
public final void maybeThrowStreamError() throws IOException {
stream.maybeThrowError();
@ -119,6 +124,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
@Override
public final void resetPosition(long positionUs) throws ExoPlaybackException {
streamIsFinal = false;
readEndOfStream = false;
onPositionReset(positionUs, false);
}
@ -194,8 +200,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
* @param joining Whether this renderer is being enabled to join an ongoing playback.
* @throws ExoPlaybackException If an error occurs.
*/
protected void onPositionReset(long positionUs, boolean joining)
throws ExoPlaybackException {
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
// Do nothing.
}
@ -243,7 +248,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
/**
* Reads from the enabled upstream source. If the upstream source has been read to the end then
* {@link C#RESULT_BUFFER_READ} is only returned if {@link #setCurrentStreamIsFinal()} has been
* {@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)

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.MediaCodec;
import android.support.annotation.IntDef;
import android.view.Surface;
@ -159,6 +160,42 @@ public final class C {
public static final int CHANNEL_OUT_7POINT1_SURROUND = Util.SDK_INT < 23
? AudioFormat.CHANNEL_OUT_7POINT1 : AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
/**
* Stream types for an {@link android.media.AudioTrack}.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({STREAM_TYPE_ALARM, STREAM_TYPE_MUSIC, STREAM_TYPE_NOTIFICATION, STREAM_TYPE_RING,
STREAM_TYPE_SYSTEM, STREAM_TYPE_VOICE_CALL})
public @interface StreamType {}
/**
* @see AudioManager#STREAM_ALARM
*/
public static final int STREAM_TYPE_ALARM = AudioManager.STREAM_ALARM;
/**
* @see AudioManager#STREAM_MUSIC
*/
public static final int STREAM_TYPE_MUSIC = AudioManager.STREAM_MUSIC;
/**
* @see AudioManager#STREAM_NOTIFICATION
*/
public static final int STREAM_TYPE_NOTIFICATION = AudioManager.STREAM_NOTIFICATION;
/**
* @see AudioManager#STREAM_RING
*/
public static final int STREAM_TYPE_RING = AudioManager.STREAM_RING;
/**
* @see AudioManager#STREAM_SYSTEM
*/
public static final int STREAM_TYPE_SYSTEM = AudioManager.STREAM_SYSTEM;
/**
* @see AudioManager#STREAM_VOICE_CALL
*/
public static final int STREAM_TYPE_VOICE_CALL = AudioManager.STREAM_VOICE_CALL;
/**
* The default stream type used by audio renderers.
*/
public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC;
/**
* Flags which can apply to a buffer containing a media sample.
*/
@ -185,6 +222,29 @@ public final class C {
*/
public static final int BUFFER_FLAG_DECODE_ONLY = 0x80000000;
/**
* Video scaling modes for {@link MediaCodec}-based {@link Renderer}s.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(value = {VIDEO_SCALING_MODE_SCALE_TO_FIT, VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING})
public @interface VideoScalingMode {}
/**
* @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT
*/
@SuppressWarnings("InlinedApi")
public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT =
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT;
/**
* @see MediaCodec#VIDEO_SCALING_MODE_SCALE_TO_FIT
*/
@SuppressWarnings("InlinedApi")
public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING =
MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING;
/**
* A default video scaling mode for {@link MediaCodec}-based {@link Renderer}s.
*/
public static final int VIDEO_SCALING_MODE_DEFAULT = VIDEO_SCALING_MODE_SCALE_TO_FIT;
/**
* Track selection flags.
*/
@ -397,21 +457,45 @@ public final class C {
public static final int MSG_SET_SURFACE = 1;
/**
* The type of a message that can be passed to an audio {@link Renderer} via
* A type of a message that can be passed to an audio {@link Renderer} via
* {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
* should be a {@link Float} with 0 being silence and 1 being unity gain.
*/
public static final int MSG_SET_VOLUME = 2;
/**
* The type of a message that can be passed to an audio {@link Renderer} via
* A type of a message that can be passed to an audio {@link Renderer} via
* {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
* should be a {@link android.media.PlaybackParams}, which will be used to configure the
* should be a {@link android.media.PlaybackParams}, or null, which will be used to configure the
* underlying {@link android.media.AudioTrack}. The message object should not be modified by the
* caller after it has been passed
*/
public static final int MSG_SET_PLAYBACK_PARAMS = 3;
/**
* A type of a message that can be passed to an audio {@link Renderer} via
* {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
* should be one of the integer stream types in {@link C.StreamType}, and will specify the stream
* type of the underlying {@link android.media.AudioTrack}. See also
* {@link android.media.AudioTrack#AudioTrack(int, int, int, int, int, int)}. If the stream type
* is not set, audio renderers use {@link #STREAM_TYPE_DEFAULT}.
* <p>
* Note that when the stream type changes, the AudioTrack must be reinitialized, which can
* introduce a brief gap in audio output. Note also that tracks in the same audio session must
* share the same routing, so a new audio session id will be generated.
*/
public static final int MSG_SET_STREAM_TYPE = 4;
/**
* The type of a message that can be passed to a {@link MediaCodec}-based video {@link Renderer}
* via {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message
* object should be one of the integer scaling modes in {@link C.VideoScalingMode}.
* <p>
* Note that the scaling mode only applies if the {@link Surface} targeted by the renderer is
* owned by a {@link android.view.SurfaceView}.
*/
public static final int MSG_SET_SCALING_MODE = 5;
/**
* Applications or extensions may define custom {@code MSG_*} constants greater than or equal to
* this value.

View file

@ -16,7 +16,7 @@
package com.google.android.exoplayer2;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelections;
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.Util;
@ -111,7 +111,7 @@ public final class DefaultLoadControl implements LoadControl {
@Override
public void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,
TrackSelections<?> trackSelections) {
TrackSelectionArray trackSelections) {
targetBufferSize = 0;
for (int i = 0; i < renderers.length; i++) {
if (trackSelections.get(i) != null) {

View file

@ -22,11 +22,13 @@ import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.text.TextRenderer;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
@ -110,6 +112,28 @@ public interface ExoPlayer {
*/
interface EventListener {
/**
* Called when the timeline and/or manifest has been refreshed.
* <p>
* Note that if the timeline has changed then a position discontinuity may also have occurred.
* For example the current period index may have changed as a result of periods being added or
* removed from the timeline. The will <em>not</em> be reported via a separate call to
* {@link #onPositionDiscontinuity()}.
*
* @param timeline The latest timeline. Never null, but may be empty.
* @param manifest The latest manifest. May be null.
*/
void onTimelineChanged(Timeline timeline, Object manifest);
/**
* Called when the available or selected tracks change.
*
* @param trackGroups The available tracks. Never null, but may be of length zero.
* @param trackSelections The track selections for each {@link Renderer}. Never null and always
* of length {@link #getRendererCount()}, but may contain null elements.
*/
void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections);
/**
* Called when the player starts or stops loading the source.
*
@ -127,14 +151,6 @@ public interface ExoPlayer {
*/
void onPlayerStateChanged(boolean playWhenReady, int playbackState);
/**
* Called when timeline and/or manifest has been refreshed.
*
* @param timeline The latest timeline, or null if the timeline is being cleared.
* @param manifest The latest manifest, or null if the manifest is being cleared.
*/
void onTimelineChanged(Timeline timeline, Object manifest);
/**
* Called when an error occurs. The playback state will transition to {@link #STATE_IDLE}
* immediately after this method is called. The player instance can still be used, and
@ -145,9 +161,14 @@ public interface ExoPlayer {
void onPlayerError(ExoPlaybackException error);
/**
* Called when a position discontinuity occurs. Position discontinuities occur when seeks are
* performed, when playbacks transition from one period in the timeline to the next, and when
* the player introduces discontinuities internally.
* Called when a position discontinuity occurs without a change to the timeline. A position
* discontinuity occurs when the current window or period index changes (as a result of playback
* transitioning from one period in the timeline to the next), or when the playback position
* jumps within the period currently being played (as a result of a seek being performed, or
* when the source introduces a discontinuity internally).
* <p>
* When a position discontinuity occurs as a result of a change to the timeline this method is
* <em>not</em> called. {@link #onTimelineChanged(Timeline, Object)} is called in this case.
*/
void onPositionDiscontinuity();
@ -259,11 +280,11 @@ public interface ExoPlayer {
* @param resetPosition Whether the playback position should be reset to the default position in
* the first {@link Timeline.Window}. If false, playback will start from the position defined
* by {@link #getCurrentWindowIndex()} and {@link #getCurrentPosition()}.
* @param resetTimeline Whether the timeline and manifest should be reset. Should be true unless
* the player is being prepared to play the same media as it was playing previously (e.g. if
* playback failed and is being retried).
* @param resetState Whether the timeline, manifest, tracks and track selections should be reset.
* Should be true unless the player is being prepared to play the same media as it was playing
* previously (e.g. if playback failed and is being retried).
*/
void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetTimeline);
void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState);
/**
* Sets whether playback should proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
@ -309,17 +330,19 @@ public interface ExoPlayer {
/**
* Seeks to a position specified in milliseconds in the current window.
*
* @param windowPositionMs The seek position in the current window.
* @param positionMs The seek position in the current window, or {@link C#TIME_UNSET} to seek to
* the window's default position.
*/
void seekTo(long windowPositionMs);
void seekTo(long positionMs);
/**
* Seeks to a position specified in milliseconds in the specified window.
*
* @param windowIndex The index of the window.
* @param windowPositionMs The seek position in the specified window.
* @param positionMs The seek position in the specified window, or {@link C#TIME_UNSET} to seek to
* the window's default position.
*/
void seekTo(int windowIndex, long windowPositionMs);
void seekTo(int windowIndex, long positionMs);
/**
* Stops playback. Use {@code setPlayWhenReady(false)} rather than this method if the intention
@ -356,19 +379,43 @@ public interface ExoPlayer {
*/
void blockingSendMessages(ExoPlayerMessage... messages);
/**
* Returns the number of renderers.
*/
int getRendererCount();
/**
* Returns the track type that the renderer at a given index handles.
*
* @see Renderer#getTrackType()
* @param index The index of the renderer.
* @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
*/
int getRendererType(int index);
/**
* Returns the available track groups.
*/
TrackGroupArray getCurrentTrackGroups();
/**
* Returns the current track selections for each renderer.
*/
TrackSelectionArray getCurrentTrackSelections();
/**
* Returns the current manifest. The type depends on the {@link MediaSource} passed to
* {@link #prepare}.
* {@link #prepare}. May be null.
*/
Object getCurrentManifest();
/**
* Returns the current {@link Timeline}, or {@code null} if there is no timeline.
* Returns the current {@link Timeline}. Never null, but may be empty.
*/
Timeline getCurrentTimeline();
/**
* Returns the index of the period currently being played, or {@link C#INDEX_UNSET} if unknown.
* Returns the index of the period currently being played.
*/
int getCurrentPeriodIndex();

View file

@ -42,14 +42,14 @@ public final class ExoPlayerFactory {
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
* @param loadControl The {@link LoadControl} that will be used by the instance.
*/
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector<?> trackSelector,
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
LoadControl loadControl) {
return newSimpleInstance(context, trackSelector, loadControl, null);
}
/**
* Creates a {@link SimpleExoPlayer} instance. Must be called from a thread that has an associated
* {@link Looper}.
* {@link Looper}. Available extension renderers are not used.
*
* @param context A {@link Context}.
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
@ -57,9 +57,10 @@ public final class ExoPlayerFactory {
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
* will not be used for DRM protected playbacks.
*/
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector<?> trackSelector,
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager) {
return newSimpleInstance(context, trackSelector, loadControl, drmSessionManager, false);
return newSimpleInstance(context, trackSelector, loadControl,
drmSessionManager, SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF);
}
/**
@ -71,15 +72,15 @@ public final class ExoPlayerFactory {
* @param loadControl The {@link LoadControl} that will be used by the instance.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
* will not be used for DRM protected playbacks.
* @param preferExtensionDecoders True to prefer {@link Renderer} instances defined in
* available extensions over those defined in the core library. Note that extensions must be
* included in the application build for setting this flag to have any effect.
* @param extensionRendererMode The extension renderer mode, which determines if and how available
* extension renderers are used. Note that extensions must be included in the application
* build for them to be considered available.
*/
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector<?> trackSelector,
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
boolean preferExtensionDecoders) {
@SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode) {
return newSimpleInstance(context, trackSelector, loadControl, drmSessionManager,
preferExtensionDecoders, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS);
extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS);
}
/**
@ -91,17 +92,18 @@ public final class ExoPlayerFactory {
* @param loadControl The {@link LoadControl} that will be used by the instance.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance
* will not be used for DRM protected playbacks.
* @param preferExtensionDecoders True to prefer {@link Renderer} instances defined in
* available extensions over those defined in the core library. Note that extensions must be
* included in the application build for setting this flag to have any effect.
* @param extensionRendererMode The extension renderer mode, which determines if and how available
* extension renderers are used. Note that extensions must be included in the application
* build for them to be considered available.
* @param allowedVideoJoiningTimeMs The maximum duration for which a video renderer can attempt to
* seamlessly join an ongoing playback.
*/
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector<?> trackSelector,
public static SimpleExoPlayer newSimpleInstance(Context context, TrackSelector trackSelector,
LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
boolean preferExtensionDecoders, long allowedVideoJoiningTimeMs) {
@SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode,
long allowedVideoJoiningTimeMs) {
return new SimpleExoPlayer(context, trackSelector, loadControl, drmSessionManager,
preferExtensionDecoders, allowedVideoJoiningTimeMs);
extensionRendererMode, allowedVideoJoiningTimeMs);
}
/**
@ -111,7 +113,7 @@ public final class ExoPlayerFactory {
* @param renderers The {@link Renderer}s that will be used by the instance.
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
*/
public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector<?> trackSelector) {
public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector) {
return newInstance(renderers, trackSelector, new DefaultLoadControl());
}
@ -123,7 +125,7 @@ public final class ExoPlayerFactory {
* @param trackSelector The {@link TrackSelector} that will be used by the instance.
* @param loadControl The {@link LoadControl} that will be used by the instance.
*/
public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector<?> trackSelector,
public static ExoPlayer newInstance(Renderer[] renderers, TrackSelector trackSelector,
LoadControl loadControl) {
return new ExoPlayerImpl(renderers, trackSelector, loadControl);
}

View file

@ -20,11 +20,16 @@ import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.util.Pair;
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.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import java.util.concurrent.CopyOnWriteArraySet;
/**
@ -34,19 +39,24 @@ import java.util.concurrent.CopyOnWriteArraySet;
private static final String TAG = "ExoPlayerImpl";
private final Renderer[] renderers;
private final TrackSelector trackSelector;
private final TrackSelectionArray emptyTrackSelections;
private final Handler eventHandler;
private final ExoPlayerImplInternal<?> internalPlayer;
private final ExoPlayerImplInternal internalPlayer;
private final CopyOnWriteArraySet<EventListener> listeners;
private final Timeline.Window window;
private final Timeline.Period period;
private boolean pendingInitialSeek;
private boolean tracksSelected;
private boolean playWhenReady;
private int playbackState;
private int pendingSeekAcks;
private boolean isLoading;
private Timeline timeline;
private Object manifest;
private TrackGroupArray trackGroups;
private TrackSelectionArray trackSelections;
// Playback information when there is no pending seek/set source operation.
private PlaybackInfo playbackInfo;
@ -63,16 +73,20 @@ import java.util.concurrent.CopyOnWriteArraySet;
* @param loadControl The {@link LoadControl} that will be used by the instance.
*/
@SuppressLint("HandlerLeak")
public ExoPlayerImpl(Renderer[] renderers, TrackSelector<?> trackSelector,
LoadControl loadControl) {
Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION);
Assertions.checkNotNull(renderers);
public ExoPlayerImpl(Renderer[] renderers, TrackSelector trackSelector, LoadControl loadControl) {
Log.i(TAG, "Init " + ExoPlayerLibraryInfo.VERSION + " [" + Util.DEVICE_DEBUG_INFO + "]");
Assertions.checkState(renderers.length > 0);
this.renderers = Assertions.checkNotNull(renderers);
this.trackSelector = Assertions.checkNotNull(trackSelector);
this.playWhenReady = false;
this.playbackState = STATE_IDLE;
this.listeners = new CopyOnWriteArraySet<>();
emptyTrackSelections = new TrackSelectionArray(new TrackSelection[renderers.length]);
timeline = Timeline.EMPTY;
window = new Timeline.Window();
period = new Timeline.Period();
trackGroups = TrackGroupArray.EMPTY;
trackSelections = emptyTrackSelections;
eventHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
@ -80,8 +94,8 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
};
playbackInfo = new ExoPlayerImplInternal.PlaybackInfo(0, 0);
internalPlayer = new ExoPlayerImplInternal<>(renderers, trackSelector, loadControl,
playWhenReady, eventHandler, playbackInfo);
internalPlayer = new ExoPlayerImplInternal(renderers, trackSelector, loadControl, playWhenReady,
eventHandler, playbackInfo, this);
}
@Override
@ -105,12 +119,23 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
@Override
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetTimeline) {
if (resetTimeline && (timeline != null || manifest != null)) {
timeline = null;
manifest = null;
for (EventListener listener : listeners) {
listener.onTimelineChanged(null, null);
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
if (resetState) {
if (!timeline.isEmpty() || manifest != null) {
timeline = Timeline.EMPTY;
manifest = null;
for (EventListener listener : listeners) {
listener.onTimelineChanged(timeline, manifest);
}
}
if (tracksSelected) {
tracksSelected = false;
trackGroups = TrackGroupArray.EMPTY;
trackSelections = emptyTrackSelections;
trackSelector.onSelectionActivated(null);
for (EventListener listener : listeners) {
listener.onTracksChanged(trackGroups, trackSelections);
}
}
}
internalPlayer.prepare(mediaSource, resetPosition);
@ -144,17 +169,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public void seekToDefaultPosition(int windowIndex) {
if (timeline == null) {
maskingWindowIndex = windowIndex;
maskingWindowPositionMs = C.TIME_UNSET;
pendingInitialSeek = true;
} else {
Assertions.checkIndex(windowIndex, 0, timeline.getWindowCount());
pendingSeekAcks++;
maskingWindowIndex = windowIndex;
maskingWindowPositionMs = 0;
internalPlayer.seekTo(timeline.getWindow(windowIndex, window).firstPeriodIndex, C.TIME_UNSET);
}
seekTo(windowIndex, C.TIME_UNSET);
}
@Override
@ -164,27 +179,17 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public void seekTo(int windowIndex, long positionMs) {
if (windowIndex < 0 || (!timeline.isEmpty() && windowIndex >= timeline.getWindowCount())) {
throw new IllegalSeekPositionException(timeline, windowIndex, positionMs);
}
pendingSeekAcks++;
maskingWindowIndex = windowIndex;
if (positionMs == C.TIME_UNSET) {
seekToDefaultPosition(windowIndex);
} else if (timeline == null) {
maskingWindowIndex = windowIndex;
maskingWindowPositionMs = positionMs;
pendingInitialSeek = true;
maskingWindowPositionMs = 0;
internalPlayer.seekTo(timeline, windowIndex, C.TIME_UNSET);
} else {
Assertions.checkIndex(windowIndex, 0, timeline.getWindowCount());
pendingSeekAcks++;
maskingWindowIndex = windowIndex;
maskingWindowPositionMs = positionMs;
timeline.getWindow(windowIndex, window);
int periodIndex = window.firstPeriodIndex;
long periodPositionMs = window.getPositionInFirstPeriodMs() + positionMs;
long periodDurationMs = timeline.getPeriod(periodIndex, period).getDurationMs();
while (periodDurationMs != C.TIME_UNSET && periodPositionMs >= periodDurationMs
&& periodIndex < window.lastPeriodIndex) {
periodPositionMs -= periodDurationMs;
periodDurationMs = timeline.getPeriod(++periodIndex, period).getDurationMs();
}
internalPlayer.seekTo(periodIndex, C.msToUs(periodPositionMs));
internalPlayer.seekTo(timeline, windowIndex, C.msToUs(positionMs));
for (EventListener listener : listeners) {
listener.onPositionDiscontinuity();
}
@ -219,7 +224,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public int getCurrentWindowIndex() {
if (timeline == null || pendingSeekAcks > 0) {
if (timeline.isEmpty() || pendingSeekAcks > 0) {
return maskingWindowIndex;
} else {
return timeline.getPeriod(playbackInfo.periodIndex, period).windowIndex;
@ -228,7 +233,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public long getDuration() {
if (timeline == null) {
if (timeline.isEmpty()) {
return C.TIME_UNSET;
}
return timeline.getWindow(getCurrentWindowIndex(), window).getDurationMs();
@ -236,7 +241,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public long getCurrentPosition() {
if (timeline == null || pendingSeekAcks > 0) {
if (timeline.isEmpty() || pendingSeekAcks > 0) {
return maskingWindowPositionMs;
} else {
timeline.getPeriod(playbackInfo.periodIndex, period);
@ -247,7 +252,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public long getBufferedPosition() {
// TODO - Implement this properly.
if (timeline == null || pendingSeekAcks > 0) {
if (timeline.isEmpty() || pendingSeekAcks > 0) {
return maskingWindowPositionMs;
} else {
timeline.getPeriod(playbackInfo.periodIndex, period);
@ -257,7 +262,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
@Override
public int getBufferedPercentage() {
if (timeline == null) {
if (timeline.isEmpty()) {
return 0;
}
long bufferedPosition = getBufferedPosition();
@ -266,6 +271,26 @@ import java.util.concurrent.CopyOnWriteArraySet;
: (int) (duration == 0 ? 100 : (bufferedPosition * 100) / duration);
}
@Override
public int getRendererCount() {
return renderers.length;
}
@Override
public int getRendererType(int index) {
return renderers[index].getTrackType();
}
@Override
public TrackGroupArray getCurrentTrackGroups() {
return trackGroups;
}
@Override
public TrackSelectionArray getCurrentTrackSelections() {
return trackSelections;
}
@Override
public Timeline getCurrentTimeline() {
return timeline;
@ -293,6 +318,17 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
break;
}
case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: {
TrackInfo trackInfo = (TrackInfo) msg.obj;
tracksSelected = true;
trackGroups = trackInfo.groups;
trackSelections = trackInfo.selections;
trackSelector.onSelectionActivated(trackInfo.info);
for (EventListener listener : listeners) {
listener.onTracksChanged(trackGroups, trackSelections);
}
break;
}
case ExoPlayerImplInternal.MSG_SEEK_ACK: {
if (--pendingSeekAcks == 0) {
playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj;
@ -312,14 +348,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
break;
}
case ExoPlayerImplInternal.MSG_SOURCE_INFO_REFRESHED: {
@SuppressWarnings("unchecked")
Pair<Timeline, Object> timelineAndManifest = (Pair<Timeline, Object>) msg.obj;
timeline = timelineAndManifest.first;
manifest = timelineAndManifest.second;
if (pendingInitialSeek) {
pendingInitialSeek = false;
seekTo(maskingWindowIndex, maskingWindowPositionMs);
}
SourceInfo sourceInfo = (SourceInfo) msg.obj;
timeline = sourceInfo.timeline;
manifest = sourceInfo.manifest;
playbackInfo = sourceInfo.playbackInfo;
pendingSeekAcks -= sourceInfo.seekAcks;
for (EventListener listener : listeners) {
listener.onTimelineChanged(timeline, manifest);
}

View file

@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo {
/**
* The version of the library, expressed as a string.
*/
String VERSION = "2.0.4";
String VERSION = "2.1.0";
/**
* The version of the library, expressed as an integer.
@ -32,7 +32,7 @@ public interface ExoPlayerLibraryInfo {
* corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding
* integer version 123045006 (123-045-006).
*/
int VERSION_INT = 2000004;
int VERSION_INT = 2001000;
/**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
@ -45,5 +45,5 @@ public interface ExoPlayerLibraryInfo {
* trace enabled.
*/
boolean TRACE_ENABLED = true;
}

View file

@ -21,6 +21,7 @@ import android.media.MediaFormat;
import android.os.Parcel;
import android.os.Parcelable;
import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer;
@ -57,6 +58,10 @@ public final class Format implements Parcelable {
* Codecs of the format as described in RFC 6381, or null if unknown or not applicable.
*/
public final String codecs;
/**
* Metadata, or null if unknown or not applicable.
*/
public final Metadata metadata;
// Container specific.
@ -173,6 +178,11 @@ public final class Format implements Parcelable {
*/
public final String language;
/**
* The Accessibility channel, or {@link #NO_VALUE} if not known or applicable.
*/
public final int accessibilityChannel;
// Lazily initialized hashcode and framework media format.
private int hashCode;
@ -185,7 +195,8 @@ public final class Format implements Parcelable {
float frameRate, List<byte[]> initializationData) {
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, OFFSET_SAMPLE_RELATIVE, initializationData, null);
NO_VALUE, NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, null,
null);
}
public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs,
@ -210,8 +221,8 @@ public final class Format implements Parcelable {
byte[] projectionData, @C.StereoMode int stereoMode, DrmInitData drmInitData) {
return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, width, height,
frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, OFFSET_SAMPLE_RELATIVE, initializationData,
drmInitData);
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
initializationData, drmInitData, null);
}
// Audio.
@ -221,8 +232,8 @@ public final class Format implements Parcelable {
List<byte[]> initializationData, @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, channelCount, sampleRate, NO_VALUE,
NO_VALUE, NO_VALUE, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, initializationData,
null);
NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
initializationData, null, null);
}
public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs,
@ -239,18 +250,18 @@ public final class Format implements Parcelable {
@C.SelectionFlags int selectionFlags, String language) {
return createAudioSampleFormat(id, sampleMimeType, codecs, bitrate, maxInputSize, channelCount,
sampleRate, pcmEncoding, NO_VALUE, NO_VALUE, initializationData, drmInitData,
selectionFlags, language);
selectionFlags, language, null);
}
public static Format createAudioSampleFormat(String id, String sampleMimeType, String codecs,
int bitrate, int maxInputSize, int channelCount, int sampleRate,
@C.PcmEncoding int pcmEncoding, int encoderDelay, int encoderPadding,
List<byte[]> initializationData, DrmInitData drmInitData,
@C.SelectionFlags int selectionFlags, String language) {
@C.SelectionFlags int selectionFlags, String language, Metadata metadata) {
return new Format(id, null, sampleMimeType, codecs, bitrate, maxInputSize, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, channelCount, sampleRate, pcmEncoding,
encoderDelay, encoderPadding, selectionFlags, language, OFFSET_SAMPLE_RELATIVE,
initializationData, drmInitData);
encoderDelay, encoderPadding, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
initializationData, drmInitData, metadata);
}
// Text.
@ -258,23 +269,46 @@ public final class Format implements Parcelable {
public static Format createTextContainerFormat(String id, String containerMimeType,
String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags,
String language) {
return createTextContainerFormat(id, containerMimeType, sampleMimeType, codecs, bitrate,
selectionFlags, language, NO_VALUE);
}
public static Format createTextContainerFormat(String id, String containerMimeType,
String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags,
String language, int accessibilityChannel) {
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, selectionFlags, language, OFFSET_SAMPLE_RELATIVE, null, null);
NO_VALUE, NO_VALUE, selectionFlags, language, accessibilityChannel,
OFFSET_SAMPLE_RELATIVE, null, null, null);
}
public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData) {
return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
drmInitData, OFFSET_SAMPLE_RELATIVE);
NO_VALUE, drmInitData, OFFSET_SAMPLE_RELATIVE);
}
public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
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);
}
public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
int bitrate, @C.SelectionFlags int selectionFlags, String language, DrmInitData drmInitData,
long subsampleOffsetUs) {
return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
NO_VALUE, drmInitData, subsampleOffsetUs);
}
public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
int bitrate, @C.SelectionFlags int selectionFlags, String language,
int accessibilityChannel, DrmInitData drmInitData, long subsampleOffsetUs) {
return new Format(id, null, 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, selectionFlags, language, subsampleOffsetUs, null, drmInitData);
NO_VALUE, selectionFlags, language, accessibilityChannel, subsampleOffsetUs, null,
drmInitData, null);
}
// Image.
@ -283,7 +317,8 @@ public final class Format implements Parcelable {
int bitrate, List<byte[]> initializationData, String language, DrmInitData drmInitData) {
return new Format(id, null, 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, language, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData);
NO_VALUE, 0, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, drmInitData,
null);
}
// Generic.
@ -292,14 +327,14 @@ public final class Format implements Parcelable {
String sampleMimeType, int bitrate) {
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, OFFSET_SAMPLE_RELATIVE, null, null);
NO_VALUE, NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, null, null);
}
public static Format createSampleFormat(String id, String sampleMimeType, String codecs,
int bitrate, DrmInitData drmInitData) {
return new Format(id, null, 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, OFFSET_SAMPLE_RELATIVE, null, drmInitData);
NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, drmInitData, null);
}
/* package */ Format(String id, String containerMimeType, String sampleMimeType, String codecs,
@ -307,7 +342,8 @@ public final class Format implements Parcelable {
float pixelWidthHeightRatio, byte[] projectionData, @C.StereoMode int stereoMode,
int channelCount, int sampleRate, @C.PcmEncoding int pcmEncoding, int encoderDelay,
int encoderPadding, @C.SelectionFlags int selectionFlags, String language,
long subsampleOffsetUs, List<byte[]> initializationData, DrmInitData drmInitData) {
int accessibilityChannel, long subsampleOffsetUs, List<byte[]> initializationData,
DrmInitData drmInitData, Metadata metadata) {
this.id = id;
this.containerMimeType = containerMimeType;
this.sampleMimeType = sampleMimeType;
@ -328,10 +364,12 @@ public final class Format implements Parcelable {
this.encoderPadding = encoderPadding;
this.selectionFlags = selectionFlags;
this.language = language;
this.accessibilityChannel = accessibilityChannel;
this.subsampleOffsetUs = subsampleOffsetUs;
this.initializationData = initializationData == null ? Collections.<byte[]>emptyList()
: initializationData;
this.drmInitData = drmInitData;
this.metadata = metadata;
}
@SuppressWarnings("ResourceType")
@ -357,6 +395,7 @@ public final class Format implements Parcelable {
encoderPadding = in.readInt();
selectionFlags = in.readInt();
language = in.readString();
accessibilityChannel = in.readInt();
subsampleOffsetUs = in.readLong();
int initializationDataSize = in.readInt();
initializationData = new ArrayList<>(initializationDataSize);
@ -364,20 +403,23 @@ public final class Format implements Parcelable {
initializationData.add(in.createByteArray());
}
drmInitData = in.readParcelable(DrmInitData.class.getClassLoader());
metadata = in.readParcelable(Metadata.class.getClassLoader());
}
public Format copyWithMaxInputSize(int maxInputSize) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData);
selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
drmInitData, metadata);
}
public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData);
selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
drmInitData, metadata);
}
public Format copyWithContainerInfo(String id, String codecs, int bitrate, int width, int height,
@ -385,7 +427,8 @@ public final class Format implements Parcelable {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData);
selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
drmInitData, metadata);
}
public Format copyWithManifestFormatInfo(Format manifestFormat,
@ -401,21 +444,32 @@ public final class Format implements Parcelable {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags,
language, subsampleOffsetUs, initializationData, drmInitData);
language, accessibilityChannel, subsampleOffsetUs, initializationData, drmInitData,
metadata);
}
public Format copyWithGaplessInfo(int encoderDelay, int encoderPadding) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData);
selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
drmInitData, metadata);
}
public Format copyWithDrmInitData(DrmInitData drmInitData) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
selectionFlags, language, subsampleOffsetUs, initializationData, drmInitData);
selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
drmInitData, metadata);
}
public Format copyWithMetadata(Metadata metadata) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize,
width, height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData,
stereoMode, channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding,
selectionFlags, language, accessibilityChannel, subsampleOffsetUs, initializationData,
drmInitData, metadata);
}
/**
@ -474,7 +528,9 @@ public final class Format implements Parcelable {
result = 31 * result + channelCount;
result = 31 * result + sampleRate;
result = 31 * result + (language == null ? 0 : language.hashCode());
result = 31 * result + accessibilityChannel;
result = 31 * result + (drmInitData == null ? 0 : drmInitData.hashCode());
result = 31 * result + (metadata == null ? 0 : metadata.hashCode());
hashCode = result;
}
return hashCode;
@ -498,10 +554,12 @@ public final class Format implements Parcelable {
|| encoderPadding != other.encoderPadding || subsampleOffsetUs != other.subsampleOffsetUs
|| selectionFlags != other.selectionFlags || !Util.areEqual(id, other.id)
|| !Util.areEqual(language, other.language)
|| accessibilityChannel != other.accessibilityChannel
|| !Util.areEqual(containerMimeType, other.containerMimeType)
|| !Util.areEqual(sampleMimeType, other.sampleMimeType)
|| !Util.areEqual(codecs, other.codecs)
|| !Util.areEqual(drmInitData, other.drmInitData)
|| !Util.areEqual(metadata, other.metadata)
|| !Arrays.equals(projectionData, other.projectionData)
|| initializationData.size() != other.initializationData.size()) {
return false;
@ -567,6 +625,7 @@ public final class Format implements Parcelable {
dest.writeInt(encoderPadding);
dest.writeInt(selectionFlags);
dest.writeString(language);
dest.writeInt(accessibilityChannel);
dest.writeLong(subsampleOffsetUs);
int initializationDataSize = initializationData.size();
dest.writeInt(initializationDataSize);
@ -574,6 +633,7 @@ public final class Format implements Parcelable {
dest.writeByteArray(initializationData.get(i));
}
dest.writeParcelable(drmInitData, 0);
dest.writeParcelable(metadata, 0);
}
/**

View file

@ -0,0 +1,48 @@
/*
* 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;
/**
* Thrown when an attempt is made to seek to a position that does not exist in the player's
* {@link Timeline}.
*/
public final class IllegalSeekPositionException extends IllegalStateException {
/**
* The {@link Timeline} in which the seek was attempted.
*/
public final Timeline timeline;
/**
* The index of the window being seeked to.
*/
public final int windowIndex;
/**
* The seek position in the specified window.
*/
public final long positionMs;
/**
* @param timeline The {@link Timeline} in which the seek was attempted.
* @param windowIndex The index of the window being seeked to.
* @param positionMs The seek position in the specified window.
*/
public IllegalSeekPositionException(Timeline timeline, int windowIndex, long positionMs) {
this.timeline = timeline;
this.windowIndex = windowIndex;
this.positionMs = positionMs;
}
}

View file

@ -17,7 +17,7 @@ package com.google.android.exoplayer2;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelections;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.Allocator;
/**
@ -38,7 +38,7 @@ public interface LoadControl {
* @param trackSelections The track selections that were made.
*/
void onTracksSelected(Renderer[] renderers, TrackGroupArray trackGroups,
TrackSelections<?> trackSelections);
TrackSelectionArray trackSelections);
/**
* Called by the player when stopped.

View file

@ -149,7 +149,13 @@ public interface Renderer extends ExoPlayerComponent {
* This method may be called when the renderer is in the following states:
* {@link #STATE_ENABLED}, {@link #STATE_STARTED}.
*/
void setCurrentStreamIsFinal();
void setCurrentStreamFinal();
/**
* Returns whether the current {@link SampleStream} will be the final one supplied before the
* renderer is next disabled or reset.
*/
boolean isCurrentStreamFinal();
/**
* Throws an error that's preventing the renderer from reading from its {@link SampleStream}. Does

View file

@ -18,10 +18,10 @@ package com.google.android.exoplayer2;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.media.AudioManager;
import android.media.MediaCodec;
import android.media.PlaybackParams;
import android.os.Handler;
import android.support.annotation.IntDef;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
@ -35,16 +35,19 @@ import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.DrmSessionManager;
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.metadata.id3.Id3Frame;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextRenderer;
import com.google.android.exoplayer2.trackselection.TrackSelections;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
@ -54,7 +57,7 @@ import java.util.List;
* be obtained from {@link ExoPlayerFactory}.
*/
@TargetApi(16)
public final class SimpleExoPlayer implements ExoPlayer {
public class SimpleExoPlayer implements ExoPlayer {
/**
* A listener for video rendering information from a {@link SimpleExoPlayer}.
@ -86,15 +89,35 @@ public final class SimpleExoPlayer implements ExoPlayer {
*/
void onRenderedFirstFrame();
/**
* Called when a video track is no longer selected.
*/
void onVideoTracksDisabled();
}
/**
* Modes for using extension renderers.
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({EXTENSION_RENDERER_MODE_OFF, EXTENSION_RENDERER_MODE_ON, EXTENSION_RENDERER_MODE_PREFER})
public @interface ExtensionRendererMode {}
/**
* Do not allow use of extension renderers.
*/
public static final int EXTENSION_RENDERER_MODE_OFF = 0;
/**
* Allow use of extension renderers. Extension renderers are indexed after core renderers of the
* same type. A {@link TrackSelector} that prefers the first suitable renderer will therefore
* prefer to use a core renderer to an extension renderer in the case that both are able to play
* a given track.
*/
public static final int EXTENSION_RENDERER_MODE_ON = 1;
/**
* Allow use of extension renderers. Extension renderers are indexed before core renderers of the
* same type. A {@link TrackSelector} that prefers the first suitable renderer will therefore
* prefer to use an extension renderer to a core renderer in the case that both are able to play
* a given track.
*/
public static final int EXTENSION_RENDERER_MODE_PREFER = 2;
private static final String TAG = "SimpleExoPlayer";
private static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50;
protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50;
private final ExoPlayer player;
private final Renderer[] renderers;
@ -103,41 +126,38 @@ public final class SimpleExoPlayer implements ExoPlayer {
private final int videoRendererCount;
private final int audioRendererCount;
private boolean videoTracksEnabled;
private Format videoFormat;
private Format audioFormat;
private Surface surface;
private boolean ownsSurface;
@C.VideoScalingMode
private int videoScalingMode;
private SurfaceHolder surfaceHolder;
private TextureView textureView;
private TextRenderer.Output textOutput;
private MetadataRenderer.Output<List<Id3Frame>> id3Output;
private MetadataRenderer.Output metadataOutput;
private VideoListener videoListener;
private AudioRendererEventListener audioDebugListener;
private VideoRendererEventListener videoDebugListener;
private DecoderCounters videoDecoderCounters;
private DecoderCounters audioDecoderCounters;
private int audioSessionId;
private float volume;
@C.StreamType
private int audioStreamType;
private float audioVolume;
private PlaybackParamsHolder playbackParamsHolder;
/* package */ SimpleExoPlayer(Context context, TrackSelector<?> trackSelector,
LoadControl loadControl, DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
boolean preferExtensionDecoders, long allowedVideoJoiningTimeMs) {
protected SimpleExoPlayer(Context context, TrackSelector trackSelector, LoadControl loadControl,
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
@ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) {
mainHandler = new Handler();
componentListener = new ComponentListener();
trackSelector.addListener(componentListener);
// Build the renderers.
ArrayList<Renderer> renderersList = new ArrayList<>();
if (preferExtensionDecoders) {
buildExtensionRenderers(renderersList, allowedVideoJoiningTimeMs);
buildRenderers(context, drmSessionManager, renderersList, allowedVideoJoiningTimeMs);
} else {
buildRenderers(context, drmSessionManager, renderersList, allowedVideoJoiningTimeMs);
buildExtensionRenderers(renderersList, allowedVideoJoiningTimeMs);
}
buildRenderers(context, mainHandler, drmSessionManager, extensionRendererMode,
allowedVideoJoiningTimeMs, renderersList);
renderers = renderersList.toArray(new Renderer[renderersList.size()]);
// Obtain counts of video and audio renderers.
@ -157,31 +177,41 @@ public final class SimpleExoPlayer implements ExoPlayer {
this.audioRendererCount = audioRendererCount;
// Set initial values.
audioVolume = 1;
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
volume = 1;
audioStreamType = C.STREAM_TYPE_DEFAULT;
videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
// Build the player and associated objects.
player = new ExoPlayerImpl(renderers, trackSelector, loadControl);
}
/**
* Returns the number of renderers.
* Sets the video scaling mode.
* <p>
* Note that the scaling mode only applies if a {@link MediaCodec}-based video {@link Renderer} is
* enabled and if the output surface is owned by a {@link android.view.SurfaceView}.
*
* @return The number of renderers.
* @param videoScalingMode The video scaling mode.
*/
public int getRendererCount() {
return renderers.length;
public void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode) {
this.videoScalingMode = videoScalingMode;
ExoPlayerMessage[] messages = new ExoPlayerMessage[videoRendererCount];
int count = 0;
for (Renderer renderer : renderers) {
if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) {
messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_SCALING_MODE,
videoScalingMode);
}
}
player.sendMessages(messages);
}
/**
* Returns the track type that the renderer at a given index handles.
*
* @see Renderer#getTrackType()
* @param index The index of the renderer.
* @return One of the {@code TRACK_TYPE_*} constants defined in {@link C}.
* Returns the video scaling mode.
*/
public int getRendererType(int index) {
return renderers[index].getTrackType();
public @C.VideoScalingMode int getVideoScalingMode() {
return videoScalingMode;
}
/**
@ -259,17 +289,47 @@ public final class SimpleExoPlayer implements ExoPlayer {
}
/**
* Sets the audio volume, with 0 being silence and 1 being unity gain.
* Sets the stream type for audio playback (see {@link C.StreamType} and
* {@link android.media.AudioTrack#AudioTrack(int, int, int, int, int, int)}). If the stream type
* is not set, audio renderers use {@link C#STREAM_TYPE_DEFAULT}.
* <p>
* Note that when the stream type changes, the AudioTrack must be reinitialized, which can
* introduce a brief gap in audio output. Note also that tracks in the same audio session must
* share the same routing, so a new audio session id will be generated.
*
* @param volume The volume.
* @param audioStreamType The stream type for audio playback.
*/
public void setVolume(float volume) {
this.volume = volume;
public void setAudioStreamType(@C.StreamType int audioStreamType) {
this.audioStreamType = audioStreamType;
ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount];
int count = 0;
for (Renderer renderer : renderers) {
if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {
messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_VOLUME, volume);
messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_STREAM_TYPE, audioStreamType);
}
}
player.sendMessages(messages);
}
/**
* Returns the stream type for audio playback.
*/
public @C.StreamType int getAudioStreamType() {
return audioStreamType;
}
/**
* Sets the audio volume, with 0 being silence and 1 being unity gain.
*
* @param audioVolume The audio volume.
*/
public void setVolume(float audioVolume) {
this.audioVolume = audioVolume;
ExoPlayerMessage[] messages = new ExoPlayerMessage[audioRendererCount];
int count = 0;
for (Renderer renderer : renderers) {
if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) {
messages[count++] = new ExoPlayerMessage(renderer, C.MSG_SET_VOLUME, audioVolume);
}
}
player.sendMessages(messages);
@ -279,7 +339,7 @@ public final class SimpleExoPlayer implements ExoPlayer {
* Returns the audio volume, with 0 being silence and 1 being unity gain.
*/
public float getVolume() {
return volume;
return audioVolume;
}
/**
@ -390,12 +450,21 @@ public final class SimpleExoPlayer implements ExoPlayer {
}
/**
* Sets a listener to receive ID3 metadata events.
* @deprecated Use {@link #setMetadataOutput(MetadataRenderer.Output)} instead.
* @param output The output.
*/
@Deprecated
public void setId3Output(MetadataRenderer.Output output) {
setMetadataOutput(output);
}
/**
* Sets a listener to receive metadata events.
*
* @param output The output.
*/
public void setId3Output(MetadataRenderer.Output<List<Id3Frame>> output) {
id3Output = output;
public void setMetadataOutput(MetadataRenderer.Output output) {
metadataOutput = output;
}
// ExoPlayer implementation
@ -517,6 +586,26 @@ public final class SimpleExoPlayer implements ExoPlayer {
return player.getBufferedPercentage();
}
@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();
@ -527,55 +616,99 @@ public final class SimpleExoPlayer implements ExoPlayer {
return player.getCurrentManifest();
}
// Internal methods.
// Renderer building.
private void buildRenderers(Context context,
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, ArrayList<Renderer> renderersList,
long allowedVideoJoiningTimeMs) {
MediaCodecVideoRenderer videoRenderer = new MediaCodecVideoRenderer(context,
MediaCodecSelector.DEFAULT, MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT,
allowedVideoJoiningTimeMs, drmSessionManager, false, mainHandler, componentListener,
MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);
renderersList.add(videoRenderer);
Renderer audioRenderer = new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT,
drmSessionManager, true, mainHandler, componentListener,
AudioCapabilities.getCapabilities(context), AudioManager.STREAM_MUSIC);
renderersList.add(audioRenderer);
Renderer textRenderer = new TextRenderer(componentListener, mainHandler.getLooper());
renderersList.add(textRenderer);
MetadataRenderer<List<Id3Frame>> id3Renderer = new MetadataRenderer<>(componentListener,
mainHandler.getLooper(), new Id3Decoder());
renderersList.add(id3Renderer);
private void buildRenderers(Context context, Handler mainHandler,
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
@ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs,
ArrayList<Renderer> out) {
buildVideoRenderers(context, mainHandler, drmSessionManager, extensionRendererMode,
componentListener, allowedVideoJoiningTimeMs, out);
buildAudioRenderers(context, mainHandler, drmSessionManager, extensionRendererMode,
componentListener, out);
buildTextRenderers(context, mainHandler, extensionRendererMode, componentListener, out);
buildMetadataRenderers(context, mainHandler, extensionRendererMode, componentListener, out);
buildMiscellaneousRenderers(context, mainHandler, extensionRendererMode, out);
}
private void buildExtensionRenderers(ArrayList<Renderer> renderersList,
long allowedVideoJoiningTimeMs) {
// Load extension renderers using reflection so that demo app doesn't depend on them.
// Class.forName(<class name>) appears for each renderer so that automated tools like proguard
// can detect the use of reflection (see http://proguard.sourceforge.net/FAQ.html#forname).
/**
* Builds video renderers for use by the player.
*
* @param context The {@link Context} associated with the player.
* @param mainHandler A handler associated with the main thread's looper.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will
* not be used for DRM protected playbacks.
* @param extensionRendererMode The extension renderer mode.
* @param eventListener An event listener.
* @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video renderers
* can attempt to seamlessly join an ongoing playback.
* @param out An array to which the built renderers should be appended.
*/
protected void buildVideoRenderers(Context context, Handler mainHandler,
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
@ExtensionRendererMode int extensionRendererMode, VideoRendererEventListener eventListener,
long allowedVideoJoiningTimeMs, ArrayList<Renderer> out) {
out.add(new MediaCodecVideoRenderer(context, MediaCodecSelector.DEFAULT,
allowedVideoJoiningTimeMs, drmSessionManager, false, mainHandler, eventListener,
MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY));
if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
return;
}
int extensionRendererIndex = out.size();
if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) {
extensionRendererIndex--;
}
try {
Class<?> clazz =
Class.forName("com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer");
Constructor<?> constructor = clazz.getConstructor(boolean.class, long.class, Handler.class,
VideoRendererEventListener.class, int.class);
renderersList.add((Renderer) constructor.newInstance(true, allowedVideoJoiningTimeMs,
mainHandler, componentListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY));
Renderer renderer = (Renderer) constructor.newInstance(true, allowedVideoJoiningTimeMs,
mainHandler, componentListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);
out.add(extensionRendererIndex++, renderer);
Log.i(TAG, "Loaded LibvpxVideoRenderer.");
} catch (ClassNotFoundException e) {
// Expected if the app was built without the extension.
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Builds audio renderers for use by the player.
*
* @param context The {@link Context} associated with the player.
* @param mainHandler A handler associated with the main thread's looper.
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will
* not be used for DRM protected playbacks.
* @param extensionRendererMode The extension renderer mode.
* @param eventListener An event listener.
* @param out An array to which the built renderers should be appended.
*/
protected void buildAudioRenderers(Context context, Handler mainHandler,
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
@ExtensionRendererMode int extensionRendererMode, AudioRendererEventListener eventListener,
ArrayList<Renderer> out) {
out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true,
mainHandler, eventListener, AudioCapabilities.getCapabilities(context)));
if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
return;
}
int extensionRendererIndex = out.size();
if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) {
extensionRendererIndex--;
}
try {
Class<?> clazz =
Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer");
Constructor<?> constructor = clazz.getConstructor(Handler.class,
AudioRendererEventListener.class);
renderersList.add((Renderer) constructor.newInstance(mainHandler, componentListener));
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener);
out.add(extensionRendererIndex++, renderer);
Log.i(TAG, "Loaded LibopusAudioRenderer.");
} catch (ClassNotFoundException e) {
// Expected if the app was built without the extension.
@ -588,7 +721,8 @@ public final class SimpleExoPlayer implements ExoPlayer {
Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer");
Constructor<?> constructor = clazz.getConstructor(Handler.class,
AudioRendererEventListener.class);
renderersList.add((Renderer) constructor.newInstance(mainHandler, componentListener));
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener);
out.add(extensionRendererIndex++, renderer);
Log.i(TAG, "Loaded LibflacAudioRenderer.");
} catch (ClassNotFoundException e) {
// Expected if the app was built without the extension.
@ -601,7 +735,8 @@ public final class SimpleExoPlayer implements ExoPlayer {
Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer");
Constructor<?> constructor = clazz.getConstructor(Handler.class,
AudioRendererEventListener.class);
renderersList.add((Renderer) constructor.newInstance(mainHandler, componentListener));
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener);
out.add(extensionRendererIndex++, renderer);
Log.i(TAG, "Loaded FfmpegAudioRenderer.");
} catch (ClassNotFoundException e) {
// Expected if the app was built without the extension.
@ -610,6 +745,51 @@ public final class SimpleExoPlayer implements ExoPlayer {
}
}
/**
* Builds text renderers for use by the player.
*
* @param context The {@link Context} associated with the player.
* @param mainHandler A handler associated with the main thread's looper.
* @param extensionRendererMode The extension renderer mode.
* @param output An output for the renderers.
* @param out An array to which the built renderers should be appended.
*/
protected void buildTextRenderers(Context context, Handler mainHandler,
@ExtensionRendererMode int extensionRendererMode, TextRenderer.Output output,
ArrayList<Renderer> out) {
out.add(new TextRenderer(output, mainHandler.getLooper()));
}
/**
* Builds metadata renderers for use by the player.
*
* @param context The {@link Context} associated with the player.
* @param mainHandler A handler associated with the main thread's looper.
* @param extensionRendererMode The extension renderer mode.
* @param output An output for the renderers.
* @param out An array to which the built renderers should be appended.
*/
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()));
}
/**
* Builds any miscellaneous renderers used by the player.
*
* @param context The {@link Context} associated with the player.
* @param mainHandler A handler associated with the main thread's looper.
* @param extensionRendererMode The extension renderer mode.
* @param out An array to which the built renderers should be appended.
*/
protected void buildMiscellaneousRenderers(Context context, Handler mainHandler,
@ExtensionRendererMode int extensionRendererMode, ArrayList<Renderer> out) {
// Do nothing.
}
// Internal methods.
private void removeSurfaceCallbacks() {
if (textureView != null) {
if (textureView.getSurfaceTextureListener() != componentListener) {
@ -650,9 +830,8 @@ public final class SimpleExoPlayer implements ExoPlayer {
}
private final class ComponentListener implements VideoRendererEventListener,
AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output<List<Id3Frame>>,
SurfaceHolder.Callback, TextureView.SurfaceTextureListener,
TrackSelector.EventListener<Object> {
AudioRendererEventListener, TextRenderer.Output, MetadataRenderer.Output,
SurfaceHolder.Callback, TextureView.SurfaceTextureListener {
// VideoRendererEventListener implementation
@ -782,12 +961,12 @@ public final class SimpleExoPlayer implements ExoPlayer {
}
}
// MetadataRenderer.Output<List<Id3Frame>> implementation
// MetadataRenderer.Output implementation
@Override
public void onMetadata(List<Id3Frame> id3Frames) {
if (id3Output != null) {
id3Output.onMetadata(id3Frames);
public void onMetadata(Metadata metadata) {
if (metadataOutput != null) {
metadataOutput.onMetadata(metadata);
}
}
@ -831,23 +1010,6 @@ public final class SimpleExoPlayer implements ExoPlayer {
// Do nothing.
}
// TrackSelector.EventListener implementation
@Override
public void onTrackSelectionsChanged(TrackSelections<?> trackSelections) {
boolean videoTracksEnabled = false;
for (int i = 0; i < renderers.length; i++) {
if (renderers[i].getTrackType() == C.TRACK_TYPE_VIDEO && trackSelections.get(i) != null) {
videoTracksEnabled = true;
break;
}
}
if (videoListener != null && SimpleExoPlayer.this.videoTracksEnabled && !videoTracksEnabled) {
videoListener.onVideoTracksDisabled();
}
SimpleExoPlayer.this.videoTracksEnabled = videoTracksEnabled;
}
}
@TargetApi(23)

View file

@ -91,6 +91,46 @@ package com.google.android.exoplayer2;
*/
public abstract class Timeline {
/**
* An empty timeline.
*/
public static final Timeline EMPTY = new Timeline() {
@Override
public int getWindowCount() {
return 0;
}
@Override
public Window getWindow(int windowIndex, Window window, boolean setIds,
long defaultPositionProjectionUs) {
throw new IndexOutOfBoundsException();
}
@Override
public int getPeriodCount() {
return 0;
}
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
throw new IndexOutOfBoundsException();
}
@Override
public int getIndexOfPeriod(Object uid) {
return C.INDEX_UNSET;
}
};
/**
* Returns whether the timeline is empty.
*/
public final boolean isEmpty() {
return getWindowCount() == 0;
}
/**
* Returns the number of windows in the timeline.
*/
@ -114,10 +154,26 @@ public abstract class Timeline {
* @param windowIndex The index of the window.
* @param window The {@link Window} to populate. Must not be null.
* @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to
* null. The caller should pass false for efficiency reasons unless the field is required.
* null. The caller should pass false for efficiency reasons unless the field is required.
* @return The populated {@link Window}, for convenience.
*/
public abstract Window getWindow(int windowIndex, Window window, boolean setIds);
public Window getWindow(int windowIndex, Window window, boolean setIds) {
return getWindow(windowIndex, window, setIds, 0);
}
/**
* Populates a {@link Window} with data for the window at the specified index.
*
* @param windowIndex The index of the window.
* @param window The {@link Window} to populate. Must not be null.
* @param setIds Whether {@link Window#id} should be populated. If false, the field will be set to
* null. The caller should pass false for efficiency reasons unless the field is required.
* @param defaultPositionProjectionUs A duration into the future that the populated window's
* default start position should be projected.
* @return The populated {@link Window}, for convenience.
*/
public abstract Window getWindow(int windowIndex, Window window, boolean setIds,
long defaultPositionProjectionUs);
/**
* Returns the number of periods in the timeline.
@ -181,8 +237,8 @@ public abstract class Timeline {
public long presentationStartTimeMs;
/**
* The windows start time in milliseconds since the epoch, or {@link C#TIME_UNSET} if unknown or
* not applicable. For informational purposes only.
* The window's start time in milliseconds since the epoch, or {@link C#TIME_UNSET} if unknown
* or not applicable. For informational purposes only.
*/
public long windowStartTimeMs;
@ -206,9 +262,24 @@ public abstract class Timeline {
*/
public int lastPeriodIndex;
private long defaultPositionUs;
private long durationUs;
private long positionInFirstPeriodUs;
/**
* The default position relative to the start of the window at which to begin playback, in
* microseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a
* non-zero default position projection, and if the specified projection cannot be performed
* whilst remaining within the bounds of the window.
*/
public long defaultPositionUs;
/**
* The duration of this window in microseconds, or {@link C#TIME_UNSET} if unknown.
*/
public long durationUs;
/**
* The position of the start of this window relative to the start of the first period belonging
* to it, in microseconds.
*/
public long positionInFirstPeriodUs;
/**
* Sets the data held by this window.
@ -231,7 +302,9 @@ public abstract class Timeline {
/**
* Returns the default position relative to the start of the window at which to begin playback,
* in milliseconds.
* in milliseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a
* non-zero default position projection, and if the specified projection cannot be performed
* whilst remaining within the bounds of the window.
*/
public long getDefaultPositionMs() {
return C.usToMs(defaultPositionUs);
@ -239,7 +312,9 @@ public abstract class Timeline {
/**
* Returns the default position relative to the start of the window at which to begin playback,
* in microseconds.
* in microseconds. May be {@link C#TIME_UNSET} if and only if the window was populated with a
* non-zero default position projection, and if the specified projection cannot be performed
* whilst remaining within the bounds of the window.
*/
public long getDefaultPositionUs() {
return defaultPositionUs;
@ -303,7 +378,11 @@ public abstract class Timeline {
*/
public int windowIndex;
private long durationUs;
/**
* The duration of this period in microseconds, or {@link C#TIME_UNSET} if unknown.
*/
public long durationUs;
private long positionInWindowUs;
/**

View file

@ -27,4 +27,15 @@ public abstract class AudioDecoderException extends Exception {
super(detailMessage);
}
/**
* @param detailMessage The detail message for this exception.
* @param cause the cause (which is saved for later retrieval by the
* {@link #getCause()} method). (A <tt>null</tt> value is
* permitted, and indicates that the cause is nonexistent or
* unknown.)
*/
public AudioDecoderException(String detailMessage, Throwable cause) {
super(detailMessage, cause);
}
}

View file

@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.audio;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.media.AudioFormat;
import android.media.AudioTimestamp;
@ -23,6 +24,7 @@ import android.os.ConditionVariable;
import android.os.SystemClock;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
@ -53,6 +55,24 @@ import java.nio.ByteBuffer;
*/
public final class AudioTrack {
/**
* Listener for audio track events.
*/
public interface Listener {
/**
* Called when the audio track underruns.
*
* @param bufferSize The size of the track's buffer, in bytes.
* @param bufferSizeMs The size of the track's buffer, in milliseconds, if it is configured for
* PCM output. {@link C#TIME_UNSET} if it is configured for passthrough output, as the
* buffered media can have a variable bitrate so the duration may be unknown.
* @param elapsedSinceLastFeedMs The time since the track was last fed data, in milliseconds.
*/
void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs);
}
/**
* Thrown when a failure occurs initializing an {@link android.media.AudioTrack}.
*/
@ -151,6 +171,40 @@ public final class AudioTrack {
*/
private static final int BUFFER_MULTIPLICATION_FACTOR = 4;
/**
* @see android.media.AudioTrack#PLAYSTATE_STOPPED
*/
private static final int PLAYSTATE_STOPPED = android.media.AudioTrack.PLAYSTATE_STOPPED;
/**
* @see android.media.AudioTrack#PLAYSTATE_PAUSED
*/
private static final int PLAYSTATE_PAUSED = android.media.AudioTrack.PLAYSTATE_PAUSED;
/**
* @see android.media.AudioTrack#PLAYSTATE_PLAYING
*/
private static final int PLAYSTATE_PLAYING = android.media.AudioTrack.PLAYSTATE_PLAYING;
/**
* @see android.media.AudioTrack#ERROR_BAD_VALUE
*/
private static final int ERROR_BAD_VALUE = android.media.AudioTrack.ERROR_BAD_VALUE;
/**
* @see android.media.AudioTrack#MODE_STATIC
*/
private static final int MODE_STATIC = android.media.AudioTrack.MODE_STATIC;
/**
* @see android.media.AudioTrack#MODE_STREAM
*/
private static final int MODE_STREAM = android.media.AudioTrack.MODE_STREAM;
/**
* @see android.media.AudioTrack#STATE_INITIALIZED
*/
private static final int STATE_INITIALIZED = android.media.AudioTrack.STATE_INITIALIZED;
/**
* @see android.media.AudioTrack#WRITE_NON_BLOCKING
*/
@SuppressLint("InlinedApi")
private static final int WRITE_NON_BLOCKING = android.media.AudioTrack.WRITE_NON_BLOCKING;
private static final String TAG = "AudioTrack";
/**
@ -195,7 +249,7 @@ public final class AudioTrack {
public static boolean failOnSpuriousAudioTimestamp = false;
private final AudioCapabilities audioCapabilities;
private final int streamType;
private final Listener listener;
private final ConditionVariable releasingConditionVariable;
private final long[] playheadOffsets;
private final AudioTrackUtil audioTrackUtil;
@ -208,6 +262,8 @@ public final class AudioTrack {
private android.media.AudioTrack audioTrack;
private int sampleRate;
private int channelConfig;
@C.StreamType
private int streamType;
@C.Encoding
private int sourceEncoding;
@C.Encoding
@ -241,13 +297,16 @@ public final class AudioTrack {
private ByteBuffer resampledBuffer;
private boolean useResampledBuffer;
private boolean hasData;
private long lastFeedElapsedRealtimeMs;
/**
* @param audioCapabilities The current audio capabilities.
* @param streamType The type of audio stream for the underlying {@link android.media.AudioTrack}.
* @param listener Listener for audio track events.
*/
public AudioTrack(AudioCapabilities audioCapabilities, int streamType) {
public AudioTrack(AudioCapabilities audioCapabilities, Listener listener) {
this.audioCapabilities = audioCapabilities;
this.streamType = streamType;
this.listener = listener;
releasingConditionVariable = new ConditionVariable(true);
if (Util.SDK_INT >= 18) {
try {
@ -267,6 +326,7 @@ public final class AudioTrack {
playheadOffsets = new long[MAX_PLAYHEAD_OFFSET_COUNT];
volume = 1.0f;
startMediaTimeState = START_NOT_SET;
streamType = C.STREAM_TYPE_DEFAULT;
}
/**
@ -304,7 +364,7 @@ public final class AudioTrack {
return CURRENT_POSITION_NOT_SET;
}
if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PLAYING) {
if (audioTrack.getPlayState() == PLAYSTATE_PLAYING) {
maybeSampleSyncParams();
}
@ -423,7 +483,7 @@ public final class AudioTrack {
} else {
int minBufferSize =
android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, targetEncoding);
Assertions.checkState(minBufferSize != android.media.AudioTrack.ERROR_BAD_VALUE);
Assertions.checkState(minBufferSize != ERROR_BAD_VALUE);
int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR;
int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * pcmFrameSize;
int maxAppBufferSize = (int) Math.max(minBufferSize,
@ -452,11 +512,11 @@ public final class AudioTrack {
if (sessionId == SESSION_ID_NOT_SET) {
audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
targetEncoding, bufferSize, android.media.AudioTrack.MODE_STREAM);
targetEncoding, bufferSize, MODE_STREAM);
} else {
// Re-attach to the same audio session.
audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
targetEncoding, bufferSize, android.media.AudioTrack.MODE_STREAM, sessionId);
targetEncoding, bufferSize, MODE_STREAM, sessionId);
}
checkAudioTrackInitialized();
@ -475,42 +535,17 @@ 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, android.media.AudioTrack.MODE_STATIC, sessionId);
channelConfig, encoding, bufferSize, MODE_STATIC, sessionId);
}
}
}
audioTrackUtil.reconfigure(audioTrack, needsPassthroughWorkarounds());
setAudioTrackVolume();
hasData = false;
return sessionId;
}
/**
* Returns the size of this {@link AudioTrack}'s buffer in bytes.
* <p>
* The value returned from this method may change as a result of calling one of the
* {@link #configure} methods.
*
* @return The size of the buffer in bytes.
*/
public int getBufferSize() {
return bufferSize;
}
/**
* Returns the size of the buffer in microseconds for PCM {@link AudioTrack}s, or
* {@link C#TIME_UNSET} for passthrough {@link AudioTrack}s.
* <p>
* The value returned from this method may change as a result of calling one of the
* {@link #configure} methods.
*
* @return The size of the buffer in microseconds for PCM {@link AudioTrack}s, or
* {@link C#TIME_UNSET} for passthrough {@link AudioTrack}s.
*/
public long getBufferSizeUs() {
return bufferSizeUs;
}
/**
* Starts or resumes playing audio if the audio track has been initialized.
*/
@ -552,6 +587,18 @@ public final class AudioTrack {
* @throws WriteException If an error occurs writing the audio data.
*/
public int handleBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException {
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);
lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime();
return result;
}
private int writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException {
boolean isNewSourceBuffer = currentSourceBuffer == null;
Assertions.checkState(isNewSourceBuffer || currentSourceBuffer == buffer);
currentSourceBuffer = buffer;
@ -559,14 +606,14 @@ public final class AudioTrack {
if (needsPassthroughWorkarounds()) {
// 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() == android.media.AudioTrack.PLAYSTATE_PAUSED) {
if (audioTrack.getPlayState() == PLAYSTATE_PAUSED) {
return 0;
}
// A new AC-3 audio track's playback position continues to increase from the old track's
// position for a short time after is has been released. Avoid writing data until the playback
// head position actually returns to zero.
if (audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_STOPPED
if (audioTrack.getPlayState() == PLAYSTATE_STOPPED
&& audioTrackUtil.getPlaybackHeadPosition() != 0) {
return 0;
}
@ -695,6 +742,24 @@ public final class AudioTrack {
audioTrackUtil.setPlaybackParams(playbackParams);
}
/**
* Sets the stream type for audio track. If the stream type has changed, {@link #isInitialized()}
* will return {@code false} and the caller must re-{@link #initialize(int)} the audio track
* before writing more data. The caller must not reuse the audio session identifier when
* re-initializing with a new stream type.
*
* @param streamType The {@link C.StreamType} to use for audio output.
* @return Whether the stream type changed.
*/
public boolean setStreamType(@C.StreamType int streamType) {
if (this.streamType == streamType) {
return false;
}
this.streamType = streamType;
reset();
return true;
}
/**
* Sets the playback volume.
*
@ -744,7 +809,7 @@ public final class AudioTrack {
latencyUs = 0;
resetSyncParams();
int playState = audioTrack.getPlayState();
if (playState == android.media.AudioTrack.PLAYSTATE_PLAYING) {
if (playState == PLAYSTATE_PLAYING) {
audioTrack.pause();
}
// AudioTrack.release can take some time, so we call it on a background thread.
@ -893,7 +958,7 @@ public final class AudioTrack {
*/
private void checkAudioTrackInitialized() throws InitializationException {
int state = audioTrack.getState();
if (state == android.media.AudioTrack.STATE_INITIALIZED) {
if (state == STATE_INITIALIZED) {
return;
}
// The track is not successfully initialized. Release and null the track.
@ -951,7 +1016,7 @@ public final class AudioTrack {
*/
private boolean overrideHasPendingData() {
return needsPassthroughWorkarounds()
&& audioTrack.getPlayState() == android.media.AudioTrack.PLAYSTATE_PAUSED
&& audioTrack.getPlayState() == PLAYSTATE_PAUSED
&& audioTrack.getPlaybackHeadPosition() == 0;
}
@ -981,6 +1046,9 @@ public final class AudioTrack {
case C.ENCODING_PCM_32BIT:
resampledSize = size / 2;
break;
case C.ENCODING_PCM_16BIT:
case C.ENCODING_INVALID:
case Format.NO_VALUE:
default:
// Never happens.
throw new IllegalStateException();
@ -1016,6 +1084,9 @@ public final class AudioTrack {
resampledBuffer.put(buffer.get(i + 3));
}
break;
case C.ENCODING_PCM_16BIT:
case C.ENCODING_INVALID:
case Format.NO_VALUE:
default:
// Never happens.
throw new IllegalStateException();
@ -1056,7 +1127,7 @@ public final class AudioTrack {
@TargetApi(21)
private static int writeNonBlockingV21(
android.media.AudioTrack audioTrack, ByteBuffer buffer, int size) {
return audioTrack.write(buffer, size, android.media.AudioTrack.WRITE_NON_BLOCKING);
return audioTrack.write(buffer, size, WRITE_NON_BLOCKING);
}
@TargetApi(21)
@ -1149,7 +1220,7 @@ public final class AudioTrack {
}
int state = audioTrack.getPlayState();
if (state == android.media.AudioTrack.PLAYSTATE_STOPPED) {
if (state == PLAYSTATE_STOPPED) {
// The audio track hasn't been started.
return 0;
}
@ -1159,7 +1230,7 @@ public final class AudioTrack {
// Work around an issue with passthrough/direct AudioTracks on platform API versions 21/22
// where the playback head position jumps back to zero on paused passthrough/direct audio
// tracks. See [Internal: b/19187573].
if (state == android.media.AudioTrack.PLAYSTATE_PAUSED && rawPlaybackHeadPosition == 0) {
if (state == PLAYSTATE_PAUSED && rawPlaybackHeadPosition == 0) {
passthroughWorkaroundPauseOffset = lastRawPlaybackHeadPosition;
}
rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset;

View file

@ -16,14 +16,12 @@
package com.google.android.exoplayer2.audio;
import android.annotation.TargetApi;
import android.media.AudioManager;
import android.media.MediaCodec;
import android.media.MediaCrypto;
import android.media.MediaFormat;
import android.media.PlaybackParams;
import android.media.audiofx.Virtualizer;
import android.os.Handler;
import android.os.SystemClock;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format;
@ -43,7 +41,8 @@ import java.nio.ByteBuffer;
* Decodes and renders audio using {@link MediaCodec} and {@link AudioTrack}.
*/
@TargetApi(16)
public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock {
public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock,
AudioTrack.Listener {
private final EventDispatcher eventDispatcher;
private final AudioTrack audioTrack;
@ -55,9 +54,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
private long currentPositionUs;
private boolean allowPositionDiscontinuity;
private boolean audioTrackHasData;
private long lastFeedElapsedRealtimeMs;
/**
* @param mediaCodecSelector A decoder selector.
*/
@ -76,7 +72,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
* has obtained the keys necessary to decrypt encrypted regions of the media.
*/
public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector,
DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys) {
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
boolean playClearSamplesWithoutKeys) {
this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, null, null);
}
@ -109,7 +106,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
boolean playClearSamplesWithoutKeys, Handler eventHandler,
AudioRendererEventListener eventListener) {
this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler,
eventListener, null, AudioManager.STREAM_MUSIC);
eventListener, null);
}
/**
@ -126,16 +123,14 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
* @param streamType The type of audio stream for the {@link AudioTrack}.
*/
public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector,
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
boolean playClearSamplesWithoutKeys, Handler eventHandler,
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities,
int streamType) {
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) {
super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys);
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
audioTrack = new AudioTrack(audioCapabilities, streamType);
audioTrack = new AudioTrack(audioCapabilities, this);
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
}
@ -149,7 +144,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
if (allowPassthrough(mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null) {
return ADAPTIVE_NOT_SEAMLESS | FORMAT_HANDLED;
}
MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, false);
MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, false, false);
if (decoderInfo == null) {
return FORMAT_UNSUPPORTED_SUBTYPE;
}
@ -340,29 +335,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
} else {
audioTrack.initialize(audioSessionId);
}
audioTrackHasData = false;
} catch (AudioTrack.InitializationException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
if (getState() == STATE_STARTED) {
audioTrack.play();
}
} else {
// Check for AudioTrack underrun.
boolean audioTrackHadData = audioTrackHasData;
audioTrackHasData = audioTrack.hasPendingData();
if (audioTrackHadData && !audioTrackHasData && getState() == STATE_STARTED) {
long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs;
long bufferSizeMs = C.usToMs(audioTrack.getBufferSizeUs());
eventDispatcher.audioTrackUnderrun(audioTrack.getBufferSize(), bufferSizeMs,
elapsedSinceLastFeedMs);
}
}
int handleBufferResult;
try {
handleBufferResult = audioTrack.handleBuffer(buffer, bufferPresentationTimeUs);
lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime();
} catch (AudioTrack.WriteException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
@ -401,10 +384,23 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
case C.MSG_SET_PLAYBACK_PARAMS:
audioTrack.setPlaybackParams((PlaybackParams) message);
break;
case C.MSG_SET_STREAM_TYPE:
@C.StreamType int streamType = (Integer) message;
if (audioTrack.setStreamType(streamType)) {
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
}
break;
default:
super.handleMessage(messageType, message);
break;
}
}
// AudioTrack.Listener implementation.
@Override
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
}
}

View file

@ -15,10 +15,11 @@
*/
package com.google.android.exoplayer2.audio;
import android.media.AudioManager;
import android.media.PlaybackParams;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.support.annotation.IntDef;
import com.google.android.exoplayer2.BaseRenderer;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
@ -29,16 +30,48 @@ import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
import com.google.android.exoplayer2.drm.DrmSession;
import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
import com.google.android.exoplayer2.util.MediaClock;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TraceUtil;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Decodes and renders audio using a {@link SimpleDecoder}.
*/
public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock {
public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock,
AudioTrack.Listener {
@Retention(RetentionPolicy.SOURCE)
@IntDef({REINITIALIZATION_STATE_NONE, REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM,
REINITIALIZATION_STATE_WAIT_END_OF_STREAM})
private @interface ReinitializationState {}
/**
* The decoder does not need to be re-initialized.
*/
private static final int REINITIALIZATION_STATE_NONE = 0;
/**
* The input format has changed in a way that requires the decoder to be re-initialized, but we
* haven't yet signaled an end of stream to the existing decoder. We need to do so in order to
* ensure that it outputs any remaining buffers before we release it.
*/
private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1;
/**
* The input format has changed in a way that requires the decoder to be re-initialized, and we've
* signaled an end of stream to the existing decoder. We're waiting for the decoder to output an
* end of stream signal to indicate that it has output any remaining buffers before we release it.
*/
private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2;
private final boolean playClearSamplesWithoutKeys;
private final EventDispatcher eventDispatcher;
private final AudioTrack audioTrack;
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
private final FormatHolder formatHolder;
private DecoderCounters decoderCounters;
@ -47,18 +80,22 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
? extends AudioDecoderException> decoder;
private DecoderInputBuffer inputBuffer;
private SimpleOutputBuffer outputBuffer;
private DrmSession<ExoMediaCrypto> drmSession;
private DrmSession<ExoMediaCrypto> pendingDrmSession;
@ReinitializationState
private int decoderReinitializationState;
private boolean decoderReceivedBuffers;
private boolean audioTrackNeedsConfigure;
private long currentPositionUs;
private boolean allowPositionDiscontinuity;
private boolean inputStreamEnded;
private boolean outputStreamEnded;
private boolean waitingForKeys;
private final AudioTrack audioTrack;
private int audioSessionId;
private boolean audioTrackHasData;
private long lastFeedElapsedRealtimeMs;
public SimpleDecoderAudioRenderer() {
this(null, null);
}
@ -70,7 +107,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
*/
public SimpleDecoderAudioRenderer(Handler eventHandler,
AudioRendererEventListener eventListener) {
this (eventHandler, eventListener, null, AudioManager.STREAM_MUSIC);
this(eventHandler, eventListener, null);
}
/**
@ -79,16 +116,38 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
* @param streamType The type of audio stream for the {@link AudioTrack}.
*/
public SimpleDecoderAudioRenderer(Handler eventHandler,
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) {
this(eventHandler, eventListener, audioCapabilities, null, false);
}
/**
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
* null if delivery of events is not required.
* @param eventListener A listener of events. May be null if delivery of events is not required.
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
* default capabilities (no encoded audio passthrough support) should be assumed.
* @param drmSessionManager For use with encrypted media. May be null if support for encrypted
* media is not required.
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
* For example a media file may start with a short clear region so as to allow playback to
* begin in parallel with key acquisition. This parameter specifies whether the renderer is
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
* has obtained the keys necessary to decrypt encrypted regions of the media.
*/
public SimpleDecoderAudioRenderer(Handler eventHandler,
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities,
int streamType) {
DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys) {
super(C.TRACK_TYPE_AUDIO);
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
audioTrack = new AudioTrack(audioCapabilities, streamType);
audioTrack = new AudioTrack(audioCapabilities, this);
this.drmSessionManager = drmSessionManager;
formatHolder = new FormatHolder();
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
audioTrackNeedsConfigure = true;
}
@Override
@ -109,43 +168,35 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
}
// If we don't have a decoder yet, we need to instantiate one.
if (decoder == null) {
maybeInitDecoder();
if (decoder != null) {
try {
long codecInitializingTimestamp = SystemClock.elapsedRealtime();
TraceUtil.beginSection("createAudioDecoder");
decoder = createDecoder(inputFormat);
// Rendering loop.
TraceUtil.beginSection("drainAndFeed");
while (drainOutputBuffer()) {}
while (feedInputBuffer()) {}
TraceUtil.endSection();
long codecInitializedTimestamp = SystemClock.elapsedRealtime();
eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp,
codecInitializedTimestamp - codecInitializingTimestamp);
decoderCounters.decoderInitCount++;
} catch (AudioDecoderException e) {
} catch (AudioTrack.InitializationException | AudioTrack.WriteException
| AudioDecoderException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
decoderCounters.ensureUpdated();
}
// Rendering loop.
try {
TraceUtil.beginSection("drainAndFeed");
while (drainOutputBuffer()) {}
while (feedInputBuffer()) {}
TraceUtil.endSection();
} catch (AudioTrack.InitializationException | AudioTrack.WriteException
| AudioDecoderException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
decoderCounters.ensureUpdated();
}
/**
* Creates a decoder for the given format.
*
* @param format The format for which a decoder is required.
* @param mediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted content.
* Maybe null and can be ignored if decoder does not handle encrypted content.
* @return The decoder.
* @throws AudioDecoderException If an error occurred creating a suitable decoder.
*/
protected abstract SimpleDecoder<DecoderInputBuffer, ? extends SimpleOutputBuffer,
? extends AudioDecoderException> createDecoder(Format format) throws AudioDecoderException;
? extends AudioDecoderException> createDecoder(Format format, ExoMediaCrypto mediaCrypto)
throws AudioDecoderException;
/**
* Returns the format of audio buffers output by the decoder. Will not be called until the first
@ -160,12 +211,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
null, null, 0, null);
}
private boolean drainOutputBuffer() throws AudioDecoderException,
private boolean drainOutputBuffer() throws ExoPlaybackException, AudioDecoderException,
AudioTrack.InitializationException, AudioTrack.WriteException {
if (outputStreamEnded) {
return false;
}
if (outputBuffer == null) {
outputBuffer = decoder.dequeueOutputBuffer();
if (outputBuffer == null) {
@ -175,17 +222,29 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
}
if (outputBuffer.isEndOfStream()) {
outputStreamEnded = true;
audioTrack.handleEndOfStream();
outputBuffer.release();
outputBuffer = null;
if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
// We're waiting to re-initialize the decoder, and have now processed all final buffers.
releaseDecoder();
maybeInitDecoder();
// The audio track may need to be recreated once the new output format is known.
audioTrackNeedsConfigure = true;
} else {
outputBuffer.release();
outputBuffer = null;
outputStreamEnded = true;
audioTrack.handleEndOfStream();
}
return false;
}
if (!audioTrack.isInitialized()) {
if (audioTrackNeedsConfigure) {
Format outputFormat = getOutputFormat();
audioTrack.configure(outputFormat.sampleMimeType, outputFormat.channelCount,
outputFormat.sampleRate, outputFormat.pcmEncoding, 0);
audioTrackNeedsConfigure = false;
}
if (!audioTrack.isInitialized()) {
if (audioSessionId == AudioTrack.SESSION_ID_NOT_SET) {
audioSessionId = audioTrack.initialize(AudioTrack.SESSION_ID_NOT_SET);
eventDispatcher.audioSessionId(audioSessionId);
@ -193,24 +252,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
} else {
audioTrack.initialize(audioSessionId);
}
audioTrackHasData = false;
if (getState() == STATE_STARTED) {
audioTrack.play();
}
} else {
// Check for AudioTrack underrun.
boolean audioTrackHadData = audioTrackHasData;
audioTrackHasData = audioTrack.hasPendingData();
if (audioTrackHadData && !audioTrackHasData && getState() == STATE_STARTED) {
long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs;
long bufferSizeMs = C.usToMs(audioTrack.getBufferSizeUs());
eventDispatcher.audioTrackUnderrun(audioTrack.getBufferSize(), bufferSizeMs,
elapsedSinceLastFeedMs);
}
}
int handleBufferResult = audioTrack.handleBuffer(outputBuffer.data, outputBuffer.timeUs);
lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime();
// If we are out of sync, allow currentPositionUs to jump backwards.
if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) {
@ -228,8 +275,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
return false;
}
private boolean feedInputBuffer() throws AudioDecoderException {
if (inputStreamEnded) {
private boolean feedInputBuffer() throws AudioDecoderException, ExoPlaybackException {
if (decoder == null || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM
|| inputStreamEnded) {
// We need to reinitialize the decoder or the input stream has ended.
return false;
}
@ -240,7 +289,22 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
}
}
int result = readSource(formatHolder, inputBuffer);
if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) {
inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
decoder.queueInputBuffer(inputBuffer);
inputBuffer = null;
decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM;
return false;
}
int result;
if (waitingForKeys) {
// We've already read an encrypted sample into buffer, and are waiting for keys.
result = C.RESULT_BUFFER_READ;
} else {
result = readSource(formatHolder, inputBuffer);
}
if (result == C.RESULT_NOTHING_READ) {
return false;
}
@ -254,20 +318,45 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
inputBuffer = null;
return false;
}
boolean bufferEncrypted = inputBuffer.isEncrypted();
waitingForKeys = shouldWaitForKeys(bufferEncrypted);
if (waitingForKeys) {
return false;
}
inputBuffer.flip();
decoder.queueInputBuffer(inputBuffer);
decoderReceivedBuffers = true;
decoderCounters.inputBufferCount++;
inputBuffer = null;
return true;
}
private void flushDecoder() {
inputBuffer = null;
if (outputBuffer != null) {
outputBuffer.release();
outputBuffer = null;
private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
if (drmSession == null) {
return false;
}
@DrmSession.State int drmSessionState = drmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
}
return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS
&& (bufferEncrypted || !playClearSamplesWithoutKeys);
}
private void flushDecoder() throws ExoPlaybackException {
waitingForKeys = false;
if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) {
releaseDecoder();
maybeInitDecoder();
} else {
inputBuffer = null;
if (outputBuffer != null) {
outputBuffer.release();
outputBuffer = null;
}
decoder.flush();
decoderReceivedBuffers = false;
}
decoder.flush();
}
@Override
@ -278,7 +367,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
@Override
public boolean isReady() {
return audioTrack.hasPendingData()
|| (inputFormat != null && (isSourceReady() || outputBuffer != null));
|| (inputFormat != null && !waitingForKeys && (isSourceReady() || outputBuffer != null));
}
@Override
@ -312,7 +401,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
}
@Override
protected void onPositionReset(long positionUs, boolean joining) {
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
audioTrack.reset();
currentPositionUs = positionUs;
allowPositionDiscontinuity = true;
@ -335,24 +424,82 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
@Override
protected void onDisabled() {
inputBuffer = null;
outputBuffer = null;
inputFormat = null;
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
audioTrackNeedsConfigure = true;
waitingForKeys = false;
try {
if (decoder != null) {
decoder.release();
decoder = null;
decoderCounters.decoderReleaseCount++;
}
releaseDecoder();
audioTrack.release();
} finally {
decoderCounters.ensureUpdated();
eventDispatcher.disabled(decoderCounters);
try {
if (drmSession != null) {
drmSessionManager.releaseSession(drmSession);
}
} finally {
try {
if (pendingDrmSession != null && pendingDrmSession != drmSession) {
drmSessionManager.releaseSession(pendingDrmSession);
}
} finally {
drmSession = null;
pendingDrmSession = null;
decoderCounters.ensureUpdated();
eventDispatcher.disabled(decoderCounters);
}
}
}
}
private boolean readFormat() {
private void maybeInitDecoder() throws ExoPlaybackException {
if (decoder != null) {
return;
}
drmSession = pendingDrmSession;
ExoMediaCrypto mediaCrypto = null;
if (drmSession != null) {
@DrmSession.State int drmSessionState = drmSession.getState();
if (drmSessionState == DrmSession.STATE_ERROR) {
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
} else if (drmSessionState == DrmSession.STATE_OPENED
|| drmSessionState == DrmSession.STATE_OPENED_WITH_KEYS) {
mediaCrypto = drmSession.getMediaCrypto();
} else {
// The drm session isn't open yet.
return;
}
}
try {
long codecInitializingTimestamp = SystemClock.elapsedRealtime();
TraceUtil.beginSection("createAudioDecoder");
decoder = createDecoder(inputFormat, mediaCrypto);
TraceUtil.endSection();
long codecInitializedTimestamp = SystemClock.elapsedRealtime();
eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp,
codecInitializedTimestamp - codecInitializingTimestamp);
decoderCounters.decoderInitCount++;
} catch (AudioDecoderException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
}
private void releaseDecoder() {
if (decoder == null) {
return;
}
inputBuffer = null;
outputBuffer = null;
decoder.release();
decoder = null;
decoderCounters.decoderReleaseCount++;
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
decoderReceivedBuffers = false;
}
private boolean readFormat() throws ExoPlaybackException {
int result = readSource(formatHolder, null);
if (result == C.RESULT_FORMAT_READ) {
onInputFormatChanged(formatHolder.format);
@ -361,8 +508,37 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
return false;
}
private void onInputFormatChanged(Format newFormat) {
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
Format oldFormat = inputFormat;
inputFormat = newFormat;
boolean drmInitDataChanged = !Util.areEqual(inputFormat.drmInitData, oldFormat == null ? null
: oldFormat.drmInitData);
if (drmInitDataChanged) {
if (inputFormat.drmInitData != null) {
if (drmSessionManager == null) {
throw ExoPlaybackException.createForRenderer(
new IllegalStateException("Media requires a DrmSessionManager"), getIndex());
}
pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(),
inputFormat.drmInitData);
if (pendingDrmSession == drmSession) {
drmSessionManager.releaseSession(pendingDrmSession);
}
} else {
pendingDrmSession = null;
}
}
if (decoderReceivedBuffers) {
// Signal end of stream and wait for any final output buffers before re-initialization.
decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
} else {
// There aren't any final output buffers, so release the decoder immediately.
releaseDecoder();
maybeInitDecoder();
}
eventDispatcher.inputFormatChanged(newFormat);
}
@ -375,10 +551,23 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
case C.MSG_SET_PLAYBACK_PARAMS:
audioTrack.setPlaybackParams((PlaybackParams) message);
break;
case C.MSG_SET_STREAM_TYPE:
@C.StreamType int streamType = (Integer) message;
if (audioTrack.setStreamType(streamType)) {
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
}
break;
default:
super.handleMessage(messageType, message);
break;
}
}
// AudioTrack.Listener implementation.
@Override
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
}
}

View file

@ -0,0 +1,20 @@
package com.google.android.exoplayer2.drm;
/**
* An exception when doing drm decryption using the In-App Drm
*/
public class DecryptionException extends Exception {
private final int errorCode;
public DecryptionException(int errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
/**
* Get error code
*/
public int getErrorCode() {
return errorCode;
}
}

View file

@ -447,15 +447,15 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
switch (msg.what) {
case MediaDrm.EVENT_KEY_REQUIRED:
postKeyRequest();
return;
break;
case MediaDrm.EVENT_KEY_EXPIRED:
state = STATE_OPENED;
onError(new KeysExpiredException());
return;
break;
case MediaDrm.EVENT_PROVISION_REQUIRED:
state = STATE_OPENED;
postProvisionRequest();
return;
break;
}
}
@ -483,10 +483,10 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
switch (msg.what) {
case MSG_PROVISION:
onProvisionResponse(msg.obj);
return;
break;
case MSG_KEYS:
onKeyResponse(msg.obj);
return;
break;
}
}

View file

@ -93,9 +93,10 @@ public interface Extractor {
* position} in the stream. Valid random access positions are the start of the stream and
* positions that can be obtained from any {@link SeekMap} passed to the {@link ExtractorOutput}.
*
* @param position The seek position.
* @param position The byte offset in the stream from which data will be provided.
* @param timeUs The seek time in microseconds.
*/
void seek(long position);
void seek(long position, long timeUs);
/**
* Releases all kept resources.

View file

@ -16,6 +16,8 @@
package com.google.android.exoplayer2.extractor;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -65,6 +67,25 @@ public final class GaplessInfoHolder {
return false;
}
/**
* Populates the holder with data parsed from ID3 {@link Metadata}.
*
* @param metadata The metadata from which to parse the gapless information.
* @return Whether the holder was populated.
*/
public boolean setFromMetadata(Metadata metadata) {
for (int i = 0; i < metadata.length(); i++) {
Metadata.Entry entry = metadata.get(i);
if (entry instanceof CommentFrame) {
CommentFrame commentFrame = (CommentFrame) entry;
if (setFromComment(commentFrame.description, commentFrame.text)) {
return true;
}
}
}
return false;
}
/**
* Populates the holder with data parsed from a gapless playback comment (stored in an ID3 header
* or MPEG 4 user data), if valid and non-zero.
@ -73,7 +94,7 @@ public final class GaplessInfoHolder {
* @param data The comment's payload data.
* @return Whether the holder was populated.
*/
public boolean setFromComment(String name, String data) {
private boolean setFromComment(String name, String data) {
if (!GAPLESS_COMMENT_ID.equals(name)) {
return false;
}

View file

@ -29,21 +29,17 @@ import java.util.Collections;
*/
/* package */ final class AudioTagPayloadReader extends TagPayloadReader {
// Audio format
private static final int AUDIO_FORMAT_ALAW = 7;
private static final int AUDIO_FORMAT_ULAW = 8;
private static final int AUDIO_FORMAT_AAC = 10;
// AAC PACKET TYPE
private static final int AAC_PACKET_TYPE_SEQUENCE_HEADER = 0;
private static final int AAC_PACKET_TYPE_AAC_RAW = 1;
// SAMPLING RATES
private static final int[] AUDIO_SAMPLING_RATE_TABLE = new int[] {
5500, 11000, 22000, 44000
};
// State variables
private boolean hasParsedAudioDataHeader;
private boolean hasOutputFormat;
private int audioFormat;
public AudioTagPayloadReader(TrackOutput output) {
super(output);
@ -58,13 +54,17 @@ import java.util.Collections;
protected boolean parseHeader(ParsableByteArray data) throws UnsupportedFormatException {
if (!hasParsedAudioDataHeader) {
int header = data.readUnsignedByte();
int audioFormat = (header >> 4) & 0x0F;
int sampleRateIndex = (header >> 2) & 0x03;
if (sampleRateIndex < 0 || sampleRateIndex >= AUDIO_SAMPLING_RATE_TABLE.length) {
throw new UnsupportedFormatException("Invalid sample rate index: " + sampleRateIndex);
}
// TODO: Add support for MP3 and PCM.
if (audioFormat != AUDIO_FORMAT_AAC) {
audioFormat = (header >> 4) & 0x0F;
// TODO: Add support for MP3.
if (audioFormat == AUDIO_FORMAT_ALAW || audioFormat == AUDIO_FORMAT_ULAW) {
String type = audioFormat == AUDIO_FORMAT_ALAW ? MimeTypes.AUDIO_ALAW
: MimeTypes.AUDIO_ULAW;
int pcmEncoding = (header & 0x01) == 1 ? C.ENCODING_PCM_16BIT : C.ENCODING_PCM_8BIT;
Format format = Format.createAudioSampleFormat(null, type, null, Format.NO_VALUE,
Format.NO_VALUE, 1, 8000, pcmEncoding, null, null, 0, null);
output.format(format);
hasOutputFormat = true;
} else if (audioFormat != AUDIO_FORMAT_AAC) {
throw new UnsupportedFormatException("Audio format not supported: " + audioFormat);
}
hasParsedAudioDataHeader = true;
@ -78,22 +78,21 @@ import java.util.Collections;
@Override
protected void parsePayload(ParsableByteArray data, long timeUs) {
int packetType = data.readUnsignedByte();
// Parse sequence header just in case it was not done before.
if (packetType == AAC_PACKET_TYPE_SEQUENCE_HEADER && !hasOutputFormat) {
byte[] audioSpecifiConfig = new byte[data.bytesLeft()];
data.readBytes(audioSpecifiConfig, 0, audioSpecifiConfig.length);
// Parse the sequence header.
byte[] audioSpecificConfig = new byte[data.bytesLeft()];
data.readBytes(audioSpecificConfig, 0, audioSpecificConfig.length);
Pair<Integer, Integer> audioParams = CodecSpecificDataUtil.parseAacAudioSpecificConfig(
audioSpecifiConfig);
audioSpecificConfig);
Format format = Format.createAudioSampleFormat(null, MimeTypes.AUDIO_AAC, null,
Format.NO_VALUE, Format.NO_VALUE, audioParams.second, audioParams.first,
Collections.singletonList(audioSpecifiConfig), null, 0, null);
Collections.singletonList(audioSpecificConfig), null, 0, null);
output.format(format);
hasOutputFormat = true;
} else if (packetType == AAC_PACKET_TYPE_AAC_RAW) {
// Sample audio AAC frames
int bytesToWrite = data.bytesLeft();
output.sampleData(data, bytesToWrite);
output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, bytesToWrite, 0, null);
} else if (audioFormat != AUDIO_FORMAT_AAC || packetType == AAC_PACKET_TYPE_AAC_RAW) {
int sampleSize = data.bytesLeft();
output.sampleData(data, sampleSize);
output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0, null);
}
}

View file

@ -126,7 +126,7 @@ public final class FlvExtractor implements Extractor, SeekMap {
}
@Override
public void seek(long position) {
public void seek(long position, long timeUs) {
parserState = STATE_READING_FLV_HEADER;
bytesToNextTagHeader = 0;
}

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