mirror of
https://github.com/samsonjs/media.git
synced 2026-03-30 10:15:48 +00:00
Merge branch 'dev-v2-r2.1.0' into release-v2
This commit is contained in:
commit
e6778c90a1
259 changed files with 12926 additions and 4645 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ android {
|
|||
noExtensions
|
||||
withExtensions
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
BIN
demo/src/main/res/drawable-xhdpi/ic_banner.png
Normal file
BIN
demo/src/main/res/drawable-xhdpi/ic_banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
|
|
@ -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"/>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,4 +26,8 @@ public final class OpusDecoderException extends AudioDecoderException {
|
|||
super(message);
|
||||
}
|
||||
|
||||
/* package */ OpusDecoderException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,5 +50,5 @@ public final class OpusLibrary {
|
|||
}
|
||||
|
||||
public static native String opusGetVersion();
|
||||
|
||||
public static native boolean opusIsSecureDecodeSupported();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,5 +59,5 @@ public final class VpxLibrary {
|
|||
|
||||
private static native String vpxGetVersion();
|
||||
private static native String vpxGetBuildConfig();
|
||||
|
||||
public static native boolean vpxIsSecureDecodeSupported();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
@ -22,7 +22,7 @@ track 0:
|
|||
encoderPadding = -1
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = und
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
data = length 19, hash BFE794DB
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ track 0:
|
|||
encoderPadding = -1
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = und
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
data = length 19, hash BFE794DB
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ track 0:
|
|||
encoderPadding = -1
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = und
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
data = length 19, hash BFE794DB
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ track 0:
|
|||
encoderPadding = -1
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = und
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
data = length 19, hash BFE794DB
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ track 0:
|
|||
encoderPadding = -1
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = und
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
data = length 19, hash BFE794DB
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
BIN
library/src/androidTest/assets/ts/sample_with_sdt.ts
Normal file
BIN
library/src/androidTest/assets/ts/sample_with_sdt.ts
Normal file
Binary file not shown.
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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++;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
162
library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java
vendored
Normal file
162
library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpanTest.java
vendored
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue