Merge pull request #2402 from google/dev-v2

r2.2.0
This commit is contained in:
ojw28 2017-01-31 14:04:01 +00:00 committed by GitHub
commit 25a966dc23
233 changed files with 9204 additions and 2342 deletions

View file

@ -1,9 +1,69 @@
# Release notes # # Release notes #
### r2.1.1 ### ### r2.2.0 ###
Bugfix release only. Users of r2.1.0 and r2.0.x should proactively update to * Demo app: Automatic recovery from BehindLiveWindowException, plus improved
this version. handling of pausing and resuming live streams
([#2344](https://github.com/google/ExoPlayer/issues/2344)).
* AndroidTV: Added Support for tunneled video playback
([#1688](https://github.com/google/ExoPlayer/issues/1688)).
* DRM: Renamed StreamingDrmSessionManager to DefaultDrmSessionManager and
added support for using offline licenses
([#876](https://github.com/google/ExoPlayer/issues/876)).
* DRM: Introduce OfflineLicenseHelper to help with offline license acquisition,
renewal and release.
* UI: Updated player control assets. Added vector drawables for use on API level
21 and above.
* UI: Made player control seek bar work correctly with key events if focusable
([#2278](https://github.com/google/ExoPlayer/issues/2278)).
* HLS: Improved support for streams that use EXT-X-DISCONTINUITY without
EXT-X-DISCONTINUITY-SEQUENCE
([#1789](https://github.com/google/ExoPlayer/issues/1789)).
* HLS: Support for EXT-X-START tag
([#1544](https://github.com/google/ExoPlayer/issues/1544)).
* HLS: Check #EXTM3U header is present when parsing the playlist. Fail
gracefully if not ([#2301](https://github.com/google/ExoPlayer/issues/2301)).
* HLS: Fix memory leak
([#2319](https://github.com/google/ExoPlayer/issues/2319)).
* HLS: Fix non-seamless first adaptation where master playlist omits resolution
tags ([#2096](https://github.com/google/ExoPlayer/issues/2096)).
* HLS: Fix handling of WebVTT subtitle renditions with non-standard segment file
extensions ([#2025](https://github.com/google/ExoPlayer/issues/2025) and
[#2355](https://github.com/google/ExoPlayer/issues/2355)).
* HLS: Better handle inconsistent HLS playlist update
([#2249](https://github.com/google/ExoPlayer/issues/2249)).
* DASH: Don't overflow when dealing with large segment numbers
([#2311](https://github.com/google/ExoPlayer/issues/2311)).
* DASH: Fix propagation of language from the manifest
([#2335](https://github.com/google/ExoPlayer/issues/2335)).
* SmoothStreaming: Work around "Offset to sample data was negative" failures
([#2292](https://github.com/google/ExoPlayer/issues/2292),
[#2101](https://github.com/google/ExoPlayer/issues/2101) and
[#1152](https://github.com/google/ExoPlayer/issues/1152)).
* MP3/ID3: Added support for parsing Chapter and URL link frames
([#2316](https://github.com/google/ExoPlayer/issues/2316)).
* MP3/ID3: Handle ID3 frames that end with empty text field
([#2309](https://github.com/google/ExoPlayer/issues/2309)).
* Added ClippingMediaSource for playing clipped portions of media
([#1988](https://github.com/google/ExoPlayer/issues/1988)).
* Added convenience methods to query whether the current window is dynamic and
seekable ([#2320](https://github.com/google/ExoPlayer/issues/2320)).
* Support setting of default headers on HttpDataSource.Factory implementations
([#2166](https://github.com/google/ExoPlayer/issues/2166)).
* Fixed cache failures when using an encrypted cache content index.
* Fix visual artifacts when switching output surface
([#2093](https://github.com/google/ExoPlayer/issues/2093)).
* Fix gradle + proguard configurations.
* Fix player position when replacing the MediaSource
([#2369](https://github.com/google/ExoPlayer/issues/2369)).
* Misc bug fixes, including
[#2330](https://github.com/google/ExoPlayer/issues/2330),
[#2269](https://github.com/google/ExoPlayer/issues/2269),
[#2252](https://github.com/google/ExoPlayer/issues/2252),
[#2264](https://github.com/google/ExoPlayer/issues/2264) and
[#2290](https://github.com/google/ExoPlayer/issues/2290).
### r2.1.1 ###
* Fix some subtitle types (e.g. WebVTT) being displayed out of sync * Fix some subtitle types (e.g. WebVTT) being displayed out of sync
([#2208](https://github.com/google/ExoPlayer/issues/2208)). ([#2208](https://github.com/google/ExoPlayer/issues/2208)).
@ -52,9 +112,9 @@ this version.
* Improved flexibility of SimpleExoPlayer * Improved flexibility of SimpleExoPlayer
([#2102](https://github.com/google/ExoPlayer/issues/2102)). ([#2102](https://github.com/google/ExoPlayer/issues/2102)).
* Fix issue where only the audio of a video would play due to capability * Fix issue where only the audio of a video would play due to capability
detection issues ([#2007](https://github.com/google/ExoPlayer/issues/2007)) detection issues ([#2007](https://github.com/google/ExoPlayer/issues/2007),
([#2034](https://github.com/google/ExoPlayer/issues/2034)) [#2034](https://github.com/google/ExoPlayer/issues/2034) and
([#2157](https://github.com/google/ExoPlayer/issues/2157)). [#2157](https://github.com/google/ExoPlayer/issues/2157)).
* Fix issues that could cause ExtractorMediaSource based playbacks to get stuck * Fix issues that could cause ExtractorMediaSource based playbacks to get stuck
buffering ([#1962](https://github.com/google/ExoPlayer/issues/1962)). buffering ([#1962](https://github.com/google/ExoPlayer/issues/1962)).
* Correctly set SimpleExoPlayerView surface aspect ratio when an active player * Correctly set SimpleExoPlayerView surface aspect ratio when an active player
@ -74,11 +134,6 @@ this version.
### r2.0.3 ### ### r2.0.3 ###
* Fix crash on Jellybean devices when using playback controls
([#1965](https://github.com/google/ExoPlayer/issues/1965)).
### r2.0.3 ###
* Fixed NullPointerException in ExtractorMediaSource * Fixed NullPointerException in ExtractorMediaSource
([#1914](https://github.com/google/ExoPlayer/issues/1914)). ([#1914](https://github.com/google/ExoPlayer/issues/1914)).
* Fixed NullPointerException in HlsMediaPeriod * Fixed NullPointerException in HlsMediaPeriod
@ -191,6 +246,14 @@ 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 however it can be assumed that all such changes are included in the most recent
V2 release. V2 release.
### r1.5.14 ###
* Fixed cache failures when using an encrypted cache content index.
* SmoothStreaming: Work around "Offset to sample data was negative" failures
([#2292](https://github.com/google/ExoPlayer/issues/2292),
[#2101](https://github.com/google/ExoPlayer/issues/2101) and
[#1152](https://github.com/google/ExoPlayer/issues/1152)).
### r1.5.13 ### ### r1.5.13 ###
* Improvements to the upstream cache package. * Improvements to the upstream cache package.

View file

@ -35,7 +35,7 @@ allprojects {
releaseRepoName = 'exoplayer' releaseRepoName = 'exoplayer'
releaseUserOrg = 'google' releaseUserOrg = 'google'
releaseGroupId = 'com.google.android.exoplayer' releaseGroupId = 'com.google.android.exoplayer'
releaseVersion = 'r2.1.1' releaseVersion = 'r2.2.0'
releaseWebsite = 'https://github.com/google/ExoPlayer' releaseWebsite = 'https://github.com/google/ExoPlayer'
} }
} }

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 31 KiB

View file

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

View file

@ -16,8 +16,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.exoplayer2.demo" package="com.google.android.exoplayer2.demo"
android:versionCode="2101" android:versionCode="2200"
android:versionName="2.1.1"> android:versionName="2.2.0">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
@ -27,7 +27,7 @@
<application <application
android:label="@string/application_name" android:label="@string/application_name"
android:icon="@drawable/ic_launcher" android:icon="@mipmap/ic_launcher"
android:banner="@drawable/ic_banner" android:banner="@drawable/ic_banner"
android:largeHeap="true" android:largeHeap="true"
android:allowBackup="false" android:allowBackup="false"

View file

@ -183,52 +183,52 @@
"uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_uhd.mpd" "uri": "https://storage.googleapis.com/wvmedia/clear/vp9/tears/tears_uhd.mpd"
}, },
{ {
"name": "WV: Secure SD & HD (WebM,VP9)", "name": "WV: Secure Fullsample SD & HD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears.mpd",
"drm_scheme": "widevine", "drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
}, },
{ {
"name": "WV: Secure SD (WebM,VP9)", "name": "WV: Secure Fullsample SD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_sd.mpd", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_sd.mpd",
"drm_scheme": "widevine", "drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
}, },
{ {
"name": "WV: Secure HD (WebM,VP9)", "name": "WV: Secure Fullsample HD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_hd.mpd", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_hd.mpd",
"drm_scheme": "widevine", "drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
}, },
{ {
"name": "WV: Secure UHD (WebM,VP9)", "name": "WV: Secure Fullsample UHD (WebM,VP9)",
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/tears/tears_uhd.mpd",
"drm_scheme": "widevine", "drm_scheme": "widevine",
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test" "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
}, },
{ {
"name": "WV: Secure Subsample (WebM, VP9 with altref)", "name": "WV: Secure Subsample SD & HD (WebM,VP9)",
"uri": "https://storage.googleapis.com/widevine_test/vp9/sintel_1080p_vp9_altref_subsample/sintel_1080p_vp9_altref_subsample.mpd", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears.mpd",
"drm_scheme": "widevine", "drm_scheme": "widevine",
"drm_license_url": "https://widevine-proxy.appspot.com/proxy" "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
}, },
{ {
"name": "WV: Secure Fullsample (WebM, VP9 with altref)", "name": "WV: Secure Subsample SD (WebM,VP9)",
"uri": "https://storage.googleapis.com/widevine_test/vp9/sintel_1080p_vp9_altref_fullsample/sintel_1080p_vp9_altref_fullsample.mpd", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_sd.mpd",
"drm_scheme": "widevine", "drm_scheme": "widevine",
"drm_license_url": "https://widevine-proxy.appspot.com/proxy" "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
}, },
{ {
"name": "WV: Secure Subsample (WebM, VP9 without altref)", "name": "WV: Secure Subsample HD (WebM,VP9)",
"uri": "https://storage.googleapis.com/widevine_test/vp9/sintel_1080p_vp9_noaltref_subsample/sintel_1080p_vp9_noaltref_subsample.mpd", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_hd.mpd",
"drm_scheme": "widevine", "drm_scheme": "widevine",
"drm_license_url": "https://widevine-proxy.appspot.com/proxy" "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
}, },
{ {
"name": "WV: Secure Fullsample (WebM, VP9 without altref)", "name": "WV: Secure Subsample UHD (WebM,VP9)",
"uri": "https://storage.googleapis.com/widevine_test/vp9/sintel_1080p_vp9_noaltref_fullsample/sintel_1080p_vp9_noaltref_fullsample.mpd", "uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd",
"drm_scheme": "widevine", "drm_scheme": "widevine",
"drm_license_url": "https://widevine-proxy.appspot.com/proxy" "drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
} }
] ]
}, },

View file

@ -26,16 +26,17 @@ import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.StreamingDrmSessionManager; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
import com.google.android.exoplayer2.metadata.id3.ApicFrame; import com.google.android.exoplayer2.metadata.id3.ApicFrame;
import com.google.android.exoplayer2.metadata.id3.CommentFrame; import com.google.android.exoplayer2.metadata.id3.CommentFrame;
import com.google.android.exoplayer2.metadata.id3.GeobFrame; import com.google.android.exoplayer2.metadata.id3.GeobFrame;
import com.google.android.exoplayer2.metadata.id3.Id3Frame; import com.google.android.exoplayer2.metadata.id3.Id3Frame;
import com.google.android.exoplayer2.metadata.id3.PrivFrame; import com.google.android.exoplayer2.metadata.id3.PrivFrame;
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.metadata.id3.TxxxFrame; import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame;
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener; import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
@ -55,7 +56,7 @@ import java.util.Locale;
*/ */
/* package */ final class EventLogger implements ExoPlayer.EventListener, /* package */ final class EventLogger implements ExoPlayer.EventListener,
AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener,
ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener, ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener,
MetadataRenderer.Output { MetadataRenderer.Output {
private static final String TAG = "EventLogger"; private static final String TAG = "EventLogger";
@ -153,7 +154,7 @@ import java.util.Locale;
String formatSupport = getFormatSupportString( String formatSupport = getFormatSupportString(
mappedTrackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex)); mappedTrackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex));
Log.d(TAG, " " + status + " Track:" + trackIndex + ", " Log.d(TAG, " " + status + " Track:" + trackIndex + ", "
+ getFormatString(trackGroup.getFormat(trackIndex)) + Format.toLogString(trackGroup.getFormat(trackIndex))
+ ", supported=" + formatSupport); + ", supported=" + formatSupport);
} }
Log.d(TAG, " ]"); Log.d(TAG, " ]");
@ -185,7 +186,7 @@ import java.util.Locale;
String formatSupport = getFormatSupportString( String formatSupport = getFormatSupportString(
RendererCapabilities.FORMAT_UNSUPPORTED_TYPE); RendererCapabilities.FORMAT_UNSUPPORTED_TYPE);
Log.d(TAG, " " + status + " Track:" + trackIndex + ", " Log.d(TAG, " " + status + " Track:" + trackIndex + ", "
+ getFormatString(trackGroup.getFormat(trackIndex)) + Format.toLogString(trackGroup.getFormat(trackIndex))
+ ", supported=" + formatSupport); + ", supported=" + formatSupport);
} }
Log.d(TAG, " ]"); Log.d(TAG, " ]");
@ -224,7 +225,7 @@ import java.util.Locale;
@Override @Override
public void onAudioInputFormatChanged(Format format) { public void onAudioInputFormatChanged(Format format) {
Log.d(TAG, "audioFormatChanged [" + getSessionTimeString() + ", " + getFormatString(format) Log.d(TAG, "audioFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format)
+ "]"); + "]");
} }
@ -254,7 +255,7 @@ import java.util.Locale;
@Override @Override
public void onVideoInputFormatChanged(Format format) { public void onVideoInputFormatChanged(Format format) {
Log.d(TAG, "videoFormatChanged [" + getSessionTimeString() + ", " + getFormatString(format) Log.d(TAG, "videoFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format)
+ "]"); + "]");
} }
@ -279,13 +280,23 @@ import java.util.Locale;
// Do nothing. // Do nothing.
} }
// StreamingDrmSessionManager.EventListener // DefaultDrmSessionManager.EventListener
@Override @Override
public void onDrmSessionManagerError(Exception e) { public void onDrmSessionManagerError(Exception e) {
printInternalError("drmSessionManagerError", e); printInternalError("drmSessionManagerError", e);
} }
@Override
public void onDrmKeysRestored() {
Log.d(TAG, "drmKeysRestored [" + getSessionTimeString() + "]");
}
@Override
public void onDrmKeysRemoved() {
Log.d(TAG, "drmKeysRemoved [" + getSessionTimeString() + "]");
}
@Override @Override
public void onDrmKeysLoaded() { public void onDrmKeysLoaded() {
Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]"); Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]");
@ -349,10 +360,13 @@ import java.util.Locale;
private void printMetadata(Metadata metadata, String prefix) { private void printMetadata(Metadata metadata, String prefix) {
for (int i = 0; i < metadata.length(); i++) { for (int i = 0; i < metadata.length(); i++) {
Metadata.Entry entry = metadata.get(i); Metadata.Entry entry = metadata.get(i);
if (entry instanceof TxxxFrame) { if (entry instanceof TextInformationFrame) {
TxxxFrame txxxFrame = (TxxxFrame) entry; TextInformationFrame textInformationFrame = (TextInformationFrame) entry;
Log.d(TAG, prefix + String.format("%s: description=%s, value=%s", txxxFrame.id, Log.d(TAG, prefix + String.format("%s: value=%s", textInformationFrame.id,
txxxFrame.description, txxxFrame.value)); textInformationFrame.value));
} else if (entry instanceof UrlLinkFrame) {
UrlLinkFrame urlLinkFrame = (UrlLinkFrame) entry;
Log.d(TAG, prefix + String.format("%s: url=%s", urlLinkFrame.id, urlLinkFrame.url));
} else if (entry instanceof PrivFrame) { } else if (entry instanceof PrivFrame) {
PrivFrame privFrame = (PrivFrame) entry; PrivFrame privFrame = (PrivFrame) entry;
Log.d(TAG, prefix + String.format("%s: owner=%s", privFrame.id, privFrame.owner)); Log.d(TAG, prefix + String.format("%s: owner=%s", privFrame.id, privFrame.owner));
@ -364,17 +378,17 @@ import java.util.Locale;
ApicFrame apicFrame = (ApicFrame) entry; ApicFrame apicFrame = (ApicFrame) entry;
Log.d(TAG, prefix + String.format("%s: mimeType=%s, description=%s", Log.d(TAG, prefix + String.format("%s: mimeType=%s, description=%s",
apicFrame.id, apicFrame.mimeType, apicFrame.description)); apicFrame.id, apicFrame.mimeType, apicFrame.description));
} else if (entry instanceof TextInformationFrame) {
TextInformationFrame textInformationFrame = (TextInformationFrame) entry;
Log.d(TAG, prefix + String.format("%s: description=%s", textInformationFrame.id,
textInformationFrame.description));
} else if (entry instanceof CommentFrame) { } else if (entry instanceof CommentFrame) {
CommentFrame commentFrame = (CommentFrame) entry; CommentFrame commentFrame = (CommentFrame) entry;
Log.d(TAG, prefix + String.format("%s: language=%s description=%s", commentFrame.id, Log.d(TAG, prefix + String.format("%s: language=%s, description=%s", commentFrame.id,
commentFrame.language, commentFrame.description)); commentFrame.language, commentFrame.description));
} else if (entry instanceof Id3Frame) { } else if (entry instanceof Id3Frame) {
Id3Frame id3Frame = (Id3Frame) entry; Id3Frame id3Frame = (Id3Frame) entry;
Log.d(TAG, prefix + String.format("%s", id3Frame.id)); Log.d(TAG, prefix + String.format("%s", id3Frame.id));
} else if (entry instanceof EventMessage) {
EventMessage eventMessage = (EventMessage) entry;
Log.d(TAG, prefix + String.format("EMSG: scheme=%s, id=%d, value=%s",
eventMessage.schemeIdUri, eventMessage.id, eventMessage.value));
} }
} }
} }
@ -433,33 +447,6 @@ import java.util.Locale;
} }
} }
private static String getFormatString(Format format) {
if (format == null) {
return "null";
}
StringBuilder builder = new StringBuilder();
builder.append("id=").append(format.id).append(", mimeType=").append(format.sampleMimeType);
if (format.bitrate != Format.NO_VALUE) {
builder.append(", bitrate=").append(format.bitrate);
}
if (format.width != Format.NO_VALUE && format.height != Format.NO_VALUE) {
builder.append(", res=").append(format.width).append("x").append(format.height);
}
if (format.frameRate != Format.NO_VALUE) {
builder.append(", fps=").append(format.frameRate);
}
if (format.channelCount != Format.NO_VALUE) {
builder.append(", channels=").append(format.channelCount);
}
if (format.sampleRate != Format.NO_VALUE) {
builder.append(", sample_rate=").append(format.sampleRate);
}
if (format.language != null) {
builder.append(", language=").append(format.language);
}
return builder.toString();
}
private static String getTrackStatusString(TrackSelection selection, TrackGroup group, private static String getTrackStatusString(TrackSelection selection, TrackGroup group,
int trackIndex) { int trackIndex) {
return getTrackStatusString(selection != null && selection.getTrackGroup() == group return getTrackStatusString(selection != null && selection.getTrackGroup() == group

View file

@ -36,15 +36,16 @@ import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.drm.StreamingDrmSessionManager;
import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.drm.UnsupportedDrmException;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
import com.google.android.exoplayer2.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
@ -100,7 +101,6 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
} }
private Handler mainHandler; private Handler mainHandler;
private Timeline.Window window;
private EventLogger eventLogger; private EventLogger eventLogger;
private SimpleExoPlayerView simpleExoPlayerView; private SimpleExoPlayerView simpleExoPlayerView;
private LinearLayout debugRootView; private LinearLayout debugRootView;
@ -115,9 +115,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
private boolean playerNeedsSource; private boolean playerNeedsSource;
private boolean shouldAutoPlay; private boolean shouldAutoPlay;
private boolean isTimelineStatic; private int resumeWindow;
private int playerWindow; private long resumePosition;
private long playerPosition;
// Activity lifecycle // Activity lifecycle
@ -125,9 +124,9 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
shouldAutoPlay = true; shouldAutoPlay = true;
clearResumePosition();
mediaDataSourceFactory = buildDataSourceFactory(true); mediaDataSourceFactory = buildDataSourceFactory(true);
mainHandler = new Handler(); mainHandler = new Handler();
window = new Timeline.Window();
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) { if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER); CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
} }
@ -148,7 +147,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
@Override @Override
public void onNewIntent(Intent intent) { public void onNewIntent(Intent intent) {
releasePlayer(); releasePlayer();
isTimelineStatic = false; shouldAutoPlay = true;
clearResumePosition();
setIntent(intent); setIntent(intent);
} }
@ -264,7 +264,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
@SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode = @SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode =
((DemoApplication) getApplication()).useExtensionRenderers() ((DemoApplication) getApplication()).useExtensionRenderers()
? (preferExtensionDecoders ? SimpleExoPlayer.EXTENSION_RENDERER_MODE_PREFER ? (preferExtensionDecoders ? SimpleExoPlayer.EXTENSION_RENDERER_MODE_PREFER
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_ON) : SimpleExoPlayer.EXTENSION_RENDERER_MODE_ON)
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF; : SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF;
TrackSelection.Factory videoTrackSelectionFactory = TrackSelection.Factory videoTrackSelectionFactory =
new AdaptiveVideoTrackSelection.Factory(BANDWIDTH_METER); new AdaptiveVideoTrackSelection.Factory(BANDWIDTH_METER);
@ -278,16 +278,9 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
player.addListener(eventLogger); player.addListener(eventLogger);
player.setAudioDebugListener(eventLogger); player.setAudioDebugListener(eventLogger);
player.setVideoDebugListener(eventLogger); player.setVideoDebugListener(eventLogger);
player.setId3Output(eventLogger); player.setMetadataOutput(eventLogger);
simpleExoPlayerView.setPlayer(player); simpleExoPlayerView.setPlayer(player);
if (isTimelineStatic) {
if (playerPosition == C.TIME_UNSET) {
player.seekToDefaultPosition(playerWindow);
} else {
player.seekTo(playerWindow, playerPosition);
}
}
player.setPlayWhenReady(shouldAutoPlay); player.setPlayWhenReady(shouldAutoPlay);
debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper = new DebugTextViewHelper(player, debugTextView);
debugViewHelper.start(); debugViewHelper.start();
@ -324,7 +317,11 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
} }
MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0] MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
: new ConcatenatingMediaSource(mediaSources); : new ConcatenatingMediaSource(mediaSources);
player.prepare(mediaSource, !isTimelineStatic, !isTimelineStatic); boolean haveResumePosition = resumeWindow != C.INDEX_UNSET;
if (haveResumePosition) {
player.seekTo(resumeWindow, resumePosition);
}
player.prepare(mediaSource, !haveResumePosition, false);
playerNeedsSource = false; playerNeedsSource = false;
updateButtonVisibilities(); updateButtonVisibilities();
} }
@ -358,7 +355,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
} }
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl, HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl,
buildHttpDataSourceFactory(false), keyRequestProperties); buildHttpDataSourceFactory(false), keyRequestProperties);
return new StreamingDrmSessionManager<>(uuid, return new DefaultDrmSessionManager<>(uuid,
FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger); FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger);
} }
@ -367,12 +364,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
debugViewHelper.stop(); debugViewHelper.stop();
debugViewHelper = null; debugViewHelper = null;
shouldAutoPlay = player.getPlayWhenReady(); shouldAutoPlay = player.getPlayWhenReady();
playerWindow = player.getCurrentWindowIndex(); updateResumePosition();
playerPosition = C.TIME_UNSET;
Timeline timeline = player.getCurrentTimeline();
if (!timeline.isEmpty() && timeline.getWindow(playerWindow, window).isSeekable) {
playerPosition = player.getCurrentPosition();
}
player.release(); player.release();
player = null; player = null;
trackSelector = null; trackSelector = null;
@ -381,6 +373,17 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
} }
} }
private void updateResumePosition() {
resumeWindow = player.getCurrentWindowIndex();
resumePosition = player.isCurrentWindowSeekable() ? Math.max(0, player.getCurrentPosition())
: C.TIME_UNSET;
}
private void clearResumePosition() {
resumeWindow = C.INDEX_UNSET;
resumePosition = C.TIME_UNSET;
}
/** /**
* Returns a new DataSource factory. * Returns a new DataSource factory.
* *
@ -422,13 +425,17 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
@Override @Override
public void onPositionDiscontinuity() { public void onPositionDiscontinuity() {
// Do nothing. if (playerNeedsSource) {
// This will only occur if the user has performed a seek whilst in the error state. Update the
// resume position so that if the user then retries, playback will resume from the position to
// which they seeked.
updateResumePosition();
}
} }
@Override @Override
public void onTimelineChanged(Timeline timeline, Object manifest) { public void onTimelineChanged(Timeline timeline, Object manifest) {
isTimelineStatic = !timeline.isEmpty() // Do nothing.
&& !timeline.getWindow(timeline.getWindowCount() - 1, window).isDynamic;
} }
@Override @Override
@ -460,8 +467,14 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
showToast(errorString); showToast(errorString);
} }
playerNeedsSource = true; playerNeedsSource = true;
updateButtonVisibilities(); if (isBehindLiveWindow(e)) {
showControls(); clearResumePosition();
initializePlayer();
} else {
updateResumePosition();
updateButtonVisibilities();
showControls();
}
} }
@Override @Override
@ -535,4 +548,18 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show(); Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
} }
private static boolean isBehindLiveWindow(ExoPlaybackException e) {
if (e.type != ExoPlaybackException.TYPE_SOURCE) {
return false;
}
Throwable cause = e.getSourceException();
while (cause != null) {
if (cause instanceof BehindLiveWindowException) {
return true;
}
cause = cause.getCause();
}
return false;
}
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -16,8 +16,7 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- The user visible name of the application. [CHAR LIMIT=20] --> <string name="application_name">ExoPlayer</string>
<string name="application_name">ExoPlayer2 Demo</string>
<string name="video">Video</string> <string name="video">Video</string>

View file

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

View file

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

View file

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

View file

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

View file

@ -63,6 +63,7 @@ git clone git://source.ffmpeg.org/ffmpeg ffmpeg && cd ffmpeg && \
--enable-decoder=vorbis \ --enable-decoder=vorbis \
--enable-decoder=opus \ --enable-decoder=opus \
--enable-decoder=flac \ --enable-decoder=flac \
--enable-decoder=alac \
&& \ && \
make -j4 && \ make -j4 && \
make install-libs make install-libs

View file

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

View file

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

View file

@ -19,6 +19,7 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
import com.google.android.exoplayer2.decoder.SimpleDecoder; import com.google.android.exoplayer2.decoder.SimpleDecoder;
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.List; import java.util.List;
@ -88,6 +89,13 @@ import java.util.List;
if (!hasOutputFormat) { if (!hasOutputFormat) {
channelCount = ffmpegGetChannelCount(nativeContext); channelCount = ffmpegGetChannelCount(nativeContext);
sampleRate = ffmpegGetSampleRate(nativeContext); sampleRate = ffmpegGetSampleRate(nativeContext);
if (sampleRate == 0 && "alac".equals(codecName)) {
// ALAC decoder did not set the sample rate in earlier versions of FFMPEG.
// See https://trac.ffmpeg.org/ticket/6096
ParsableByteArray parsableExtraData = new ParsableByteArray(extraData);
parsableExtraData.setPosition(extraData.length - 4);
sampleRate = parsableExtraData.readUnsignedIntToInt();
}
hasOutputFormat = true; hasOutputFormat = true;
} }
outputBuffer.data.position(0); outputBuffer.data.position(0);
@ -123,6 +131,7 @@ import java.util.List;
private static byte[] getExtraData(String mimeType, List<byte[]> initializationData) { private static byte[] getExtraData(String mimeType, List<byte[]> initializationData) {
switch (mimeType) { switch (mimeType) {
case MimeTypes.AUDIO_AAC: case MimeTypes.AUDIO_AAC:
case MimeTypes.AUDIO_ALAC:
case MimeTypes.AUDIO_OPUS: case MimeTypes.AUDIO_OPUS:
return initializationData.get(0); return initializationData.get(0);
case MimeTypes.AUDIO_VORBIS: case MimeTypes.AUDIO_VORBIS:

View file

@ -92,6 +92,8 @@ public final class FfmpegLibrary {
return "amrwb"; return "amrwb";
case MimeTypes.AUDIO_FLAC: case MimeTypes.AUDIO_FLAC:
return "flac"; return "flac";
case MimeTypes.AUDIO_ALAC:
return "alac";
default: default:
return null; return null;
} }

View file

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

View file

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

View file

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

View file

@ -261,7 +261,7 @@ public class OkHttpDataSource implements HttpDataSource {
private Request makeRequest(DataSpec dataSpec) { private Request makeRequest(DataSpec dataSpec) {
long position = dataSpec.position; long position = dataSpec.position;
long length = dataSpec.length; long length = dataSpec.length;
boolean allowGzip = (dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) != 0; boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
HttpUrl url = HttpUrl.parse(dataSpec.uri.toString()); HttpUrl url = HttpUrl.parse(dataSpec.uri.toString());
Request.Builder builder = new Request.Builder().url(url); Request.Builder builder = new Request.Builder().url(url);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,7 @@
# Accessed via reflection in SubtitleDecoderFactory.DEFAULT
-keepclassmembers class com.google.android.exoplayer2.text.cea.Cea608Decoder {
public <init>(java.lang.String, int);
}
-keepclassmembers class com.google.android.exoplayer2.text.cea.Cea708Decoder {
public <init>(int);
}

View file

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

View file

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

View file

@ -0,0 +1,229 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.drm;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.when;
import android.test.InstrumentationTestCase;
import android.test.MoreAsserts;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.source.dash.manifest.Period;
import com.google.android.exoplayer2.source.dash.manifest.Representation;
import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import java.util.Arrays;
import java.util.HashMap;
import org.mockito.Mock;
/**
* Tests {@link OfflineLicenseHelper}.
*/
public class OfflineLicenseHelperTest extends InstrumentationTestCase {
private OfflineLicenseHelper<?> offlineLicenseHelper;
@Mock private HttpDataSource httpDataSource;
@Mock private MediaDrmCallback mediaDrmCallback;
@Mock private ExoMediaDrm<ExoMediaCrypto> mediaDrm;
@Override
protected void setUp() throws Exception {
TestUtil.setUpMockito(this);
when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3});
offlineLicenseHelper = new OfflineLicenseHelper<>(mediaDrm, mediaDrmCallback, null);
}
@Override
protected void tearDown() throws Exception {
offlineLicenseHelper.releaseResources();
}
public void testDownloadRenewReleaseKey() throws Exception {
DashManifest manifest = newDashManifestWithAllElements();
setStubLicenseAndPlaybackDurationValues(1000, 200);
byte[] keySetId = {2, 5, 8};
setStubKeySetId(keySetId);
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
assertOfflineLicenseKeySetIdEqual(keySetId, offlineLicenseKeySetId);
byte[] keySetId2 = {6, 7, 0, 1, 4};
setStubKeySetId(keySetId2);
byte[] offlineLicenseKeySetId2 = offlineLicenseHelper.renew(offlineLicenseKeySetId);
assertOfflineLicenseKeySetIdEqual(keySetId2, offlineLicenseKeySetId2);
offlineLicenseHelper.release(offlineLicenseKeySetId2);
}
public void testDownloadFailsIfThereIsNoInitData() throws Exception {
setDefaultStubValues();
DashManifest manifest =
newDashManifest(newPeriods(newAdaptationSets(newRepresentations(null /*no init data*/))));
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
assertNull(offlineLicenseKeySetId);
}
public void testDownloadFailsIfThereIsNoRepresentation() throws Exception {
setDefaultStubValues();
DashManifest manifest = newDashManifest(newPeriods(newAdaptationSets(/*no representation*/)));
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
assertNull(offlineLicenseKeySetId);
}
public void testDownloadFailsIfThereIsNoAdaptationSet() throws Exception {
setDefaultStubValues();
DashManifest manifest = newDashManifest(newPeriods(/*no adaptation set*/));
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
assertNull(offlineLicenseKeySetId);
}
public void testDownloadFailsIfThereIsNoPeriod() throws Exception {
setDefaultStubValues();
DashManifest manifest = newDashManifest(/*no periods*/);
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
assertNull(offlineLicenseKeySetId);
}
public void testDownloadFailsIfNoKeySetIdIsReturned() throws Exception {
setStubLicenseAndPlaybackDurationValues(1000, 200);
DashManifest manifest = newDashManifestWithAllElements();
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
assertNull(offlineLicenseKeySetId);
}
public void testDownloadDoesNotFailIfDurationNotAvailable() throws Exception {
setDefaultStubKeySetId();
DashManifest manifest = newDashManifestWithAllElements();
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
assertNotNull(offlineLicenseKeySetId);
}
public void testGetLicenseDurationRemainingSec() throws Exception {
long licenseDuration = 1000;
int playbackDuration = 200;
setStubLicenseAndPlaybackDurationValues(licenseDuration, playbackDuration);
setDefaultStubKeySetId();
DashManifest manifest = newDashManifestWithAllElements();
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
Pair<Long, Long> licenseDurationRemainingSec = offlineLicenseHelper
.getLicenseDurationRemainingSec(offlineLicenseKeySetId);
assertEquals(licenseDuration, (long) licenseDurationRemainingSec.first);
assertEquals(playbackDuration, (long) licenseDurationRemainingSec.second);
}
public void testGetLicenseDurationRemainingSecExpiredLicense() throws Exception {
long licenseDuration = 0;
int playbackDuration = 0;
setStubLicenseAndPlaybackDurationValues(licenseDuration, playbackDuration);
setDefaultStubKeySetId();
DashManifest manifest = newDashManifestWithAllElements();
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
Pair<Long, Long> licenseDurationRemainingSec = offlineLicenseHelper
.getLicenseDurationRemainingSec(offlineLicenseKeySetId);
assertEquals(licenseDuration, (long) licenseDurationRemainingSec.first);
assertEquals(playbackDuration, (long) licenseDurationRemainingSec.second);
}
private void setDefaultStubValues()
throws android.media.NotProvisionedException, android.media.DeniedByServerException {
setDefaultStubKeySetId();
setStubLicenseAndPlaybackDurationValues(1000, 200);
}
private void setDefaultStubKeySetId()
throws android.media.NotProvisionedException, android.media.DeniedByServerException {
setStubKeySetId(new byte[] {2, 5, 8});
}
private void setStubKeySetId(byte[] keySetId)
throws android.media.NotProvisionedException, android.media.DeniedByServerException {
when(mediaDrm.provideKeyResponse(any(byte[].class), any(byte[].class))).thenReturn(keySetId);
}
private static void assertOfflineLicenseKeySetIdEqual(
byte[] expectedKeySetId, byte[] actualKeySetId) throws Exception {
assertNotNull(actualKeySetId);
MoreAsserts.assertEquals(expectedKeySetId, actualKeySetId);
}
private void setStubLicenseAndPlaybackDurationValues(long licenseDuration,
long playbackDuration) {
HashMap<String, String> keyStatus = new HashMap<>();
keyStatus.put(WidevineUtil.PROPERTY_LICENSE_DURATION_REMAINING,
String.valueOf(licenseDuration));
keyStatus.put(WidevineUtil.PROPERTY_PLAYBACK_DURATION_REMAINING,
String.valueOf(playbackDuration));
when(mediaDrm.queryKeyStatus(any(byte[].class))).thenReturn(keyStatus);
}
private static DashManifest newDashManifestWithAllElements() {
return newDashManifest(newPeriods(newAdaptationSets(newRepresentations(newDrmInitData()))));
}
private static DashManifest newDashManifest(Period... periods) {
return new DashManifest(0, 0, 0, false, 0, 0, 0, null, null, Arrays.asList(periods));
}
private static Period newPeriods(AdaptationSet... adaptationSets) {
return new Period("", 0, Arrays.asList(adaptationSets));
}
private static AdaptationSet newAdaptationSets(Representation... representations) {
return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations), null);
}
private static Representation newRepresentations(DrmInitData drmInitData) {
Format format = Format.createVideoSampleFormat("", "", "", 0, 0, 0, 0, 0, null, drmInitData);
return Representation.newInstance("", 0, format, "", new SingleSegmentBase());
}
private static DrmInitData newDrmInitData() {
return new DrmInitData(new SchemeData(C.WIDEVINE_UUID, "mimeType",
new byte[]{1, 4, 7, 0, 3, 6}));
}
}

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.extractor.mp4; package com.google.android.exoplayer2.extractor.mp4;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
@ -24,13 +25,21 @@ import com.google.android.exoplayer2.testutil.TestUtil;
*/ */
public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase { public final class FragmentedMp4ExtractorTest extends InstrumentationTestCase {
private static final TestUtil.ExtractorFactory EXTRACTOR_FACTORY =
new TestUtil.ExtractorFactory() {
@Override
public Extractor create() {
return new FragmentedMp4Extractor();
}
};
public void testSample() throws Exception { public void testSample() throws Exception {
TestUtil.assertOutput(new TestUtil.ExtractorFactory() { TestUtil.assertOutput(EXTRACTOR_FACTORY, "mp4/sample_fragmented.mp4", getInstrumentation());
@Override }
public Extractor create() {
return new FragmentedMp4Extractor(); public void testAtomWithZeroSize() throws Exception {
} TestUtil.assertThrows(EXTRACTOR_FACTORY, "mp4/sample_fragmented_zero_size_atom.mp4",
}, "mp4/sample_fragmented.mp4", getInstrumentation()); getInstrumentation(), ParserException.class);
} }
} }

View file

@ -16,9 +16,9 @@
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import com.google.android.exoplayer2.extractor.ExtractorOutput; 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.testutil.FakeExtractorOutput;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;

View file

@ -21,7 +21,6 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.Extractor;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.PositionHolder; 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.TrackOutput;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
@ -30,6 +29,7 @@ import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
import com.google.android.exoplayer2.testutil.FakeTrackOutput; import com.google.android.exoplayer2.testutil.FakeTrackOutput;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.util.Random; import java.util.Random;

View file

@ -0,0 +1,51 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata.emsg;
import android.test.MoreAsserts;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import java.nio.ByteBuffer;
import junit.framework.TestCase;
/**
* Test for {@link EventMessageDecoder}.
*/
public final class EventMessageDecoderTest extends TestCase {
public void testDecodeEventMessage() {
byte[] rawEmsgBody = new byte[] {
117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test"
49, 50, 51, 0, // value = "123"
0, 0, -69, -128, // timescale = 48000
0, 0, 0, 0, // presentation_time_delta (ignored) = 0
0, 2, 50, -128, // event_duration = 144000
0, 15, 67, -45, // id = 1000403
0, 1, 2, 3, 4}; // message_data = {0, 1, 2, 3, 4}
EventMessageDecoder decoder = new EventMessageDecoder();
MetadataInputBuffer buffer = new MetadataInputBuffer();
buffer.data = ByteBuffer.allocate(rawEmsgBody.length).put(rawEmsgBody);
Metadata metadata = decoder.decode(buffer);
assertEquals(1, metadata.length());
EventMessage eventMessage = (EventMessage) metadata.get(0);
assertEquals("urn:test", eventMessage.schemeIdUri);
assertEquals("123", eventMessage.value);
assertEquals(3000, eventMessage.durationMs);
assertEquals(1000403, eventMessage.id);
MoreAsserts.assertEquals(new byte[] {0, 1, 2, 3, 4}, eventMessage.messageData);
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata.emsg;
import android.os.Parcel;
import junit.framework.TestCase;
/**
* Test for {@link EventMessage}.
*/
public final class EventMessageTest extends TestCase {
public void testEventMessageParcelable() {
EventMessage eventMessage = new EventMessage("urn:test", "123", 3000, 1000403,
new byte[] {0, 1, 2, 3, 4});
// Write to parcel.
Parcel parcel = Parcel.obtain();
eventMessage.writeToParcel(parcel, 0);
// Create from parcel.
parcel.setDataPosition(0);
EventMessage fromParcelEventMessage = EventMessage.CREATOR.createFromParcel(parcel);
// Assert equals.
assertEquals(eventMessage, fromParcelEventMessage);
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import junit.framework.TestCase;
/**
* Test for {@link ChapterFrame}.
*/
public final class ChapterFrameTest extends TestCase {
public void testParcelable() {
Id3Frame[] subFrames = new Id3Frame[] {
new TextInformationFrame("TIT2", null, "title"),
new UrlLinkFrame("WXXX", "description", "url")
};
ChapterFrame chapterFrameToParcel = new ChapterFrame("id", 0, 1, 2, 3, subFrames);
Parcel parcel = Parcel.obtain();
chapterFrameToParcel.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
ChapterFrame chapterFrameFromParcel = ChapterFrame.CREATOR.createFromParcel(parcel);
assertEquals(chapterFrameToParcel, chapterFrameFromParcel);
parcel.recycle();
}
}

View file

@ -0,0 +1,45 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import junit.framework.TestCase;
/**
* Test for {@link ChapterTocFrame}.
*/
public final class ChapterTocFrameTest extends TestCase {
public void testParcelable() {
String[] children = new String[] {"child0", "child1"};
Id3Frame[] subFrames = new Id3Frame[] {
new TextInformationFrame("TIT2", null, "title"),
new UrlLinkFrame("WXXX", "description", "url")
};
ChapterTocFrame chapterTocFrameToParcel = new ChapterTocFrame("id", false, true, children,
subFrames);
Parcel parcel = Parcel.obtain();
chapterTocFrameToParcel.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
ChapterTocFrame chapterTocFrameFromParcel = ChapterTocFrame.CREATOR.createFromParcel(parcel);
assertEquals(chapterTocFrameToParcel, chapterTocFrameFromParcel);
parcel.recycle();
}
}

View file

@ -21,9 +21,9 @@ import com.google.android.exoplayer2.metadata.MetadataDecoderException;
import junit.framework.TestCase; import junit.framework.TestCase;
/** /**
* Test for {@link Id3Decoder} * Test for {@link Id3Decoder}.
*/ */
public class Id3DecoderTest extends TestCase { public final class Id3DecoderTest extends TestCase {
public void testDecodeTxxxFrame() throws MetadataDecoderException { public void testDecodeTxxxFrame() throws MetadataDecoderException {
byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 41, 84, 88, 88, 88, 0, 0, 0, 31, 0, 0, byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 41, 84, 88, 88, 88, 0, 0, 0, 31, 0, 0,
@ -32,9 +32,10 @@ public class Id3DecoderTest extends TestCase {
Id3Decoder decoder = new Id3Decoder(); Id3Decoder decoder = new Id3Decoder();
Metadata metadata = decoder.decode(rawId3, rawId3.length); Metadata metadata = decoder.decode(rawId3, rawId3.length);
assertEquals(1, metadata.length()); assertEquals(1, metadata.length());
TxxxFrame txxxFrame = (TxxxFrame) metadata.get(0); TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0);
assertEquals("", txxxFrame.description); assertEquals("TXXX", textInformationFrame.id);
assertEquals("mdialog_VINDICO1527664_start", txxxFrame.value); assertEquals("", textInformationFrame.description);
assertEquals("mdialog_VINDICO1527664_start", textInformationFrame.value);
} }
public void testDecodeApicFrame() throws MetadataDecoderException { public void testDecodeApicFrame() throws MetadataDecoderException {
@ -60,7 +61,19 @@ public class Id3DecoderTest extends TestCase {
assertEquals(1, metadata.length()); assertEquals(1, metadata.length());
TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0); TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0);
assertEquals("TIT2", textInformationFrame.id); assertEquals("TIT2", textInformationFrame.id);
assertEquals("Hello World", textInformationFrame.description); assertNull(textInformationFrame.description);
assertEquals("Hello World", textInformationFrame.value);
}
public void testDecodePrivFrame() throws MetadataDecoderException {
byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 19, 80, 82, 73, 86, 0, 0, 0, 9, 0, 0,
116, 101, 115, 116, 0, 1, 2, 3, 4};
Id3Decoder decoder = new Id3Decoder();
Metadata metadata = decoder.decode(rawId3, rawId3.length);
assertEquals(1, metadata.length());
PrivFrame privFrame = (PrivFrame) metadata.get(0);
assertEquals("test", privFrame.owner);
MoreAsserts.assertEquals(new byte[] {1, 2, 3, 4}, privFrame.privateData);
} }
} }

View file

@ -0,0 +1,173 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata.scte35;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataDecoderException;
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
import com.google.android.exoplayer2.util.TimestampAdjuster;
import java.nio.ByteBuffer;
import java.util.List;
import junit.framework.TestCase;
/**
* Test for {@link SpliceInfoDecoder}.
*/
public final class SpliceInfoDecoderTest extends TestCase {
private SpliceInfoDecoder decoder;
private MetadataInputBuffer inputBuffer;
@Override
public void setUp() {
decoder = new SpliceInfoDecoder();
inputBuffer = new MetadataInputBuffer();
}
public void testWrappedAroundTimeSignalCommand() throws MetadataDecoderException {
byte[] rawTimeSignalSection = new byte[] {
0, // table_id.
(byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4).
0x14, // section_length(8).
0x00, // protocol_version.
0x00, // encrypted_packet, encryption_algorithm, pts_adjustment(1).
0x00, 0x00, 0x00, 0x00, // pts_adjustment(32).
0x00, // cw_index.
0x00, // tier(8).
0x00, // tier(4), splice_command_length(4).
0x05, // splice_command_length(8).
0x06, // splice_command_type = time_signal.
// Start of splice_time().
(byte) 0x80, // time_specified_flag, reserved, pts_time(1).
0x52, 0x03, 0x02, (byte) 0x8f, // pts_time(32). PTS for a second after playback position.
0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction).
// The playback position is 57:15:58.43 approximately.
// With this offset, the playback position pts before wrapping is 0x451ebf851.
Metadata metadata = feedInputBuffer(rawTimeSignalSection, 0x3000000000L, -0x50000L);
assertEquals(1, metadata.length());
assertEquals(removePtsConversionPrecisionError(0x3001000000L, inputBuffer.subsampleOffsetUs),
((TimeSignalCommand) metadata.get(0)).playbackPositionUs);
}
public void test2SpliceInsertCommands() throws MetadataDecoderException {
byte[] rawSpliceInsertCommand1 = new byte[] {
0, // table_id.
(byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4).
0x19, // section_length(8).
0x00, // protocol_version.
0x00, // encrypted_packet, encryption_algorithm, pts_adjustment(1).
0x00, 0x00, 0x00, 0x00, // pts_adjustment(32).
0x00, // cw_index.
0x00, // tier(8).
0x00, // tier(4), splice_command_length(4).
0x0e, // splice_command_length(8).
0x05, // splice_command_type = splice_insert.
// Start of splice_insert().
0x00, 0x00, 0x00, 0x42, // splice_event_id.
0x00, // splice_event_cancel_indicator, reserved.
0x40, // out_of_network_indicator, program_splice_flag, duration_flag,
// splice_immediate_flag, reserved.
// start of splice_time().
(byte) 0x80, // time_specified_flag, reserved, pts_time(1).
0x00, 0x00, 0x00, 0x00, // PTS for playback position 3s.
0x00, 0x10, // unique_program_id.
0x01, // avail_num.
0x02, // avails_expected.
0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction).
Metadata metadata = feedInputBuffer(rawSpliceInsertCommand1, 2000000, 3000000);
assertEquals(1, metadata.length());
SpliceInsertCommand command = (SpliceInsertCommand) metadata.get(0);
assertEquals(66, command.spliceEventId);
assertFalse(command.spliceEventCancelIndicator);
assertFalse(command.outOfNetworkIndicator);
assertTrue(command.programSpliceFlag);
assertFalse(command.spliceImmediateFlag);
assertEquals(3000000, command.programSplicePlaybackPositionUs);
assertEquals(C.TIME_UNSET, command.breakDuration);
assertEquals(16, command.uniqueProgramId);
assertEquals(1, command.availNum);
assertEquals(2, command.availsExpected);
byte[] rawSpliceInsertCommand2 = new byte[] {
0, // table_id.
(byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4).
0x22, // section_length(8).
0x00, // protocol_version.
0x00, // encrypted_packet, encryption_algorithm, pts_adjustment(1).
0x00, 0x00, 0x00, 0x00, // pts_adjustment(32).
0x00, // cw_index.
0x00, // tier(8).
0x00, // tier(4), splice_command_length(4).
0x13, // splice_command_length(8).
0x05, // splice_command_type = splice_insert.
// Start of splice_insert().
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // splice_event_id.
0x00, // splice_event_cancel_indicator, reserved.
0x00, // out_of_network_indicator, program_splice_flag, duration_flag,
// splice_immediate_flag, reserved.
0x02, // component_count.
0x10, // component_tag.
// start of splice_time().
(byte) 0x81, // time_specified_flag, reserved, pts_time(1).
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // PTS for playback position 10s.
// start of splice_time().
0x11, // component_tag.
0x00, // time_specified_flag, reserved.
0x00, 0x20, // unique_program_id.
0x01, // avail_num.
0x02, // avails_expected.
0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction).
// By changing the subsample offset we force adjuster reconstruction.
long subsampleOffset = 1000011;
metadata = feedInputBuffer(rawSpliceInsertCommand2, 1000000, subsampleOffset);
assertEquals(1, metadata.length());
command = (SpliceInsertCommand) metadata.get(0);
assertEquals(0xffffffffL, command.spliceEventId);
assertFalse(command.spliceEventCancelIndicator);
assertFalse(command.outOfNetworkIndicator);
assertFalse(command.programSpliceFlag);
assertFalse(command.spliceImmediateFlag);
assertEquals(C.TIME_UNSET, command.programSplicePlaybackPositionUs);
assertEquals(C.TIME_UNSET, command.breakDuration);
List<SpliceInsertCommand.ComponentSplice> componentSplices = command.componentSpliceList;
assertEquals(2, componentSplices.size());
assertEquals(16, componentSplices.get(0).componentTag);
assertEquals(1000000, componentSplices.get(0).componentSplicePlaybackPositionUs);
assertEquals(17, componentSplices.get(1).componentTag);
assertEquals(C.TIME_UNSET, componentSplices.get(1).componentSplicePts);
assertEquals(32, command.uniqueProgramId);
assertEquals(1, command.availNum);
assertEquals(2, command.availsExpected);
}
private Metadata feedInputBuffer(byte[] data, long timeUs, long subsampleOffset)
throws MetadataDecoderException{
inputBuffer.clear();
inputBuffer.data = ByteBuffer.allocate(data.length).put(data);
inputBuffer.timeUs = timeUs;
inputBuffer.subsampleOffsetUs = subsampleOffset;
return decoder.decode(inputBuffer);
}
private static long removePtsConversionPrecisionError(long timeUs, long offsetUs) {
return TimestampAdjuster.ptsToUs(TimestampAdjuster.usToPts(timeUs - offsetUs)) + offsetUs;
}
}

View file

@ -0,0 +1,143 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source;
import static org.mockito.Mockito.doAnswer;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Timeline.Period;
import com.google.android.exoplayer2.Timeline.Window;
import com.google.android.exoplayer2.source.MediaSource.Listener;
import com.google.android.exoplayer2.testutil.TestUtil;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
/**
* Unit tests for {@link ClippingMediaSource}.
*/
public final class ClippingMediaSourceTest extends InstrumentationTestCase {
private static final long TEST_PERIOD_DURATION_US = 1000000;
private static final long TEST_CLIP_AMOUNT_US = 300000;
@Mock
private MediaSource mockMediaSource;
private Timeline clippedTimeline;
private Window window;
private Period period;
@Override
protected void setUp() throws Exception {
TestUtil.setUpMockito(this);
window = new Timeline.Window();
period = new Timeline.Period();
}
public void testNoClipping() {
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true);
Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US);
assertEquals(1, clippedTimeline.getWindowCount());
assertEquals(1, clippedTimeline.getPeriodCount());
assertEquals(TEST_PERIOD_DURATION_US, clippedTimeline.getWindow(0, window).getDurationUs());
assertEquals(TEST_PERIOD_DURATION_US, clippedTimeline.getPeriod(0, period).getDurationUs());
}
public void testClippingUnseekableWindowThrows() {
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false);
// If the unseekable window isn't clipped, clipping succeeds.
getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US);
try {
// If the unseekable window is clipped, clipping fails.
getClippedTimeline(timeline, 1, TEST_PERIOD_DURATION_US);
fail("Expected clipping to fail.");
} catch (IllegalArgumentException e) {
// Expected.
}
}
public void testClippingStart() {
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true);
Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US,
TEST_PERIOD_DURATION_US);
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
clippedTimeline.getWindow(0, window).getDurationUs());
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
clippedTimeline.getPeriod(0, period).getDurationUs());
}
public void testClippingEnd() {
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true);
Timeline clippedTimeline = getClippedTimeline(timeline, 0,
TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
clippedTimeline.getWindow(0, window).getDurationUs());
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
clippedTimeline.getPeriod(0, period).getDurationUs());
}
public void testClippingStartAndEnd() {
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true);
Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US,
TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2);
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3,
clippedTimeline.getWindow(0, window).getDurationUs());
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3,
clippedTimeline.getPeriod(0, period).getDurationUs());
}
/**
* Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline.
*/
private Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) {
mockMediaSourceSourceWithTimeline(timeline);
new ClippingMediaSource(mockMediaSource, startMs, endMs).prepareSource(null, true,
new Listener() {
@Override
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
clippedTimeline = timeline;
}
});
return clippedTimeline;
}
/**
* Returns a mock {@link MediaSource} with the specified {@link Timeline} in its source info.
*/
private MediaSource mockMediaSourceSourceWithTimeline(final Timeline timeline) {
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
MediaSource.Listener listener = (MediaSource.Listener) invocation.getArguments()[2];
listener.onSourceInfoRefreshed(timeline, null);
return null;
}
}).when(mockMediaSource).prepareSource(Mockito.any(ExoPlayer.class), Mockito.anyBoolean(),
Mockito.any(MediaSource.Listener.class));
return mockMediaSource;
}
}

View file

@ -20,6 +20,8 @@ import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.List;
/** /**
* Unit tests for {@link DashManifestParser}. * Unit tests for {@link DashManifestParser}.
@ -70,34 +72,57 @@ public class DashManifestParserTest extends InstrumentationTestCase {
} }
public void testParseCea608AccessibilityChannel() { public void testParseCea608AccessibilityChannel() {
assertEquals(1, DashManifestParser.parseCea608AccessibilityChannel("CC1=eng")); assertEquals(1, DashManifestParser.parseCea608AccessibilityChannel(
assertEquals(2, DashManifestParser.parseCea608AccessibilityChannel("CC2=eng")); buildCea608AccessibilityDescriptors("CC1=eng")));
assertEquals(3, DashManifestParser.parseCea608AccessibilityChannel("CC3=eng")); assertEquals(2, DashManifestParser.parseCea608AccessibilityChannel(
assertEquals(4, DashManifestParser.parseCea608AccessibilityChannel("CC4=eng")); buildCea608AccessibilityDescriptors("CC2=eng")));
assertEquals(3, DashManifestParser.parseCea608AccessibilityChannel(
buildCea608AccessibilityDescriptors("CC3=eng")));
assertEquals(4, DashManifestParser.parseCea608AccessibilityChannel(
buildCea608AccessibilityDescriptors("CC4=eng")));
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(null)); assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel("")); buildCea608AccessibilityDescriptors(null)));
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel("CC0=eng")); assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel("CC5=eng")); buildCea608AccessibilityDescriptors("")));
assertEquals(Format.NO_VALUE, assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(
DashManifestParser.parseCea608AccessibilityChannel("Wrong format")); buildCea608AccessibilityDescriptors("CC0=eng")));
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(
buildCea608AccessibilityDescriptors("CC5=eng")));
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(
buildCea608AccessibilityDescriptors("Wrong format")));
} }
public void testParseCea708AccessibilityChannel() { public void testParseCea708AccessibilityChannel() {
assertEquals(1, DashManifestParser.parseCea708AccessibilityChannel("1=lang:eng")); assertEquals(1, DashManifestParser.parseCea708AccessibilityChannel(
assertEquals(2, DashManifestParser.parseCea708AccessibilityChannel("2=lang:eng")); buildCea708AccessibilityDescriptors("1=lang:eng")));
assertEquals(3, DashManifestParser.parseCea708AccessibilityChannel("3=lang:eng")); assertEquals(2, DashManifestParser.parseCea708AccessibilityChannel(
assertEquals(62, DashManifestParser.parseCea708AccessibilityChannel("62=lang:eng")); buildCea708AccessibilityDescriptors("2=lang:eng")));
assertEquals(63, DashManifestParser.parseCea708AccessibilityChannel("63=lang:eng")); assertEquals(3, DashManifestParser.parseCea708AccessibilityChannel(
buildCea708AccessibilityDescriptors("3=lang:eng")));
assertEquals(62, DashManifestParser.parseCea708AccessibilityChannel(
buildCea708AccessibilityDescriptors("62=lang:eng")));
assertEquals(63, DashManifestParser.parseCea708AccessibilityChannel(
buildCea708AccessibilityDescriptors("63=lang:eng")));
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(null)); assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel("")); buildCea708AccessibilityDescriptors(null)));
assertEquals(Format.NO_VALUE, assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(
DashManifestParser.parseCea708AccessibilityChannel("0=lang:eng")); buildCea708AccessibilityDescriptors("")));
assertEquals(Format.NO_VALUE, assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(
DashManifestParser.parseCea708AccessibilityChannel("64=lang:eng")); buildCea708AccessibilityDescriptors("0=lang:eng")));
assertEquals(Format.NO_VALUE, assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(
DashManifestParser.parseCea708AccessibilityChannel("Wrong format")); buildCea708AccessibilityDescriptors("64=lang:eng")));
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(
buildCea708AccessibilityDescriptors("Wrong format")));
}
private static List<SchemeValuePair> buildCea608AccessibilityDescriptors(String value) {
return Collections.singletonList(new SchemeValuePair("urn:scte:dash:cc:cea-608:2015", value));
}
private static List<SchemeValuePair> buildCea708AccessibilityDescriptors(String value) {
return Collections.singletonList(new SchemeValuePair("urn:scte:dash:cc:cea-708:2015", value));
} }
} }

View file

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

View file

@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.hls.playlist;
import android.net.Uri; import android.net.Uri;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
@ -29,70 +30,86 @@ import junit.framework.TestCase;
*/ */
public class HlsMasterPlaylistParserTest extends TestCase { public class HlsMasterPlaylistParserTest extends TestCase {
public void testParseMasterPlaylist() { private static final String PLAYLIST_URI = "https://example.com/test.m3u8";
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
String playlistString = "#EXTM3U\n" private static final String MASTER_PLAYLIST = " #EXTM3U \n"
+ "\n" + "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+ "http://example.com/low.m3u8\n" + "http://example.com/low.m3u8\n"
+ "\n" + "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n" + "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n"
+ "http://example.com/spaces_in_codecs.m3u8\n" + "http://example.com/spaces_in_codecs.m3u8\n"
+ "\n" + "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=384x160\n" + "#EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=384x160\n"
+ "http://example.com/mid.m3u8\n" + "http://example.com/mid.m3u8\n"
+ "\n" + "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=7680000\n" + "#EXT-X-STREAM-INF:BANDWIDTH=7680000\n"
+ "http://example.com/hi.m3u8\n" + "http://example.com/hi.m3u8\n"
+ "\n" + "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n" + "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n"
+ "http://example.com/audio-only.m3u8"; + "http://example.com/audio-only.m3u8";
ByteArrayInputStream inputStream = new ByteArrayInputStream(
playlistString.getBytes(Charset.forName(C.UTF8_NAME))); private static final String PLAYLIST_WITH_INVALID_HEADER = "#EXTMU3\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
+ "http://example.com/low.m3u8\n";
public void testParseMasterPlaylist() throws IOException{
HlsPlaylist playlist = parsePlaylist(PLAYLIST_URI, MASTER_PLAYLIST);
assertNotNull(playlist);
assertEquals(HlsPlaylist.TYPE_MASTER, playlist.type);
HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist;
List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
assertNotNull(variants);
assertEquals(5, variants.size());
assertEquals(1280000, variants.get(0).format.bitrate);
assertNotNull(variants.get(0).format.codecs);
assertEquals("mp4a.40.2,avc1.66.30", variants.get(0).format.codecs);
assertEquals(304, variants.get(0).format.width);
assertEquals(128, variants.get(0).format.height);
assertEquals("http://example.com/low.m3u8", variants.get(0).url);
assertEquals(1280000, variants.get(1).format.bitrate);
assertNotNull(variants.get(1).format.codecs);
assertEquals("mp4a.40.2 , avc1.66.30 ", variants.get(1).format.codecs);
assertEquals("http://example.com/spaces_in_codecs.m3u8", variants.get(1).url);
assertEquals(2560000, variants.get(2).format.bitrate);
assertEquals(null, variants.get(2).format.codecs);
assertEquals(384, variants.get(2).format.width);
assertEquals(160, variants.get(2).format.height);
assertEquals("http://example.com/mid.m3u8", variants.get(2).url);
assertEquals(7680000, variants.get(3).format.bitrate);
assertEquals(null, variants.get(3).format.codecs);
assertEquals(Format.NO_VALUE, variants.get(3).format.width);
assertEquals(Format.NO_VALUE, variants.get(3).format.height);
assertEquals("http://example.com/hi.m3u8", variants.get(3).url);
assertEquals(65000, variants.get(4).format.bitrate);
assertNotNull(variants.get(4).format.codecs);
assertEquals("mp4a.40.5", variants.get(4).format.codecs);
assertEquals(Format.NO_VALUE, variants.get(4).format.width);
assertEquals(Format.NO_VALUE, variants.get(4).format.height);
assertEquals("http://example.com/audio-only.m3u8", variants.get(4).url);
}
public void testPlaylistWithInvalidHeader() throws IOException {
try { try {
HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream); parsePlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER);
assertNotNull(playlist); fail("Expected exception not thrown.");
assertEquals(HlsPlaylist.TYPE_MASTER, playlist.type); } catch (ParserException e) {
// Expected due to invalid header.
HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist;
List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
assertNotNull(variants);
assertEquals(5, variants.size());
assertEquals(1280000, variants.get(0).format.bitrate);
assertNotNull(variants.get(0).format.codecs);
assertEquals("mp4a.40.2,avc1.66.30", variants.get(0).format.codecs);
assertEquals(304, variants.get(0).format.width);
assertEquals(128, variants.get(0).format.height);
assertEquals("http://example.com/low.m3u8", variants.get(0).url);
assertEquals(1280000, variants.get(1).format.bitrate);
assertNotNull(variants.get(1).format.codecs);
assertEquals("mp4a.40.2 , avc1.66.30 ", variants.get(1).format.codecs);
assertEquals("http://example.com/spaces_in_codecs.m3u8", variants.get(1).url);
assertEquals(2560000, variants.get(2).format.bitrate);
assertEquals(null, variants.get(2).format.codecs);
assertEquals(384, variants.get(2).format.width);
assertEquals(160, variants.get(2).format.height);
assertEquals("http://example.com/mid.m3u8", variants.get(2).url);
assertEquals(7680000, variants.get(3).format.bitrate);
assertEquals(null, variants.get(3).format.codecs);
assertEquals(Format.NO_VALUE, variants.get(3).format.width);
assertEquals(Format.NO_VALUE, variants.get(3).format.height);
assertEquals("http://example.com/hi.m3u8", variants.get(3).url);
assertEquals(65000, variants.get(4).format.bitrate);
assertNotNull(variants.get(4).format.codecs);
assertEquals("mp4a.40.5", variants.get(4).format.codecs);
assertEquals(Format.NO_VALUE, variants.get(4).format.width);
assertEquals(Format.NO_VALUE, variants.get(4).format.height);
assertEquals("http://example.com/audio-only.m3u8", variants.get(4).url);
} catch (IOException exception) {
fail(exception.getMessage());
} }
} }
private static HlsPlaylist parsePlaylist(String uri, String playlistString) throws IOException {
Uri playlistUri = Uri.parse(uri);
ByteArrayInputStream inputStream = new ByteArrayInputStream(
playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
return new HlsPlaylistParser().parse(playlistUri, inputStream);
}
} }

View file

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls.playlist;
import android.net.Uri; import android.net.Uri;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -34,6 +35,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
String playlistString = "#EXTM3U\n" String playlistString = "#EXTM3U\n"
+ "#EXT-X-VERSION:3\n" + "#EXT-X-VERSION:3\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-TARGETDURATION:8\n" + "#EXT-X-TARGETDURATION:8\n"
+ "#EXT-X-MEDIA-SEQUENCE:2679\n" + "#EXT-X-MEDIA-SEQUENCE:2679\n"
+ "#EXT-X-DISCONTINUITY-SEQUENCE:4\n" + "#EXT-X-DISCONTINUITY-SEQUENCE:4\n"
@ -70,62 +72,68 @@ public class HlsMediaPlaylistParserTest extends TestCase {
assertEquals(HlsPlaylist.TYPE_MEDIA, playlist.type); assertEquals(HlsPlaylist.TYPE_MEDIA, playlist.type);
HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist; HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
assertEquals(HlsMediaPlaylist.PLAYLIST_TYPE_VOD, mediaPlaylist.playlistType);
assertEquals(2679, mediaPlaylist.mediaSequence); assertEquals(2679, mediaPlaylist.mediaSequence);
assertEquals(3, mediaPlaylist.version); assertEquals(3, mediaPlaylist.version);
assertEquals(true, mediaPlaylist.hasEndTag); assertTrue(mediaPlaylist.hasEndTag);
List<HlsMediaPlaylist.Segment> segments = mediaPlaylist.segments; List<Segment> segments = mediaPlaylist.segments;
assertNotNull(segments); assertNotNull(segments);
assertEquals(5, segments.size()); assertEquals(5, segments.size());
assertEquals(4, segments.get(0).discontinuitySequenceNumber); Segment segment = segments.get(0);
assertEquals(7975000, segments.get(0).durationUs); assertEquals(4, mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence);
assertEquals(false, segments.get(0).isEncrypted); assertEquals(7975000, segment.durationUs);
assertEquals(null, segments.get(0).encryptionKeyUri); assertFalse(segment.isEncrypted);
assertEquals(null, segments.get(0).encryptionIV); assertEquals(null, segment.encryptionKeyUri);
assertEquals(51370, segments.get(0).byterangeLength); assertEquals(null, segment.encryptionIV);
assertEquals(0, segments.get(0).byterangeOffset); assertEquals(51370, segment.byterangeLength);
assertEquals("https://priv.example.com/fileSequence2679.ts", segments.get(0).url); assertEquals(0, segment.byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2679.ts", segment.url);
assertEquals(4, segments.get(1).discontinuitySequenceNumber); segment = segments.get(1);
assertEquals(7975000, segments.get(1).durationUs); assertEquals(0, segment.relativeDiscontinuitySequence);
assertEquals(true, segments.get(1).isEncrypted); assertEquals(7975000, segment.durationUs);
assertEquals("https://priv.example.com/key.php?r=2680", segments.get(1).encryptionKeyUri); assertTrue(segment.isEncrypted);
assertEquals("0x1566B", segments.get(1).encryptionIV); assertEquals("https://priv.example.com/key.php?r=2680", segment.encryptionKeyUri);
assertEquals(51501, segments.get(1).byterangeLength); assertEquals("0x1566B", segment.encryptionIV);
assertEquals(2147483648L, segments.get(1).byterangeOffset); assertEquals(51501, segment.byterangeLength);
assertEquals("https://priv.example.com/fileSequence2680.ts", segments.get(1).url); assertEquals(2147483648L, segment.byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2680.ts", segment.url);
assertEquals(4, segments.get(2).discontinuitySequenceNumber); segment = segments.get(2);
assertEquals(7941000, segments.get(2).durationUs); assertEquals(0, segment.relativeDiscontinuitySequence);
assertEquals(false, segments.get(2).isEncrypted); assertEquals(7941000, segment.durationUs);
assertEquals(null, segments.get(2).encryptionKeyUri); assertFalse(segment.isEncrypted);
assertEquals(null, segments.get(2).encryptionIV); assertEquals(null, segment.encryptionKeyUri);
assertEquals(51501, segments.get(2).byterangeLength); assertEquals(null, segment.encryptionIV);
assertEquals(2147535149L, segments.get(2).byterangeOffset); assertEquals(51501, segment.byterangeLength);
assertEquals("https://priv.example.com/fileSequence2681.ts", segments.get(2).url); assertEquals(2147535149L, segment.byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2681.ts", segment.url);
assertEquals(5, segments.get(3).discontinuitySequenceNumber); segment = segments.get(3);
assertEquals(7975000, segments.get(3).durationUs); assertEquals(1, segment.relativeDiscontinuitySequence);
assertEquals(true, segments.get(3).isEncrypted); assertEquals(7975000, segment.durationUs);
assertEquals("https://priv.example.com/key.php?r=2682", segments.get(3).encryptionKeyUri); assertTrue(segment.isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2682", segment.encryptionKeyUri);
// 0xA7A == 2682. // 0xA7A == 2682.
assertNotNull(segments.get(3).encryptionIV); assertNotNull(segment.encryptionIV);
assertEquals("A7A", segments.get(3).encryptionIV.toUpperCase(Locale.getDefault())); assertEquals("A7A", segment.encryptionIV.toUpperCase(Locale.getDefault()));
assertEquals(51740, segments.get(3).byterangeLength); assertEquals(51740, segment.byterangeLength);
assertEquals(2147586650L, segments.get(3).byterangeOffset); assertEquals(2147586650L, segment.byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2682.ts", segments.get(3).url); assertEquals("https://priv.example.com/fileSequence2682.ts", segment.url);
assertEquals(5, segments.get(4).discontinuitySequenceNumber); segment = segments.get(4);
assertEquals(7975000, segments.get(4).durationUs); assertEquals(1, segment.relativeDiscontinuitySequence);
assertEquals(true, segments.get(4).isEncrypted); assertEquals(7975000, segment.durationUs);
assertEquals("https://priv.example.com/key.php?r=2682", segments.get(4).encryptionKeyUri); assertTrue(segment.isEncrypted);
assertEquals("https://priv.example.com/key.php?r=2682", segment.encryptionKeyUri);
// 0xA7B == 2683. // 0xA7B == 2683.
assertNotNull(segments.get(4).encryptionIV); assertNotNull(segment.encryptionIV);
assertEquals("A7B", segments.get(4).encryptionIV.toUpperCase(Locale.getDefault())); assertEquals("A7B", segment.encryptionIV.toUpperCase(Locale.getDefault()));
assertEquals(C.LENGTH_UNSET, segments.get(4).byterangeLength); assertEquals(C.LENGTH_UNSET, segment.byterangeLength);
assertEquals(0, segments.get(4).byterangeOffset); assertEquals(0, segment.byterangeOffset);
assertEquals("https://priv.example.com/fileSequence2683.ts", segments.get(4).url); assertEquals("https://priv.example.com/fileSequence2683.ts", segment.url);
} catch (IOException exception) { } catch (IOException exception) {
fail(exception.getMessage()); fail(exception.getMessage());
} }

View file

@ -27,7 +27,9 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
/** Unit tests for {@link CacheDataSource}. */ /**
* Unit tests for {@link CacheDataSource}.
*/
public class CacheDataSourceTest extends InstrumentationTestCase { public class CacheDataSourceTest extends InstrumentationTestCase {
private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
@ -117,6 +119,13 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
C.LENGTH_UNSET, KEY_2))); C.LENGTH_UNSET, KEY_2)));
} }
public void testIgnoreCacheForUnsetLengthRequests() throws Exception {
CacheDataSource cacheDataSource = createCacheDataSource(false, true,
CacheDataSource.FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS);
assertReadData(cacheDataSource, true, 0, C.LENGTH_UNSET);
MoreAsserts.assertEmpty(simpleCache.getKeys());
}
private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength) private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength)
throws IOException { throws IOException {
// Read all data from upstream and cache // Read all data from upstream and cache
@ -169,6 +178,12 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
private CacheDataSource createCacheDataSource(boolean setReadException, private CacheDataSource createCacheDataSource(boolean setReadException,
boolean simulateUnknownLength) { boolean simulateUnknownLength) {
return createCacheDataSource(setReadException, simulateUnknownLength,
CacheDataSource.FLAG_BLOCK_ON_CACHE);
}
private CacheDataSource createCacheDataSource(boolean setReadException,
boolean simulateUnknownLength, @CacheDataSource.Flags int flags) {
Builder builder = new Builder(); Builder builder = new Builder();
if (setReadException) { if (setReadException) {
builder.appendReadError(new IOException("Shouldn't read from upstream")); builder.appendReadError(new IOException("Shouldn't read from upstream"));
@ -176,8 +191,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
builder.setSimulateUnknownLength(simulateUnknownLength); builder.setSimulateUnknownLength(simulateUnknownLength);
builder.appendReadData(TEST_DATA); builder.appendReadData(TEST_DATA);
FakeDataSource upstream = builder.build(); FakeDataSource upstream = builder.build();
return new CacheDataSource(simpleCache, upstream, CacheDataSource.FLAG_BLOCK_ON_CACHE, return new CacheDataSource(simpleCache, upstream, flags, MAX_CACHE_FILE_SIZE);
MAX_CACHE_FILE_SIZE);
} }
} }

View file

@ -0,0 +1,181 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.upstream.cache;
import android.content.Context;
import android.net.Uri;
import android.test.AndroidTestCase;
import android.test.MoreAsserts;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DataSink;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.upstream.crypto.AesCipherDataSink;
import com.google.android.exoplayer2.upstream.crypto.AesCipherDataSource;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Random;
/**
* Additional tests for {@link CacheDataSource}.
*/
public class CacheDataSourceTest2 extends AndroidTestCase {
private static final String EXO_CACHE_DIR = "exo";
private static final int EXO_CACHE_MAX_FILESIZE = 128;
private static final Uri URI = Uri.parse("http://test.com/content");
private static final String KEY = "key";
private static final byte[] DATA = TestUtil.buildTestData(8 * EXO_CACHE_MAX_FILESIZE + 1);
// A DataSpec that covers the full file.
private static final DataSpec FULL = new DataSpec(URI, 0, DATA.length, KEY);
private static final int OFFSET_ON_BOUNDARY = EXO_CACHE_MAX_FILESIZE;
// A DataSpec that starts at 0 and extends to a cache file boundary.
private static final DataSpec END_ON_BOUNDARY = new DataSpec(URI, 0, OFFSET_ON_BOUNDARY, KEY);
// A DataSpec that starts on the same boundary and extends to the end of the file.
private static final DataSpec START_ON_BOUNDARY = new DataSpec(URI, OFFSET_ON_BOUNDARY,
DATA.length - OFFSET_ON_BOUNDARY, KEY);
private static final int OFFSET_OFF_BOUNDARY = EXO_CACHE_MAX_FILESIZE * 2 + 1;
// A DataSpec that starts at 0 and extends to just past a cache file boundary.
private static final DataSpec END_OFF_BOUNDARY = new DataSpec(URI, 0, OFFSET_OFF_BOUNDARY, KEY);
// A DataSpec that starts on the same boundary and extends to the end of the file.
private static final DataSpec START_OFF_BOUNDARY = new DataSpec(URI, OFFSET_OFF_BOUNDARY,
DATA.length - OFFSET_OFF_BOUNDARY, KEY);
public void testWithoutEncryption() throws IOException {
testReads(false);
}
public void testWithEncryption() throws IOException {
testReads(true);
}
private void testReads(boolean useEncryption) throws IOException {
FakeDataSource upstreamSource = buildFakeUpstreamSource();
CacheDataSource source = buildCacheDataSource(getContext(), upstreamSource, useEncryption);
// First read, should arrive from upstream.
testRead(END_ON_BOUNDARY, source);
assertSingleOpen(upstreamSource, 0, OFFSET_ON_BOUNDARY);
// Second read, should arrive from upstream.
testRead(START_OFF_BOUNDARY, source);
assertSingleOpen(upstreamSource, OFFSET_OFF_BOUNDARY, DATA.length);
// Second read, should arrive part from cache and part from upstream.
testRead(END_OFF_BOUNDARY, source);
assertSingleOpen(upstreamSource, OFFSET_ON_BOUNDARY, OFFSET_OFF_BOUNDARY);
// Third read, should arrive from cache.
testRead(FULL, source);
assertNoOpen(upstreamSource);
// Various reads, should all arrive from cache.
testRead(FULL, source);
assertNoOpen(upstreamSource);
testRead(START_ON_BOUNDARY, source);
assertNoOpen(upstreamSource);
testRead(END_ON_BOUNDARY, source);
assertNoOpen(upstreamSource);
testRead(START_OFF_BOUNDARY, source);
assertNoOpen(upstreamSource);
testRead(END_OFF_BOUNDARY, source);
assertNoOpen(upstreamSource);
}
private void testRead(DataSpec dataSpec, CacheDataSource source) throws IOException {
byte[] scratch = new byte[4096];
Random random = new Random(0);
source.open(dataSpec);
int position = (int) dataSpec.absoluteStreamPosition;
int bytesRead = 0;
while (bytesRead != C.RESULT_END_OF_INPUT) {
int maxBytesToRead = random.nextInt(scratch.length) + 1;
bytesRead = source.read(scratch, 0, maxBytesToRead);
if (bytesRead != C.RESULT_END_OF_INPUT) {
MoreAsserts.assertEquals(Arrays.copyOfRange(DATA, position, position + bytesRead),
Arrays.copyOf(scratch, bytesRead));
position += bytesRead;
}
}
source.close();
}
/**
* Asserts that a single {@link DataSource#open(DataSpec)} call has been made to the upstream
* source, with the specified start (inclusive) and end (exclusive) positions.
*/
private void assertSingleOpen(FakeDataSource upstreamSource, int start, int end) {
DataSpec[] openedDataSpecs = upstreamSource.getAndClearOpenedDataSpecs();
assertEquals(1, openedDataSpecs.length);
assertEquals(start, openedDataSpecs[0].position);
assertEquals(start, openedDataSpecs[0].absoluteStreamPosition);
assertEquals(end - start, openedDataSpecs[0].length);
}
/**
* Asserts that the upstream source was not opened.
*/
private void assertNoOpen(FakeDataSource upstreamSource) {
DataSpec[] openedDataSpecs = upstreamSource.getAndClearOpenedDataSpecs();
assertEquals(0, openedDataSpecs.length);
}
private static FakeDataSource buildFakeUpstreamSource() {
return new FakeDataSource.Builder().appendReadData(DATA).build();
}
private static CacheDataSource buildCacheDataSource(Context context, DataSource upstreamSource,
boolean useAesEncryption) throws CacheException {
File cacheDir = context.getExternalCacheDir();
Cache cache = new SimpleCache(new File(cacheDir, EXO_CACHE_DIR), new NoOpCacheEvictor());
emptyCache(cache);
// Source and cipher
final String secretKey = "testKey:12345678";
DataSource file = new FileDataSource();
DataSource cacheReadDataSource = useAesEncryption
? new AesCipherDataSource(Util.getUtf8Bytes(secretKey), file) : file;
// Sink and cipher
CacheDataSink cacheSink = new CacheDataSink(cache, EXO_CACHE_MAX_FILESIZE);
byte[] scratch = new byte[3897];
DataSink cacheWriteDataSink = useAesEncryption
? new AesCipherDataSink(Util.getUtf8Bytes(secretKey), cacheSink, scratch) : cacheSink;
return new CacheDataSource(cache,
upstreamSource,
cacheReadDataSource,
cacheWriteDataSink,
CacheDataSource.FLAG_BLOCK_ON_CACHE,
null); // eventListener
}
private static void emptyCache(Cache cache) throws CacheException {
for (String key : cache.getKeys()) {
for (CacheSpan span : cache.getCachedSpans(key)) {
cache.removeSpan(span);
}
}
// Sanity check that the cache really is empty now.
assertTrue(cache.getKeys().isEmpty());
}
}

View file

@ -163,7 +163,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
public void testEncryption() throws Exception { public void testEncryption() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
byte[] key2 = "bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key), assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
new CachedContentIndex(cacheDir, key)); new CachedContentIndex(cacheDir, key));
@ -181,7 +181,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
// Assert file content is different // Assert file content is different
FileInputStream fis1 = new FileInputStream(file1); FileInputStream fis1 = new FileInputStream(file1);
FileInputStream fis2 = new FileInputStream(file2); FileInputStream fis2 = new FileInputStream(file2);
for (int b; (b = fis1.read()) == fis2.read();) { for (int b; (b = fis1.read()) == fis2.read(); ) {
assertTrue(b != -1); assertTrue(b != -1);
} }
@ -205,6 +205,12 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
// Non encrypted index file can be read even when encryption key provided. // Non encrypted index file can be read even when encryption key provided.
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir), assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir),
new CachedContentIndex(cacheDir, key)); new CachedContentIndex(cacheDir, key));
// Test multiple store() calls
CachedContentIndex index = new CachedContentIndex(cacheDir, key);
index.addNew(new CachedContent(15, "key3", 110));
index.store();
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir, key));
} }
private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentIndex index2) private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentIndex index2)

View file

@ -0,0 +1,126 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.upstream.cache;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.ChunkIndex;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.File;
import java.io.IOException;
import org.mockito.Mock;
/**
* Tests for {@link CachedRegionTracker}.
*/
public final class CachedRegionTrackerTest extends InstrumentationTestCase {
private static final String CACHE_KEY = "abc";
private static final long MS_IN_US = 1000;
// 5 chunks, each 20 bytes long and 100 ms long.
private static final ChunkIndex CHUNK_INDEX = new ChunkIndex(
new int[] {20, 20, 20, 20, 20},
new long[] {100, 120, 140, 160, 180},
new long[] {100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US},
new long[] {0, 100 * MS_IN_US, 200 * MS_IN_US, 300 * MS_IN_US, 400 * MS_IN_US});
@Mock private Cache cache;
private CachedRegionTracker tracker;
private CachedContentIndex index;
private File cacheDir;
@Override
protected void setUp() throws Exception {
TestUtil.setUpMockito(this);
tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX);
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
index = new CachedContentIndex(cacheDir);
}
@Override
protected void tearDown() throws Exception {
TestUtil.recursiveDelete(cacheDir);
}
public void testGetRegion_noSpansInCache() {
assertEquals(CachedRegionTracker.NOT_CACHED, tracker.getRegionEndTimeMs(100));
assertEquals(CachedRegionTracker.NOT_CACHED, tracker.getRegionEndTimeMs(150));
}
public void testGetRegion_fullyCached() throws Exception {
tracker.onSpanAdded(
cache,
newCacheSpan(100, 100));
assertEquals(CachedRegionTracker.CACHED_TO_END, tracker.getRegionEndTimeMs(101));
assertEquals(CachedRegionTracker.CACHED_TO_END, tracker.getRegionEndTimeMs(121));
}
public void testGetRegion_partiallyCached() throws Exception {
tracker.onSpanAdded(
cache,
newCacheSpan(100, 40));
assertEquals(200, tracker.getRegionEndTimeMs(101));
assertEquals(200, tracker.getRegionEndTimeMs(121));
}
public void testGetRegion_multipleSpanAddsJoinedCorrectly() throws Exception {
tracker.onSpanAdded(
cache,
newCacheSpan(100, 20));
tracker.onSpanAdded(
cache,
newCacheSpan(120, 20));
assertEquals(200, tracker.getRegionEndTimeMs(101));
assertEquals(200, tracker.getRegionEndTimeMs(121));
}
public void testGetRegion_fullyCachedThenPartiallyRemoved() throws Exception {
// Start with the full stream in cache.
tracker.onSpanAdded(
cache,
newCacheSpan(100, 100));
// Remove the middle bit.
tracker.onSpanRemoved(
cache,
newCacheSpan(140, 40));
assertEquals(200, tracker.getRegionEndTimeMs(101));
assertEquals(200, tracker.getRegionEndTimeMs(121));
assertEquals(CachedRegionTracker.CACHED_TO_END, tracker.getRegionEndTimeMs(181));
}
public void testGetRegion_subchunkEstimation() throws Exception {
tracker.onSpanAdded(
cache,
newCacheSpan(100, 10));
assertEquals(50, tracker.getRegionEndTimeMs(101));
assertEquals(CachedRegionTracker.NOT_CACHED, tracker.getRegionEndTimeMs(111));
}
private CacheSpan newCacheSpan(int position, int length) throws IOException {
return SimpleCacheSpanTest.createCacheSpan(index, cacheDir, CACHE_KEY, position, length, 0);
}
}

View file

@ -16,12 +16,16 @@
package com.google.android.exoplayer2.upstream.cache; package com.google.android.exoplayer2.upstream.cache;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import android.test.MoreAsserts;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.NavigableSet; import java.util.NavigableSet;
import java.util.Random;
import java.util.Set; import java.util.Set;
/** /**
@ -46,9 +50,9 @@ public class SimpleCacheTest extends InstrumentationTestCase {
public void testCommittingOneFile() throws Exception { public void testCommittingOneFile() throws Exception {
SimpleCache simpleCache = getSimpleCache(); SimpleCache simpleCache = getSimpleCache();
CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0); CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
assertFalse(cacheSpan.isCached); assertFalse(cacheSpan1.isCached);
assertTrue(cacheSpan.isOpenEnded()); assertTrue(cacheSpan1.isOpenEnded());
assertNull(simpleCache.startReadWriteNonBlocking(KEY_1, 0)); assertNull(simpleCache.startReadWriteNonBlocking(KEY_1, 0));
@ -58,20 +62,33 @@ public class SimpleCacheTest extends InstrumentationTestCase {
assertEquals(0, simpleCache.getCacheSpace()); assertEquals(0, simpleCache.getCacheSpace());
assertEquals(0, cacheDir.listFiles().length); assertEquals(0, cacheDir.listFiles().length);
addCache(simpleCache, 0, 15); addCache(simpleCache, KEY_1, 0, 15);
Set<String> cachedKeys = simpleCache.getKeys(); Set<String> cachedKeys = simpleCache.getKeys();
assertEquals(1, cachedKeys.size()); assertEquals(1, cachedKeys.size());
assertTrue(cachedKeys.contains(KEY_1)); assertTrue(cachedKeys.contains(KEY_1));
cachedSpans = simpleCache.getCachedSpans(KEY_1); cachedSpans = simpleCache.getCachedSpans(KEY_1);
assertEquals(1, cachedSpans.size()); assertEquals(1, cachedSpans.size());
assertTrue(cachedSpans.contains(cacheSpan)); assertTrue(cachedSpans.contains(cacheSpan1));
assertEquals(15, simpleCache.getCacheSpace()); assertEquals(15, simpleCache.getCacheSpace());
cacheSpan = simpleCache.startReadWrite(KEY_1, 0); simpleCache.releaseHoleSpan(cacheSpan1);
assertTrue(cacheSpan.isCached);
assertFalse(cacheSpan.isOpenEnded()); CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);
assertEquals(15, cacheSpan.length); assertTrue(cacheSpan2.isCached);
assertFalse(cacheSpan2.isOpenEnded());
assertEquals(15, cacheSpan2.length);
assertCachedDataReadCorrect(cacheSpan2);
}
public void testReadCacheWithoutReleasingWriteCacheSpan() throws Exception {
SimpleCache simpleCache = getSimpleCache();
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15);
CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);
assertCachedDataReadCorrect(cacheSpan2);
simpleCache.releaseHoleSpan(cacheSpan1);
} }
public void testSetGetLength() throws Exception { public void testSetGetLength() throws Exception {
@ -83,12 +100,12 @@ public class SimpleCacheTest extends InstrumentationTestCase {
simpleCache.startReadWrite(KEY_1, 0); simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, 0, 15); addCache(simpleCache, KEY_1, 0, 15);
simpleCache.setContentLength(KEY_1, 150); simpleCache.setContentLength(KEY_1, 150);
assertEquals(150, simpleCache.getContentLength(KEY_1)); assertEquals(150, simpleCache.getContentLength(KEY_1));
addCache(simpleCache, 140, 10); addCache(simpleCache, KEY_1, 140, 10);
// Check if values are kept after cache is reloaded. // Check if values are kept after cache is reloaded.
SimpleCache simpleCache2 = getSimpleCache(); SimpleCache simpleCache2 = getSimpleCache();
@ -107,16 +124,109 @@ public class SimpleCacheTest extends InstrumentationTestCase {
assertEquals(150, simpleCache2.getContentLength(KEY_1)); assertEquals(150, simpleCache2.getContentLength(KEY_1));
} }
public void testReloadCache() throws Exception {
SimpleCache simpleCache = getSimpleCache();
// write data
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15);
simpleCache.releaseHoleSpan(cacheSpan1);
// Reload cache
simpleCache = getSimpleCache();
// read data back
CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);
assertCachedDataReadCorrect(cacheSpan2);
}
public void testEncryptedIndex() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
SimpleCache simpleCache = getEncryptedSimpleCache(key);
// write data
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15);
simpleCache.releaseHoleSpan(cacheSpan1);
// Reload cache
simpleCache = getEncryptedSimpleCache(key);
// read data back
CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);
assertCachedDataReadCorrect(cacheSpan2);
}
public void testEncryptedIndexWrongKey() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
SimpleCache simpleCache = getEncryptedSimpleCache(key);
// write data
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15);
simpleCache.releaseHoleSpan(cacheSpan1);
// Reload cache
byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key
simpleCache = getEncryptedSimpleCache(key2);
// Cache should be cleared
assertEquals(0, simpleCache.getKeys().size());
assertEquals(0, cacheDir.listFiles().length);
}
public void testEncryptedIndexLostKey() throws Exception {
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
SimpleCache simpleCache = getEncryptedSimpleCache(key);
// write data
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
addCache(simpleCache, KEY_1, 0, 15);
simpleCache.releaseHoleSpan(cacheSpan1);
// Reload cache
simpleCache = getSimpleCache();
// Cache should be cleared
assertEquals(0, simpleCache.getKeys().size());
assertEquals(0, cacheDir.listFiles().length);
}
private SimpleCache getSimpleCache() { private SimpleCache getSimpleCache() {
return new SimpleCache(cacheDir, new NoOpCacheEvictor()); return new SimpleCache(cacheDir, new NoOpCacheEvictor());
} }
private void addCache(SimpleCache simpleCache, int position, int length) throws IOException { private SimpleCache getEncryptedSimpleCache(byte[] secretKey) {
File file = simpleCache.startFile(KEY_1, position, length); return new SimpleCache(cacheDir, new NoOpCacheEvictor(), secretKey);
}
private static void addCache(SimpleCache simpleCache, String key, int position, int length)
throws IOException {
File file = simpleCache.startFile(key, position, length);
FileOutputStream fos = new FileOutputStream(file); FileOutputStream fos = new FileOutputStream(file);
fos.write(new byte[length]); try {
fos.close(); fos.write(generateData(key, position, length));
} finally {
fos.close();
}
simpleCache.commitFile(file); simpleCache.commitFile(file);
} }
private static void assertCachedDataReadCorrect(CacheSpan cacheSpan) throws IOException {
assertTrue(cacheSpan.isCached);
byte[] expected = generateData(cacheSpan.key, (int) cacheSpan.position, (int) cacheSpan.length);
FileInputStream inputStream = new FileInputStream(cacheSpan.file);
try {
MoreAsserts.assertEquals(expected, Util.toByteArray(inputStream));
} finally {
inputStream.close();
}
}
private static byte[] generateData(String key, int position, int length) {
byte[] bytes = new byte[length];
new Random((long) (key.hashCode() ^ position)).nextBytes(bytes);
return bytes;
}
} }

View file

@ -0,0 +1,186 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.upstream.crypto;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util;
import java.util.Random;
import javax.crypto.Cipher;
import junit.framework.TestCase;
/**
* Unit tests for {@link AesFlushingCipher}.
*/
public class AesFlushingCipherTest extends TestCase {
private static final int DATA_LENGTH = 65536;
private static final byte[] KEY = Util.getUtf8Bytes("testKey:12345678");
private static final long NONCE = 0;
private static final long START_OFFSET = 11;
private static final long RANDOM_SEED = 0x12345678;
private AesFlushingCipher encryptCipher;
private AesFlushingCipher decryptCipher;
@Override
protected void setUp() {
encryptCipher = new AesFlushingCipher(Cipher.ENCRYPT_MODE, KEY, NONCE, START_OFFSET);
decryptCipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, KEY, NONCE, START_OFFSET);
}
@Override
protected void tearDown() {
encryptCipher = null;
decryptCipher = null;
}
private long getMaxUnchangedBytesAllowedPostEncryption(long length) {
// Assuming that not more than 10% of the resultant bytes should be identical.
// The value of 10% is arbitrary, ciphers standards do not name a value.
return length / 10;
}
// Count the number of bytes that do not match.
private int getDifferingByteCount(byte[] data1, byte[] data2, int startOffset) {
int count = 0;
for (int i = startOffset; i < data1.length; i++) {
if (data1[i] != data2[i]) {
count++;
}
}
return count;
}
// Count the number of bytes that do not match.
private int getDifferingByteCount(byte[] data1, byte[] data2) {
return getDifferingByteCount(data1, data2, 0);
}
// Test a single encrypt and decrypt call
public void testSingle() {
byte[] reference = TestUtil.buildTestData(DATA_LENGTH);
byte[] data = reference.clone();
encryptCipher.updateInPlace(data, 0, data.length);
int unchangedByteCount = data.length - getDifferingByteCount(reference, data);
assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length));
decryptCipher.updateInPlace(data, 0, data.length);
int differingByteCount = getDifferingByteCount(reference, data);
assertEquals(0, differingByteCount);
}
// Test several encrypt and decrypt calls, each aligned on a 16 byte block size
public void testAligned() {
byte[] reference = TestUtil.buildTestData(DATA_LENGTH);
byte[] data = reference.clone();
Random random = new Random(RANDOM_SEED);
int offset = 0;
while (offset < data.length) {
int bytes = (1 + random.nextInt(50)) * 16;
bytes = Math.min(bytes, data.length - offset);
assertEquals(0, bytes % 16);
encryptCipher.updateInPlace(data, offset, bytes);
offset += bytes;
}
int unchangedByteCount = data.length - getDifferingByteCount(reference, data);
assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length));
offset = 0;
while (offset < data.length) {
int bytes = (1 + random.nextInt(50)) * 16;
bytes = Math.min(bytes, data.length - offset);
assertEquals(0, bytes % 16);
decryptCipher.updateInPlace(data, offset, bytes);
offset += bytes;
}
int differingByteCount = getDifferingByteCount(reference, data);
assertEquals(0, differingByteCount);
}
// Test several encrypt and decrypt calls, not aligned on block boundary
public void testUnAligned() {
byte[] reference = TestUtil.buildTestData(DATA_LENGTH);
byte[] data = reference.clone();
Random random = new Random(RANDOM_SEED);
// Encrypt
int offset = 0;
while (offset < data.length) {
int bytes = 1 + random.nextInt(4095);
bytes = Math.min(bytes, data.length - offset);
encryptCipher.updateInPlace(data, offset, bytes);
offset += bytes;
}
int unchangedByteCount = data.length - getDifferingByteCount(reference, data);
assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length));
offset = 0;
while (offset < data.length) {
int bytes = 1 + random.nextInt(4095);
bytes = Math.min(bytes, data.length - offset);
decryptCipher.updateInPlace(data, offset, bytes);
offset += bytes;
}
int differingByteCount = getDifferingByteCount(reference, data);
assertEquals(0, differingByteCount);
}
// Test decryption starting from the middle of an encrypted block
public void testMidJoin() {
byte[] reference = TestUtil.buildTestData(DATA_LENGTH);
byte[] data = reference.clone();
Random random = new Random(RANDOM_SEED);
// Encrypt
int offset = 0;
while (offset < data.length) {
int bytes = 1 + random.nextInt(4095);
bytes = Math.min(bytes, data.length - offset);
encryptCipher.updateInPlace(data, offset, bytes);
offset += bytes;
}
// Verify
int unchangedByteCount = data.length - getDifferingByteCount(reference, data);
assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length));
// Setup decryption from random location
offset = random.nextInt(4096);
decryptCipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, KEY, NONCE, offset + START_OFFSET);
int remainingLength = data.length - offset;
int originalOffset = offset;
// Decrypt
while (remainingLength > 0) {
int bytes = 1 + random.nextInt(4095);
bytes = Math.min(bytes, remainingLength);
decryptCipher.updateInPlace(data, offset, bytes);
offset += bytes;
remainingLength -= bytes;
}
// Verify
int differingByteCount = getDifferingByteCount(reference, data, originalOffset);
assertEquals(0, differingByteCount);
}
}

View file

@ -371,6 +371,73 @@ public class ParsableByteArrayTest extends TestCase {
assertNull(parser.readLine()); assertNull(parser.readLine());
} }
public void testReadNullTerminatedStringWithLengths() {
byte[] bytes = new byte[] {
'f', 'o', 'o', 0, 'b', 'a', 'r', 0
};
// Test with lengths that match NUL byte positions.
ParsableByteArray parser = new ParsableByteArray(bytes);
assertEquals("foo", parser.readNullTerminatedString(4));
assertEquals(4, parser.getPosition());
assertEquals("bar", parser.readNullTerminatedString(4));
assertEquals(8, parser.getPosition());
assertNull(parser.readNullTerminatedString());
// Test with lengths that do not match NUL byte positions.
parser = new ParsableByteArray(bytes);
assertEquals("fo", parser.readNullTerminatedString(2));
assertEquals(2, parser.getPosition());
assertEquals("o", parser.readNullTerminatedString(2));
assertEquals(4, parser.getPosition());
assertEquals("bar", parser.readNullTerminatedString(3));
assertEquals(7, parser.getPosition());
assertEquals("", parser.readNullTerminatedString(1));
assertEquals(8, parser.getPosition());
assertNull(parser.readNullTerminatedString());
// Test with limit at NUL
parser = new ParsableByteArray(bytes, 4);
assertEquals("foo", parser.readNullTerminatedString(4));
assertEquals(4, parser.getPosition());
assertNull(parser.readNullTerminatedString());
// Test with limit before NUL
parser = new ParsableByteArray(bytes, 3);
assertEquals("foo", parser.readNullTerminatedString(3));
assertEquals(3, parser.getPosition());
assertNull(parser.readNullTerminatedString());
}
public void testReadNullTerminatedString() {
byte[] bytes = new byte[] {
'f', 'o', 'o', 0, 'b', 'a', 'r', 0
};
// Test normal case.
ParsableByteArray parser = new ParsableByteArray(bytes);
assertEquals("foo", parser.readNullTerminatedString());
assertEquals(4, parser.getPosition());
assertEquals("bar", parser.readNullTerminatedString());
assertEquals(8, parser.getPosition());
assertNull(parser.readNullTerminatedString());
// Test with limit at NUL.
parser = new ParsableByteArray(bytes, 4);
assertEquals("foo", parser.readNullTerminatedString());
assertEquals(4, parser.getPosition());
assertNull(parser.readNullTerminatedString());
// Test with limit before NUL.
parser = new ParsableByteArray(bytes, 3);
assertEquals("foo", parser.readNullTerminatedString());
assertEquals(3, parser.getPosition());
assertNull(parser.readNullTerminatedString());
}
public void testReadNullTerminatedStringWithoutEndingNull() {
byte[] bytes = new byte[] {
'f', 'o', 'o', 0, 'b', 'a', 'r'
};
ParsableByteArray parser = new ParsableByteArray(bytes);
assertEquals("foo", parser.readNullTerminatedString());
assertEquals("bar", parser.readNullTerminatedString());
assertNull(parser.readNullTerminatedString());
}
public void testReadSingleLineWithoutEndingTrail() { public void testReadSingleLineWithoutEndingTrail() {
byte[] bytes = new byte[] { byte[] bytes = new byte[] {
'f', 'o', 'o' 'f', 'o', 'o'

View file

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

View file

@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2; package com.google.android.exoplayer2;
import android.annotation.TargetApi;
import android.content.Context;
import android.media.AudioFormat; import android.media.AudioFormat;
import android.media.AudioManager; import android.media.AudioManager;
import android.media.MediaCodec; import android.media.MediaCodec;
@ -96,6 +98,13 @@ public final class C {
@SuppressWarnings("InlinedApi") @SuppressWarnings("InlinedApi")
public static final int CRYPTO_MODE_AES_CBC = MediaCodec.CRYPTO_MODE_AES_CBC; public static final int CRYPTO_MODE_AES_CBC = MediaCodec.CRYPTO_MODE_AES_CBC;
/**
* Represents an unset {@link android.media.AudioTrack} session identifier. Equal to
* {@link AudioManager#AUDIO_SESSION_ID_GENERATE}.
*/
@SuppressWarnings("InlinedApi")
public static final int AUDIO_SESSION_ID_UNSET = AudioManager.AUDIO_SESSION_ID_GENERATE;
/** /**
* Represents an audio encoding, or an invalid or unset value. * Represents an audio encoding, or an invalid or unset value.
*/ */
@ -543,4 +552,13 @@ public final class C {
return timeMs == TIME_UNSET ? TIME_UNSET : (timeMs * 1000); return timeMs == TIME_UNSET ? TIME_UNSET : (timeMs * 1000);
} }
/**
* Returns a newly generated {@link android.media.AudioTrack} session identifier.
*/
@TargetApi(21)
public static int generateAudioSessionIdV21(Context context) {
return ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE))
.generateAudioSessionId();
}
} }

View file

@ -19,6 +19,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.upstream.DefaultAllocator;
import com.google.android.exoplayer2.util.PriorityTaskManager;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
/** /**
@ -50,6 +51,11 @@ public final class DefaultLoadControl implements LoadControl {
*/ */
public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000; public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000;
/**
* Priority for media loading.
*/
public static final int LOADING_PRIORITY = 0;
private static final int ABOVE_HIGH_WATERMARK = 0; private static final int ABOVE_HIGH_WATERMARK = 0;
private static final int BETWEEN_WATERMARKS = 1; private static final int BETWEEN_WATERMARKS = 1;
private static final int BELOW_LOW_WATERMARK = 2; private static final int BELOW_LOW_WATERMARK = 2;
@ -60,6 +66,7 @@ public final class DefaultLoadControl implements LoadControl {
private final long maxBufferUs; private final long maxBufferUs;
private final long bufferForPlaybackUs; private final long bufferForPlaybackUs;
private final long bufferForPlaybackAfterRebufferUs; private final long bufferForPlaybackAfterRebufferUs;
private final PriorityTaskManager priorityTaskManager;
private int targetBufferSize; private int targetBufferSize;
private boolean isBuffering; private boolean isBuffering;
@ -97,11 +104,36 @@ public final class DefaultLoadControl implements LoadControl {
*/ */
public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs, public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs) { long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs) {
this(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs,
null);
}
/**
* Constructs a new instance.
*
* @param allocator The {@link DefaultAllocator} used by the loader.
* @param minBufferMs The minimum duration of media that the player will attempt to ensure is
* buffered at all times, in milliseconds.
* @param maxBufferMs The maximum duration of media that the player will attempt buffer, in
* milliseconds.
* @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or
* resume following a user action such as a seek, in milliseconds.
* @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for
* playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by
* buffer depletion rather than a user action.
* @param priorityTaskManager If not null, registers itself as a task with priority
* {@link #LOADING_PRIORITY} during loading periods, and unregisters itself during draining
* periods.
*/
public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs,
PriorityTaskManager priorityTaskManager) {
this.allocator = allocator; this.allocator = allocator;
minBufferUs = minBufferMs * 1000L; minBufferUs = minBufferMs * 1000L;
maxBufferUs = maxBufferMs * 1000L; maxBufferUs = maxBufferMs * 1000L;
bufferForPlaybackUs = bufferForPlaybackMs * 1000L; bufferForPlaybackUs = bufferForPlaybackMs * 1000L;
bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L; bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L;
this.priorityTaskManager = priorityTaskManager;
} }
@Override @Override
@ -146,8 +178,16 @@ public final class DefaultLoadControl implements LoadControl {
public boolean shouldContinueLoading(long bufferedDurationUs) { public boolean shouldContinueLoading(long bufferedDurationUs) {
int bufferTimeState = getBufferTimeState(bufferedDurationUs); int bufferTimeState = getBufferTimeState(bufferedDurationUs);
boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize; boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize;
boolean wasBuffering = isBuffering;
isBuffering = bufferTimeState == BELOW_LOW_WATERMARK isBuffering = bufferTimeState == BELOW_LOW_WATERMARK
|| (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached); || (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached);
if (priorityTaskManager != null && isBuffering != wasBuffering) {
if (isBuffering) {
priorityTaskManager.add(LOADING_PRIORITY);
} else {
priorityTaskManager.remove(LOADING_PRIORITY);
}
}
return isBuffering; return isBuffering;
} }
@ -158,6 +198,9 @@ public final class DefaultLoadControl implements LoadControl {
private void reset(boolean resetAllocator) { private void reset(boolean resetAllocator) {
targetBufferSize = 0; targetBufferSize = 0;
if (priorityTaskManager != null && isBuffering) {
priorityTaskManager.remove(LOADING_PRIORITY);
}
isBuffering = false; isBuffering = false;
if (resetAllocator) { if (resetAllocator) {
allocator.reset(); allocator.reset();

View file

@ -447,4 +447,20 @@ public interface ExoPlayer {
*/ */
int getBufferedPercentage(); int getBufferedPercentage();
/**
* Returns whether the current window is dynamic, or {@code false} if the {@link Timeline} is
* empty.
*
* @see Timeline.Window#isDynamic
*/
boolean isCurrentWindowDynamic();
/**
* Returns whether the current window is seekable, or {@code false} if the {@link Timeline} is
* empty.
*
* @see Timeline.Window#isSeekable
*/
boolean isCurrentWindowSeekable();
} }

View file

@ -22,12 +22,12 @@ import android.os.Message;
import android.util.Log; import android.util.Log;
import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo; import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo;
import com.google.android.exoplayer2.ExoPlayerImplInternal.SourceInfo; import com.google.android.exoplayer2.ExoPlayerImplInternal.SourceInfo;
import com.google.android.exoplayer2.ExoPlayerImplInternal.TrackInfo;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
@ -271,6 +271,22 @@ import java.util.concurrent.CopyOnWriteArraySet;
: (int) (duration == 0 ? 100 : (bufferedPosition * 100) / duration); : (int) (duration == 0 ? 100 : (bufferedPosition * 100) / duration);
} }
@Override
public boolean isCurrentWindowDynamic() {
if (timeline.isEmpty()) {
return false;
}
return timeline.getWindow(getCurrentWindowIndex(), window).isDynamic;
}
@Override
public boolean isCurrentWindowSeekable() {
if (timeline.isEmpty()) {
return false;
}
return timeline.getWindow(getCurrentWindowIndex(), window).isSeekable;
}
@Override @Override
public int getRendererCount() { public int getRendererCount() {
return renderers.length; return renderers.length;
@ -319,11 +335,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
break; break;
} }
case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: { case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: {
TrackInfo trackInfo = (TrackInfo) msg.obj; TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj;
tracksSelected = true; tracksSelected = true;
trackGroups = trackInfo.groups; trackGroups = trackSelectorResult.groups;
trackSelections = trackInfo.selections; trackSelections = trackSelectorResult.selections;
trackSelector.onSelectionActivated(trackInfo.info); trackSelector.onSelectionActivated(trackSelectorResult.info);
for (EventListener listener : listeners) { for (EventListener listener : listeners) {
listener.onTracksChanged(trackGroups, trackSelections); listener.onTracksChanged(trackGroups, trackSelections);
} }
@ -332,8 +348,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
case ExoPlayerImplInternal.MSG_SEEK_ACK: { case ExoPlayerImplInternal.MSG_SEEK_ACK: {
if (--pendingSeekAcks == 0) { if (--pendingSeekAcks == 0) {
playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj; playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj;
for (EventListener listener : listeners) { if (msg.arg1 != 0) {
listener.onPositionDiscontinuity(); for (EventListener listener : listeners) {
listener.onPositionDiscontinuity();
}
} }
} }
break; break;

View file

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

View file

@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo {
/** /**
* The version of the library, expressed as a string. * The version of the library, expressed as a string.
*/ */
String VERSION = "2.1.1"; String VERSION = "2.2.0";
/** /**
* The version of the library, expressed as an integer. * 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 * corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding
* integer version 123045006 (123-045-006). * integer version 123045006 (123-045-006).
*/ */
int VERSION_INT = 2001001; int VERSION_INT = 2002000;
/** /**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}

View file

@ -183,20 +183,18 @@ public final class Format implements Parcelable {
*/ */
public final int accessibilityChannel; public final int accessibilityChannel;
// Lazily initialized hashcode and framework media format. // Lazily initialized hashcode.
private int hashCode; private int hashCode;
private MediaFormat frameworkMediaFormat;
// Video. // Video.
public static Format createVideoContainerFormat(String id, String containerMimeType, public static Format createVideoContainerFormat(String id, String containerMimeType,
String sampleMimeType, String codecs, int bitrate, int width, int height, String sampleMimeType, String codecs, int bitrate, int width, int height,
float frameRate, List<byte[]> initializationData) { float frameRate, List<byte[]> initializationData, @C.SelectionFlags int selectionFlags) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, width, return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, width,
height, frameRate, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, height, frameRate, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, null, NO_VALUE, NO_VALUE, selectionFlags, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
null); initializationData, null, null);
} }
public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs, public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs,
@ -289,8 +287,8 @@ public final class Format implements Parcelable {
} }
public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs, public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
int bitrate, @C.SelectionFlags int selectionFlags, String language, int bitrate, @C.SelectionFlags int selectionFlags, String language, int accessibilityChannel,
int accessibilityChannel, DrmInitData drmInitData) { DrmInitData drmInitData) {
return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language, return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
accessibilityChannel, drmInitData, OFFSET_SAMPLE_RELATIVE); accessibilityChannel, drmInitData, OFFSET_SAMPLE_RELATIVE);
} }
@ -323,11 +321,20 @@ public final class Format implements Parcelable {
// Generic. // Generic.
public static Format createContainerFormat(String id, String containerMimeType, String codecs, public static Format createContainerFormat(String id, String containerMimeType,
String sampleMimeType, int bitrate) { String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags,
String language) {
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE, return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, null, null); NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, null,
null);
}
public static Format createSampleFormat(String id, String sampleMimeType,
long subsampleOffsetUs) {
return new Format(id, null, sampleMimeType, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
NO_VALUE, 0, null, NO_VALUE, subsampleOffsetUs, null, null, null);
} }
public static Format createSampleFormat(String id, String sampleMimeType, String codecs, public static Format createSampleFormat(String id, String sampleMimeType, String codecs,
@ -486,31 +493,28 @@ public final class Format implements Parcelable {
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
@TargetApi(16) @TargetApi(16)
public final MediaFormat getFrameworkMediaFormatV16() { public final MediaFormat getFrameworkMediaFormatV16() {
if (frameworkMediaFormat == null) { MediaFormat format = new MediaFormat();
MediaFormat format = new MediaFormat(); format.setString(MediaFormat.KEY_MIME, sampleMimeType);
format.setString(MediaFormat.KEY_MIME, sampleMimeType); maybeSetStringV16(format, MediaFormat.KEY_LANGUAGE, language);
maybeSetStringV16(format, MediaFormat.KEY_LANGUAGE, language); maybeSetIntegerV16(format, MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
maybeSetIntegerV16(format, MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize); maybeSetIntegerV16(format, MediaFormat.KEY_WIDTH, width);
maybeSetIntegerV16(format, MediaFormat.KEY_WIDTH, width); maybeSetIntegerV16(format, MediaFormat.KEY_HEIGHT, height);
maybeSetIntegerV16(format, MediaFormat.KEY_HEIGHT, height); maybeSetFloatV16(format, MediaFormat.KEY_FRAME_RATE, frameRate);
maybeSetFloatV16(format, MediaFormat.KEY_FRAME_RATE, frameRate); maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees);
maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees); maybeSetIntegerV16(format, MediaFormat.KEY_CHANNEL_COUNT, channelCount);
maybeSetIntegerV16(format, MediaFormat.KEY_CHANNEL_COUNT, channelCount); maybeSetIntegerV16(format, MediaFormat.KEY_SAMPLE_RATE, sampleRate);
maybeSetIntegerV16(format, MediaFormat.KEY_SAMPLE_RATE, sampleRate); maybeSetIntegerV16(format, "encoder-delay", encoderDelay);
maybeSetIntegerV16(format, "encoder-delay", encoderDelay); maybeSetIntegerV16(format, "encoder-padding", encoderPadding);
maybeSetIntegerV16(format, "encoder-padding", encoderPadding); for (int i = 0; i < initializationData.size(); i++) {
for (int i = 0; i < initializationData.size(); i++) { format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
}
frameworkMediaFormat = format;
} }
return frameworkMediaFormat; return format;
} }
@Override @Override
public String toString() { public String toString() {
return "Format(" + id + ", " + containerMimeType + ", " + sampleMimeType + ", " + bitrate + ", " return "Format(" + id + ", " + containerMimeType + ", " + sampleMimeType + ", " + bitrate + ", "
+ ", " + language + ", [" + width + ", " + height + ", " + frameRate + "]" + language + ", [" + width + ", " + height + ", " + frameRate + "]"
+ ", [" + channelCount + ", " + sampleRate + "])"; + ", [" + channelCount + ", " + sampleRate + "])";
} }
@ -593,6 +597,38 @@ public final class Format implements Parcelable {
} }
} }
// Utility methods
/**
* Returns a prettier {@link String} than {@link #toString()}, intended for logging.
*/
public static String toLogString(Format format) {
if (format == null) {
return "null";
}
StringBuilder builder = new StringBuilder();
builder.append("id=").append(format.id).append(", mimeType=").append(format.sampleMimeType);
if (format.bitrate != Format.NO_VALUE) {
builder.append(", bitrate=").append(format.bitrate);
}
if (format.width != Format.NO_VALUE && format.height != Format.NO_VALUE) {
builder.append(", res=").append(format.width).append("x").append(format.height);
}
if (format.frameRate != Format.NO_VALUE) {
builder.append(", fps=").append(format.frameRate);
}
if (format.channelCount != Format.NO_VALUE) {
builder.append(", channels=").append(format.channelCount);
}
if (format.sampleRate != Format.NO_VALUE) {
builder.append(", sample_rate=").append(format.sampleRate);
}
if (format.language != null) {
builder.append(", language=").append(format.language);
}
return builder.toString();
}
// Parcelable implementation. // Parcelable implementation.
@Override @Override

View file

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

View file

@ -79,6 +79,20 @@ public interface RendererCapabilities {
*/ */
int ADAPTIVE_NOT_SUPPORTED = 0b0000; int ADAPTIVE_NOT_SUPPORTED = 0b0000;
/**
* A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of
* {@link #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}.
*/
int TUNNELING_SUPPORT_MASK = 0b10000;
/**
* The {@link Renderer} supports tunneled output.
*/
int TUNNELING_SUPPORTED = 0b10000;
/**
* The {@link Renderer} does not support tunneled output.
*/
int TUNNELING_NOT_SUPPORTED = 0b00000;
/** /**
* Returns the track type that the {@link Renderer} handles. For example, a video renderer will * Returns the track type that the {@link Renderer} handles. For example, a video renderer will
* return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will return {@link C#TRACK_TYPE_AUDIO}, a * return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will return {@link C#TRACK_TYPE_AUDIO}, a
@ -91,7 +105,7 @@ public interface RendererCapabilities {
/** /**
* Returns the extent to which the {@link Renderer} supports a given format. The returned value is * Returns the extent to which the {@link Renderer} supports a given format. The returned value is
* the bitwise OR of two properties: * the bitwise OR of three properties:
* <ul> * <ul>
* <li>The level of support for the format itself. One of {@link #FORMAT_HANDLED}, * <li>The level of support for the format itself. One of {@link #FORMAT_HANDLED},
* {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_SUBTYPE} and * {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_SUBTYPE} and
@ -99,9 +113,12 @@ public interface RendererCapabilities {
* <li>The level of support for adapting from the format to another format of the same mime type. * <li>The level of support for adapting from the format to another format of the same mime type.
* One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and * One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and
* {@link #ADAPTIVE_NOT_SUPPORTED}.</li> * {@link #ADAPTIVE_NOT_SUPPORTED}.</li>
* <li>The level of support for tunneling. One of {@link #TUNNELING_SUPPORTED} and
* {@link #TUNNELING_NOT_SUPPORTED}.</li>
* </ul> * </ul>
* The individual properties can be retrieved by performing a bitwise AND with * The individual properties can be retrieved by performing a bitwise AND with
* {@link #FORMAT_SUPPORT_MASK} and {@link #ADAPTIVE_SUPPORT_MASK} respectively. * {@link #FORMAT_SUPPORT_MASK}, {@link #ADAPTIVE_SUPPORT_MASK} and
* {@link #TUNNELING_SUPPORT_MASK} respectively.
* *
* @param format The format. * @param format The format.
* @return The extent to which the renderer is capable of supporting the given format. * @return The extent to which the renderer is capable of supporting the given format.

View file

@ -0,0 +1,60 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2;
/**
* The configuration of a {@link Renderer}.
*/
public final class RendererConfiguration {
/**
* The default configuration.
*/
public static final RendererConfiguration DEFAULT =
new RendererConfiguration(C.AUDIO_SESSION_ID_UNSET);
/**
* The audio session id to use for tunneling, or {@link C#AUDIO_SESSION_ID_UNSET} if tunneling
* should not be enabled.
*/
public final int tunnelingAudioSessionId;
/**
* @param tunnelingAudioSessionId The audio session id to use for tunneling, or
* {@link C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled.
*/
public RendererConfiguration(int tunnelingAudioSessionId) {
this.tunnelingAudioSessionId = tunnelingAudioSessionId;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
RendererConfiguration other = (RendererConfiguration) obj;
return tunnelingAudioSessionId == other.tunnelingAudioSessionId;
}
@Override
public int hashCode() {
return tunnelingAudioSessionId;
}
}

View file

@ -29,7 +29,6 @@ import android.view.SurfaceView;
import android.view.TextureView; import android.view.TextureView;
import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioCapabilities;
import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener;
import com.google.android.exoplayer2.audio.AudioTrack;
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager;
@ -37,7 +36,6 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
@ -178,7 +176,7 @@ public class SimpleExoPlayer implements ExoPlayer {
// Set initial values. // Set initial values.
audioVolume = 1; audioVolume = 1;
audioSessionId = AudioTrack.SESSION_ID_NOT_SET; audioSessionId = C.AUDIO_SESSION_ID_UNSET;
audioStreamType = C.STREAM_TYPE_DEFAULT; audioStreamType = C.STREAM_TYPE_DEFAULT;
videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT; videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
@ -393,7 +391,7 @@ public class SimpleExoPlayer implements ExoPlayer {
} }
/** /**
* Returns the audio session identifier, or {@code AudioTrack.SESSION_ID_NOT_SET} if not set. * Returns the audio session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} if not set.
*/ */
public int getAudioSessionId() { public int getAudioSessionId() {
return audioSessionId; return audioSessionId;
@ -449,15 +447,6 @@ public class SimpleExoPlayer implements ExoPlayer {
textOutput = output; textOutput = output;
} }
/**
* @deprecated Use {@link #setMetadataOutput(MetadataRenderer.Output)} instead.
* @param output The output.
*/
@Deprecated
public void setId3Output(MetadataRenderer.Output output) {
setMetadataOutput(output);
}
/** /**
* Sets a listener to receive metadata events. * Sets a listener to receive metadata events.
* *
@ -490,8 +479,8 @@ public class SimpleExoPlayer implements ExoPlayer {
} }
@Override @Override
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetTimeline) { public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
player.prepare(mediaSource, resetPosition, resetTimeline); player.prepare(mediaSource, resetPosition, resetState);
} }
@Override @Override
@ -556,6 +545,36 @@ public class SimpleExoPlayer implements ExoPlayer {
player.blockingSendMessages(messages); player.blockingSendMessages(messages);
} }
@Override
public int getRendererCount() {
return player.getRendererCount();
}
@Override
public int getRendererType(int index) {
return player.getRendererType(index);
}
@Override
public TrackGroupArray getCurrentTrackGroups() {
return player.getCurrentTrackGroups();
}
@Override
public TrackSelectionArray getCurrentTrackSelections() {
return player.getCurrentTrackSelections();
}
@Override
public Timeline getCurrentTimeline() {
return player.getCurrentTimeline();
}
@Override
public Object getCurrentManifest() {
return player.getCurrentManifest();
}
@Override @Override
public int getCurrentPeriodIndex() { public int getCurrentPeriodIndex() {
return player.getCurrentPeriodIndex(); return player.getCurrentPeriodIndex();
@ -587,33 +606,13 @@ public class SimpleExoPlayer implements ExoPlayer {
} }
@Override @Override
public int getRendererCount() { public boolean isCurrentWindowDynamic() {
return player.getRendererCount(); return player.isCurrentWindowDynamic();
} }
@Override @Override
public int getRendererType(int index) { public boolean isCurrentWindowSeekable() {
return player.getRendererType(index); return player.isCurrentWindowSeekable();
}
@Override
public TrackGroupArray getCurrentTrackGroups() {
return player.getCurrentTrackGroups();
}
@Override
public TrackSelectionArray getCurrentTrackSelections() {
return player.getCurrentTrackSelections();
}
@Override
public Timeline getCurrentTimeline() {
return player.getCurrentTimeline();
}
@Override
public Object getCurrentManifest() {
return player.getCurrentManifest();
} }
// Renderer building. // Renderer building.
@ -772,7 +771,7 @@ public class SimpleExoPlayer implements ExoPlayer {
protected void buildMetadataRenderers(Context context, Handler mainHandler, protected void buildMetadataRenderers(Context context, Handler mainHandler,
@ExtensionRendererMode int extensionRendererMode, MetadataRenderer.Output output, @ExtensionRendererMode int extensionRendererMode, MetadataRenderer.Output output,
ArrayList<Renderer> out) { ArrayList<Renderer> out) {
out.add(new MetadataRenderer(output, mainHandler.getLooper(), new Id3Decoder())); out.add(new MetadataRenderer(output, mainHandler.getLooper()));
} }
/** /**
@ -949,7 +948,7 @@ public class SimpleExoPlayer implements ExoPlayer {
} }
audioFormat = null; audioFormat = null;
audioDecoderCounters = null; audioDecoderCounters = null;
audioSessionId = AudioTrack.SESSION_ID_NOT_SET; audioSessionId = C.AUDIO_SESSION_ID_UNSET;
} }
// TextRenderer.Output implementation // TextRenderer.Output implementation

View file

@ -17,6 +17,7 @@ package com.google.android.exoplayer2.audio;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.media.AudioAttributes;
import android.media.AudioFormat; import android.media.AudioFormat;
import android.media.AudioTimestamp; import android.media.AudioTimestamp;
import android.media.PlaybackParams; import android.media.PlaybackParams;
@ -30,28 +31,32 @@ import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/** /**
* Plays audio data. The implementation delegates to an {@link android.media.AudioTrack} and handles * Plays audio data. The implementation delegates to an {@link android.media.AudioTrack} and handles
* playback position smoothing, non-blocking writes and reconfiguration. * playback position smoothing, non-blocking writes and reconfiguration.
* <p> * <p>
* Before starting playback, specify the input format by calling * Before starting playback, specify the input format by calling
* {@link #configure(String, int, int, int, int)}. Next call {@link #initialize(int)}, optionally * {@link #configure(String, int, int, int, int)}. Optionally call {@link #setAudioSessionId(int)},
* specifying an audio session. * {@link #setStreamType(int)}, {@link #enableTunnelingV21(int)} and {@link #disableTunneling()}
* to configure audio playback. These methods may be called after writing data to the track, in
* which case it will be reinitialized as required.
* <p> * <p>
* Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()} * Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()}
* when the data being fed is discontinuous. Call {@link #play()} to start playing the written data. * when the data being fed is discontinuous. Call {@link #play()} to start playing the written data.
* <p> * <p>
* Call {@link #configure(String, int, int, int, int)} whenever the input format changes. If * Call {@link #configure(String, int, int, int, int)} whenever the input format changes. The track
* {@link #isInitialized()} returns {@code false} after the call, it is necessary to call * will be reinitialized on the next call to {@link #handleBuffer(ByteBuffer, long)}.
* {@link #initialize(int)} before writing more data.
* <p> * <p>
* The underlying {@link android.media.AudioTrack} is created by {@link #initialize(int)} and * Calling {@link #reset()} releases the underlying {@link android.media.AudioTrack} (and so does
* released by {@link #reset()} (and {@link #configure(String, int, int, int, int)} unless the input * calling {@link #configure(String, int, int, int, int)} unless the format is unchanged). It is
* format is unchanged). It is safe to call {@link #initialize(int)} after calling {@link #reset()} * safe to call {@link #handleBuffer(ByteBuffer, long)} after {@link #reset()} without calling
* without reconfiguration. * {@link #configure(String, int, int, int, int)}.
* <p> * <p>
* Call {@link #release()} when the instance is no longer required. * Call {@link #handleEndOfStream()} to play out all data when no more input buffers will be
* provided via {@link #handleBuffer(ByteBuffer, long)} until the next {@link #reset}. Call
* {@link #release()} when the instance is no longer required.
*/ */
public final class AudioTrack { public final class AudioTrack {
@ -60,6 +65,19 @@ public final class AudioTrack {
*/ */
public interface Listener { public interface Listener {
/**
* Called when the audio track has been initialized with a newly generated audio session id.
*
* @param audioSessionId The newly generated audio session id.
*/
void onAudioSessionId(int audioSessionId);
/**
* Called when the audio track handles a buffer whose timestamp is discontinuous with the last
* buffer handled since it was reset.
*/
void onPositionDiscontinuity();
/** /**
* Called when the audio track underruns. * Called when the audio track underruns.
* *
@ -104,13 +122,15 @@ public final class AudioTrack {
public static final class WriteException extends Exception { public static final class WriteException extends Exception {
/** /**
* An error value returned from {@link android.media.AudioTrack#write(byte[], int, int)}. * The error value returned from {@link android.media.AudioTrack#write(byte[], int, int)} or
* {@link android.media.AudioTrack#write(ByteBuffer, int, int)}.
*/ */
public final int errorCode; public final int errorCode;
/** /**
* @param errorCode An error value returned from * @param errorCode The error value returned from
* {@link android.media.AudioTrack#write(byte[], int, int)}. * {@link android.media.AudioTrack#write(byte[], int, int)} or
* {@link android.media.AudioTrack#write(ByteBuffer, int, int)}.
*/ */
public WriteException(int errorCode) { public WriteException(int errorCode) {
super("AudioTrack write failed: " + errorCode); super("AudioTrack write failed: " + errorCode);
@ -134,20 +154,6 @@ public final class AudioTrack {
} }
/**
* Returned in the result of {@link #handleBuffer} if the buffer was discontinuous.
*/
public static final int RESULT_POSITION_DISCONTINUITY = 1;
/**
* Returned in the result of {@link #handleBuffer} if the buffer can be released.
*/
public static final int RESULT_BUFFER_CONSUMED = 2;
/**
* Represents an unset {@link android.media.AudioTrack} session identifier.
*/
public static final int SESSION_ID_NOT_SET = 0;
/** /**
* Returned by {@link #getCurrentPositionUs} when the position is not set. * Returned by {@link #getCurrentPositionUs} when the position is not set.
*/ */
@ -210,15 +216,15 @@ public final class AudioTrack {
/** /**
* AudioTrack timestamps are deemed spurious if they are offset from the system clock by more * AudioTrack timestamps are deemed spurious if they are offset from the system clock by more
* than this amount. * than this amount.
* * <p>
* <p>This is a fail safe that should not be required on correctly functioning devices. * This is a fail safe that should not be required on correctly functioning devices.
*/ */
private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 5 * C.MICROS_PER_SECOND; private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 5 * C.MICROS_PER_SECOND;
/** /**
* AudioTrack latencies are deemed impossibly large if they are greater than this amount. * AudioTrack latencies are deemed impossibly large if they are greater than this amount.
* * <p>
* <p>This is a fail safe that should not be required on correctly functioning devices. * This is a fail safe that should not be required on correctly functioning devices.
*/ */
private static final long MAX_LATENCY_US = 5 * C.MICROS_PER_SECOND; private static final long MAX_LATENCY_US = 5 * C.MICROS_PER_SECOND;
@ -255,7 +261,7 @@ public final class AudioTrack {
private final AudioTrackUtil audioTrackUtil; private final AudioTrackUtil audioTrackUtil;
/** /**
* Used to keep the audio session active on pre-V21 builds (see {@link #initialize(int)}). * Used to keep the audio session active on pre-V21 builds (see {@link #initialize()}).
*/ */
private android.media.AudioTrack keepSessionIdAudioTrack; private android.media.AudioTrack keepSessionIdAudioTrack;
@ -273,6 +279,9 @@ public final class AudioTrack {
private int bufferSize; private int bufferSize;
private long bufferSizeUs; private long bufferSizeUs;
private ByteBuffer avSyncHeader;
private int bytesUntilNextAvSync;
private int nextPlayheadOffsetIndex; private int nextPlayheadOffsetIndex;
private int playheadOffsetCount; private int playheadOffsetCount;
private long smoothedPlayheadOffsetUs; private long smoothedPlayheadOffsetUs;
@ -297,6 +306,9 @@ public final class AudioTrack {
private ByteBuffer resampledBuffer; private ByteBuffer resampledBuffer;
private boolean useResampledBuffer; private boolean useResampledBuffer;
private boolean playing;
private int audioSessionId;
private boolean tunneling;
private boolean hasData; private boolean hasData;
private long lastFeedElapsedRealtimeMs; private long lastFeedElapsedRealtimeMs;
@ -327,6 +339,7 @@ public final class AudioTrack {
volume = 1.0f; volume = 1.0f;
startMediaTimeState = START_NOT_SET; startMediaTimeState = START_NOT_SET;
streamType = C.STREAM_TYPE_DEFAULT; streamType = C.STREAM_TYPE_DEFAULT;
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
} }
/** /**
@ -340,14 +353,6 @@ public final class AudioTrack {
&& audioCapabilities.supportsEncoding(getEncodingForMimeType(mimeType)); && audioCapabilities.supportsEncoding(getEncodingForMimeType(mimeType));
} }
/**
* Returns whether the audio track has been successfully initialized via {@link #initialize} and
* not yet {@link #reset}.
*/
public boolean isInitialized() {
return audioTrack != null;
}
/** /**
* Returns the playback position in the stream starting at zero, in microseconds, or * Returns the playback position in the stream starting at zero, in microseconds, or
* {@link #CURRENT_POSITION_NOT_SET} if it is not yet available. * {@link #CURRENT_POSITION_NOT_SET} if it is not yet available.
@ -385,7 +390,7 @@ public final class AudioTrack {
// The AudioTrack has started, but we don't have any samples to compute a smoothed position. // The AudioTrack has started, but we don't have any samples to compute a smoothed position.
currentPositionUs = audioTrackUtil.getPlaybackHeadPositionUs() + startMediaTimeUs; currentPositionUs = audioTrackUtil.getPlaybackHeadPositionUs() + startMediaTimeUs;
} else { } else {
// getPlayheadPositionUs() only has a granularity of ~20ms, so we base the position off the // getPlayheadPositionUs() only has a granularity of ~20 ms, so we base the position off the
// system clock (and a smoothed offset between it and the playhead position) so as to // system clock (and a smoothed offset between it and the playhead position) so as to
// prevent jitter in the reported positions. // prevent jitter in the reported positions.
currentPositionUs = systemClockUs + smoothedPlayheadOffsetUs + startMediaTimeUs; currentPositionUs = systemClockUs + smoothedPlayheadOffsetUs + startMediaTimeUs;
@ -442,7 +447,29 @@ public final class AudioTrack {
throw new IllegalArgumentException("Unsupported channel count: " + channelCount); throw new IllegalArgumentException("Unsupported channel count: " + channelCount);
} }
// Workaround for overly strict channel configuration checks on nVidia Shield.
if (Util.SDK_INT <= 23 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER)) {
switch (channelCount) {
case 7:
channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND;
break;
case 3:
case 5:
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
break;
default:
break;
}
}
boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType); boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType);
// Workaround for Nexus Player not reporting support for mono passthrough.
// (See [Internal: b/34268671].)
if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && passthrough && channelCount == 1) {
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
}
@C.Encoding int sourceEncoding; @C.Encoding int sourceEncoding;
if (passthrough) { if (passthrough) {
sourceEncoding = getEncodingForMimeType(mimeType); sourceEncoding = getEncodingForMimeType(mimeType);
@ -495,14 +522,7 @@ public final class AudioTrack {
bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(pcmBytesToFrames(bufferSize)); bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(pcmBytesToFrames(bufferSize));
} }
/** private void initialize() throws InitializationException {
* Initializes the audio track for writing new buffers using {@link #handleBuffer}.
*
* @param sessionId Audio track session identifier to re-use, or {@link #SESSION_ID_NOT_SET} to
* create a new one.
* @return The new (or re-used) session identifier.
*/
public int initialize(int sessionId) throws InitializationException {
// If we're asynchronously releasing a previous audio track then we block until it has been // If we're asynchronously releasing a previous audio track then we block until it has been
// released. This guarantees that we cannot end up in a state where we have multiple audio // released. This guarantees that we cannot end up in a state where we have multiple audio
// track instances. Without this guarantee it would be possible, in extreme cases, to exhaust // track instances. Without this guarantee it would be possible, in extreme cases, to exhaust
@ -510,23 +530,26 @@ public final class AudioTrack {
// initialization of the audio track to fail. // initialization of the audio track to fail.
releasingConditionVariable.block(); releasingConditionVariable.block();
if (sessionId == SESSION_ID_NOT_SET) { if (tunneling) {
audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, targetEncoding,
bufferSize, audioSessionId);
} else if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
targetEncoding, bufferSize, MODE_STREAM); targetEncoding, bufferSize, MODE_STREAM);
} else { } else {
// Re-attach to the same audio session. // Re-attach to the same audio session.
audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig, audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
targetEncoding, bufferSize, MODE_STREAM, sessionId); targetEncoding, bufferSize, MODE_STREAM, audioSessionId);
} }
checkAudioTrackInitialized(); checkAudioTrackInitialized();
sessionId = audioTrack.getAudioSessionId(); int audioSessionId = audioTrack.getAudioSessionId();
if (enablePreV21AudioSessionWorkaround) { if (enablePreV21AudioSessionWorkaround) {
if (Util.SDK_INT < 21) { if (Util.SDK_INT < 21) {
// The workaround creates an audio track with a two byte buffer on the same session, and // The workaround creates an audio track with a two byte buffer on the same session, and
// does not release it until this object is released, which keeps the session active. // does not release it until this object is released, which keeps the session active.
if (keepSessionIdAudioTrack != null if (keepSessionIdAudioTrack != null
&& sessionId != keepSessionIdAudioTrack.getAudioSessionId()) { && audioSessionId != keepSessionIdAudioTrack.getAudioSessionId()) {
releaseKeepSessionIdAudioTrack(); releaseKeepSessionIdAudioTrack();
} }
if (keepSessionIdAudioTrack == null) { if (keepSessionIdAudioTrack == null) {
@ -535,21 +558,25 @@ public final class AudioTrack {
@C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT; @C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT;
int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback. int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback.
keepSessionIdAudioTrack = new android.media.AudioTrack(streamType, sampleRate, keepSessionIdAudioTrack = new android.media.AudioTrack(streamType, sampleRate,
channelConfig, encoding, bufferSize, MODE_STATIC, sessionId); channelConfig, encoding, bufferSize, MODE_STATIC, audioSessionId);
} }
} }
} }
if (this.audioSessionId != audioSessionId) {
this.audioSessionId = audioSessionId;
listener.onAudioSessionId(audioSessionId);
}
audioTrackUtil.reconfigure(audioTrack, needsPassthroughWorkarounds()); audioTrackUtil.reconfigure(audioTrack, needsPassthroughWorkarounds());
setAudioTrackVolume(); setVolumeInternal();
hasData = false; hasData = false;
return sessionId;
} }
/** /**
* Starts or resumes playing audio if the audio track has been initialized. * Starts or resumes playing audio if the audio track has been initialized.
*/ */
public void play() { public void play() {
playing = true;
if (isInitialized()) { if (isInitialized()) {
resumeSystemTimeUs = System.nanoTime() / 1000; resumeSystemTimeUs = System.nanoTime() / 1000;
audioTrack.play(); audioTrack.play();
@ -570,35 +597,42 @@ public final class AudioTrack {
* Attempts to write data from a {@link ByteBuffer} to the audio track, starting from its current * Attempts to write data from a {@link ByteBuffer} to the audio track, starting from its current
* position and ending at its limit (exclusive). The position of the {@link ByteBuffer} is * position and ending at its limit (exclusive). The position of the {@link ByteBuffer} is
* advanced by the number of bytes that were successfully written. * advanced by the number of bytes that were successfully written.
* {@link Listener#onPositionDiscontinuity()} will be called if {@code presentationTimeUs} is
* discontinuous with the last buffer handled since the track was reset.
* <p> * <p>
* Returns a bit field containing {@link #RESULT_BUFFER_CONSUMED} if the data was written in full, * Returns whether the data was written in full. If the data was not written in full then the same
* and {@link #RESULT_POSITION_DISCONTINUITY} if the buffer was discontinuous with previously * {@link ByteBuffer} must be provided to subsequent calls until it has been fully consumed,
* written data. * except in the case of an interleaving call to {@link #reset()} (or an interleaving call to
* <p> * {@link #configure(String, int, int, int, int)} that caused the track to be reset).
* If the data was not written in full then the same {@link ByteBuffer} must be provided to
* subsequent calls until it has been fully consumed, except in the case of an interleaving call
* to {@link #configure} or {@link #reset}.
* *
* @param buffer The buffer containing audio data to play back. * @param buffer The buffer containing audio data to play back.
* @param presentationTimeUs Presentation timestamp of the next buffer in microseconds. * @param presentationTimeUs Presentation timestamp of the next buffer in microseconds.
* @return A bit field with {@link #RESULT_BUFFER_CONSUMED} if the buffer can be released, and * @return Whether the buffer was consumed fully.
* {@link #RESULT_POSITION_DISCONTINUITY} if the buffer was not contiguous with previously * @throws InitializationException If an error occurs initializing the track.
* written data.
* @throws WriteException If an error occurs writing the audio data. * @throws WriteException If an error occurs writing the audio data.
*/ */
public int handleBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException { public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs)
throws InitializationException, WriteException {
if (!isInitialized()) {
initialize();
if (playing) {
play();
}
}
boolean hadData = hasData; boolean hadData = hasData;
hasData = hasPendingData(); hasData = hasPendingData();
if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) { if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) {
long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs;
listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs); listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs);
} }
int result = writeBuffer(buffer, presentationTimeUs); boolean result = writeBuffer(buffer, presentationTimeUs);
lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime();
return result; return result;
} }
private int writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException { @SuppressWarnings("ReferenceEquality")
private boolean writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException {
boolean isNewSourceBuffer = currentSourceBuffer == null; boolean isNewSourceBuffer = currentSourceBuffer == null;
Assertions.checkState(isNewSourceBuffer || currentSourceBuffer == buffer); Assertions.checkState(isNewSourceBuffer || currentSourceBuffer == buffer);
currentSourceBuffer = buffer; currentSourceBuffer = buffer;
@ -607,7 +641,7 @@ public final class AudioTrack {
// An AC-3 audio track continues to play data written while it is paused. Stop writing so its // An AC-3 audio track continues to play data written while it is paused. Stop writing so its
// buffer empties. See [Internal: b/18899620]. // buffer empties. See [Internal: b/18899620].
if (audioTrack.getPlayState() == PLAYSTATE_PAUSED) { if (audioTrack.getPlayState() == PLAYSTATE_PAUSED) {
return 0; return false;
} }
// A new AC-3 audio track's playback position continues to increase from the old track's // A new AC-3 audio track's playback position continues to increase from the old track's
@ -615,18 +649,17 @@ public final class AudioTrack {
// head position actually returns to zero. // head position actually returns to zero.
if (audioTrack.getPlayState() == PLAYSTATE_STOPPED if (audioTrack.getPlayState() == PLAYSTATE_STOPPED
&& audioTrackUtil.getPlaybackHeadPosition() != 0) { && audioTrackUtil.getPlaybackHeadPosition() != 0) {
return 0; return false;
} }
} }
int result = 0;
if (isNewSourceBuffer) { if (isNewSourceBuffer) {
// We're seeing this buffer for the first time. // We're seeing this buffer for the first time.
if (!currentSourceBuffer.hasRemaining()) { if (!currentSourceBuffer.hasRemaining()) {
// The buffer is empty. // The buffer is empty.
currentSourceBuffer = null; currentSourceBuffer = null;
return RESULT_BUFFER_CONSUMED; return true;
} }
useResampledBuffer = targetEncoding != sourceEncoding; useResampledBuffer = targetEncoding != sourceEncoding;
@ -659,7 +692,7 @@ public final class AudioTrack {
// number of bytes submitted. // number of bytes submitted.
startMediaTimeUs += (presentationTimeUs - expectedPresentationTimeUs); startMediaTimeUs += (presentationTimeUs - expectedPresentationTimeUs);
startMediaTimeState = START_IN_SYNC; startMediaTimeState = START_IN_SYNC;
result |= RESULT_POSITION_DISCONTINUITY; listener.onPositionDiscontinuity();
} }
} }
if (Util.SDK_INT < 21) { if (Util.SDK_INT < 21) {
@ -692,7 +725,9 @@ public final class AudioTrack {
buffer.position(buffer.position() + bytesWritten); buffer.position(buffer.position() + bytesWritten);
} }
} else { } else {
bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining); bytesWritten = tunneling
? writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, presentationTimeUs)
: writeNonBlockingV21(audioTrack, buffer, bytesRemaining);
} }
if (bytesWritten < 0) { if (bytesWritten < 0) {
@ -707,9 +742,9 @@ public final class AudioTrack {
submittedEncodedFrames += framesPerEncodedSample; submittedEncodedFrames += framesPerEncodedSample;
} }
currentSourceBuffer = null; currentSourceBuffer = null;
result |= RESULT_BUFFER_CONSUMED; return true;
} }
return result; return false;
} }
/** /**
@ -718,6 +753,7 @@ public final class AudioTrack {
public void handleEndOfStream() { public void handleEndOfStream() {
if (isInitialized()) { if (isInitialized()) {
audioTrackUtil.handleEndOfStream(getSubmittedFrames()); audioTrackUtil.handleEndOfStream(getSubmittedFrames());
bytesUntilNextAvSync = 0;
} }
} }
@ -743,21 +779,65 @@ public final class AudioTrack {
} }
/** /**
* Sets the stream type for audio track. If the stream type has changed, {@link #isInitialized()} * Sets the stream type for audio track. If the stream type has changed and if the audio track
* will return {@code false} and the caller must re-{@link #initialize(int)} the audio track * is not configured for use with tunneling, then the audio track is reset and the audio session
* before writing more data. The caller must not reuse the audio session identifier when * id is cleared.
* re-initializing with a new stream type. * <p>
* If the audio track is configured for use with tunneling then the stream type is ignored, the
* audio track is not reset and the audio session id is not cleared. The passed stream type will
* be used if the audio track is later re-configured into non-tunneled mode.
* *
* @param streamType The {@link C.StreamType} to use for audio output. * @param streamType The {@link C.StreamType} to use for audio output.
* @return Whether the stream type changed.
*/ */
public boolean setStreamType(@C.StreamType int streamType) { public void setStreamType(@C.StreamType int streamType) {
if (this.streamType == streamType) { if (this.streamType == streamType) {
return false; return;
} }
this.streamType = streamType; this.streamType = streamType;
if (tunneling) {
// The stream type is ignored in tunneling mode, so no need to reset.
return;
}
reset(); reset();
return true; audioSessionId = C.AUDIO_SESSION_ID_UNSET;
}
/**
* Sets the audio session id. The audio track is reset if the audio session id has changed.
*/
public void setAudioSessionId(int audioSessionId) {
if (this.audioSessionId != audioSessionId) {
this.audioSessionId = audioSessionId;
reset();
}
}
/**
* Enables tunneling. The audio track is reset if tunneling was previously disabled or if the
* audio session id has changed. Enabling tunneling requires platform API version 21 onwards.
*
* @param tunnelingAudioSessionId The audio session id to use.
* @throws IllegalStateException Thrown if enabling tunneling on platform API version < 21.
*/
public void enableTunnelingV21(int tunnelingAudioSessionId) {
Assertions.checkState(Util.SDK_INT >= 21);
if (!tunneling || audioSessionId != tunnelingAudioSessionId) {
tunneling = true;
audioSessionId = tunnelingAudioSessionId;
reset();
}
}
/**
* Disables tunneling. If tunneling was previously enabled then the audio track is reset and the
* audio session id is cleared.
*/
public void disableTunneling() {
if (tunneling) {
tunneling = false;
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
reset();
}
} }
/** /**
@ -768,17 +848,17 @@ public final class AudioTrack {
public void setVolume(float volume) { public void setVolume(float volume) {
if (this.volume != volume) { if (this.volume != volume) {
this.volume = volume; this.volume = volume;
setAudioTrackVolume(); setVolumeInternal();
} }
} }
private void setAudioTrackVolume() { private void setVolumeInternal() {
if (!isInitialized()) { if (!isInitialized()) {
// Do nothing. // Do nothing.
} else if (Util.SDK_INT >= 21) { } else if (Util.SDK_INT >= 21) {
setAudioTrackVolumeV21(audioTrack, volume); setVolumeInternalV21(audioTrack, volume);
} else { } else {
setAudioTrackVolumeV3(audioTrack, volume); setVolumeInternalV3(audioTrack, volume);
} }
} }
@ -786,6 +866,7 @@ public final class AudioTrack {
* Pauses playback. * Pauses playback.
*/ */
public void pause() { public void pause() {
playing = false;
if (isInitialized()) { if (isInitialized()) {
resetSyncParams(); resetSyncParams();
audioTrackUtil.pause(); audioTrackUtil.pause();
@ -795,9 +876,9 @@ public final class AudioTrack {
/** /**
* Releases the underlying audio track asynchronously. * Releases the underlying audio track asynchronously.
* <p> * <p>
* Calling {@link #initialize(int)} will block until the audio track has been released, so it is * Calling {@link #handleBuffer(ByteBuffer, long)} will block until the audio track has been
* safe to initialize immediately after a reset. The audio session may remain active until * released, so it is safe to use the audio track immediately after a reset. The audio session may
* {@link #release()} is called. * remain active until {@link #release()} is called.
*/ */
public void reset() { public void reset() {
if (isInitialized()) { if (isInitialized()) {
@ -805,6 +886,8 @@ public final class AudioTrack {
submittedEncodedFrames = 0; submittedEncodedFrames = 0;
framesPerEncodedSample = 0; framesPerEncodedSample = 0;
currentSourceBuffer = null; currentSourceBuffer = null;
avSyncHeader = null;
bytesUntilNextAvSync = 0;
startMediaTimeState = START_NOT_SET; startMediaTimeState = START_NOT_SET;
latencyUs = 0; latencyUs = 0;
resetSyncParams(); resetSyncParams();
@ -837,6 +920,8 @@ public final class AudioTrack {
public void release() { public void release() {
reset(); reset();
releaseKeepSessionIdAudioTrack(); releaseKeepSessionIdAudioTrack();
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
playing = false;
} }
/** /**
@ -974,6 +1059,10 @@ public final class AudioTrack {
throw new InitializationException(state, sampleRate, channelConfig, bufferSize); throw new InitializationException(state, sampleRate, channelConfig, bufferSize);
} }
private boolean isInitialized() {
return audioTrack != null;
}
private long pcmBytesToFrames(long byteCount) { private long pcmBytesToFrames(long byteCount) {
return byteCount / pcmFrameSize; return byteCount / pcmFrameSize;
} }
@ -1020,6 +1109,26 @@ public final class AudioTrack {
&& audioTrack.getPlaybackHeadPosition() == 0; && audioTrack.getPlaybackHeadPosition() == 0;
} }
/**
* Instantiates an {@link android.media.AudioTrack} to be used with tunneling video playback.
*/
@TargetApi(21)
private static android.media.AudioTrack createHwAvSyncAudioTrackV21(int sampleRate,
int channelConfig, int encoding, int bufferSize, int sessionId) {
AudioAttributes attributesBuilder = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
.setFlags(AudioAttributes.FLAG_HW_AV_SYNC)
.build();
AudioFormat format = new AudioFormat.Builder()
.setChannelMask(channelConfig)
.setEncoding(encoding)
.setSampleRate(sampleRate)
.build();
return new android.media.AudioTrack(attributesBuilder, format, bufferSize, MODE_STREAM,
sessionId);
}
/** /**
* Converts the provided buffer into 16-bit PCM. * Converts the provided buffer into 16-bit PCM.
* *
@ -1125,18 +1234,57 @@ public final class AudioTrack {
} }
@TargetApi(21) @TargetApi(21)
private static int writeNonBlockingV21( private static int writeNonBlockingV21(android.media.AudioTrack audioTrack, ByteBuffer buffer,
android.media.AudioTrack audioTrack, ByteBuffer buffer, int size) { int size) {
return audioTrack.write(buffer, size, WRITE_NON_BLOCKING); return audioTrack.write(buffer, size, WRITE_NON_BLOCKING);
} }
@TargetApi(21) @TargetApi(21)
private static void setAudioTrackVolumeV21(android.media.AudioTrack audioTrack, float volume) { private int writeNonBlockingWithAvSyncV21(android.media.AudioTrack audioTrack,
ByteBuffer buffer, int size, long presentationTimeUs) {
// TODO: Uncomment this when [Internal ref b/33627517] is clarified or fixed.
// if (Util.SDK_INT >= 23) {
// // The underlying platform AudioTrack writes AV sync headers directly.
// return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000);
// }
if (avSyncHeader == null) {
avSyncHeader = ByteBuffer.allocate(16);
avSyncHeader.order(ByteOrder.BIG_ENDIAN);
avSyncHeader.putInt(0x55550001);
}
if (bytesUntilNextAvSync == 0) {
avSyncHeader.putInt(4, size);
avSyncHeader.putLong(8, presentationTimeUs * 1000);
avSyncHeader.position(0);
bytesUntilNextAvSync = size;
}
int avSyncHeaderBytesRemaining = avSyncHeader.remaining();
if (avSyncHeaderBytesRemaining > 0) {
int result = audioTrack.write(avSyncHeader, avSyncHeaderBytesRemaining, WRITE_NON_BLOCKING);
if (result < 0) {
bytesUntilNextAvSync = 0;
return result;
}
if (result < avSyncHeaderBytesRemaining) {
return 0;
}
}
int result = writeNonBlockingV21(audioTrack, buffer, size);
if (result < 0) {
bytesUntilNextAvSync = 0;
return result;
}
bytesUntilNextAvSync -= result;
return result;
}
@TargetApi(21)
private static void setVolumeInternalV21(android.media.AudioTrack audioTrack, float volume) {
audioTrack.setVolume(volume); audioTrack.setVolume(volume);
} }
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
private static void setAudioTrackVolumeV3(android.media.AudioTrack audioTrack, float volume) { private static void setVolumeInternalV3(android.media.AudioTrack audioTrack, float volume) {
audioTrack.setStereoVolume(volume, volume); audioTrack.setStereoVolume(volume, volume);
} }
@ -1385,7 +1533,7 @@ public final class AudioTrack {
playbackParams = (playbackParams != null ? playbackParams : new PlaybackParams()) playbackParams = (playbackParams != null ? playbackParams : new PlaybackParams())
.allowDefaults(); .allowDefaults();
this.playbackParams = playbackParams; this.playbackParams = playbackParams;
this.playbackSpeed = playbackParams.getSpeed(); playbackSpeed = playbackParams.getSpeed();
maybeApplyPlaybackParams(); maybeApplyPlaybackParams();
} }

View file

@ -41,8 +41,7 @@ import java.nio.ByteBuffer;
* Decodes and renders audio using {@link MediaCodec} and {@link AudioTrack}. * Decodes and renders audio using {@link MediaCodec} and {@link AudioTrack}.
*/ */
@TargetApi(16) @TargetApi(16)
public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock, public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock {
AudioTrack.Listener {
private final EventDispatcher eventDispatcher; private final EventDispatcher eventDispatcher;
private final AudioTrack audioTrack; private final AudioTrack audioTrack;
@ -50,7 +49,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
private boolean passthroughEnabled; private boolean passthroughEnabled;
private android.media.MediaFormat passthroughMediaFormat; private android.media.MediaFormat passthroughMediaFormat;
private int pcmEncoding; private int pcmEncoding;
private int audioSessionId;
private long currentPositionUs; private long currentPositionUs;
private boolean allowPositionDiscontinuity; private boolean allowPositionDiscontinuity;
@ -129,8 +127,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
boolean playClearSamplesWithoutKeys, Handler eventHandler, boolean playClearSamplesWithoutKeys, Handler eventHandler,
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) { AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) {
super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys);
audioSessionId = AudioTrack.SESSION_ID_NOT_SET; audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener());
audioTrack = new AudioTrack(audioCapabilities, this);
eventDispatcher = new EventDispatcher(eventHandler, eventListener); eventDispatcher = new EventDispatcher(eventHandler, eventListener);
} }
@ -141,10 +138,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
if (!MimeTypes.isAudio(mimeType)) { if (!MimeTypes.isAudio(mimeType)) {
return FORMAT_UNSUPPORTED_TYPE; return FORMAT_UNSUPPORTED_TYPE;
} }
int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;
if (allowPassthrough(mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null) { if (allowPassthrough(mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null) {
return ADAPTIVE_NOT_SEAMLESS | FORMAT_HANDLED; return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED;
} }
MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, false, false); MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, false);
if (decoderInfo == null) { if (decoderInfo == null) {
return FORMAT_UNSUPPORTED_SUBTYPE; return FORMAT_UNSUPPORTED_SUBTYPE;
} }
@ -155,7 +153,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
&& (format.channelCount == Format.NO_VALUE && (format.channelCount == Format.NO_VALUE
|| decoderInfo.isAudioChannelCountSupportedV21(format.channelCount))); || decoderInfo.isAudioChannelCountSupportedV21(format.channelCount)));
int formatSupport = decoderCapable ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES; int formatSupport = decoderCapable ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES;
return ADAPTIVE_NOT_SEAMLESS | formatSupport; return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport;
} }
@Override @Override
@ -185,7 +183,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
} }
@Override @Override
protected void configureCodec(MediaCodec codec, Format format, MediaCrypto crypto) { protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
MediaCrypto crypto) {
if (passthroughEnabled) { if (passthroughEnabled) {
// Override the MIME type used to configure the codec if we are using a passthrough decoder. // Override the MIME type used to configure the codec if we are using a passthrough decoder.
passthroughMediaFormat = format.getFrameworkMediaFormatV16(); passthroughMediaFormat = format.getFrameworkMediaFormatV16();
@ -231,25 +230,42 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
} }
/** /**
* Called when the audio session id becomes known. Once the id is known it will not change (and * Called when the audio session id becomes known. The default implementation is a no-op. One
* hence this method will not be called again) unless the renderer is disabled and then * reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in
* subsequently re-enabled. * order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances
* <p> * should be released in {@link #onDisabled()} (if not before).
* The default implementation is a no-op. One reason for overriding this method would be to
* instantiate and enable a {@link Virtualizer} in order to spatialize the audio channels. For
* this use case, any {@link Virtualizer} instances should be released in {@link #onDisabled()}
* (if not before).
* *
* @param audioSessionId The audio session id. * @see AudioTrack.Listener#onAudioSessionId(int)
*/ */
protected void onAudioSessionId(int audioSessionId) { protected void onAudioSessionId(int audioSessionId) {
// Do nothing. // Do nothing.
} }
/**
* @see AudioTrack.Listener#onPositionDiscontinuity()
*/
protected void onAudioTrackPositionDiscontinuity() {
// Do nothing.
}
/**
* @see AudioTrack.Listener#onUnderrun(int, long, long)
*/
protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs,
long elapsedSinceLastFeedMs) {
// Do nothing.
}
@Override @Override
protected void onEnabled(boolean joining) throws ExoPlaybackException { protected void onEnabled(boolean joining) throws ExoPlaybackException {
super.onEnabled(joining); super.onEnabled(joining);
eventDispatcher.enabled(decoderCounters); eventDispatcher.enabled(decoderCounters);
int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;
if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {
audioTrack.enableTunnelingV21(tunnelingAudioSessionId);
} else {
audioTrack.disableTunneling();
}
} }
@Override @Override
@ -274,7 +290,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
@Override @Override
protected void onDisabled() { protected void onDisabled() {
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
try { try {
audioTrack.release(); audioTrack.release();
} finally { } finally {
@ -325,44 +340,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
return true; return true;
} }
if (!audioTrack.isInitialized()) {
// Initialize the AudioTrack now.
try {
if (audioSessionId == AudioTrack.SESSION_ID_NOT_SET) {
audioSessionId = audioTrack.initialize(AudioTrack.SESSION_ID_NOT_SET);
eventDispatcher.audioSessionId(audioSessionId);
onAudioSessionId(audioSessionId);
} else {
audioTrack.initialize(audioSessionId);
}
} catch (AudioTrack.InitializationException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex());
}
if (getState() == STATE_STARTED) {
audioTrack.play();
}
}
int handleBufferResult;
try { try {
handleBufferResult = audioTrack.handleBuffer(buffer, bufferPresentationTimeUs); if (audioTrack.handleBuffer(buffer, bufferPresentationTimeUs)) {
} catch (AudioTrack.WriteException e) { codec.releaseOutputBuffer(bufferIndex, false);
decoderCounters.renderedOutputBufferCount++;
return true;
}
} catch (AudioTrack.InitializationException | AudioTrack.WriteException e) {
throw ExoPlaybackException.createForRenderer(e, getIndex()); throw ExoPlaybackException.createForRenderer(e, getIndex());
} }
// If we are out of sync, allow currentPositionUs to jump backwards.
if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) {
handleAudioTrackDiscontinuity();
allowPositionDiscontinuity = true;
}
// Release the buffer if it was consumed.
if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) {
codec.releaseOutputBuffer(bufferIndex, false);
decoderCounters.renderedOutputBufferCount++;
return true;
}
return false; return false;
} }
@ -371,10 +357,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
audioTrack.handleEndOfStream(); audioTrack.handleEndOfStream();
} }
protected void handleAudioTrackDiscontinuity() {
// Do nothing
}
@Override @Override
public void handleMessage(int messageType, Object message) throws ExoPlaybackException { public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
switch (messageType) { switch (messageType) {
@ -386,9 +368,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
break; break;
case C.MSG_SET_STREAM_TYPE: case C.MSG_SET_STREAM_TYPE:
@C.StreamType int streamType = (Integer) message; @C.StreamType int streamType = (Integer) message;
if (audioTrack.setStreamType(streamType)) { audioTrack.setStreamType(streamType);
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
}
break; break;
default: default:
super.handleMessage(messageType, message); super.handleMessage(messageType, message);
@ -396,11 +376,27 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
} }
} }
// AudioTrack.Listener implementation. private final class AudioTrackListener implements AudioTrack.Listener {
@Override
public void onAudioSessionId(int audioSessionId) {
eventDispatcher.audioSessionId(audioSessionId);
MediaCodecAudioRenderer.this.onAudioSessionId(audioSessionId);
}
@Override
public void onPositionDiscontinuity() {
onAudioTrackPositionDiscontinuity();
// We are out of sync so allow currentPositionUs to jump backwards.
MediaCodecAudioRenderer.this.allowPositionDiscontinuity = true;
}
@Override
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
}
@Override
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
} }
} }

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.audio; package com.google.android.exoplayer2.audio;
import android.media.PlaybackParams; import android.media.PlaybackParams;
import android.media.audiofx.Virtualizer;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.SystemClock; import android.os.SystemClock;
@ -43,8 +44,7 @@ import java.lang.annotation.RetentionPolicy;
/** /**
* Decodes and renders audio using a {@link SimpleDecoder}. * Decodes and renders audio using a {@link SimpleDecoder}.
*/ */
public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock, public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock {
AudioTrack.Listener {
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef({REINITIALIZATION_STATE_NONE, REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM, @IntDef({REINITIALIZATION_STATE_NONE, REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM,
@ -94,8 +94,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
private boolean outputStreamEnded; private boolean outputStreamEnded;
private boolean waitingForKeys; private boolean waitingForKeys;
private int audioSessionId;
public SimpleDecoderAudioRenderer() { public SimpleDecoderAudioRenderer() {
this(null, null); this(null, null);
} }
@ -141,11 +139,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys) { DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys) {
super(C.TRACK_TYPE_AUDIO); super(C.TRACK_TYPE_AUDIO);
eventDispatcher = new EventDispatcher(eventHandler, eventListener); eventDispatcher = new EventDispatcher(eventHandler, eventListener);
audioTrack = new AudioTrack(audioCapabilities, this); audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener());
this.drmSessionManager = drmSessionManager; this.drmSessionManager = drmSessionManager;
formatHolder = new FormatHolder(); formatHolder = new FormatHolder();
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
decoderReinitializationState = REINITIALIZATION_STATE_NONE; decoderReinitializationState = REINITIALIZATION_STATE_NONE;
audioTrackNeedsConfigure = true; audioTrackNeedsConfigure = true;
} }
@ -155,6 +152,25 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
return this; return this;
} }
@Override
public final int supportsFormat(Format format) {
int formatSupport = supportsFormatInternal(format);
if (formatSupport == FORMAT_UNSUPPORTED_TYPE || formatSupport == FORMAT_UNSUPPORTED_SUBTYPE) {
return formatSupport;
}
int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;
return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport;
}
/**
* Returns the {@link #FORMAT_SUPPORT_MASK} component of the return value for
* {@link #supportsFormat(Format)}.
*
* @param format The format.
* @return The extent to which the renderer supports the format itself.
*/
protected abstract int supportsFormatInternal(Format format);
@Override @Override
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
if (outputStreamEnded) { if (outputStreamEnded) {
@ -185,6 +201,33 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
} }
} }
/**
* Called when the audio session id becomes known. The default implementation is a no-op. One
* reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in
* order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances
* should be released in {@link #onDisabled()} (if not before).
*
* @see AudioTrack.Listener#onAudioSessionId(int)
*/
protected void onAudioSessionId(int audioSessionId) {
// Do nothing.
}
/**
* @see AudioTrack.Listener#onPositionDiscontinuity()
*/
protected void onAudioTrackPositionDiscontinuity() {
// Do nothing.
}
/**
* @see AudioTrack.Listener#onUnderrun(int, long, long)
*/
protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs,
long elapsedSinceLastFeedMs) {
// Do nothing.
}
/** /**
* Creates a decoder for the given format. * Creates a decoder for the given format.
* *
@ -244,28 +287,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
audioTrackNeedsConfigure = false; audioTrackNeedsConfigure = false;
} }
if (!audioTrack.isInitialized()) { if (audioTrack.handleBuffer(outputBuffer.data, outputBuffer.timeUs)) {
if (audioSessionId == AudioTrack.SESSION_ID_NOT_SET) {
audioSessionId = audioTrack.initialize(AudioTrack.SESSION_ID_NOT_SET);
eventDispatcher.audioSessionId(audioSessionId);
onAudioSessionId(audioSessionId);
} else {
audioTrack.initialize(audioSessionId);
}
if (getState() == STATE_STARTED) {
audioTrack.play();
}
}
int handleBufferResult = audioTrack.handleBuffer(outputBuffer.data, outputBuffer.timeUs);
// If we are out of sync, allow currentPositionUs to jump backwards.
if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) {
allowPositionDiscontinuity = true;
}
// Release the buffer if it was consumed.
if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) {
decoderCounters.renderedOutputBufferCount++; decoderCounters.renderedOutputBufferCount++;
outputBuffer.release(); outputBuffer.release();
outputBuffer = null; outputBuffer = null;
@ -381,23 +403,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
return currentPositionUs; return currentPositionUs;
} }
/**
* Called when the audio session id becomes known. Once the id is known it will not change (and
* hence this method will not be called again) unless the renderer is disabled and then
* subsequently re-enabled.
* <p>
* The default implementation is a no-op.
*
* @param audioSessionId The audio session id.
*/
protected void onAudioSessionId(int audioSessionId) {
// Do nothing.
}
@Override @Override
protected void onEnabled(boolean joining) throws ExoPlaybackException { protected void onEnabled(boolean joining) throws ExoPlaybackException {
decoderCounters = new DecoderCounters(); decoderCounters = new DecoderCounters();
eventDispatcher.enabled(decoderCounters); eventDispatcher.enabled(decoderCounters);
int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;
if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {
audioTrack.enableTunnelingV21(tunnelingAudioSessionId);
} else {
audioTrack.disableTunneling();
}
} }
@Override @Override
@ -425,7 +440,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
@Override @Override
protected void onDisabled() { protected void onDisabled() {
inputFormat = null; inputFormat = null;
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
audioTrackNeedsConfigure = true; audioTrackNeedsConfigure = true;
waitingForKeys = false; waitingForKeys = false;
try { try {
@ -537,6 +551,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
// There aren't any final output buffers, so release the decoder immediately. // There aren't any final output buffers, so release the decoder immediately.
releaseDecoder(); releaseDecoder();
maybeInitDecoder(); maybeInitDecoder();
audioTrackNeedsConfigure = true;
} }
eventDispatcher.inputFormatChanged(newFormat); eventDispatcher.inputFormatChanged(newFormat);
@ -553,9 +568,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
break; break;
case C.MSG_SET_STREAM_TYPE: case C.MSG_SET_STREAM_TYPE:
@C.StreamType int streamType = (Integer) message; @C.StreamType int streamType = (Integer) message;
if (audioTrack.setStreamType(streamType)) { audioTrack.setStreamType(streamType);
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
}
break; break;
default: default:
super.handleMessage(messageType, message); super.handleMessage(messageType, message);
@ -563,11 +576,27 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
} }
} }
// AudioTrack.Listener implementation. private final class AudioTrackListener implements AudioTrack.Listener {
@Override
public void onAudioSessionId(int audioSessionId) {
eventDispatcher.audioSessionId(audioSessionId);
SimpleDecoderAudioRenderer.this.onAudioSessionId(audioSessionId);
}
@Override
public void onPositionDiscontinuity() {
onAudioTrackPositionDiscontinuity();
// We are out of sync so allow currentPositionUs to jump backwards.
SimpleDecoderAudioRenderer.this.allowPositionDiscontinuity = true;
}
@Override
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
}
@Override
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
} }
} }

View file

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

View file

@ -16,9 +16,11 @@
package com.google.android.exoplayer2.drm; package com.google.android.exoplayer2.drm;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.media.MediaDrm;
import android.support.annotation.IntDef; import android.support.annotation.IntDef;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.Map;
/** /**
* A DRM session. * A DRM session.
@ -26,6 +28,15 @@ import java.lang.annotation.RetentionPolicy;
@TargetApi(16) @TargetApi(16)
public interface DrmSession<T extends ExoMediaCrypto> { public interface DrmSession<T extends ExoMediaCrypto> {
/** Wraps the exception which is the cause of the error state. */
class DrmSessionException extends Exception {
DrmSessionException(Exception e) {
super(e);
}
}
/** /**
* The state of the DRM session. * The state of the DRM session.
*/ */
@ -96,6 +107,26 @@ public interface DrmSession<T extends ExoMediaCrypto> {
* *
* @return An exception if the state is {@link #STATE_ERROR}. Null otherwise. * @return An exception if the state is {@link #STATE_ERROR}. Null otherwise.
*/ */
Exception getError(); DrmSessionException getError();
/**
* Returns an informative description of the key status for the session. The status is in the form
* of {name, value} pairs.
*
* <p>Since DRM license policies vary by vendor, the specific status field names are determined by
* each DRM vendor. Refer to your DRM provider documentation for definitions of the field names
* for a particular DRM engine plugin.
*
* @return A map of key status.
* @throws IllegalStateException If called when the session isn't opened.
* @see MediaDrm#queryKeyStatus(byte[])
*/
Map<String, String> queryKeyStatus();
/**
* Returns the key set id of the offline license loaded into this session, if there is one. Null
* otherwise.
*/
byte[] getOfflineLicenseKeySetId();
} }

View file

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

View file

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

View file

@ -0,0 +1,62 @@
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.drm;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import java.util.Map;
/**
* Utility methods for Widevine.
*/
public final class WidevineUtil {
/** Widevine specific key status field name for the remaining license duration, in seconds. */
public static final String PROPERTY_LICENSE_DURATION_REMAINING = "LicenseDurationRemaining";
/** Widevine specific key status field name for the remaining playback duration, in seconds. */
public static final String PROPERTY_PLAYBACK_DURATION_REMAINING = "PlaybackDurationRemaining";
private WidevineUtil() {}
/**
* Returns license and playback durations remaining in seconds.
*
* @return A {@link Pair} consisting of the remaining license and playback durations in seconds.
* @throws IllegalStateException If called when a session isn't opened.
* @param drmSession
*/
public static Pair<Long, Long> getLicenseDurationRemainingSec(DrmSession drmSession) {
Map<String, String> keyStatus = drmSession.queryKeyStatus();
return new Pair<>(
getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING),
getDurationRemainingSec(keyStatus, PROPERTY_PLAYBACK_DURATION_REMAINING));
}
private static long getDurationRemainingSec(Map<String, String> keyStatus, String property) {
if (keyStatus != null) {
try {
String value = keyStatus.get(property);
if (value != null) {
return Long.parseLong(value);
}
} catch (NumberFormatException e) {
// do nothing.
}
}
return C.TIME_UNSET;
}
}

View file

@ -226,13 +226,32 @@ public final class DefaultTrackOutput implements TrackOutput {
} }
/** /**
* Attempts to skip to the keyframe before the specified time, if it's present in the buffer. * Attempts to skip to the keyframe before or at the specified time. Succeeds only if the buffer
* contains a keyframe with a timestamp of {@code timeUs} or earlier, and if {@code timeUs} falls
* within the currently buffered media.
* <p>
* This method is equivalent to {@code skipToKeyframeBefore(timeUs, false)}.
* *
* @param timeUs The seek time. * @param timeUs The seek time.
* @return Whether the skip was successful. * @return Whether the skip was successful.
*/ */
public boolean skipToKeyframeBefore(long timeUs) { public boolean skipToKeyframeBefore(long timeUs) {
long nextOffset = infoQueue.skipToKeyframeBefore(timeUs); return skipToKeyframeBefore(timeUs, false);
}
/**
* Attempts to skip to the keyframe before or at the specified time. Succeeds only if the buffer
* contains a keyframe with a timestamp of {@code timeUs} or earlier. If
* {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs}
* falls within the buffer.
*
* @param timeUs The seek time.
* @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end
* of the buffer.
* @return Whether the skip was successful.
*/
public boolean skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) {
long nextOffset = infoQueue.skipToKeyframeBefore(timeUs, allowTimeBeyondBuffer);
if (nextOffset == C.POSITION_UNSET) { if (nextOffset == C.POSITION_UNSET) {
return false; return false;
} }
@ -246,7 +265,8 @@ public final class DefaultTrackOutput implements TrackOutput {
* @param formatHolder A {@link FormatHolder} to populate in the case of reading a format. * @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
* @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the * @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
* end of the stream. If the end of the stream has been reached, the * end of the stream. If the end of the stream has been reached, the
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. * {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. May be null if the
* caller requires that the format of the stream be read even if it's not changing.
* @param loadingFinished True if an empty queue should be considered the end of the stream. * @param loadingFinished True if an empty queue should be considered the end of the stream.
* @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will * @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will
* be set if the buffer's timestamp is less than this value. * be set if the buffer's timestamp is less than this value.
@ -732,7 +752,8 @@ public final class DefaultTrackOutput implements TrackOutput {
* about the sample, but not its data. The size and absolute position of the data in the * about the sample, but not its data. The size and absolute position of the data in the
* rolling buffer is stored in {@code extrasHolder}, along with an encryption id if present * rolling buffer is stored in {@code extrasHolder}, along with an encryption id if present
* and the absolute position of the first byte that may still be required after the current * and the absolute position of the first byte that may still be required after the current
* sample has been read. * sample has been read. May be null if the caller requires that the format of the stream be
* read even if it's not changing.
* @param downstreamFormat The current downstream {@link Format}. If the format of the next * @param downstreamFormat The current downstream {@link Format}. If the format of the next
* sample is different to the current downstream format then a format will be read. * sample is different to the current downstream format then a format will be read.
* @param extrasHolder The holder into which extra sample information should be written. * @param extrasHolder The holder into which extra sample information should be written.
@ -742,14 +763,14 @@ public final class DefaultTrackOutput implements TrackOutput {
public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
Format downstreamFormat, BufferExtrasHolder extrasHolder) { Format downstreamFormat, BufferExtrasHolder extrasHolder) {
if (queueSize == 0) { if (queueSize == 0) {
if (upstreamFormat != null && upstreamFormat != downstreamFormat) { if (upstreamFormat != null && (buffer == null || upstreamFormat != downstreamFormat)) {
formatHolder.format = upstreamFormat; formatHolder.format = upstreamFormat;
return C.RESULT_FORMAT_READ; return C.RESULT_FORMAT_READ;
} }
return C.RESULT_NOTHING_READ; return C.RESULT_NOTHING_READ;
} }
if (formats[relativeReadIndex] != downstreamFormat) { if (buffer == null || formats[relativeReadIndex] != downstreamFormat) {
formatHolder.format = formats[relativeReadIndex]; formatHolder.format = formats[relativeReadIndex];
return C.RESULT_FORMAT_READ; return C.RESULT_FORMAT_READ;
} }
@ -775,20 +796,22 @@ public final class DefaultTrackOutput implements TrackOutput {
} }
/** /**
* Attempts to locate the keyframe before the specified time, if it's present in the buffer. * Attempts to locate the keyframe before or at the specified time. If
* {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs}
* falls within the buffer.
* *
* @param timeUs The seek time. * @param timeUs The seek time.
* @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end
* of the buffer.
* @return The offset of the keyframe's data if the keyframe was present. * @return The offset of the keyframe's data if the keyframe was present.
* {@link C#POSITION_UNSET} otherwise. * {@link C#POSITION_UNSET} otherwise.
*/ */
public synchronized long skipToKeyframeBefore(long timeUs) { public synchronized long skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) {
if (queueSize == 0 || timeUs < timesUs[relativeReadIndex]) { if (queueSize == 0 || timeUs < timesUs[relativeReadIndex]) {
return C.POSITION_UNSET; return C.POSITION_UNSET;
} }
int lastWriteIndex = (relativeWriteIndex == 0 ? capacity : relativeWriteIndex) - 1; if (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer) {
long lastTimeUs = timesUs[lastWriteIndex];
if (timeUs > lastTimeUs) {
return C.POSITION_UNSET; return C.POSITION_UNSET;
} }

View file

@ -529,11 +529,9 @@ public final class MatroskaExtractor implements Extractor {
} }
break; break;
case ID_TRACK_ENTRY: case ID_TRACK_ENTRY:
if (tracks.get(currentTrack.number) == null && isCodecSupported(currentTrack.codecId)) { if (isCodecSupported(currentTrack.codecId)) {
currentTrack.initializeOutput(extractorOutput, currentTrack.number); currentTrack.initializeOutput(extractorOutput, currentTrack.number);
tracks.put(currentTrack.number, currentTrack); tracks.put(currentTrack.number, currentTrack);
} else {
// We've seen this track entry before, or the codec is unsupported. Do nothing.
} }
currentTrack = null; currentTrack = null;
break; break;

View file

@ -127,6 +127,7 @@ import java.util.List;
public static final int TYPE_mean = Util.getIntegerCodeForString("mean"); public static final int TYPE_mean = Util.getIntegerCodeForString("mean");
public static final int TYPE_name = Util.getIntegerCodeForString("name"); public static final int TYPE_name = Util.getIntegerCodeForString("name");
public static final int TYPE_data = Util.getIntegerCodeForString("data"); public static final int TYPE_data = Util.getIntegerCodeForString("data");
public static final int TYPE_emsg = Util.getIntegerCodeForString("emsg");
public static final int TYPE_st3d = Util.getIntegerCodeForString("st3d"); public static final int TYPE_st3d = Util.getIntegerCodeForString("st3d");
public static final int TYPE_sv3d = Util.getIntegerCodeForString("sv3d"); public static final int TYPE_sv3d = Util.getIntegerCodeForString("sv3d");
public static final int TYPE_proj = Util.getIntegerCodeForString("proj"); public static final int TYPE_proj = Util.getIntegerCodeForString("proj");
@ -134,6 +135,7 @@ import java.util.List;
public static final int TYPE_vp09 = Util.getIntegerCodeForString("vp09"); public static final int TYPE_vp09 = Util.getIntegerCodeForString("vp09");
public static final int TYPE_vpcC = Util.getIntegerCodeForString("vpcC"); public static final int TYPE_vpcC = Util.getIntegerCodeForString("vpcC");
public static final int TYPE_camm = Util.getIntegerCodeForString("camm"); public static final int TYPE_camm = Util.getIntegerCodeForString("camm");
public static final int TYPE_alac = Util.getIntegerCodeForString("alac");
public final int type; public final int type;

View file

@ -604,7 +604,7 @@ import java.util.List;
|| childAtomType == Atom.TYPE_dtsh || childAtomType == Atom.TYPE_dtsl || childAtomType == Atom.TYPE_dtsh || childAtomType == Atom.TYPE_dtsl
|| childAtomType == Atom.TYPE_samr || childAtomType == Atom.TYPE_sawb || childAtomType == Atom.TYPE_samr || childAtomType == Atom.TYPE_sawb
|| childAtomType == Atom.TYPE_lpcm || childAtomType == Atom.TYPE_sowt || childAtomType == Atom.TYPE_lpcm || childAtomType == Atom.TYPE_sowt
|| childAtomType == Atom.TYPE__mp3) { || childAtomType == Atom.TYPE__mp3 || childAtomType == Atom.TYPE_alac) {
parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, parseAudioSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId,
language, isQuickTime, drmInitData, out, i); language, isQuickTime, drmInitData, out, i);
} else if (childAtomType == Atom.TYPE_TTML) { } else if (childAtomType == Atom.TYPE_TTML) {
@ -839,6 +839,8 @@ import java.util.List;
mimeType = MimeTypes.AUDIO_RAW; mimeType = MimeTypes.AUDIO_RAW;
} else if (atomType == Atom.TYPE__mp3) { } else if (atomType == Atom.TYPE__mp3) {
mimeType = MimeTypes.AUDIO_MPEG; mimeType = MimeTypes.AUDIO_MPEG;
} else if (atomType == Atom.TYPE_alac) {
mimeType = MimeTypes.AUDIO_ALAC;
} }
byte[] initializationData = null; byte[] initializationData = null;
@ -876,6 +878,10 @@ import java.util.List;
out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null, out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0, Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0,
language); language);
} else if (childAtomType == Atom.TYPE_alac) {
initializationData = new byte[childAtomSize];
parent.setPosition(childPosition);
parent.readBytes(initializationData, 0, childAtomSize);
} }
childPosition += childAtomSize; childPosition += childAtomSize;
} }

View file

@ -20,6 +20,7 @@ import android.util.Log;
import android.util.Pair; import android.util.Pair;
import android.util.SparseArray; import android.util.SparseArray;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData;
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
@ -30,20 +31,22 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom; import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom;
import com.google.android.exoplayer2.extractor.mp4.Atom.LeafAtom; import com.google.android.exoplayer2.extractor.mp4.Atom.LeafAtom;
import com.google.android.exoplayer2.text.cea.CeaUtil;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.NalUnitUtil;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Stack; import java.util.Stack;
import java.util.UUID; import java.util.UUID;
@ -65,15 +68,13 @@ public final class FragmentedMp4Extractor implements Extractor {
}; };
private static final String TAG = "FragmentedMp4Extractor";
private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig");
/** /**
* Flags controlling the behavior of the extractor. * Flags controlling the behavior of the extractor.
*/ */
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME, @IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME,
FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_SIDELOADED}) FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_ENABLE_CEA608_TRACK,
FLAG_SIDELOADED})
public @interface Flags {} public @interface Flags {}
/** /**
* Flag to work around an issue in some video streams where every frame is marked as a sync frame. * Flag to work around an issue in some video streams where every frame is marked as a sync frame.
@ -87,12 +88,25 @@ public final class FragmentedMp4Extractor implements Extractor {
* Flag to ignore any tfdt boxes in the stream. * Flag to ignore any tfdt boxes in the stream.
*/ */
public static final int FLAG_WORKAROUND_IGNORE_TFDT_BOX = 2; public static final int FLAG_WORKAROUND_IGNORE_TFDT_BOX = 2;
/**
* Flag to indicate that the extractor should output an event message metadata track. Any event
* messages in the stream will be delivered as samples to this track.
*/
public static final int FLAG_ENABLE_EMSG_TRACK = 4;
/**
* Flag to indicate that the extractor should output a CEA-608 text track. Any CEA-608 messages
* contained within SEI NAL units in the stream will be delivered as samples to this track.
*/
public static final int FLAG_ENABLE_CEA608_TRACK = 8;
/** /**
* Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4 * Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4
* container. * container.
*/ */
private static final int FLAG_SIDELOADED = 4; private static final int FLAG_SIDELOADED = 16;
private static final String TAG = "FragmentedMp4Extractor";
private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig");
private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information
private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE = private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE =
new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12}; new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12};
@ -114,6 +128,7 @@ public final class FragmentedMp4Extractor implements Extractor {
// Temporary arrays. // Temporary arrays.
private final ParsableByteArray nalStartCode; private final ParsableByteArray nalStartCode;
private final ParsableByteArray nalLength; private final ParsableByteArray nalLength;
private final ParsableByteArray nalPayload;
private final ParsableByteArray encryptionSignalByte; private final ParsableByteArray encryptionSignalByte;
// Adjusts sample timestamps. // Adjusts sample timestamps.
@ -123,6 +138,7 @@ public final class FragmentedMp4Extractor implements Extractor {
private final ParsableByteArray atomHeader; private final ParsableByteArray atomHeader;
private final byte[] extendedTypeScratch; private final byte[] extendedTypeScratch;
private final Stack<ContainerAtom> containerAtoms; private final Stack<ContainerAtom> containerAtoms;
private final LinkedList<MetadataSampleInfo> pendingMetadataSampleInfos;
private int parserState; private int parserState;
private int atomType; private int atomType;
@ -130,8 +146,10 @@ public final class FragmentedMp4Extractor implements Extractor {
private int atomHeaderBytesRead; private int atomHeaderBytesRead;
private ParsableByteArray atomData; private ParsableByteArray atomData;
private long endOfMdatPosition; private long endOfMdatPosition;
private int pendingMetadataSampleBytes;
private long durationUs; private long durationUs;
private long segmentIndexEarliestPresentationTimeUs;
private TrackBundle currentTrackBundle; private TrackBundle currentTrackBundle;
private int sampleSize; private int sampleSize;
private int sampleBytesWritten; private int sampleBytesWritten;
@ -139,6 +157,8 @@ public final class FragmentedMp4Extractor implements Extractor {
// Extractor output. // Extractor output.
private ExtractorOutput extractorOutput; private ExtractorOutput extractorOutput;
private TrackOutput eventMessageTrackOutput;
private TrackOutput cea608TrackOutput;
// Whether extractorOutput.seekMap has been called. // Whether extractorOutput.seekMap has been called.
private boolean haveOutputSeekMap; private boolean haveOutputSeekMap;
@ -169,11 +189,14 @@ public final class FragmentedMp4Extractor implements Extractor {
atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE); atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE); nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
nalLength = new ParsableByteArray(4); nalLength = new ParsableByteArray(4);
nalPayload = new ParsableByteArray(1);
encryptionSignalByte = new ParsableByteArray(1); encryptionSignalByte = new ParsableByteArray(1);
extendedTypeScratch = new byte[16]; extendedTypeScratch = new byte[16];
containerAtoms = new Stack<>(); containerAtoms = new Stack<>();
pendingMetadataSampleInfos = new LinkedList<>();
trackBundles = new SparseArray<>(); trackBundles = new SparseArray<>();
durationUs = C.TIME_UNSET; durationUs = C.TIME_UNSET;
segmentIndexEarliestPresentationTimeUs = C.TIME_UNSET;
enterReadingAtomHeaderState(); enterReadingAtomHeaderState();
} }
@ -189,6 +212,7 @@ public final class FragmentedMp4Extractor implements Extractor {
TrackBundle bundle = new TrackBundle(output.track(0)); TrackBundle bundle = new TrackBundle(output.track(0));
bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0)); bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0));
trackBundles.put(0, bundle); trackBundles.put(0, bundle);
maybeInitExtraTracks();
extractorOutput.endTracks(); extractorOutput.endTracks();
} }
} }
@ -199,6 +223,8 @@ public final class FragmentedMp4Extractor implements Extractor {
for (int i = 0; i < trackCount; i++) { for (int i = 0; i < trackCount; i++) {
trackBundles.valueAt(i).reset(); trackBundles.valueAt(i).reset();
} }
pendingMetadataSampleInfos.clear();
pendingMetadataSampleBytes = 0;
containerAtoms.clear(); containerAtoms.clear();
enterReadingAtomHeaderState(); enterReadingAtomHeaderState();
} }
@ -257,6 +283,10 @@ public final class FragmentedMp4Extractor implements Extractor {
atomSize = atomHeader.readUnsignedLongToLong(); atomSize = atomHeader.readUnsignedLongToLong();
} }
if (atomSize < atomHeaderBytesRead) {
throw new ParserException("Atom size less than header length (unsupported).");
}
long atomPosition = input.getPosition() - atomHeaderBytesRead; long atomPosition = input.getPosition() - atomHeaderBytesRead;
if (atomType == Atom.TYPE_moof) { if (atomType == Atom.TYPE_moof) {
// The data positions may be updated when parsing the tfhd/trun. // The data positions may be updated when parsing the tfhd/trun.
@ -332,9 +362,12 @@ public final class FragmentedMp4Extractor implements Extractor {
if (!containerAtoms.isEmpty()) { if (!containerAtoms.isEmpty()) {
containerAtoms.peek().add(leaf); containerAtoms.peek().add(leaf);
} else if (leaf.type == Atom.TYPE_sidx) { } else if (leaf.type == Atom.TYPE_sidx) {
ChunkIndex segmentIndex = parseSidx(leaf.data, inputPosition); Pair<Long, ChunkIndex> result = parseSidx(leaf.data, inputPosition);
extractorOutput.seekMap(segmentIndex); segmentIndexEarliestPresentationTimeUs = result.first;
extractorOutput.seekMap(result.second);
haveOutputSeekMap = true; haveOutputSeekMap = true;
} else if (leaf.type == Atom.TYPE_emsg) {
onEmsgLeafAtomRead(leaf.data);
} }
} }
@ -387,18 +420,19 @@ public final class FragmentedMp4Extractor implements Extractor {
// We need to create the track bundles. // We need to create the track bundles.
for (int i = 0; i < trackCount; i++) { for (int i = 0; i < trackCount; i++) {
Track track = tracks.valueAt(i); Track track = tracks.valueAt(i);
trackBundles.put(track.id, new TrackBundle(extractorOutput.track(i))); TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i));
trackBundle.init(track, defaultSampleValuesArray.get(track.id));
trackBundles.put(track.id, trackBundle);
durationUs = Math.max(durationUs, track.durationUs); durationUs = Math.max(durationUs, track.durationUs);
} }
maybeInitExtraTracks();
extractorOutput.endTracks(); extractorOutput.endTracks();
} else { } else {
Assertions.checkState(trackBundles.size() == trackCount); Assertions.checkState(trackBundles.size() == trackCount);
} for (int i = 0; i < trackCount; i++) {
Track track = tracks.valueAt(i);
// Initialization of tracks and default sample values. trackBundles.get(track.id).init(track, defaultSampleValuesArray.get(track.id));
for (int i = 0; i < trackCount; i++) { }
Track track = tracks.valueAt(i);
trackBundles.get(track.id).init(track, defaultSampleValuesArray.get(track.id));
} }
} }
@ -413,6 +447,51 @@ public final class FragmentedMp4Extractor implements Extractor {
} }
} }
private void maybeInitExtraTracks() {
if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0 && eventMessageTrackOutput == null) {
eventMessageTrackOutput = extractorOutput.track(trackBundles.size());
eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG,
Format.OFFSET_SAMPLE_RELATIVE));
}
if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutput == null) {
cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1);
cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608,
null, Format.NO_VALUE, 0, null, null));
}
}
/**
* Handles an emsg atom (defined in 23009-1).
*/
private void onEmsgLeafAtomRead(ParsableByteArray atom) {
if (eventMessageTrackOutput == null) {
return;
}
// Parse the event's presentation time delta.
atom.setPosition(Atom.FULL_HEADER_SIZE);
atom.readNullTerminatedString(); // schemeIdUri
atom.readNullTerminatedString(); // value
long timescale = atom.readUnsignedInt();
long presentationTimeDeltaUs =
Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale);
// Output the sample data.
atom.setPosition(Atom.FULL_HEADER_SIZE);
int sampleSize = atom.bytesLeft();
eventMessageTrackOutput.sampleData(atom, sampleSize);
// Output the sample metadata.
if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) {
// We can output the sample metadata immediately.
eventMessageTrackOutput.sampleMetadata(
segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs,
C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0 /* offset */, null);
} else {
// We need the first sample timestamp in the segment before we can output the metadata.
pendingMetadataSampleInfos.addLast(
new MetadataSampleInfo(presentationTimeDeltaUs, sampleSize));
pendingMetadataSampleBytes += sampleSize;
}
}
/** /**
* Parses a trex atom (defined in 14496-12). * Parses a trex atom (defined in 14496-12).
*/ */
@ -624,7 +703,7 @@ public final class FragmentedMp4Extractor implements Extractor {
DefaultSampleValues defaultSampleValues = trackBundle.defaultSampleValues; DefaultSampleValues defaultSampleValues = trackBundle.defaultSampleValues;
int defaultSampleDescriptionIndex = int defaultSampleDescriptionIndex =
((atomFlags & 0x02 /* default_sample_description_index_present */) != 0) ((atomFlags & 0x02 /* default_sample_description_index_present */) != 0)
? tfhd.readUnsignedIntToInt() - 1 : defaultSampleValues.sampleDescriptionIndex; ? tfhd.readUnsignedIntToInt() - 1 : defaultSampleValues.sampleDescriptionIndex;
int defaultSampleDuration = ((atomFlags & 0x08 /* default_sample_duration_present */) != 0) int defaultSampleDuration = ((atomFlags & 0x08 /* default_sample_duration_present */) != 0)
? tfhd.readUnsignedIntToInt() : defaultSampleValues.duration; ? tfhd.readUnsignedIntToInt() : defaultSampleValues.duration;
int defaultSampleSize = ((atomFlags & 0x10 /* default_sample_size_present */) != 0) int defaultSampleSize = ((atomFlags & 0x10 /* default_sample_size_present */) != 0)
@ -828,8 +907,13 @@ public final class FragmentedMp4Extractor implements Extractor {
/** /**
* Parses a sidx atom (defined in 14496-12). * Parses a sidx atom (defined in 14496-12).
*
* @param atom The atom data.
* @param inputPosition The input position of the first byte after the atom.
* @return A pair consisting of the earliest presentation time in microseconds, and the parsed
* {@link ChunkIndex}.
*/ */
private static ChunkIndex parseSidx(ParsableByteArray atom, long inputPosition) private static Pair<Long, ChunkIndex> parseSidx(ParsableByteArray atom, long inputPosition)
throws ParserException { throws ParserException {
atom.setPosition(Atom.HEADER_SIZE); atom.setPosition(Atom.HEADER_SIZE);
int fullAtom = atom.readInt(); int fullAtom = atom.readInt();
@ -846,6 +930,8 @@ public final class FragmentedMp4Extractor implements Extractor {
earliestPresentationTime = atom.readUnsignedLongToLong(); earliestPresentationTime = atom.readUnsignedLongToLong();
offset += atom.readUnsignedLongToLong(); offset += atom.readUnsignedLongToLong();
} }
long earliestPresentationTimeUs = Util.scaleLargeTimestamp(earliestPresentationTime,
C.MICROS_PER_SECOND, timescale);
atom.skipBytes(2); atom.skipBytes(2);
@ -856,7 +942,7 @@ public final class FragmentedMp4Extractor implements Extractor {
long[] timesUs = new long[referenceCount]; long[] timesUs = new long[referenceCount];
long time = earliestPresentationTime; long time = earliestPresentationTime;
long timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale); long timeUs = earliestPresentationTimeUs;
for (int i = 0; i < referenceCount; i++) { for (int i = 0; i < referenceCount; i++) {
int firstInt = atom.readInt(); int firstInt = atom.readInt();
@ -880,7 +966,8 @@ public final class FragmentedMp4Extractor implements Extractor {
offset += sizes[i]; offset += sizes[i];
} }
return new ChunkIndex(sizes, offsets, durationsUs, timesUs); return Pair.create(earliestPresentationTimeUs,
new ChunkIndex(sizes, offsets, durationsUs, timesUs));
} }
private void readEncryptionData(ExtractorInput input) throws IOException, InterruptedException { private void readEncryptionData(ExtractorInput input) throws IOException, InterruptedException {
@ -942,13 +1029,9 @@ public final class FragmentedMp4Extractor implements Extractor {
// We skip bytes preceding the next sample to read. // We skip bytes preceding the next sample to read.
int bytesToSkip = (int) (nextDataPosition - input.getPosition()); int bytesToSkip = (int) (nextDataPosition - input.getPosition());
if (bytesToSkip < 0) { if (bytesToSkip < 0) {
if (nextDataPosition == currentTrackBundle.fragment.atomPosition) { // Assume the sample data must be contiguous in the mdat with no preceding data.
// Assume the sample data must be contiguous in the mdat with no preceeding data. Log.w(TAG, "Ignoring negative offset to sample data.");
Log.w(TAG, "Offset to sample data was missing."); bytesToSkip = 0;
bytesToSkip = 0;
} else {
throw new ParserException("Offset to sample data was negative.");
}
} }
input.skipFully(bytesToSkip); input.skipFully(bytesToSkip);
this.currentTrackBundle = currentTrackBundle; this.currentTrackBundle = currentTrackBundle;
@ -996,6 +1079,26 @@ public final class FragmentedMp4Extractor implements Extractor {
output.sampleData(nalStartCode, 4); output.sampleData(nalStartCode, 4);
sampleBytesWritten += 4; sampleBytesWritten += 4;
sampleSize += nalUnitLengthFieldLengthDiff; sampleSize += nalUnitLengthFieldLengthDiff;
if (cea608TrackOutput != null) {
byte[] nalPayloadData = nalPayload.data;
// Peek the NAL unit type byte.
input.peekFully(nalPayloadData, 0, 1);
if ((nalPayloadData[0] & 0x1F) == NAL_UNIT_TYPE_SEI) {
// Read the whole SEI NAL unit into nalWrapper, including the NAL unit type byte.
nalPayload.reset(sampleCurrentNalBytesRemaining);
input.readFully(nalPayloadData, 0, sampleCurrentNalBytesRemaining);
// Write the SEI unit straight to the output.
output.sampleData(nalPayload, sampleCurrentNalBytesRemaining);
sampleBytesWritten += sampleCurrentNalBytesRemaining;
sampleCurrentNalBytesRemaining = 0;
// Unescape and process the SEI unit.
int unescapedLength = NalUnitUtil.unescapeStream(nalPayloadData, nalPayload.limit());
nalPayload.setPosition(1); // Skip the NAL unit type byte.
nalPayload.setLimit(unescapedLength);
CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalPayload,
cea608TrackOutput);
}
}
} else { } else {
// Write the payload of the NAL unit. // Write the payload of the NAL unit.
int writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false); int writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false);
@ -1025,6 +1128,14 @@ public final class FragmentedMp4Extractor implements Extractor {
} }
output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, encryptionKey); output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, encryptionKey);
while (!pendingMetadataSampleInfos.isEmpty()) {
MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst();
pendingMetadataSampleBytes -= sampleInfo.size;
eventMessageTrackOutput.sampleMetadata(
sampleTimeUs + sampleInfo.presentationTimeDeltaUs,
C.BUFFER_FLAG_KEY_FRAME, sampleInfo.size, pendingMetadataSampleBytes, null);
}
currentTrackBundle.currentSampleIndex++; currentTrackBundle.currentSampleIndex++;
currentTrackBundle.currentSampleInTrackRun++; currentTrackBundle.currentSampleInTrackRun++;
if (currentTrackBundle.currentSampleInTrackRun if (currentTrackBundle.currentSampleInTrackRun
@ -1130,7 +1241,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|| atom == Atom.TYPE_trun || atom == Atom.TYPE_pssh || atom == Atom.TYPE_saiz || atom == Atom.TYPE_trun || atom == Atom.TYPE_pssh || atom == Atom.TYPE_saiz
|| atom == Atom.TYPE_saio || atom == Atom.TYPE_senc || atom == Atom.TYPE_uuid || atom == Atom.TYPE_saio || atom == Atom.TYPE_senc || atom == Atom.TYPE_uuid
|| atom == Atom.TYPE_sbgp || atom == Atom.TYPE_sgpd || atom == Atom.TYPE_elst || atom == Atom.TYPE_sbgp || atom == Atom.TYPE_sgpd || atom == Atom.TYPE_elst
|| atom == Atom.TYPE_mehd; || atom == Atom.TYPE_mehd || atom == Atom.TYPE_emsg;
} }
/** Returns whether the extractor should decode a container atom with type {@code atom}. */ /** Returns whether the extractor should decode a container atom with type {@code atom}. */
@ -1140,6 +1251,21 @@ public final class FragmentedMp4Extractor implements Extractor {
|| atom == Atom.TYPE_traf || atom == Atom.TYPE_mvex || atom == Atom.TYPE_edts; || atom == Atom.TYPE_traf || atom == Atom.TYPE_mvex || atom == Atom.TYPE_edts;
} }
/**
* Holds data corresponding to a metadata sample.
*/
private static final class MetadataSampleInfo {
public final long presentationTimeDeltaUs;
public final int size;
public MetadataSampleInfo(long presentationTimeDeltaUs, int size) {
this.presentationTimeDeltaUs = presentationTimeDeltaUs;
this.size = size;
}
}
/** /**
* Holds data corresponding to a single track. * Holds data corresponding to a single track.
*/ */

View file

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

View file

@ -83,8 +83,11 @@ public final class RawCcExtractor implements Extractor {
while (true) { while (true) {
switch (parserState) { switch (parserState) {
case STATE_READING_HEADER: case STATE_READING_HEADER:
parseHeader(input); if (parseHeader(input)) {
parserState = STATE_READING_TIMESTAMP_AND_COUNT; parserState = STATE_READING_TIMESTAMP_AND_COUNT;
} else {
return RESULT_END_OF_INPUT;
}
break; break;
case STATE_READING_TIMESTAMP_AND_COUNT: case STATE_READING_TIMESTAMP_AND_COUNT:
if (parseTimestampAndSampleCount(input)) { if (parseTimestampAndSampleCount(input)) {
@ -114,14 +117,18 @@ public final class RawCcExtractor implements Extractor {
// Do nothing // Do nothing
} }
private void parseHeader(ExtractorInput input) throws IOException, InterruptedException { private boolean parseHeader(ExtractorInput input) throws IOException, InterruptedException {
dataScratch.reset(); dataScratch.reset();
input.readFully(dataScratch.data, 0, HEADER_SIZE); if (input.readFully(dataScratch.data, 0, HEADER_SIZE, true)) {
if (dataScratch.readInt() != HEADER_ID) { if (dataScratch.readInt() != HEADER_ID) {
throw new IOException("Input not RawCC"); throw new IOException("Input not RawCC");
}
version = dataScratch.readUnsignedByte();
// no versions use the flag fields yet
return true;
} else {
return false;
} }
version = dataScratch.readUnsignedByte();
// no versions use the flag fields yet
} }
private boolean parseTimestampAndSampleCount(ExtractorInput input) throws IOException, private boolean parseTimestampAndSampleCount(ExtractorInput input) throws IOException,

View file

@ -16,12 +16,11 @@
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import android.util.Log; import android.util.Log;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster;
/** /**
* Parses PES packet data and extracts samples. * Parses PES packet data and extracts samples.

View file

@ -23,10 +23,10 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.extractor.PositionHolder; import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.extractor.SeekMap; import com.google.android.exoplayer2.extractor.SeekMap;
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
import com.google.android.exoplayer2.util.ParsableBitArray; import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster;
import java.io.IOException; import java.io.IOException;
/** /**

View file

@ -16,10 +16,10 @@
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster;
/** /**
* Reads section data. * Reads section data.

View file

@ -17,8 +17,8 @@ package com.google.android.exoplayer2.extractor.ts;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.ExtractorOutput; import com.google.android.exoplayer2.extractor.ExtractorOutput;
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.TimestampAdjuster;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
/** /**

View file

@ -15,10 +15,9 @@
*/ */
package com.google.android.exoplayer2.extractor.ts; package com.google.android.exoplayer2.extractor.ts;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.TrackOutput; import com.google.android.exoplayer2.extractor.TrackOutput;
import com.google.android.exoplayer2.text.cea.Cea608Decoder; import com.google.android.exoplayer2.text.cea.CeaUtil;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
@ -36,40 +35,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
} }
public void consume(long pesTimeUs, ParsableByteArray seiBuffer) { public void consume(long pesTimeUs, ParsableByteArray seiBuffer) {
int b; CeaUtil.consume(pesTimeUs, seiBuffer, output);
while (seiBuffer.bytesLeft() > 1 /* last byte will be rbsp_trailing_bits */) {
// Parse payload type.
int payloadType = 0;
do {
b = seiBuffer.readUnsignedByte();
payloadType += b;
} while (b == 0xFF);
// Parse payload size.
int payloadSize = 0;
do {
b = seiBuffer.readUnsignedByte();
payloadSize += b;
} while (b == 0xFF);
// Process the payload.
if (Cea608Decoder.isSeiMessageCea608(payloadType, payloadSize, seiBuffer)) {
// Ignore country_code (1) + provider_code (2) + user_identifier (4)
// + user_data_type_code (1).
seiBuffer.skipBytes(8);
// Ignore first three bits: reserved (1) + process_cc_data_flag (1) + zero_bit (1).
int ccCount = seiBuffer.readUnsignedByte() & 0x1F;
// Ignore em_data (1)
seiBuffer.skipBytes(1);
// Each data packet consists of 24 bits: marker bits (5) + cc_valid (1) + cc_type (2)
// + cc_data_1 (8) + cc_data_2 (8).
int sampleLength = ccCount * 3;
output.sampleData(seiBuffer, sampleLength);
output.sampleMetadata(pesTimeUs, C.BUFFER_FLAG_KEY_FRAME, sampleLength, 0, null);
// Ignore trailing information in SEI, if any.
seiBuffer.skipBytes(payloadSize - (10 + ccCount * 3));
} else {
seiBuffer.skipBytes(payloadSize);
}
}
} }
} }

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