Commit 25a966dc by ojw28 Committed by GitHub

Merge pull request #2402 from google/dev-v2

r2.2.0
parents 0cacd388 6a844ebc
Showing with 2507 additions and 652 deletions
# Release notes # # Release notes #
### r2.1.1 ### ### r2.2.0 ###
* Demo app: Automatic recovery from BehindLiveWindowException, plus improved
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).
Bugfix release only. Users of r2.1.0 and r2.0.x should proactively update to ### r2.1.1 ###
this version.
* 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. ...@@ -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. ...@@ -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, ...@@ -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.
......
...@@ -35,7 +35,7 @@ allprojects { ...@@ -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'
} }
} }
...@@ -24,24 +24,19 @@ android { ...@@ -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 {
......
...@@ -16,8 +16,8 @@ ...@@ -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 @@ ...@@ -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"
......
...@@ -183,52 +183,52 @@ ...@@ -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"
} }
] ]
}, },
......
...@@ -26,16 +26,17 @@ import com.google.android.exoplayer2.RendererCapabilities; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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,7 +280,7 @@ import java.util.Locale; ...@@ -279,7 +280,7 @@ import java.util.Locale;
// Do nothing. // Do nothing.
} }
// StreamingDrmSessionManager.EventListener // DefaultDrmSessionManager.EventListener
@Override @Override
public void onDrmSessionManagerError(Exception e) { public void onDrmSessionManagerError(Exception e) {
...@@ -287,6 +288,16 @@ import java.util.Locale; ...@@ -287,6 +288,16 @@ import java.util.Locale;
} }
@Override @Override
public void onDrmKeysRestored() {
Log.d(TAG, "drmKeysRestored [" + getSessionTimeString() + "]");
}
@Override
public void onDrmKeysRemoved() {
Log.d(TAG, "drmKeysRemoved [" + getSessionTimeString() + "]");
}
@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; ...@@ -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; ...@@ -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; ...@@ -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
......
...@@ -36,15 +36,16 @@ import com.google.android.exoplayer2.ExoPlayer; ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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;
}
} }

8.46 KB | W: | H:

6.72 KB | W: | H:

demo/src/main/res/drawable-xhdpi/ic_banner.png
demo/src/main/res/drawable-xhdpi/ic_banner.png
demo/src/main/res/drawable-xhdpi/ic_banner.png
demo/src/main/res/drawable-xhdpi/ic_banner.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -16,8 +16,7 @@ ...@@ -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>
......
...@@ -23,17 +23,6 @@ android { ...@@ -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']
} }
......
...@@ -57,8 +57,8 @@ import java.util.Map; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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));
......
...@@ -40,9 +40,10 @@ import java.util.concurrent.Executor; ...@@ -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 ...@@ -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();
} }
......
...@@ -16,6 +16,7 @@ ...@@ -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; ...@@ -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 { ...@@ -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);
} }
......
...@@ -63,6 +63,7 @@ git clone git://source.ffmpeg.org/ffmpeg ffmpeg && cd ffmpeg && \ ...@@ -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
......
...@@ -20,17 +20,7 @@ android { ...@@ -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 {
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.ffmpeg; ...@@ -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 { ...@@ -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;
} }
...@@ -70,6 +71,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -70,6 +71,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
} }
@Override @Override
public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {
return ADAPTIVE_NOT_SEAMLESS;
}
@Override
protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto) protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
throws FfmpegDecoderException { throws FfmpegDecoderException {
decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE, decoder = new FfmpegDecoder(NUM_BUFFERS, NUM_BUFFERS, INITIAL_INPUT_BUFFER_SIZE,
......
...@@ -19,6 +19,7 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer; ...@@ -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; ...@@ -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; ...@@ -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:
......
...@@ -92,6 +92,8 @@ public final class FfmpegLibrary { ...@@ -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;
} }
......
...@@ -20,17 +20,7 @@ android { ...@@ -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 {
......
...@@ -56,7 +56,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -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;
} }
......
...@@ -22,17 +22,6 @@ android { ...@@ -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 {
......
...@@ -261,7 +261,7 @@ public class OkHttpDataSource implements HttpDataSource { ...@@ -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);
......
...@@ -16,6 +16,7 @@ ...@@ -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; ...@@ -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 { ...@@ -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);
} }
......
...@@ -20,17 +20,7 @@ android { ...@@ -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 {
......
...@@ -72,7 +72,7 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer { ...@@ -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;
} }
......
...@@ -20,17 +20,7 @@ android { ...@@ -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 {
......
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 ...@@ -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 { ...@@ -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 { ...@@ -42,10 +39,6 @@ android {
// } // }
} }
lintOptions {
abortOnError false
}
sourceSets { sourceSets {
androidTest { androidTest {
java.srcDirs += "../testutils/src/main/java/" java.srcDirs += "../testutils/src/main/java/"
......
# 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);
}
...@@ -59,8 +59,8 @@ public final class FormatTest extends TestCase { ...@@ -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,
......
/*
* 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}));
}
}
...@@ -16,6 +16,7 @@ ...@@ -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; ...@@ -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);
} }
} }
...@@ -16,9 +16,9 @@ ...@@ -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;
......
...@@ -21,7 +21,6 @@ import com.google.android.exoplayer2.Format; ...@@ -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; ...@@ -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;
......
/*
* 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);
}
}
/*
* 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);
}
}
/*
* 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();
}
}
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.metadata.id3;
import android.os.Parcel;
import junit.framework.TestCase;
/**
* Test for {@link ChapterTocFrame}.
*/
public final class ChapterTocFrameTest extends TestCase {
public void testParcelable() {
String[] children = new String[] {"child0", "child1"};
Id3Frame[] subFrames = new Id3Frame[] {
new TextInformationFrame("TIT2", null, "title"),
new UrlLinkFrame("WXXX", "description", "url")
};
ChapterTocFrame chapterTocFrameToParcel = new ChapterTocFrame("id", false, true, children,
subFrames);
Parcel parcel = Parcel.obtain();
chapterTocFrameToParcel.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
ChapterTocFrame chapterTocFrameFromParcel = ChapterTocFrame.CREATOR.createFromParcel(parcel);
assertEquals(chapterTocFrameToParcel, chapterTocFrameFromParcel);
parcel.recycle();
}
}
...@@ -21,9 +21,9 @@ import com.google.android.exoplayer2.metadata.MetadataDecoderException; ...@@ -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 { ...@@ -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 { ...@@ -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);
} }
} }
/*
* 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;
}
}
/*
* 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;
}
}
...@@ -20,6 +20,8 @@ import android.test.InstrumentationTestCase; ...@@ -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 { ...@@ -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));
} }
} }
...@@ -29,13 +29,13 @@ public class RepresentationTest extends TestCase { ...@@ -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());
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.hls.playlist; ...@@ -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; ...@@ -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);
}
} }
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls.playlist; ...@@ -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 { ...@@ -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 { ...@@ -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());
} }
......
...@@ -27,7 +27,9 @@ import java.io.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 { ...@@ -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 { ...@@ -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 { ...@@ -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);
} }
} }
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.upstream.cache;
import android.content.Context;
import android.net.Uri;
import android.test.AndroidTestCase;
import android.test.MoreAsserts;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.testutil.FakeDataSource;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.upstream.DataSink;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.upstream.crypto.AesCipherDataSink;
import com.google.android.exoplayer2.upstream.crypto.AesCipherDataSource;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Random;
/**
* Additional tests for {@link CacheDataSource}.
*/
public class CacheDataSourceTest2 extends AndroidTestCase {
private static final String EXO_CACHE_DIR = "exo";
private static final int EXO_CACHE_MAX_FILESIZE = 128;
private static final Uri URI = Uri.parse("http://test.com/content");
private static final String KEY = "key";
private static final byte[] DATA = TestUtil.buildTestData(8 * EXO_CACHE_MAX_FILESIZE + 1);
// A DataSpec that covers the full file.
private static final DataSpec FULL = new DataSpec(URI, 0, DATA.length, KEY);
private static final int OFFSET_ON_BOUNDARY = EXO_CACHE_MAX_FILESIZE;
// A DataSpec that starts at 0 and extends to a cache file boundary.
private static final DataSpec END_ON_BOUNDARY = new DataSpec(URI, 0, OFFSET_ON_BOUNDARY, KEY);
// A DataSpec that starts on the same boundary and extends to the end of the file.
private static final DataSpec START_ON_BOUNDARY = new DataSpec(URI, OFFSET_ON_BOUNDARY,
DATA.length - OFFSET_ON_BOUNDARY, KEY);
private static final int OFFSET_OFF_BOUNDARY = EXO_CACHE_MAX_FILESIZE * 2 + 1;
// A DataSpec that starts at 0 and extends to just past a cache file boundary.
private static final DataSpec END_OFF_BOUNDARY = new DataSpec(URI, 0, OFFSET_OFF_BOUNDARY, KEY);
// A DataSpec that starts on the same boundary and extends to the end of the file.
private static final DataSpec START_OFF_BOUNDARY = new DataSpec(URI, OFFSET_OFF_BOUNDARY,
DATA.length - OFFSET_OFF_BOUNDARY, KEY);
public void testWithoutEncryption() throws IOException {
testReads(false);
}
public void testWithEncryption() throws IOException {
testReads(true);
}
private void testReads(boolean useEncryption) throws IOException {
FakeDataSource upstreamSource = buildFakeUpstreamSource();
CacheDataSource source = buildCacheDataSource(getContext(), upstreamSource, useEncryption);
// First read, should arrive from upstream.
testRead(END_ON_BOUNDARY, source);
assertSingleOpen(upstreamSource, 0, OFFSET_ON_BOUNDARY);
// Second read, should arrive from upstream.
testRead(START_OFF_BOUNDARY, source);
assertSingleOpen(upstreamSource, OFFSET_OFF_BOUNDARY, DATA.length);
// Second read, should arrive part from cache and part from upstream.
testRead(END_OFF_BOUNDARY, source);
assertSingleOpen(upstreamSource, OFFSET_ON_BOUNDARY, OFFSET_OFF_BOUNDARY);
// Third read, should arrive from cache.
testRead(FULL, source);
assertNoOpen(upstreamSource);
// Various reads, should all arrive from cache.
testRead(FULL, source);
assertNoOpen(upstreamSource);
testRead(START_ON_BOUNDARY, source);
assertNoOpen(upstreamSource);
testRead(END_ON_BOUNDARY, source);
assertNoOpen(upstreamSource);
testRead(START_OFF_BOUNDARY, source);
assertNoOpen(upstreamSource);
testRead(END_OFF_BOUNDARY, source);
assertNoOpen(upstreamSource);
}
private void testRead(DataSpec dataSpec, CacheDataSource source) throws IOException {
byte[] scratch = new byte[4096];
Random random = new Random(0);
source.open(dataSpec);
int position = (int) dataSpec.absoluteStreamPosition;
int bytesRead = 0;
while (bytesRead != C.RESULT_END_OF_INPUT) {
int maxBytesToRead = random.nextInt(scratch.length) + 1;
bytesRead = source.read(scratch, 0, maxBytesToRead);
if (bytesRead != C.RESULT_END_OF_INPUT) {
MoreAsserts.assertEquals(Arrays.copyOfRange(DATA, position, position + bytesRead),
Arrays.copyOf(scratch, bytesRead));
position += bytesRead;
}
}
source.close();
}
/**
* Asserts that a single {@link DataSource#open(DataSpec)} call has been made to the upstream
* source, with the specified start (inclusive) and end (exclusive) positions.
*/
private void assertSingleOpen(FakeDataSource upstreamSource, int start, int end) {
DataSpec[] openedDataSpecs = upstreamSource.getAndClearOpenedDataSpecs();
assertEquals(1, openedDataSpecs.length);
assertEquals(start, openedDataSpecs[0].position);
assertEquals(start, openedDataSpecs[0].absoluteStreamPosition);
assertEquals(end - start, openedDataSpecs[0].length);
}
/**
* Asserts that the upstream source was not opened.
*/
private void assertNoOpen(FakeDataSource upstreamSource) {
DataSpec[] openedDataSpecs = upstreamSource.getAndClearOpenedDataSpecs();
assertEquals(0, openedDataSpecs.length);
}
private static FakeDataSource buildFakeUpstreamSource() {
return new FakeDataSource.Builder().appendReadData(DATA).build();
}
private static CacheDataSource buildCacheDataSource(Context context, DataSource upstreamSource,
boolean useAesEncryption) throws CacheException {
File cacheDir = context.getExternalCacheDir();
Cache cache = new SimpleCache(new File(cacheDir, EXO_CACHE_DIR), new NoOpCacheEvictor());
emptyCache(cache);
// Source and cipher
final String secretKey = "testKey:12345678";
DataSource file = new FileDataSource();
DataSource cacheReadDataSource = useAesEncryption
? new AesCipherDataSource(Util.getUtf8Bytes(secretKey), file) : file;
// Sink and cipher
CacheDataSink cacheSink = new CacheDataSink(cache, EXO_CACHE_MAX_FILESIZE);
byte[] scratch = new byte[3897];
DataSink cacheWriteDataSink = useAesEncryption
? new AesCipherDataSink(Util.getUtf8Bytes(secretKey), cacheSink, scratch) : cacheSink;
return new CacheDataSource(cache,
upstreamSource,
cacheReadDataSource,
cacheWriteDataSink,
CacheDataSource.FLAG_BLOCK_ON_CACHE,
null); // eventListener
}
private static void emptyCache(Cache cache) throws CacheException {
for (String key : cache.getKeys()) {
for (CacheSpan span : cache.getCachedSpans(key)) {
cache.removeSpan(span);
}
}
// Sanity check that the cache really is empty now.
assertTrue(cache.getKeys().isEmpty());
}
}
...@@ -163,7 +163,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase { ...@@ -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 { ...@@ -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 { ...@@ -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)
......
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.upstream.cache;
import android.test.InstrumentationTestCase;
import com.google.android.exoplayer2.extractor.ChunkIndex;
import com.google.android.exoplayer2.testutil.TestUtil;
import java.io.File;
import java.io.IOException;
import org.mockito.Mock;
/**
* Tests for {@link CachedRegionTracker}.
*/
public final class CachedRegionTrackerTest extends InstrumentationTestCase {
private static final String CACHE_KEY = "abc";
private static final long MS_IN_US = 1000;
// 5 chunks, each 20 bytes long and 100 ms long.
private static final ChunkIndex CHUNK_INDEX = new ChunkIndex(
new int[] {20, 20, 20, 20, 20},
new long[] {100, 120, 140, 160, 180},
new long[] {100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US},
new long[] {0, 100 * MS_IN_US, 200 * MS_IN_US, 300 * MS_IN_US, 400 * MS_IN_US});
@Mock private Cache cache;
private CachedRegionTracker tracker;
private CachedContentIndex index;
private File cacheDir;
@Override
protected void setUp() throws Exception {
TestUtil.setUpMockito(this);
tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX);
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
index = new CachedContentIndex(cacheDir);
}
@Override
protected void tearDown() throws Exception {
TestUtil.recursiveDelete(cacheDir);
}
public void testGetRegion_noSpansInCache() {
assertEquals(CachedRegionTracker.NOT_CACHED, tracker.getRegionEndTimeMs(100));
assertEquals(CachedRegionTracker.NOT_CACHED, tracker.getRegionEndTimeMs(150));
}
public void testGetRegion_fullyCached() throws Exception {
tracker.onSpanAdded(
cache,
newCacheSpan(100, 100));
assertEquals(CachedRegionTracker.CACHED_TO_END, tracker.getRegionEndTimeMs(101));
assertEquals(CachedRegionTracker.CACHED_TO_END, tracker.getRegionEndTimeMs(121));
}
public void testGetRegion_partiallyCached() throws Exception {
tracker.onSpanAdded(
cache,
newCacheSpan(100, 40));
assertEquals(200, tracker.getRegionEndTimeMs(101));
assertEquals(200, tracker.getRegionEndTimeMs(121));
}
public void testGetRegion_multipleSpanAddsJoinedCorrectly() throws Exception {
tracker.onSpanAdded(
cache,
newCacheSpan(100, 20));
tracker.onSpanAdded(
cache,
newCacheSpan(120, 20));
assertEquals(200, tracker.getRegionEndTimeMs(101));
assertEquals(200, tracker.getRegionEndTimeMs(121));
}
public void testGetRegion_fullyCachedThenPartiallyRemoved() throws Exception {
// Start with the full stream in cache.
tracker.onSpanAdded(
cache,
newCacheSpan(100, 100));
// Remove the middle bit.
tracker.onSpanRemoved(
cache,
newCacheSpan(140, 40));
assertEquals(200, tracker.getRegionEndTimeMs(101));
assertEquals(200, tracker.getRegionEndTimeMs(121));
assertEquals(CachedRegionTracker.CACHED_TO_END, tracker.getRegionEndTimeMs(181));
}
public void testGetRegion_subchunkEstimation() throws Exception {
tracker.onSpanAdded(
cache,
newCacheSpan(100, 10));
assertEquals(50, tracker.getRegionEndTimeMs(101));
assertEquals(CachedRegionTracker.NOT_CACHED, tracker.getRegionEndTimeMs(111));
}
private CacheSpan newCacheSpan(int position, int length) throws IOException {
return SimpleCacheSpanTest.createCacheSpan(index, cacheDir, CACHE_KEY, position, length, 0);
}
}
...@@ -16,12 +16,16 @@ ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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;
}
} }
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.upstream.crypto;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util;
import java.util.Random;
import javax.crypto.Cipher;
import junit.framework.TestCase;
/**
* Unit tests for {@link AesFlushingCipher}.
*/
public class AesFlushingCipherTest extends TestCase {
private static final int DATA_LENGTH = 65536;
private static final byte[] KEY = Util.getUtf8Bytes("testKey:12345678");
private static final long NONCE = 0;
private static final long START_OFFSET = 11;
private static final long RANDOM_SEED = 0x12345678;
private AesFlushingCipher encryptCipher;
private AesFlushingCipher decryptCipher;
@Override
protected void setUp() {
encryptCipher = new AesFlushingCipher(Cipher.ENCRYPT_MODE, KEY, NONCE, START_OFFSET);
decryptCipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, KEY, NONCE, START_OFFSET);
}
@Override
protected void tearDown() {
encryptCipher = null;
decryptCipher = null;
}
private long getMaxUnchangedBytesAllowedPostEncryption(long length) {
// Assuming that not more than 10% of the resultant bytes should be identical.
// The value of 10% is arbitrary, ciphers standards do not name a value.
return length / 10;
}
// Count the number of bytes that do not match.
private int getDifferingByteCount(byte[] data1, byte[] data2, int startOffset) {
int count = 0;
for (int i = startOffset; i < data1.length; i++) {
if (data1[i] != data2[i]) {
count++;
}
}
return count;
}
// Count the number of bytes that do not match.
private int getDifferingByteCount(byte[] data1, byte[] data2) {
return getDifferingByteCount(data1, data2, 0);
}
// Test a single encrypt and decrypt call
public void testSingle() {
byte[] reference = TestUtil.buildTestData(DATA_LENGTH);
byte[] data = reference.clone();
encryptCipher.updateInPlace(data, 0, data.length);
int unchangedByteCount = data.length - getDifferingByteCount(reference, data);
assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length));
decryptCipher.updateInPlace(data, 0, data.length);
int differingByteCount = getDifferingByteCount(reference, data);
assertEquals(0, differingByteCount);
}
// Test several encrypt and decrypt calls, each aligned on a 16 byte block size
public void testAligned() {
byte[] reference = TestUtil.buildTestData(DATA_LENGTH);
byte[] data = reference.clone();
Random random = new Random(RANDOM_SEED);
int offset = 0;
while (offset < data.length) {
int bytes = (1 + random.nextInt(50)) * 16;
bytes = Math.min(bytes, data.length - offset);
assertEquals(0, bytes % 16);
encryptCipher.updateInPlace(data, offset, bytes);
offset += bytes;
}
int unchangedByteCount = data.length - getDifferingByteCount(reference, data);
assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length));
offset = 0;
while (offset < data.length) {
int bytes = (1 + random.nextInt(50)) * 16;
bytes = Math.min(bytes, data.length - offset);
assertEquals(0, bytes % 16);
decryptCipher.updateInPlace(data, offset, bytes);
offset += bytes;
}
int differingByteCount = getDifferingByteCount(reference, data);
assertEquals(0, differingByteCount);
}
// Test several encrypt and decrypt calls, not aligned on block boundary
public void testUnAligned() {
byte[] reference = TestUtil.buildTestData(DATA_LENGTH);
byte[] data = reference.clone();
Random random = new Random(RANDOM_SEED);
// Encrypt
int offset = 0;
while (offset < data.length) {
int bytes = 1 + random.nextInt(4095);
bytes = Math.min(bytes, data.length - offset);
encryptCipher.updateInPlace(data, offset, bytes);
offset += bytes;
}
int unchangedByteCount = data.length - getDifferingByteCount(reference, data);
assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length));
offset = 0;
while (offset < data.length) {
int bytes = 1 + random.nextInt(4095);
bytes = Math.min(bytes, data.length - offset);
decryptCipher.updateInPlace(data, offset, bytes);
offset += bytes;
}
int differingByteCount = getDifferingByteCount(reference, data);
assertEquals(0, differingByteCount);
}
// Test decryption starting from the middle of an encrypted block
public void testMidJoin() {
byte[] reference = TestUtil.buildTestData(DATA_LENGTH);
byte[] data = reference.clone();
Random random = new Random(RANDOM_SEED);
// Encrypt
int offset = 0;
while (offset < data.length) {
int bytes = 1 + random.nextInt(4095);
bytes = Math.min(bytes, data.length - offset);
encryptCipher.updateInPlace(data, offset, bytes);
offset += bytes;
}
// Verify
int unchangedByteCount = data.length - getDifferingByteCount(reference, data);
assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length));
// Setup decryption from random location
offset = random.nextInt(4096);
decryptCipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, KEY, NONCE, offset + START_OFFSET);
int remainingLength = data.length - offset;
int originalOffset = offset;
// Decrypt
while (remainingLength > 0) {
int bytes = 1 + random.nextInt(4095);
bytes = Math.min(bytes, remainingLength);
decryptCipher.updateInPlace(data, offset, bytes);
offset += bytes;
remainingLength -= bytes;
}
// Verify
int differingByteCount = getDifferingByteCount(reference, data, originalOffset);
assertEquals(0, differingByteCount);
}
}
...@@ -371,6 +371,73 @@ public class ParsableByteArrayTest extends TestCase { ...@@ -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'
......
...@@ -28,6 +28,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { ...@@ -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 { ...@@ -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);
...@@ -238,9 +241,14 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { ...@@ -238,9 +241,14 @@ 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 { ...@@ -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}.
*/ */
......
...@@ -15,6 +15,8 @@ ...@@ -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;
...@@ -97,6 +99,13 @@ public final class C { ...@@ -97,6 +99,13 @@ public final class C {
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.
*/ */
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
...@@ -543,4 +552,13 @@ public final class C { ...@@ -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();
}
} }
...@@ -19,6 +19,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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();
......
...@@ -447,4 +447,20 @@ public interface ExoPlayer { ...@@ -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();
} }
...@@ -22,12 +22,12 @@ import android.os.Message; ...@@ -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;
...@@ -272,6 +272,22 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -272,6 +272,22 @@ import java.util.concurrent.CopyOnWriteArraySet;
} }
@Override @Override
public boolean isCurrentWindowDynamic() {
if (timeline.isEmpty()) {
return false;
}
return timeline.getWindow(getCurrentWindowIndex(), window).isDynamic;
}
@Override
public boolean isCurrentWindowSeekable() {
if (timeline.isEmpty()) {
return false;
}
return timeline.getWindow(getCurrentWindowIndex(), window).isSeekable;
}
@Override
public int getRendererCount() { public int getRendererCount() {
return renderers.length; return renderers.length;
} }
...@@ -319,11 +335,11 @@ import java.util.concurrent.CopyOnWriteArraySet; ...@@ -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; ...@@ -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;
......
...@@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo { ...@@ -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 { ...@@ -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}
......
...@@ -183,20 +183,18 @@ public final class Format implements Parcelable { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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
......
...@@ -92,6 +92,7 @@ public interface Renderer extends ExoPlayerComponent { ...@@ -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 { ...@@ -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
......
...@@ -80,6 +80,20 @@ public interface RendererCapabilities { ...@@ -80,6 +80,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
* text renderer will return {@link C#TRACK_TYPE_TEXT}, and so on. * text renderer will return {@link C#TRACK_TYPE_TEXT}, and so on.
...@@ -91,7 +105,7 @@ public interface RendererCapabilities { ...@@ -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 { ...@@ -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.
......
/*
* 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;
}
}
...@@ -29,7 +29,6 @@ import android.view.SurfaceView; ...@@ -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; ...@@ -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 { ...@@ -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 { ...@@ -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;
...@@ -450,15 +448,6 @@ public class SimpleExoPlayer implements ExoPlayer { ...@@ -450,15 +448,6 @@ public class SimpleExoPlayer implements ExoPlayer {
} }
/** /**
* @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.
* *
* @param output The output. * @param output The output.
...@@ -490,8 +479,8 @@ public class SimpleExoPlayer implements ExoPlayer { ...@@ -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
...@@ -557,63 +546,73 @@ public class SimpleExoPlayer implements ExoPlayer { ...@@ -557,63 +546,73 @@ public class SimpleExoPlayer implements ExoPlayer {
} }
@Override @Override
public int getCurrentPeriodIndex() { public int getRendererCount() {
return player.getCurrentPeriodIndex(); return player.getRendererCount();
} }
@Override @Override
public int getCurrentWindowIndex() { public int getRendererType(int index) {
return player.getCurrentWindowIndex(); return player.getRendererType(index);
} }
@Override @Override
public long getDuration() { public TrackGroupArray getCurrentTrackGroups() {
return player.getDuration(); return player.getCurrentTrackGroups();
} }
@Override @Override
public long getCurrentPosition() { public TrackSelectionArray getCurrentTrackSelections() {
return player.getCurrentPosition(); return player.getCurrentTrackSelections();
} }
@Override @Override
public long getBufferedPosition() { public Timeline getCurrentTimeline() {
return player.getBufferedPosition(); return player.getCurrentTimeline();
} }
@Override @Override
public int getBufferedPercentage() { public Object getCurrentManifest() {
return player.getBufferedPercentage(); return player.getCurrentManifest();
} }
@Override @Override
public int getRendererCount() { public int getCurrentPeriodIndex() {
return player.getRendererCount(); return player.getCurrentPeriodIndex();
} }
@Override @Override
public int getRendererType(int index) { public int getCurrentWindowIndex() {
return player.getRendererType(index); return player.getCurrentWindowIndex();
} }
@Override @Override
public TrackGroupArray getCurrentTrackGroups() { public long getDuration() {
return player.getCurrentTrackGroups(); return player.getDuration();
} }
@Override @Override
public TrackSelectionArray getCurrentTrackSelections() { public long getCurrentPosition() {
return player.getCurrentTrackSelections(); return player.getCurrentPosition();
} }
@Override @Override
public Timeline getCurrentTimeline() { public long getBufferedPosition() {
return player.getCurrentTimeline(); return player.getBufferedPosition();
} }
@Override @Override
public Object getCurrentManifest() { public int getBufferedPercentage() {
return player.getCurrentManifest(); return player.getBufferedPercentage();
}
@Override
public boolean isCurrentWindowDynamic() {
return player.isCurrentWindowDynamic();
}
@Override
public boolean isCurrentWindowSeekable() {
return player.isCurrentWindowSeekable();
} }
// Renderer building. // Renderer building.
...@@ -772,7 +771,7 @@ public class SimpleExoPlayer implements ExoPlayer { ...@@ -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 { ...@@ -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
......
...@@ -41,8 +41,7 @@ import java.nio.ByteBuffer; ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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);
} }
} }
...@@ -16,6 +16,7 @@ ...@@ -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; ...@@ -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 ...@@ -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 ...@@ -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;
} }
...@@ -156,6 +153,25 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ...@@ -156,6 +153,25 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
} }
@Override @Override
public final int supportsFormat(Format format) {
int formatSupport = supportsFormatInternal(format);
if (formatSupport == FORMAT_UNSUPPORTED_TYPE || formatSupport == FORMAT_UNSUPPORTED_SUBTYPE) {
return formatSupport;
}
int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;
return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport;
}
/**
* Returns the {@link #FORMAT_SUPPORT_MASK} component of the return value for
* {@link #supportsFormat(Format)}.
*
* @param format The format.
* @return The extent to which the renderer supports the format itself.
*/
protected abstract int supportsFormatInternal(Format format);
@Override
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
if (outputStreamEnded) { if (outputStreamEnded) {
return; return;
...@@ -186,6 +202,33 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ...@@ -186,6 +202,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.
* *
* @param format The format for which a decoder is required. * @param format The format for which a decoder is required.
...@@ -244,28 +287,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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);
} }
} }
...@@ -16,9 +16,11 @@ ...@@ -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; ...@@ -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> { ...@@ -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();
} }
...@@ -105,7 +105,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback { ...@@ -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);
} }
} }
......
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.drm;
import android.util.Pair;
import com.google.android.exoplayer2.C;
import java.util.Map;
/**
* Utility methods for Widevine.
*/
public final class WidevineUtil {
/** Widevine specific key status field name for the remaining license duration, in seconds. */
public static final String PROPERTY_LICENSE_DURATION_REMAINING = "LicenseDurationRemaining";
/** Widevine specific key status field name for the remaining playback duration, in seconds. */
public static final String PROPERTY_PLAYBACK_DURATION_REMAINING = "PlaybackDurationRemaining";
private WidevineUtil() {}
/**
* Returns license and playback durations remaining in seconds.
*
* @return A {@link Pair} consisting of the remaining license and playback durations in seconds.
* @throws IllegalStateException If called when a session isn't opened.
* @param drmSession
*/
public static Pair<Long, Long> getLicenseDurationRemainingSec(DrmSession drmSession) {
Map<String, String> keyStatus = drmSession.queryKeyStatus();
return new Pair<>(
getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING),
getDurationRemainingSec(keyStatus, PROPERTY_PLAYBACK_DURATION_REMAINING));
}
private static long getDurationRemainingSec(Map<String, String> keyStatus, String property) {
if (keyStatus != null) {
try {
String value = keyStatus.get(property);
if (value != null) {
return Long.parseLong(value);
}
} catch (NumberFormatException e) {
// do nothing.
}
}
return C.TIME_UNSET;
}
}
...@@ -226,13 +226,32 @@ public final class DefaultTrackOutput implements TrackOutput { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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;
} }
......
...@@ -529,11 +529,9 @@ public final class MatroskaExtractor implements Extractor { ...@@ -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;
......
...@@ -127,6 +127,7 @@ import java.util.List; ...@@ -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; ...@@ -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;
......
...@@ -604,7 +604,7 @@ import java.util.List; ...@@ -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; ...@@ -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; ...@@ -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;
} }
......
...@@ -188,7 +188,7 @@ import com.google.android.exoplayer2.util.Util; ...@@ -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; ...@@ -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; ...@@ -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; ...@@ -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;
......
...@@ -83,8 +83,11 @@ public final class RawCcExtractor implements Extractor { ...@@ -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 { ...@@ -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,
......
...@@ -16,12 +16,11 @@ ...@@ -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.
......
...@@ -23,10 +23,10 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput; ...@@ -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;
/** /**
......
...@@ -16,10 +16,10 @@ ...@@ -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.
......
...@@ -17,8 +17,8 @@ package com.google.android.exoplayer2.extractor.ts; ...@@ -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;
/** /**
......
...@@ -15,10 +15,9 @@ ...@@ -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; ...@@ -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);
}
}
} }
} }

1.09 KB | W: | H:

354 Bytes | W: | H:

library/src/main/res/drawable-hdpi/exo_controls_fastforward.png
library/src/main/res/drawable-hdpi/exo_controls_fastforward.png
library/src/main/res/drawable-hdpi/exo_controls_fastforward.png
library/src/main/res/drawable-hdpi/exo_controls_fastforward.png
  • 2-up
  • Swipe
  • Onion skin

1.05 KB | W: | H:

323 Bytes | W: | H:

library/src/main/res/drawable-hdpi/exo_controls_next.png
library/src/main/res/drawable-hdpi/exo_controls_next.png
library/src/main/res/drawable-hdpi/exo_controls_next.png
library/src/main/res/drawable-hdpi/exo_controls_next.png
  • 2-up
  • Swipe
  • Onion skin

599 Bytes | W: | H:

108 Bytes | W: | H:

library/src/main/res/drawable-hdpi/exo_controls_pause.png
library/src/main/res/drawable-hdpi/exo_controls_pause.png
library/src/main/res/drawable-hdpi/exo_controls_pause.png
library/src/main/res/drawable-hdpi/exo_controls_pause.png
  • 2-up
  • Swipe
  • Onion skin

1.14 KB | W: | H:

286 Bytes | W: | H:

library/src/main/res/drawable-hdpi/exo_controls_play.png
library/src/main/res/drawable-hdpi/exo_controls_play.png
library/src/main/res/drawable-hdpi/exo_controls_play.png
library/src/main/res/drawable-hdpi/exo_controls_play.png
  • 2-up
  • Swipe
  • Onion skin

1.04 KB | W: | H:

292 Bytes | W: | H:

library/src/main/res/drawable-hdpi/exo_controls_previous.png
library/src/main/res/drawable-hdpi/exo_controls_previous.png
library/src/main/res/drawable-hdpi/exo_controls_previous.png
library/src/main/res/drawable-hdpi/exo_controls_previous.png
  • 2-up
  • Swipe
  • Onion skin

1.22 KB | W: | H:

347 Bytes | W: | H:

library/src/main/res/drawable-hdpi/exo_controls_rewind.png
library/src/main/res/drawable-hdpi/exo_controls_rewind.png
library/src/main/res/drawable-hdpi/exo_controls_rewind.png
library/src/main/res/drawable-hdpi/exo_controls_rewind.png
  • 2-up
  • Swipe
  • Onion skin

886 Bytes | W: | H:

192 Bytes | W: | H:

library/src/main/res/drawable-ldpi/exo_controls_fastforward.png
library/src/main/res/drawable-ldpi/exo_controls_fastforward.png
library/src/main/res/drawable-ldpi/exo_controls_fastforward.png
library/src/main/res/drawable-ldpi/exo_controls_fastforward.png
  • 2-up
  • Swipe
  • Onion skin

735 Bytes | W: | H:

167 Bytes | W: | H:

library/src/main/res/drawable-ldpi/exo_controls_next.png
library/src/main/res/drawable-ldpi/exo_controls_next.png
library/src/main/res/drawable-ldpi/exo_controls_next.png
library/src/main/res/drawable-ldpi/exo_controls_next.png
  • 2-up
  • Swipe
  • Onion skin

3.17 KB | W: | H:

91 Bytes | W: | H:

library/src/main/res/drawable-ldpi/exo_controls_pause.png
library/src/main/res/drawable-ldpi/exo_controls_pause.png
library/src/main/res/drawable-ldpi/exo_controls_pause.png
library/src/main/res/drawable-ldpi/exo_controls_pause.png
  • 2-up
  • Swipe
  • Onion skin

673 Bytes | W: | H:

182 Bytes | W: | H:

library/src/main/res/drawable-ldpi/exo_controls_play.png
library/src/main/res/drawable-ldpi/exo_controls_play.png
library/src/main/res/drawable-ldpi/exo_controls_play.png
library/src/main/res/drawable-ldpi/exo_controls_play.png
  • 2-up
  • Swipe
  • Onion skin

770 Bytes | W: | H:

187 Bytes | W: | H:

library/src/main/res/drawable-ldpi/exo_controls_previous.png
library/src/main/res/drawable-ldpi/exo_controls_previous.png
library/src/main/res/drawable-ldpi/exo_controls_previous.png
library/src/main/res/drawable-ldpi/exo_controls_previous.png
  • 2-up
  • Swipe
  • Onion skin

906 Bytes | W: | H:

214 Bytes | W: | H:

library/src/main/res/drawable-ldpi/exo_controls_rewind.png
library/src/main/res/drawable-ldpi/exo_controls_rewind.png
library/src/main/res/drawable-ldpi/exo_controls_rewind.png
library/src/main/res/drawable-ldpi/exo_controls_rewind.png
  • 2-up
  • Swipe
  • Onion skin

929 Bytes | W: | H:

255 Bytes | W: | H:

library/src/main/res/drawable-mdpi/exo_controls_fastforward.png
library/src/main/res/drawable-mdpi/exo_controls_fastforward.png
library/src/main/res/drawable-mdpi/exo_controls_fastforward.png
library/src/main/res/drawable-mdpi/exo_controls_fastforward.png
  • 2-up
  • Swipe
  • Onion skin

843 Bytes | W: | H:

276 Bytes | W: | H:

library/src/main/res/drawable-mdpi/exo_controls_next.png
library/src/main/res/drawable-mdpi/exo_controls_next.png
library/src/main/res/drawable-mdpi/exo_controls_next.png
library/src/main/res/drawable-mdpi/exo_controls_next.png
  • 2-up
  • Swipe
  • Onion skin

540 Bytes | W: | H:

153 Bytes | W: | H:

library/src/main/res/drawable-mdpi/exo_controls_pause.png
library/src/main/res/drawable-mdpi/exo_controls_pause.png
library/src/main/res/drawable-mdpi/exo_controls_pause.png
library/src/main/res/drawable-mdpi/exo_controls_pause.png
  • 2-up
  • Swipe
  • Onion skin

897 Bytes | W: | H:

228 Bytes | W: | H:

library/src/main/res/drawable-mdpi/exo_controls_play.png
library/src/main/res/drawable-mdpi/exo_controls_play.png
library/src/main/res/drawable-mdpi/exo_controls_play.png
library/src/main/res/drawable-mdpi/exo_controls_play.png
  • 2-up
  • Swipe
  • Onion skin

837 Bytes | W: | H:

227 Bytes | W: | H:

library/src/main/res/drawable-mdpi/exo_controls_previous.png
library/src/main/res/drawable-mdpi/exo_controls_previous.png
library/src/main/res/drawable-mdpi/exo_controls_previous.png
library/src/main/res/drawable-mdpi/exo_controls_previous.png
  • 2-up
  • Swipe
  • Onion skin

997 Bytes | W: | H:

273 Bytes | W: | H:

library/src/main/res/drawable-mdpi/exo_controls_rewind.png
library/src/main/res/drawable-mdpi/exo_controls_rewind.png
library/src/main/res/drawable-mdpi/exo_controls_rewind.png
library/src/main/res/drawable-mdpi/exo_controls_rewind.png
  • 2-up
  • Swipe
  • Onion skin

1.44 KB | W: | H:

392 Bytes | W: | H:

library/src/main/res/drawable-xhdpi/exo_controls_fastforward.png
library/src/main/res/drawable-xhdpi/exo_controls_fastforward.png
library/src/main/res/drawable-xhdpi/exo_controls_fastforward.png
library/src/main/res/drawable-xhdpi/exo_controls_fastforward.png
  • 2-up
  • Swipe
  • Onion skin

1.33 KB | W: | H:

334 Bytes | W: | H:

library/src/main/res/drawable-xhdpi/exo_controls_next.png
library/src/main/res/drawable-xhdpi/exo_controls_next.png
library/src/main/res/drawable-xhdpi/exo_controls_next.png
library/src/main/res/drawable-xhdpi/exo_controls_next.png
  • 2-up
  • Swipe
  • Onion skin

685 Bytes | W: | H:

164 Bytes | W: | H:

library/src/main/res/drawable-xhdpi/exo_controls_pause.png
library/src/main/res/drawable-xhdpi/exo_controls_pause.png
library/src/main/res/drawable-xhdpi/exo_controls_pause.png
library/src/main/res/drawable-xhdpi/exo_controls_pause.png
  • 2-up
  • Swipe
  • Onion skin

1.58 KB | W: | H:

343 Bytes | W: | H:

library/src/main/res/drawable-xhdpi/exo_controls_play.png
library/src/main/res/drawable-xhdpi/exo_controls_play.png
library/src/main/res/drawable-xhdpi/exo_controls_play.png
library/src/main/res/drawable-xhdpi/exo_controls_play.png
  • 2-up
  • Swipe
  • Onion skin

1.34 KB | W: | H:

339 Bytes | W: | H:

library/src/main/res/drawable-xhdpi/exo_controls_previous.png
library/src/main/res/drawable-xhdpi/exo_controls_previous.png
library/src/main/res/drawable-xhdpi/exo_controls_previous.png
library/src/main/res/drawable-xhdpi/exo_controls_previous.png
  • 2-up
  • Swipe
  • Onion skin

1.64 KB | W: | H:

400 Bytes | W: | H:

library/src/main/res/drawable-xhdpi/exo_controls_rewind.png
library/src/main/res/drawable-xhdpi/exo_controls_rewind.png
library/src/main/res/drawable-xhdpi/exo_controls_rewind.png
library/src/main/res/drawable-xhdpi/exo_controls_rewind.png
  • 2-up
  • Swipe
  • Onion skin

1.29 KB | W: | H:

391 Bytes | W: | H:

library/src/main/res/drawable-xxhdpi/exo_controls_next.png
library/src/main/res/drawable-xxhdpi/exo_controls_next.png
library/src/main/res/drawable-xxhdpi/exo_controls_next.png
library/src/main/res/drawable-xxhdpi/exo_controls_next.png
  • 2-up
  • Swipe
  • Onion skin

611 Bytes | W: | H:

113 Bytes | W: | H:

library/src/main/res/drawable-xxhdpi/exo_controls_pause.png
library/src/main/res/drawable-xxhdpi/exo_controls_pause.png
library/src/main/res/drawable-xxhdpi/exo_controls_pause.png
library/src/main/res/drawable-xxhdpi/exo_controls_pause.png
  • 2-up
  • Swipe
  • Onion skin

1.16 KB | W: | H:

384 Bytes | W: | H:

library/src/main/res/drawable-xxhdpi/exo_controls_play.png
library/src/main/res/drawable-xxhdpi/exo_controls_play.png
library/src/main/res/drawable-xxhdpi/exo_controls_play.png
library/src/main/res/drawable-xxhdpi/exo_controls_play.png
  • 2-up
  • Swipe
  • Onion skin

1.26 KB | W: | H:

464 Bytes | W: | H:

library/src/main/res/drawable-xxhdpi/exo_controls_previous.png
library/src/main/res/drawable-xxhdpi/exo_controls_previous.png
library/src/main/res/drawable-xxhdpi/exo_controls_previous.png
library/src/main/res/drawable-xxhdpi/exo_controls_previous.png
  • 2-up
  • Swipe
  • Onion skin

1.17 KB | W: | H:

571 Bytes | W: | H:

library/src/main/res/drawable-xxhdpi/exo_controls_rewind.png
library/src/main/res/drawable-xxhdpi/exo_controls_rewind.png
library/src/main/res/drawable-xxhdpi/exo_controls_rewind.png
library/src/main/res/drawable-xxhdpi/exo_controls_rewind.png
  • 2-up
  • Swipe
  • Onion skin
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment