|
|
@ -1,9 +1,69 @@
|
|||
# Release notes #
|
||||
|
||||
### r2.1.1 ###
|
||||
### r2.2.0 ###
|
||||
|
||||
Bugfix release only. Users of r2.1.0 and r2.0.x should proactively update to
|
||||
this version.
|
||||
* 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).
|
||||
|
||||
### r2.1.1 ###
|
||||
|
||||
* Fix some subtitle types (e.g. WebVTT) being displayed out of sync
|
||||
([#2208](https://github.com/google/ExoPlayer/issues/2208)).
|
||||
|
|
@ -52,9 +112,9 @@ this version.
|
|||
* Improved flexibility of SimpleExoPlayer
|
||||
([#2102](https://github.com/google/ExoPlayer/issues/2102)).
|
||||
* Fix issue where only the audio of a video would play due to capability
|
||||
detection issues ([#2007](https://github.com/google/ExoPlayer/issues/2007))
|
||||
([#2034](https://github.com/google/ExoPlayer/issues/2034))
|
||||
([#2157](https://github.com/google/ExoPlayer/issues/2157)).
|
||||
detection issues ([#2007](https://github.com/google/ExoPlayer/issues/2007),
|
||||
[#2034](https://github.com/google/ExoPlayer/issues/2034) and
|
||||
[#2157](https://github.com/google/ExoPlayer/issues/2157)).
|
||||
* Fix issues that could cause ExtractorMediaSource based playbacks to get stuck
|
||||
buffering ([#1962](https://github.com/google/ExoPlayer/issues/1962)).
|
||||
* Correctly set SimpleExoPlayerView surface aspect ratio when an active player
|
||||
|
|
@ -74,11 +134,6 @@ this version.
|
|||
|
||||
### 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
|
||||
([#1914](https://github.com/google/ExoPlayer/issues/1914)).
|
||||
* Fixed NullPointerException in HlsMediaPeriod
|
||||
|
|
@ -191,6 +246,14 @@ in all V2 releases. This cannot be assumed for changes in r1.5.12 and later,
|
|||
however it can be assumed that all such changes are included in the most recent
|
||||
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 ###
|
||||
|
||||
* Improvements to the upstream cache package.
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ allprojects {
|
|||
releaseRepoName = 'exoplayer'
|
||||
releaseUserOrg = 'google'
|
||||
releaseGroupId = 'com.google.android.exoplayer'
|
||||
releaseVersion = 'r2.1.1'
|
||||
releaseVersion = 'r2.2.0'
|
||||
releaseWebsite = 'https://github.com/google/ExoPlayer'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 31 KiB |
|
|
@ -24,24 +24,19 @@ android {
|
|||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
shrinkResources true
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||
}
|
||||
debug {
|
||||
jniDebuggable = true
|
||||
debuggable = true
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
productFlavors {
|
||||
noExtensions
|
||||
withExtensions
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@
|
|||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.demo"
|
||||
android:versionCode="2101"
|
||||
android:versionName="2.1.1">
|
||||
android:versionCode="2200"
|
||||
android:versionName="2.2.0">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
|
||||
<application
|
||||
android:label="@string/application_name"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:banner="@drawable/ic_banner"
|
||||
android:largeHeap="true"
|
||||
android:allowBackup="false"
|
||||
|
|
|
|||
|
|
@ -183,52 +183,52 @@
|
|||
"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",
|
||||
"drm_scheme": "widevine",
|
||||
"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",
|
||||
"drm_scheme": "widevine",
|
||||
"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",
|
||||
"drm_scheme": "widevine",
|
||||
"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",
|
||||
"drm_scheme": "widevine",
|
||||
"drm_license_url": "https://proxy.uat.widevine.com/proxy?provider=widevine_test"
|
||||
},
|
||||
{
|
||||
"name": "WV: Secure Subsample (WebM, VP9 with altref)",
|
||||
"uri": "https://storage.googleapis.com/widevine_test/vp9/sintel_1080p_vp9_altref_subsample/sintel_1080p_vp9_altref_subsample.mpd",
|
||||
"name": "WV: Secure Subsample SD & HD (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears.mpd",
|
||||
"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)",
|
||||
"uri": "https://storage.googleapis.com/widevine_test/vp9/sintel_1080p_vp9_altref_fullsample/sintel_1080p_vp9_altref_fullsample.mpd",
|
||||
"name": "WV: Secure Subsample SD (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_sd.mpd",
|
||||
"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)",
|
||||
"uri": "https://storage.googleapis.com/widevine_test/vp9/sintel_1080p_vp9_noaltref_subsample/sintel_1080p_vp9_noaltref_subsample.mpd",
|
||||
"name": "WV: Secure Subsample HD (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_hd.mpd",
|
||||
"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)",
|
||||
"uri": "https://storage.googleapis.com/widevine_test/vp9/sintel_1080p_vp9_noaltref_fullsample/sintel_1080p_vp9_noaltref_fullsample.mpd",
|
||||
"name": "WV: Secure Subsample UHD (WebM,VP9)",
|
||||
"uri": "https://storage.googleapis.com/wvmedia/cenc/vp9/subsample/24fps/tears/tears_uhd.mpd",
|
||||
"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;
|
|||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
import com.google.android.exoplayer2.drm.StreamingDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.MetadataRenderer;
|
||||
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
|
||||
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.GeobFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
|
||||
import com.google.android.exoplayer2.metadata.id3.PrivFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.TxxxFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.UrlLinkFrame;
|
||||
import com.google.android.exoplayer2.source.AdaptiveMediaSourceEventListener;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
|
|
@ -55,7 +56,7 @@ import java.util.Locale;
|
|||
*/
|
||||
/* package */ final class EventLogger implements ExoPlayer.EventListener,
|
||||
AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener,
|
||||
ExtractorMediaSource.EventListener, StreamingDrmSessionManager.EventListener,
|
||||
ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener,
|
||||
MetadataRenderer.Output {
|
||||
|
||||
private static final String TAG = "EventLogger";
|
||||
|
|
@ -153,7 +154,7 @@ import java.util.Locale;
|
|||
String formatSupport = getFormatSupportString(
|
||||
mappedTrackInfo.getTrackFormatSupport(rendererIndex, groupIndex, trackIndex));
|
||||
Log.d(TAG, " " + status + " Track:" + trackIndex + ", "
|
||||
+ getFormatString(trackGroup.getFormat(trackIndex))
|
||||
+ Format.toLogString(trackGroup.getFormat(trackIndex))
|
||||
+ ", supported=" + formatSupport);
|
||||
}
|
||||
Log.d(TAG, " ]");
|
||||
|
|
@ -185,7 +186,7 @@ import java.util.Locale;
|
|||
String formatSupport = getFormatSupportString(
|
||||
RendererCapabilities.FORMAT_UNSUPPORTED_TYPE);
|
||||
Log.d(TAG, " " + status + " Track:" + trackIndex + ", "
|
||||
+ getFormatString(trackGroup.getFormat(trackIndex))
|
||||
+ Format.toLogString(trackGroup.getFormat(trackIndex))
|
||||
+ ", supported=" + formatSupport);
|
||||
}
|
||||
Log.d(TAG, " ]");
|
||||
|
|
@ -224,7 +225,7 @@ import java.util.Locale;
|
|||
|
||||
@Override
|
||||
public void onAudioInputFormatChanged(Format format) {
|
||||
Log.d(TAG, "audioFormatChanged [" + getSessionTimeString() + ", " + getFormatString(format)
|
||||
Log.d(TAG, "audioFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format)
|
||||
+ "]");
|
||||
}
|
||||
|
||||
|
|
@ -254,7 +255,7 @@ import java.util.Locale;
|
|||
|
||||
@Override
|
||||
public void onVideoInputFormatChanged(Format format) {
|
||||
Log.d(TAG, "videoFormatChanged [" + getSessionTimeString() + ", " + getFormatString(format)
|
||||
Log.d(TAG, "videoFormatChanged [" + getSessionTimeString() + ", " + Format.toLogString(format)
|
||||
+ "]");
|
||||
}
|
||||
|
||||
|
|
@ -279,13 +280,23 @@ import java.util.Locale;
|
|||
// Do nothing.
|
||||
}
|
||||
|
||||
// StreamingDrmSessionManager.EventListener
|
||||
// DefaultDrmSessionManager.EventListener
|
||||
|
||||
@Override
|
||||
public void onDrmSessionManagerError(Exception e) {
|
||||
printInternalError("drmSessionManagerError", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmKeysRestored() {
|
||||
Log.d(TAG, "drmKeysRestored [" + getSessionTimeString() + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmKeysRemoved() {
|
||||
Log.d(TAG, "drmKeysRemoved [" + getSessionTimeString() + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmKeysLoaded() {
|
||||
Log.d(TAG, "drmKeysLoaded [" + getSessionTimeString() + "]");
|
||||
|
|
@ -349,10 +360,13 @@ import java.util.Locale;
|
|||
private void printMetadata(Metadata metadata, String prefix) {
|
||||
for (int i = 0; i < metadata.length(); i++) {
|
||||
Metadata.Entry entry = metadata.get(i);
|
||||
if (entry instanceof TxxxFrame) {
|
||||
TxxxFrame txxxFrame = (TxxxFrame) entry;
|
||||
Log.d(TAG, prefix + String.format("%s: description=%s, value=%s", txxxFrame.id,
|
||||
txxxFrame.description, txxxFrame.value));
|
||||
if (entry instanceof TextInformationFrame) {
|
||||
TextInformationFrame textInformationFrame = (TextInformationFrame) entry;
|
||||
Log.d(TAG, prefix + String.format("%s: value=%s", textInformationFrame.id,
|
||||
textInformationFrame.value));
|
||||
} else if (entry instanceof UrlLinkFrame) {
|
||||
UrlLinkFrame urlLinkFrame = (UrlLinkFrame) entry;
|
||||
Log.d(TAG, prefix + String.format("%s: url=%s", urlLinkFrame.id, urlLinkFrame.url));
|
||||
} else if (entry instanceof PrivFrame) {
|
||||
PrivFrame privFrame = (PrivFrame) entry;
|
||||
Log.d(TAG, prefix + String.format("%s: owner=%s", privFrame.id, privFrame.owner));
|
||||
|
|
@ -364,17 +378,17 @@ import java.util.Locale;
|
|||
ApicFrame apicFrame = (ApicFrame) entry;
|
||||
Log.d(TAG, prefix + String.format("%s: mimeType=%s, description=%s",
|
||||
apicFrame.id, apicFrame.mimeType, apicFrame.description));
|
||||
} else if (entry instanceof TextInformationFrame) {
|
||||
TextInformationFrame textInformationFrame = (TextInformationFrame) entry;
|
||||
Log.d(TAG, prefix + String.format("%s: description=%s", textInformationFrame.id,
|
||||
textInformationFrame.description));
|
||||
} else if (entry instanceof CommentFrame) {
|
||||
CommentFrame commentFrame = (CommentFrame) entry;
|
||||
Log.d(TAG, prefix + String.format("%s: language=%s description=%s", commentFrame.id,
|
||||
Log.d(TAG, prefix + String.format("%s: language=%s, description=%s", commentFrame.id,
|
||||
commentFrame.language, commentFrame.description));
|
||||
} else if (entry instanceof Id3Frame) {
|
||||
Id3Frame id3Frame = (Id3Frame) entry;
|
||||
Log.d(TAG, prefix + String.format("%s", id3Frame.id));
|
||||
} else if (entry instanceof EventMessage) {
|
||||
EventMessage eventMessage = (EventMessage) entry;
|
||||
Log.d(TAG, prefix + String.format("EMSG: scheme=%s, id=%d, value=%s",
|
||||
eventMessage.schemeIdUri, eventMessage.id, eventMessage.value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -433,33 +447,6 @@ import java.util.Locale;
|
|||
}
|
||||
}
|
||||
|
||||
private static String getFormatString(Format format) {
|
||||
if (format == null) {
|
||||
return "null";
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("id=").append(format.id).append(", mimeType=").append(format.sampleMimeType);
|
||||
if (format.bitrate != Format.NO_VALUE) {
|
||||
builder.append(", bitrate=").append(format.bitrate);
|
||||
}
|
||||
if (format.width != Format.NO_VALUE && format.height != Format.NO_VALUE) {
|
||||
builder.append(", res=").append(format.width).append("x").append(format.height);
|
||||
}
|
||||
if (format.frameRate != Format.NO_VALUE) {
|
||||
builder.append(", fps=").append(format.frameRate);
|
||||
}
|
||||
if (format.channelCount != Format.NO_VALUE) {
|
||||
builder.append(", channels=").append(format.channelCount);
|
||||
}
|
||||
if (format.sampleRate != Format.NO_VALUE) {
|
||||
builder.append(", sample_rate=").append(format.sampleRate);
|
||||
}
|
||||
if (format.language != null) {
|
||||
builder.append(", language=").append(format.language);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static String getTrackStatusString(TrackSelection selection, TrackGroup group,
|
||||
int trackIndex) {
|
||||
return getTrackStatusString(selection != null && selection.getTrackGroup() == group
|
||||
|
|
|
|||
|
|
@ -36,15 +36,16 @@ import com.google.android.exoplayer2.ExoPlayer;
|
|||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
||||
import com.google.android.exoplayer2.drm.FrameworkMediaDrm;
|
||||
import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
||||
import com.google.android.exoplayer2.drm.StreamingDrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
|
||||
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||
import com.google.android.exoplayer2.source.ExtractorMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
|
|
@ -100,7 +101,6 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||
}
|
||||
|
||||
private Handler mainHandler;
|
||||
private Timeline.Window window;
|
||||
private EventLogger eventLogger;
|
||||
private SimpleExoPlayerView simpleExoPlayerView;
|
||||
private LinearLayout debugRootView;
|
||||
|
|
@ -115,9 +115,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||
private boolean playerNeedsSource;
|
||||
|
||||
private boolean shouldAutoPlay;
|
||||
private boolean isTimelineStatic;
|
||||
private int playerWindow;
|
||||
private long playerPosition;
|
||||
private int resumeWindow;
|
||||
private long resumePosition;
|
||||
|
||||
// Activity lifecycle
|
||||
|
||||
|
|
@ -125,9 +124,9 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
shouldAutoPlay = true;
|
||||
clearResumePosition();
|
||||
mediaDataSourceFactory = buildDataSourceFactory(true);
|
||||
mainHandler = new Handler();
|
||||
window = new Timeline.Window();
|
||||
if (CookieHandler.getDefault() != DEFAULT_COOKIE_MANAGER) {
|
||||
CookieHandler.setDefault(DEFAULT_COOKIE_MANAGER);
|
||||
}
|
||||
|
|
@ -148,7 +147,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
releasePlayer();
|
||||
isTimelineStatic = false;
|
||||
shouldAutoPlay = true;
|
||||
clearResumePosition();
|
||||
setIntent(intent);
|
||||
}
|
||||
|
||||
|
|
@ -264,7 +264,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||
@SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode =
|
||||
((DemoApplication) getApplication()).useExtensionRenderers()
|
||||
? (preferExtensionDecoders ? SimpleExoPlayer.EXTENSION_RENDERER_MODE_PREFER
|
||||
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_ON)
|
||||
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_ON)
|
||||
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF;
|
||||
TrackSelection.Factory videoTrackSelectionFactory =
|
||||
new AdaptiveVideoTrackSelection.Factory(BANDWIDTH_METER);
|
||||
|
|
@ -278,16 +278,9 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||
player.addListener(eventLogger);
|
||||
player.setAudioDebugListener(eventLogger);
|
||||
player.setVideoDebugListener(eventLogger);
|
||||
player.setId3Output(eventLogger);
|
||||
player.setMetadataOutput(eventLogger);
|
||||
|
||||
simpleExoPlayerView.setPlayer(player);
|
||||
if (isTimelineStatic) {
|
||||
if (playerPosition == C.TIME_UNSET) {
|
||||
player.seekToDefaultPosition(playerWindow);
|
||||
} else {
|
||||
player.seekTo(playerWindow, playerPosition);
|
||||
}
|
||||
}
|
||||
player.setPlayWhenReady(shouldAutoPlay);
|
||||
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
|
||||
debugViewHelper.start();
|
||||
|
|
@ -324,7 +317,11 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||
}
|
||||
MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
|
||||
: 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;
|
||||
updateButtonVisibilities();
|
||||
}
|
||||
|
|
@ -358,7 +355,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||
}
|
||||
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl,
|
||||
buildHttpDataSourceFactory(false), keyRequestProperties);
|
||||
return new StreamingDrmSessionManager<>(uuid,
|
||||
return new DefaultDrmSessionManager<>(uuid,
|
||||
FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger);
|
||||
}
|
||||
|
||||
|
|
@ -367,12 +364,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||
debugViewHelper.stop();
|
||||
debugViewHelper = null;
|
||||
shouldAutoPlay = player.getPlayWhenReady();
|
||||
playerWindow = player.getCurrentWindowIndex();
|
||||
playerPosition = C.TIME_UNSET;
|
||||
Timeline timeline = player.getCurrentTimeline();
|
||||
if (!timeline.isEmpty() && timeline.getWindow(playerWindow, window).isSeekable) {
|
||||
playerPosition = player.getCurrentPosition();
|
||||
}
|
||||
updateResumePosition();
|
||||
player.release();
|
||||
player = null;
|
||||
trackSelector = null;
|
||||
|
|
@ -381,6 +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.
|
||||
*
|
||||
|
|
@ -422,13 +425,17 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||
|
||||
@Override
|
||||
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
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||
isTimelineStatic = !timeline.isEmpty()
|
||||
&& !timeline.getWindow(timeline.getWindowCount() - 1, window).isDynamic;
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -460,8 +467,14 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||
showToast(errorString);
|
||||
}
|
||||
playerNeedsSource = true;
|
||||
updateButtonVisibilities();
|
||||
showControls();
|
||||
if (isBehindLiveWindow(e)) {
|
||||
clearResumePosition();
|
||||
initializePlayer();
|
||||
} else {
|
||||
updateResumePosition();
|
||||
updateButtonVisibilities();
|
||||
showControls();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -535,4 +548,18 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private static boolean isBehindLiveWindow(ExoPlaybackException e) {
|
||||
if (e.type != ExoPlaybackException.TYPE_SOURCE) {
|
||||
return false;
|
||||
}
|
||||
Throwable cause = e.getSourceException();
|
||||
while (cause != null) {
|
||||
if (cause instanceof BehindLiveWindowException) {
|
||||
return true;
|
||||
}
|
||||
cause = cause.getCause();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
BIN
demo/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
demo/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
demo/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
demo/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -16,8 +16,7 @@
|
|||
|
||||
<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">ExoPlayer2 Demo</string>
|
||||
<string name="application_name">ExoPlayer</string>
|
||||
|
||||
<string name="video">Video</string>
|
||||
|
||||
|
|
|
|||
|
|
@ -23,17 +23,6 @@ android {
|
|||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
sourceSets.main {
|
||||
jniLibs.srcDirs = ['jniLibs']
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ import java.util.Map;
|
|||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.chromium.net.CronetEngine;
|
||||
import org.chromium.net.NetworkException;
|
||||
import org.chromium.net.UrlRequest;
|
||||
import org.chromium.net.UrlRequestException;
|
||||
import org.chromium.net.UrlResponseInfo;
|
||||
import org.chromium.net.impl.UrlResponseInfoImpl;
|
||||
import org.junit.Before;
|
||||
|
|
@ -99,7 +99,7 @@ public final class CronetDataSourceTest {
|
|||
@Mock
|
||||
private Executor mockExecutor;
|
||||
@Mock
|
||||
private UrlRequestException mockUrlRequestException;
|
||||
private NetworkException mockNetworkException;
|
||||
@Mock private CronetEngine mockCronetEngine;
|
||||
|
||||
private CronetDataSource dataSourceUnderTest;
|
||||
|
|
@ -172,7 +172,7 @@ public final class CronetDataSourceTest {
|
|||
dataSourceUnderTest.onFailed(
|
||||
mockUrlRequest,
|
||||
testUrlResponseInfo,
|
||||
mockUrlRequestException);
|
||||
mockNetworkException);
|
||||
dataSourceUnderTest.onResponseStarted(
|
||||
mockUrlRequest2,
|
||||
testUrlResponseInfo);
|
||||
|
|
@ -245,8 +245,8 @@ public final class CronetDataSourceTest {
|
|||
@Test
|
||||
public void testRequestOpenFailDueToDnsFailure() {
|
||||
mockResponseStartFailure();
|
||||
when(mockUrlRequestException.getErrorCode()).thenReturn(
|
||||
UrlRequestException.ERROR_HOSTNAME_NOT_RESOLVED);
|
||||
when(mockNetworkException.getErrorCode()).thenReturn(
|
||||
NetworkException.ERROR_HOSTNAME_NOT_RESOLVED);
|
||||
|
||||
try {
|
||||
dataSourceUnderTest.open(testDataSpec);
|
||||
|
|
@ -728,7 +728,7 @@ public final class CronetDataSourceTest {
|
|||
dataSourceUnderTest.onFailed(
|
||||
mockUrlRequest,
|
||||
createUrlResponseInfo(500), // statusCode
|
||||
mockUrlRequestException);
|
||||
mockNetworkException);
|
||||
return null;
|
||||
}
|
||||
}).when(mockUrlRequest).start();
|
||||
|
|
@ -764,7 +764,7 @@ public final class CronetDataSourceTest {
|
|||
dataSourceUnderTest.onFailed(
|
||||
mockUrlRequest,
|
||||
createUrlResponseInfo(500), // statusCode
|
||||
mockUrlRequestException);
|
||||
mockNetworkException);
|
||||
return null;
|
||||
}
|
||||
}).when(mockUrlRequest).read(any(ByteBuffer.class));
|
||||
|
|
|
|||
|
|
@ -40,9 +40,10 @@ import java.util.concurrent.Executor;
|
|||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.chromium.net.CronetEngine;
|
||||
import org.chromium.net.CronetException;
|
||||
import org.chromium.net.NetworkException;
|
||||
import org.chromium.net.UrlRequest;
|
||||
import org.chromium.net.UrlRequest.Status;
|
||||
import org.chromium.net.UrlRequestException;
|
||||
import org.chromium.net.UrlResponseInfo;
|
||||
|
||||
/**
|
||||
|
|
@ -400,12 +401,17 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
|
||||
@Override
|
||||
public synchronized void onFailed(UrlRequest request, UrlResponseInfo info,
|
||||
UrlRequestException error) {
|
||||
CronetException error) {
|
||||
if (request != currentUrlRequest) {
|
||||
return;
|
||||
}
|
||||
exception = error.getErrorCode() == UrlRequestException.ERROR_HOSTNAME_NOT_RESOLVED
|
||||
? new UnknownHostException() : error;
|
||||
if (error instanceof NetworkException
|
||||
&& ((NetworkException) error).getErrorCode()
|
||||
== NetworkException.ERROR_HOSTNAME_NOT_RESOLVED) {
|
||||
exception = new UnknownHostException();
|
||||
} else {
|
||||
exception = error;
|
||||
}
|
||||
operation.open();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer2.ext.cronet;
|
||||
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.util.Predicate;
|
||||
|
|
@ -25,7 +26,7 @@ import org.chromium.net.CronetEngine;
|
|||
/**
|
||||
* A {@link Factory} that produces {@link CronetDataSource}.
|
||||
*/
|
||||
public final class CronetDataSourceFactory implements Factory {
|
||||
public final class CronetDataSourceFactory extends BaseFactory {
|
||||
|
||||
/**
|
||||
* The default connection timeout, in milliseconds.
|
||||
|
|
@ -67,7 +68,7 @@ public final class CronetDataSourceFactory implements Factory {
|
|||
}
|
||||
|
||||
@Override
|
||||
public CronetDataSource createDataSource() {
|
||||
protected CronetDataSource createDataSourceInternal() {
|
||||
return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener,
|
||||
connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ git clone git://source.ffmpeg.org/ffmpeg ffmpeg && cd ffmpeg && \
|
|||
--enable-decoder=vorbis \
|
||||
--enable-decoder=opus \
|
||||
--enable-decoder=flac \
|
||||
--enable-decoder=alac \
|
||||
&& \
|
||||
make -j4 && \
|
||||
make install-libs
|
||||
|
|
|
|||
|
|
@ -20,17 +20,7 @@ android {
|
|||
defaultConfig {
|
||||
minSdkVersion 9
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
|
||||
sourceSets.main {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.ffmpeg;
|
|||
|
||||
import android.os.Handler;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
|
|
@ -60,7 +61,7 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int supportsFormat(Format format) {
|
||||
protected int supportsFormatInternal(Format format) {
|
||||
if (!FfmpegLibrary.isAvailable()) {
|
||||
return FORMAT_UNSUPPORTED_TYPE;
|
||||
}
|
||||
|
|
@ -69,6 +70,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||
: MimeTypes.isAudio(mimeType) ? FORMAT_UNSUPPORTED_SUBTYPE : FORMAT_UNSUPPORTED_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int supportsMixedMimeTypeAdaptation() throws ExoPlaybackException {
|
||||
return ADAPTIVE_NOT_SEAMLESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FfmpegDecoder createDecoder(Format format, ExoMediaCrypto mediaCrypto)
|
||||
throws FfmpegDecoderException {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
|||
import com.google.android.exoplayer2.decoder.SimpleDecoder;
|
||||
import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
|
|
@ -88,6 +89,13 @@ import java.util.List;
|
|||
if (!hasOutputFormat) {
|
||||
channelCount = ffmpegGetChannelCount(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;
|
||||
}
|
||||
outputBuffer.data.position(0);
|
||||
|
|
@ -123,6 +131,7 @@ import java.util.List;
|
|||
private static byte[] getExtraData(String mimeType, List<byte[]> initializationData) {
|
||||
switch (mimeType) {
|
||||
case MimeTypes.AUDIO_AAC:
|
||||
case MimeTypes.AUDIO_ALAC:
|
||||
case MimeTypes.AUDIO_OPUS:
|
||||
return initializationData.get(0);
|
||||
case MimeTypes.AUDIO_VORBIS:
|
||||
|
|
|
|||
|
|
@ -92,6 +92,8 @@ public final class FfmpegLibrary {
|
|||
return "amrwb";
|
||||
case MimeTypes.AUDIO_FLAC:
|
||||
return "flac";
|
||||
case MimeTypes.AUDIO_ALAC:
|
||||
return "alac";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,17 +20,7 @@ android {
|
|||
defaultConfig {
|
||||
minSdkVersion 9
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
|
||||
sourceSets.main {
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int supportsFormat(Format format) {
|
||||
protected int supportsFormatInternal(Format format) {
|
||||
return FlacLibrary.isAvailable() && MimeTypes.AUDIO_FLAC.equalsIgnoreCase(format.sampleMimeType)
|
||||
? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,17 +22,6 @@ android {
|
|||
minSdkVersion 9
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||
private Request makeRequest(DataSpec dataSpec) {
|
||||
long position = dataSpec.position;
|
||||
long length = dataSpec.length;
|
||||
boolean allowGzip = (dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) != 0;
|
||||
boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
|
||||
|
||||
HttpUrl url = HttpUrl.parse(dataSpec.uri.toString());
|
||||
Request.Builder builder = new Request.Builder().url(url);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer2.ext.okhttp;
|
||||
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import okhttp3.CacheControl;
|
||||
|
|
@ -24,7 +25,7 @@ import okhttp3.Call;
|
|||
/**
|
||||
* A {@link Factory} that produces {@link OkHttpDataSource}.
|
||||
*/
|
||||
public final class OkHttpDataSourceFactory implements Factory {
|
||||
public final class OkHttpDataSourceFactory extends BaseFactory {
|
||||
|
||||
private final Call.Factory callFactory;
|
||||
private final String userAgent;
|
||||
|
|
@ -58,7 +59,7 @@ public final class OkHttpDataSourceFactory implements Factory {
|
|||
}
|
||||
|
||||
@Override
|
||||
public OkHttpDataSource createDataSource() {
|
||||
protected OkHttpDataSource createDataSourceInternal() {
|
||||
return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,17 +20,7 @@ android {
|
|||
defaultConfig {
|
||||
minSdkVersion 9
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
|
||||
sourceSets.main {
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int supportsFormat(Format format) {
|
||||
protected int supportsFormatInternal(Format format) {
|
||||
return OpusLibrary.isAvailable() && MimeTypes.AUDIO_OPUS.equalsIgnoreCase(format.sampleMimeType)
|
||||
? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,17 +20,7 @@ android {
|
|||
defaultConfig {
|
||||
minSdkVersion 9
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
|
||||
sourceSets.main {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import com.android.builder.core.BuilderConstants
|
||||
|
||||
// Copyright (C) 2016 The Android Open Source Project
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
|
@ -13,6 +11,8 @@ import com.android.builder.core.BuilderConstants
|
|||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import com.android.builder.core.BuilderConstants
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'bintray-release'
|
||||
|
||||
|
|
@ -28,13 +28,10 @@ android {
|
|||
// greater.
|
||||
minSdkVersion 9
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
}
|
||||
// Re-enable test coverage when the following issue is fixed:
|
||||
// https://code.google.com/p/android/issues/detail?id=226070
|
||||
// debug {
|
||||
|
|
@ -42,10 +39,6 @@ android {
|
|||
// }
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
androidTest {
|
||||
java.srcDirs += "../testutils/src/main/java/"
|
||||
|
|
|
|||
7
library/proguard-rules.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Accessed via reflection in SubtitleDecoderFactory.DEFAULT
|
||||
-keepclassmembers class com.google.android.exoplayer2.text.cea.Cea608Decoder {
|
||||
public <init>(java.lang.String, int);
|
||||
}
|
||||
-keepclassmembers class com.google.android.exoplayer2.text.cea.Cea708Decoder {
|
||||
public <init>(int);
|
||||
}
|
||||
|
|
@ -21,7 +21,6 @@ import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
|||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.SampleStream;
|
||||
import com.google.android.exoplayer2.source.SinglePeriodTimeline;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
|
|
@ -29,8 +28,10 @@ import com.google.android.exoplayer2.trackselection.TrackSelection;
|
|||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MediaClock;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
|
@ -48,12 +49,112 @@ public final class ExoPlayerTest extends TestCase {
|
|||
*/
|
||||
private static final int TIMEOUT_MS = 10000;
|
||||
|
||||
public void testPlayToEnd() throws Exception {
|
||||
private static final Format TEST_VIDEO_FORMAT = Format.createVideoSampleFormat(null,
|
||||
MimeTypes.VIDEO_H264, null, Format.NO_VALUE, Format.NO_VALUE, 1280, 720, Format.NO_VALUE,
|
||||
null, null);
|
||||
private static final Format TEST_AUDIO_FORMAT = Format.createAudioSampleFormat(null,
|
||||
MimeTypes.AUDIO_AAC, null, Format.NO_VALUE, Format.NO_VALUE, 2, 44100, null, null, 0, null);
|
||||
|
||||
/**
|
||||
* Tests playback of a source that exposes an empty timeline. Playback is expected to end without
|
||||
* error.
|
||||
*/
|
||||
public void testPlayEmptyTimeline() throws Exception {
|
||||
PlayerWrapper playerWrapper = new PlayerWrapper();
|
||||
Format format = Format.createVideoSampleFormat(null, MimeTypes.VIDEO_H264, null,
|
||||
Format.NO_VALUE, Format.NO_VALUE, 1280, 720, Format.NO_VALUE, null, null);
|
||||
playerWrapper.setup(new SinglePeriodTimeline(0, false), new Object(), format);
|
||||
playerWrapper.blockUntilEndedOrError(TIMEOUT_MS);
|
||||
Timeline timeline = Timeline.EMPTY;
|
||||
MediaSource mediaSource = new FakeMediaSource(timeline, null);
|
||||
FakeRenderer renderer = new FakeRenderer(null);
|
||||
playerWrapper.setup(mediaSource, renderer);
|
||||
playerWrapper.blockUntilEnded(TIMEOUT_MS);
|
||||
assertEquals(0, playerWrapper.positionDiscontinuityCount);
|
||||
assertEquals(0, renderer.formatReadCount);
|
||||
assertEquals(0, renderer.bufferReadCount);
|
||||
assertFalse(renderer.isEnded);
|
||||
assertEquals(timeline, playerWrapper.timeline);
|
||||
assertNull(playerWrapper.manifest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests playback of a source that exposes a single period.
|
||||
*/
|
||||
public void testPlaySinglePeriodTimeline() throws Exception {
|
||||
PlayerWrapper playerWrapper = new PlayerWrapper();
|
||||
Timeline timeline = new FakeTimeline(new TimelineWindowDefinition(false, false, 0));
|
||||
Object manifest = new Object();
|
||||
MediaSource mediaSource = new FakeMediaSource(timeline, manifest, TEST_VIDEO_FORMAT);
|
||||
FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
|
||||
playerWrapper.setup(mediaSource, renderer);
|
||||
playerWrapper.blockUntilEnded(TIMEOUT_MS);
|
||||
assertEquals(0, playerWrapper.positionDiscontinuityCount);
|
||||
assertEquals(1, renderer.formatReadCount);
|
||||
assertEquals(1, renderer.bufferReadCount);
|
||||
assertTrue(renderer.isEnded);
|
||||
assertEquals(timeline, playerWrapper.timeline);
|
||||
assertEquals(manifest, playerWrapper.manifest);
|
||||
assertEquals(new TrackGroupArray(new TrackGroup(TEST_VIDEO_FORMAT)), playerWrapper.trackGroups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests playback of a source that exposes three periods.
|
||||
*/
|
||||
public void testPlayMultiPeriodTimeline() throws Exception {
|
||||
PlayerWrapper playerWrapper = new PlayerWrapper();
|
||||
Timeline timeline = new FakeTimeline(
|
||||
new TimelineWindowDefinition(false, false, 0),
|
||||
new TimelineWindowDefinition(false, false, 0),
|
||||
new TimelineWindowDefinition(false, false, 0));
|
||||
MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT);
|
||||
FakeRenderer renderer = new FakeRenderer(TEST_VIDEO_FORMAT);
|
||||
playerWrapper.setup(mediaSource, renderer);
|
||||
playerWrapper.blockUntilEnded(TIMEOUT_MS);
|
||||
assertEquals(2, playerWrapper.positionDiscontinuityCount);
|
||||
assertEquals(3, renderer.formatReadCount);
|
||||
assertEquals(1, renderer.bufferReadCount);
|
||||
assertTrue(renderer.isEnded);
|
||||
assertEquals(timeline, playerWrapper.timeline);
|
||||
assertNull(playerWrapper.manifest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the player does not unnecessarily reset renderers when playing a multi-period
|
||||
* source.
|
||||
*/
|
||||
public void testReadAheadToEndDoesNotResetRenderer() throws Exception {
|
||||
final PlayerWrapper playerWrapper = new PlayerWrapper();
|
||||
Timeline timeline = new FakeTimeline(
|
||||
new TimelineWindowDefinition(false, false, 10),
|
||||
new TimelineWindowDefinition(false, false, 10),
|
||||
new TimelineWindowDefinition(false, false, 10));
|
||||
MediaSource mediaSource = new FakeMediaSource(timeline, null, TEST_VIDEO_FORMAT,
|
||||
TEST_AUDIO_FORMAT);
|
||||
|
||||
FakeRenderer videoRenderer = new FakeRenderer(TEST_VIDEO_FORMAT);
|
||||
FakeMediaClockRenderer audioRenderer = new FakeMediaClockRenderer(TEST_AUDIO_FORMAT) {
|
||||
|
||||
@Override
|
||||
public long getPositionUs() {
|
||||
// Simulate the playback position lagging behind the reading position: the renderer media
|
||||
// clock position will be the start of the timeline until the stream is set to be final, at
|
||||
// which point it jumps to the end of the timeline allowing the playing period to advance.
|
||||
// TODO: Avoid hard-coding ExoPlayerImplInternal.RENDERER_TIMESTAMP_OFFSET_US.
|
||||
return isCurrentStreamFinal() ? 60000030 : 60000000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnded() {
|
||||
// Allow playback to end once the final period is playing.
|
||||
return playerWrapper.positionDiscontinuityCount == 2;
|
||||
}
|
||||
|
||||
};
|
||||
playerWrapper.setup(mediaSource, videoRenderer, audioRenderer);
|
||||
playerWrapper.blockUntilEnded(TIMEOUT_MS);
|
||||
assertEquals(2, playerWrapper.positionDiscontinuityCount);
|
||||
assertEquals(1, audioRenderer.positionResetCount);
|
||||
assertTrue(videoRenderer.isEnded);
|
||||
assertTrue(audioRenderer.isEnded);
|
||||
assertEquals(timeline, playerWrapper.timeline);
|
||||
assertNull(playerWrapper.manifest);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -65,12 +166,14 @@ public final class ExoPlayerTest extends TestCase {
|
|||
private final HandlerThread playerThread;
|
||||
private final Handler handler;
|
||||
|
||||
private Timeline expectedTimeline;
|
||||
private Object expectedManifest;
|
||||
private Format expectedFormat;
|
||||
private ExoPlayer player;
|
||||
private Timeline timeline;
|
||||
private Object manifest;
|
||||
private TrackGroupArray trackGroups;
|
||||
private Exception exception;
|
||||
private boolean seenPositionDiscontinuity;
|
||||
|
||||
// Written only on the main thread.
|
||||
private volatile int positionDiscontinuityCount;
|
||||
|
||||
public PlayerWrapper() {
|
||||
endedCountDownLatch = new CountDownLatch(1);
|
||||
|
|
@ -81,34 +184,28 @@ public final class ExoPlayerTest extends TestCase {
|
|||
|
||||
// Called on the test thread.
|
||||
|
||||
public void blockUntilEndedOrError(long timeoutMs) throws Exception {
|
||||
public void blockUntilEnded(long timeoutMs) throws Exception {
|
||||
if (!endedCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
|
||||
exception = new TimeoutException("Test playback timed out.");
|
||||
}
|
||||
release();
|
||||
|
||||
// Throw any pending exception (from playback, timing out or releasing).
|
||||
if (exception != null) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
public void setup(final Timeline timeline, final Object manifest, final Format format) {
|
||||
expectedTimeline = timeline;
|
||||
expectedManifest = manifest;
|
||||
expectedFormat = format;
|
||||
public void setup(final MediaSource mediaSource, final Renderer... renderers) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Renderer fakeRenderer = new FakeVideoRenderer(expectedFormat);
|
||||
player = ExoPlayerFactory.newInstance(new Renderer[] {fakeRenderer},
|
||||
new DefaultTrackSelector());
|
||||
player = ExoPlayerFactory.newInstance(renderers, new DefaultTrackSelector());
|
||||
player.addListener(PlayerWrapper.this);
|
||||
player.setPlayWhenReady(true);
|
||||
player.prepare(new FakeMediaSource(timeline, manifest, format));
|
||||
player.prepare(mediaSource);
|
||||
} catch (Exception e) {
|
||||
handlePlayerException(e);
|
||||
handleError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -123,7 +220,7 @@ public final class ExoPlayerTest extends TestCase {
|
|||
player.release();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
handlePlayerException(e);
|
||||
handleError(e);
|
||||
} finally {
|
||||
playerThread.quit();
|
||||
}
|
||||
|
|
@ -132,7 +229,7 @@ public final class ExoPlayerTest extends TestCase {
|
|||
playerThread.join();
|
||||
}
|
||||
|
||||
private void handlePlayerException(Exception exception) {
|
||||
private void handleError(Exception exception) {
|
||||
if (this.exception == null) {
|
||||
this.exception = exception;
|
||||
}
|
||||
|
|
@ -155,32 +252,83 @@ public final class ExoPlayerTest extends TestCase {
|
|||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||
assertEquals(expectedTimeline, timeline);
|
||||
assertEquals(expectedManifest, manifest);
|
||||
this.timeline = timeline;
|
||||
this.manifest = manifest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(TrackGroupArray trackGroups,
|
||||
TrackSelectionArray trackSelections) {
|
||||
assertEquals(new TrackGroupArray(new TrackGroup(expectedFormat)), trackGroups);
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
this.trackGroups = trackGroups;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerError(ExoPlaybackException exception) {
|
||||
this.exception = exception;
|
||||
endedCountDownLatch.countDown();
|
||||
handleError(exception);
|
||||
}
|
||||
|
||||
@SuppressWarnings("NonAtomicVolatileUpdate")
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
positionDiscontinuityCount++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class TimelineWindowDefinition {
|
||||
|
||||
public final boolean isSeekable;
|
||||
public final boolean isDynamic;
|
||||
public final long durationUs;
|
||||
|
||||
public TimelineWindowDefinition(boolean isSeekable, boolean isDynamic, long durationUs) {
|
||||
this.isSeekable = isSeekable;
|
||||
this.isDynamic = isDynamic;
|
||||
this.durationUs = durationUs;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class FakeTimeline extends Timeline {
|
||||
|
||||
private final TimelineWindowDefinition[] windowDefinitions;
|
||||
|
||||
public FakeTimeline(TimelineWindowDefinition... windowDefinitions) {
|
||||
this.windowDefinitions = windowDefinitions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
assertFalse(seenPositionDiscontinuity);
|
||||
assertEquals(0, player.getCurrentWindowIndex());
|
||||
assertEquals(0, player.getCurrentPeriodIndex());
|
||||
assertEquals(0, player.getCurrentPosition());
|
||||
assertEquals(0, player.getBufferedPosition());
|
||||
assertEquals(expectedTimeline, player.getCurrentTimeline());
|
||||
assertEquals(expectedManifest, player.getCurrentManifest());
|
||||
seenPositionDiscontinuity = true;
|
||||
public int getWindowCount() {
|
||||
return windowDefinitions.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Window getWindow(int windowIndex, Window window, boolean setIds,
|
||||
long defaultPositionProjectionUs) {
|
||||
TimelineWindowDefinition windowDefinition = windowDefinitions[windowIndex];
|
||||
Object id = setIds ? windowIndex : null;
|
||||
return window.set(id, C.TIME_UNSET, C.TIME_UNSET, windowDefinition.isSeekable,
|
||||
windowDefinition.isDynamic, 0, windowDefinition.durationUs, windowIndex, windowIndex, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPeriodCount() {
|
||||
return windowDefinitions.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
|
||||
TimelineWindowDefinition windowDefinition = windowDefinitions[periodIndex];
|
||||
Object id = setIds ? periodIndex : null;
|
||||
return period.set(id, id, periodIndex, windowDefinition.durationUs, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIndexOfPeriod(Object uid) {
|
||||
if (!(uid instanceof Integer)) {
|
||||
return C.INDEX_UNSET;
|
||||
}
|
||||
int index = (Integer) uid;
|
||||
return index >= 0 && index < windowDefinitions.length ? index : C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -193,18 +341,21 @@ public final class ExoPlayerTest extends TestCase {
|
|||
|
||||
private final Timeline timeline;
|
||||
private final Object manifest;
|
||||
private final Format format;
|
||||
private final TrackGroupArray trackGroupArray;
|
||||
private final ArrayList<FakeMediaPeriod> activeMediaPeriods;
|
||||
|
||||
private FakeMediaPeriod mediaPeriod;
|
||||
private boolean preparedSource;
|
||||
private boolean releasedPeriod;
|
||||
private boolean releasedSource;
|
||||
|
||||
public FakeMediaSource(Timeline timeline, Object manifest, Format format) {
|
||||
Assertions.checkArgument(timeline.getPeriodCount() == 1);
|
||||
public FakeMediaSource(Timeline timeline, Object manifest, Format... formats) {
|
||||
this.timeline = timeline;
|
||||
this.manifest = manifest;
|
||||
this.format = format;
|
||||
TrackGroup[] trackGroups = new TrackGroup[formats.length];
|
||||
for (int i = 0; i < formats.length; i++) {
|
||||
trackGroups[i] = new TrackGroup(formats[i]);
|
||||
}
|
||||
trackGroupArray = new TrackGroupArray(trackGroups);
|
||||
activeMediaPeriods = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -221,33 +372,29 @@ public final class ExoPlayerTest extends TestCase {
|
|||
|
||||
@Override
|
||||
public MediaPeriod createPeriod(int index, Allocator allocator, long positionUs) {
|
||||
Assertions.checkIndex(index, 0, timeline.getPeriodCount());
|
||||
assertTrue(preparedSource);
|
||||
assertNull(mediaPeriod);
|
||||
assertFalse(releasedPeriod);
|
||||
assertFalse(releasedSource);
|
||||
assertEquals(0, index);
|
||||
assertEquals(0, positionUs);
|
||||
mediaPeriod = new FakeMediaPeriod(format);
|
||||
FakeMediaPeriod mediaPeriod = new FakeMediaPeriod(trackGroupArray);
|
||||
activeMediaPeriods.add(mediaPeriod);
|
||||
return mediaPeriod;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releasePeriod(MediaPeriod mediaPeriod) {
|
||||
assertTrue(preparedSource);
|
||||
assertNotNull(this.mediaPeriod);
|
||||
assertFalse(releasedPeriod);
|
||||
assertFalse(releasedSource);
|
||||
assertEquals(this.mediaPeriod, mediaPeriod);
|
||||
this.mediaPeriod.release();
|
||||
releasedPeriod = true;
|
||||
FakeMediaPeriod fakeMediaPeriod = (FakeMediaPeriod) mediaPeriod;
|
||||
assertTrue(activeMediaPeriods.remove(fakeMediaPeriod));
|
||||
fakeMediaPeriod.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releaseSource() {
|
||||
assertTrue(preparedSource);
|
||||
assertNotNull(this.mediaPeriod);
|
||||
assertTrue(releasedPeriod);
|
||||
assertFalse(releasedSource);
|
||||
assertTrue(activeMediaPeriods.isEmpty());
|
||||
releasedSource = true;
|
||||
}
|
||||
|
||||
|
|
@ -259,12 +406,12 @@ public final class ExoPlayerTest extends TestCase {
|
|||
*/
|
||||
private static final class FakeMediaPeriod implements MediaPeriod {
|
||||
|
||||
private final TrackGroup trackGroup;
|
||||
private final TrackGroupArray trackGroupArray;
|
||||
|
||||
private boolean preparedPeriod;
|
||||
|
||||
public FakeMediaPeriod(Format format) {
|
||||
trackGroup = new TrackGroup(format);
|
||||
public FakeMediaPeriod(TrackGroupArray trackGroupArray) {
|
||||
this.trackGroupArray = trackGroupArray;
|
||||
}
|
||||
|
||||
public void release() {
|
||||
|
|
@ -286,26 +433,29 @@ public final class ExoPlayerTest extends TestCase {
|
|||
@Override
|
||||
public TrackGroupArray getTrackGroups() {
|
||||
assertTrue(preparedPeriod);
|
||||
return new TrackGroupArray(trackGroup);
|
||||
return trackGroupArray;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
|
||||
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
|
||||
assertTrue(preparedPeriod);
|
||||
assertEquals(1, selections.length);
|
||||
assertEquals(1, mayRetainStreamFlags.length);
|
||||
assertEquals(1, streams.length);
|
||||
assertEquals(1, streamResetFlags.length);
|
||||
assertEquals(0, positionUs);
|
||||
if (streams[0] != null && (selections[0] == null || !mayRetainStreamFlags[0])) {
|
||||
streams[0] = null;
|
||||
int rendererCount = selections.length;
|
||||
for (int i = 0; i < rendererCount; i++) {
|
||||
if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
|
||||
streams[i] = null;
|
||||
}
|
||||
}
|
||||
if (streams[0] == null && selections[0] != null) {
|
||||
FakeSampleStream stream = new FakeSampleStream(trackGroup.getFormat(0));
|
||||
assertEquals(trackGroup, selections[0].getTrackGroup());
|
||||
streams[0] = stream;
|
||||
streamResetFlags[0] = true;
|
||||
for (int i = 0; i < rendererCount; i++) {
|
||||
if (streams[i] == null && selections[i] != null) {
|
||||
TrackSelection selection = selections[i];
|
||||
assertEquals(1, selection.length());
|
||||
assertEquals(0, selection.getIndexInTrackGroup(0));
|
||||
TrackGroup trackGroup = selection.getTrackGroup();
|
||||
assertTrue(trackGroupArray.indexOf(trackGroup) != C.INDEX_UNSET);
|
||||
streams[i] = new FakeSampleStream(trackGroup.getFormat(0));
|
||||
streamResetFlags[i] = true;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -332,7 +482,7 @@ public final class ExoPlayerTest extends TestCase {
|
|||
@Override
|
||||
public long getNextLoadPositionUs() {
|
||||
assertTrue(preparedPeriod);
|
||||
return 0;
|
||||
return C.TIME_END_OF_SOURCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -352,7 +502,6 @@ public final class ExoPlayerTest extends TestCase {
|
|||
private final Format format;
|
||||
|
||||
private boolean readFormat;
|
||||
private boolean readEndOfStream;
|
||||
|
||||
public FakeSampleStream(Format format) {
|
||||
this.format = format;
|
||||
|
|
@ -365,15 +514,14 @@ public final class ExoPlayerTest extends TestCase {
|
|||
|
||||
@Override
|
||||
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) {
|
||||
Assertions.checkState(!readEndOfStream);
|
||||
if (readFormat) {
|
||||
if (buffer == null || !readFormat) {
|
||||
formatHolder.format = format;
|
||||
readFormat = true;
|
||||
return C.RESULT_FORMAT_READ;
|
||||
} else {
|
||||
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
|
||||
readEndOfStream = true;
|
||||
return C.RESULT_BUFFER_READ;
|
||||
}
|
||||
formatHolder.format = format;
|
||||
readFormat = true;
|
||||
return C.RESULT_FORMAT_READ;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -389,21 +537,30 @@ public final class ExoPlayerTest extends TestCase {
|
|||
}
|
||||
|
||||
/**
|
||||
* Fake {@link Renderer} that supports any video format. The renderer verifies that it reads a
|
||||
* given {@link Format} then a buffer with the end of stream flag set.
|
||||
* Fake {@link Renderer} that supports any format with the matching MIME type. The renderer
|
||||
* verifies that it reads a given {@link Format}.
|
||||
*/
|
||||
private static final class FakeVideoRenderer extends BaseRenderer {
|
||||
private static class FakeRenderer extends BaseRenderer {
|
||||
|
||||
private final Format expectedFormat;
|
||||
|
||||
private boolean isEnded;
|
||||
public int positionResetCount;
|
||||
public int formatReadCount;
|
||||
public int bufferReadCount;
|
||||
public boolean isEnded;
|
||||
|
||||
public FakeVideoRenderer(Format expectedFormat) {
|
||||
super(C.TRACK_TYPE_VIDEO);
|
||||
Assertions.checkArgument(MimeTypes.isVideo(expectedFormat.sampleMimeType));
|
||||
public FakeRenderer(Format expectedFormat) {
|
||||
super(expectedFormat == null ? C.TRACK_TYPE_UNKNOWN
|
||||
: MimeTypes.getTrackType(expectedFormat.sampleMimeType));
|
||||
this.expectedFormat = expectedFormat;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
|
||||
positionResetCount++;
|
||||
isEnded = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
|
||||
if (isEnded) {
|
||||
|
|
@ -412,20 +569,23 @@ public final class ExoPlayerTest extends TestCase {
|
|||
|
||||
// Verify the format matches the expected format.
|
||||
FormatHolder formatHolder = new FormatHolder();
|
||||
readSource(formatHolder, null);
|
||||
assertEquals(expectedFormat, formatHolder.format);
|
||||
|
||||
// Verify that we get an end-of-stream buffer.
|
||||
DecoderInputBuffer buffer =
|
||||
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
|
||||
readSource(null, buffer);
|
||||
assertTrue(buffer.isEndOfStream());
|
||||
isEnded = true;
|
||||
int result = readSource(formatHolder, buffer);
|
||||
if (result == C.RESULT_FORMAT_READ) {
|
||||
formatReadCount++;
|
||||
assertEquals(expectedFormat, formatHolder.format);
|
||||
} else if (result == C.RESULT_BUFFER_READ) {
|
||||
bufferReadCount++;
|
||||
if (buffer.isEndOfStream()) {
|
||||
isEnded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return isEnded;
|
||||
return isSourceReady();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -435,7 +595,21 @@ public final class ExoPlayerTest extends TestCase {
|
|||
|
||||
@Override
|
||||
public int supportsFormat(Format format) throws ExoPlaybackException {
|
||||
return MimeTypes.isVideo(format.sampleMimeType) ? FORMAT_HANDLED : FORMAT_UNSUPPORTED_TYPE;
|
||||
return getTrackType() == MimeTypes.getTrackType(format.sampleMimeType) ? FORMAT_HANDLED
|
||||
: FORMAT_UNSUPPORTED_TYPE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private abstract static class FakeMediaClockRenderer extends FakeRenderer implements MediaClock {
|
||||
|
||||
public FakeMediaClockRenderer(Format expectedFormat) {
|
||||
super(expectedFormat);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaClock getMediaClock() {
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ public final class FormatTest extends TestCase {
|
|||
DrmInitData drmInitData = new DrmInitData(DRM_DATA_1, DRM_DATA_2);
|
||||
byte[] projectionData = new byte[] {1, 2, 3};
|
||||
Metadata metadata = new Metadata(
|
||||
new TextInformationFrame("id1", "description1"),
|
||||
new TextInformationFrame("id2", "description2"));
|
||||
new TextInformationFrame("id1", "description1", "value1"),
|
||||
new TextInformationFrame("id2", "description2", "value2"));
|
||||
|
||||
Format formatToParcel = new Format("id", MimeTypes.VIDEO_MP4, MimeTypes.VIDEO_H264, null,
|
||||
1024, 2048, 1920, 1080, 24, 90, 2, projectionData, C.STEREO_MODE_TOP_BOTTOM, 6, 44100,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.drm;
|
||||
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import android.test.MoreAsserts;
|
||||
import android.util.Pair;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
|
||||
import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;
|
||||
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
|
||||
import com.google.android.exoplayer2.source.dash.manifest.Period;
|
||||
import com.google.android.exoplayer2.source.dash.manifest.Representation;
|
||||
import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import org.mockito.Mock;
|
||||
|
||||
/**
|
||||
* Tests {@link OfflineLicenseHelper}.
|
||||
*/
|
||||
public class OfflineLicenseHelperTest extends InstrumentationTestCase {
|
||||
|
||||
private OfflineLicenseHelper<?> offlineLicenseHelper;
|
||||
@Mock private HttpDataSource httpDataSource;
|
||||
@Mock private MediaDrmCallback mediaDrmCallback;
|
||||
@Mock private ExoMediaDrm<ExoMediaCrypto> mediaDrm;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
TestUtil.setUpMockito(this);
|
||||
|
||||
when(mediaDrm.openSession()).thenReturn(new byte[] {1, 2, 3});
|
||||
|
||||
offlineLicenseHelper = new OfflineLicenseHelper<>(mediaDrm, mediaDrmCallback, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
offlineLicenseHelper.releaseResources();
|
||||
}
|
||||
|
||||
public void testDownloadRenewReleaseKey() throws Exception {
|
||||
DashManifest manifest = newDashManifestWithAllElements();
|
||||
setStubLicenseAndPlaybackDurationValues(1000, 200);
|
||||
|
||||
byte[] keySetId = {2, 5, 8};
|
||||
setStubKeySetId(keySetId);
|
||||
|
||||
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
|
||||
|
||||
assertOfflineLicenseKeySetIdEqual(keySetId, offlineLicenseKeySetId);
|
||||
|
||||
byte[] keySetId2 = {6, 7, 0, 1, 4};
|
||||
setStubKeySetId(keySetId2);
|
||||
|
||||
byte[] offlineLicenseKeySetId2 = offlineLicenseHelper.renew(offlineLicenseKeySetId);
|
||||
|
||||
assertOfflineLicenseKeySetIdEqual(keySetId2, offlineLicenseKeySetId2);
|
||||
|
||||
offlineLicenseHelper.release(offlineLicenseKeySetId2);
|
||||
}
|
||||
|
||||
public void testDownloadFailsIfThereIsNoInitData() throws Exception {
|
||||
setDefaultStubValues();
|
||||
DashManifest manifest =
|
||||
newDashManifest(newPeriods(newAdaptationSets(newRepresentations(null /*no init data*/))));
|
||||
|
||||
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
|
||||
|
||||
assertNull(offlineLicenseKeySetId);
|
||||
}
|
||||
|
||||
public void testDownloadFailsIfThereIsNoRepresentation() throws Exception {
|
||||
setDefaultStubValues();
|
||||
DashManifest manifest = newDashManifest(newPeriods(newAdaptationSets(/*no representation*/)));
|
||||
|
||||
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
|
||||
|
||||
assertNull(offlineLicenseKeySetId);
|
||||
}
|
||||
|
||||
public void testDownloadFailsIfThereIsNoAdaptationSet() throws Exception {
|
||||
setDefaultStubValues();
|
||||
DashManifest manifest = newDashManifest(newPeriods(/*no adaptation set*/));
|
||||
|
||||
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
|
||||
|
||||
assertNull(offlineLicenseKeySetId);
|
||||
}
|
||||
|
||||
public void testDownloadFailsIfThereIsNoPeriod() throws Exception {
|
||||
setDefaultStubValues();
|
||||
DashManifest manifest = newDashManifest(/*no periods*/);
|
||||
|
||||
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
|
||||
|
||||
assertNull(offlineLicenseKeySetId);
|
||||
}
|
||||
|
||||
public void testDownloadFailsIfNoKeySetIdIsReturned() throws Exception {
|
||||
setStubLicenseAndPlaybackDurationValues(1000, 200);
|
||||
DashManifest manifest = newDashManifestWithAllElements();
|
||||
|
||||
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
|
||||
|
||||
assertNull(offlineLicenseKeySetId);
|
||||
}
|
||||
|
||||
public void testDownloadDoesNotFailIfDurationNotAvailable() throws Exception {
|
||||
setDefaultStubKeySetId();
|
||||
DashManifest manifest = newDashManifestWithAllElements();
|
||||
|
||||
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
|
||||
|
||||
assertNotNull(offlineLicenseKeySetId);
|
||||
}
|
||||
|
||||
public void testGetLicenseDurationRemainingSec() throws Exception {
|
||||
long licenseDuration = 1000;
|
||||
int playbackDuration = 200;
|
||||
setStubLicenseAndPlaybackDurationValues(licenseDuration, playbackDuration);
|
||||
setDefaultStubKeySetId();
|
||||
DashManifest manifest = newDashManifestWithAllElements();
|
||||
|
||||
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
|
||||
|
||||
Pair<Long, Long> licenseDurationRemainingSec = offlineLicenseHelper
|
||||
.getLicenseDurationRemainingSec(offlineLicenseKeySetId);
|
||||
|
||||
assertEquals(licenseDuration, (long) licenseDurationRemainingSec.first);
|
||||
assertEquals(playbackDuration, (long) licenseDurationRemainingSec.second);
|
||||
}
|
||||
|
||||
public void testGetLicenseDurationRemainingSecExpiredLicense() throws Exception {
|
||||
long licenseDuration = 0;
|
||||
int playbackDuration = 0;
|
||||
setStubLicenseAndPlaybackDurationValues(licenseDuration, playbackDuration);
|
||||
setDefaultStubKeySetId();
|
||||
DashManifest manifest = newDashManifestWithAllElements();
|
||||
|
||||
byte[] offlineLicenseKeySetId = offlineLicenseHelper.download(httpDataSource, manifest);
|
||||
|
||||
Pair<Long, Long> licenseDurationRemainingSec = offlineLicenseHelper
|
||||
.getLicenseDurationRemainingSec(offlineLicenseKeySetId);
|
||||
|
||||
assertEquals(licenseDuration, (long) licenseDurationRemainingSec.first);
|
||||
assertEquals(playbackDuration, (long) licenseDurationRemainingSec.second);
|
||||
}
|
||||
|
||||
private void setDefaultStubValues()
|
||||
throws android.media.NotProvisionedException, android.media.DeniedByServerException {
|
||||
setDefaultStubKeySetId();
|
||||
setStubLicenseAndPlaybackDurationValues(1000, 200);
|
||||
}
|
||||
|
||||
private void setDefaultStubKeySetId()
|
||||
throws android.media.NotProvisionedException, android.media.DeniedByServerException {
|
||||
setStubKeySetId(new byte[] {2, 5, 8});
|
||||
}
|
||||
|
||||
private void setStubKeySetId(byte[] keySetId)
|
||||
throws android.media.NotProvisionedException, android.media.DeniedByServerException {
|
||||
when(mediaDrm.provideKeyResponse(any(byte[].class), any(byte[].class))).thenReturn(keySetId);
|
||||
}
|
||||
|
||||
private static void assertOfflineLicenseKeySetIdEqual(
|
||||
byte[] expectedKeySetId, byte[] actualKeySetId) throws Exception {
|
||||
assertNotNull(actualKeySetId);
|
||||
MoreAsserts.assertEquals(expectedKeySetId, actualKeySetId);
|
||||
}
|
||||
|
||||
private void setStubLicenseAndPlaybackDurationValues(long licenseDuration,
|
||||
long playbackDuration) {
|
||||
HashMap<String, String> keyStatus = new HashMap<>();
|
||||
keyStatus.put(WidevineUtil.PROPERTY_LICENSE_DURATION_REMAINING,
|
||||
String.valueOf(licenseDuration));
|
||||
keyStatus.put(WidevineUtil.PROPERTY_PLAYBACK_DURATION_REMAINING,
|
||||
String.valueOf(playbackDuration));
|
||||
when(mediaDrm.queryKeyStatus(any(byte[].class))).thenReturn(keyStatus);
|
||||
}
|
||||
|
||||
private static DashManifest newDashManifestWithAllElements() {
|
||||
return newDashManifest(newPeriods(newAdaptationSets(newRepresentations(newDrmInitData()))));
|
||||
}
|
||||
|
||||
private static DashManifest newDashManifest(Period... periods) {
|
||||
return new DashManifest(0, 0, 0, false, 0, 0, 0, null, null, Arrays.asList(periods));
|
||||
}
|
||||
|
||||
private static Period newPeriods(AdaptationSet... adaptationSets) {
|
||||
return new Period("", 0, Arrays.asList(adaptationSets));
|
||||
}
|
||||
|
||||
private static AdaptationSet newAdaptationSets(Representation... representations) {
|
||||
return new AdaptationSet(0, C.TRACK_TYPE_VIDEO, Arrays.asList(representations), 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 @@
|
|||
package com.google.android.exoplayer2.extractor.mp4;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
|
||||
|
|
@ -24,13 +25,21 @@ import com.google.android.exoplayer2.testutil.TestUtil;
|
|||
*/
|
||||
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 {
|
||||
TestUtil.assertOutput(new TestUtil.ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new FragmentedMp4Extractor();
|
||||
}
|
||||
}, "mp4/sample_fragmented.mp4", getInstrumentation());
|
||||
TestUtil.assertOutput(EXTRACTOR_FACTORY, "mp4/sample_fragmented.mp4", getInstrumentation());
|
||||
}
|
||||
|
||||
public void testAtomWithZeroSize() throws Exception {
|
||||
TestUtil.assertThrows(EXTRACTOR_FACTORY, "mp4/sample_fragmented_zero_size_atom.mp4",
|
||||
getInstrumentation(), ParserException.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@
|
|||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
|
||||
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import com.google.android.exoplayer2.Format;
|
|||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.EsInfo;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||
|
|
@ -30,6 +29,7 @@ import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
|
|||
import com.google.android.exoplayer2.testutil.FakeTrackOutput;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Random;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.metadata.emsg;
|
||||
|
||||
import android.test.MoreAsserts;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
|
||||
import java.nio.ByteBuffer;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* Test for {@link EventMessageDecoder}.
|
||||
*/
|
||||
public final class EventMessageDecoderTest extends TestCase {
|
||||
|
||||
public void testDecodeEventMessage() {
|
||||
byte[] rawEmsgBody = new byte[] {
|
||||
117, 114, 110, 58, 116, 101, 115, 116, 0, // scheme_id_uri = "urn:test"
|
||||
49, 50, 51, 0, // value = "123"
|
||||
0, 0, -69, -128, // timescale = 48000
|
||||
0, 0, 0, 0, // presentation_time_delta (ignored) = 0
|
||||
0, 2, 50, -128, // event_duration = 144000
|
||||
0, 15, 67, -45, // id = 1000403
|
||||
0, 1, 2, 3, 4}; // message_data = {0, 1, 2, 3, 4}
|
||||
EventMessageDecoder decoder = new EventMessageDecoder();
|
||||
MetadataInputBuffer buffer = new MetadataInputBuffer();
|
||||
buffer.data = ByteBuffer.allocate(rawEmsgBody.length).put(rawEmsgBody);
|
||||
Metadata metadata = decoder.decode(buffer);
|
||||
assertEquals(1, metadata.length());
|
||||
EventMessage eventMessage = (EventMessage) metadata.get(0);
|
||||
assertEquals("urn:test", eventMessage.schemeIdUri);
|
||||
assertEquals("123", eventMessage.value);
|
||||
assertEquals(3000, eventMessage.durationMs);
|
||||
assertEquals(1000403, eventMessage.id);
|
||||
MoreAsserts.assertEquals(new byte[] {0, 1, 2, 3, 4}, eventMessage.messageData);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.metadata.emsg;
|
||||
|
||||
import android.os.Parcel;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* Test for {@link EventMessage}.
|
||||
*/
|
||||
public final class EventMessageTest extends TestCase {
|
||||
|
||||
public void testEventMessageParcelable() {
|
||||
EventMessage eventMessage = new EventMessage("urn:test", "123", 3000, 1000403,
|
||||
new byte[] {0, 1, 2, 3, 4});
|
||||
// Write to parcel.
|
||||
Parcel parcel = Parcel.obtain();
|
||||
eventMessage.writeToParcel(parcel, 0);
|
||||
// Create from parcel.
|
||||
parcel.setDataPosition(0);
|
||||
EventMessage fromParcelEventMessage = EventMessage.CREATOR.createFromParcel(parcel);
|
||||
// Assert equals.
|
||||
assertEquals(eventMessage, fromParcelEventMessage);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.metadata.id3;
|
||||
|
||||
import android.os.Parcel;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* Test for {@link ChapterFrame}.
|
||||
*/
|
||||
public final class ChapterFrameTest extends TestCase {
|
||||
|
||||
public void testParcelable() {
|
||||
Id3Frame[] subFrames = new Id3Frame[] {
|
||||
new TextInformationFrame("TIT2", null, "title"),
|
||||
new UrlLinkFrame("WXXX", "description", "url")
|
||||
};
|
||||
ChapterFrame chapterFrameToParcel = new ChapterFrame("id", 0, 1, 2, 3, subFrames);
|
||||
|
||||
Parcel parcel = Parcel.obtain();
|
||||
chapterFrameToParcel.writeToParcel(parcel, 0);
|
||||
parcel.setDataPosition(0);
|
||||
|
||||
ChapterFrame chapterFrameFromParcel = ChapterFrame.CREATOR.createFromParcel(parcel);
|
||||
assertEquals(chapterFrameToParcel, chapterFrameFromParcel);
|
||||
|
||||
parcel.recycle();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.metadata.id3;
|
||||
|
||||
import android.os.Parcel;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* Test for {@link ChapterTocFrame}.
|
||||
*/
|
||||
public final class ChapterTocFrameTest extends TestCase {
|
||||
|
||||
public void testParcelable() {
|
||||
String[] children = new String[] {"child0", "child1"};
|
||||
Id3Frame[] subFrames = new Id3Frame[] {
|
||||
new TextInformationFrame("TIT2", null, "title"),
|
||||
new UrlLinkFrame("WXXX", "description", "url")
|
||||
};
|
||||
ChapterTocFrame chapterTocFrameToParcel = new ChapterTocFrame("id", false, true, children,
|
||||
subFrames);
|
||||
|
||||
Parcel parcel = Parcel.obtain();
|
||||
chapterTocFrameToParcel.writeToParcel(parcel, 0);
|
||||
parcel.setDataPosition(0);
|
||||
|
||||
ChapterTocFrame chapterTocFrameFromParcel = ChapterTocFrame.CREATOR.createFromParcel(parcel);
|
||||
assertEquals(chapterTocFrameToParcel, chapterTocFrameFromParcel);
|
||||
|
||||
parcel.recycle();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -21,9 +21,9 @@ import com.google.android.exoplayer2.metadata.MetadataDecoderException;
|
|||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* Test for {@link Id3Decoder}
|
||||
* Test for {@link Id3Decoder}.
|
||||
*/
|
||||
public class Id3DecoderTest extends TestCase {
|
||||
public final class Id3DecoderTest extends TestCase {
|
||||
|
||||
public void testDecodeTxxxFrame() throws MetadataDecoderException {
|
||||
byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 41, 84, 88, 88, 88, 0, 0, 0, 31, 0, 0,
|
||||
|
|
@ -32,9 +32,10 @@ public class Id3DecoderTest extends TestCase {
|
|||
Id3Decoder decoder = new Id3Decoder();
|
||||
Metadata metadata = decoder.decode(rawId3, rawId3.length);
|
||||
assertEquals(1, metadata.length());
|
||||
TxxxFrame txxxFrame = (TxxxFrame) metadata.get(0);
|
||||
assertEquals("", txxxFrame.description);
|
||||
assertEquals("mdialog_VINDICO1527664_start", txxxFrame.value);
|
||||
TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0);
|
||||
assertEquals("TXXX", textInformationFrame.id);
|
||||
assertEquals("", textInformationFrame.description);
|
||||
assertEquals("mdialog_VINDICO1527664_start", textInformationFrame.value);
|
||||
}
|
||||
|
||||
public void testDecodeApicFrame() throws MetadataDecoderException {
|
||||
|
|
@ -60,7 +61,19 @@ public class Id3DecoderTest extends TestCase {
|
|||
assertEquals(1, metadata.length());
|
||||
TextInformationFrame textInformationFrame = (TextInformationFrame) metadata.get(0);
|
||||
assertEquals("TIT2", textInformationFrame.id);
|
||||
assertEquals("Hello World", textInformationFrame.description);
|
||||
assertNull(textInformationFrame.description);
|
||||
assertEquals("Hello World", textInformationFrame.value);
|
||||
}
|
||||
|
||||
public void testDecodePrivFrame() throws MetadataDecoderException {
|
||||
byte[] rawId3 = new byte[] {73, 68, 51, 4, 0, 0, 0, 0, 0, 19, 80, 82, 73, 86, 0, 0, 0, 9, 0, 0,
|
||||
116, 101, 115, 116, 0, 1, 2, 3, 4};
|
||||
Id3Decoder decoder = new Id3Decoder();
|
||||
Metadata metadata = decoder.decode(rawId3, rawId3.length);
|
||||
assertEquals(1, metadata.length());
|
||||
PrivFrame privFrame = (PrivFrame) metadata.get(0);
|
||||
assertEquals("test", privFrame.owner);
|
||||
MoreAsserts.assertEquals(new byte[] {1, 2, 3, 4}, privFrame.privateData);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.metadata.scte35;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.MetadataDecoderException;
|
||||
import com.google.android.exoplayer2.metadata.MetadataInputBuffer;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* Test for {@link SpliceInfoDecoder}.
|
||||
*/
|
||||
public final class SpliceInfoDecoderTest extends TestCase {
|
||||
|
||||
private SpliceInfoDecoder decoder;
|
||||
private MetadataInputBuffer inputBuffer;
|
||||
|
||||
@Override
|
||||
public void setUp() {
|
||||
decoder = new SpliceInfoDecoder();
|
||||
inputBuffer = new MetadataInputBuffer();
|
||||
}
|
||||
|
||||
public void testWrappedAroundTimeSignalCommand() throws MetadataDecoderException {
|
||||
byte[] rawTimeSignalSection = new byte[] {
|
||||
0, // table_id.
|
||||
(byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4).
|
||||
0x14, // section_length(8).
|
||||
0x00, // protocol_version.
|
||||
0x00, // encrypted_packet, encryption_algorithm, pts_adjustment(1).
|
||||
0x00, 0x00, 0x00, 0x00, // pts_adjustment(32).
|
||||
0x00, // cw_index.
|
||||
0x00, // tier(8).
|
||||
0x00, // tier(4), splice_command_length(4).
|
||||
0x05, // splice_command_length(8).
|
||||
0x06, // splice_command_type = time_signal.
|
||||
// Start of splice_time().
|
||||
(byte) 0x80, // time_specified_flag, reserved, pts_time(1).
|
||||
0x52, 0x03, 0x02, (byte) 0x8f, // pts_time(32). PTS for a second after playback position.
|
||||
0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction).
|
||||
|
||||
// The playback position is 57:15:58.43 approximately.
|
||||
// With this offset, the playback position pts before wrapping is 0x451ebf851.
|
||||
Metadata metadata = feedInputBuffer(rawTimeSignalSection, 0x3000000000L, -0x50000L);
|
||||
assertEquals(1, metadata.length());
|
||||
assertEquals(removePtsConversionPrecisionError(0x3001000000L, inputBuffer.subsampleOffsetUs),
|
||||
((TimeSignalCommand) metadata.get(0)).playbackPositionUs);
|
||||
}
|
||||
|
||||
public void test2SpliceInsertCommands() throws MetadataDecoderException {
|
||||
byte[] rawSpliceInsertCommand1 = new byte[] {
|
||||
0, // table_id.
|
||||
(byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4).
|
||||
0x19, // section_length(8).
|
||||
0x00, // protocol_version.
|
||||
0x00, // encrypted_packet, encryption_algorithm, pts_adjustment(1).
|
||||
0x00, 0x00, 0x00, 0x00, // pts_adjustment(32).
|
||||
0x00, // cw_index.
|
||||
0x00, // tier(8).
|
||||
0x00, // tier(4), splice_command_length(4).
|
||||
0x0e, // splice_command_length(8).
|
||||
0x05, // splice_command_type = splice_insert.
|
||||
// Start of splice_insert().
|
||||
0x00, 0x00, 0x00, 0x42, // splice_event_id.
|
||||
0x00, // splice_event_cancel_indicator, reserved.
|
||||
0x40, // out_of_network_indicator, program_splice_flag, duration_flag,
|
||||
// splice_immediate_flag, reserved.
|
||||
// start of splice_time().
|
||||
(byte) 0x80, // time_specified_flag, reserved, pts_time(1).
|
||||
0x00, 0x00, 0x00, 0x00, // PTS for playback position 3s.
|
||||
0x00, 0x10, // unique_program_id.
|
||||
0x01, // avail_num.
|
||||
0x02, // avails_expected.
|
||||
0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction).
|
||||
|
||||
Metadata metadata = feedInputBuffer(rawSpliceInsertCommand1, 2000000, 3000000);
|
||||
assertEquals(1, metadata.length());
|
||||
SpliceInsertCommand command = (SpliceInsertCommand) metadata.get(0);
|
||||
assertEquals(66, command.spliceEventId);
|
||||
assertFalse(command.spliceEventCancelIndicator);
|
||||
assertFalse(command.outOfNetworkIndicator);
|
||||
assertTrue(command.programSpliceFlag);
|
||||
assertFalse(command.spliceImmediateFlag);
|
||||
assertEquals(3000000, command.programSplicePlaybackPositionUs);
|
||||
assertEquals(C.TIME_UNSET, command.breakDuration);
|
||||
assertEquals(16, command.uniqueProgramId);
|
||||
assertEquals(1, command.availNum);
|
||||
assertEquals(2, command.availsExpected);
|
||||
|
||||
byte[] rawSpliceInsertCommand2 = new byte[] {
|
||||
0, // table_id.
|
||||
(byte) 0x80, // section_syntax_indicator, private_indicator, reserved, section_length(4).
|
||||
0x22, // section_length(8).
|
||||
0x00, // protocol_version.
|
||||
0x00, // encrypted_packet, encryption_algorithm, pts_adjustment(1).
|
||||
0x00, 0x00, 0x00, 0x00, // pts_adjustment(32).
|
||||
0x00, // cw_index.
|
||||
0x00, // tier(8).
|
||||
0x00, // tier(4), splice_command_length(4).
|
||||
0x13, // splice_command_length(8).
|
||||
0x05, // splice_command_type = splice_insert.
|
||||
// Start of splice_insert().
|
||||
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // splice_event_id.
|
||||
0x00, // splice_event_cancel_indicator, reserved.
|
||||
0x00, // out_of_network_indicator, program_splice_flag, duration_flag,
|
||||
// splice_immediate_flag, reserved.
|
||||
0x02, // component_count.
|
||||
0x10, // component_tag.
|
||||
// start of splice_time().
|
||||
(byte) 0x81, // time_specified_flag, reserved, pts_time(1).
|
||||
(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, // PTS for playback position 10s.
|
||||
// start of splice_time().
|
||||
0x11, // component_tag.
|
||||
0x00, // time_specified_flag, reserved.
|
||||
0x00, 0x20, // unique_program_id.
|
||||
0x01, // avail_num.
|
||||
0x02, // avails_expected.
|
||||
0x00, 0x00, 0x00, 0x00}; // CRC_32 (ignored, check happens at extraction).
|
||||
|
||||
// By changing the subsample offset we force adjuster reconstruction.
|
||||
long subsampleOffset = 1000011;
|
||||
metadata = feedInputBuffer(rawSpliceInsertCommand2, 1000000, subsampleOffset);
|
||||
assertEquals(1, metadata.length());
|
||||
command = (SpliceInsertCommand) metadata.get(0);
|
||||
assertEquals(0xffffffffL, command.spliceEventId);
|
||||
assertFalse(command.spliceEventCancelIndicator);
|
||||
assertFalse(command.outOfNetworkIndicator);
|
||||
assertFalse(command.programSpliceFlag);
|
||||
assertFalse(command.spliceImmediateFlag);
|
||||
assertEquals(C.TIME_UNSET, command.programSplicePlaybackPositionUs);
|
||||
assertEquals(C.TIME_UNSET, command.breakDuration);
|
||||
List<SpliceInsertCommand.ComponentSplice> componentSplices = command.componentSpliceList;
|
||||
assertEquals(2, componentSplices.size());
|
||||
assertEquals(16, componentSplices.get(0).componentTag);
|
||||
assertEquals(1000000, componentSplices.get(0).componentSplicePlaybackPositionUs);
|
||||
assertEquals(17, componentSplices.get(1).componentTag);
|
||||
assertEquals(C.TIME_UNSET, componentSplices.get(1).componentSplicePts);
|
||||
assertEquals(32, command.uniqueProgramId);
|
||||
assertEquals(1, command.availNum);
|
||||
assertEquals(2, command.availsExpected);
|
||||
}
|
||||
|
||||
private Metadata feedInputBuffer(byte[] data, long timeUs, long subsampleOffset)
|
||||
throws MetadataDecoderException{
|
||||
inputBuffer.clear();
|
||||
inputBuffer.data = ByteBuffer.allocate(data.length).put(data);
|
||||
inputBuffer.timeUs = timeUs;
|
||||
inputBuffer.subsampleOffsetUs = subsampleOffset;
|
||||
return decoder.decode(inputBuffer);
|
||||
}
|
||||
|
||||
private static long removePtsConversionPrecisionError(long timeUs, long offsetUs) {
|
||||
return TimestampAdjuster.ptsToUs(TimestampAdjuster.usToPts(timeUs - offsetUs)) + offsetUs;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.source;
|
||||
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.Timeline;
|
||||
import com.google.android.exoplayer2.Timeline.Period;
|
||||
import com.google.android.exoplayer2.Timeline.Window;
|
||||
import com.google.android.exoplayer2.source.MediaSource.Listener;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ClippingMediaSource}.
|
||||
*/
|
||||
public final class ClippingMediaSourceTest extends InstrumentationTestCase {
|
||||
|
||||
private static final long TEST_PERIOD_DURATION_US = 1000000;
|
||||
private static final long TEST_CLIP_AMOUNT_US = 300000;
|
||||
|
||||
@Mock
|
||||
private MediaSource mockMediaSource;
|
||||
private Timeline clippedTimeline;
|
||||
private Window window;
|
||||
private Period period;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
TestUtil.setUpMockito(this);
|
||||
window = new Timeline.Window();
|
||||
period = new Timeline.Period();
|
||||
}
|
||||
|
||||
public void testNoClipping() {
|
||||
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true);
|
||||
|
||||
Timeline clippedTimeline = getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US);
|
||||
|
||||
assertEquals(1, clippedTimeline.getWindowCount());
|
||||
assertEquals(1, clippedTimeline.getPeriodCount());
|
||||
assertEquals(TEST_PERIOD_DURATION_US, clippedTimeline.getWindow(0, window).getDurationUs());
|
||||
assertEquals(TEST_PERIOD_DURATION_US, clippedTimeline.getPeriod(0, period).getDurationUs());
|
||||
}
|
||||
|
||||
public void testClippingUnseekableWindowThrows() {
|
||||
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), false);
|
||||
|
||||
// If the unseekable window isn't clipped, clipping succeeds.
|
||||
getClippedTimeline(timeline, 0, TEST_PERIOD_DURATION_US);
|
||||
try {
|
||||
// If the unseekable window is clipped, clipping fails.
|
||||
getClippedTimeline(timeline, 1, TEST_PERIOD_DURATION_US);
|
||||
fail("Expected clipping to fail.");
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Expected.
|
||||
}
|
||||
}
|
||||
|
||||
public void testClippingStart() {
|
||||
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true);
|
||||
|
||||
Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US,
|
||||
TEST_PERIOD_DURATION_US);
|
||||
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
|
||||
clippedTimeline.getWindow(0, window).getDurationUs());
|
||||
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
|
||||
clippedTimeline.getPeriod(0, period).getDurationUs());
|
||||
}
|
||||
|
||||
public void testClippingEnd() {
|
||||
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true);
|
||||
|
||||
Timeline clippedTimeline = getClippedTimeline(timeline, 0,
|
||||
TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US);
|
||||
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
|
||||
clippedTimeline.getWindow(0, window).getDurationUs());
|
||||
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US,
|
||||
clippedTimeline.getPeriod(0, period).getDurationUs());
|
||||
}
|
||||
|
||||
public void testClippingStartAndEnd() {
|
||||
Timeline timeline = new SinglePeriodTimeline(C.msToUs(TEST_PERIOD_DURATION_US), true);
|
||||
|
||||
Timeline clippedTimeline = getClippedTimeline(timeline, TEST_CLIP_AMOUNT_US,
|
||||
TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 2);
|
||||
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3,
|
||||
clippedTimeline.getWindow(0, window).getDurationUs());
|
||||
assertEquals(TEST_PERIOD_DURATION_US - TEST_CLIP_AMOUNT_US * 3,
|
||||
clippedTimeline.getPeriod(0, period).getDurationUs());
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the specified timeline in a {@link ClippingMediaSource} and returns the clipped timeline.
|
||||
*/
|
||||
private Timeline getClippedTimeline(Timeline timeline, long startMs, long endMs) {
|
||||
mockMediaSourceSourceWithTimeline(timeline);
|
||||
new ClippingMediaSource(mockMediaSource, startMs, endMs).prepareSource(null, true,
|
||||
new Listener() {
|
||||
@Override
|
||||
public void onSourceInfoRefreshed(Timeline timeline, Object manifest) {
|
||||
clippedTimeline = timeline;
|
||||
}
|
||||
});
|
||||
return clippedTimeline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a mock {@link MediaSource} with the specified {@link Timeline} in its source info.
|
||||
*/
|
||||
private MediaSource mockMediaSourceSourceWithTimeline(final Timeline timeline) {
|
||||
doAnswer(new Answer<Void>() {
|
||||
@Override
|
||||
public Void answer(InvocationOnMock invocation) throws Throwable {
|
||||
MediaSource.Listener listener = (MediaSource.Listener) invocation.getArguments()[2];
|
||||
listener.onSourceInfoRefreshed(timeline, null);
|
||||
return null;
|
||||
}
|
||||
}).when(mockMediaSource).prepareSource(Mockito.any(ExoPlayer.class), Mockito.anyBoolean(),
|
||||
Mockito.any(MediaSource.Listener.class));
|
||||
return mockMediaSource;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -20,6 +20,8 @@ import android.test.InstrumentationTestCase;
|
|||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link DashManifestParser}.
|
||||
|
|
@ -70,34 +72,57 @@ public class DashManifestParserTest extends InstrumentationTestCase {
|
|||
}
|
||||
|
||||
public void testParseCea608AccessibilityChannel() {
|
||||
assertEquals(1, DashManifestParser.parseCea608AccessibilityChannel("CC1=eng"));
|
||||
assertEquals(2, DashManifestParser.parseCea608AccessibilityChannel("CC2=eng"));
|
||||
assertEquals(3, DashManifestParser.parseCea608AccessibilityChannel("CC3=eng"));
|
||||
assertEquals(4, DashManifestParser.parseCea608AccessibilityChannel("CC4=eng"));
|
||||
assertEquals(1, DashManifestParser.parseCea608AccessibilityChannel(
|
||||
buildCea608AccessibilityDescriptors("CC1=eng")));
|
||||
assertEquals(2, DashManifestParser.parseCea608AccessibilityChannel(
|
||||
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("CC0=eng"));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel("CC5=eng"));
|
||||
assertEquals(Format.NO_VALUE,
|
||||
DashManifestParser.parseCea608AccessibilityChannel("Wrong format"));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(
|
||||
buildCea608AccessibilityDescriptors(null)));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(
|
||||
buildCea608AccessibilityDescriptors("")));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(
|
||||
buildCea608AccessibilityDescriptors("CC0=eng")));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(
|
||||
buildCea608AccessibilityDescriptors("CC5=eng")));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea608AccessibilityChannel(
|
||||
buildCea608AccessibilityDescriptors("Wrong format")));
|
||||
}
|
||||
|
||||
public void testParseCea708AccessibilityChannel() {
|
||||
assertEquals(1, DashManifestParser.parseCea708AccessibilityChannel("1=lang:eng"));
|
||||
assertEquals(2, DashManifestParser.parseCea708AccessibilityChannel("2=lang:eng"));
|
||||
assertEquals(3, DashManifestParser.parseCea708AccessibilityChannel("3=lang:eng"));
|
||||
assertEquals(62, DashManifestParser.parseCea708AccessibilityChannel("62=lang:eng"));
|
||||
assertEquals(63, DashManifestParser.parseCea708AccessibilityChannel("63=lang:eng"));
|
||||
assertEquals(1, DashManifestParser.parseCea708AccessibilityChannel(
|
||||
buildCea708AccessibilityDescriptors("1=lang:eng")));
|
||||
assertEquals(2, DashManifestParser.parseCea708AccessibilityChannel(
|
||||
buildCea708AccessibilityDescriptors("2=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("0=lang:eng"));
|
||||
assertEquals(Format.NO_VALUE,
|
||||
DashManifestParser.parseCea708AccessibilityChannel("64=lang:eng"));
|
||||
assertEquals(Format.NO_VALUE,
|
||||
DashManifestParser.parseCea708AccessibilityChannel("Wrong format"));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(
|
||||
buildCea708AccessibilityDescriptors(null)));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(
|
||||
buildCea708AccessibilityDescriptors("")));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(
|
||||
buildCea708AccessibilityDescriptors("0=lang:eng")));
|
||||
assertEquals(Format.NO_VALUE, DashManifestParser.parseCea708AccessibilityChannel(
|
||||
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 {
|
|||
String uri = "http://www.google.com";
|
||||
SegmentBase base = new SingleSegmentBase(new RangedUri(null, 0, 1), 1, 0, 1, 1);
|
||||
Format format = Format.createVideoContainerFormat("0", MimeTypes.APPLICATION_MP4, null,
|
||||
MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null);
|
||||
MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null, 0);
|
||||
Representation representation = Representation.newInstance("test_stream_1", 3, format, uri,
|
||||
base);
|
||||
assertEquals("test_stream_1.0.3", representation.getCacheKey());
|
||||
|
||||
format = Format.createVideoContainerFormat("150", MimeTypes.APPLICATION_MP4, null,
|
||||
MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null);
|
||||
MimeTypes.VIDEO_H264, 2500000, 1920, 1080, Format.NO_VALUE, null, 0);
|
||||
representation = Representation.newInstance("test_stream_1", Representation.REVISION_ID_DEFAULT,
|
||||
format, uri, base);
|
||||
assertEquals("test_stream_1.150.-1", representation.getCacheKey());
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.hls.playlist;
|
|||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
|
|
@ -29,70 +30,86 @@ import junit.framework.TestCase;
|
|||
*/
|
||||
public class HlsMasterPlaylistParserTest extends TestCase {
|
||||
|
||||
public void testParseMasterPlaylist() {
|
||||
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
|
||||
String playlistString = "#EXTM3U\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
|
||||
+ "http://example.com/low.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n"
|
||||
+ "http://example.com/spaces_in_codecs.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=384x160\n"
|
||||
+ "http://example.com/mid.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=7680000\n"
|
||||
+ "http://example.com/hi.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n"
|
||||
+ "http://example.com/audio-only.m3u8";
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(
|
||||
playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
|
||||
private static final String PLAYLIST_URI = "https://example.com/test.m3u8";
|
||||
|
||||
private static final String MASTER_PLAYLIST = " #EXTM3U \n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
|
||||
+ "http://example.com/low.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2 , avc1.66.30 \"\n"
|
||||
+ "http://example.com/spaces_in_codecs.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=2560000,RESOLUTION=384x160\n"
|
||||
+ "http://example.com/mid.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=7680000\n"
|
||||
+ "http://example.com/hi.m3u8\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n"
|
||||
+ "http://example.com/audio-only.m3u8";
|
||||
|
||||
private static final String PLAYLIST_WITH_INVALID_HEADER = "#EXTMU3\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
|
||||
+ "http://example.com/low.m3u8\n";
|
||||
|
||||
public void testParseMasterPlaylist() throws IOException{
|
||||
HlsPlaylist playlist = parsePlaylist(PLAYLIST_URI, MASTER_PLAYLIST);
|
||||
assertNotNull(playlist);
|
||||
assertEquals(HlsPlaylist.TYPE_MASTER, playlist.type);
|
||||
|
||||
HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist;
|
||||
|
||||
List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
|
||||
assertNotNull(variants);
|
||||
assertEquals(5, variants.size());
|
||||
|
||||
assertEquals(1280000, variants.get(0).format.bitrate);
|
||||
assertNotNull(variants.get(0).format.codecs);
|
||||
assertEquals("mp4a.40.2,avc1.66.30", variants.get(0).format.codecs);
|
||||
assertEquals(304, variants.get(0).format.width);
|
||||
assertEquals(128, variants.get(0).format.height);
|
||||
assertEquals("http://example.com/low.m3u8", variants.get(0).url);
|
||||
|
||||
assertEquals(1280000, variants.get(1).format.bitrate);
|
||||
assertNotNull(variants.get(1).format.codecs);
|
||||
assertEquals("mp4a.40.2 , avc1.66.30 ", variants.get(1).format.codecs);
|
||||
assertEquals("http://example.com/spaces_in_codecs.m3u8", variants.get(1).url);
|
||||
|
||||
assertEquals(2560000, variants.get(2).format.bitrate);
|
||||
assertEquals(null, variants.get(2).format.codecs);
|
||||
assertEquals(384, variants.get(2).format.width);
|
||||
assertEquals(160, variants.get(2).format.height);
|
||||
assertEquals("http://example.com/mid.m3u8", variants.get(2).url);
|
||||
|
||||
assertEquals(7680000, variants.get(3).format.bitrate);
|
||||
assertEquals(null, variants.get(3).format.codecs);
|
||||
assertEquals(Format.NO_VALUE, variants.get(3).format.width);
|
||||
assertEquals(Format.NO_VALUE, variants.get(3).format.height);
|
||||
assertEquals("http://example.com/hi.m3u8", variants.get(3).url);
|
||||
|
||||
assertEquals(65000, variants.get(4).format.bitrate);
|
||||
assertNotNull(variants.get(4).format.codecs);
|
||||
assertEquals("mp4a.40.5", variants.get(4).format.codecs);
|
||||
assertEquals(Format.NO_VALUE, variants.get(4).format.width);
|
||||
assertEquals(Format.NO_VALUE, variants.get(4).format.height);
|
||||
assertEquals("http://example.com/audio-only.m3u8", variants.get(4).url);
|
||||
}
|
||||
|
||||
public void testPlaylistWithInvalidHeader() throws IOException {
|
||||
try {
|
||||
HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream);
|
||||
assertNotNull(playlist);
|
||||
assertEquals(HlsPlaylist.TYPE_MASTER, playlist.type);
|
||||
|
||||
HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist;
|
||||
|
||||
List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
|
||||
assertNotNull(variants);
|
||||
assertEquals(5, variants.size());
|
||||
|
||||
assertEquals(1280000, variants.get(0).format.bitrate);
|
||||
assertNotNull(variants.get(0).format.codecs);
|
||||
assertEquals("mp4a.40.2,avc1.66.30", variants.get(0).format.codecs);
|
||||
assertEquals(304, variants.get(0).format.width);
|
||||
assertEquals(128, variants.get(0).format.height);
|
||||
assertEquals("http://example.com/low.m3u8", variants.get(0).url);
|
||||
|
||||
assertEquals(1280000, variants.get(1).format.bitrate);
|
||||
assertNotNull(variants.get(1).format.codecs);
|
||||
assertEquals("mp4a.40.2 , avc1.66.30 ", variants.get(1).format.codecs);
|
||||
assertEquals("http://example.com/spaces_in_codecs.m3u8", variants.get(1).url);
|
||||
|
||||
assertEquals(2560000, variants.get(2).format.bitrate);
|
||||
assertEquals(null, variants.get(2).format.codecs);
|
||||
assertEquals(384, variants.get(2).format.width);
|
||||
assertEquals(160, variants.get(2).format.height);
|
||||
assertEquals("http://example.com/mid.m3u8", variants.get(2).url);
|
||||
|
||||
assertEquals(7680000, variants.get(3).format.bitrate);
|
||||
assertEquals(null, variants.get(3).format.codecs);
|
||||
assertEquals(Format.NO_VALUE, variants.get(3).format.width);
|
||||
assertEquals(Format.NO_VALUE, variants.get(3).format.height);
|
||||
assertEquals("http://example.com/hi.m3u8", variants.get(3).url);
|
||||
|
||||
assertEquals(65000, variants.get(4).format.bitrate);
|
||||
assertNotNull(variants.get(4).format.codecs);
|
||||
assertEquals("mp4a.40.5", variants.get(4).format.codecs);
|
||||
assertEquals(Format.NO_VALUE, variants.get(4).format.width);
|
||||
assertEquals(Format.NO_VALUE, variants.get(4).format.height);
|
||||
assertEquals("http://example.com/audio-only.m3u8", variants.get(4).url);
|
||||
} catch (IOException exception) {
|
||||
fail(exception.getMessage());
|
||||
parsePlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER);
|
||||
fail("Expected exception not thrown.");
|
||||
} catch (ParserException e) {
|
||||
// Expected due to invalid header.
|
||||
}
|
||||
}
|
||||
|
||||
private static HlsPlaylist parsePlaylist(String uri, String playlistString) throws IOException {
|
||||
Uri playlistUri = Uri.parse(uri);
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(
|
||||
playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
|
||||
return new HlsPlaylistParser().parse(playlistUri, inputStream);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls.playlist;
|
|||
|
||||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
|
@ -34,6 +35,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
|
|||
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
|
||||
String playlistString = "#EXTM3U\n"
|
||||
+ "#EXT-X-VERSION:3\n"
|
||||
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
|
||||
+ "#EXT-X-TARGETDURATION:8\n"
|
||||
+ "#EXT-X-MEDIA-SEQUENCE:2679\n"
|
||||
+ "#EXT-X-DISCONTINUITY-SEQUENCE:4\n"
|
||||
|
|
@ -70,62 +72,68 @@ public class HlsMediaPlaylistParserTest extends TestCase {
|
|||
assertEquals(HlsPlaylist.TYPE_MEDIA, playlist.type);
|
||||
|
||||
HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
|
||||
assertEquals(HlsMediaPlaylist.PLAYLIST_TYPE_VOD, mediaPlaylist.playlistType);
|
||||
|
||||
assertEquals(2679, mediaPlaylist.mediaSequence);
|
||||
assertEquals(3, mediaPlaylist.version);
|
||||
assertEquals(true, mediaPlaylist.hasEndTag);
|
||||
List<HlsMediaPlaylist.Segment> segments = mediaPlaylist.segments;
|
||||
assertTrue(mediaPlaylist.hasEndTag);
|
||||
List<Segment> segments = mediaPlaylist.segments;
|
||||
assertNotNull(segments);
|
||||
assertEquals(5, segments.size());
|
||||
|
||||
assertEquals(4, segments.get(0).discontinuitySequenceNumber);
|
||||
assertEquals(7975000, segments.get(0).durationUs);
|
||||
assertEquals(false, segments.get(0).isEncrypted);
|
||||
assertEquals(null, segments.get(0).encryptionKeyUri);
|
||||
assertEquals(null, segments.get(0).encryptionIV);
|
||||
assertEquals(51370, segments.get(0).byterangeLength);
|
||||
assertEquals(0, segments.get(0).byterangeOffset);
|
||||
assertEquals("https://priv.example.com/fileSequence2679.ts", segments.get(0).url);
|
||||
Segment segment = segments.get(0);
|
||||
assertEquals(4, mediaPlaylist.discontinuitySequence + segment.relativeDiscontinuitySequence);
|
||||
assertEquals(7975000, segment.durationUs);
|
||||
assertFalse(segment.isEncrypted);
|
||||
assertEquals(null, segment.encryptionKeyUri);
|
||||
assertEquals(null, segment.encryptionIV);
|
||||
assertEquals(51370, segment.byterangeLength);
|
||||
assertEquals(0, segment.byterangeOffset);
|
||||
assertEquals("https://priv.example.com/fileSequence2679.ts", segment.url);
|
||||
|
||||
assertEquals(4, segments.get(1).discontinuitySequenceNumber);
|
||||
assertEquals(7975000, segments.get(1).durationUs);
|
||||
assertEquals(true, segments.get(1).isEncrypted);
|
||||
assertEquals("https://priv.example.com/key.php?r=2680", segments.get(1).encryptionKeyUri);
|
||||
assertEquals("0x1566B", segments.get(1).encryptionIV);
|
||||
assertEquals(51501, segments.get(1).byterangeLength);
|
||||
assertEquals(2147483648L, segments.get(1).byterangeOffset);
|
||||
assertEquals("https://priv.example.com/fileSequence2680.ts", segments.get(1).url);
|
||||
segment = segments.get(1);
|
||||
assertEquals(0, segment.relativeDiscontinuitySequence);
|
||||
assertEquals(7975000, segment.durationUs);
|
||||
assertTrue(segment.isEncrypted);
|
||||
assertEquals("https://priv.example.com/key.php?r=2680", segment.encryptionKeyUri);
|
||||
assertEquals("0x1566B", segment.encryptionIV);
|
||||
assertEquals(51501, segment.byterangeLength);
|
||||
assertEquals(2147483648L, segment.byterangeOffset);
|
||||
assertEquals("https://priv.example.com/fileSequence2680.ts", segment.url);
|
||||
|
||||
assertEquals(4, segments.get(2).discontinuitySequenceNumber);
|
||||
assertEquals(7941000, segments.get(2).durationUs);
|
||||
assertEquals(false, segments.get(2).isEncrypted);
|
||||
assertEquals(null, segments.get(2).encryptionKeyUri);
|
||||
assertEquals(null, segments.get(2).encryptionIV);
|
||||
assertEquals(51501, segments.get(2).byterangeLength);
|
||||
assertEquals(2147535149L, segments.get(2).byterangeOffset);
|
||||
assertEquals("https://priv.example.com/fileSequence2681.ts", segments.get(2).url);
|
||||
segment = segments.get(2);
|
||||
assertEquals(0, segment.relativeDiscontinuitySequence);
|
||||
assertEquals(7941000, segment.durationUs);
|
||||
assertFalse(segment.isEncrypted);
|
||||
assertEquals(null, segment.encryptionKeyUri);
|
||||
assertEquals(null, segment.encryptionIV);
|
||||
assertEquals(51501, segment.byterangeLength);
|
||||
assertEquals(2147535149L, segment.byterangeOffset);
|
||||
assertEquals("https://priv.example.com/fileSequence2681.ts", segment.url);
|
||||
|
||||
assertEquals(5, segments.get(3).discontinuitySequenceNumber);
|
||||
assertEquals(7975000, segments.get(3).durationUs);
|
||||
assertEquals(true, segments.get(3).isEncrypted);
|
||||
assertEquals("https://priv.example.com/key.php?r=2682", segments.get(3).encryptionKeyUri);
|
||||
segment = segments.get(3);
|
||||
assertEquals(1, segment.relativeDiscontinuitySequence);
|
||||
assertEquals(7975000, segment.durationUs);
|
||||
assertTrue(segment.isEncrypted);
|
||||
assertEquals("https://priv.example.com/key.php?r=2682", segment.encryptionKeyUri);
|
||||
// 0xA7A == 2682.
|
||||
assertNotNull(segments.get(3).encryptionIV);
|
||||
assertEquals("A7A", segments.get(3).encryptionIV.toUpperCase(Locale.getDefault()));
|
||||
assertEquals(51740, segments.get(3).byterangeLength);
|
||||
assertEquals(2147586650L, segments.get(3).byterangeOffset);
|
||||
assertEquals("https://priv.example.com/fileSequence2682.ts", segments.get(3).url);
|
||||
assertNotNull(segment.encryptionIV);
|
||||
assertEquals("A7A", segment.encryptionIV.toUpperCase(Locale.getDefault()));
|
||||
assertEquals(51740, segment.byterangeLength);
|
||||
assertEquals(2147586650L, segment.byterangeOffset);
|
||||
assertEquals("https://priv.example.com/fileSequence2682.ts", segment.url);
|
||||
|
||||
assertEquals(5, segments.get(4).discontinuitySequenceNumber);
|
||||
assertEquals(7975000, segments.get(4).durationUs);
|
||||
assertEquals(true, segments.get(4).isEncrypted);
|
||||
assertEquals("https://priv.example.com/key.php?r=2682", segments.get(4).encryptionKeyUri);
|
||||
segment = segments.get(4);
|
||||
assertEquals(1, segment.relativeDiscontinuitySequence);
|
||||
assertEquals(7975000, segment.durationUs);
|
||||
assertTrue(segment.isEncrypted);
|
||||
assertEquals("https://priv.example.com/key.php?r=2682", segment.encryptionKeyUri);
|
||||
// 0xA7B == 2683.
|
||||
assertNotNull(segments.get(4).encryptionIV);
|
||||
assertEquals("A7B", segments.get(4).encryptionIV.toUpperCase(Locale.getDefault()));
|
||||
assertEquals(C.LENGTH_UNSET, segments.get(4).byterangeLength);
|
||||
assertEquals(0, segments.get(4).byterangeOffset);
|
||||
assertEquals("https://priv.example.com/fileSequence2683.ts", segments.get(4).url);
|
||||
assertNotNull(segment.encryptionIV);
|
||||
assertEquals("A7B", segment.encryptionIV.toUpperCase(Locale.getDefault()));
|
||||
assertEquals(C.LENGTH_UNSET, segment.byterangeLength);
|
||||
assertEquals(0, segment.byterangeOffset);
|
||||
assertEquals("https://priv.example.com/fileSequence2683.ts", segment.url);
|
||||
} catch (IOException exception) {
|
||||
fail(exception.getMessage());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ import java.io.File;
|
|||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
/** Unit tests for {@link CacheDataSource}. */
|
||||
/**
|
||||
* Unit tests for {@link CacheDataSource}.
|
||||
*/
|
||||
public class CacheDataSourceTest extends InstrumentationTestCase {
|
||||
|
||||
private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
|
||||
|
|
@ -117,6 +119,13 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
|
|||
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)
|
||||
throws IOException {
|
||||
// Read all data from upstream and cache
|
||||
|
|
@ -169,6 +178,12 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
|
|||
|
||||
private CacheDataSource createCacheDataSource(boolean setReadException,
|
||||
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();
|
||||
if (setReadException) {
|
||||
builder.appendReadError(new IOException("Shouldn't read from upstream"));
|
||||
|
|
@ -176,8 +191,7 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
|
|||
builder.setSimulateUnknownLength(simulateUnknownLength);
|
||||
builder.appendReadData(TEST_DATA);
|
||||
FakeDataSource upstream = builder.build();
|
||||
return new CacheDataSource(simpleCache, upstream, CacheDataSource.FLAG_BLOCK_ON_CACHE,
|
||||
MAX_CACHE_FILE_SIZE);
|
||||
return new CacheDataSource(simpleCache, upstream, flags, MAX_CACHE_FILE_SIZE);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
181
library/src/androidTest/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest2.java
vendored
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.upstream.cache;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.test.AndroidTestCase;
|
||||
import android.test.MoreAsserts;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSource;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.upstream.DataSink;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.upstream.FileDataSource;
|
||||
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
|
||||
import com.google.android.exoplayer2.upstream.crypto.AesCipherDataSink;
|
||||
import com.google.android.exoplayer2.upstream.crypto.AesCipherDataSource;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Additional tests for {@link CacheDataSource}.
|
||||
*/
|
||||
public class CacheDataSourceTest2 extends AndroidTestCase {
|
||||
|
||||
private static final String EXO_CACHE_DIR = "exo";
|
||||
private static final int EXO_CACHE_MAX_FILESIZE = 128;
|
||||
|
||||
private static final Uri URI = Uri.parse("http://test.com/content");
|
||||
private static final String KEY = "key";
|
||||
private static final byte[] DATA = TestUtil.buildTestData(8 * EXO_CACHE_MAX_FILESIZE + 1);
|
||||
|
||||
// A DataSpec that covers the full file.
|
||||
private static final DataSpec FULL = new DataSpec(URI, 0, DATA.length, KEY);
|
||||
|
||||
private static final int OFFSET_ON_BOUNDARY = EXO_CACHE_MAX_FILESIZE;
|
||||
// A DataSpec that starts at 0 and extends to a cache file boundary.
|
||||
private static final DataSpec END_ON_BOUNDARY = new DataSpec(URI, 0, OFFSET_ON_BOUNDARY, KEY);
|
||||
// A DataSpec that starts on the same boundary and extends to the end of the file.
|
||||
private static final DataSpec START_ON_BOUNDARY = new DataSpec(URI, OFFSET_ON_BOUNDARY,
|
||||
DATA.length - OFFSET_ON_BOUNDARY, KEY);
|
||||
|
||||
private static final int OFFSET_OFF_BOUNDARY = EXO_CACHE_MAX_FILESIZE * 2 + 1;
|
||||
// A DataSpec that starts at 0 and extends to just past a cache file boundary.
|
||||
private static final DataSpec END_OFF_BOUNDARY = new DataSpec(URI, 0, OFFSET_OFF_BOUNDARY, KEY);
|
||||
// A DataSpec that starts on the same boundary and extends to the end of the file.
|
||||
private static final DataSpec START_OFF_BOUNDARY = new DataSpec(URI, OFFSET_OFF_BOUNDARY,
|
||||
DATA.length - OFFSET_OFF_BOUNDARY, KEY);
|
||||
|
||||
public void testWithoutEncryption() throws IOException {
|
||||
testReads(false);
|
||||
}
|
||||
|
||||
public void testWithEncryption() throws IOException {
|
||||
testReads(true);
|
||||
}
|
||||
|
||||
private void testReads(boolean useEncryption) throws IOException {
|
||||
FakeDataSource upstreamSource = buildFakeUpstreamSource();
|
||||
CacheDataSource source = buildCacheDataSource(getContext(), upstreamSource, useEncryption);
|
||||
// First read, should arrive from upstream.
|
||||
testRead(END_ON_BOUNDARY, source);
|
||||
assertSingleOpen(upstreamSource, 0, OFFSET_ON_BOUNDARY);
|
||||
// Second read, should arrive from upstream.
|
||||
testRead(START_OFF_BOUNDARY, source);
|
||||
assertSingleOpen(upstreamSource, OFFSET_OFF_BOUNDARY, DATA.length);
|
||||
// Second read, should arrive part from cache and part from upstream.
|
||||
testRead(END_OFF_BOUNDARY, source);
|
||||
assertSingleOpen(upstreamSource, OFFSET_ON_BOUNDARY, OFFSET_OFF_BOUNDARY);
|
||||
// Third read, should arrive from cache.
|
||||
testRead(FULL, source);
|
||||
assertNoOpen(upstreamSource);
|
||||
// Various reads, should all arrive from cache.
|
||||
testRead(FULL, source);
|
||||
assertNoOpen(upstreamSource);
|
||||
testRead(START_ON_BOUNDARY, source);
|
||||
assertNoOpen(upstreamSource);
|
||||
testRead(END_ON_BOUNDARY, source);
|
||||
assertNoOpen(upstreamSource);
|
||||
testRead(START_OFF_BOUNDARY, source);
|
||||
assertNoOpen(upstreamSource);
|
||||
testRead(END_OFF_BOUNDARY, source);
|
||||
assertNoOpen(upstreamSource);
|
||||
}
|
||||
|
||||
private void testRead(DataSpec dataSpec, CacheDataSource source) throws IOException {
|
||||
byte[] scratch = new byte[4096];
|
||||
Random random = new Random(0);
|
||||
source.open(dataSpec);
|
||||
int position = (int) dataSpec.absoluteStreamPosition;
|
||||
int bytesRead = 0;
|
||||
while (bytesRead != C.RESULT_END_OF_INPUT) {
|
||||
int maxBytesToRead = random.nextInt(scratch.length) + 1;
|
||||
bytesRead = source.read(scratch, 0, maxBytesToRead);
|
||||
if (bytesRead != C.RESULT_END_OF_INPUT) {
|
||||
MoreAsserts.assertEquals(Arrays.copyOfRange(DATA, position, position + bytesRead),
|
||||
Arrays.copyOf(scratch, bytesRead));
|
||||
position += bytesRead;
|
||||
}
|
||||
}
|
||||
source.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that a single {@link DataSource#open(DataSpec)} call has been made to the upstream
|
||||
* source, with the specified start (inclusive) and end (exclusive) positions.
|
||||
*/
|
||||
private void assertSingleOpen(FakeDataSource upstreamSource, int start, int end) {
|
||||
DataSpec[] openedDataSpecs = upstreamSource.getAndClearOpenedDataSpecs();
|
||||
assertEquals(1, openedDataSpecs.length);
|
||||
assertEquals(start, openedDataSpecs[0].position);
|
||||
assertEquals(start, openedDataSpecs[0].absoluteStreamPosition);
|
||||
assertEquals(end - start, openedDataSpecs[0].length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the upstream source was not opened.
|
||||
*/
|
||||
private void assertNoOpen(FakeDataSource upstreamSource) {
|
||||
DataSpec[] openedDataSpecs = upstreamSource.getAndClearOpenedDataSpecs();
|
||||
assertEquals(0, openedDataSpecs.length);
|
||||
}
|
||||
|
||||
private static FakeDataSource buildFakeUpstreamSource() {
|
||||
return new FakeDataSource.Builder().appendReadData(DATA).build();
|
||||
}
|
||||
|
||||
private static CacheDataSource buildCacheDataSource(Context context, DataSource upstreamSource,
|
||||
boolean useAesEncryption) throws CacheException {
|
||||
File cacheDir = context.getExternalCacheDir();
|
||||
Cache cache = new SimpleCache(new File(cacheDir, EXO_CACHE_DIR), new NoOpCacheEvictor());
|
||||
emptyCache(cache);
|
||||
|
||||
// Source and cipher
|
||||
final String secretKey = "testKey:12345678";
|
||||
DataSource file = new FileDataSource();
|
||||
DataSource cacheReadDataSource = useAesEncryption
|
||||
? new AesCipherDataSource(Util.getUtf8Bytes(secretKey), file) : file;
|
||||
|
||||
// Sink and cipher
|
||||
CacheDataSink cacheSink = new CacheDataSink(cache, EXO_CACHE_MAX_FILESIZE);
|
||||
byte[] scratch = new byte[3897];
|
||||
DataSink cacheWriteDataSink = useAesEncryption
|
||||
? new AesCipherDataSink(Util.getUtf8Bytes(secretKey), cacheSink, scratch) : cacheSink;
|
||||
|
||||
return new CacheDataSource(cache,
|
||||
upstreamSource,
|
||||
cacheReadDataSource,
|
||||
cacheWriteDataSink,
|
||||
CacheDataSource.FLAG_BLOCK_ON_CACHE,
|
||||
null); // eventListener
|
||||
}
|
||||
|
||||
private static void emptyCache(Cache cache) throws CacheException {
|
||||
for (String key : cache.getKeys()) {
|
||||
for (CacheSpan span : cache.getCachedSpans(key)) {
|
||||
cache.removeSpan(span);
|
||||
}
|
||||
}
|
||||
// Sanity check that the cache really is empty now.
|
||||
assertTrue(cache.getKeys().isEmpty());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -163,7 +163,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
|||
|
||||
public void testEncryption() throws Exception {
|
||||
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
|
||||
byte[] key2 = "bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
|
||||
byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key
|
||||
|
||||
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir, key),
|
||||
new CachedContentIndex(cacheDir, key));
|
||||
|
|
@ -181,7 +181,7 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
|||
// Assert file content is different
|
||||
FileInputStream fis1 = new FileInputStream(file1);
|
||||
FileInputStream fis2 = new FileInputStream(file2);
|
||||
for (int b; (b = fis1.read()) == fis2.read();) {
|
||||
for (int b; (b = fis1.read()) == fis2.read(); ) {
|
||||
assertTrue(b != -1);
|
||||
}
|
||||
|
||||
|
|
@ -205,6 +205,12 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
|||
// Non encrypted index file can be read even when encryption key provided.
|
||||
assertStoredAndLoadedEqual(new CachedContentIndex(cacheDir),
|
||||
new CachedContentIndex(cacheDir, key));
|
||||
|
||||
// Test multiple store() calls
|
||||
CachedContentIndex index = new CachedContentIndex(cacheDir, key);
|
||||
index.addNew(new CachedContent(15, "key3", 110));
|
||||
index.store();
|
||||
assertStoredAndLoadedEqual(index, new CachedContentIndex(cacheDir, key));
|
||||
}
|
||||
|
||||
private void assertStoredAndLoadedEqual(CachedContentIndex index, CachedContentIndex index2)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.upstream.cache;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.extractor.ChunkIndex;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import org.mockito.Mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link CachedRegionTracker}.
|
||||
*/
|
||||
public final class CachedRegionTrackerTest extends InstrumentationTestCase {
|
||||
|
||||
private static final String CACHE_KEY = "abc";
|
||||
private static final long MS_IN_US = 1000;
|
||||
|
||||
// 5 chunks, each 20 bytes long and 100 ms long.
|
||||
private static final ChunkIndex CHUNK_INDEX = new ChunkIndex(
|
||||
new int[] {20, 20, 20, 20, 20},
|
||||
new long[] {100, 120, 140, 160, 180},
|
||||
new long[] {100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US, 100 * MS_IN_US},
|
||||
new long[] {0, 100 * MS_IN_US, 200 * MS_IN_US, 300 * MS_IN_US, 400 * MS_IN_US});
|
||||
|
||||
@Mock private Cache cache;
|
||||
private CachedRegionTracker tracker;
|
||||
|
||||
private CachedContentIndex index;
|
||||
private File cacheDir;
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
TestUtil.setUpMockito(this);
|
||||
|
||||
tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX);
|
||||
|
||||
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
|
||||
index = new CachedContentIndex(cacheDir);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
TestUtil.recursiveDelete(cacheDir);
|
||||
}
|
||||
|
||||
public void testGetRegion_noSpansInCache() {
|
||||
assertEquals(CachedRegionTracker.NOT_CACHED, tracker.getRegionEndTimeMs(100));
|
||||
assertEquals(CachedRegionTracker.NOT_CACHED, tracker.getRegionEndTimeMs(150));
|
||||
}
|
||||
|
||||
public void testGetRegion_fullyCached() throws Exception {
|
||||
tracker.onSpanAdded(
|
||||
cache,
|
||||
newCacheSpan(100, 100));
|
||||
|
||||
assertEquals(CachedRegionTracker.CACHED_TO_END, tracker.getRegionEndTimeMs(101));
|
||||
assertEquals(CachedRegionTracker.CACHED_TO_END, tracker.getRegionEndTimeMs(121));
|
||||
}
|
||||
|
||||
public void testGetRegion_partiallyCached() throws Exception {
|
||||
tracker.onSpanAdded(
|
||||
cache,
|
||||
newCacheSpan(100, 40));
|
||||
|
||||
assertEquals(200, tracker.getRegionEndTimeMs(101));
|
||||
assertEquals(200, tracker.getRegionEndTimeMs(121));
|
||||
}
|
||||
|
||||
public void testGetRegion_multipleSpanAddsJoinedCorrectly() throws Exception {
|
||||
tracker.onSpanAdded(
|
||||
cache,
|
||||
newCacheSpan(100, 20));
|
||||
tracker.onSpanAdded(
|
||||
cache,
|
||||
newCacheSpan(120, 20));
|
||||
|
||||
assertEquals(200, tracker.getRegionEndTimeMs(101));
|
||||
assertEquals(200, tracker.getRegionEndTimeMs(121));
|
||||
}
|
||||
|
||||
public void testGetRegion_fullyCachedThenPartiallyRemoved() throws Exception {
|
||||
// Start with the full stream in cache.
|
||||
tracker.onSpanAdded(
|
||||
cache,
|
||||
newCacheSpan(100, 100));
|
||||
|
||||
// Remove the middle bit.
|
||||
tracker.onSpanRemoved(
|
||||
cache,
|
||||
newCacheSpan(140, 40));
|
||||
|
||||
assertEquals(200, tracker.getRegionEndTimeMs(101));
|
||||
assertEquals(200, tracker.getRegionEndTimeMs(121));
|
||||
|
||||
assertEquals(CachedRegionTracker.CACHED_TO_END, tracker.getRegionEndTimeMs(181));
|
||||
}
|
||||
|
||||
public void testGetRegion_subchunkEstimation() throws Exception {
|
||||
tracker.onSpanAdded(
|
||||
cache,
|
||||
newCacheSpan(100, 10));
|
||||
|
||||
assertEquals(50, tracker.getRegionEndTimeMs(101));
|
||||
assertEquals(CachedRegionTracker.NOT_CACHED, tracker.getRegionEndTimeMs(111));
|
||||
}
|
||||
|
||||
private CacheSpan newCacheSpan(int position, int length) throws IOException {
|
||||
return SimpleCacheSpanTest.createCacheSpan(index, cacheDir, CACHE_KEY, position, length, 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -16,12 +16,16 @@
|
|||
package com.google.android.exoplayer2.upstream.cache;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import android.test.MoreAsserts;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.NavigableSet;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
|
|
@ -46,9 +50,9 @@ public class SimpleCacheTest extends InstrumentationTestCase {
|
|||
public void testCommittingOneFile() throws Exception {
|
||||
SimpleCache simpleCache = getSimpleCache();
|
||||
|
||||
CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0);
|
||||
assertFalse(cacheSpan.isCached);
|
||||
assertTrue(cacheSpan.isOpenEnded());
|
||||
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
|
||||
assertFalse(cacheSpan1.isCached);
|
||||
assertTrue(cacheSpan1.isOpenEnded());
|
||||
|
||||
assertNull(simpleCache.startReadWriteNonBlocking(KEY_1, 0));
|
||||
|
||||
|
|
@ -58,20 +62,33 @@ public class SimpleCacheTest extends InstrumentationTestCase {
|
|||
assertEquals(0, simpleCache.getCacheSpace());
|
||||
assertEquals(0, cacheDir.listFiles().length);
|
||||
|
||||
addCache(simpleCache, 0, 15);
|
||||
addCache(simpleCache, KEY_1, 0, 15);
|
||||
|
||||
Set<String> cachedKeys = simpleCache.getKeys();
|
||||
assertEquals(1, cachedKeys.size());
|
||||
assertTrue(cachedKeys.contains(KEY_1));
|
||||
cachedSpans = simpleCache.getCachedSpans(KEY_1);
|
||||
assertEquals(1, cachedSpans.size());
|
||||
assertTrue(cachedSpans.contains(cacheSpan));
|
||||
assertTrue(cachedSpans.contains(cacheSpan1));
|
||||
assertEquals(15, simpleCache.getCacheSpace());
|
||||
|
||||
cacheSpan = simpleCache.startReadWrite(KEY_1, 0);
|
||||
assertTrue(cacheSpan.isCached);
|
||||
assertFalse(cacheSpan.isOpenEnded());
|
||||
assertEquals(15, cacheSpan.length);
|
||||
simpleCache.releaseHoleSpan(cacheSpan1);
|
||||
|
||||
CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);
|
||||
assertTrue(cacheSpan2.isCached);
|
||||
assertFalse(cacheSpan2.isOpenEnded());
|
||||
assertEquals(15, cacheSpan2.length);
|
||||
assertCachedDataReadCorrect(cacheSpan2);
|
||||
}
|
||||
|
||||
public void testReadCacheWithoutReleasingWriteCacheSpan() throws Exception {
|
||||
SimpleCache simpleCache = getSimpleCache();
|
||||
|
||||
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
|
||||
addCache(simpleCache, KEY_1, 0, 15);
|
||||
CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);
|
||||
assertCachedDataReadCorrect(cacheSpan2);
|
||||
simpleCache.releaseHoleSpan(cacheSpan1);
|
||||
}
|
||||
|
||||
public void testSetGetLength() throws Exception {
|
||||
|
|
@ -83,12 +100,12 @@ public class SimpleCacheTest extends InstrumentationTestCase {
|
|||
|
||||
simpleCache.startReadWrite(KEY_1, 0);
|
||||
|
||||
addCache(simpleCache, 0, 15);
|
||||
addCache(simpleCache, KEY_1, 0, 15);
|
||||
|
||||
simpleCache.setContentLength(KEY_1, 150);
|
||||
assertEquals(150, simpleCache.getContentLength(KEY_1));
|
||||
|
||||
addCache(simpleCache, 140, 10);
|
||||
addCache(simpleCache, KEY_1, 140, 10);
|
||||
|
||||
// Check if values are kept after cache is reloaded.
|
||||
SimpleCache simpleCache2 = getSimpleCache();
|
||||
|
|
@ -107,16 +124,109 @@ public class SimpleCacheTest extends InstrumentationTestCase {
|
|||
assertEquals(150, simpleCache2.getContentLength(KEY_1));
|
||||
}
|
||||
|
||||
public void testReloadCache() throws Exception {
|
||||
SimpleCache simpleCache = getSimpleCache();
|
||||
|
||||
// write data
|
||||
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
|
||||
addCache(simpleCache, KEY_1, 0, 15);
|
||||
simpleCache.releaseHoleSpan(cacheSpan1);
|
||||
|
||||
// Reload cache
|
||||
simpleCache = getSimpleCache();
|
||||
|
||||
// read data back
|
||||
CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);
|
||||
assertCachedDataReadCorrect(cacheSpan2);
|
||||
}
|
||||
|
||||
public void testEncryptedIndex() throws Exception {
|
||||
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
|
||||
SimpleCache simpleCache = getEncryptedSimpleCache(key);
|
||||
|
||||
// write data
|
||||
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
|
||||
addCache(simpleCache, KEY_1, 0, 15);
|
||||
simpleCache.releaseHoleSpan(cacheSpan1);
|
||||
|
||||
// Reload cache
|
||||
simpleCache = getEncryptedSimpleCache(key);
|
||||
|
||||
// read data back
|
||||
CacheSpan cacheSpan2 = simpleCache.startReadWrite(KEY_1, 0);
|
||||
assertCachedDataReadCorrect(cacheSpan2);
|
||||
}
|
||||
|
||||
public void testEncryptedIndexWrongKey() throws Exception {
|
||||
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
|
||||
SimpleCache simpleCache = getEncryptedSimpleCache(key);
|
||||
|
||||
// write data
|
||||
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
|
||||
addCache(simpleCache, KEY_1, 0, 15);
|
||||
simpleCache.releaseHoleSpan(cacheSpan1);
|
||||
|
||||
// Reload cache
|
||||
byte[] key2 = "Foo12345Foo12345".getBytes(C.UTF8_NAME); // 128 bit key
|
||||
simpleCache = getEncryptedSimpleCache(key2);
|
||||
|
||||
// Cache should be cleared
|
||||
assertEquals(0, simpleCache.getKeys().size());
|
||||
assertEquals(0, cacheDir.listFiles().length);
|
||||
}
|
||||
|
||||
public void testEncryptedIndexLostKey() throws Exception {
|
||||
byte[] key = "Bar12345Bar12345".getBytes(C.UTF8_NAME); // 128 bit key
|
||||
SimpleCache simpleCache = getEncryptedSimpleCache(key);
|
||||
|
||||
// write data
|
||||
CacheSpan cacheSpan1 = simpleCache.startReadWrite(KEY_1, 0);
|
||||
addCache(simpleCache, KEY_1, 0, 15);
|
||||
simpleCache.releaseHoleSpan(cacheSpan1);
|
||||
|
||||
// Reload cache
|
||||
simpleCache = getSimpleCache();
|
||||
|
||||
// Cache should be cleared
|
||||
assertEquals(0, simpleCache.getKeys().size());
|
||||
assertEquals(0, cacheDir.listFiles().length);
|
||||
}
|
||||
|
||||
private SimpleCache getSimpleCache() {
|
||||
return new SimpleCache(cacheDir, new NoOpCacheEvictor());
|
||||
}
|
||||
|
||||
private void addCache(SimpleCache simpleCache, int position, int length) throws IOException {
|
||||
File file = simpleCache.startFile(KEY_1, position, length);
|
||||
private SimpleCache getEncryptedSimpleCache(byte[] secretKey) {
|
||||
return new SimpleCache(cacheDir, new NoOpCacheEvictor(), secretKey);
|
||||
}
|
||||
|
||||
private static void addCache(SimpleCache simpleCache, String key, int position, int length)
|
||||
throws IOException {
|
||||
File file = simpleCache.startFile(key, position, length);
|
||||
FileOutputStream fos = new FileOutputStream(file);
|
||||
fos.write(new byte[length]);
|
||||
fos.close();
|
||||
try {
|
||||
fos.write(generateData(key, position, length));
|
||||
} finally {
|
||||
fos.close();
|
||||
}
|
||||
simpleCache.commitFile(file);
|
||||
}
|
||||
|
||||
private static void assertCachedDataReadCorrect(CacheSpan cacheSpan) throws IOException {
|
||||
assertTrue(cacheSpan.isCached);
|
||||
byte[] expected = generateData(cacheSpan.key, (int) cacheSpan.position, (int) cacheSpan.length);
|
||||
FileInputStream inputStream = new FileInputStream(cacheSpan.file);
|
||||
try {
|
||||
MoreAsserts.assertEquals(expected, Util.toByteArray(inputStream));
|
||||
} finally {
|
||||
inputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] generateData(String key, int position, int length) {
|
||||
byte[] bytes = new byte[length];
|
||||
new Random((long) (key.hashCode() ^ position)).nextBytes(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.upstream.crypto;
|
||||
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.Random;
|
||||
import javax.crypto.Cipher;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link AesFlushingCipher}.
|
||||
*/
|
||||
public class AesFlushingCipherTest extends TestCase {
|
||||
|
||||
private static final int DATA_LENGTH = 65536;
|
||||
private static final byte[] KEY = Util.getUtf8Bytes("testKey:12345678");
|
||||
private static final long NONCE = 0;
|
||||
private static final long START_OFFSET = 11;
|
||||
private static final long RANDOM_SEED = 0x12345678;
|
||||
|
||||
private AesFlushingCipher encryptCipher;
|
||||
private AesFlushingCipher decryptCipher;
|
||||
|
||||
@Override
|
||||
protected void setUp() {
|
||||
encryptCipher = new AesFlushingCipher(Cipher.ENCRYPT_MODE, KEY, NONCE, START_OFFSET);
|
||||
decryptCipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, KEY, NONCE, START_OFFSET);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() {
|
||||
encryptCipher = null;
|
||||
decryptCipher = null;
|
||||
}
|
||||
|
||||
private long getMaxUnchangedBytesAllowedPostEncryption(long length) {
|
||||
// Assuming that not more than 10% of the resultant bytes should be identical.
|
||||
// The value of 10% is arbitrary, ciphers standards do not name a value.
|
||||
return length / 10;
|
||||
}
|
||||
|
||||
// Count the number of bytes that do not match.
|
||||
private int getDifferingByteCount(byte[] data1, byte[] data2, int startOffset) {
|
||||
int count = 0;
|
||||
for (int i = startOffset; i < data1.length; i++) {
|
||||
if (data1[i] != data2[i]) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Count the number of bytes that do not match.
|
||||
private int getDifferingByteCount(byte[] data1, byte[] data2) {
|
||||
return getDifferingByteCount(data1, data2, 0);
|
||||
}
|
||||
|
||||
// Test a single encrypt and decrypt call
|
||||
public void testSingle() {
|
||||
byte[] reference = TestUtil.buildTestData(DATA_LENGTH);
|
||||
byte[] data = reference.clone();
|
||||
|
||||
encryptCipher.updateInPlace(data, 0, data.length);
|
||||
int unchangedByteCount = data.length - getDifferingByteCount(reference, data);
|
||||
assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length));
|
||||
|
||||
decryptCipher.updateInPlace(data, 0, data.length);
|
||||
int differingByteCount = getDifferingByteCount(reference, data);
|
||||
assertEquals(0, differingByteCount);
|
||||
}
|
||||
|
||||
// Test several encrypt and decrypt calls, each aligned on a 16 byte block size
|
||||
public void testAligned() {
|
||||
byte[] reference = TestUtil.buildTestData(DATA_LENGTH);
|
||||
byte[] data = reference.clone();
|
||||
Random random = new Random(RANDOM_SEED);
|
||||
|
||||
int offset = 0;
|
||||
while (offset < data.length) {
|
||||
int bytes = (1 + random.nextInt(50)) * 16;
|
||||
bytes = Math.min(bytes, data.length - offset);
|
||||
assertEquals(0, bytes % 16);
|
||||
encryptCipher.updateInPlace(data, offset, bytes);
|
||||
offset += bytes;
|
||||
}
|
||||
|
||||
int unchangedByteCount = data.length - getDifferingByteCount(reference, data);
|
||||
assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length));
|
||||
|
||||
offset = 0;
|
||||
while (offset < data.length) {
|
||||
int bytes = (1 + random.nextInt(50)) * 16;
|
||||
bytes = Math.min(bytes, data.length - offset);
|
||||
assertEquals(0, bytes % 16);
|
||||
decryptCipher.updateInPlace(data, offset, bytes);
|
||||
offset += bytes;
|
||||
}
|
||||
|
||||
int differingByteCount = getDifferingByteCount(reference, data);
|
||||
assertEquals(0, differingByteCount);
|
||||
}
|
||||
|
||||
// Test several encrypt and decrypt calls, not aligned on block boundary
|
||||
public void testUnAligned() {
|
||||
byte[] reference = TestUtil.buildTestData(DATA_LENGTH);
|
||||
byte[] data = reference.clone();
|
||||
Random random = new Random(RANDOM_SEED);
|
||||
|
||||
// Encrypt
|
||||
int offset = 0;
|
||||
while (offset < data.length) {
|
||||
int bytes = 1 + random.nextInt(4095);
|
||||
bytes = Math.min(bytes, data.length - offset);
|
||||
encryptCipher.updateInPlace(data, offset, bytes);
|
||||
offset += bytes;
|
||||
}
|
||||
|
||||
int unchangedByteCount = data.length - getDifferingByteCount(reference, data);
|
||||
assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length));
|
||||
|
||||
offset = 0;
|
||||
while (offset < data.length) {
|
||||
int bytes = 1 + random.nextInt(4095);
|
||||
bytes = Math.min(bytes, data.length - offset);
|
||||
decryptCipher.updateInPlace(data, offset, bytes);
|
||||
offset += bytes;
|
||||
}
|
||||
|
||||
int differingByteCount = getDifferingByteCount(reference, data);
|
||||
assertEquals(0, differingByteCount);
|
||||
}
|
||||
|
||||
// Test decryption starting from the middle of an encrypted block
|
||||
public void testMidJoin() {
|
||||
byte[] reference = TestUtil.buildTestData(DATA_LENGTH);
|
||||
byte[] data = reference.clone();
|
||||
Random random = new Random(RANDOM_SEED);
|
||||
|
||||
// Encrypt
|
||||
int offset = 0;
|
||||
while (offset < data.length) {
|
||||
int bytes = 1 + random.nextInt(4095);
|
||||
bytes = Math.min(bytes, data.length - offset);
|
||||
encryptCipher.updateInPlace(data, offset, bytes);
|
||||
offset += bytes;
|
||||
}
|
||||
|
||||
// Verify
|
||||
int unchangedByteCount = data.length - getDifferingByteCount(reference, data);
|
||||
assertTrue(unchangedByteCount <= getMaxUnchangedBytesAllowedPostEncryption(data.length));
|
||||
|
||||
// Setup decryption from random location
|
||||
offset = random.nextInt(4096);
|
||||
decryptCipher = new AesFlushingCipher(Cipher.DECRYPT_MODE, KEY, NONCE, offset + START_OFFSET);
|
||||
int remainingLength = data.length - offset;
|
||||
int originalOffset = offset;
|
||||
|
||||
// Decrypt
|
||||
while (remainingLength > 0) {
|
||||
int bytes = 1 + random.nextInt(4095);
|
||||
bytes = Math.min(bytes, remainingLength);
|
||||
decryptCipher.updateInPlace(data, offset, bytes);
|
||||
offset += bytes;
|
||||
remainingLength -= bytes;
|
||||
}
|
||||
|
||||
// Verify
|
||||
int differingByteCount = getDifferingByteCount(reference, data, originalOffset);
|
||||
assertEquals(0, differingByteCount);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -371,6 +371,73 @@ public class ParsableByteArrayTest extends TestCase {
|
|||
assertNull(parser.readLine());
|
||||
}
|
||||
|
||||
public void testReadNullTerminatedStringWithLengths() {
|
||||
byte[] bytes = new byte[] {
|
||||
'f', 'o', 'o', 0, 'b', 'a', 'r', 0
|
||||
};
|
||||
// Test with lengths that match NUL byte positions.
|
||||
ParsableByteArray parser = new ParsableByteArray(bytes);
|
||||
assertEquals("foo", parser.readNullTerminatedString(4));
|
||||
assertEquals(4, parser.getPosition());
|
||||
assertEquals("bar", parser.readNullTerminatedString(4));
|
||||
assertEquals(8, parser.getPosition());
|
||||
assertNull(parser.readNullTerminatedString());
|
||||
// Test with lengths that do not match NUL byte positions.
|
||||
parser = new ParsableByteArray(bytes);
|
||||
assertEquals("fo", parser.readNullTerminatedString(2));
|
||||
assertEquals(2, parser.getPosition());
|
||||
assertEquals("o", parser.readNullTerminatedString(2));
|
||||
assertEquals(4, parser.getPosition());
|
||||
assertEquals("bar", parser.readNullTerminatedString(3));
|
||||
assertEquals(7, parser.getPosition());
|
||||
assertEquals("", parser.readNullTerminatedString(1));
|
||||
assertEquals(8, parser.getPosition());
|
||||
assertNull(parser.readNullTerminatedString());
|
||||
// Test with limit at NUL
|
||||
parser = new ParsableByteArray(bytes, 4);
|
||||
assertEquals("foo", parser.readNullTerminatedString(4));
|
||||
assertEquals(4, parser.getPosition());
|
||||
assertNull(parser.readNullTerminatedString());
|
||||
// Test with limit before NUL
|
||||
parser = new ParsableByteArray(bytes, 3);
|
||||
assertEquals("foo", parser.readNullTerminatedString(3));
|
||||
assertEquals(3, parser.getPosition());
|
||||
assertNull(parser.readNullTerminatedString());
|
||||
}
|
||||
|
||||
public void testReadNullTerminatedString() {
|
||||
byte[] bytes = new byte[] {
|
||||
'f', 'o', 'o', 0, 'b', 'a', 'r', 0
|
||||
};
|
||||
// Test normal case.
|
||||
ParsableByteArray parser = new ParsableByteArray(bytes);
|
||||
assertEquals("foo", parser.readNullTerminatedString());
|
||||
assertEquals(4, parser.getPosition());
|
||||
assertEquals("bar", parser.readNullTerminatedString());
|
||||
assertEquals(8, parser.getPosition());
|
||||
assertNull(parser.readNullTerminatedString());
|
||||
// Test with limit at NUL.
|
||||
parser = new ParsableByteArray(bytes, 4);
|
||||
assertEquals("foo", parser.readNullTerminatedString());
|
||||
assertEquals(4, parser.getPosition());
|
||||
assertNull(parser.readNullTerminatedString());
|
||||
// Test with limit before NUL.
|
||||
parser = new ParsableByteArray(bytes, 3);
|
||||
assertEquals("foo", parser.readNullTerminatedString());
|
||||
assertEquals(3, parser.getPosition());
|
||||
assertNull(parser.readNullTerminatedString());
|
||||
}
|
||||
|
||||
public void testReadNullTerminatedStringWithoutEndingNull() {
|
||||
byte[] bytes = new byte[] {
|
||||
'f', 'o', 'o', 0, 'b', 'a', 'r'
|
||||
};
|
||||
ParsableByteArray parser = new ParsableByteArray(bytes);
|
||||
assertEquals("foo", parser.readNullTerminatedString());
|
||||
assertEquals("bar", parser.readNullTerminatedString());
|
||||
assertNull(parser.readNullTerminatedString());
|
||||
}
|
||||
|
||||
public void testReadSingleLineWithoutEndingTrail() {
|
||||
byte[] bytes = new byte[] {
|
||||
'f', 'o', 'o'
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
|||
|
||||
private final int trackType;
|
||||
|
||||
private RendererConfiguration configuration;
|
||||
private int index;
|
||||
private int state;
|
||||
private SampleStream stream;
|
||||
|
|
@ -70,9 +71,11 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
|||
}
|
||||
|
||||
@Override
|
||||
public final void enable(Format[] formats, SampleStream stream, long positionUs, boolean joining,
|
||||
long offsetUs) throws ExoPlaybackException {
|
||||
public final void enable(RendererConfiguration configuration, Format[] formats,
|
||||
SampleStream stream, long positionUs, boolean joining, long offsetUs)
|
||||
throws ExoPlaybackException {
|
||||
Assertions.checkState(state == STATE_DISABLED);
|
||||
this.configuration = configuration;
|
||||
state = STATE_ENABLED;
|
||||
onEnabled(joining);
|
||||
replaceStream(formats, stream, offsetUs);
|
||||
|
|
@ -237,10 +240,15 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
|||
|
||||
// Methods to be called by subclasses.
|
||||
|
||||
/**
|
||||
* Returns the configuration set when the renderer was most recently enabled.
|
||||
*/
|
||||
protected final RendererConfiguration getConfiguration() {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the renderer within the player.
|
||||
*
|
||||
* @return The index of the renderer within the player.
|
||||
*/
|
||||
protected final int getIndex() {
|
||||
return index;
|
||||
|
|
@ -251,11 +259,11 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
|||
* {@link C#RESULT_BUFFER_READ} is only returned if {@link #setCurrentStreamFinal()} has been
|
||||
* called. {@link C#RESULT_NOTHING_READ} is returned otherwise.
|
||||
*
|
||||
* @see SampleStream#readData(FormatHolder, DecoderInputBuffer)
|
||||
* @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
|
||||
* @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
|
||||
* end of the stream. If the end of the stream has been reached, the
|
||||
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.
|
||||
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. May be null if the
|
||||
* caller requires that the format of the stream be read even if it's not changing.
|
||||
* @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
|
||||
* {@link C#RESULT_BUFFER_READ}.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaCodec;
|
||||
|
|
@ -96,6 +98,13 @@ public final class C {
|
|||
@SuppressWarnings("InlinedApi")
|
||||
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.
|
||||
*/
|
||||
|
|
@ -543,4 +552,13 @@ public final class C {
|
|||
return timeMs == TIME_UNSET ? TIME_UNSET : (timeMs * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a newly generated {@link android.media.AudioTrack} session identifier.
|
||||
*/
|
||||
@TargetApi(21)
|
||||
public static int generateAudioSessionIdV21(Context context) {
|
||||
return ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE))
|
||||
.generateAudioSessionId();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import com.google.android.exoplayer2.source.TrackGroupArray;
|
|||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.DefaultAllocator;
|
||||
import com.google.android.exoplayer2.util.PriorityTaskManager;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/**
|
||||
|
|
@ -50,6 +51,11 @@ public final class DefaultLoadControl implements LoadControl {
|
|||
*/
|
||||
public static final int DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 5000;
|
||||
|
||||
/**
|
||||
* Priority for media loading.
|
||||
*/
|
||||
public static final int LOADING_PRIORITY = 0;
|
||||
|
||||
private static final int ABOVE_HIGH_WATERMARK = 0;
|
||||
private static final int BETWEEN_WATERMARKS = 1;
|
||||
private static final int BELOW_LOW_WATERMARK = 2;
|
||||
|
|
@ -60,6 +66,7 @@ public final class DefaultLoadControl implements LoadControl {
|
|||
private final long maxBufferUs;
|
||||
private final long bufferForPlaybackUs;
|
||||
private final long bufferForPlaybackAfterRebufferUs;
|
||||
private final PriorityTaskManager priorityTaskManager;
|
||||
|
||||
private int targetBufferSize;
|
||||
private boolean isBuffering;
|
||||
|
|
@ -97,11 +104,36 @@ public final class DefaultLoadControl implements LoadControl {
|
|||
*/
|
||||
public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
|
||||
long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs) {
|
||||
this(allocator, minBufferMs, maxBufferMs, bufferForPlaybackMs, bufferForPlaybackAfterRebufferMs,
|
||||
null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param allocator The {@link DefaultAllocator} used by the loader.
|
||||
* @param minBufferMs The minimum duration of media that the player will attempt to ensure is
|
||||
* buffered at all times, in milliseconds.
|
||||
* @param maxBufferMs The maximum duration of media that the player will attempt buffer, in
|
||||
* milliseconds.
|
||||
* @param bufferForPlaybackMs The duration of media that must be buffered for playback to start or
|
||||
* resume following a user action such as a seek, in milliseconds.
|
||||
* @param bufferForPlaybackAfterRebufferMs The default duration of media that must be buffered for
|
||||
* playback to resume after a rebuffer, in milliseconds. A rebuffer is defined to be caused by
|
||||
* buffer depletion rather than a user action.
|
||||
* @param priorityTaskManager If not null, registers itself as a task with priority
|
||||
* {@link #LOADING_PRIORITY} during loading periods, and unregisters itself during draining
|
||||
* periods.
|
||||
*/
|
||||
public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
|
||||
long bufferForPlaybackMs, long bufferForPlaybackAfterRebufferMs,
|
||||
PriorityTaskManager priorityTaskManager) {
|
||||
this.allocator = allocator;
|
||||
minBufferUs = minBufferMs * 1000L;
|
||||
maxBufferUs = maxBufferMs * 1000L;
|
||||
bufferForPlaybackUs = bufferForPlaybackMs * 1000L;
|
||||
bufferForPlaybackAfterRebufferUs = bufferForPlaybackAfterRebufferMs * 1000L;
|
||||
this.priorityTaskManager = priorityTaskManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -146,8 +178,16 @@ public final class DefaultLoadControl implements LoadControl {
|
|||
public boolean shouldContinueLoading(long bufferedDurationUs) {
|
||||
int bufferTimeState = getBufferTimeState(bufferedDurationUs);
|
||||
boolean targetBufferSizeReached = allocator.getTotalBytesAllocated() >= targetBufferSize;
|
||||
boolean wasBuffering = isBuffering;
|
||||
isBuffering = bufferTimeState == BELOW_LOW_WATERMARK
|
||||
|| (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached);
|
||||
if (priorityTaskManager != null && isBuffering != wasBuffering) {
|
||||
if (isBuffering) {
|
||||
priorityTaskManager.add(LOADING_PRIORITY);
|
||||
} else {
|
||||
priorityTaskManager.remove(LOADING_PRIORITY);
|
||||
}
|
||||
}
|
||||
return isBuffering;
|
||||
}
|
||||
|
||||
|
|
@ -158,6 +198,9 @@ public final class DefaultLoadControl implements LoadControl {
|
|||
|
||||
private void reset(boolean resetAllocator) {
|
||||
targetBufferSize = 0;
|
||||
if (priorityTaskManager != null && isBuffering) {
|
||||
priorityTaskManager.remove(LOADING_PRIORITY);
|
||||
}
|
||||
isBuffering = false;
|
||||
if (resetAllocator) {
|
||||
allocator.reset();
|
||||
|
|
|
|||
|
|
@ -447,4 +447,20 @@ public interface ExoPlayer {
|
|||
*/
|
||||
int getBufferedPercentage();
|
||||
|
||||
/**
|
||||
* Returns whether the current window is dynamic, or {@code false} if the {@link Timeline} is
|
||||
* empty.
|
||||
*
|
||||
* @see Timeline.Window#isDynamic
|
||||
*/
|
||||
boolean isCurrentWindowDynamic();
|
||||
|
||||
/**
|
||||
* Returns whether the current window is seekable, or {@code false} if the {@link Timeline} is
|
||||
* empty.
|
||||
*
|
||||
* @see Timeline.Window#isSeekable
|
||||
*/
|
||||
boolean isCurrentWindowSeekable();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,12 +22,12 @@ import android.os.Message;
|
|||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.ExoPlayerImplInternal.PlaybackInfo;
|
||||
import com.google.android.exoplayer2.ExoPlayerImplInternal.SourceInfo;
|
||||
import com.google.android.exoplayer2.ExoPlayerImplInternal.TrackInfo;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
|
@ -271,6 +271,22 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
: (int) (duration == 0 ? 100 : (bufferedPosition * 100) / duration);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCurrentWindowDynamic() {
|
||||
if (timeline.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return timeline.getWindow(getCurrentWindowIndex(), window).isDynamic;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCurrentWindowSeekable() {
|
||||
if (timeline.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return timeline.getWindow(getCurrentWindowIndex(), window).isSeekable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRendererCount() {
|
||||
return renderers.length;
|
||||
|
|
@ -319,11 +335,11 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
break;
|
||||
}
|
||||
case ExoPlayerImplInternal.MSG_TRACKS_CHANGED: {
|
||||
TrackInfo trackInfo = (TrackInfo) msg.obj;
|
||||
TrackSelectorResult trackSelectorResult = (TrackSelectorResult) msg.obj;
|
||||
tracksSelected = true;
|
||||
trackGroups = trackInfo.groups;
|
||||
trackSelections = trackInfo.selections;
|
||||
trackSelector.onSelectionActivated(trackInfo.info);
|
||||
trackGroups = trackSelectorResult.groups;
|
||||
trackSelections = trackSelectorResult.selections;
|
||||
trackSelector.onSelectionActivated(trackSelectorResult.info);
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onTracksChanged(trackGroups, trackSelections);
|
||||
}
|
||||
|
|
@ -332,8 +348,10 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
case ExoPlayerImplInternal.MSG_SEEK_ACK: {
|
||||
if (--pendingSeekAcks == 0) {
|
||||
playbackInfo = (ExoPlayerImplInternal.PlaybackInfo) msg.obj;
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onPositionDiscontinuity();
|
||||
if (msg.arg1 != 0) {
|
||||
for (EventListener listener : listeners) {
|
||||
listener.onPositionDiscontinuity();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -26,16 +26,15 @@ import com.google.android.exoplayer2.ExoPlayer.ExoPlayerMessage;
|
|||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.SampleStream;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MediaClock;
|
||||
import com.google.android.exoplayer2.util.PriorityHandlerThread;
|
||||
import com.google.android.exoplayer2.util.StandaloneMediaClock;
|
||||
import com.google.android.exoplayer2.util.TraceUtil;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
|
|
@ -72,20 +71,6 @@ import java.io.IOException;
|
|||
|
||||
}
|
||||
|
||||
public static final class TrackInfo {
|
||||
|
||||
public final TrackGroupArray groups;
|
||||
public final TrackSelectionArray selections;
|
||||
public final Object info;
|
||||
|
||||
public TrackInfo(TrackGroupArray groups, TrackSelectionArray selections, Object info) {
|
||||
this.groups = groups;
|
||||
this.selections = selections;
|
||||
this.info = info;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static final class SourceInfo {
|
||||
|
||||
public final Timeline timeline;
|
||||
|
|
@ -559,7 +544,7 @@ import java.io.IOException;
|
|||
// The seek position was valid for the timeline that it was performed into, but the
|
||||
// timeline has changed and a suitable seek position could not be resolved in the new one.
|
||||
playbackInfo = new PlaybackInfo(0, 0);
|
||||
eventHandler.obtainMessage(MSG_SEEK_ACK, playbackInfo).sendToTarget();
|
||||
eventHandler.obtainMessage(MSG_SEEK_ACK, 1, 0, playbackInfo).sendToTarget();
|
||||
// Set the internal position to (0,TIME_UNSET) so that a subsequent seek to (0,0) isn't
|
||||
// ignored.
|
||||
playbackInfo = new PlaybackInfo(0, C.TIME_UNSET);
|
||||
|
|
@ -569,6 +554,7 @@ import java.io.IOException;
|
|||
return;
|
||||
}
|
||||
|
||||
boolean seekPositionAdjusted = seekPosition.windowPositionUs == C.TIME_UNSET;
|
||||
int periodIndex = periodPosition.first;
|
||||
long periodPositionUs = periodPosition.second;
|
||||
|
||||
|
|
@ -578,10 +564,13 @@ import java.io.IOException;
|
|||
// Seek position equals the current position. Do nothing.
|
||||
return;
|
||||
}
|
||||
periodPositionUs = seekToPeriodPosition(periodIndex, periodPositionUs);
|
||||
long newPeriodPositionUs = seekToPeriodPosition(periodIndex, periodPositionUs);
|
||||
seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs;
|
||||
periodPositionUs = newPeriodPositionUs;
|
||||
} finally {
|
||||
playbackInfo = new PlaybackInfo(periodIndex, periodPositionUs);
|
||||
eventHandler.obtainMessage(MSG_SEEK_ACK, playbackInfo).sendToTarget();
|
||||
eventHandler.obtainMessage(MSG_SEEK_ACK, seekPositionAdjusted ? 1 : 0, 0, playbackInfo)
|
||||
.sendToTarget();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -620,6 +609,7 @@ import java.io.IOException;
|
|||
enabledRenderers = new Renderer[0];
|
||||
rendererMediaClock = null;
|
||||
rendererMediaClockSource = null;
|
||||
playingPeriodHolder = null;
|
||||
}
|
||||
|
||||
// Update the holders.
|
||||
|
|
@ -795,7 +785,8 @@ import java.io.IOException;
|
|||
}
|
||||
}
|
||||
}
|
||||
eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.getTrackInfo()).sendToTarget();
|
||||
eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.trackSelectorResult)
|
||||
.sendToTarget();
|
||||
enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
|
||||
} else {
|
||||
// Release and re-prepare/buffer periods after the one whose selection changed.
|
||||
|
|
@ -1134,33 +1125,38 @@ import java.io.IOException;
|
|||
}
|
||||
|
||||
if (readingPeriodHolder.isLast) {
|
||||
for (Renderer renderer : enabledRenderers) {
|
||||
for (int i = 0; i < renderers.length; i++) {
|
||||
Renderer renderer = renderers[i];
|
||||
SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
|
||||
// Defer setting the stream as final until the renderer has actually consumed the whole
|
||||
// stream in case of playlist changes that cause the stream to be no longer final.
|
||||
if (renderer.hasReadStreamToEnd()) {
|
||||
if (sampleStream != null && renderer.getStream() == sampleStream
|
||||
&& renderer.hasReadStreamToEnd()) {
|
||||
renderer.setCurrentStreamFinal();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (Renderer renderer : enabledRenderers) {
|
||||
if (!renderer.hasReadStreamToEnd()) {
|
||||
for (int i = 0; i < renderers.length; i++) {
|
||||
Renderer renderer = renderers[i];
|
||||
SampleStream sampleStream = readingPeriodHolder.sampleStreams[i];
|
||||
if (renderer.getStream() != sampleStream
|
||||
|| (sampleStream != null && !renderer.hasReadStreamToEnd())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (readingPeriodHolder.next != null && readingPeriodHolder.next.prepared) {
|
||||
TrackSelectionArray oldTrackSelections = readingPeriodHolder.trackSelections;
|
||||
TrackSelectorResult oldTrackSelectorResult = readingPeriodHolder.trackSelectorResult;
|
||||
readingPeriodHolder = readingPeriodHolder.next;
|
||||
TrackSelectionArray newTrackSelections = readingPeriodHolder.trackSelections;
|
||||
TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.trackSelectorResult;
|
||||
|
||||
boolean initialDiscontinuity =
|
||||
readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET;
|
||||
for (int i = 0; i < renderers.length; i++) {
|
||||
Renderer renderer = renderers[i];
|
||||
TrackSelection oldSelection = oldTrackSelections.get(i);
|
||||
TrackSelection newSelection = newTrackSelections.get(i);
|
||||
TrackSelection oldSelection = oldTrackSelectorResult.selections.get(i);
|
||||
if (oldSelection == null) {
|
||||
// The renderer has no current stream and will be enabled when we play the next period.
|
||||
} else if (initialDiscontinuity) {
|
||||
|
|
@ -1168,9 +1164,12 @@ import java.io.IOException;
|
|||
// be disabled and re-enabled when it starts playing the next period.
|
||||
renderer.setCurrentStreamFinal();
|
||||
} else if (!renderer.isCurrentStreamFinal()) {
|
||||
if (newSelection != null) {
|
||||
// Replace the renderer's SampleStream so the transition to playing the next period
|
||||
// can be seamless.
|
||||
TrackSelection newSelection = newTrackSelectorResult.selections.get(i);
|
||||
RendererConfiguration oldConfig = oldTrackSelectorResult.rendererConfigurations[i];
|
||||
RendererConfiguration newConfig = newTrackSelectorResult.rendererConfigurations[i];
|
||||
if (newSelection != null && newConfig.equals(oldConfig)) {
|
||||
// Replace the renderer's SampleStream so the transition to playing the next period can
|
||||
// be seamless.
|
||||
Format[] formats = new Format[newSelection.length()];
|
||||
for (int j = 0; j < formats.length; j++) {
|
||||
formats[j] = newSelection.getFormat(j);
|
||||
|
|
@ -1178,8 +1177,9 @@ import java.io.IOException;
|
|||
renderer.replaceStream(formats, readingPeriodHolder.sampleStreams[i],
|
||||
readingPeriodHolder.getRendererOffset());
|
||||
} else {
|
||||
// The renderer will be disabled when transitioning to playing the next period. Mark the
|
||||
// SampleStream as final to play out any remaining data.
|
||||
// The renderer will be disabled when transitioning to playing the next period, either
|
||||
// because there's no new selection or because a configuration change is required. Mark
|
||||
// the SampleStream as final to play out any remaining data.
|
||||
renderer.setCurrentStreamFinal();
|
||||
}
|
||||
}
|
||||
|
|
@ -1215,7 +1215,7 @@ import java.io.IOException;
|
|||
|
||||
long newLoadingPeriodStartPositionUs;
|
||||
if (loadingPeriodHolder == null) {
|
||||
newLoadingPeriodStartPositionUs = playbackInfo.startPositionUs;
|
||||
newLoadingPeriodStartPositionUs = playbackInfo.positionUs;
|
||||
} else {
|
||||
int newLoadingWindowIndex = timeline.getPeriod(newLoadingPeriodIndex, period).windowIndex;
|
||||
if (newLoadingPeriodIndex
|
||||
|
|
@ -1315,20 +1315,21 @@ import java.io.IOException;
|
|||
return;
|
||||
}
|
||||
|
||||
playingPeriodHolder = periodHolder;
|
||||
int enabledRendererCount = 0;
|
||||
boolean[] rendererWasEnabledFlags = new boolean[renderers.length];
|
||||
for (int i = 0; i < renderers.length; i++) {
|
||||
Renderer renderer = renderers[i];
|
||||
rendererWasEnabledFlags[i] = renderer.getState() != Renderer.STATE_DISABLED;
|
||||
TrackSelection newSelection = periodHolder.trackSelections.get(i);
|
||||
TrackSelection newSelection = periodHolder.trackSelectorResult.selections.get(i);
|
||||
if (newSelection != null) {
|
||||
enabledRendererCount++;
|
||||
}
|
||||
if (rendererWasEnabledFlags[i] && (newSelection == null || renderer.isCurrentStreamFinal())) {
|
||||
if (rendererWasEnabledFlags[i] && (newSelection == null
|
||||
|| (renderer.isCurrentStreamFinal()
|
||||
&& renderer.getStream() == playingPeriodHolder.sampleStreams[i]))) {
|
||||
// The renderer should be disabled before playing the next period, either because it's not
|
||||
// needed to play the next period, or because we need to disable and re-enable it because
|
||||
// the renderer thinks that its current stream is final.
|
||||
// needed to play the next period, or because we need to re-enable it as its current stream
|
||||
// is final and it's not reading ahead.
|
||||
if (renderer == rendererMediaClockSource) {
|
||||
// Sync standaloneMediaClock so that it can take over timing responsibilities.
|
||||
standaloneMediaClock.setPositionUs(rendererMediaClock.getPositionUs());
|
||||
|
|
@ -1340,7 +1341,8 @@ import java.io.IOException;
|
|||
}
|
||||
}
|
||||
|
||||
eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.getTrackInfo()).sendToTarget();
|
||||
playingPeriodHolder = periodHolder;
|
||||
eventHandler.obtainMessage(MSG_TRACKS_CHANGED, periodHolder.trackSelectorResult).sendToTarget();
|
||||
enableRenderers(rendererWasEnabledFlags, enabledRendererCount);
|
||||
}
|
||||
|
||||
|
|
@ -1350,10 +1352,12 @@ import java.io.IOException;
|
|||
enabledRendererCount = 0;
|
||||
for (int i = 0; i < renderers.length; i++) {
|
||||
Renderer renderer = renderers[i];
|
||||
TrackSelection newSelection = playingPeriodHolder.trackSelections.get(i);
|
||||
TrackSelection newSelection = playingPeriodHolder.trackSelectorResult.selections.get(i);
|
||||
if (newSelection != null) {
|
||||
enabledRenderers[enabledRendererCount++] = renderer;
|
||||
if (renderer.getState() == Renderer.STATE_DISABLED) {
|
||||
RendererConfiguration rendererConfiguration =
|
||||
playingPeriodHolder.trackSelectorResult.rendererConfigurations[i];
|
||||
// The renderer needs enabling with its new track selection.
|
||||
boolean playing = playWhenReady && state == ExoPlayer.STATE_READY;
|
||||
// Consider as joining only if the renderer was previously disabled.
|
||||
|
|
@ -1364,8 +1368,8 @@ import java.io.IOException;
|
|||
formats[j] = newSelection.getFormat(j);
|
||||
}
|
||||
// Enable the renderer.
|
||||
renderer.enable(formats, playingPeriodHolder.sampleStreams[i], rendererPositionUs,
|
||||
joining, playingPeriodHolder.getRendererOffset());
|
||||
renderer.enable(rendererConfiguration, formats, playingPeriodHolder.sampleStreams[i],
|
||||
rendererPositionUs, joining, playingPeriodHolder.getRendererOffset());
|
||||
MediaClock mediaClock = renderer.getMediaClock();
|
||||
if (mediaClock != null) {
|
||||
if (rendererMediaClock != null) {
|
||||
|
|
@ -1402,6 +1406,7 @@ import java.io.IOException;
|
|||
public boolean hasEnabledTracks;
|
||||
public MediaPeriodHolder next;
|
||||
public boolean needsContinueLoading;
|
||||
public TrackSelectorResult trackSelectorResult;
|
||||
|
||||
private final Renderer[] renderers;
|
||||
private final RendererCapabilities[] rendererCapabilities;
|
||||
|
|
@ -1409,10 +1414,7 @@ import java.io.IOException;
|
|||
private final LoadControl loadControl;
|
||||
private final MediaSource mediaSource;
|
||||
|
||||
private Object trackSelectionsInfo;
|
||||
private TrackGroupArray trackGroups;
|
||||
private TrackSelectionArray trackSelections;
|
||||
private TrackSelectionArray periodTrackSelections;
|
||||
private TrackSelectorResult periodTrackSelectorResult;
|
||||
|
||||
public MediaPeriodHolder(Renderer[] renderers, RendererCapabilities[] rendererCapabilities,
|
||||
long rendererPositionOffsetUs, TrackSelector trackSelector, LoadControl loadControl,
|
||||
|
|
@ -1458,20 +1460,17 @@ import java.io.IOException;
|
|||
|
||||
public void handlePrepared() throws ExoPlaybackException {
|
||||
prepared = true;
|
||||
trackGroups = mediaPeriod.getTrackGroups();
|
||||
selectTracks();
|
||||
startPositionUs = updatePeriodTrackSelection(startPositionUs, false);
|
||||
}
|
||||
|
||||
public boolean selectTracks() throws ExoPlaybackException {
|
||||
Pair<TrackSelectionArray, Object> selectorResult = trackSelector.selectTracks(
|
||||
rendererCapabilities, trackGroups);
|
||||
TrackSelectionArray newTrackSelections = selectorResult.first;
|
||||
if (newTrackSelections.equals(periodTrackSelections)) {
|
||||
TrackSelectorResult selectorResult = trackSelector.selectTracks(rendererCapabilities,
|
||||
mediaPeriod.getTrackGroups());
|
||||
if (selectorResult.isEquivalent(periodTrackSelectorResult)) {
|
||||
return false;
|
||||
}
|
||||
trackSelections = newTrackSelections;
|
||||
trackSelectionsInfo = selectorResult.second;
|
||||
trackSelectorResult = selectorResult;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1482,16 +1481,16 @@ import java.io.IOException;
|
|||
|
||||
public long updatePeriodTrackSelection(long positionUs, boolean forceRecreateStreams,
|
||||
boolean[] streamResetFlags) {
|
||||
TrackSelectionArray trackSelections = trackSelectorResult.selections;
|
||||
for (int i = 0; i < trackSelections.length; i++) {
|
||||
mayRetainStreamFlags[i] = !forceRecreateStreams
|
||||
&& Util.areEqual(periodTrackSelections == null ? null : periodTrackSelections.get(i),
|
||||
trackSelections.get(i));
|
||||
&& trackSelectorResult.isEquivalent(periodTrackSelectorResult, i);
|
||||
}
|
||||
|
||||
// Disable streams on the period and get new streams for updated/newly-enabled tracks.
|
||||
positionUs = mediaPeriod.selectTracks(trackSelections.getAll(), mayRetainStreamFlags,
|
||||
sampleStreams, streamResetFlags, positionUs);
|
||||
periodTrackSelections = trackSelections;
|
||||
periodTrackSelectorResult = trackSelectorResult;
|
||||
|
||||
// Update whether we have enabled tracks and sanity check the expected streams are non-null.
|
||||
hasEnabledTracks = false;
|
||||
|
|
@ -1505,14 +1504,10 @@ import java.io.IOException;
|
|||
}
|
||||
|
||||
// The track selection has changed.
|
||||
loadControl.onTracksSelected(renderers, trackGroups, trackSelections);
|
||||
loadControl.onTracksSelected(renderers, trackSelectorResult.groups, trackSelections);
|
||||
return positionUs;
|
||||
}
|
||||
|
||||
public TrackInfo getTrackInfo() {
|
||||
return new TrackInfo(trackGroups, trackSelections, trackSelectionsInfo);
|
||||
}
|
||||
|
||||
public void release() {
|
||||
try {
|
||||
mediaSource.releasePeriod(mediaPeriod);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo {
|
|||
/**
|
||||
* 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.
|
||||
|
|
@ -32,7 +32,7 @@ public interface ExoPlayerLibraryInfo {
|
|||
* corresponding integer version 1002003 (001-002-003), and "123.45.6" has the corresponding
|
||||
* integer version 123045006 (123-045-006).
|
||||
*/
|
||||
int VERSION_INT = 2001001;
|
||||
int VERSION_INT = 2002000;
|
||||
|
||||
/**
|
||||
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
|
||||
|
|
|
|||
|
|
@ -183,20 +183,18 @@ public final class Format implements Parcelable {
|
|||
*/
|
||||
public final int accessibilityChannel;
|
||||
|
||||
// Lazily initialized hashcode and framework media format.
|
||||
|
||||
// Lazily initialized hashcode.
|
||||
private int hashCode;
|
||||
private MediaFormat frameworkMediaFormat;
|
||||
|
||||
// Video.
|
||||
|
||||
public static Format createVideoContainerFormat(String id, String containerMimeType,
|
||||
String sampleMimeType, String codecs, int bitrate, int width, int height,
|
||||
float frameRate, List<byte[]> initializationData) {
|
||||
float frameRate, List<byte[]> initializationData, @C.SelectionFlags int selectionFlags) {
|
||||
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, width,
|
||||
height, frameRate, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
|
||||
NO_VALUE, NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, initializationData, null,
|
||||
null);
|
||||
NO_VALUE, NO_VALUE, selectionFlags, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE,
|
||||
initializationData, null, null);
|
||||
}
|
||||
|
||||
public static Format createVideoSampleFormat(String id, String sampleMimeType, String codecs,
|
||||
|
|
@ -289,8 +287,8 @@ public final class Format implements Parcelable {
|
|||
}
|
||||
|
||||
public static Format createTextSampleFormat(String id, String sampleMimeType, String codecs,
|
||||
int bitrate, @C.SelectionFlags int selectionFlags, String language,
|
||||
int accessibilityChannel, DrmInitData drmInitData) {
|
||||
int bitrate, @C.SelectionFlags int selectionFlags, String language, int accessibilityChannel,
|
||||
DrmInitData drmInitData) {
|
||||
return createTextSampleFormat(id, sampleMimeType, codecs, bitrate, selectionFlags, language,
|
||||
accessibilityChannel, drmInitData, OFFSET_SAMPLE_RELATIVE);
|
||||
}
|
||||
|
|
@ -323,11 +321,20 @@ public final class Format implements Parcelable {
|
|||
|
||||
// Generic.
|
||||
|
||||
public static Format createContainerFormat(String id, String containerMimeType, String codecs,
|
||||
String sampleMimeType, int bitrate) {
|
||||
public static Format createContainerFormat(String id, String containerMimeType,
|
||||
String sampleMimeType, String codecs, int bitrate, @C.SelectionFlags int selectionFlags,
|
||||
String language) {
|
||||
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, NO_VALUE, NO_VALUE,
|
||||
NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
|
||||
NO_VALUE, NO_VALUE, 0, null, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, null, null);
|
||||
NO_VALUE, NO_VALUE, selectionFlags, language, NO_VALUE, OFFSET_SAMPLE_RELATIVE, null, null,
|
||||
null);
|
||||
}
|
||||
|
||||
public static Format createSampleFormat(String id, String sampleMimeType,
|
||||
long subsampleOffsetUs) {
|
||||
return new Format(id, null, sampleMimeType, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
|
||||
NO_VALUE, NO_VALUE, NO_VALUE, null, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE, NO_VALUE,
|
||||
NO_VALUE, 0, null, NO_VALUE, subsampleOffsetUs, null, null, null);
|
||||
}
|
||||
|
||||
public static Format createSampleFormat(String id, String sampleMimeType, String codecs,
|
||||
|
|
@ -486,31 +493,28 @@ public final class Format implements Parcelable {
|
|||
@SuppressLint("InlinedApi")
|
||||
@TargetApi(16)
|
||||
public final MediaFormat getFrameworkMediaFormatV16() {
|
||||
if (frameworkMediaFormat == null) {
|
||||
MediaFormat format = new MediaFormat();
|
||||
format.setString(MediaFormat.KEY_MIME, sampleMimeType);
|
||||
maybeSetStringV16(format, MediaFormat.KEY_LANGUAGE, language);
|
||||
maybeSetIntegerV16(format, MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
|
||||
maybeSetIntegerV16(format, MediaFormat.KEY_WIDTH, width);
|
||||
maybeSetIntegerV16(format, MediaFormat.KEY_HEIGHT, height);
|
||||
maybeSetFloatV16(format, MediaFormat.KEY_FRAME_RATE, frameRate);
|
||||
maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees);
|
||||
maybeSetIntegerV16(format, MediaFormat.KEY_CHANNEL_COUNT, channelCount);
|
||||
maybeSetIntegerV16(format, MediaFormat.KEY_SAMPLE_RATE, sampleRate);
|
||||
maybeSetIntegerV16(format, "encoder-delay", encoderDelay);
|
||||
maybeSetIntegerV16(format, "encoder-padding", encoderPadding);
|
||||
for (int i = 0; i < initializationData.size(); i++) {
|
||||
format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
|
||||
}
|
||||
frameworkMediaFormat = format;
|
||||
MediaFormat format = new MediaFormat();
|
||||
format.setString(MediaFormat.KEY_MIME, sampleMimeType);
|
||||
maybeSetStringV16(format, MediaFormat.KEY_LANGUAGE, language);
|
||||
maybeSetIntegerV16(format, MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize);
|
||||
maybeSetIntegerV16(format, MediaFormat.KEY_WIDTH, width);
|
||||
maybeSetIntegerV16(format, MediaFormat.KEY_HEIGHT, height);
|
||||
maybeSetFloatV16(format, MediaFormat.KEY_FRAME_RATE, frameRate);
|
||||
maybeSetIntegerV16(format, "rotation-degrees", rotationDegrees);
|
||||
maybeSetIntegerV16(format, MediaFormat.KEY_CHANNEL_COUNT, channelCount);
|
||||
maybeSetIntegerV16(format, MediaFormat.KEY_SAMPLE_RATE, sampleRate);
|
||||
maybeSetIntegerV16(format, "encoder-delay", encoderDelay);
|
||||
maybeSetIntegerV16(format, "encoder-padding", encoderPadding);
|
||||
for (int i = 0; i < initializationData.size(); i++) {
|
||||
format.setByteBuffer("csd-" + i, ByteBuffer.wrap(initializationData.get(i)));
|
||||
}
|
||||
return frameworkMediaFormat;
|
||||
return format;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Format(" + id + ", " + containerMimeType + ", " + sampleMimeType + ", " + bitrate + ", "
|
||||
+ ", " + language + ", [" + width + ", " + height + ", " + frameRate + "]"
|
||||
+ language + ", [" + width + ", " + height + ", " + frameRate + "]"
|
||||
+ ", [" + channelCount + ", " + sampleRate + "])";
|
||||
}
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ public interface Renderer extends ExoPlayerComponent {
|
|||
* This method may be called when the renderer is in the following states:
|
||||
* {@link #STATE_DISABLED}.
|
||||
*
|
||||
* @param configuration The renderer configuration.
|
||||
* @param formats The enabled formats.
|
||||
* @param stream The {@link SampleStream} from which the renderer should consume.
|
||||
* @param positionUs The player's current position.
|
||||
|
|
@ -100,8 +101,8 @@ public interface Renderer extends ExoPlayerComponent {
|
|||
* before they are rendered.
|
||||
* @throws ExoPlaybackException If an error occurs.
|
||||
*/
|
||||
void enable(Format[] formats, SampleStream stream, long positionUs, boolean joining,
|
||||
long offsetUs) throws ExoPlaybackException;
|
||||
void enable(RendererConfiguration configuration, Format[] formats, SampleStream stream,
|
||||
long positionUs, boolean joining, long offsetUs) throws ExoPlaybackException;
|
||||
|
||||
/**
|
||||
* Starts the renderer, meaning that calls to {@link #render(long, long)} will cause media to be
|
||||
|
|
|
|||
|
|
@ -79,6 +79,20 @@ public interface RendererCapabilities {
|
|||
*/
|
||||
int ADAPTIVE_NOT_SUPPORTED = 0b0000;
|
||||
|
||||
/**
|
||||
* A mask to apply to the result of {@link #supportsFormat(Format)} to obtain one of
|
||||
* {@link #TUNNELING_SUPPORTED} and {@link #TUNNELING_NOT_SUPPORTED}.
|
||||
*/
|
||||
int TUNNELING_SUPPORT_MASK = 0b10000;
|
||||
/**
|
||||
* The {@link Renderer} supports tunneled output.
|
||||
*/
|
||||
int TUNNELING_SUPPORTED = 0b10000;
|
||||
/**
|
||||
* The {@link Renderer} does not support tunneled output.
|
||||
*/
|
||||
int TUNNELING_NOT_SUPPORTED = 0b00000;
|
||||
|
||||
/**
|
||||
* Returns the track type that the {@link Renderer} handles. For example, a video renderer will
|
||||
* return {@link C#TRACK_TYPE_VIDEO}, an audio renderer will return {@link C#TRACK_TYPE_AUDIO}, a
|
||||
|
|
@ -91,7 +105,7 @@ public interface RendererCapabilities {
|
|||
|
||||
/**
|
||||
* Returns the extent to which the {@link Renderer} supports a given format. The returned value is
|
||||
* the bitwise OR of two properties:
|
||||
* the bitwise OR of three properties:
|
||||
* <ul>
|
||||
* <li>The level of support for the format itself. One of {@link #FORMAT_HANDLED},
|
||||
* {@link #FORMAT_EXCEEDS_CAPABILITIES}, {@link #FORMAT_UNSUPPORTED_SUBTYPE} and
|
||||
|
|
@ -99,9 +113,12 @@ public interface RendererCapabilities {
|
|||
* <li>The level of support for adapting from the format to another format of the same mime type.
|
||||
* One of {@link #ADAPTIVE_SEAMLESS}, {@link #ADAPTIVE_NOT_SEAMLESS} and
|
||||
* {@link #ADAPTIVE_NOT_SUPPORTED}.</li>
|
||||
* <li>The level of support for tunneling. One of {@link #TUNNELING_SUPPORTED} and
|
||||
* {@link #TUNNELING_NOT_SUPPORTED}.</li>
|
||||
* </ul>
|
||||
* The individual properties can be retrieved by performing a bitwise AND with
|
||||
* {@link #FORMAT_SUPPORT_MASK} and {@link #ADAPTIVE_SUPPORT_MASK} respectively.
|
||||
* {@link #FORMAT_SUPPORT_MASK}, {@link #ADAPTIVE_SUPPORT_MASK} and
|
||||
* {@link #TUNNELING_SUPPORT_MASK} respectively.
|
||||
*
|
||||
* @param format The format.
|
||||
* @return The extent to which the renderer is capable of supporting the given format.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2;
|
||||
|
||||
/**
|
||||
* The configuration of a {@link Renderer}.
|
||||
*/
|
||||
public final class RendererConfiguration {
|
||||
|
||||
/**
|
||||
* The default configuration.
|
||||
*/
|
||||
public static final RendererConfiguration DEFAULT =
|
||||
new RendererConfiguration(C.AUDIO_SESSION_ID_UNSET);
|
||||
|
||||
/**
|
||||
* The audio session id to use for tunneling, or {@link C#AUDIO_SESSION_ID_UNSET} if tunneling
|
||||
* should not be enabled.
|
||||
*/
|
||||
public final int tunnelingAudioSessionId;
|
||||
|
||||
/**
|
||||
* @param tunnelingAudioSessionId The audio session id to use for tunneling, or
|
||||
* {@link C#AUDIO_SESSION_ID_UNSET} if tunneling should not be enabled.
|
||||
*/
|
||||
public RendererConfiguration(int tunnelingAudioSessionId) {
|
||||
this.tunnelingAudioSessionId = tunnelingAudioSessionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
RendererConfiguration other = (RendererConfiguration) obj;
|
||||
return tunnelingAudioSessionId == other.tunnelingAudioSessionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return tunnelingAudioSessionId;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -29,7 +29,6 @@ import android.view.SurfaceView;
|
|||
import android.view.TextureView;
|
||||
import com.google.android.exoplayer2.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.audio.AudioTrack;
|
||||
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
|
|
@ -37,7 +36,6 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
|||
import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.MetadataRenderer;
|
||||
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
|
|
@ -178,7 +176,7 @@ public class SimpleExoPlayer implements ExoPlayer {
|
|||
|
||||
// Set initial values.
|
||||
audioVolume = 1;
|
||||
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
|
||||
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
|
||||
audioStreamType = C.STREAM_TYPE_DEFAULT;
|
||||
videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT;
|
||||
|
||||
|
|
@ -393,7 +391,7 @@ public class SimpleExoPlayer implements ExoPlayer {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the audio session identifier, or {@code AudioTrack.SESSION_ID_NOT_SET} if not set.
|
||||
* Returns the audio session identifier, or {@link C#AUDIO_SESSION_ID_UNSET} if not set.
|
||||
*/
|
||||
public int getAudioSessionId() {
|
||||
return audioSessionId;
|
||||
|
|
@ -449,15 +447,6 @@ public class SimpleExoPlayer implements ExoPlayer {
|
|||
textOutput = output;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #setMetadataOutput(MetadataRenderer.Output)} instead.
|
||||
* @param output The output.
|
||||
*/
|
||||
@Deprecated
|
||||
public void setId3Output(MetadataRenderer.Output output) {
|
||||
setMetadataOutput(output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a listener to receive metadata events.
|
||||
*
|
||||
|
|
@ -490,8 +479,8 @@ public class SimpleExoPlayer implements ExoPlayer {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetTimeline) {
|
||||
player.prepare(mediaSource, resetPosition, resetTimeline);
|
||||
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
|
||||
player.prepare(mediaSource, resetPosition, resetState);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -556,6 +545,36 @@ public class SimpleExoPlayer implements ExoPlayer {
|
|||
player.blockingSendMessages(messages);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRendererCount() {
|
||||
return player.getRendererCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRendererType(int index) {
|
||||
return player.getRendererType(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackGroupArray getCurrentTrackGroups() {
|
||||
return player.getCurrentTrackGroups();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackSelectionArray getCurrentTrackSelections() {
|
||||
return player.getCurrentTrackSelections();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Timeline getCurrentTimeline() {
|
||||
return player.getCurrentTimeline();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCurrentManifest() {
|
||||
return player.getCurrentManifest();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentPeriodIndex() {
|
||||
return player.getCurrentPeriodIndex();
|
||||
|
|
@ -587,33 +606,13 @@ public class SimpleExoPlayer implements ExoPlayer {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int getRendererCount() {
|
||||
return player.getRendererCount();
|
||||
public boolean isCurrentWindowDynamic() {
|
||||
return player.isCurrentWindowDynamic();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRendererType(int index) {
|
||||
return player.getRendererType(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackGroupArray getCurrentTrackGroups() {
|
||||
return player.getCurrentTrackGroups();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackSelectionArray getCurrentTrackSelections() {
|
||||
return player.getCurrentTrackSelections();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Timeline getCurrentTimeline() {
|
||||
return player.getCurrentTimeline();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCurrentManifest() {
|
||||
return player.getCurrentManifest();
|
||||
public boolean isCurrentWindowSeekable() {
|
||||
return player.isCurrentWindowSeekable();
|
||||
}
|
||||
|
||||
// Renderer building.
|
||||
|
|
@ -772,7 +771,7 @@ public class SimpleExoPlayer implements ExoPlayer {
|
|||
protected void buildMetadataRenderers(Context context, Handler mainHandler,
|
||||
@ExtensionRendererMode int extensionRendererMode, MetadataRenderer.Output output,
|
||||
ArrayList<Renderer> out) {
|
||||
out.add(new MetadataRenderer(output, mainHandler.getLooper(), new Id3Decoder()));
|
||||
out.add(new MetadataRenderer(output, mainHandler.getLooper()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -949,7 +948,7 @@ public class SimpleExoPlayer implements ExoPlayer {
|
|||
}
|
||||
audioFormat = null;
|
||||
audioDecoderCounters = null;
|
||||
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
|
||||
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
|
||||
}
|
||||
|
||||
// TextRenderer.Output implementation
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.audio;
|
|||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.AudioTimestamp;
|
||||
import android.media.PlaybackParams;
|
||||
|
|
@ -30,28 +31,32 @@ import com.google.android.exoplayer2.util.MimeTypes;
|
|||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* Plays audio data. The implementation delegates to an {@link android.media.AudioTrack} and handles
|
||||
* playback position smoothing, non-blocking writes and reconfiguration.
|
||||
* <p>
|
||||
* Before starting playback, specify the input format by calling
|
||||
* {@link #configure(String, int, int, int, int)}. Next call {@link #initialize(int)}, optionally
|
||||
* specifying an audio session.
|
||||
* {@link #configure(String, int, int, int, int)}. Optionally call {@link #setAudioSessionId(int)},
|
||||
* {@link #setStreamType(int)}, {@link #enableTunnelingV21(int)} and {@link #disableTunneling()}
|
||||
* to configure audio playback. These methods may be called after writing data to the track, in
|
||||
* which case it will be reinitialized as required.
|
||||
* <p>
|
||||
* Call {@link #handleBuffer(ByteBuffer, long)} to write data, and {@link #handleDiscontinuity()}
|
||||
* when the data being fed is discontinuous. Call {@link #play()} to start playing the written data.
|
||||
* <p>
|
||||
* Call {@link #configure(String, int, int, int, int)} whenever the input format changes. If
|
||||
* {@link #isInitialized()} returns {@code false} after the call, it is necessary to call
|
||||
* {@link #initialize(int)} before writing more data.
|
||||
* Call {@link #configure(String, int, int, int, int)} whenever the input format changes. The track
|
||||
* will be reinitialized on the next call to {@link #handleBuffer(ByteBuffer, long)}.
|
||||
* <p>
|
||||
* The underlying {@link android.media.AudioTrack} is created by {@link #initialize(int)} and
|
||||
* released by {@link #reset()} (and {@link #configure(String, int, int, int, int)} unless the input
|
||||
* format is unchanged). It is safe to call {@link #initialize(int)} after calling {@link #reset()}
|
||||
* without reconfiguration.
|
||||
* Calling {@link #reset()} releases the underlying {@link android.media.AudioTrack} (and so does
|
||||
* calling {@link #configure(String, int, int, int, int)} unless the format is unchanged). It is
|
||||
* safe to call {@link #handleBuffer(ByteBuffer, long)} after {@link #reset()} without calling
|
||||
* {@link #configure(String, int, int, int, int)}.
|
||||
* <p>
|
||||
* Call {@link #release()} when the instance is no longer required.
|
||||
* Call {@link #handleEndOfStream()} to play out all data when no more input buffers will be
|
||||
* provided via {@link #handleBuffer(ByteBuffer, long)} until the next {@link #reset}. Call
|
||||
* {@link #release()} when the instance is no longer required.
|
||||
*/
|
||||
public final class AudioTrack {
|
||||
|
||||
|
|
@ -60,6 +65,19 @@ public final class AudioTrack {
|
|||
*/
|
||||
public interface Listener {
|
||||
|
||||
/**
|
||||
* Called when the audio track has been initialized with a newly generated audio session id.
|
||||
*
|
||||
* @param audioSessionId The newly generated audio session id.
|
||||
*/
|
||||
void onAudioSessionId(int audioSessionId);
|
||||
|
||||
/**
|
||||
* Called when the audio track handles a buffer whose timestamp is discontinuous with the last
|
||||
* buffer handled since it was reset.
|
||||
*/
|
||||
void onPositionDiscontinuity();
|
||||
|
||||
/**
|
||||
* Called when the audio track underruns.
|
||||
*
|
||||
|
|
@ -104,13 +122,15 @@ public final class AudioTrack {
|
|||
public static final class WriteException extends Exception {
|
||||
|
||||
/**
|
||||
* An error value returned from {@link android.media.AudioTrack#write(byte[], int, int)}.
|
||||
* The error value returned from {@link android.media.AudioTrack#write(byte[], int, int)} or
|
||||
* {@link android.media.AudioTrack#write(ByteBuffer, int, int)}.
|
||||
*/
|
||||
public final int errorCode;
|
||||
|
||||
/**
|
||||
* @param errorCode An error value returned from
|
||||
* {@link android.media.AudioTrack#write(byte[], int, int)}.
|
||||
* @param errorCode The error value returned from
|
||||
* {@link android.media.AudioTrack#write(byte[], int, int)} or
|
||||
* {@link android.media.AudioTrack#write(ByteBuffer, int, int)}.
|
||||
*/
|
||||
public WriteException(int errorCode) {
|
||||
super("AudioTrack write failed: " + errorCode);
|
||||
|
|
@ -134,20 +154,6 @@ public final class AudioTrack {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returned in the result of {@link #handleBuffer} if the buffer was discontinuous.
|
||||
*/
|
||||
public static final int RESULT_POSITION_DISCONTINUITY = 1;
|
||||
/**
|
||||
* Returned in the result of {@link #handleBuffer} if the buffer can be released.
|
||||
*/
|
||||
public static final int RESULT_BUFFER_CONSUMED = 2;
|
||||
|
||||
/**
|
||||
* Represents an unset {@link android.media.AudioTrack} session identifier.
|
||||
*/
|
||||
public static final int SESSION_ID_NOT_SET = 0;
|
||||
|
||||
/**
|
||||
* Returned by {@link #getCurrentPositionUs} when the position is not set.
|
||||
*/
|
||||
|
|
@ -210,15 +216,15 @@ public final class AudioTrack {
|
|||
/**
|
||||
* AudioTrack timestamps are deemed spurious if they are offset from the system clock by more
|
||||
* than this amount.
|
||||
*
|
||||
* <p>This is a fail safe that should not be required on correctly functioning devices.
|
||||
* <p>
|
||||
* This is a fail safe that should not be required on correctly functioning devices.
|
||||
*/
|
||||
private static final long MAX_AUDIO_TIMESTAMP_OFFSET_US = 5 * C.MICROS_PER_SECOND;
|
||||
|
||||
/**
|
||||
* AudioTrack latencies are deemed impossibly large if they are greater than this amount.
|
||||
*
|
||||
* <p>This is a fail safe that should not be required on correctly functioning devices.
|
||||
* <p>
|
||||
* This is a fail safe that should not be required on correctly functioning devices.
|
||||
*/
|
||||
private static final long MAX_LATENCY_US = 5 * C.MICROS_PER_SECOND;
|
||||
|
||||
|
|
@ -255,7 +261,7 @@ public final class AudioTrack {
|
|||
private final AudioTrackUtil audioTrackUtil;
|
||||
|
||||
/**
|
||||
* Used to keep the audio session active on pre-V21 builds (see {@link #initialize(int)}).
|
||||
* Used to keep the audio session active on pre-V21 builds (see {@link #initialize()}).
|
||||
*/
|
||||
private android.media.AudioTrack keepSessionIdAudioTrack;
|
||||
|
||||
|
|
@ -273,6 +279,9 @@ public final class AudioTrack {
|
|||
private int bufferSize;
|
||||
private long bufferSizeUs;
|
||||
|
||||
private ByteBuffer avSyncHeader;
|
||||
private int bytesUntilNextAvSync;
|
||||
|
||||
private int nextPlayheadOffsetIndex;
|
||||
private int playheadOffsetCount;
|
||||
private long smoothedPlayheadOffsetUs;
|
||||
|
|
@ -297,6 +306,9 @@ public final class AudioTrack {
|
|||
private ByteBuffer resampledBuffer;
|
||||
private boolean useResampledBuffer;
|
||||
|
||||
private boolean playing;
|
||||
private int audioSessionId;
|
||||
private boolean tunneling;
|
||||
private boolean hasData;
|
||||
private long lastFeedElapsedRealtimeMs;
|
||||
|
||||
|
|
@ -327,6 +339,7 @@ public final class AudioTrack {
|
|||
volume = 1.0f;
|
||||
startMediaTimeState = START_NOT_SET;
|
||||
streamType = C.STREAM_TYPE_DEFAULT;
|
||||
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -340,14 +353,6 @@ public final class AudioTrack {
|
|||
&& audioCapabilities.supportsEncoding(getEncodingForMimeType(mimeType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the audio track has been successfully initialized via {@link #initialize} and
|
||||
* not yet {@link #reset}.
|
||||
*/
|
||||
public boolean isInitialized() {
|
||||
return audioTrack != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the playback position in the stream starting at zero, in microseconds, or
|
||||
* {@link #CURRENT_POSITION_NOT_SET} if it is not yet available.
|
||||
|
|
@ -385,7 +390,7 @@ public final class AudioTrack {
|
|||
// The AudioTrack has started, but we don't have any samples to compute a smoothed position.
|
||||
currentPositionUs = audioTrackUtil.getPlaybackHeadPositionUs() + startMediaTimeUs;
|
||||
} else {
|
||||
// getPlayheadPositionUs() only has a granularity of ~20ms, so we base the position off the
|
||||
// getPlayheadPositionUs() only has a granularity of ~20 ms, so we base the position off the
|
||||
// system clock (and a smoothed offset between it and the playhead position) so as to
|
||||
// prevent jitter in the reported positions.
|
||||
currentPositionUs = systemClockUs + smoothedPlayheadOffsetUs + startMediaTimeUs;
|
||||
|
|
@ -442,7 +447,29 @@ public final class AudioTrack {
|
|||
throw new IllegalArgumentException("Unsupported channel count: " + channelCount);
|
||||
}
|
||||
|
||||
// Workaround for overly strict channel configuration checks on nVidia Shield.
|
||||
if (Util.SDK_INT <= 23 && "foster".equals(Util.DEVICE) && "NVIDIA".equals(Util.MANUFACTURER)) {
|
||||
switch (channelCount) {
|
||||
case 7:
|
||||
channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND;
|
||||
break;
|
||||
case 3:
|
||||
case 5:
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType);
|
||||
|
||||
// Workaround for Nexus Player not reporting support for mono passthrough.
|
||||
// (See [Internal: b/34268671].)
|
||||
if (Util.SDK_INT <= 25 && "fugu".equals(Util.DEVICE) && passthrough && channelCount == 1) {
|
||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||
}
|
||||
|
||||
@C.Encoding int sourceEncoding;
|
||||
if (passthrough) {
|
||||
sourceEncoding = getEncodingForMimeType(mimeType);
|
||||
|
|
@ -495,14 +522,7 @@ public final class AudioTrack {
|
|||
bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(pcmBytesToFrames(bufferSize));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the audio track for writing new buffers using {@link #handleBuffer}.
|
||||
*
|
||||
* @param sessionId Audio track session identifier to re-use, or {@link #SESSION_ID_NOT_SET} to
|
||||
* create a new one.
|
||||
* @return The new (or re-used) session identifier.
|
||||
*/
|
||||
public int initialize(int sessionId) throws InitializationException {
|
||||
private void initialize() throws InitializationException {
|
||||
// If we're asynchronously releasing a previous audio track then we block until it has been
|
||||
// released. This guarantees that we cannot end up in a state where we have multiple audio
|
||||
// track instances. Without this guarantee it would be possible, in extreme cases, to exhaust
|
||||
|
|
@ -510,23 +530,26 @@ public final class AudioTrack {
|
|||
// initialization of the audio track to fail.
|
||||
releasingConditionVariable.block();
|
||||
|
||||
if (sessionId == SESSION_ID_NOT_SET) {
|
||||
if (tunneling) {
|
||||
audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, targetEncoding,
|
||||
bufferSize, audioSessionId);
|
||||
} else if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
|
||||
audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
|
||||
targetEncoding, bufferSize, MODE_STREAM);
|
||||
} else {
|
||||
// Re-attach to the same audio session.
|
||||
audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
|
||||
targetEncoding, bufferSize, MODE_STREAM, sessionId);
|
||||
targetEncoding, bufferSize, MODE_STREAM, audioSessionId);
|
||||
}
|
||||
checkAudioTrackInitialized();
|
||||
|
||||
sessionId = audioTrack.getAudioSessionId();
|
||||
int audioSessionId = audioTrack.getAudioSessionId();
|
||||
if (enablePreV21AudioSessionWorkaround) {
|
||||
if (Util.SDK_INT < 21) {
|
||||
// The workaround creates an audio track with a two byte buffer on the same session, and
|
||||
// does not release it until this object is released, which keeps the session active.
|
||||
if (keepSessionIdAudioTrack != null
|
||||
&& sessionId != keepSessionIdAudioTrack.getAudioSessionId()) {
|
||||
&& audioSessionId != keepSessionIdAudioTrack.getAudioSessionId()) {
|
||||
releaseKeepSessionIdAudioTrack();
|
||||
}
|
||||
if (keepSessionIdAudioTrack == null) {
|
||||
|
|
@ -535,21 +558,25 @@ public final class AudioTrack {
|
|||
@C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT;
|
||||
int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback.
|
||||
keepSessionIdAudioTrack = new android.media.AudioTrack(streamType, sampleRate,
|
||||
channelConfig, encoding, bufferSize, MODE_STATIC, sessionId);
|
||||
channelConfig, encoding, bufferSize, MODE_STATIC, audioSessionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.audioSessionId != audioSessionId) {
|
||||
this.audioSessionId = audioSessionId;
|
||||
listener.onAudioSessionId(audioSessionId);
|
||||
}
|
||||
|
||||
audioTrackUtil.reconfigure(audioTrack, needsPassthroughWorkarounds());
|
||||
setAudioTrackVolume();
|
||||
setVolumeInternal();
|
||||
hasData = false;
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts or resumes playing audio if the audio track has been initialized.
|
||||
*/
|
||||
public void play() {
|
||||
playing = true;
|
||||
if (isInitialized()) {
|
||||
resumeSystemTimeUs = System.nanoTime() / 1000;
|
||||
audioTrack.play();
|
||||
|
|
@ -570,35 +597,42 @@ public final class AudioTrack {
|
|||
* Attempts to write data from a {@link ByteBuffer} to the audio track, starting from its current
|
||||
* position and ending at its limit (exclusive). The position of the {@link ByteBuffer} is
|
||||
* advanced by the number of bytes that were successfully written.
|
||||
* {@link Listener#onPositionDiscontinuity()} will be called if {@code presentationTimeUs} is
|
||||
* discontinuous with the last buffer handled since the track was reset.
|
||||
* <p>
|
||||
* Returns a bit field containing {@link #RESULT_BUFFER_CONSUMED} if the data was written in full,
|
||||
* and {@link #RESULT_POSITION_DISCONTINUITY} if the buffer was discontinuous with previously
|
||||
* written data.
|
||||
* <p>
|
||||
* If the data was not written in full then the same {@link ByteBuffer} must be provided to
|
||||
* subsequent calls until it has been fully consumed, except in the case of an interleaving call
|
||||
* to {@link #configure} or {@link #reset}.
|
||||
* Returns whether the data was written in full. If the data was not written in full then the same
|
||||
* {@link ByteBuffer} must be provided to subsequent calls until it has been fully consumed,
|
||||
* except in the case of an interleaving call to {@link #reset()} (or an interleaving call to
|
||||
* {@link #configure(String, int, int, int, int)} that caused the track to be reset).
|
||||
*
|
||||
* @param buffer The buffer containing audio data to play back.
|
||||
* @param presentationTimeUs Presentation timestamp of the next buffer in microseconds.
|
||||
* @return A bit field with {@link #RESULT_BUFFER_CONSUMED} if the buffer can be released, and
|
||||
* {@link #RESULT_POSITION_DISCONTINUITY} if the buffer was not contiguous with previously
|
||||
* written data.
|
||||
* @return Whether the buffer was consumed fully.
|
||||
* @throws InitializationException If an error occurs initializing the track.
|
||||
* @throws WriteException If an error occurs writing the audio data.
|
||||
*/
|
||||
public int handleBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException {
|
||||
public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs)
|
||||
throws InitializationException, WriteException {
|
||||
if (!isInitialized()) {
|
||||
initialize();
|
||||
if (playing) {
|
||||
play();
|
||||
}
|
||||
}
|
||||
|
||||
boolean hadData = hasData;
|
||||
hasData = hasPendingData();
|
||||
if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) {
|
||||
long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs;
|
||||
listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs);
|
||||
}
|
||||
int result = writeBuffer(buffer, presentationTimeUs);
|
||||
boolean result = writeBuffer(buffer, presentationTimeUs);
|
||||
lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime();
|
||||
return result;
|
||||
}
|
||||
|
||||
private int writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException {
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
private boolean writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException {
|
||||
boolean isNewSourceBuffer = currentSourceBuffer == null;
|
||||
Assertions.checkState(isNewSourceBuffer || currentSourceBuffer == buffer);
|
||||
currentSourceBuffer = buffer;
|
||||
|
|
@ -607,7 +641,7 @@ public final class AudioTrack {
|
|||
// An AC-3 audio track continues to play data written while it is paused. Stop writing so its
|
||||
// buffer empties. See [Internal: b/18899620].
|
||||
if (audioTrack.getPlayState() == PLAYSTATE_PAUSED) {
|
||||
return 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// A new AC-3 audio track's playback position continues to increase from the old track's
|
||||
|
|
@ -615,18 +649,17 @@ public final class AudioTrack {
|
|||
// head position actually returns to zero.
|
||||
if (audioTrack.getPlayState() == PLAYSTATE_STOPPED
|
||||
&& audioTrackUtil.getPlaybackHeadPosition() != 0) {
|
||||
return 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int result = 0;
|
||||
if (isNewSourceBuffer) {
|
||||
// We're seeing this buffer for the first time.
|
||||
|
||||
if (!currentSourceBuffer.hasRemaining()) {
|
||||
// The buffer is empty.
|
||||
currentSourceBuffer = null;
|
||||
return RESULT_BUFFER_CONSUMED;
|
||||
return true;
|
||||
}
|
||||
|
||||
useResampledBuffer = targetEncoding != sourceEncoding;
|
||||
|
|
@ -659,7 +692,7 @@ public final class AudioTrack {
|
|||
// number of bytes submitted.
|
||||
startMediaTimeUs += (presentationTimeUs - expectedPresentationTimeUs);
|
||||
startMediaTimeState = START_IN_SYNC;
|
||||
result |= RESULT_POSITION_DISCONTINUITY;
|
||||
listener.onPositionDiscontinuity();
|
||||
}
|
||||
}
|
||||
if (Util.SDK_INT < 21) {
|
||||
|
|
@ -692,7 +725,9 @@ public final class AudioTrack {
|
|||
buffer.position(buffer.position() + bytesWritten);
|
||||
}
|
||||
} else {
|
||||
bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining);
|
||||
bytesWritten = tunneling
|
||||
? writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, presentationTimeUs)
|
||||
: writeNonBlockingV21(audioTrack, buffer, bytesRemaining);
|
||||
}
|
||||
|
||||
if (bytesWritten < 0) {
|
||||
|
|
@ -707,9 +742,9 @@ public final class AudioTrack {
|
|||
submittedEncodedFrames += framesPerEncodedSample;
|
||||
}
|
||||
currentSourceBuffer = null;
|
||||
result |= RESULT_BUFFER_CONSUMED;
|
||||
return true;
|
||||
}
|
||||
return result;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -718,6 +753,7 @@ public final class AudioTrack {
|
|||
public void handleEndOfStream() {
|
||||
if (isInitialized()) {
|
||||
audioTrackUtil.handleEndOfStream(getSubmittedFrames());
|
||||
bytesUntilNextAvSync = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -743,21 +779,65 @@ public final class AudioTrack {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the stream type for audio track. If the stream type has changed, {@link #isInitialized()}
|
||||
* will return {@code false} and the caller must re-{@link #initialize(int)} the audio track
|
||||
* before writing more data. The caller must not reuse the audio session identifier when
|
||||
* re-initializing with a new stream type.
|
||||
* Sets the stream type for audio track. If the stream type has changed and if the audio track
|
||||
* is not configured for use with tunneling, then the audio track is reset and the audio session
|
||||
* id is cleared.
|
||||
* <p>
|
||||
* If the audio track is configured for use with tunneling then the stream type is ignored, the
|
||||
* audio track is not reset and the audio session id is not cleared. The passed stream type will
|
||||
* be used if the audio track is later re-configured into non-tunneled mode.
|
||||
*
|
||||
* @param streamType The {@link C.StreamType} to use for audio output.
|
||||
* @return Whether the stream type changed.
|
||||
*/
|
||||
public boolean setStreamType(@C.StreamType int streamType) {
|
||||
public void setStreamType(@C.StreamType int streamType) {
|
||||
if (this.streamType == streamType) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
this.streamType = streamType;
|
||||
if (tunneling) {
|
||||
// The stream type is ignored in tunneling mode, so no need to reset.
|
||||
return;
|
||||
}
|
||||
reset();
|
||||
return true;
|
||||
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the audio session id. The audio track is reset if the audio session id has changed.
|
||||
*/
|
||||
public void setAudioSessionId(int audioSessionId) {
|
||||
if (this.audioSessionId != audioSessionId) {
|
||||
this.audioSessionId = audioSessionId;
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables tunneling. The audio track is reset if tunneling was previously disabled or if the
|
||||
* audio session id has changed. Enabling tunneling requires platform API version 21 onwards.
|
||||
*
|
||||
* @param tunnelingAudioSessionId The audio session id to use.
|
||||
* @throws IllegalStateException Thrown if enabling tunneling on platform API version < 21.
|
||||
*/
|
||||
public void enableTunnelingV21(int tunnelingAudioSessionId) {
|
||||
Assertions.checkState(Util.SDK_INT >= 21);
|
||||
if (!tunneling || audioSessionId != tunnelingAudioSessionId) {
|
||||
tunneling = true;
|
||||
audioSessionId = tunnelingAudioSessionId;
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables tunneling. If tunneling was previously enabled then the audio track is reset and the
|
||||
* audio session id is cleared.
|
||||
*/
|
||||
public void disableTunneling() {
|
||||
if (tunneling) {
|
||||
tunneling = false;
|
||||
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -768,17 +848,17 @@ public final class AudioTrack {
|
|||
public void setVolume(float volume) {
|
||||
if (this.volume != volume) {
|
||||
this.volume = volume;
|
||||
setAudioTrackVolume();
|
||||
setVolumeInternal();
|
||||
}
|
||||
}
|
||||
|
||||
private void setAudioTrackVolume() {
|
||||
private void setVolumeInternal() {
|
||||
if (!isInitialized()) {
|
||||
// Do nothing.
|
||||
} else if (Util.SDK_INT >= 21) {
|
||||
setAudioTrackVolumeV21(audioTrack, volume);
|
||||
setVolumeInternalV21(audioTrack, volume);
|
||||
} else {
|
||||
setAudioTrackVolumeV3(audioTrack, volume);
|
||||
setVolumeInternalV3(audioTrack, volume);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -786,6 +866,7 @@ public final class AudioTrack {
|
|||
* Pauses playback.
|
||||
*/
|
||||
public void pause() {
|
||||
playing = false;
|
||||
if (isInitialized()) {
|
||||
resetSyncParams();
|
||||
audioTrackUtil.pause();
|
||||
|
|
@ -795,9 +876,9 @@ public final class AudioTrack {
|
|||
/**
|
||||
* Releases the underlying audio track asynchronously.
|
||||
* <p>
|
||||
* Calling {@link #initialize(int)} will block until the audio track has been released, so it is
|
||||
* safe to initialize immediately after a reset. The audio session may remain active until
|
||||
* {@link #release()} is called.
|
||||
* Calling {@link #handleBuffer(ByteBuffer, long)} will block until the audio track has been
|
||||
* released, so it is safe to use the audio track immediately after a reset. The audio session may
|
||||
* remain active until {@link #release()} is called.
|
||||
*/
|
||||
public void reset() {
|
||||
if (isInitialized()) {
|
||||
|
|
@ -805,6 +886,8 @@ public final class AudioTrack {
|
|||
submittedEncodedFrames = 0;
|
||||
framesPerEncodedSample = 0;
|
||||
currentSourceBuffer = null;
|
||||
avSyncHeader = null;
|
||||
bytesUntilNextAvSync = 0;
|
||||
startMediaTimeState = START_NOT_SET;
|
||||
latencyUs = 0;
|
||||
resetSyncParams();
|
||||
|
|
@ -837,6 +920,8 @@ public final class AudioTrack {
|
|||
public void release() {
|
||||
reset();
|
||||
releaseKeepSessionIdAudioTrack();
|
||||
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
|
||||
playing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -974,6 +1059,10 @@ public final class AudioTrack {
|
|||
throw new InitializationException(state, sampleRate, channelConfig, bufferSize);
|
||||
}
|
||||
|
||||
private boolean isInitialized() {
|
||||
return audioTrack != null;
|
||||
}
|
||||
|
||||
private long pcmBytesToFrames(long byteCount) {
|
||||
return byteCount / pcmFrameSize;
|
||||
}
|
||||
|
|
@ -1020,6 +1109,26 @@ public final class AudioTrack {
|
|||
&& audioTrack.getPlaybackHeadPosition() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates an {@link android.media.AudioTrack} to be used with tunneling video playback.
|
||||
*/
|
||||
@TargetApi(21)
|
||||
private static android.media.AudioTrack createHwAvSyncAudioTrackV21(int sampleRate,
|
||||
int channelConfig, int encoding, int bufferSize, int sessionId) {
|
||||
AudioAttributes attributesBuilder = new AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
|
||||
.setFlags(AudioAttributes.FLAG_HW_AV_SYNC)
|
||||
.build();
|
||||
AudioFormat format = new AudioFormat.Builder()
|
||||
.setChannelMask(channelConfig)
|
||||
.setEncoding(encoding)
|
||||
.setSampleRate(sampleRate)
|
||||
.build();
|
||||
return new android.media.AudioTrack(attributesBuilder, format, bufferSize, MODE_STREAM,
|
||||
sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the provided buffer into 16-bit PCM.
|
||||
*
|
||||
|
|
@ -1125,18 +1234,57 @@ public final class AudioTrack {
|
|||
}
|
||||
|
||||
@TargetApi(21)
|
||||
private static int writeNonBlockingV21(
|
||||
android.media.AudioTrack audioTrack, ByteBuffer buffer, int size) {
|
||||
private static int writeNonBlockingV21(android.media.AudioTrack audioTrack, ByteBuffer buffer,
|
||||
int size) {
|
||||
return audioTrack.write(buffer, size, WRITE_NON_BLOCKING);
|
||||
}
|
||||
|
||||
@TargetApi(21)
|
||||
private static void setAudioTrackVolumeV21(android.media.AudioTrack audioTrack, float volume) {
|
||||
private int writeNonBlockingWithAvSyncV21(android.media.AudioTrack audioTrack,
|
||||
ByteBuffer buffer, int size, long presentationTimeUs) {
|
||||
// TODO: Uncomment this when [Internal ref b/33627517] is clarified or fixed.
|
||||
// if (Util.SDK_INT >= 23) {
|
||||
// // The underlying platform AudioTrack writes AV sync headers directly.
|
||||
// return audioTrack.write(buffer, size, WRITE_NON_BLOCKING, presentationTimeUs * 1000);
|
||||
// }
|
||||
if (avSyncHeader == null) {
|
||||
avSyncHeader = ByteBuffer.allocate(16);
|
||||
avSyncHeader.order(ByteOrder.BIG_ENDIAN);
|
||||
avSyncHeader.putInt(0x55550001);
|
||||
}
|
||||
if (bytesUntilNextAvSync == 0) {
|
||||
avSyncHeader.putInt(4, size);
|
||||
avSyncHeader.putLong(8, presentationTimeUs * 1000);
|
||||
avSyncHeader.position(0);
|
||||
bytesUntilNextAvSync = size;
|
||||
}
|
||||
int avSyncHeaderBytesRemaining = avSyncHeader.remaining();
|
||||
if (avSyncHeaderBytesRemaining > 0) {
|
||||
int result = audioTrack.write(avSyncHeader, avSyncHeaderBytesRemaining, WRITE_NON_BLOCKING);
|
||||
if (result < 0) {
|
||||
bytesUntilNextAvSync = 0;
|
||||
return result;
|
||||
}
|
||||
if (result < avSyncHeaderBytesRemaining) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
int result = writeNonBlockingV21(audioTrack, buffer, size);
|
||||
if (result < 0) {
|
||||
bytesUntilNextAvSync = 0;
|
||||
return result;
|
||||
}
|
||||
bytesUntilNextAvSync -= result;
|
||||
return result;
|
||||
}
|
||||
|
||||
@TargetApi(21)
|
||||
private static void setVolumeInternalV21(android.media.AudioTrack audioTrack, float volume) {
|
||||
audioTrack.setVolume(volume);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private static void setAudioTrackVolumeV3(android.media.AudioTrack audioTrack, float volume) {
|
||||
private static void setVolumeInternalV3(android.media.AudioTrack audioTrack, float volume) {
|
||||
audioTrack.setStereoVolume(volume, volume);
|
||||
}
|
||||
|
||||
|
|
@ -1385,7 +1533,7 @@ public final class AudioTrack {
|
|||
playbackParams = (playbackParams != null ? playbackParams : new PlaybackParams())
|
||||
.allowDefaults();
|
||||
this.playbackParams = playbackParams;
|
||||
this.playbackSpeed = playbackParams.getSpeed();
|
||||
playbackSpeed = playbackParams.getSpeed();
|
||||
maybeApplyPlaybackParams();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,8 +41,7 @@ import java.nio.ByteBuffer;
|
|||
* Decodes and renders audio using {@link MediaCodec} and {@link AudioTrack}.
|
||||
*/
|
||||
@TargetApi(16)
|
||||
public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock,
|
||||
AudioTrack.Listener {
|
||||
public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock {
|
||||
|
||||
private final EventDispatcher eventDispatcher;
|
||||
private final AudioTrack audioTrack;
|
||||
|
|
@ -50,7 +49,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
private boolean passthroughEnabled;
|
||||
private android.media.MediaFormat passthroughMediaFormat;
|
||||
private int pcmEncoding;
|
||||
private int audioSessionId;
|
||||
private long currentPositionUs;
|
||||
private boolean allowPositionDiscontinuity;
|
||||
|
||||
|
|
@ -129,8 +127,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
boolean playClearSamplesWithoutKeys, Handler eventHandler,
|
||||
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) {
|
||||
super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys);
|
||||
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
|
||||
audioTrack = new AudioTrack(audioCapabilities, this);
|
||||
audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener());
|
||||
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
||||
}
|
||||
|
||||
|
|
@ -141,10 +138,11 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
if (!MimeTypes.isAudio(mimeType)) {
|
||||
return FORMAT_UNSUPPORTED_TYPE;
|
||||
}
|
||||
int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;
|
||||
if (allowPassthrough(mimeType) && mediaCodecSelector.getPassthroughDecoderInfo() != null) {
|
||||
return ADAPTIVE_NOT_SEAMLESS | FORMAT_HANDLED;
|
||||
return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | FORMAT_HANDLED;
|
||||
}
|
||||
MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, false, false);
|
||||
MediaCodecInfo decoderInfo = mediaCodecSelector.getDecoderInfo(mimeType, false);
|
||||
if (decoderInfo == null) {
|
||||
return FORMAT_UNSUPPORTED_SUBTYPE;
|
||||
}
|
||||
|
|
@ -155,7 +153,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
&& (format.channelCount == Format.NO_VALUE
|
||||
|| decoderInfo.isAudioChannelCountSupportedV21(format.channelCount)));
|
||||
int formatSupport = decoderCapable ? FORMAT_HANDLED : FORMAT_EXCEEDS_CAPABILITIES;
|
||||
return ADAPTIVE_NOT_SEAMLESS | formatSupport;
|
||||
return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -185,7 +183,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void configureCodec(MediaCodec codec, Format format, MediaCrypto crypto) {
|
||||
protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
|
||||
MediaCrypto crypto) {
|
||||
if (passthroughEnabled) {
|
||||
// Override the MIME type used to configure the codec if we are using a passthrough decoder.
|
||||
passthroughMediaFormat = format.getFrameworkMediaFormatV16();
|
||||
|
|
@ -231,25 +230,42 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
}
|
||||
|
||||
/**
|
||||
* Called when the audio session id becomes known. Once the id is known it will not change (and
|
||||
* hence this method will not be called again) unless the renderer is disabled and then
|
||||
* subsequently re-enabled.
|
||||
* <p>
|
||||
* The default implementation is a no-op. One reason for overriding this method would be to
|
||||
* instantiate and enable a {@link Virtualizer} in order to spatialize the audio channels. For
|
||||
* this use case, any {@link Virtualizer} instances should be released in {@link #onDisabled()}
|
||||
* (if not before).
|
||||
* Called when the audio session id becomes known. The default implementation is a no-op. One
|
||||
* reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in
|
||||
* order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances
|
||||
* should be released in {@link #onDisabled()} (if not before).
|
||||
*
|
||||
* @param audioSessionId The audio session id.
|
||||
* @see AudioTrack.Listener#onAudioSessionId(int)
|
||||
*/
|
||||
protected void onAudioSessionId(int audioSessionId) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* @see AudioTrack.Listener#onPositionDiscontinuity()
|
||||
*/
|
||||
protected void onAudioTrackPositionDiscontinuity() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* @see AudioTrack.Listener#onUnderrun(int, long, long)
|
||||
*/
|
||||
protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs,
|
||||
long elapsedSinceLastFeedMs) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnabled(boolean joining) throws ExoPlaybackException {
|
||||
super.onEnabled(joining);
|
||||
eventDispatcher.enabled(decoderCounters);
|
||||
int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;
|
||||
if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {
|
||||
audioTrack.enableTunnelingV21(tunnelingAudioSessionId);
|
||||
} else {
|
||||
audioTrack.disableTunneling();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -274,7 +290,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
|
||||
@Override
|
||||
protected void onDisabled() {
|
||||
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
|
||||
try {
|
||||
audioTrack.release();
|
||||
} finally {
|
||||
|
|
@ -325,44 +340,15 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
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 {
|
||||
handleBufferResult = audioTrack.handleBuffer(buffer, bufferPresentationTimeUs);
|
||||
} catch (AudioTrack.WriteException e) {
|
||||
if (audioTrack.handleBuffer(buffer, bufferPresentationTimeUs)) {
|
||||
codec.releaseOutputBuffer(bufferIndex, false);
|
||||
decoderCounters.renderedOutputBufferCount++;
|
||||
return true;
|
||||
}
|
||||
} catch (AudioTrack.InitializationException | AudioTrack.WriteException e) {
|
||||
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
||||
}
|
||||
|
||||
// If we are out of sync, allow currentPositionUs to jump backwards.
|
||||
if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) {
|
||||
handleAudioTrackDiscontinuity();
|
||||
allowPositionDiscontinuity = true;
|
||||
}
|
||||
|
||||
// Release the buffer if it was consumed.
|
||||
if ((handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0) {
|
||||
codec.releaseOutputBuffer(bufferIndex, false);
|
||||
decoderCounters.renderedOutputBufferCount++;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -371,10 +357,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
audioTrack.handleEndOfStream();
|
||||
}
|
||||
|
||||
protected void handleAudioTrackDiscontinuity() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(int messageType, Object message) throws ExoPlaybackException {
|
||||
switch (messageType) {
|
||||
|
|
@ -386,9 +368,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
break;
|
||||
case C.MSG_SET_STREAM_TYPE:
|
||||
@C.StreamType int streamType = (Integer) message;
|
||||
if (audioTrack.setStreamType(streamType)) {
|
||||
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
|
||||
}
|
||||
audioTrack.setStreamType(streamType);
|
||||
break;
|
||||
default:
|
||||
super.handleMessage(messageType, message);
|
||||
|
|
@ -396,11 +376,27 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
}
|
||||
}
|
||||
|
||||
// AudioTrack.Listener implementation.
|
||||
private final class AudioTrackListener implements AudioTrack.Listener {
|
||||
|
||||
@Override
|
||||
public void onAudioSessionId(int audioSessionId) {
|
||||
eventDispatcher.audioSessionId(audioSessionId);
|
||||
MediaCodecAudioRenderer.this.onAudioSessionId(audioSessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
onAudioTrackPositionDiscontinuity();
|
||||
// We are out of sync so allow currentPositionUs to jump backwards.
|
||||
MediaCodecAudioRenderer.this.allowPositionDiscontinuity = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||
onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer2.audio;
|
||||
|
||||
import android.media.PlaybackParams;
|
||||
import android.media.audiofx.Virtualizer;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
|
|
@ -43,8 +44,7 @@ import java.lang.annotation.RetentionPolicy;
|
|||
/**
|
||||
* Decodes and renders audio using a {@link SimpleDecoder}.
|
||||
*/
|
||||
public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock,
|
||||
AudioTrack.Listener {
|
||||
public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock {
|
||||
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({REINITIALIZATION_STATE_NONE, REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM,
|
||||
|
|
@ -94,8 +94,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
private boolean outputStreamEnded;
|
||||
private boolean waitingForKeys;
|
||||
|
||||
private int audioSessionId;
|
||||
|
||||
public SimpleDecoderAudioRenderer() {
|
||||
this(null, null);
|
||||
}
|
||||
|
|
@ -141,11 +139,10 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys) {
|
||||
super(C.TRACK_TYPE_AUDIO);
|
||||
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
||||
audioTrack = new AudioTrack(audioCapabilities, this);
|
||||
audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener());
|
||||
this.drmSessionManager = drmSessionManager;
|
||||
formatHolder = new FormatHolder();
|
||||
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
|
||||
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
|
||||
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
|
||||
audioTrackNeedsConfigure = true;
|
||||
}
|
||||
|
|
@ -155,6 +152,25 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int supportsFormat(Format format) {
|
||||
int formatSupport = supportsFormatInternal(format);
|
||||
if (formatSupport == FORMAT_UNSUPPORTED_TYPE || formatSupport == FORMAT_UNSUPPORTED_SUBTYPE) {
|
||||
return formatSupport;
|
||||
}
|
||||
int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;
|
||||
return ADAPTIVE_NOT_SEAMLESS | tunnelingSupport | formatSupport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link #FORMAT_SUPPORT_MASK} component of the return value for
|
||||
* {@link #supportsFormat(Format)}.
|
||||
*
|
||||
* @param format The format.
|
||||
* @return The extent to which the renderer supports the format itself.
|
||||
*/
|
||||
protected abstract int supportsFormatInternal(Format format);
|
||||
|
||||
@Override
|
||||
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
|
||||
if (outputStreamEnded) {
|
||||
|
|
@ -185,6 +201,33 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the audio session id becomes known. The default implementation is a no-op. One
|
||||
* reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in
|
||||
* order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances
|
||||
* should be released in {@link #onDisabled()} (if not before).
|
||||
*
|
||||
* @see AudioTrack.Listener#onAudioSessionId(int)
|
||||
*/
|
||||
protected void onAudioSessionId(int audioSessionId) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* @see AudioTrack.Listener#onPositionDiscontinuity()
|
||||
*/
|
||||
protected void onAudioTrackPositionDiscontinuity() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* @see AudioTrack.Listener#onUnderrun(int, long, long)
|
||||
*/
|
||||
protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs,
|
||||
long elapsedSinceLastFeedMs) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a decoder for the given format.
|
||||
*
|
||||
|
|
@ -244,28 +287,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
audioTrackNeedsConfigure = false;
|
||||
}
|
||||
|
||||
if (!audioTrack.isInitialized()) {
|
||||
if (audioSessionId == 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) {
|
||||
if (audioTrack.handleBuffer(outputBuffer.data, outputBuffer.timeUs)) {
|
||||
decoderCounters.renderedOutputBufferCount++;
|
||||
outputBuffer.release();
|
||||
outputBuffer = null;
|
||||
|
|
@ -381,23 +403,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
return currentPositionUs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the audio session id becomes known. Once the id is known it will not change (and
|
||||
* hence this method will not be called again) unless the renderer is disabled and then
|
||||
* subsequently re-enabled.
|
||||
* <p>
|
||||
* The default implementation is a no-op.
|
||||
*
|
||||
* @param audioSessionId The audio session id.
|
||||
*/
|
||||
protected void onAudioSessionId(int audioSessionId) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnabled(boolean joining) throws ExoPlaybackException {
|
||||
decoderCounters = new DecoderCounters();
|
||||
eventDispatcher.enabled(decoderCounters);
|
||||
int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;
|
||||
if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {
|
||||
audioTrack.enableTunnelingV21(tunnelingAudioSessionId);
|
||||
} else {
|
||||
audioTrack.disableTunneling();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -425,7 +440,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
@Override
|
||||
protected void onDisabled() {
|
||||
inputFormat = null;
|
||||
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
|
||||
audioTrackNeedsConfigure = true;
|
||||
waitingForKeys = false;
|
||||
try {
|
||||
|
|
@ -537,6 +551,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
// There aren't any final output buffers, so release the decoder immediately.
|
||||
releaseDecoder();
|
||||
maybeInitDecoder();
|
||||
audioTrackNeedsConfigure = true;
|
||||
}
|
||||
|
||||
eventDispatcher.inputFormatChanged(newFormat);
|
||||
|
|
@ -553,9 +568,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
break;
|
||||
case C.MSG_SET_STREAM_TYPE:
|
||||
@C.StreamType int streamType = (Integer) message;
|
||||
if (audioTrack.setStreamType(streamType)) {
|
||||
audioSessionId = AudioTrack.SESSION_ID_NOT_SET;
|
||||
}
|
||||
audioTrack.setStreamType(streamType);
|
||||
break;
|
||||
default:
|
||||
super.handleMessage(messageType, message);
|
||||
|
|
@ -563,11 +576,27 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
}
|
||||
}
|
||||
|
||||
// AudioTrack.Listener implementation.
|
||||
private final class AudioTrackListener implements AudioTrack.Listener {
|
||||
|
||||
@Override
|
||||
public void onAudioSessionId(int audioSessionId) {
|
||||
eventDispatcher.audioSessionId(audioSessionId);
|
||||
SimpleDecoderAudioRenderer.this.onAudioSessionId(audioSessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
onAudioTrackPositionDiscontinuity();
|
||||
// We are out of sync so allow currentPositionUs to jump backwards.
|
||||
SimpleDecoderAudioRenderer.this.allowPositionDiscontinuity = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||
onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
|
||||
eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,10 @@ import android.os.Handler;
|
|||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.support.annotation.IntDef;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaDrm.KeyRequest;
|
||||
|
|
@ -33,18 +36,21 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
|
|||
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A {@link DrmSessionManager} that supports streaming playbacks using {@link MediaDrm}.
|
||||
* A {@link DrmSessionManager} that supports playbacks using {@link MediaDrm}.
|
||||
*/
|
||||
@TargetApi(18)
|
||||
public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T>,
|
||||
public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T>,
|
||||
DrmSession<T> {
|
||||
|
||||
/**
|
||||
* Listener of {@link StreamingDrmSessionManager} events.
|
||||
* Listener of {@link DefaultDrmSessionManager} events.
|
||||
*/
|
||||
public interface EventListener {
|
||||
|
||||
|
|
@ -60,6 +66,16 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
|||
*/
|
||||
void onDrmSessionManagerError(Exception e);
|
||||
|
||||
/**
|
||||
* Called each time offline keys are restored.
|
||||
*/
|
||||
void onDrmKeysRestored();
|
||||
|
||||
/**
|
||||
* Called each time offline keys are removed.
|
||||
*/
|
||||
void onDrmKeysRemoved();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -67,9 +83,32 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
|||
*/
|
||||
public static final String PLAYREADY_CUSTOM_DATA_KEY = "PRCustomData";
|
||||
|
||||
/** Determines the action to be done after a session acquired. */
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({MODE_PLAYBACK, MODE_QUERY, MODE_DOWNLOAD, MODE_RELEASE})
|
||||
public @interface Mode {}
|
||||
/**
|
||||
* Loads and refreshes (if necessary) a license for playback. Supports streaming and offline
|
||||
* licenses.
|
||||
*/
|
||||
public static final int MODE_PLAYBACK = 0;
|
||||
/**
|
||||
* Restores an offline license to allow its status to be queried. If the offline license is
|
||||
* expired sets state to {@link #STATE_ERROR}.
|
||||
*/
|
||||
public static final int MODE_QUERY = 1;
|
||||
/** Downloads an offline license or renews an existing one. */
|
||||
public static final int MODE_DOWNLOAD = 2;
|
||||
/** Releases an existing offline license. */
|
||||
public static final int MODE_RELEASE = 3;
|
||||
|
||||
private static final String TAG = "OfflineDrmSessionMngr";
|
||||
|
||||
private static final int MSG_PROVISION = 0;
|
||||
private static final int MSG_KEYS = 1;
|
||||
|
||||
private static final int MAX_LICENSE_DURATION_TO_RENEW = 60;
|
||||
|
||||
private final Handler eventHandler;
|
||||
private final EventListener eventListener;
|
||||
private final ExoMediaDrm<T> mediaDrm;
|
||||
|
|
@ -85,14 +124,17 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
|||
private HandlerThread requestHandlerThread;
|
||||
private Handler postRequestHandler;
|
||||
|
||||
private int mode;
|
||||
private int openCount;
|
||||
private boolean provisioningInProgress;
|
||||
@DrmSession.State
|
||||
private int state;
|
||||
private T mediaCrypto;
|
||||
private Exception lastException;
|
||||
private SchemeData schemeData;
|
||||
private DrmSessionException lastException;
|
||||
private byte[] schemeInitData;
|
||||
private String schemeMimeType;
|
||||
private byte[] sessionId;
|
||||
private byte[] offlineLicenseKeySetId;
|
||||
|
||||
/**
|
||||
* Instantiates a new instance using the Widevine scheme.
|
||||
|
|
@ -105,7 +147,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
|||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @throws UnsupportedDrmException If the specified DRM scheme is not supported.
|
||||
*/
|
||||
public static StreamingDrmSessionManager<FrameworkMediaCrypto> newWidevineInstance(
|
||||
public static DefaultDrmSessionManager<FrameworkMediaCrypto> newWidevineInstance(
|
||||
MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters,
|
||||
Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException {
|
||||
return newFrameworkInstance(C.WIDEVINE_UUID, callback, optionalKeyRequestParameters,
|
||||
|
|
@ -125,7 +167,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
|||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @throws UnsupportedDrmException If the specified DRM scheme is not supported.
|
||||
*/
|
||||
public static StreamingDrmSessionManager<FrameworkMediaCrypto> newPlayReadyInstance(
|
||||
public static DefaultDrmSessionManager<FrameworkMediaCrypto> newPlayReadyInstance(
|
||||
MediaDrmCallback callback, String customData, Handler eventHandler,
|
||||
EventListener eventListener) throws UnsupportedDrmException {
|
||||
HashMap<String, String> optionalKeyRequestParameters;
|
||||
|
|
@ -151,10 +193,10 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
|||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @throws UnsupportedDrmException If the specified DRM scheme is not supported.
|
||||
*/
|
||||
public static StreamingDrmSessionManager<FrameworkMediaCrypto> newFrameworkInstance(
|
||||
public static DefaultDrmSessionManager<FrameworkMediaCrypto> newFrameworkInstance(
|
||||
UUID uuid, MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters,
|
||||
Handler eventHandler, EventListener eventListener) throws UnsupportedDrmException {
|
||||
return new StreamingDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), callback,
|
||||
return new DefaultDrmSessionManager<>(uuid, FrameworkMediaDrm.newInstance(uuid), callback,
|
||||
optionalKeyRequestParameters, eventHandler, eventListener);
|
||||
}
|
||||
|
||||
|
|
@ -168,7 +210,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
|||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
*/
|
||||
public StreamingDrmSessionManager(UUID uuid, ExoMediaDrm<T> mediaDrm, MediaDrmCallback callback,
|
||||
public DefaultDrmSessionManager(UUID uuid, ExoMediaDrm<T> mediaDrm, MediaDrmCallback callback,
|
||||
HashMap<String, String> optionalKeyRequestParameters, Handler eventHandler,
|
||||
EventListener eventListener) {
|
||||
this.uuid = uuid;
|
||||
|
|
@ -179,6 +221,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
|||
this.eventListener = eventListener;
|
||||
mediaDrm.setOnEventListener(new MediaDrmEventListener());
|
||||
state = STATE_CLOSED;
|
||||
mode = MODE_PLAYBACK;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -229,6 +272,35 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
|||
mediaDrm.setPropertyByteArray(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the mode, which determines the role of sessions acquired from the instance. This must be
|
||||
* called before {@link #acquireSession(Looper, DrmInitData)} is called.
|
||||
*
|
||||
* <p>By default, the mode is {@link #MODE_PLAYBACK} and a streaming license is requested when
|
||||
* required.
|
||||
*
|
||||
* <p>{@code mode} must be one of these:
|
||||
* <li>{@link #MODE_PLAYBACK}: If {@code offlineLicenseKeySetId} is null, a streaming license is
|
||||
* requested otherwise the offline license is restored.
|
||||
* <li>{@link #MODE_QUERY}: {@code offlineLicenseKeySetId} can not be null. The offline license
|
||||
* is restored.
|
||||
* <li>{@link #MODE_DOWNLOAD}: If {@code offlineLicenseKeySetId} is null, an offline license is
|
||||
* requested otherwise the offline license is renewed.
|
||||
* <li>{@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline license
|
||||
* is released.
|
||||
*
|
||||
* @param mode The mode to be set.
|
||||
* @param offlineLicenseKeySetId The key set id of the license to be used with the given mode.
|
||||
*/
|
||||
public void setMode(@Mode int mode, byte[] offlineLicenseKeySetId) {
|
||||
Assertions.checkState(openCount == 0);
|
||||
if (mode == MODE_QUERY || mode == MODE_RELEASE) {
|
||||
Assertions.checkNotNull(offlineLicenseKeySetId);
|
||||
}
|
||||
this.mode = mode;
|
||||
this.offlineLicenseKeySetId = offlineLicenseKeySetId;
|
||||
}
|
||||
|
||||
// DrmSessionManager implementation.
|
||||
|
||||
@Override
|
||||
|
|
@ -248,18 +320,22 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
|||
requestHandlerThread.start();
|
||||
postRequestHandler = new PostRequestHandler(requestHandlerThread.getLooper());
|
||||
|
||||
schemeData = drmInitData.get(uuid);
|
||||
if (schemeData == null) {
|
||||
onError(new IllegalStateException("Media does not support uuid: " + uuid));
|
||||
return this;
|
||||
}
|
||||
if (Util.SDK_INT < 21) {
|
||||
// Prior to L the Widevine CDM required data to be extracted from the PSSH atom.
|
||||
byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeData.data, C.WIDEVINE_UUID);
|
||||
if (psshData == null) {
|
||||
// Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged.
|
||||
} else {
|
||||
schemeData = new SchemeData(C.WIDEVINE_UUID, schemeData.mimeType, psshData);
|
||||
if (offlineLicenseKeySetId == null) {
|
||||
SchemeData schemeData = drmInitData.get(uuid);
|
||||
if (schemeData == null) {
|
||||
onError(new IllegalStateException("Media does not support uuid: " + uuid));
|
||||
return this;
|
||||
}
|
||||
schemeInitData = schemeData.data;
|
||||
schemeMimeType = schemeData.mimeType;
|
||||
if (Util.SDK_INT < 21) {
|
||||
// Prior to L the Widevine CDM required data to be extracted from the PSSH atom.
|
||||
byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitData, C.WIDEVINE_UUID);
|
||||
if (psshData == null) {
|
||||
// Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged.
|
||||
} else {
|
||||
schemeInitData = psshData;
|
||||
}
|
||||
}
|
||||
}
|
||||
state = STATE_OPENING;
|
||||
|
|
@ -280,7 +356,8 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
|||
postRequestHandler = null;
|
||||
requestHandlerThread.quit();
|
||||
requestHandlerThread = null;
|
||||
schemeData = null;
|
||||
schemeInitData = null;
|
||||
schemeMimeType = null;
|
||||
mediaCrypto = null;
|
||||
lastException = null;
|
||||
if (sessionId != null) {
|
||||
|
|
@ -314,10 +391,25 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
|||
}
|
||||
|
||||
@Override
|
||||
public final Exception getError() {
|
||||
public final DrmSessionException getError() {
|
||||
return state == STATE_ERROR ? lastException : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> queryKeyStatus() {
|
||||
// User may call this method rightfully even if state == STATE_ERROR. So only check if there is
|
||||
// a sessionId
|
||||
if (sessionId == null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
return mediaDrm.queryKeyStatus(sessionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getOfflineLicenseKeySetId() {
|
||||
return offlineLicenseKeySetId;
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void openInternal(boolean allowProvisioning) {
|
||||
|
|
@ -325,7 +417,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
|||
sessionId = mediaDrm.openSession();
|
||||
mediaCrypto = mediaDrm.createMediaCrypto(uuid, sessionId);
|
||||
state = STATE_OPENED;
|
||||
postKeyRequest();
|
||||
doLicense();
|
||||
} catch (NotProvisionedException e) {
|
||||
if (allowProvisioning) {
|
||||
postProvisionRequest();
|
||||
|
|
@ -363,20 +455,86 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
|||
if (state == STATE_OPENING) {
|
||||
openInternal(false);
|
||||
} else {
|
||||
postKeyRequest();
|
||||
doLicense();
|
||||
}
|
||||
} catch (DeniedByServerException e) {
|
||||
onError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void postKeyRequest() {
|
||||
KeyRequest keyRequest;
|
||||
private void doLicense() {
|
||||
switch (mode) {
|
||||
case MODE_PLAYBACK:
|
||||
case MODE_QUERY:
|
||||
if (offlineLicenseKeySetId == null) {
|
||||
postKeyRequest(sessionId, MediaDrm.KEY_TYPE_STREAMING);
|
||||
} else {
|
||||
if (restoreKeys()) {
|
||||
long licenseDurationRemainingSec = getLicenseDurationRemainingSec();
|
||||
if (mode == MODE_PLAYBACK
|
||||
&& licenseDurationRemainingSec <= MAX_LICENSE_DURATION_TO_RENEW) {
|
||||
Log.d(TAG, "Offline license has expired or will expire soon. "
|
||||
+ "Remaining seconds: " + licenseDurationRemainingSec);
|
||||
postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE);
|
||||
} else if (licenseDurationRemainingSec <= 0) {
|
||||
onError(new KeysExpiredException());
|
||||
} else {
|
||||
state = STATE_OPENED_WITH_KEYS;
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onDrmKeysRestored();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MODE_DOWNLOAD:
|
||||
if (offlineLicenseKeySetId == null) {
|
||||
postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE);
|
||||
} else {
|
||||
// Renew
|
||||
if (restoreKeys()) {
|
||||
postKeyRequest(sessionId, MediaDrm.KEY_TYPE_OFFLINE);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MODE_RELEASE:
|
||||
if (restoreKeys()) {
|
||||
postKeyRequest(offlineLicenseKeySetId, MediaDrm.KEY_TYPE_RELEASE);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean restoreKeys() {
|
||||
try {
|
||||
keyRequest = mediaDrm.getKeyRequest(sessionId, schemeData.data, schemeData.mimeType,
|
||||
MediaDrm.KEY_TYPE_STREAMING, optionalKeyRequestParameters);
|
||||
mediaDrm.restoreKeys(sessionId, offlineLicenseKeySetId);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error trying to restore Widevine keys.", e);
|
||||
onError(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private long getLicenseDurationRemainingSec() {
|
||||
if (!C.WIDEVINE_UUID.equals(uuid)) {
|
||||
return Long.MAX_VALUE;
|
||||
}
|
||||
Pair<Long, Long> pair = WidevineUtil.getLicenseDurationRemainingSec(this);
|
||||
return Math.min(pair.first, pair.second);
|
||||
}
|
||||
|
||||
private void postKeyRequest(byte[] scope, int keyType) {
|
||||
try {
|
||||
KeyRequest keyRequest = mediaDrm.getKeyRequest(scope, schemeInitData, schemeMimeType, keyType,
|
||||
optionalKeyRequestParameters);
|
||||
postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
|
||||
} catch (NotProvisionedException e) {
|
||||
} catch (Exception e) {
|
||||
onKeysError(e);
|
||||
}
|
||||
}
|
||||
|
|
@ -393,15 +551,31 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
|||
}
|
||||
|
||||
try {
|
||||
mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
|
||||
state = STATE_OPENED_WITH_KEYS;
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onDrmKeysLoaded();
|
||||
}
|
||||
});
|
||||
if (mode == MODE_RELEASE) {
|
||||
mediaDrm.provideKeyResponse(offlineLicenseKeySetId, (byte[]) response);
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onDrmKeysRemoved();
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
byte[] keySetId = mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
|
||||
if ((mode == MODE_DOWNLOAD || (mode == MODE_PLAYBACK && offlineLicenseKeySetId != null))
|
||||
&& keySetId != null && keySetId.length != 0) {
|
||||
offlineLicenseKeySetId = keySetId;
|
||||
}
|
||||
state = STATE_OPENED_WITH_KEYS;
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
eventListener.onDrmKeysLoaded();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
onKeysError(e);
|
||||
|
|
@ -417,7 +591,7 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
|||
}
|
||||
|
||||
private void onError(final Exception e) {
|
||||
lastException = e;
|
||||
lastException = new DrmSessionException(e);
|
||||
if (eventHandler != null && eventListener != null) {
|
||||
eventHandler.post(new Runnable() {
|
||||
@Override
|
||||
|
|
@ -446,11 +620,16 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
|||
}
|
||||
switch (msg.what) {
|
||||
case MediaDrm.EVENT_KEY_REQUIRED:
|
||||
postKeyRequest();
|
||||
doLicense();
|
||||
break;
|
||||
case MediaDrm.EVENT_KEY_EXPIRED:
|
||||
state = STATE_OPENED;
|
||||
onError(new KeysExpiredException());
|
||||
// When an already expired key is loaded MediaDrm sends this event immediately. Ignore
|
||||
// this event if the state isn't STATE_OPENED_WITH_KEYS yet which means we're still
|
||||
// waiting for key response.
|
||||
if (state == STATE_OPENED_WITH_KEYS) {
|
||||
state = STATE_OPENED;
|
||||
onError(new KeysExpiredException());
|
||||
}
|
||||
break;
|
||||
case MediaDrm.EVENT_PROVISION_REQUIRED:
|
||||
state = STATE_OPENED;
|
||||
|
|
@ -466,7 +645,9 @@ public class StreamingDrmSessionManager<T extends ExoMediaCrypto> implements Drm
|
|||
@Override
|
||||
public void onEvent(ExoMediaDrm<? extends T> md, byte[] sessionId, int event, int extra,
|
||||
byte[] data) {
|
||||
mediaDrmHandler.sendEmptyMessage(event);
|
||||
if (mode == MODE_PLAYBACK) {
|
||||
mediaDrmHandler.sendEmptyMessage(event);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -16,9 +16,11 @@
|
|||
package com.google.android.exoplayer2.drm;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.MediaDrm;
|
||||
import android.support.annotation.IntDef;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A DRM session.
|
||||
|
|
@ -26,6 +28,15 @@ import java.lang.annotation.RetentionPolicy;
|
|||
@TargetApi(16)
|
||||
public interface DrmSession<T extends ExoMediaCrypto> {
|
||||
|
||||
/** Wraps the exception which is the cause of the error state. */
|
||||
class DrmSessionException extends Exception {
|
||||
|
||||
DrmSessionException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The state of the DRM session.
|
||||
*/
|
||||
|
|
@ -96,6 +107,26 @@ public interface DrmSession<T extends ExoMediaCrypto> {
|
|||
*
|
||||
* @return An exception if the state is {@link #STATE_ERROR}. Null otherwise.
|
||||
*/
|
||||
Exception getError();
|
||||
DrmSessionException getError();
|
||||
|
||||
/**
|
||||
* Returns an informative description of the key status for the session. The status is in the form
|
||||
* of {name, value} pairs.
|
||||
*
|
||||
* <p>Since DRM license policies vary by vendor, the specific status field names are determined by
|
||||
* each DRM vendor. Refer to your DRM provider documentation for definitions of the field names
|
||||
* for a particular DRM engine plugin.
|
||||
*
|
||||
* @return A map of key status.
|
||||
* @throws IllegalStateException If called when the session isn't opened.
|
||||
* @see MediaDrm#queryKeyStatus(byte[])
|
||||
*/
|
||||
Map<String, String> queryKeyStatus();
|
||||
|
||||
/**
|
||||
* Returns the key set id of the offline license loaded into this session, if there is one. Null
|
||||
* otherwise.
|
||||
*/
|
||||
byte[] getOfflineLicenseKeySetId();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
|
|||
try {
|
||||
return Util.toByteArray(inputStream);
|
||||
} finally {
|
||||
inputStream.close();
|
||||
Util.closeQuietly(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,315 @@
|
|||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.google.android.exoplayer2.drm;
|
||||
|
||||
import android.media.MediaDrm;
|
||||
import android.net.Uri;
|
||||
import android.os.ConditionVariable;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.util.Pair;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.EventListener;
|
||||
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager.Mode;
|
||||
import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor;
|
||||
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
|
||||
import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper;
|
||||
import com.google.android.exoplayer2.source.chunk.InitializationChunk;
|
||||
import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;
|
||||
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
|
||||
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
|
||||
import com.google.android.exoplayer2.source.dash.manifest.Period;
|
||||
import com.google.android.exoplayer2.source.dash.manifest.RangedUri;
|
||||
import com.google.android.exoplayer2.source.dash.manifest.Representation;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSourceInputStream;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* Helper class to download, renew and release offline licenses. It utilizes {@link
|
||||
* DefaultDrmSessionManager}.
|
||||
*/
|
||||
public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
|
||||
|
||||
private final ConditionVariable conditionVariable;
|
||||
private final DefaultDrmSessionManager<T> drmSessionManager;
|
||||
private final HandlerThread handlerThread;
|
||||
|
||||
/**
|
||||
* Helper method to download a DASH manifest.
|
||||
*
|
||||
* @param dataSource The {@link HttpDataSource} from which the manifest should be read.
|
||||
* @param manifestUriString The URI of the manifest to be read.
|
||||
* @return An instance of {@link DashManifest}.
|
||||
* @throws IOException If an error occurs reading data from the stream.
|
||||
* @see DashManifestParser
|
||||
*/
|
||||
public static DashManifest downloadManifest(HttpDataSource dataSource, String manifestUriString)
|
||||
throws IOException {
|
||||
DataSourceInputStream inputStream = new DataSourceInputStream(
|
||||
dataSource, new DataSpec(Uri.parse(manifestUriString)));
|
||||
try {
|
||||
inputStream.open();
|
||||
DashManifestParser parser = new DashManifestParser();
|
||||
return parser.parse(dataSource.getUri(), inputStream);
|
||||
} finally {
|
||||
inputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new instance which uses Widevine CDM. Call {@link #releaseResources()} when
|
||||
* you're done with the helper instance.
|
||||
*
|
||||
* @param licenseUrl The default license URL.
|
||||
* @param httpDataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
|
||||
* @return A new instance which uses Widevine CDM.
|
||||
* @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be
|
||||
* instantiated.
|
||||
*/
|
||||
public static OfflineLicenseHelper<FrameworkMediaCrypto> newWidevineInstance(
|
||||
String licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException {
|
||||
return newWidevineInstance(
|
||||
new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory, null), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new instance which uses Widevine CDM. Call {@link #releaseResources()} when
|
||||
* you're done with the helper instance.
|
||||
*
|
||||
* @param callback Performs key and provisioning requests.
|
||||
* @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
|
||||
* to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
|
||||
* @return A new instance which uses Widevine CDM.
|
||||
* @throws UnsupportedDrmException If the Widevine DRM scheme is unsupported or cannot be
|
||||
* instantiated.
|
||||
* @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm,
|
||||
* MediaDrmCallback, HashMap, Handler, EventListener)
|
||||
*/
|
||||
public static OfflineLicenseHelper<FrameworkMediaCrypto> newWidevineInstance(
|
||||
MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters)
|
||||
throws UnsupportedDrmException {
|
||||
return new OfflineLicenseHelper<>(FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), callback,
|
||||
optionalKeyRequestParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance. Call {@link #releaseResources()} when you're done with it.
|
||||
*
|
||||
* @param mediaDrm An underlying {@link ExoMediaDrm} for use by the manager.
|
||||
* @param callback Performs key and provisioning requests.
|
||||
* @param optionalKeyRequestParameters An optional map of parameters to pass as the last argument
|
||||
* to {@link MediaDrm#getKeyRequest(byte[], byte[], String, int, HashMap)}. May be null.
|
||||
* @see DefaultDrmSessionManager#DefaultDrmSessionManager(java.util.UUID, ExoMediaDrm,
|
||||
* MediaDrmCallback, HashMap, Handler, EventListener)
|
||||
*/
|
||||
public OfflineLicenseHelper(ExoMediaDrm<T> mediaDrm, MediaDrmCallback callback,
|
||||
HashMap<String, String> optionalKeyRequestParameters) {
|
||||
handlerThread = new HandlerThread("OfflineLicenseHelper");
|
||||
handlerThread.start();
|
||||
|
||||
conditionVariable = new ConditionVariable();
|
||||
EventListener eventListener = new EventListener() {
|
||||
@Override
|
||||
public void onDrmKeysLoaded() {
|
||||
conditionVariable.open();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmSessionManagerError(Exception e) {
|
||||
conditionVariable.open();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmKeysRestored() {
|
||||
conditionVariable.open();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrmKeysRemoved() {
|
||||
conditionVariable.open();
|
||||
}
|
||||
};
|
||||
drmSessionManager = new DefaultDrmSessionManager<>(C.WIDEVINE_UUID, mediaDrm, callback,
|
||||
optionalKeyRequestParameters, new Handler(handlerThread.getLooper()), eventListener);
|
||||
}
|
||||
|
||||
/** Releases the used resources. */
|
||||
public void releaseResources() {
|
||||
handlerThread.quit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads an offline license.
|
||||
*
|
||||
* @param dataSource The {@link HttpDataSource} to be used for download.
|
||||
* @param manifestUriString The URI of the manifest to be read.
|
||||
* @return The downloaded offline license key set id.
|
||||
* @throws IOException If an error occurs reading data from the stream.
|
||||
* @throws InterruptedException If the thread has been interrupted.
|
||||
* @throws DrmSessionException Thrown when there is an error during DRM session.
|
||||
*/
|
||||
public byte[] download(HttpDataSource dataSource, String manifestUriString)
|
||||
throws IOException, InterruptedException, DrmSessionException {
|
||||
return download(dataSource, downloadManifest(dataSource, manifestUriString));
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads an offline license.
|
||||
*
|
||||
* @param dataSource The {@link HttpDataSource} to be used for download.
|
||||
* @param dashManifest The {@link DashManifest} of the DASH content.
|
||||
* @return The downloaded offline license key set id.
|
||||
* @throws IOException If an error occurs reading data from the stream.
|
||||
* @throws InterruptedException If the thread has been interrupted.
|
||||
* @throws DrmSessionException Thrown when there is an error during DRM session.
|
||||
*/
|
||||
public byte[] download(HttpDataSource dataSource, DashManifest dashManifest)
|
||||
throws IOException, InterruptedException, DrmSessionException {
|
||||
// Get DrmInitData
|
||||
// Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream,
|
||||
// as per DASH IF Interoperability Recommendations V3.0, 7.5.3.
|
||||
if (dashManifest.getPeriodCount() < 1) {
|
||||
return null;
|
||||
}
|
||||
Period period = dashManifest.getPeriod(0);
|
||||
int adaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_VIDEO);
|
||||
if (adaptationSetIndex == C.INDEX_UNSET) {
|
||||
adaptationSetIndex = period.getAdaptationSetIndex(C.TRACK_TYPE_AUDIO);
|
||||
if (adaptationSetIndex == C.INDEX_UNSET) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex);
|
||||
if (adaptationSet.representations.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Representation representation = adaptationSet.representations.get(0);
|
||||
DrmInitData drmInitData = representation.format.drmInitData;
|
||||
if (drmInitData == null) {
|
||||
InitializationChunk initializationChunk = loadInitializationChunk(dataSource, representation);
|
||||
if (initializationChunk == null) {
|
||||
return null;
|
||||
}
|
||||
Format sampleFormat = initializationChunk.getSampleFormat();
|
||||
if (sampleFormat != null) {
|
||||
drmInitData = sampleFormat.drmInitData;
|
||||
}
|
||||
if (drmInitData == null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, null, drmInitData);
|
||||
return drmSessionManager.getOfflineLicenseKeySetId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renews an offline license.
|
||||
*
|
||||
* @param offlineLicenseKeySetId The key set id of the license to be renewed.
|
||||
* @return Renewed offline license key set id.
|
||||
* @throws DrmSessionException Thrown when there is an error during DRM session.
|
||||
*/
|
||||
public byte[] renew(byte[] offlineLicenseKeySetId) throws DrmSessionException {
|
||||
Assertions.checkNotNull(offlineLicenseKeySetId);
|
||||
blockingKeyRequest(DefaultDrmSessionManager.MODE_DOWNLOAD, offlineLicenseKeySetId, null);
|
||||
return drmSessionManager.getOfflineLicenseKeySetId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases an offline license.
|
||||
*
|
||||
* @param offlineLicenseKeySetId The key set id of the license to be released.
|
||||
* @throws DrmSessionException Thrown when there is an error during DRM session.
|
||||
*/
|
||||
public void release(byte[] offlineLicenseKeySetId) throws DrmSessionException {
|
||||
Assertions.checkNotNull(offlineLicenseKeySetId);
|
||||
blockingKeyRequest(DefaultDrmSessionManager.MODE_RELEASE, offlineLicenseKeySetId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns license and playback durations remaining in seconds of the given offline license.
|
||||
*
|
||||
* @param offlineLicenseKeySetId The key set id of the license.
|
||||
*/
|
||||
public Pair<Long, Long> getLicenseDurationRemainingSec(byte[] offlineLicenseKeySetId)
|
||||
throws DrmSessionException {
|
||||
Assertions.checkNotNull(offlineLicenseKeySetId);
|
||||
DrmSession<T> session = openBlockingKeyRequest(DefaultDrmSessionManager.MODE_QUERY,
|
||||
offlineLicenseKeySetId, null);
|
||||
Pair<Long, Long> licenseDurationRemainingSec =
|
||||
WidevineUtil.getLicenseDurationRemainingSec(drmSessionManager);
|
||||
drmSessionManager.releaseSession(session);
|
||||
return licenseDurationRemainingSec;
|
||||
}
|
||||
|
||||
private void blockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId,
|
||||
DrmInitData drmInitData) throws DrmSessionException {
|
||||
DrmSession<T> session = openBlockingKeyRequest(licenseMode, offlineLicenseKeySetId,
|
||||
drmInitData);
|
||||
DrmSessionException error = session.getError();
|
||||
if (error != null) {
|
||||
throw error;
|
||||
}
|
||||
drmSessionManager.releaseSession(session);
|
||||
}
|
||||
|
||||
private DrmSession<T> openBlockingKeyRequest(@Mode int licenseMode, byte[] offlineLicenseKeySetId,
|
||||
DrmInitData drmInitData) {
|
||||
drmSessionManager.setMode(licenseMode, offlineLicenseKeySetId);
|
||||
conditionVariable.close();
|
||||
DrmSession<T> session = drmSessionManager.acquireSession(handlerThread.getLooper(),
|
||||
drmInitData);
|
||||
// Block current thread until key loading is finished
|
||||
conditionVariable.block();
|
||||
return session;
|
||||
}
|
||||
|
||||
private static InitializationChunk loadInitializationChunk(final DataSource dataSource,
|
||||
final Representation representation) throws IOException, InterruptedException {
|
||||
RangedUri rangedUri = representation.getInitializationUri();
|
||||
if (rangedUri == null) {
|
||||
return null;
|
||||
}
|
||||
DataSpec dataSpec = new DataSpec(rangedUri.resolveUri(representation.baseUrl), rangedUri.start,
|
||||
rangedUri.length, representation.getCacheKey());
|
||||
InitializationChunk initializationChunk = new InitializationChunk(dataSource, dataSpec,
|
||||
representation.format, C.SELECTION_REASON_UNKNOWN, null /* trackSelectionData */,
|
||||
newWrappedExtractor(representation.format));
|
||||
initializationChunk.load();
|
||||
return initializationChunk;
|
||||
}
|
||||
|
||||
private static ChunkExtractorWrapper newWrappedExtractor(final Format format) {
|
||||
final String mimeType = format.containerMimeType;
|
||||
final boolean isWebm = mimeType.startsWith(MimeTypes.VIDEO_WEBM)
|
||||
|| mimeType.startsWith(MimeTypes.AUDIO_WEBM);
|
||||
final Extractor extractor = isWebm ? new MatroskaExtractor() : new FragmentedMp4Extractor();
|
||||
return new ChunkExtractorWrapper(extractor, format, false /* preferManifestDrmInitData */,
|
||||
false /* resendFormatOnInit */);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.android.exoplayer2.drm;
|
||||
|
||||
import android.util.Pair;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Utility methods for Widevine.
|
||||
*/
|
||||
public final class WidevineUtil {
|
||||
|
||||
/** Widevine specific key status field name for the remaining license duration, in seconds. */
|
||||
public static final String PROPERTY_LICENSE_DURATION_REMAINING = "LicenseDurationRemaining";
|
||||
/** Widevine specific key status field name for the remaining playback duration, in seconds. */
|
||||
public static final String PROPERTY_PLAYBACK_DURATION_REMAINING = "PlaybackDurationRemaining";
|
||||
|
||||
private WidevineUtil() {}
|
||||
|
||||
/**
|
||||
* Returns license and playback durations remaining in seconds.
|
||||
*
|
||||
* @return A {@link Pair} consisting of the remaining license and playback durations in seconds.
|
||||
* @throws IllegalStateException If called when a session isn't opened.
|
||||
* @param drmSession
|
||||
*/
|
||||
public static Pair<Long, Long> getLicenseDurationRemainingSec(DrmSession drmSession) {
|
||||
Map<String, String> keyStatus = drmSession.queryKeyStatus();
|
||||
return new Pair<>(
|
||||
getDurationRemainingSec(keyStatus, PROPERTY_LICENSE_DURATION_REMAINING),
|
||||
getDurationRemainingSec(keyStatus, PROPERTY_PLAYBACK_DURATION_REMAINING));
|
||||
}
|
||||
|
||||
private static long getDurationRemainingSec(Map<String, String> keyStatus, String property) {
|
||||
if (keyStatus != null) {
|
||||
try {
|
||||
String value = keyStatus.get(property);
|
||||
if (value != null) {
|
||||
return Long.parseLong(value);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// do nothing.
|
||||
}
|
||||
}
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -226,13 +226,32 @@ public final class DefaultTrackOutput implements TrackOutput {
|
|||
}
|
||||
|
||||
/**
|
||||
* Attempts to skip to the keyframe before the specified time, if it's present in the buffer.
|
||||
* Attempts to skip to the keyframe before or at the specified time. Succeeds only if the buffer
|
||||
* contains a keyframe with a timestamp of {@code timeUs} or earlier, and if {@code timeUs} falls
|
||||
* within the currently buffered media.
|
||||
* <p>
|
||||
* This method is equivalent to {@code skipToKeyframeBefore(timeUs, false)}.
|
||||
*
|
||||
* @param timeUs The seek time.
|
||||
* @return Whether the skip was successful.
|
||||
*/
|
||||
public boolean skipToKeyframeBefore(long timeUs) {
|
||||
long nextOffset = infoQueue.skipToKeyframeBefore(timeUs);
|
||||
return skipToKeyframeBefore(timeUs, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to skip to the keyframe before or at the specified time. Succeeds only if the buffer
|
||||
* contains a keyframe with a timestamp of {@code timeUs} or earlier. If
|
||||
* {@code allowTimeBeyondBuffer} is {@code false} then it is also required that {@code timeUs}
|
||||
* falls within the buffer.
|
||||
*
|
||||
* @param timeUs The seek time.
|
||||
* @param allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end
|
||||
* of the buffer.
|
||||
* @return Whether the skip was successful.
|
||||
*/
|
||||
public boolean skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) {
|
||||
long nextOffset = infoQueue.skipToKeyframeBefore(timeUs, allowTimeBeyondBuffer);
|
||||
if (nextOffset == C.POSITION_UNSET) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -246,7 +265,8 @@ public final class DefaultTrackOutput implements TrackOutput {
|
|||
* @param formatHolder A {@link FormatHolder} to populate in the case of reading a format.
|
||||
* @param buffer A {@link DecoderInputBuffer} to populate in the case of reading a sample or the
|
||||
* end of the stream. If the end of the stream has been reached, the
|
||||
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.
|
||||
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer. May be null if the
|
||||
* caller requires that the format of the stream be read even if it's not changing.
|
||||
* @param loadingFinished True if an empty queue should be considered the end of the stream.
|
||||
* @param decodeOnlyUntilUs If a buffer is read, the {@link C#BUFFER_FLAG_DECODE_ONLY} flag will
|
||||
* be set if the buffer's timestamp is less than this value.
|
||||
|
|
@ -732,7 +752,8 @@ public final class DefaultTrackOutput implements TrackOutput {
|
|||
* about the sample, but not its data. The size and absolute position of the data in the
|
||||
* rolling buffer is stored in {@code extrasHolder}, along with an encryption id if present
|
||||
* and the absolute position of the first byte that may still be required after the current
|
||||
* sample has been read.
|
||||
* sample has been read. May be null if the caller requires that the format of the stream be
|
||||
* read even if it's not changing.
|
||||
* @param downstreamFormat The current downstream {@link Format}. If the format of the next
|
||||
* sample is different to the current downstream format then a format will be read.
|
||||
* @param extrasHolder The holder into which extra sample information should be written.
|
||||
|
|
@ -742,14 +763,14 @@ public final class DefaultTrackOutput implements TrackOutput {
|
|||
public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
|
||||
Format downstreamFormat, BufferExtrasHolder extrasHolder) {
|
||||
if (queueSize == 0) {
|
||||
if (upstreamFormat != null && upstreamFormat != downstreamFormat) {
|
||||
if (upstreamFormat != null && (buffer == null || upstreamFormat != downstreamFormat)) {
|
||||
formatHolder.format = upstreamFormat;
|
||||
return C.RESULT_FORMAT_READ;
|
||||
}
|
||||
return C.RESULT_NOTHING_READ;
|
||||
}
|
||||
|
||||
if (formats[relativeReadIndex] != downstreamFormat) {
|
||||
if (buffer == null || formats[relativeReadIndex] != downstreamFormat) {
|
||||
formatHolder.format = formats[relativeReadIndex];
|
||||
return C.RESULT_FORMAT_READ;
|
||||
}
|
||||
|
|
@ -775,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 allowTimeBeyondBuffer Whether the skip can succeed if {@code timeUs} is beyond the end
|
||||
* of the buffer.
|
||||
* @return The offset of the keyframe's data if the keyframe was present.
|
||||
* {@link C#POSITION_UNSET} otherwise.
|
||||
*/
|
||||
public synchronized long skipToKeyframeBefore(long timeUs) {
|
||||
public synchronized long skipToKeyframeBefore(long timeUs, boolean allowTimeBeyondBuffer) {
|
||||
if (queueSize == 0 || timeUs < timesUs[relativeReadIndex]) {
|
||||
return C.POSITION_UNSET;
|
||||
}
|
||||
|
||||
int lastWriteIndex = (relativeWriteIndex == 0 ? capacity : relativeWriteIndex) - 1;
|
||||
long lastTimeUs = timesUs[lastWriteIndex];
|
||||
if (timeUs > lastTimeUs) {
|
||||
if (timeUs > largestQueuedTimestampUs && !allowTimeBeyondBuffer) {
|
||||
return C.POSITION_UNSET;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -529,11 +529,9 @@ public final class MatroskaExtractor implements Extractor {
|
|||
}
|
||||
break;
|
||||
case ID_TRACK_ENTRY:
|
||||
if (tracks.get(currentTrack.number) == null && isCodecSupported(currentTrack.codecId)) {
|
||||
if (isCodecSupported(currentTrack.codecId)) {
|
||||
currentTrack.initializeOutput(extractorOutput, currentTrack.number);
|
||||
tracks.put(currentTrack.number, currentTrack);
|
||||
} else {
|
||||
// We've seen this track entry before, or the codec is unsupported. Do nothing.
|
||||
}
|
||||
currentTrack = null;
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ import java.util.List;
|
|||
public static final int TYPE_mean = Util.getIntegerCodeForString("mean");
|
||||
public static final int TYPE_name = Util.getIntegerCodeForString("name");
|
||||
public static final int TYPE_data = Util.getIntegerCodeForString("data");
|
||||
public static final int TYPE_emsg = Util.getIntegerCodeForString("emsg");
|
||||
public static final int TYPE_st3d = Util.getIntegerCodeForString("st3d");
|
||||
public static final int TYPE_sv3d = Util.getIntegerCodeForString("sv3d");
|
||||
public static final int TYPE_proj = Util.getIntegerCodeForString("proj");
|
||||
|
|
@ -134,6 +135,7 @@ import java.util.List;
|
|||
public static final int TYPE_vp09 = Util.getIntegerCodeForString("vp09");
|
||||
public static final int TYPE_vpcC = Util.getIntegerCodeForString("vpcC");
|
||||
public static final int TYPE_camm = Util.getIntegerCodeForString("camm");
|
||||
public static final int TYPE_alac = Util.getIntegerCodeForString("alac");
|
||||
|
||||
public final int type;
|
||||
|
||||
|
|
|
|||
|
|
@ -604,7 +604,7 @@ import java.util.List;
|
|||
|| childAtomType == Atom.TYPE_dtsh || childAtomType == Atom.TYPE_dtsl
|
||||
|| childAtomType == Atom.TYPE_samr || childAtomType == Atom.TYPE_sawb
|
||||
|| 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,
|
||||
language, isQuickTime, drmInitData, out, i);
|
||||
} else if (childAtomType == Atom.TYPE_TTML) {
|
||||
|
|
@ -839,6 +839,8 @@ import java.util.List;
|
|||
mimeType = MimeTypes.AUDIO_RAW;
|
||||
} else if (atomType == Atom.TYPE__mp3) {
|
||||
mimeType = MimeTypes.AUDIO_MPEG;
|
||||
} else if (atomType == Atom.TYPE_alac) {
|
||||
mimeType = MimeTypes.AUDIO_ALAC;
|
||||
}
|
||||
|
||||
byte[] initializationData = null;
|
||||
|
|
@ -876,6 +878,10 @@ import java.util.List;
|
|||
out.format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
|
||||
Format.NO_VALUE, Format.NO_VALUE, channelCount, sampleRate, null, drmInitData, 0,
|
||||
language);
|
||||
} else if (childAtomType == Atom.TYPE_alac) {
|
||||
initializationData = new byte[childAtomSize];
|
||||
parent.setPosition(childPosition);
|
||||
parent.readBytes(initializationData, 0, childAtomSize);
|
||||
}
|
||||
childPosition += childAtomSize;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import android.util.Log;
|
|||
import android.util.Pair;
|
||||
import android.util.SparseArray;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData.SchemeData;
|
||||
|
|
@ -30,20 +31,22 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
|||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
import com.google.android.exoplayer2.extractor.SeekMap;
|
||||
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.mp4.Atom.ContainerAtom;
|
||||
import com.google.android.exoplayer2.extractor.mp4.Atom.LeafAtom;
|
||||
import com.google.android.exoplayer2.text.cea.CeaUtil;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.NalUnitUtil;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Stack;
|
||||
import java.util.UUID;
|
||||
|
|
@ -65,15 +68,13 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
|
||||
};
|
||||
|
||||
private static final String TAG = "FragmentedMp4Extractor";
|
||||
private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig");
|
||||
|
||||
/**
|
||||
* Flags controlling the behavior of the extractor.
|
||||
*/
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef(flag = true, value = {FLAG_WORKAROUND_EVERY_VIDEO_FRAME_IS_SYNC_FRAME,
|
||||
FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_SIDELOADED})
|
||||
FLAG_WORKAROUND_IGNORE_TFDT_BOX, FLAG_ENABLE_EMSG_TRACK, FLAG_ENABLE_CEA608_TRACK,
|
||||
FLAG_SIDELOADED})
|
||||
public @interface Flags {}
|
||||
/**
|
||||
* Flag to work around an issue in some video streams where every frame is marked as a sync frame.
|
||||
|
|
@ -87,12 +88,25 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
* Flag to ignore any tfdt boxes in the stream.
|
||||
*/
|
||||
public static final int FLAG_WORKAROUND_IGNORE_TFDT_BOX = 2;
|
||||
/**
|
||||
* Flag to indicate that the extractor should output an event message metadata track. Any event
|
||||
* messages in the stream will be delivered as samples to this track.
|
||||
*/
|
||||
public static final int FLAG_ENABLE_EMSG_TRACK = 4;
|
||||
/**
|
||||
* Flag to indicate that the extractor should output a CEA-608 text track. Any CEA-608 messages
|
||||
* contained within SEI NAL units in the stream will be delivered as samples to this track.
|
||||
*/
|
||||
public static final int FLAG_ENABLE_CEA608_TRACK = 8;
|
||||
/**
|
||||
* Flag to indicate that the {@link Track} was sideloaded, instead of being declared by the MP4
|
||||
* container.
|
||||
*/
|
||||
private static final int FLAG_SIDELOADED = 4;
|
||||
private static final int FLAG_SIDELOADED = 16;
|
||||
|
||||
private static final String TAG = "FragmentedMp4Extractor";
|
||||
private static final int SAMPLE_GROUP_TYPE_seig = Util.getIntegerCodeForString("seig");
|
||||
private static final int NAL_UNIT_TYPE_SEI = 6; // Supplemental enhancement information
|
||||
private static final byte[] PIFF_SAMPLE_ENCRYPTION_BOX_EXTENDED_TYPE =
|
||||
new byte[] {-94, 57, 79, 82, 90, -101, 79, 20, -94, 68, 108, 66, 124, 100, -115, -12};
|
||||
|
||||
|
|
@ -114,6 +128,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
// Temporary arrays.
|
||||
private final ParsableByteArray nalStartCode;
|
||||
private final ParsableByteArray nalLength;
|
||||
private final ParsableByteArray nalPayload;
|
||||
private final ParsableByteArray encryptionSignalByte;
|
||||
|
||||
// Adjusts sample timestamps.
|
||||
|
|
@ -123,6 +138,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
private final ParsableByteArray atomHeader;
|
||||
private final byte[] extendedTypeScratch;
|
||||
private final Stack<ContainerAtom> containerAtoms;
|
||||
private final LinkedList<MetadataSampleInfo> pendingMetadataSampleInfos;
|
||||
|
||||
private int parserState;
|
||||
private int atomType;
|
||||
|
|
@ -130,8 +146,10 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
private int atomHeaderBytesRead;
|
||||
private ParsableByteArray atomData;
|
||||
private long endOfMdatPosition;
|
||||
private int pendingMetadataSampleBytes;
|
||||
|
||||
private long durationUs;
|
||||
private long segmentIndexEarliestPresentationTimeUs;
|
||||
private TrackBundle currentTrackBundle;
|
||||
private int sampleSize;
|
||||
private int sampleBytesWritten;
|
||||
|
|
@ -139,6 +157,8 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
|
||||
// Extractor output.
|
||||
private ExtractorOutput extractorOutput;
|
||||
private TrackOutput eventMessageTrackOutput;
|
||||
private TrackOutput cea608TrackOutput;
|
||||
|
||||
// Whether extractorOutput.seekMap has been called.
|
||||
private boolean haveOutputSeekMap;
|
||||
|
|
@ -169,11 +189,14 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
|
||||
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
|
||||
nalLength = new ParsableByteArray(4);
|
||||
nalPayload = new ParsableByteArray(1);
|
||||
encryptionSignalByte = new ParsableByteArray(1);
|
||||
extendedTypeScratch = new byte[16];
|
||||
containerAtoms = new Stack<>();
|
||||
pendingMetadataSampleInfos = new LinkedList<>();
|
||||
trackBundles = new SparseArray<>();
|
||||
durationUs = C.TIME_UNSET;
|
||||
segmentIndexEarliestPresentationTimeUs = C.TIME_UNSET;
|
||||
enterReadingAtomHeaderState();
|
||||
}
|
||||
|
||||
|
|
@ -189,6 +212,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
TrackBundle bundle = new TrackBundle(output.track(0));
|
||||
bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0));
|
||||
trackBundles.put(0, bundle);
|
||||
maybeInitExtraTracks();
|
||||
extractorOutput.endTracks();
|
||||
}
|
||||
}
|
||||
|
|
@ -199,6 +223,8 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
for (int i = 0; i < trackCount; i++) {
|
||||
trackBundles.valueAt(i).reset();
|
||||
}
|
||||
pendingMetadataSampleInfos.clear();
|
||||
pendingMetadataSampleBytes = 0;
|
||||
containerAtoms.clear();
|
||||
enterReadingAtomHeaderState();
|
||||
}
|
||||
|
|
@ -257,6 +283,10 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
atomSize = atomHeader.readUnsignedLongToLong();
|
||||
}
|
||||
|
||||
if (atomSize < atomHeaderBytesRead) {
|
||||
throw new ParserException("Atom size less than header length (unsupported).");
|
||||
}
|
||||
|
||||
long atomPosition = input.getPosition() - atomHeaderBytesRead;
|
||||
if (atomType == Atom.TYPE_moof) {
|
||||
// The data positions may be updated when parsing the tfhd/trun.
|
||||
|
|
@ -332,9 +362,12 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
if (!containerAtoms.isEmpty()) {
|
||||
containerAtoms.peek().add(leaf);
|
||||
} else if (leaf.type == Atom.TYPE_sidx) {
|
||||
ChunkIndex segmentIndex = parseSidx(leaf.data, inputPosition);
|
||||
extractorOutput.seekMap(segmentIndex);
|
||||
Pair<Long, ChunkIndex> result = parseSidx(leaf.data, inputPosition);
|
||||
segmentIndexEarliestPresentationTimeUs = result.first;
|
||||
extractorOutput.seekMap(result.second);
|
||||
haveOutputSeekMap = true;
|
||||
} else if (leaf.type == Atom.TYPE_emsg) {
|
||||
onEmsgLeafAtomRead(leaf.data);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -387,18 +420,19 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
// We need to create the track bundles.
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
Track track = tracks.valueAt(i);
|
||||
trackBundles.put(track.id, new TrackBundle(extractorOutput.track(i)));
|
||||
TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i));
|
||||
trackBundle.init(track, defaultSampleValuesArray.get(track.id));
|
||||
trackBundles.put(track.id, trackBundle);
|
||||
durationUs = Math.max(durationUs, track.durationUs);
|
||||
}
|
||||
maybeInitExtraTracks();
|
||||
extractorOutput.endTracks();
|
||||
} else {
|
||||
Assertions.checkState(trackBundles.size() == trackCount);
|
||||
}
|
||||
|
||||
// Initialization of tracks and default sample values.
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
Track track = tracks.valueAt(i);
|
||||
trackBundles.get(track.id).init(track, defaultSampleValuesArray.get(track.id));
|
||||
for (int i = 0; i < trackCount; i++) {
|
||||
Track track = tracks.valueAt(i);
|
||||
trackBundles.get(track.id).init(track, defaultSampleValuesArray.get(track.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -413,6 +447,51 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
}
|
||||
}
|
||||
|
||||
private void maybeInitExtraTracks() {
|
||||
if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0 && eventMessageTrackOutput == null) {
|
||||
eventMessageTrackOutput = extractorOutput.track(trackBundles.size());
|
||||
eventMessageTrackOutput.format(Format.createSampleFormat(null, MimeTypes.APPLICATION_EMSG,
|
||||
Format.OFFSET_SAMPLE_RELATIVE));
|
||||
}
|
||||
if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutput == null) {
|
||||
cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1);
|
||||
cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608,
|
||||
null, Format.NO_VALUE, 0, null, null));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an emsg atom (defined in 23009-1).
|
||||
*/
|
||||
private void onEmsgLeafAtomRead(ParsableByteArray atom) {
|
||||
if (eventMessageTrackOutput == null) {
|
||||
return;
|
||||
}
|
||||
// Parse the event's presentation time delta.
|
||||
atom.setPosition(Atom.FULL_HEADER_SIZE);
|
||||
atom.readNullTerminatedString(); // schemeIdUri
|
||||
atom.readNullTerminatedString(); // value
|
||||
long timescale = atom.readUnsignedInt();
|
||||
long presentationTimeDeltaUs =
|
||||
Util.scaleLargeTimestamp(atom.readUnsignedInt(), C.MICROS_PER_SECOND, timescale);
|
||||
// Output the sample data.
|
||||
atom.setPosition(Atom.FULL_HEADER_SIZE);
|
||||
int sampleSize = atom.bytesLeft();
|
||||
eventMessageTrackOutput.sampleData(atom, sampleSize);
|
||||
// Output the sample metadata.
|
||||
if (segmentIndexEarliestPresentationTimeUs != C.TIME_UNSET) {
|
||||
// We can output the sample metadata immediately.
|
||||
eventMessageTrackOutput.sampleMetadata(
|
||||
segmentIndexEarliestPresentationTimeUs + presentationTimeDeltaUs,
|
||||
C.BUFFER_FLAG_KEY_FRAME, sampleSize, 0 /* offset */, null);
|
||||
} else {
|
||||
// We need the first sample timestamp in the segment before we can output the metadata.
|
||||
pendingMetadataSampleInfos.addLast(
|
||||
new MetadataSampleInfo(presentationTimeDeltaUs, sampleSize));
|
||||
pendingMetadataSampleBytes += sampleSize;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a trex atom (defined in 14496-12).
|
||||
*/
|
||||
|
|
@ -624,7 +703,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
DefaultSampleValues defaultSampleValues = trackBundle.defaultSampleValues;
|
||||
int defaultSampleDescriptionIndex =
|
||||
((atomFlags & 0x02 /* default_sample_description_index_present */) != 0)
|
||||
? tfhd.readUnsignedIntToInt() - 1 : defaultSampleValues.sampleDescriptionIndex;
|
||||
? tfhd.readUnsignedIntToInt() - 1 : defaultSampleValues.sampleDescriptionIndex;
|
||||
int defaultSampleDuration = ((atomFlags & 0x08 /* default_sample_duration_present */) != 0)
|
||||
? tfhd.readUnsignedIntToInt() : defaultSampleValues.duration;
|
||||
int defaultSampleSize = ((atomFlags & 0x10 /* default_sample_size_present */) != 0)
|
||||
|
|
@ -828,8 +907,13 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
|
||||
/**
|
||||
* Parses a sidx atom (defined in 14496-12).
|
||||
*
|
||||
* @param atom The atom data.
|
||||
* @param inputPosition The input position of the first byte after the atom.
|
||||
* @return A pair consisting of the earliest presentation time in microseconds, and the parsed
|
||||
* {@link ChunkIndex}.
|
||||
*/
|
||||
private static ChunkIndex parseSidx(ParsableByteArray atom, long inputPosition)
|
||||
private static Pair<Long, ChunkIndex> parseSidx(ParsableByteArray atom, long inputPosition)
|
||||
throws ParserException {
|
||||
atom.setPosition(Atom.HEADER_SIZE);
|
||||
int fullAtom = atom.readInt();
|
||||
|
|
@ -846,6 +930,8 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
earliestPresentationTime = atom.readUnsignedLongToLong();
|
||||
offset += atom.readUnsignedLongToLong();
|
||||
}
|
||||
long earliestPresentationTimeUs = Util.scaleLargeTimestamp(earliestPresentationTime,
|
||||
C.MICROS_PER_SECOND, timescale);
|
||||
|
||||
atom.skipBytes(2);
|
||||
|
||||
|
|
@ -856,7 +942,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
long[] timesUs = new long[referenceCount];
|
||||
|
||||
long time = earliestPresentationTime;
|
||||
long timeUs = Util.scaleLargeTimestamp(time, C.MICROS_PER_SECOND, timescale);
|
||||
long timeUs = earliestPresentationTimeUs;
|
||||
for (int i = 0; i < referenceCount; i++) {
|
||||
int firstInt = atom.readInt();
|
||||
|
||||
|
|
@ -880,7 +966,8 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
offset += sizes[i];
|
||||
}
|
||||
|
||||
return new ChunkIndex(sizes, offsets, durationsUs, timesUs);
|
||||
return Pair.create(earliestPresentationTimeUs,
|
||||
new ChunkIndex(sizes, offsets, durationsUs, timesUs));
|
||||
}
|
||||
|
||||
private void readEncryptionData(ExtractorInput input) throws IOException, InterruptedException {
|
||||
|
|
@ -942,13 +1029,9 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
// We skip bytes preceding the next sample to read.
|
||||
int bytesToSkip = (int) (nextDataPosition - input.getPosition());
|
||||
if (bytesToSkip < 0) {
|
||||
if (nextDataPosition == currentTrackBundle.fragment.atomPosition) {
|
||||
// Assume the sample data must be contiguous in the mdat with no preceeding data.
|
||||
Log.w(TAG, "Offset to sample data was missing.");
|
||||
bytesToSkip = 0;
|
||||
} else {
|
||||
throw new ParserException("Offset to sample data was negative.");
|
||||
}
|
||||
// Assume the sample data must be contiguous in the mdat with no preceding data.
|
||||
Log.w(TAG, "Ignoring negative offset to sample data.");
|
||||
bytesToSkip = 0;
|
||||
}
|
||||
input.skipFully(bytesToSkip);
|
||||
this.currentTrackBundle = currentTrackBundle;
|
||||
|
|
@ -996,6 +1079,26 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
output.sampleData(nalStartCode, 4);
|
||||
sampleBytesWritten += 4;
|
||||
sampleSize += nalUnitLengthFieldLengthDiff;
|
||||
if (cea608TrackOutput != null) {
|
||||
byte[] nalPayloadData = nalPayload.data;
|
||||
// Peek the NAL unit type byte.
|
||||
input.peekFully(nalPayloadData, 0, 1);
|
||||
if ((nalPayloadData[0] & 0x1F) == NAL_UNIT_TYPE_SEI) {
|
||||
// Read the whole SEI NAL unit into nalWrapper, including the NAL unit type byte.
|
||||
nalPayload.reset(sampleCurrentNalBytesRemaining);
|
||||
input.readFully(nalPayloadData, 0, sampleCurrentNalBytesRemaining);
|
||||
// Write the SEI unit straight to the output.
|
||||
output.sampleData(nalPayload, sampleCurrentNalBytesRemaining);
|
||||
sampleBytesWritten += sampleCurrentNalBytesRemaining;
|
||||
sampleCurrentNalBytesRemaining = 0;
|
||||
// Unescape and process the SEI unit.
|
||||
int unescapedLength = NalUnitUtil.unescapeStream(nalPayloadData, nalPayload.limit());
|
||||
nalPayload.setPosition(1); // Skip the NAL unit type byte.
|
||||
nalPayload.setLimit(unescapedLength);
|
||||
CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalPayload,
|
||||
cea608TrackOutput);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Write the payload of the NAL unit.
|
||||
int writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false);
|
||||
|
|
@ -1025,6 +1128,14 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
}
|
||||
output.sampleMetadata(sampleTimeUs, sampleFlags, sampleSize, 0, encryptionKey);
|
||||
|
||||
while (!pendingMetadataSampleInfos.isEmpty()) {
|
||||
MetadataSampleInfo sampleInfo = pendingMetadataSampleInfos.removeFirst();
|
||||
pendingMetadataSampleBytes -= sampleInfo.size;
|
||||
eventMessageTrackOutput.sampleMetadata(
|
||||
sampleTimeUs + sampleInfo.presentationTimeDeltaUs,
|
||||
C.BUFFER_FLAG_KEY_FRAME, sampleInfo.size, pendingMetadataSampleBytes, null);
|
||||
}
|
||||
|
||||
currentTrackBundle.currentSampleIndex++;
|
||||
currentTrackBundle.currentSampleInTrackRun++;
|
||||
if (currentTrackBundle.currentSampleInTrackRun
|
||||
|
|
@ -1130,7 +1241,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
|| atom == Atom.TYPE_trun || atom == Atom.TYPE_pssh || atom == Atom.TYPE_saiz
|
||||
|| atom == Atom.TYPE_saio || atom == Atom.TYPE_senc || atom == Atom.TYPE_uuid
|
||||
|| atom == Atom.TYPE_sbgp || atom == Atom.TYPE_sgpd || atom == Atom.TYPE_elst
|
||||
|| atom == Atom.TYPE_mehd;
|
||||
|| atom == Atom.TYPE_mehd || atom == Atom.TYPE_emsg;
|
||||
}
|
||||
|
||||
/** Returns whether the extractor should decode a container atom with type {@code atom}. */
|
||||
|
|
@ -1140,6 +1251,21 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
|| atom == Atom.TYPE_traf || atom == Atom.TYPE_mvex || atom == Atom.TYPE_edts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds data corresponding to a metadata sample.
|
||||
*/
|
||||
private static final class MetadataSampleInfo {
|
||||
|
||||
public final long presentationTimeDeltaUs;
|
||||
public final int size;
|
||||
|
||||
public MetadataSampleInfo(long presentationTimeDeltaUs, int size) {
|
||||
this.presentationTimeDeltaUs = presentationTimeDeltaUs;
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds data corresponding to a single track.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||
if (atomType == Atom.TYPE_data) {
|
||||
data.skipBytes(8); // version (1), flags (3), empty (4)
|
||||
String value = data.readNullTerminatedString(atomSize - 16);
|
||||
return new TextInformationFrame(id, value);
|
||||
return new TextInformationFrame(id, null, value);
|
||||
}
|
||||
Log.w(TAG, "Failed to parse text attribute: " + Atom.getAtomTypeString(type));
|
||||
return null;
|
||||
|
|
@ -213,7 +213,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||
value = Math.min(1, value);
|
||||
}
|
||||
if (value >= 0) {
|
||||
return isTextInformationFrame ? new TextInformationFrame(id, Integer.toString(value))
|
||||
return isTextInformationFrame ? new TextInformationFrame(id, null, Integer.toString(value))
|
||||
: new CommentFrame(LANGUAGE_UNDEFINED, id, Integer.toString(value));
|
||||
}
|
||||
Log.w(TAG, "Failed to parse uint8 attribute: " + Atom.getAtomTypeString(type));
|
||||
|
|
@ -228,12 +228,12 @@ import com.google.android.exoplayer2.util.Util;
|
|||
data.skipBytes(10); // version (1), flags (3), empty (4), empty (2)
|
||||
int index = data.readUnsignedShort();
|
||||
if (index > 0) {
|
||||
String description = "" + index;
|
||||
String value = "" + index;
|
||||
int count = data.readUnsignedShort();
|
||||
if (count > 0) {
|
||||
description += "/" + count;
|
||||
value += "/" + count;
|
||||
}
|
||||
return new TextInformationFrame(attributeName, description);
|
||||
return new TextInformationFrame(attributeName, null, value);
|
||||
}
|
||||
}
|
||||
Log.w(TAG, "Failed to parse index/count attribute: " + Atom.getAtomTypeString(type));
|
||||
|
|
@ -245,7 +245,7 @@ import com.google.android.exoplayer2.util.Util;
|
|||
String genreString = (0 < genreCode && genreCode <= STANDARD_GENRES.length)
|
||||
? STANDARD_GENRES[genreCode - 1] : null;
|
||||
if (genreString != null) {
|
||||
return new TextInformationFrame("TCON", genreString);
|
||||
return new TextInformationFrame("TCON", null, genreString);
|
||||
}
|
||||
Log.w(TAG, "Failed to parse standard genre code");
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -83,8 +83,11 @@ public final class RawCcExtractor implements Extractor {
|
|||
while (true) {
|
||||
switch (parserState) {
|
||||
case STATE_READING_HEADER:
|
||||
parseHeader(input);
|
||||
parserState = STATE_READING_TIMESTAMP_AND_COUNT;
|
||||
if (parseHeader(input)) {
|
||||
parserState = STATE_READING_TIMESTAMP_AND_COUNT;
|
||||
} else {
|
||||
return RESULT_END_OF_INPUT;
|
||||
}
|
||||
break;
|
||||
case STATE_READING_TIMESTAMP_AND_COUNT:
|
||||
if (parseTimestampAndSampleCount(input)) {
|
||||
|
|
@ -114,14 +117,18 @@ public final class RawCcExtractor implements Extractor {
|
|||
// Do nothing
|
||||
}
|
||||
|
||||
private void parseHeader(ExtractorInput input) throws IOException, InterruptedException {
|
||||
private boolean parseHeader(ExtractorInput input) throws IOException, InterruptedException {
|
||||
dataScratch.reset();
|
||||
input.readFully(dataScratch.data, 0, HEADER_SIZE);
|
||||
if (dataScratch.readInt() != HEADER_ID) {
|
||||
throw new IOException("Input not RawCC");
|
||||
if (input.readFully(dataScratch.data, 0, HEADER_SIZE, true)) {
|
||||
if (dataScratch.readInt() != HEADER_ID) {
|
||||
throw new IOException("Input not RawCC");
|
||||
}
|
||||
version = dataScratch.readUnsignedByte();
|
||||
// no versions use the flag fields yet
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
version = dataScratch.readUnsignedByte();
|
||||
// no versions use the flag fields yet
|
||||
}
|
||||
|
||||
private boolean parseTimestampAndSampleCount(ExtractorInput input) throws IOException,
|
||||
|
|
|
|||
|
|
@ -16,12 +16,11 @@
|
|||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
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.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
|
||||
/**
|
||||
* Parses PES packet data and extracts samples.
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
|||
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
|
||||
import com.google.android.exoplayer2.extractor.PositionHolder;
|
||||
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.util.ParsableBitArray;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@
|
|||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
import com.google.android.exoplayer2.extractor.TimestampAdjuster;
|
||||
import com.google.android.exoplayer2.extractor.TrackOutput;
|
||||
import com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
|
||||
/**
|
||||
* Reads section data.
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@ package com.google.android.exoplayer2.extractor.ts;
|
|||
|
||||
import com.google.android.exoplayer2.C;
|
||||
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.TimestampAdjuster;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -15,10 +15,9 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.extractor.ts;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
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.ParsableByteArray;
|
||||
|
||||
|
|
@ -36,40 +35,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
|||
}
|
||||
|
||||
public void consume(long pesTimeUs, ParsableByteArray seiBuffer) {
|
||||
int b;
|
||||
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);
|
||||
}
|
||||
}
|
||||
CeaUtil.consume(pesTimeUs, seiBuffer, output);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||