mirror of
https://github.com/samsonjs/media.git
synced 2026-04-08 11:45:51 +00:00
commit
86230c4510
211 changed files with 6440 additions and 2584 deletions
|
|
@ -1,5 +1,52 @@
|
|||
# Release notes #
|
||||
|
||||
### r2.3.0 ###
|
||||
|
||||
* GVR extension: Wraps the Google VR Audio SDK to provide spatial audio
|
||||
rendering. You can read more about the GVR extension
|
||||
[here](https://medium.com/google-exoplayer/spatial-audio-with-exoplayer-and-gvr-cecb00e9da5f#.xdjebjd7g).
|
||||
* DASH improvements:
|
||||
* Support embedded CEA-608 closed captions
|
||||
([#2362](https://github.com/google/ExoPlayer/issues/2362)).
|
||||
* Support embedded EMSG events
|
||||
([#2176](https://github.com/google/ExoPlayer/issues/2176)).
|
||||
* Support mspr:pro manifest element
|
||||
([#2386](https://github.com/google/ExoPlayer/issues/2386)).
|
||||
* Correct handling of empty segment indices at the start of live events
|
||||
([#1865](https://github.com/google/ExoPlayer/issues/1865)).
|
||||
* HLS improvements:
|
||||
* Respect initial track selection
|
||||
([#2353](https://github.com/google/ExoPlayer/issues/2353)).
|
||||
* Reduced frequency of media playlist requests when playback position is close
|
||||
to the live edge ([#2548](https://github.com/google/ExoPlayer/issues/2548)).
|
||||
* Exposed the master playlist through ExoPlayer.getCurrentManifest()
|
||||
([#2537](https://github.com/google/ExoPlayer/issues/2537)).
|
||||
* Support CLOSED-CAPTIONS #EXT-X-MEDIA type
|
||||
([#341](https://github.com/google/ExoPlayer/issues/341)).
|
||||
* Fixed handling of negative values in #EXT-X-SUPPORT
|
||||
([#2495](https://github.com/google/ExoPlayer/issues/2495)).
|
||||
* Fixed potential endless buffering state for streams with WebVTT subtitles
|
||||
([#2424](https://github.com/google/ExoPlayer/issues/2424)).
|
||||
* MPEG-TS improvements:
|
||||
* Support for multiple programs.
|
||||
* Support for multiple closed captions and caption service descriptors
|
||||
([#2161](https://github.com/google/ExoPlayer/issues/2161)).
|
||||
* MP3: Add `FLAG_ENABLE_CONSTANT_BITRATE_SEEKING` extractor option to enable
|
||||
constant bitrate seeking in MP3 files that would otherwise be unseekable
|
||||
([#2445](https://github.com/google/ExoPlayer/issues/2445)).
|
||||
* ID3: Better handle malformed ID3 data
|
||||
([#2486](https://github.com/google/ExoPlayer/issues/2486)).
|
||||
* Track selection: Added maxVideoBitrate parameter to DefaultTrackSelector.
|
||||
* DRM: Add support for CENC ClearKey on API level 21+
|
||||
([#2361](https://github.com/google/ExoPlayer/issues/2361)).
|
||||
* DRM: Support dynamic setting of key request headers
|
||||
([#1924](https://github.com/google/ExoPlayer/issues/1924)).
|
||||
* SmoothStreaming: Fixed handling of start_time placeholder
|
||||
([#2447](https://github.com/google/ExoPlayer/issues/2447)).
|
||||
* FLAC extension: Fix proguard configuration
|
||||
([#2427](https://github.com/google/ExoPlayer/issues/2427)).
|
||||
* Misc bugfixes.
|
||||
|
||||
### r2.2.0 ###
|
||||
|
||||
* Demo app: Automatic recovery from BehindLiveWindowException, plus improved
|
||||
|
|
@ -246,6 +293,12 @@ 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.15 ###
|
||||
|
||||
* SmoothStreaming: Fixed handling of start_time placeholder
|
||||
([#2447](https://github.com/google/ExoPlayer/issues/2447)).
|
||||
* Misc bugfixes.
|
||||
|
||||
### r1.5.14 ###
|
||||
|
||||
* Fixed cache failures when using an encrypted cache content index.
|
||||
|
|
|
|||
28
build.gradle
28
build.gradle
|
|
@ -11,16 +11,13 @@
|
|||
// 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.
|
||||
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:2.2.1'
|
||||
classpath 'com.novoda:bintray-release:0.3.4'
|
||||
classpath 'com.android.tools.build:gradle:2.3.0'
|
||||
classpath 'com.novoda:bintray-release:0.4.0'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -29,13 +26,24 @@ allprojects {
|
|||
jcenter()
|
||||
}
|
||||
project.ext {
|
||||
compileSdkVersion=24
|
||||
targetSdkVersion=24
|
||||
buildToolsVersion='23.0.3'
|
||||
releaseRepoName = 'exoplayer'
|
||||
// Important: ExoPlayer specifies a minSdkVersion of 9 because various
|
||||
// components provided by the library may be of use on older devices.
|
||||
// However, please note that the core media playback functionality
|
||||
// provided by the library requires API level 16 or greater.
|
||||
minSdkVersion=9
|
||||
compileSdkVersion=25
|
||||
targetSdkVersion=25
|
||||
buildToolsVersion='25'
|
||||
releaseRepoName = getBintrayRepo()
|
||||
releaseUserOrg = 'google'
|
||||
releaseGroupId = 'com.google.android.exoplayer'
|
||||
releaseVersion = 'r2.2.0'
|
||||
releaseVersion = 'r2.3.0'
|
||||
releaseWebsite = 'https://github.com/google/ExoPlayer'
|
||||
}
|
||||
}
|
||||
|
||||
def getBintrayRepo() {
|
||||
boolean publicRepo = hasProperty('publicRepo') &&
|
||||
property('publicRepo').toBoolean()
|
||||
return publicRepo ? 'exoplayer' : 'exoplayer-test'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
// The demo app does not have translations.
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
|
||||
productFlavors {
|
||||
noExtensions
|
||||
withExtensions
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@
|
|||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.google.android.exoplayer2.demo"
|
||||
android:versionCode="2200"
|
||||
android:versionName="2.2.0">
|
||||
android:versionCode="2300"
|
||||
android:versionName="2.3.0">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
|
|
|||
|
|
@ -277,6 +277,18 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ClearKey DASH",
|
||||
"samples": [
|
||||
{
|
||||
"name": "Big Buck Bunny (CENC ClearKey)",
|
||||
"uri": "http://html5.cablelabs.com:8100/cenc/ck/dash.mpd",
|
||||
"extension": "mpd",
|
||||
"drm_scheme": "cenc",
|
||||
"drm_license_url": "https://wasabeef.jp/demos/cenc-ck-dash.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SmoothStreaming",
|
||||
"samples": [
|
||||
|
|
|
|||
|
|
@ -101,9 +101,6 @@ import java.util.Locale;
|
|||
|
||||
@Override
|
||||
public void onTimelineChanged(Timeline timeline, Object manifest) {
|
||||
if (timeline == null) {
|
||||
return;
|
||||
}
|
||||
int periodCount = timeline.getPeriodCount();
|
||||
int windowCount = timeline.getWindowCount();
|
||||
Log.d(TAG, "sourceInfo [periodCount=" + periodCount + ", windowCount=" + windowCount);
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
|
|||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
|
||||
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.AdaptiveVideoTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelection;
|
||||
|
|
@ -70,8 +70,6 @@ import com.google.android.exoplayer2.util.Util;
|
|||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
|
|
@ -112,7 +110,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||
private DefaultTrackSelector trackSelector;
|
||||
private TrackSelectionHelper trackSelectionHelper;
|
||||
private DebugTextViewHelper debugViewHelper;
|
||||
private boolean playerNeedsSource;
|
||||
private boolean needRetrySource;
|
||||
|
||||
private boolean shouldAutoPlay;
|
||||
private int resumeWindow;
|
||||
|
|
@ -231,7 +229,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||
|
||||
private void initializePlayer() {
|
||||
Intent intent = getIntent();
|
||||
if (player == null) {
|
||||
boolean needNewPlayer = player == null;
|
||||
if (needNewPlayer) {
|
||||
boolean preferExtensionDecoders = intent.getBooleanExtra(PREFER_EXTENSION_DECODERS, false);
|
||||
UUID drmSchemeUuid = intent.hasExtra(DRM_SCHEME_UUID_EXTRA)
|
||||
? UUID.fromString(intent.getStringExtra(DRM_SCHEME_UUID_EXTRA)) : null;
|
||||
|
|
@ -239,19 +238,9 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||
if (drmSchemeUuid != null) {
|
||||
String drmLicenseUrl = intent.getStringExtra(DRM_LICENSE_URL);
|
||||
String[] keyRequestPropertiesArray = intent.getStringArrayExtra(DRM_KEY_REQUEST_PROPERTIES);
|
||||
Map<String, String> keyRequestProperties;
|
||||
if (keyRequestPropertiesArray == null || keyRequestPropertiesArray.length < 2) {
|
||||
keyRequestProperties = null;
|
||||
} else {
|
||||
keyRequestProperties = new HashMap<>();
|
||||
for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
|
||||
keyRequestProperties.put(keyRequestPropertiesArray[i],
|
||||
keyRequestPropertiesArray[i + 1]);
|
||||
}
|
||||
}
|
||||
try {
|
||||
drmSessionManager = buildDrmSessionManager(drmSchemeUuid, drmLicenseUrl,
|
||||
keyRequestProperties);
|
||||
keyRequestPropertiesArray);
|
||||
} catch (UnsupportedDrmException e) {
|
||||
int errorStringId = Util.SDK_INT < 18 ? R.string.error_drm_not_supported
|
||||
: (e.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
|
||||
|
|
@ -267,7 +256,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_ON)
|
||||
: SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF;
|
||||
TrackSelection.Factory videoTrackSelectionFactory =
|
||||
new AdaptiveVideoTrackSelection.Factory(BANDWIDTH_METER);
|
||||
new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
|
||||
trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
|
||||
trackSelectionHelper = new TrackSelectionHelper(trackSelector, videoTrackSelectionFactory);
|
||||
player = ExoPlayerFactory.newSimpleInstance(this, trackSelector, new DefaultLoadControl(),
|
||||
|
|
@ -284,9 +273,8 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||
player.setPlayWhenReady(shouldAutoPlay);
|
||||
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
|
||||
debugViewHelper.start();
|
||||
playerNeedsSource = true;
|
||||
}
|
||||
if (playerNeedsSource) {
|
||||
if (needNewPlayer || needRetrySource) {
|
||||
String action = intent.getAction();
|
||||
Uri[] uris;
|
||||
String[] extensions;
|
||||
|
|
@ -322,14 +310,14 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||
player.seekTo(resumeWindow, resumePosition);
|
||||
}
|
||||
player.prepare(mediaSource, !haveResumePosition, false);
|
||||
playerNeedsSource = false;
|
||||
needRetrySource = false;
|
||||
updateButtonVisibilities();
|
||||
}
|
||||
}
|
||||
|
||||
private MediaSource buildMediaSource(Uri uri, String overrideExtension) {
|
||||
int type = Util.inferContentType(!TextUtils.isEmpty(overrideExtension) ? "." + overrideExtension
|
||||
: uri.getLastPathSegment());
|
||||
int type = TextUtils.isEmpty(overrideExtension) ? Util.inferContentType(uri)
|
||||
: Util.inferContentType("." + overrideExtension);
|
||||
switch (type) {
|
||||
case C.TYPE_SS:
|
||||
return new SsMediaSource(uri, buildDataSourceFactory(false),
|
||||
|
|
@ -349,12 +337,18 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||
}
|
||||
|
||||
private DrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManager(UUID uuid,
|
||||
String licenseUrl, Map<String, String> keyRequestProperties) throws UnsupportedDrmException {
|
||||
String licenseUrl, String[] keyRequestPropertiesArray) throws UnsupportedDrmException {
|
||||
if (Util.SDK_INT < 18) {
|
||||
return null;
|
||||
}
|
||||
HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(licenseUrl,
|
||||
buildHttpDataSourceFactory(false), keyRequestProperties);
|
||||
buildHttpDataSourceFactory(false));
|
||||
if (keyRequestPropertiesArray != null) {
|
||||
for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
|
||||
drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i],
|
||||
keyRequestPropertiesArray[i + 1]);
|
||||
}
|
||||
}
|
||||
return new DefaultDrmSessionManager<>(uuid,
|
||||
FrameworkMediaDrm.newInstance(uuid), drmCallback, null, mainHandler, eventLogger);
|
||||
}
|
||||
|
|
@ -425,7 +419,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||
|
||||
@Override
|
||||
public void onPositionDiscontinuity() {
|
||||
if (playerNeedsSource) {
|
||||
if (needRetrySource) {
|
||||
// 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.
|
||||
|
|
@ -466,7 +460,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||
if (errorString != null) {
|
||||
showToast(errorString);
|
||||
}
|
||||
playerNeedsSource = true;
|
||||
needRetrySource = true;
|
||||
if (isBehindLiveWindow(e)) {
|
||||
clearResumePosition();
|
||||
initializePlayer();
|
||||
|
|
@ -498,7 +492,7 @@ public class PlayerActivity extends Activity implements OnClickListener, ExoPlay
|
|||
private void updateButtonVisibilities() {
|
||||
debugRootView.removeAllViews();
|
||||
|
||||
retryButton.setVisibility(playerNeedsSource ? View.VISIBLE : View.GONE);
|
||||
retryButton.setVisibility(needRetrySource ? View.VISIBLE : View.GONE);
|
||||
debugRootView.addView(retryButton);
|
||||
|
||||
if (player == null) {
|
||||
|
|
|
|||
|
|
@ -262,11 +262,13 @@ public class SampleChooserActivity extends Activity {
|
|||
}
|
||||
|
||||
private UUID getDrmUuid(String typeString) throws ParserException {
|
||||
switch (typeString.toLowerCase()) {
|
||||
switch (Util.toLowerInvariant(typeString)) {
|
||||
case "widevine":
|
||||
return C.WIDEVINE_UUID;
|
||||
case "playready":
|
||||
return C.PLAYREADY_UUID;
|
||||
case "cenc":
|
||||
return C.CLEARKEY_UUID;
|
||||
default:
|
||||
try {
|
||||
return UUID.fromString(typeString);
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ import java.util.Locale;
|
|||
private static final TrackSelection.Factory RANDOM_FACTORY = new RandomTrackSelection.Factory();
|
||||
|
||||
private final MappingTrackSelector selector;
|
||||
private final TrackSelection.Factory adaptiveVideoTrackSelectionFactory;
|
||||
private final TrackSelection.Factory adaptiveTrackSelectionFactory;
|
||||
|
||||
private MappedTrackInfo trackInfo;
|
||||
private int rendererIndex;
|
||||
|
|
@ -67,13 +67,13 @@ import java.util.Locale;
|
|||
|
||||
/**
|
||||
* @param selector The track selector.
|
||||
* @param adaptiveVideoTrackSelectionFactory A factory for adaptive video {@link TrackSelection}s,
|
||||
* or null if the selection helper should not support adaptive video.
|
||||
* @param adaptiveTrackSelectionFactory A factory for adaptive {@link TrackSelection}s, or null
|
||||
* if the selection helper should not support adaptive tracks.
|
||||
*/
|
||||
public TrackSelectionHelper(MappingTrackSelector selector,
|
||||
TrackSelection.Factory adaptiveVideoTrackSelectionFactory) {
|
||||
TrackSelection.Factory adaptiveTrackSelectionFactory) {
|
||||
this.selector = selector;
|
||||
this.adaptiveVideoTrackSelectionFactory = adaptiveVideoTrackSelectionFactory;
|
||||
this.adaptiveTrackSelectionFactory = adaptiveTrackSelectionFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -92,7 +92,7 @@ import java.util.Locale;
|
|||
trackGroups = trackInfo.getTrackGroups(rendererIndex);
|
||||
trackGroupsAdaptive = new boolean[trackGroups.length];
|
||||
for (int i = 0; i < trackGroups.length; i++) {
|
||||
trackGroupsAdaptive[i] = adaptiveVideoTrackSelectionFactory != null
|
||||
trackGroupsAdaptive[i] = adaptiveTrackSelectionFactory != null
|
||||
&& trackInfo.getAdaptiveSupport(rendererIndex, i, false)
|
||||
!= RendererCapabilities.ADAPTIVE_NOT_SUPPORTED
|
||||
&& trackGroups.get(i).length > 1;
|
||||
|
|
@ -271,7 +271,7 @@ import java.util.Locale;
|
|||
|
||||
private void setOverride(int group, int[] tracks, boolean enableRandomAdaptation) {
|
||||
TrackSelection.Factory factory = tracks.length == 1 ? FIXED_FACTORY
|
||||
: (enableRandomAdaptation ? RANDOM_FACTORY : adaptiveVideoTrackSelectionFactory);
|
||||
: (enableRandomAdaptation ? RANDOM_FACTORY : adaptiveTrackSelectionFactory);
|
||||
override = new SelectionOverride(factory, group, tracks);
|
||||
}
|
||||
|
||||
|
|
@ -301,15 +301,18 @@ import java.util.Locale;
|
|||
private static String buildTrackName(Format format) {
|
||||
String trackName;
|
||||
if (MimeTypes.isVideo(format.sampleMimeType)) {
|
||||
trackName = joinWithSeparator(joinWithSeparator(buildResolutionString(format),
|
||||
buildBitrateString(format)), buildTrackIdString(format));
|
||||
trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(
|
||||
buildResolutionString(format), buildBitrateString(format)), buildTrackIdString(format)),
|
||||
buildSampleMimeTypeString(format));
|
||||
} else if (MimeTypes.isAudio(format.sampleMimeType)) {
|
||||
trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format),
|
||||
buildAudioPropertyString(format)), buildBitrateString(format)),
|
||||
buildTrackIdString(format));
|
||||
trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(joinWithSeparator(
|
||||
buildLanguageString(format), buildAudioPropertyString(format)),
|
||||
buildBitrateString(format)), buildTrackIdString(format)),
|
||||
buildSampleMimeTypeString(format));
|
||||
} else {
|
||||
trackName = joinWithSeparator(joinWithSeparator(buildLanguageString(format),
|
||||
buildBitrateString(format)), buildTrackIdString(format));
|
||||
trackName = joinWithSeparator(joinWithSeparator(joinWithSeparator(buildLanguageString(format),
|
||||
buildBitrateString(format)), buildTrackIdString(format)),
|
||||
buildSampleMimeTypeString(format));
|
||||
}
|
||||
return trackName.length() == 0 ? "unknown" : trackName;
|
||||
}
|
||||
|
|
@ -342,4 +345,8 @@ import java.util.Locale;
|
|||
return format.id == null ? "" : ("id:" + format.id);
|
||||
}
|
||||
|
||||
private static String buildSampleMimeTypeString(Format format) {
|
||||
return format.sampleMimeType == null ? "" : format.sampleMimeType;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ android {
|
|||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 9
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,7 +118,8 @@ public final class CronetDataSourceTest {
|
|||
TEST_CONNECT_TIMEOUT_MS,
|
||||
TEST_READ_TIMEOUT_MS,
|
||||
true, // resetTimeoutOnRedirects
|
||||
mockClock));
|
||||
mockClock,
|
||||
null));
|
||||
when(mockContentTypePredicate.evaluate(anyString())).thenReturn(true);
|
||||
when(mockCronetEngine.newUrlRequestBuilder(
|
||||
anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ import java.io.IOException;
|
|||
import java.net.SocketTimeoutException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
|
@ -98,7 +97,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
private final int connectTimeoutMs;
|
||||
private final int readTimeoutMs;
|
||||
private final boolean resetTimeoutOnRedirects;
|
||||
private final Map<String, String> requestProperties;
|
||||
private final RequestProperties defaultRequestProperties;
|
||||
private final RequestProperties requestProperties;
|
||||
private final ConditionVariable operation;
|
||||
private final Clock clock;
|
||||
|
||||
|
|
@ -136,7 +136,7 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
public CronetDataSource(CronetEngine cronetEngine, Executor executor,
|
||||
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener) {
|
||||
this(cronetEngine, executor, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS,
|
||||
DEFAULT_READ_TIMEOUT_MILLIS, false);
|
||||
DEFAULT_READ_TIMEOUT_MILLIS, false, null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -149,17 +149,20 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
* @param connectTimeoutMs The connection timeout, in milliseconds.
|
||||
* @param readTimeoutMs The read timeout, in milliseconds.
|
||||
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||
* @param defaultRequestProperties The default request properties to be used.
|
||||
*/
|
||||
public CronetDataSource(CronetEngine cronetEngine, Executor executor,
|
||||
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener,
|
||||
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects) {
|
||||
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects,
|
||||
RequestProperties defaultRequestProperties) {
|
||||
this(cronetEngine, executor, contentTypePredicate, listener, connectTimeoutMs,
|
||||
readTimeoutMs, resetTimeoutOnRedirects, new SystemClock());
|
||||
readTimeoutMs, resetTimeoutOnRedirects, new SystemClock(), defaultRequestProperties);
|
||||
}
|
||||
|
||||
/* package */ CronetDataSource(CronetEngine cronetEngine, Executor executor,
|
||||
Predicate<String> contentTypePredicate, TransferListener<? super CronetDataSource> listener,
|
||||
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, Clock clock) {
|
||||
int connectTimeoutMs, int readTimeoutMs, boolean resetTimeoutOnRedirects, Clock clock,
|
||||
RequestProperties defaultRequestProperties) {
|
||||
this.cronetEngine = Assertions.checkNotNull(cronetEngine);
|
||||
this.executor = Assertions.checkNotNull(executor);
|
||||
this.contentTypePredicate = contentTypePredicate;
|
||||
|
|
@ -168,7 +171,8 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
this.readTimeoutMs = readTimeoutMs;
|
||||
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
|
||||
this.clock = Assertions.checkNotNull(clock);
|
||||
requestProperties = new HashMap<>();
|
||||
this.defaultRequestProperties = defaultRequestProperties;
|
||||
requestProperties = new RequestProperties();
|
||||
operation = new ConditionVariable();
|
||||
}
|
||||
|
||||
|
|
@ -176,23 +180,17 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
|
||||
@Override
|
||||
public void setRequestProperty(String name, String value) {
|
||||
synchronized (requestProperties) {
|
||||
requestProperties.put(name, value);
|
||||
}
|
||||
requestProperties.set(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearRequestProperty(String name) {
|
||||
synchronized (requestProperties) {
|
||||
requestProperties.remove(name);
|
||||
}
|
||||
requestProperties.remove(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearAllRequestProperties() {
|
||||
synchronized (requestProperties) {
|
||||
requestProperties.clear();
|
||||
}
|
||||
requestProperties.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -421,16 +419,24 @@ public class CronetDataSource extends UrlRequest.Callback implements HttpDataSou
|
|||
UrlRequest.Builder requestBuilder = cronetEngine.newUrlRequestBuilder(dataSpec.uri.toString(),
|
||||
this, executor);
|
||||
// Set the headers.
|
||||
synchronized (requestProperties) {
|
||||
if (dataSpec.postBody != null && dataSpec.postBody.length != 0
|
||||
&& !requestProperties.containsKey(CONTENT_TYPE)) {
|
||||
throw new OpenException("POST request with non-empty body must set Content-Type", dataSpec,
|
||||
Status.IDLE);
|
||||
}
|
||||
for (Entry<String, String> headerEntry : requestProperties.entrySet()) {
|
||||
requestBuilder.addHeader(headerEntry.getKey(), headerEntry.getValue());
|
||||
boolean isContentTypeHeaderSet = false;
|
||||
if (defaultRequestProperties != null) {
|
||||
for (Entry<String, String> headerEntry : defaultRequestProperties.getSnapshot().entrySet()) {
|
||||
String key = headerEntry.getKey();
|
||||
isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key);
|
||||
requestBuilder.addHeader(key, headerEntry.getValue());
|
||||
}
|
||||
}
|
||||
Map<String, String> requestPropertiesSnapshot = requestProperties.getSnapshot();
|
||||
for (Entry<String, String> headerEntry : requestPropertiesSnapshot.entrySet()) {
|
||||
String key = headerEntry.getKey();
|
||||
isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key);
|
||||
requestBuilder.addHeader(key, headerEntry.getValue());
|
||||
}
|
||||
if (dataSpec.postBody != null && dataSpec.postBody.length != 0 && !isContentTypeHeaderSet) {
|
||||
throw new OpenException("POST request with non-empty body must set Content-Type", dataSpec,
|
||||
Status.IDLE);
|
||||
}
|
||||
// Set the Range header.
|
||||
if (currentDataSpec.position != 0 || currentDataSpec.length != C.LENGTH_UNSET) {
|
||||
StringBuilder rangeValue = new StringBuilder();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
|
|
@ -68,9 +69,10 @@ public final class CronetDataSourceFactory extends BaseFactory {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected CronetDataSource createDataSourceInternal() {
|
||||
protected CronetDataSource createDataSourceInternal(HttpDataSource.RequestProperties
|
||||
defaultRequestProperties) {
|
||||
return new CronetDataSource(cronetEngine, executor, contentTypePredicate, transferListener,
|
||||
connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects);
|
||||
connectTimeoutMs, readTimeoutMs, resetTimeoutOnRedirects, null, defaultRequestProperties);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,9 +31,7 @@ FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main"
|
|||
NDK_PATH="<path to Android NDK>"
|
||||
```
|
||||
|
||||
* Fetch and build FFmpeg.
|
||||
|
||||
For example, to fetch and build for armv7a:
|
||||
* Fetch and build FFmpeg. For example, to fetch and build for armv7a:
|
||||
|
||||
```
|
||||
cd "${FFMPEG_EXT_PATH}/jni" && \
|
||||
|
|
@ -69,15 +67,14 @@ make -j4 && \
|
|||
make install-libs
|
||||
```
|
||||
|
||||
* Build the JNI native libraries.
|
||||
* Build the JNI native libraries. Repeat this step for any other architectures
|
||||
you need to support.
|
||||
|
||||
```
|
||||
cd "${FFMPEG_EXT_PATH}"/jni && \
|
||||
${NDK_PATH}/ndk-build APP_ABI=armeabi-v7a -j4
|
||||
```
|
||||
|
||||
Repeat these steps for any other architectures you need to support.
|
||||
|
||||
* In your project, you can add a dependency on the extension by using a rule
|
||||
like this:
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ android {
|
|||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 9
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ 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.AudioProcessor;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
|
|
@ -43,21 +43,11 @@ public final class FfmpegAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
*/
|
||||
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) {
|
||||
super(eventHandler, eventListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
|
||||
* default capabilities (no encoded audio passthrough support) should be assumed.
|
||||
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
|
||||
*/
|
||||
public FfmpegAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
|
||||
AudioCapabilities audioCapabilities) {
|
||||
super(eventHandler, eventListener, audioCapabilities);
|
||||
AudioProcessor... audioProcessors) {
|
||||
super(eventHandler, eventListener, audioProcessors);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ android {
|
|||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 9
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@
|
|||
native <methods>;
|
||||
}
|
||||
|
||||
# Some members of this class are being accessed from native methods. Keep them unobfuscated.
|
||||
# Some members of these classes are being accessed from native methods. Keep them unobfuscated.
|
||||
-keep class com.google.android.exoplayer2.ext.flac.FlacDecoderJni {
|
||||
*;
|
||||
}
|
||||
-keep class com.google.android.exoplayer2.util.FlacStreamInfo {
|
||||
*;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ public final class FlacExtractor implements Extractor {
|
|||
@Override
|
||||
public void init(ExtractorOutput output) {
|
||||
extractorOutput = output;
|
||||
trackOutput = extractorOutput.track(0);
|
||||
trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO);
|
||||
extractorOutput.endTracks();
|
||||
try {
|
||||
decoderJni = new FlacDecoderJni();
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ package com.google.android.exoplayer2.ext.flac;
|
|||
|
||||
import android.os.Handler;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
|
|
@ -38,21 +38,11 @@ public class LibflacAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
*/
|
||||
public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) {
|
||||
super(eventHandler, eventListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
|
||||
* default capabilities (no encoded audio passthrough support) should be assumed.
|
||||
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
|
||||
*/
|
||||
public LibflacAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
|
||||
AudioCapabilities audioCapabilities) {
|
||||
super(eventHandler, eventListener, audioCapabilities);
|
||||
AudioProcessor... audioProcessors) {
|
||||
super(eventHandler, eventListener, audioProcessors);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ LOCAL_C_INCLUDES := \
|
|||
LOCAL_SRC_FILES := $(FLAC_SOURCES)
|
||||
|
||||
LOCAL_CFLAGS += '-DVERSION="1.3.1"' -DFLAC__NO_MD5 -DFLAC__INTEGER_ONLY_LIBRARY -DFLAC__NO_ASM
|
||||
LOCAL_CFLAGS += -D_REENTRANT -DPIC -DU_COMMON_IMPLEMENTATION -fPIC
|
||||
LOCAL_CFLAGS += -D_REENTRANT -DPIC -DU_COMMON_IMPLEMENTATION -fPIC -DHAVE_SYS_PARAM_H
|
||||
LOCAL_CFLAGS += -O3 -funroll-loops -finline-functions
|
||||
|
||||
LOCAL_LDLIBS := -llog -lz -lm
|
||||
|
|
|
|||
|
|
@ -453,7 +453,8 @@ int64_t FLACParser::getSeekPosition(int64_t timeUs) {
|
|||
}
|
||||
|
||||
FLAC__StreamMetadata_SeekPoint* points = mSeekTable->points;
|
||||
for (unsigned i = mSeekTable->num_points - 1; i >= 0; i--) {
|
||||
for (unsigned i = mSeekTable->num_points; i > 0; ) {
|
||||
i--;
|
||||
if (points[i].sample_number <= sample) {
|
||||
return firstFrameOffset + points[i].stream_offset;
|
||||
}
|
||||
|
|
|
|||
38
extensions/gvr/README.md
Normal file
38
extensions/gvr/README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# ExoPlayer GVR Extension #
|
||||
|
||||
## Description ##
|
||||
|
||||
The GVR extension wraps the [Google VR SDK for Android][]. It provides a
|
||||
GvrAudioProcessor, which uses [GvrAudioSurround][] to provide binaural rendering
|
||||
of surround sound and ambisonic soundfields.
|
||||
|
||||
## Using the extension ##
|
||||
|
||||
The easiest way to use the extension is to add it as a gradle dependency. You
|
||||
need to make sure you have the jcenter repository included in the `build.gradle`
|
||||
file in the root of your project:
|
||||
|
||||
```gradle
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
```
|
||||
|
||||
Next, include the following in your module's `build.gradle` file:
|
||||
|
||||
```gradle
|
||||
compile 'com.google.android.exoplayer:extension-gvr:rX.X.X'
|
||||
```
|
||||
|
||||
where `rX.X.X` is the version, which must match the version of the ExoPlayer
|
||||
library being used.
|
||||
|
||||
## Using GvrAudioProcessor ##
|
||||
|
||||
* If using SimpleExoPlayer, override SimpleExoPlayer.buildAudioProcessors to
|
||||
return a GvrAudioProcessor.
|
||||
* If constructing renderers directly, pass a GvrAudioProcessor to
|
||||
MediaCodecAudioRenderer's constructor.
|
||||
|
||||
[Google VR SDK for Android]: https://developers.google.com/vr/android/
|
||||
[GvrAudioSurround]: https://developers.google.com/vr/android/reference/com/google/vr/sdk/audio/GvrAudioSurround
|
||||
35
extensions/gvr/build.gradle
Normal file
35
extensions/gvr/build.gradle
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// 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.
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 19
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':library')
|
||||
compile 'com.google.vr:sdk-audio:1.30.0'
|
||||
}
|
||||
|
||||
ext {
|
||||
releaseArtifact = 'extension-gvr'
|
||||
releaseDescription = 'Google VR extension for ExoPlayer.'
|
||||
}
|
||||
apply from: '../../publish.gradle'
|
||||
16
extensions/gvr/src/main/AndroidManifest.xml
Normal file
16
extensions/gvr/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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.
|
||||
-->
|
||||
<manifest package="com.google.android.exoplayer2.ext.gvr"/>
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* 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.ext.gvr;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||
import com.google.vr.sdk.audio.GvrAudioSurround;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* An {@link AudioProcessor} that uses {@code GvrAudioSurround} to provide binaural rendering of
|
||||
* surround sound and ambisonic soundfields.
|
||||
*/
|
||||
public final class GvrAudioProcessor implements AudioProcessor {
|
||||
|
||||
private static final int FRAMES_PER_OUTPUT_BUFFER = 1024;
|
||||
private static final int OUTPUT_CHANNEL_COUNT = 2;
|
||||
private static final int OUTPUT_FRAME_SIZE = OUTPUT_CHANNEL_COUNT * 2; // 16-bit stereo output.
|
||||
|
||||
private int sampleRateHz;
|
||||
private int channelCount;
|
||||
private GvrAudioSurround gvrAudioSurround;
|
||||
private ByteBuffer buffer;
|
||||
private boolean inputEnded;
|
||||
|
||||
private float w;
|
||||
private float x;
|
||||
private float y;
|
||||
private float z;
|
||||
|
||||
/**
|
||||
* Creates a new GVR audio processor.
|
||||
*/
|
||||
public GvrAudioProcessor() {
|
||||
// Use the identity for the initial orientation.
|
||||
w = 1f;
|
||||
sampleRateHz = Format.NO_VALUE;
|
||||
channelCount = Format.NO_VALUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the listener head orientation. May be called on any thread. See
|
||||
* {@code GvrAudioSurround.updateNativeOrientation}.
|
||||
*/
|
||||
public synchronized void updateOrientation(float w, float x, float y, float z) {
|
||||
this.w = w;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
if (gvrAudioSurround != null) {
|
||||
gvrAudioSurround.updateNativeOrientation(w, x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean configure(int sampleRateHz, int channelCount,
|
||||
@C.Encoding int encoding) throws UnhandledFormatException {
|
||||
if (encoding != C.ENCODING_PCM_16BIT) {
|
||||
maybeReleaseGvrAudioSurround();
|
||||
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
|
||||
}
|
||||
if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount) {
|
||||
return false;
|
||||
}
|
||||
this.sampleRateHz = sampleRateHz;
|
||||
this.channelCount = channelCount;
|
||||
maybeReleaseGvrAudioSurround();
|
||||
int surroundFormat;
|
||||
switch (channelCount) {
|
||||
case 2:
|
||||
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_STEREO;
|
||||
break;
|
||||
case 4:
|
||||
surroundFormat = GvrAudioSurround.SurroundFormat.FIRST_ORDER_AMBISONICS;
|
||||
break;
|
||||
case 6:
|
||||
surroundFormat = GvrAudioSurround.SurroundFormat.SURROUND_FIVE_DOT_ONE;
|
||||
break;
|
||||
case 9:
|
||||
surroundFormat = GvrAudioSurround.SurroundFormat.SECOND_ORDER_AMBISONICS;
|
||||
break;
|
||||
case 16:
|
||||
surroundFormat = GvrAudioSurround.SurroundFormat.THIRD_ORDER_AMBISONICS;
|
||||
break;
|
||||
default:
|
||||
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
|
||||
}
|
||||
gvrAudioSurround = new GvrAudioSurround(surroundFormat, sampleRateHz, channelCount,
|
||||
FRAMES_PER_OUTPUT_BUFFER);
|
||||
gvrAudioSurround.updateNativeOrientation(w, x, y, z);
|
||||
if (buffer == null) {
|
||||
buffer = ByteBuffer.allocateDirect(FRAMES_PER_OUTPUT_BUFFER * OUTPUT_FRAME_SIZE)
|
||||
.order(ByteOrder.nativeOrder());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return gvrAudioSurround != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOutputChannelCount() {
|
||||
return OUTPUT_CHANNEL_COUNT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOutputEncoding() {
|
||||
return C.ENCODING_PCM_16BIT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueInput(ByteBuffer input) {
|
||||
int position = input.position();
|
||||
int readBytes = gvrAudioSurround.addInput(input, position, input.limit() - position);
|
||||
input.position(position + readBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueEndOfStream() {
|
||||
inputEnded = true;
|
||||
gvrAudioSurround.triggerProcessing();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer getOutput() {
|
||||
int writtenBytes = gvrAudioSurround.getOutput(buffer, 0, buffer.capacity());
|
||||
buffer.position(0).limit(writtenBytes);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnded() {
|
||||
return inputEnded && gvrAudioSurround.getAvailableOutputSize() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
gvrAudioSurround.flush();
|
||||
inputEnded = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void release() {
|
||||
buffer = null;
|
||||
maybeReleaseGvrAudioSurround();
|
||||
}
|
||||
|
||||
private void maybeReleaseGvrAudioSurround() {
|
||||
if (this.gvrAudioSurround != null) {
|
||||
GvrAudioSurround gvrAudioSurround = this.gvrAudioSurround;
|
||||
this.gvrAudioSurround = null;
|
||||
gvrAudioSurround.release();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -12,31 +12,31 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'bintray-release'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 9
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
// See: https://github.com/square/okio/issues/58
|
||||
warning 'InvalidPackage'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':library')
|
||||
compile('com.squareup.okhttp3:okhttp:3.4.1') {
|
||||
compile('com.squareup.okhttp3:okhttp:3.6.0') {
|
||||
exclude group: 'org.json'
|
||||
}
|
||||
}
|
||||
|
||||
publish {
|
||||
artifactId = 'extension-okhttp'
|
||||
description = 'An OkHttp extension for ExoPlayer.'
|
||||
repoName = releaseRepoName
|
||||
userOrg = releaseUserOrg
|
||||
groupId = releaseGroupId
|
||||
version = releaseVersion
|
||||
website = releaseWebsite
|
||||
ext {
|
||||
releaseArtifact = 'extension-okhttp'
|
||||
releaseDescription = 'OkHttp extension for ExoPlayer.'
|
||||
}
|
||||
apply from: '../../publish.gradle'
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import java.io.EOFException;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
|
@ -51,7 +50,8 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||
private final Predicate<String> contentTypePredicate;
|
||||
private final TransferListener<? super OkHttpDataSource> listener;
|
||||
private final CacheControl cacheControl;
|
||||
private final HashMap<String, String> requestProperties;
|
||||
private final RequestProperties defaultRequestProperties;
|
||||
private final RequestProperties requestProperties;
|
||||
|
||||
private DataSpec dataSpec;
|
||||
private Response response;
|
||||
|
|
@ -87,7 +87,7 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||
*/
|
||||
public OkHttpDataSource(Call.Factory callFactory, String userAgent,
|
||||
Predicate<String> contentTypePredicate, TransferListener<? super OkHttpDataSource> listener) {
|
||||
this(callFactory, userAgent, contentTypePredicate, listener, null);
|
||||
this(callFactory, userAgent, contentTypePredicate, listener, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -99,16 +99,19 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||
* {@link #open(DataSpec)}.
|
||||
* @param listener An optional listener.
|
||||
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
|
||||
* @param defaultRequestProperties The optional default {@link RequestProperties} to be sent to
|
||||
* the server as HTTP headers on every request.
|
||||
*/
|
||||
public OkHttpDataSource(Call.Factory callFactory, String userAgent,
|
||||
Predicate<String> contentTypePredicate, TransferListener<? super OkHttpDataSource> listener,
|
||||
CacheControl cacheControl) {
|
||||
CacheControl cacheControl, RequestProperties defaultRequestProperties) {
|
||||
this.callFactory = Assertions.checkNotNull(callFactory);
|
||||
this.userAgent = Assertions.checkNotEmpty(userAgent);
|
||||
this.contentTypePredicate = contentTypePredicate;
|
||||
this.listener = listener;
|
||||
this.cacheControl = cacheControl;
|
||||
this.requestProperties = new HashMap<>();
|
||||
this.defaultRequestProperties = defaultRequestProperties;
|
||||
this.requestProperties = new RequestProperties();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -125,24 +128,18 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||
public void setRequestProperty(String name, String value) {
|
||||
Assertions.checkNotNull(name);
|
||||
Assertions.checkNotNull(value);
|
||||
synchronized (requestProperties) {
|
||||
requestProperties.put(name, value);
|
||||
}
|
||||
requestProperties.set(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearRequestProperty(String name) {
|
||||
Assertions.checkNotNull(name);
|
||||
synchronized (requestProperties) {
|
||||
requestProperties.remove(name);
|
||||
}
|
||||
requestProperties.remove(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearAllRequestProperties() {
|
||||
synchronized (requestProperties) {
|
||||
requestProperties.clear();
|
||||
}
|
||||
requestProperties.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -268,11 +265,14 @@ public class OkHttpDataSource implements HttpDataSource {
|
|||
if (cacheControl != null) {
|
||||
builder.cacheControl(cacheControl);
|
||||
}
|
||||
synchronized (requestProperties) {
|
||||
for (Map.Entry<String, String> property : requestProperties.entrySet()) {
|
||||
builder.addHeader(property.getKey(), property.getValue());
|
||||
if (defaultRequestProperties != null) {
|
||||
for (Map.Entry<String, String> property : defaultRequestProperties.getSnapshot().entrySet()) {
|
||||
builder.header(property.getKey(), property.getValue());
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, String> property : requestProperties.getSnapshot().entrySet()) {
|
||||
builder.header(property.getKey(), property.getValue());
|
||||
}
|
||||
if (!(position == 0 && length == C.LENGTH_UNSET)) {
|
||||
String rangeRequest = "bytes=" + position + "-";
|
||||
if (length != C.LENGTH_UNSET) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory;
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource.Factory;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
|
|
@ -59,8 +60,10 @@ public final class OkHttpDataSourceFactory extends BaseFactory {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected OkHttpDataSource createDataSourceInternal() {
|
||||
return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl);
|
||||
protected OkHttpDataSource createDataSourceInternal(
|
||||
HttpDataSource.RequestProperties defaultRequestProperties) {
|
||||
return new OkHttpDataSource(callFactory, userAgent, null, listener, cacheControl,
|
||||
defaultRequestProperties);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ android {
|
|||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 9
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
|
|
@ -32,4 +32,3 @@ android {
|
|||
dependencies {
|
||||
compile project(':library')
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ package com.google.android.exoplayer2.ext.opus;
|
|||
|
||||
import android.os.Handler;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.audio.SimpleDecoderAudioRenderer;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
|
|
@ -40,35 +40,24 @@ public final class LibopusAudioRenderer extends SimpleDecoderAudioRenderer {
|
|||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
|
||||
*/
|
||||
public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener) {
|
||||
super(eventHandler, eventListener);
|
||||
public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
|
||||
AudioProcessor... audioProcessors) {
|
||||
super(eventHandler, eventListener, audioProcessors);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
|
||||
* default capabilities (no encoded audio passthrough support) should be assumed.
|
||||
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
|
||||
*/
|
||||
public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
|
||||
AudioCapabilities audioCapabilities) {
|
||||
super(eventHandler, eventListener, audioCapabilities);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
|
||||
* default capabilities (no encoded audio passthrough support) should be assumed.
|
||||
*/
|
||||
public LibopusAudioRenderer(Handler eventHandler, AudioRendererEventListener eventListener,
|
||||
AudioCapabilities audioCapabilities, DrmSessionManager<ExoMediaCrypto> drmSessionManager,
|
||||
boolean playClearSamplesWithoutKeys) {
|
||||
super(eventHandler, eventListener, audioCapabilities, drmSessionManager,
|
||||
playClearSamplesWithoutKeys);
|
||||
DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys,
|
||||
AudioProcessor... audioProcessors) {
|
||||
super(eventHandler, eventListener, null, drmSessionManager, playClearSamplesWithoutKeys,
|
||||
audioProcessors);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ import java.util.List;
|
|||
SimpleOutputBuffer outputBuffer, int sampleRate);
|
||||
private native int opusSecureDecode(long decoder, long timeUs, ByteBuffer inputBuffer,
|
||||
int inputSize, SimpleOutputBuffer outputBuffer, int sampleRate,
|
||||
ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv,
|
||||
ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv,
|
||||
int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
|
||||
private native void opusClose(long decoder);
|
||||
private native void opusReset(long decoder);
|
||||
|
|
|
|||
|
|
@ -40,6 +40,18 @@ git clone https://chromium.googlesource.com/webm/libvpx libvpx && \
|
|||
git clone https://chromium.googlesource.com/libyuv/libyuv libyuv
|
||||
```
|
||||
|
||||
* Checkout the appropriate branches of libvpx and libyuv (the scripts and
|
||||
makefiles bundled in this repo are known to work only at these versions of the
|
||||
libraries - we will update this periodically as newer versions of
|
||||
libvpx/libyuv are released):
|
||||
|
||||
```
|
||||
cd "${VP9_EXT_PATH}/jni/libvpx" && \
|
||||
git checkout tags/v1.6.1 -b v1.6.1 && \
|
||||
cd "${VP9_EXT_PATH}/jni/libyuv" && \
|
||||
git checkout e2611a73
|
||||
```
|
||||
|
||||
* Run a script that generates necessary configuration files for libvpx:
|
||||
|
||||
```
|
||||
|
|
@ -79,5 +91,7 @@ dependencies {
|
|||
`generate_libvpx_android_configs.sh`
|
||||
* Clean and re-build the project.
|
||||
* If you want to use your own version of libvpx or libyuv, place it in
|
||||
`${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively.
|
||||
`${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. But
|
||||
please note that `generate_libvpx_android_configs.sh` and the makefiles need
|
||||
to be modified to work with arbitrary versions of libvpx and libyuv.
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ android {
|
|||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 9
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
|
|
|
|||
BIN
extensions/vp9/src/androidTest/assets/roadtrip-vp92-10bit.webm
Normal file
BIN
extensions/vp9/src/androidTest/assets/roadtrip-vp92-10bit.webm
Normal file
Binary file not shown.
|
|
@ -19,6 +19,7 @@ import android.content.Context;
|
|||
import android.net.Uri;
|
||||
import android.os.Looper;
|
||||
import android.test.InstrumentationTestCase;
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
import com.google.android.exoplayer2.ExoPlayer;
|
||||
import com.google.android.exoplayer2.ExoPlayerFactory;
|
||||
|
|
@ -38,8 +39,11 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
|
|||
|
||||
private static final String BEAR_URI = "asset:///bear-vp9.webm";
|
||||
private static final String BEAR_ODD_DIMENSIONS_URI = "asset:///bear-vp9-odd-dimensions.webm";
|
||||
private static final String ROADTRIP_10BIT_URI = "asset:///roadtrip-vp92-10bit.webm";
|
||||
private static final String INVALID_BITSTREAM_URI = "asset:///invalid-bitstream.webm";
|
||||
|
||||
private static final String TAG = "VpxPlaybackTest";
|
||||
|
||||
public void testBasicPlayback() throws ExoPlaybackException {
|
||||
playUri(BEAR_URI);
|
||||
}
|
||||
|
|
@ -48,6 +52,15 @@ public class VpxPlaybackTest extends InstrumentationTestCase {
|
|||
playUri(BEAR_ODD_DIMENSIONS_URI);
|
||||
}
|
||||
|
||||
public void test10BitProfile2Playback() throws ExoPlaybackException {
|
||||
if (VpxLibrary.isHighBitDepthSupported()) {
|
||||
Log.d(TAG, "High Bit Depth supported.");
|
||||
playUri(ROADTRIP_10BIT_URI);
|
||||
return;
|
||||
}
|
||||
Log.d(TAG, "High Bit Depth not supported.");
|
||||
}
|
||||
|
||||
public void testInvalidBitstream() {
|
||||
try {
|
||||
playUri(INVALID_BITSTREAM_URI);
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||
private final boolean playClearSamplesWithoutKeys;
|
||||
private final EventDispatcher eventDispatcher;
|
||||
private final FormatHolder formatHolder;
|
||||
private final DecoderInputBuffer flagsOnlyBuffer;
|
||||
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
|
||||
|
||||
private DecoderCounters decoderCounters;
|
||||
|
|
@ -149,6 +150,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||
joiningDeadlineMs = -1;
|
||||
clearLastReportedVideoSize();
|
||||
formatHolder = new FormatHolder();
|
||||
flagsOnlyBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
|
||||
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
||||
outputMode = VpxDecoder.OUTPUT_MODE_NONE;
|
||||
}
|
||||
|
|
@ -165,10 +167,22 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||
return;
|
||||
}
|
||||
|
||||
// Try and read a format if we don't have one already.
|
||||
if (format == null && !readFormat()) {
|
||||
// We can't make progress without one.
|
||||
return;
|
||||
if (format == null) {
|
||||
// We don't have a format yet, so try and read one.
|
||||
flagsOnlyBuffer.clear();
|
||||
int result = readSource(formatHolder, flagsOnlyBuffer, true);
|
||||
if (result == C.RESULT_FORMAT_READ) {
|
||||
onInputFormatChanged(formatHolder.format);
|
||||
} else if (result == C.RESULT_BUFFER_READ) {
|
||||
// End of stream read having not read a format.
|
||||
Assertions.checkState(flagsOnlyBuffer.isEndOfStream());
|
||||
inputStreamEnded = true;
|
||||
outputStreamEnded = true;
|
||||
return;
|
||||
} else {
|
||||
// We still don't have a format and can't make progress without one.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isRendererAvailable()) {
|
||||
|
|
@ -327,7 +341,7 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||
// We've already read an encrypted sample into buffer, and are waiting for keys.
|
||||
result = C.RESULT_BUFFER_READ;
|
||||
} else {
|
||||
result = readSource(formatHolder, inputBuffer);
|
||||
result = readSource(formatHolder, inputBuffer, false);
|
||||
}
|
||||
|
||||
if (result == C.RESULT_NOTHING_READ) {
|
||||
|
|
@ -485,15 +499,6 @@ public final class LibvpxVideoRenderer extends BaseRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
private boolean readFormat() throws ExoPlaybackException {
|
||||
int result = readSource(formatHolder, null);
|
||||
if (result == C.RESULT_FORMAT_READ) {
|
||||
onInputFormatChanged(formatHolder.format);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
|
||||
Format oldFormat = format;
|
||||
format = newFormat;
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ import java.nio.ByteBuffer;
|
|||
private native long vpxClose(long context);
|
||||
private native long vpxDecode(long context, ByteBuffer encoded, int length);
|
||||
private native long vpxSecureDecode(long context, ByteBuffer encoded, int length,
|
||||
ExoMediaCrypto wvCrypto, int inputMode, byte[] key, byte[] iv,
|
||||
ExoMediaCrypto mediaCrypto, int inputMode, byte[] key, byte[] iv,
|
||||
int numSubSamples, int[] numBytesOfClearData, int[] numBytesOfEncryptedData);
|
||||
private native int vpxGetFrame(long context, VpxOutputBuffer outputBuffer);
|
||||
private native int vpxGetErrorCode(long context);
|
||||
|
|
|
|||
|
|
@ -57,6 +57,16 @@ public final class VpxLibrary {
|
|||
return isAvailable() ? vpxGetBuildConfig() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the underlying libvpx library supports high bit depth.
|
||||
*/
|
||||
public static boolean isHighBitDepthSupported() {
|
||||
String config = getBuildConfig();
|
||||
int indexHbd = config != null
|
||||
? config.indexOf("--enable-vp9-highbitdepth") : -1;
|
||||
return indexHbd >= 0;
|
||||
}
|
||||
|
||||
private static native String vpxGetVersion();
|
||||
private static native String vpxGetBuildConfig();
|
||||
public static native boolean vpxIsSecureDecodeSupported();
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import java.nio.ByteBuffer;
|
|||
public static final int COLORSPACE_UNKNOWN = 0;
|
||||
public static final int COLORSPACE_BT601 = 1;
|
||||
public static final int COLORSPACE_BT709 = 2;
|
||||
public static final int COLORSPACE_BT2020 = 3;
|
||||
|
||||
private final VpxDecoder owner;
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,12 @@ import javax.microedition.khronos.opengles.GL10;
|
|||
1.793f, -0.533f, 0.0f,
|
||||
};
|
||||
|
||||
private static final float[] kColorConversion2020 = {
|
||||
1.168f, 1.168f, 1.168f,
|
||||
0.0f, -0.188f, 2.148f,
|
||||
1.683f, -0.652f, 0.0f,
|
||||
};
|
||||
|
||||
private static final String VERTEX_SHADER =
|
||||
"varying vec2 interp_tc;\n"
|
||||
+ "attribute vec4 in_pos;\n"
|
||||
|
|
@ -59,12 +65,13 @@ import javax.microedition.khronos.opengles.GL10;
|
|||
+ "uniform sampler2D v_tex;\n"
|
||||
+ "uniform mat3 mColorConversion;\n"
|
||||
+ "void main() {\n"
|
||||
+ " vec3 yuv;"
|
||||
+ " vec3 yuv;\n"
|
||||
+ " yuv.x = texture2D(y_tex, interp_tc).r - 0.0625;\n"
|
||||
+ " yuv.y = texture2D(u_tex, interp_tc).r - 0.5;\n"
|
||||
+ " yuv.z = texture2D(v_tex, interp_tc).r - 0.5;\n"
|
||||
+ " gl_FragColor = vec4(mColorConversion * yuv, 1.0);"
|
||||
+ " gl_FragColor = vec4(mColorConversion * yuv, 1.0);\n"
|
||||
+ "}\n";
|
||||
|
||||
private static final FloatBuffer TEXTURE_VERTICES = nativeFloatBuffer(
|
||||
-1.0f, 1.0f,
|
||||
-1.0f, -1.0f,
|
||||
|
|
@ -156,8 +163,18 @@ import javax.microedition.khronos.opengles.GL10;
|
|||
}
|
||||
VpxOutputBuffer outputBuffer = renderedOutputBuffer;
|
||||
// Set color matrix. Assume BT709 if the color space is unknown.
|
||||
float[] colorConversion = outputBuffer.colorspace == VpxOutputBuffer.COLORSPACE_BT601
|
||||
? kColorConversion601 : kColorConversion709;
|
||||
float[] colorConversion = kColorConversion709;
|
||||
switch (outputBuffer.colorspace) {
|
||||
case VpxOutputBuffer.COLORSPACE_BT601:
|
||||
colorConversion = kColorConversion601;
|
||||
break;
|
||||
case VpxOutputBuffer.COLORSPACE_BT2020:
|
||||
colorConversion = kColorConversion2020;
|
||||
break;
|
||||
case VpxOutputBuffer.COLORSPACE_BT709:
|
||||
default:
|
||||
break; // Do nothing
|
||||
}
|
||||
GLES20.glUniformMatrix3fv(colorMatrixLocation, 1, false, colorConversion, 0);
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ config[0]+=" --enable-neon-asm"
|
|||
|
||||
arch[1]="armeabi"
|
||||
config[1]="--target=armv7-android-gcc --sdk-path=$ndk --disable-neon"
|
||||
config[1]+=" --disable-neon-asm --disable-media"
|
||||
config[1]+=" --disable-neon-asm"
|
||||
|
||||
arch[2]="mips"
|
||||
config[2]="--force-target=mips32-android-gcc --sdk-path=$ndk"
|
||||
|
|
@ -78,12 +78,12 @@ convert_asm() {
|
|||
for i in $(seq 0 ${limit}); do
|
||||
while read file; do
|
||||
case "${file}" in
|
||||
*.asm.s)
|
||||
*.asm.[sS])
|
||||
# Some files may already have been processed (there are duplicated
|
||||
# .asm.s files for vp8 in the armeabi/armeabi-v7a configurations).
|
||||
file="libvpx/${file}"
|
||||
if [[ ! -e "${file}" ]]; then
|
||||
asm_file="${file%.s}"
|
||||
asm_file="${file%.[sS]}"
|
||||
cat "${asm_file}" | libvpx/build/make/ads2gas.pl > "${file}"
|
||||
remove_trailing_whitespace "${file}"
|
||||
rm "${asm_file}"
|
||||
|
|
@ -105,7 +105,11 @@ for i in $(seq 0 ${limit}); do
|
|||
echo "configure ${config[${i}]} ${common_params}"
|
||||
../../libvpx/configure ${config[${i}]} ${common_params}
|
||||
rm -f libvpx_srcs.txt
|
||||
make libvpx_srcs.txt
|
||||
for f in ${allowed_files}; do
|
||||
# the build system supports multiple different configurations. avoid
|
||||
# failing out when, for example, vp8_rtcd.h is not part of a configuration
|
||||
make "${f}" || true
|
||||
done
|
||||
|
||||
# remove files that aren't needed
|
||||
rm -rf !(${allowed_files// /|})
|
||||
|
|
|
|||
|
|
@ -35,16 +35,22 @@ LOCAL_SRC_FILES += $(addprefix libvpx/, $(filter-out vpx_config.c, \
|
|||
$(filter %.c, $(libvpx_codec_srcs))))
|
||||
|
||||
# include assembly files if they exist
|
||||
# "%.asm.s" covers neon assembly and "%.asm" covers x86 assembly
|
||||
# "%.asm.[sS]" covers neon assembly and "%.asm" covers x86 assembly
|
||||
LOCAL_SRC_FILES += $(addprefix libvpx/, \
|
||||
$(filter %.asm.s %.asm, $(libvpx_codec_srcs)))
|
||||
LOCAL_SRC_FILES += $(addprefix libvpx/, \
|
||||
$(filter %.asm.S %.asm, $(libvpx_codec_srcs)))
|
||||
|
||||
ifneq ($(findstring armeabi-v7a, $(TARGET_ARCH_ABI)),)
|
||||
# append .neon to *_neon.c and *.s
|
||||
# append .neon to *_neon.c and *.[sS]
|
||||
LOCAL_SRC_FILES := $(subst _neon.c,_neon.c.neon,$(LOCAL_SRC_FILES))
|
||||
LOCAL_SRC_FILES := $(subst .s,.s.neon,$(LOCAL_SRC_FILES))
|
||||
LOCAL_SRC_FILES := $(subst .S,.S.neon,$(LOCAL_SRC_FILES))
|
||||
endif
|
||||
|
||||
# remove duplicates
|
||||
LOCAL_SRC_FILES := $(sort $(LOCAL_SRC_FILES))
|
||||
|
||||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/libvpx \
|
||||
$(LOCAL_PATH)/libvpx/vpx
|
||||
|
||||
|
|
|
|||
|
|
@ -74,8 +74,11 @@ DECODER_FUNC(jlong, vpxInit) {
|
|||
vpx_codec_dec_cfg_t cfg = {0, 0, 0};
|
||||
cfg.threads = android_getCpuCount();
|
||||
errorCode = 0;
|
||||
if (vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo, &cfg, 0)) {
|
||||
LOGE("ERROR: Fail to initialize libvpx decoder.");
|
||||
vpx_codec_err_t err = vpx_codec_dec_init(context, &vpx_codec_vp9_dx_algo,
|
||||
&cfg, 0);
|
||||
if (err) {
|
||||
LOGE("ERROR: Failed to initialize libvpx decoder, error = %d.", err);
|
||||
errorCode = err;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -160,6 +163,7 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
|
|||
const int kColorspaceUnknown = 0;
|
||||
const int kColorspaceBT601 = 1;
|
||||
const int kColorspaceBT709 = 2;
|
||||
const int kColorspaceBT2020 = 3;
|
||||
|
||||
int colorspace = kColorspaceUnknown;
|
||||
switch (img->cs) {
|
||||
|
|
@ -169,6 +173,9 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
|
|||
case VPX_CS_BT_709:
|
||||
colorspace = kColorspaceBT709;
|
||||
break;
|
||||
case VPX_CS_BT_2020:
|
||||
colorspace = kColorspaceBT2020;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
@ -186,14 +193,55 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) {
|
|||
jbyte* const data =
|
||||
reinterpret_cast<jbyte*>(env->GetDirectBufferAddress(dataObject));
|
||||
|
||||
// TODO: This copy can be eliminated by using external frame buffers. NOLINT
|
||||
// This is insignificant for smaller videos but takes ~1.5ms for 1080p
|
||||
// clips. So this should eventually be gotten rid of.
|
||||
const uint64_t y_length = img->stride[VPX_PLANE_Y] * img->d_h;
|
||||
const uint64_t uv_length = img->stride[VPX_PLANE_U] * ((img->d_h + 1) / 2);
|
||||
memcpy(data, img->planes[VPX_PLANE_Y], y_length);
|
||||
memcpy(data + y_length, img->planes[VPX_PLANE_U], uv_length);
|
||||
memcpy(data + y_length + uv_length, img->planes[VPX_PLANE_V], uv_length);
|
||||
const int32_t uvHeight = (img->d_h + 1) / 2;
|
||||
const uint64_t yLength = img->stride[VPX_PLANE_Y] * img->d_h;
|
||||
const uint64_t uvLength = img->stride[VPX_PLANE_U] * uvHeight;
|
||||
int sample = 0;
|
||||
if (img->fmt == VPX_IMG_FMT_I42016) { // HBD planar 420.
|
||||
// Note: The stride for BT2020 is twice of what we use so this is wasting
|
||||
// memory. The long term goal however is to upload half-float/short so
|
||||
// it's not important to optimize the stride at this time.
|
||||
// Y
|
||||
for (int y = 0; y < img->d_h; y++) {
|
||||
const uint16_t* srcBase = reinterpret_cast<uint16_t*>(
|
||||
img->planes[VPX_PLANE_Y] + img->stride[VPX_PLANE_Y] * y);
|
||||
int8_t* destBase = data + img->stride[VPX_PLANE_Y] * y;
|
||||
for (int x = 0; x < img->d_w; x++) {
|
||||
// Lightweight dither. Carryover the remainder of each 10->8 bit
|
||||
// conversion to the next pixel.
|
||||
sample += *srcBase++;
|
||||
*destBase++ = sample >> 2;
|
||||
sample = sample & 3; // Remainder.
|
||||
}
|
||||
}
|
||||
// UV
|
||||
const int32_t uvWidth = (img->d_w + 1) / 2;
|
||||
for (int y = 0; y < uvHeight; y++) {
|
||||
const uint16_t* srcUBase = reinterpret_cast<uint16_t*>(
|
||||
img->planes[VPX_PLANE_U] + img->stride[VPX_PLANE_U] * y);
|
||||
const uint16_t* srcVBase = reinterpret_cast<uint16_t*>(
|
||||
img->planes[VPX_PLANE_V] + img->stride[VPX_PLANE_V] * y);
|
||||
int8_t* destUBase = data + yLength + img->stride[VPX_PLANE_U] * y;
|
||||
int8_t* destVBase = data + yLength + uvLength
|
||||
+ img->stride[VPX_PLANE_V] * y;
|
||||
for (int x = 0; x < uvWidth; x++) {
|
||||
// Lightweight dither. Carryover the remainder of each 10->8 bit
|
||||
// conversion to the next pixel.
|
||||
sample += *srcUBase++;
|
||||
*destUBase++ = sample >> 2;
|
||||
sample = (*srcVBase++) + (sample & 3); // srcV + previousRemainder.
|
||||
*destVBase++ = sample >> 2;
|
||||
sample = sample & 3; // Remainder.
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO: This copy can be eliminated by using external frame buffers. This
|
||||
// is insignificant for smaller videos but takes ~1.5ms for 1080p clips.
|
||||
// So this should eventually be gotten rid of.
|
||||
memcpy(data, img->planes[VPX_PLANE_Y], yLength);
|
||||
memcpy(data + yLength, img->planes[VPX_PLANE_U], uvLength);
|
||||
memcpy(data + yLength + uvLength, img->planes[VPX_PLANE_V], uvLength);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -1,6 +1,6 @@
|
|||
#Mon Oct 24 14:40:37 BST 2016
|
||||
#Mon Mar 13 11:17:14 GMT 2017
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
|
||||
|
|
|
|||
|
|
@ -14,19 +14,13 @@
|
|||
import com.android.builder.core.BuilderConstants
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'bintray-release'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
buildToolsVersion project.ext.buildToolsVersion
|
||||
|
||||
defaultConfig {
|
||||
// Important: ExoPlayerLib specifies a minSdkVersion of 9 because
|
||||
// various components provided by the library may be of use on older
|
||||
// devices. However, please note that the core video playback
|
||||
// functionality provided by the library requires API level 16 or
|
||||
// greater.
|
||||
minSdkVersion 9
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
}
|
||||
|
|
@ -47,10 +41,10 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile 'com.android.support:support-annotations:25.2.0'
|
||||
androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
|
||||
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2'
|
||||
androidTestCompile 'org.mockito:mockito-core:1.9.5'
|
||||
compile 'com.android.support:support-annotations:25.0.1'
|
||||
}
|
||||
|
||||
android.libraryVariants.all { variant ->
|
||||
|
|
@ -86,12 +80,8 @@ android.libraryVariants.all { variant ->
|
|||
}
|
||||
}
|
||||
|
||||
publish {
|
||||
artifactId = 'exoplayer'
|
||||
description = 'The ExoPlayer library.'
|
||||
repoName = releaseRepoName
|
||||
userOrg = releaseUserOrg
|
||||
groupId = releaseGroupId
|
||||
version = releaseVersion
|
||||
website = releaseWebsite
|
||||
ext {
|
||||
releaseArtifact = 'exoplayer'
|
||||
releaseDescription = 'The ExoPlayer library.'
|
||||
}
|
||||
apply from: '../publish.gradle'
|
||||
|
|
|
|||
BIN
library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4
Normal file
BIN
library/src/androidTest/assets/mp4/sample_fragmented_sei.mp4
Normal file
Binary file not shown.
|
|
@ -0,0 +1,382 @@
|
|||
seekMap:
|
||||
isSeekable = false
|
||||
duration = UNSET TIME
|
||||
getPosition(0) = 0
|
||||
numberOfTracks = 3
|
||||
track 0:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = 1
|
||||
containerMimeType = null
|
||||
sampleMimeType = video/avc
|
||||
maxInputSize = -1
|
||||
width = 1080
|
||||
height = 720
|
||||
frameRate = -1.0
|
||||
rotationDegrees = 0
|
||||
pixelWidthHeightRatio = 1.0
|
||||
channelCount = -1
|
||||
sampleRate = -1
|
||||
pcmEncoding = -1
|
||||
encoderDelay = -1
|
||||
encoderPadding = -1
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
data = length 29, hash 4746B5D9
|
||||
data = length 10, hash 7A0D0F2B
|
||||
sample count = 30
|
||||
sample 0:
|
||||
time = 66000
|
||||
flags = 1
|
||||
data = length 38070, hash B58E1AEE
|
||||
sample 1:
|
||||
time = 199000
|
||||
flags = 0
|
||||
data = length 8340, hash 8AC449FF
|
||||
sample 2:
|
||||
time = 132000
|
||||
flags = 0
|
||||
data = length 1295, hash C0DA5090
|
||||
sample 3:
|
||||
time = 100000
|
||||
flags = 0
|
||||
data = length 469, hash D6E0A200
|
||||
sample 4:
|
||||
time = 166000
|
||||
flags = 0
|
||||
data = length 564, hash E5F56C5B
|
||||
sample 5:
|
||||
time = 332000
|
||||
flags = 0
|
||||
data = length 6075, hash 8756E49E
|
||||
sample 6:
|
||||
time = 266000
|
||||
flags = 0
|
||||
data = length 847, hash DCC2B618
|
||||
sample 7:
|
||||
time = 233000
|
||||
flags = 0
|
||||
data = length 455, hash B9CCE047
|
||||
sample 8:
|
||||
time = 299000
|
||||
flags = 0
|
||||
data = length 467, hash 69806D94
|
||||
sample 9:
|
||||
time = 466000
|
||||
flags = 0
|
||||
data = length 4549, hash 3944F501
|
||||
sample 10:
|
||||
time = 399000
|
||||
flags = 0
|
||||
data = length 1087, hash 491BF106
|
||||
sample 11:
|
||||
time = 367000
|
||||
flags = 0
|
||||
data = length 380, hash 5FED016A
|
||||
sample 12:
|
||||
time = 433000
|
||||
flags = 0
|
||||
data = length 455, hash 8A0610
|
||||
sample 13:
|
||||
time = 599000
|
||||
flags = 0
|
||||
data = length 5190, hash B9031D8
|
||||
sample 14:
|
||||
time = 533000
|
||||
flags = 0
|
||||
data = length 1071, hash 684E7DC8
|
||||
sample 15:
|
||||
time = 500000
|
||||
flags = 0
|
||||
data = length 653, hash 8494F326
|
||||
sample 16:
|
||||
time = 566000
|
||||
flags = 0
|
||||
data = length 485, hash 2CCC85F4
|
||||
sample 17:
|
||||
time = 733000
|
||||
flags = 0
|
||||
data = length 4884, hash D16B6A96
|
||||
sample 18:
|
||||
time = 666000
|
||||
flags = 0
|
||||
data = length 997, hash 164FF210
|
||||
sample 19:
|
||||
time = 633000
|
||||
flags = 0
|
||||
data = length 640, hash F664125B
|
||||
sample 20:
|
||||
time = 700000
|
||||
flags = 0
|
||||
data = length 491, hash B5930C7C
|
||||
sample 21:
|
||||
time = 866000
|
||||
flags = 0
|
||||
data = length 2989, hash 92CF4FCF
|
||||
sample 22:
|
||||
time = 800000
|
||||
flags = 0
|
||||
data = length 838, hash 294A3451
|
||||
sample 23:
|
||||
time = 767000
|
||||
flags = 0
|
||||
data = length 544, hash FCCE2DE6
|
||||
sample 24:
|
||||
time = 833000
|
||||
flags = 0
|
||||
data = length 329, hash A654FFA1
|
||||
sample 25:
|
||||
time = 1000000
|
||||
flags = 0
|
||||
data = length 1517, hash 5F7EBF8B
|
||||
sample 26:
|
||||
time = 933000
|
||||
flags = 0
|
||||
data = length 803, hash 7A5C4C1D
|
||||
sample 27:
|
||||
time = 900000
|
||||
flags = 0
|
||||
data = length 415, hash B31BBC3B
|
||||
sample 28:
|
||||
time = 967000
|
||||
flags = 0
|
||||
data = length 415, hash 850DFEA3
|
||||
sample 29:
|
||||
time = 1033000
|
||||
flags = 0
|
||||
data = length 619, hash AB5E56CA
|
||||
track 1:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = 2
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/mp4a-latm
|
||||
maxInputSize = -1
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
rotationDegrees = -1
|
||||
pixelWidthHeightRatio = -1.0
|
||||
channelCount = 1
|
||||
sampleRate = 44100
|
||||
pcmEncoding = -1
|
||||
encoderDelay = -1
|
||||
encoderPadding = -1
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = und
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
data = length 5, hash 2B7623A
|
||||
sample count = 46
|
||||
sample 0:
|
||||
time = 0
|
||||
flags = 1
|
||||
data = length 18, hash 96519432
|
||||
sample 1:
|
||||
time = 23000
|
||||
flags = 1
|
||||
data = length 4, hash EE9DF
|
||||
sample 2:
|
||||
time = 46000
|
||||
flags = 1
|
||||
data = length 4, hash EEDBF
|
||||
sample 3:
|
||||
time = 69000
|
||||
flags = 1
|
||||
data = length 157, hash E2F078F4
|
||||
sample 4:
|
||||
time = 92000
|
||||
flags = 1
|
||||
data = length 371, hash B9471F94
|
||||
sample 5:
|
||||
time = 116000
|
||||
flags = 1
|
||||
data = length 373, hash 2AB265CB
|
||||
sample 6:
|
||||
time = 139000
|
||||
flags = 1
|
||||
data = length 402, hash 1295477C
|
||||
sample 7:
|
||||
time = 162000
|
||||
flags = 1
|
||||
data = length 455, hash 2D8146C8
|
||||
sample 8:
|
||||
time = 185000
|
||||
flags = 1
|
||||
data = length 434, hash F2C5D287
|
||||
sample 9:
|
||||
time = 208000
|
||||
flags = 1
|
||||
data = length 450, hash 84143FCD
|
||||
sample 10:
|
||||
time = 232000
|
||||
flags = 1
|
||||
data = length 429, hash EF769D50
|
||||
sample 11:
|
||||
time = 255000
|
||||
flags = 1
|
||||
data = length 450, hash EC3DE692
|
||||
sample 12:
|
||||
time = 278000
|
||||
flags = 1
|
||||
data = length 447, hash 3E519E13
|
||||
sample 13:
|
||||
time = 301000
|
||||
flags = 1
|
||||
data = length 457, hash 1E4F23A0
|
||||
sample 14:
|
||||
time = 325000
|
||||
flags = 1
|
||||
data = length 447, hash A439EA97
|
||||
sample 15:
|
||||
time = 348000
|
||||
flags = 1
|
||||
data = length 456, hash 1E9034C6
|
||||
sample 16:
|
||||
time = 371000
|
||||
flags = 1
|
||||
data = length 398, hash 99DB7345
|
||||
sample 17:
|
||||
time = 394000
|
||||
flags = 1
|
||||
data = length 474, hash 3F05F10A
|
||||
sample 18:
|
||||
time = 417000
|
||||
flags = 1
|
||||
data = length 416, hash C105EE09
|
||||
sample 19:
|
||||
time = 441000
|
||||
flags = 1
|
||||
data = length 454, hash 5FDBE458
|
||||
sample 20:
|
||||
time = 464000
|
||||
flags = 1
|
||||
data = length 438, hash 41A93AC3
|
||||
sample 21:
|
||||
time = 487000
|
||||
flags = 1
|
||||
data = length 443, hash 10FDA652
|
||||
sample 22:
|
||||
time = 510000
|
||||
flags = 1
|
||||
data = length 412, hash 1F791E25
|
||||
sample 23:
|
||||
time = 534000
|
||||
flags = 1
|
||||
data = length 482, hash A6D983D
|
||||
sample 24:
|
||||
time = 557000
|
||||
flags = 1
|
||||
data = length 386, hash BED7392F
|
||||
sample 25:
|
||||
time = 580000
|
||||
flags = 1
|
||||
data = length 463, hash 5309F8C9
|
||||
sample 26:
|
||||
time = 603000
|
||||
flags = 1
|
||||
data = length 394, hash 21C7321F
|
||||
sample 27:
|
||||
time = 626000
|
||||
flags = 1
|
||||
data = length 489, hash 71B4730D
|
||||
sample 28:
|
||||
time = 650000
|
||||
flags = 1
|
||||
data = length 403, hash D9C6DE89
|
||||
sample 29:
|
||||
time = 673000
|
||||
flags = 1
|
||||
data = length 447, hash 9B14B73B
|
||||
sample 30:
|
||||
time = 696000
|
||||
flags = 1
|
||||
data = length 439, hash 4760D35B
|
||||
sample 31:
|
||||
time = 719000
|
||||
flags = 1
|
||||
data = length 463, hash 1601F88D
|
||||
sample 32:
|
||||
time = 743000
|
||||
flags = 1
|
||||
data = length 423, hash D4AE6773
|
||||
sample 33:
|
||||
time = 766000
|
||||
flags = 1
|
||||
data = length 497, hash A3C674D3
|
||||
sample 34:
|
||||
time = 789000
|
||||
flags = 1
|
||||
data = length 419, hash D3734A1F
|
||||
sample 35:
|
||||
time = 812000
|
||||
flags = 1
|
||||
data = length 474, hash DFB41F9
|
||||
sample 36:
|
||||
time = 835000
|
||||
flags = 1
|
||||
data = length 413, hash 53E7CB9F
|
||||
sample 37:
|
||||
time = 859000
|
||||
flags = 1
|
||||
data = length 445, hash D15B0E39
|
||||
sample 38:
|
||||
time = 882000
|
||||
flags = 1
|
||||
data = length 453, hash 77ED81E4
|
||||
sample 39:
|
||||
time = 905000
|
||||
flags = 1
|
||||
data = length 545, hash 3321AEB9
|
||||
sample 40:
|
||||
time = 928000
|
||||
flags = 1
|
||||
data = length 317, hash F557D0E
|
||||
sample 41:
|
||||
time = 952000
|
||||
flags = 1
|
||||
data = length 537, hash ED58CF7B
|
||||
sample 42:
|
||||
time = 975000
|
||||
flags = 1
|
||||
data = length 458, hash 51CDAA10
|
||||
sample 43:
|
||||
time = 998000
|
||||
flags = 1
|
||||
data = length 465, hash CBA1EFD7
|
||||
sample 44:
|
||||
time = 1021000
|
||||
flags = 1
|
||||
data = length 446, hash D6735B8A
|
||||
sample 45:
|
||||
time = 1044000
|
||||
flags = 1
|
||||
data = length 10, hash A453EEBE
|
||||
track 3:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = null
|
||||
containerMimeType = null
|
||||
sampleMimeType = application/cea-608
|
||||
maxInputSize = -1
|
||||
width = -1
|
||||
height = -1
|
||||
frameRate = -1.0
|
||||
rotationDegrees = -1
|
||||
pixelWidthHeightRatio = -1.0
|
||||
channelCount = -1
|
||||
sampleRate = -1
|
||||
pcmEncoding = -1
|
||||
encoderDelay = -1
|
||||
encoderPadding = -1
|
||||
subsampleOffsetUs = 9223372036854775807
|
||||
selectionFlags = 0
|
||||
language = null
|
||||
drmInitData = -
|
||||
initializationData:
|
||||
sample count = 0
|
||||
tracksEnded = true
|
||||
|
|
@ -6,7 +6,7 @@ numberOfTracks = 1
|
|||
track 0:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = null
|
||||
id = 0
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/ac3
|
||||
maxInputSize = -1
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ numberOfTracks = 2
|
|||
track 0:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = null
|
||||
id = 0
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/mp4a-latm
|
||||
maxInputSize = -1
|
||||
|
|
@ -606,7 +606,7 @@ track 0:
|
|||
track 1:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = null
|
||||
id = 1
|
||||
containerMimeType = null
|
||||
sampleMimeType = application/id3
|
||||
maxInputSize = -1
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ numberOfTracks = 2
|
|||
track 192:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = null
|
||||
id = 192
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/mpeg-L2
|
||||
maxInputSize = 4096
|
||||
|
|
@ -45,7 +45,7 @@ track 192:
|
|||
track 224:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = null
|
||||
id = 224
|
||||
containerMimeType = null
|
||||
sampleMimeType = video/mpeg2
|
||||
maxInputSize = -1
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ numberOfTracks = 2
|
|||
track 256:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = null
|
||||
id = 1/256
|
||||
containerMimeType = null
|
||||
sampleMimeType = video/mpeg2
|
||||
maxInputSize = -1
|
||||
|
|
@ -38,7 +38,7 @@ track 256:
|
|||
track 257:
|
||||
format:
|
||||
bitrate = -1
|
||||
id = null
|
||||
id = 1/257
|
||||
containerMimeType = null
|
||||
sampleMimeType = audio/mpeg-L2
|
||||
maxInputSize = 4096
|
||||
|
|
|
|||
|
|
@ -460,6 +460,11 @@ public final class ExoPlayerTest extends TestCase {
|
|||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void discardBuffer(long positionUs) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public long readDiscontinuity() {
|
||||
assertTrue(preparedPeriod);
|
||||
|
|
@ -513,8 +518,9 @@ public final class ExoPlayerTest extends TestCase {
|
|||
}
|
||||
|
||||
@Override
|
||||
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer) {
|
||||
if (buffer == null || !readFormat) {
|
||||
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
|
||||
boolean formatRequired) {
|
||||
if (formatRequired || !readFormat) {
|
||||
formatHolder.format = format;
|
||||
readFormat = true;
|
||||
return C.RESULT_FORMAT_READ;
|
||||
|
|
@ -571,7 +577,7 @@ public final class ExoPlayerTest extends TestCase {
|
|||
FormatHolder formatHolder = new FormatHolder();
|
||||
DecoderInputBuffer buffer =
|
||||
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
|
||||
int result = readSource(formatHolder, buffer);
|
||||
int result = readSource(formatHolder, buffer, false);
|
||||
if (result == C.RESULT_FORMAT_READ) {
|
||||
formatReadCount++;
|
||||
assertEquals(expectedFormat, formatHolder.format);
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ 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 com.google.android.exoplayer2.util.MimeTypes;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import org.mockito.Mock;
|
||||
|
|
@ -217,7 +218,11 @@ public class OfflineLicenseHelperTest extends InstrumentationTestCase {
|
|||
}
|
||||
|
||||
private static Representation newRepresentations(DrmInitData drmInitData) {
|
||||
Format format = Format.createVideoSampleFormat("", "", "", 0, 0, 0, 0, 0, null, drmInitData);
|
||||
Format format = Format.createVideoContainerFormat("id", MimeTypes.VIDEO_MP4,
|
||||
MimeTypes.VIDEO_H264, "", Format.NO_VALUE, 1024, 768, Format.NO_VALUE, null, 0);
|
||||
if (drmInitData != null) {
|
||||
format = format.copyWithDrmInitData(drmInitData);
|
||||
}
|
||||
return Representation.newInstance("", 0, format, "", new SingleSegmentBase());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,21 +25,32 @@ 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(EXTRACTOR_FACTORY, "mp4/sample_fragmented.mp4", getInstrumentation());
|
||||
TestUtil.assertOutput(getExtractorFactory(), "mp4/sample_fragmented.mp4", getInstrumentation());
|
||||
}
|
||||
|
||||
public void testSampleWithSeiPayloadParsing() throws Exception {
|
||||
// Enabling the CEA-608 track enables SEI payload parsing.
|
||||
TestUtil.assertOutput(getExtractorFactory(FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK),
|
||||
"mp4/sample_fragmented_sei.mp4", getInstrumentation());
|
||||
}
|
||||
|
||||
public void testAtomWithZeroSize() throws Exception {
|
||||
TestUtil.assertThrows(EXTRACTOR_FACTORY, "mp4/sample_fragmented_zero_size_atom.mp4",
|
||||
TestUtil.assertThrows(getExtractorFactory(), "mp4/sample_fragmented_zero_size_atom.mp4",
|
||||
getInstrumentation(), ParserException.class);
|
||||
}
|
||||
|
||||
private static TestUtil.ExtractorFactory getExtractorFactory() {
|
||||
return getExtractorFactory(0);
|
||||
}
|
||||
|
||||
private static TestUtil.ExtractorFactory getExtractorFactory(final int flags) {
|
||||
return new TestUtil.ExtractorFactory() {
|
||||
@Override
|
||||
public Extractor create() {
|
||||
return new FragmentedMp4Extractor(flags, null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,8 +69,8 @@ public class AdtsReaderTest extends TestCase {
|
|||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
FakeExtractorOutput fakeExtractorOutput = new FakeExtractorOutput();
|
||||
adtsOutput = fakeExtractorOutput.track(0);
|
||||
id3Output = fakeExtractorOutput.track(1);
|
||||
adtsOutput = fakeExtractorOutput.track(0, C.TRACK_TYPE_AUDIO);
|
||||
id3Output = fakeExtractorOutput.track(1, C.TRACK_TYPE_METADATA);
|
||||
adtsReader = new AdtsReader(true);
|
||||
TrackIdGenerator idGenerator = new TrackIdGenerator(0, 1);
|
||||
adtsReader.createTracks(fakeExtractorOutput, idGenerator);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ package com.google.android.exoplayer2.extractor.ts;
|
|||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import android.util.SparseArray;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
|
|
@ -74,7 +75,8 @@ public final class TsExtractorTest extends InstrumentationTestCase {
|
|||
|
||||
public void testCustomPesReader() throws Exception {
|
||||
CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(true, false);
|
||||
TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory, false);
|
||||
TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_NORMAL, new TimestampAdjuster(0),
|
||||
factory);
|
||||
FakeExtractorInput input = new FakeExtractorInput.Builder()
|
||||
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample.ts"))
|
||||
.setSimulateIOErrors(false)
|
||||
|
|
@ -92,13 +94,14 @@ public final class TsExtractorTest extends InstrumentationTestCase {
|
|||
TrackOutput trackOutput = reader.getTrackOutput();
|
||||
assertTrue(trackOutput == output.trackOutputs.get(257 /* PID of audio track. */));
|
||||
assertEquals(
|
||||
Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0, "und", null, 0),
|
||||
Format.createTextSampleFormat("1/257", "mime", null, 0, 0, "und", null, 0),
|
||||
((FakeTrackOutput) trackOutput).format);
|
||||
}
|
||||
|
||||
public void testCustomInitialSectionReader() throws Exception {
|
||||
CustomTsPayloadReaderFactory factory = new CustomTsPayloadReaderFactory(false, true);
|
||||
TsExtractor tsExtractor = new TsExtractor(new TimestampAdjuster(0), factory, false);
|
||||
TsExtractor tsExtractor = new TsExtractor(TsExtractor.MODE_NORMAL, new TimestampAdjuster(0),
|
||||
factory);
|
||||
FakeExtractorInput input = new FakeExtractorInput.Builder()
|
||||
.setData(TestUtil.getByteArray(getInstrumentation(), "ts/sample_with_sdt.ts"))
|
||||
.setSimulateIOErrors(false)
|
||||
|
|
@ -178,8 +181,9 @@ public final class TsExtractorTest extends InstrumentationTestCase {
|
|||
|
||||
@Override
|
||||
public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) {
|
||||
output = extractorOutput.track(idGenerator.getNextId());
|
||||
output.format(Format.createTextSampleFormat("Overriding format", "mime", null, 0, 0,
|
||||
idGenerator.generateNewId();
|
||||
output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_UNKNOWN);
|
||||
output.format(Format.createTextSampleFormat(idGenerator.getFormatId(), "mime", null, 0, 0,
|
||||
language, null, 0));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* 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.source.dash.manifest;
|
||||
|
||||
import android.net.Uri;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link DashManifest}.
|
||||
*/
|
||||
public class DashManifestTest extends TestCase {
|
||||
|
||||
private static final UtcTimingElement DUMMY_UTC_TIMING = new UtcTimingElement("", "");
|
||||
private static final List<SchemeValuePair> DUMMY_ACCESSIBILITY_DESCRIPTORS =
|
||||
Collections.emptyList();
|
||||
private static final SingleSegmentBase DUMMY_SEGMENT_BASE = new SingleSegmentBase();
|
||||
private static final Format DUMMY_FORMAT = Format.createSampleFormat("", "", 0);
|
||||
|
||||
public void testCopy() throws Exception {
|
||||
Representation[][][] representations = newRepresentations(3, 2, 3);
|
||||
DashManifest sourceManifest = newDashManifest(10,
|
||||
newPeriod("1", 1,
|
||||
newAdaptationSet(2, representations[0][0]),
|
||||
newAdaptationSet(3, representations[0][1])),
|
||||
newPeriod("4", 4,
|
||||
newAdaptationSet(5, representations[1][0]),
|
||||
newAdaptationSet(6, representations[1][1])),
|
||||
newPeriod("7", 7,
|
||||
newAdaptationSet(8, representations[2][0]),
|
||||
newAdaptationSet(9, representations[2][1])));
|
||||
|
||||
List<RepresentationKey> keys = Arrays.asList(
|
||||
new RepresentationKey(0, 0, 0),
|
||||
new RepresentationKey(0, 0, 1),
|
||||
new RepresentationKey(0, 1, 2),
|
||||
|
||||
new RepresentationKey(1, 0, 1),
|
||||
new RepresentationKey(1, 1, 0),
|
||||
new RepresentationKey(1, 1, 2),
|
||||
|
||||
new RepresentationKey(2, 0, 1),
|
||||
new RepresentationKey(2, 0, 2),
|
||||
new RepresentationKey(2, 1, 0));
|
||||
// Keys don't need to be in any particular order
|
||||
Collections.shuffle(keys, new Random(0));
|
||||
|
||||
DashManifest copyManifest = sourceManifest.copy(keys);
|
||||
|
||||
DashManifest expectedManifest = newDashManifest(10,
|
||||
newPeriod("1", 1,
|
||||
newAdaptationSet(2, representations[0][0][0], representations[0][0][1]),
|
||||
newAdaptationSet(3, representations[0][1][2])),
|
||||
newPeriod("4", 4,
|
||||
newAdaptationSet(5, representations[1][0][1]),
|
||||
newAdaptationSet(6, representations[1][1][0], representations[1][1][2])),
|
||||
newPeriod("7", 7,
|
||||
newAdaptationSet(8, representations[2][0][1], representations[2][0][2]),
|
||||
newAdaptationSet(9, representations[2][1][0])));
|
||||
assertManifestEquals(expectedManifest, copyManifest);
|
||||
}
|
||||
|
||||
public void testCopySameAdaptationIndexButDifferentPeriod() throws Exception {
|
||||
Representation[][][] representations = newRepresentations(2, 1, 1);
|
||||
DashManifest sourceManifest = newDashManifest(10,
|
||||
newPeriod("1", 1,
|
||||
newAdaptationSet(2, representations[0][0])),
|
||||
newPeriod("4", 4,
|
||||
newAdaptationSet(5, representations[1][0])));
|
||||
|
||||
DashManifest copyManifest = sourceManifest.copy(Arrays.asList(
|
||||
new RepresentationKey(0, 0, 0),
|
||||
new RepresentationKey(1, 0, 0)));
|
||||
|
||||
DashManifest expectedManifest = newDashManifest(10,
|
||||
newPeriod("1", 1,
|
||||
newAdaptationSet(2, representations[0][0])),
|
||||
newPeriod("4", 4,
|
||||
newAdaptationSet(5, representations[1][0])));
|
||||
assertManifestEquals(expectedManifest, copyManifest);
|
||||
}
|
||||
|
||||
public void testCopySkipPeriod() throws Exception {
|
||||
Representation[][][] representations = newRepresentations(3, 2, 3);
|
||||
DashManifest sourceManifest = newDashManifest(10,
|
||||
newPeriod("1", 1,
|
||||
newAdaptationSet(2, representations[0][0]),
|
||||
newAdaptationSet(3, representations[0][1])),
|
||||
newPeriod("4", 4,
|
||||
newAdaptationSet(5, representations[1][0]),
|
||||
newAdaptationSet(6, representations[1][1])),
|
||||
newPeriod("7", 7,
|
||||
newAdaptationSet(8, representations[2][0]),
|
||||
newAdaptationSet(9, representations[2][1])));
|
||||
|
||||
DashManifest copyManifest = sourceManifest.copy(Arrays.asList(
|
||||
new RepresentationKey(0, 0, 0),
|
||||
new RepresentationKey(0, 0, 1),
|
||||
new RepresentationKey(0, 1, 2),
|
||||
|
||||
new RepresentationKey(2, 0, 1),
|
||||
new RepresentationKey(2, 0, 2),
|
||||
new RepresentationKey(2, 1, 0)));
|
||||
|
||||
DashManifest expectedManifest = newDashManifest(7,
|
||||
newPeriod("1", 1,
|
||||
newAdaptationSet(2, representations[0][0][0], representations[0][0][1]),
|
||||
newAdaptationSet(3, representations[0][1][2])),
|
||||
newPeriod("7", 4,
|
||||
newAdaptationSet(8, representations[2][0][1], representations[2][0][2]),
|
||||
newAdaptationSet(9, representations[2][1][0])));
|
||||
assertManifestEquals(expectedManifest, copyManifest);
|
||||
}
|
||||
|
||||
private static void assertManifestEquals(DashManifest expected, DashManifest actual) {
|
||||
assertEquals(expected.availabilityStartTime, actual.availabilityStartTime);
|
||||
assertEquals(expected.duration, actual.duration);
|
||||
assertEquals(expected.minBufferTime, actual.minBufferTime);
|
||||
assertEquals(expected.dynamic, actual.dynamic);
|
||||
assertEquals(expected.minUpdatePeriod, actual.minUpdatePeriod);
|
||||
assertEquals(expected.timeShiftBufferDepth, actual.timeShiftBufferDepth);
|
||||
assertEquals(expected.suggestedPresentationDelay, actual.suggestedPresentationDelay);
|
||||
assertEquals(expected.utcTiming, actual.utcTiming);
|
||||
assertEquals(expected.location, actual.location);
|
||||
assertEquals(expected.getPeriodCount(), actual.getPeriodCount());
|
||||
for (int i = 0; i < expected.getPeriodCount(); i++) {
|
||||
Period expectedPeriod = expected.getPeriod(i);
|
||||
Period actualPeriod = actual.getPeriod(i);
|
||||
assertEquals(expectedPeriod.id, actualPeriod.id);
|
||||
assertEquals(expectedPeriod.startMs, actualPeriod.startMs);
|
||||
List<AdaptationSet> expectedAdaptationSets = expectedPeriod.adaptationSets;
|
||||
List<AdaptationSet> actualAdaptationSets = actualPeriod.adaptationSets;
|
||||
assertEquals(expectedAdaptationSets.size(), actualAdaptationSets.size());
|
||||
for (int j = 0; j < expectedAdaptationSets.size(); j++) {
|
||||
AdaptationSet expectedAdaptationSet = expectedAdaptationSets.get(j);
|
||||
AdaptationSet actualAdaptationSet = actualAdaptationSets.get(j);
|
||||
assertEquals(expectedAdaptationSet.id, actualAdaptationSet.id);
|
||||
assertEquals(expectedAdaptationSet.type, actualAdaptationSet.type);
|
||||
assertEquals(expectedAdaptationSet.accessibilityDescriptors,
|
||||
actualAdaptationSet.accessibilityDescriptors);
|
||||
assertEquals(expectedAdaptationSet.representations, actualAdaptationSet.representations);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Representation[][][] newRepresentations(int periodCount, int adaptationSetCounts,
|
||||
int representationCounts) {
|
||||
Representation[][][] representations = new Representation[periodCount][][];
|
||||
for (int i = 0; i < periodCount; i++) {
|
||||
representations[i] = new Representation[adaptationSetCounts][];
|
||||
for (int j = 0; j < adaptationSetCounts; j++) {
|
||||
representations[i][j] = new Representation[representationCounts];
|
||||
for (int k = 0; k < representationCounts; k++) {
|
||||
representations[i][j][k] = newRepresentation();
|
||||
}
|
||||
}
|
||||
}
|
||||
return representations;
|
||||
}
|
||||
|
||||
private static Representation newRepresentation() {
|
||||
return Representation.newInstance("", 0, DUMMY_FORMAT, "", DUMMY_SEGMENT_BASE);
|
||||
}
|
||||
|
||||
private static DashManifest newDashManifest(int duration, Period... periods) {
|
||||
return new DashManifest(0, duration, 1, false, 2, 3, 4, DUMMY_UTC_TIMING, Uri.EMPTY,
|
||||
Arrays.asList(periods));
|
||||
}
|
||||
|
||||
private static Period newPeriod(String id, int startMs, AdaptationSet... adaptationSets) {
|
||||
return new Period(id, startMs, Arrays.asList(adaptationSets));
|
||||
}
|
||||
|
||||
private static AdaptationSet newAdaptationSet(int seed, Representation... representations) {
|
||||
return new AdaptationSet(++seed, ++seed, Arrays.asList(representations),
|
||||
DUMMY_ACCESSIBILITY_DESCRIPTORS);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ import android.net.Uri;
|
|||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
|
|
@ -53,12 +54,14 @@ public class HlsMasterPlaylistParserTest extends TestCase {
|
|||
+ "#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);
|
||||
private static final String MASTER_PLAYLIST_WITH_CC = " #EXTM3U \n"
|
||||
+ "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,LANGUAGE=\"es\",NAME=\"Eng\",INSTREAM-ID=\"SERVICE4\"\n"
|
||||
+ "\n"
|
||||
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n"
|
||||
+ "http://example.com/low.m3u8\n";
|
||||
|
||||
HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist;
|
||||
public void testParseMasterPlaylist() throws IOException{
|
||||
HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST);
|
||||
|
||||
List<HlsMasterPlaylist.HlsUrl> variants = masterPlaylist.variants;
|
||||
assertNotNull(variants);
|
||||
|
|
@ -98,18 +101,28 @@ public class HlsMasterPlaylistParserTest extends TestCase {
|
|||
|
||||
public void testPlaylistWithInvalidHeader() throws IOException {
|
||||
try {
|
||||
parsePlaylist(PLAYLIST_URI, PLAYLIST_WITH_INVALID_HEADER);
|
||||
parseMasterPlaylist(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 {
|
||||
public void testPlaylistWithClosedCaption() throws IOException {
|
||||
HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, MASTER_PLAYLIST_WITH_CC);
|
||||
assertEquals(1, playlist.muxedCaptionFormats.size());
|
||||
Format closedCaptionFormat = playlist.muxedCaptionFormats.get(0);
|
||||
assertEquals(MimeTypes.APPLICATION_CEA708, closedCaptionFormat.sampleMimeType);
|
||||
assertEquals(4, closedCaptionFormat.accessibilityChannel);
|
||||
assertEquals("es", closedCaptionFormat.language);
|
||||
}
|
||||
|
||||
private static HlsMasterPlaylist parseMasterPlaylist(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);
|
||||
return (HlsMasterPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
|
|||
String playlistString = "#EXTM3U\n"
|
||||
+ "#EXT-X-VERSION:3\n"
|
||||
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
|
||||
+ "#EXT-X-START:TIME-OFFSET=-25"
|
||||
+ "#EXT-X-TARGETDURATION:8\n"
|
||||
+ "#EXT-X-MEDIA-SEQUENCE:2679\n"
|
||||
+ "#EXT-X-DISCONTINUITY-SEQUENCE:4\n"
|
||||
|
|
@ -73,6 +74,7 @@ public class HlsMediaPlaylistParserTest extends TestCase {
|
|||
|
||||
HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
|
||||
assertEquals(HlsMediaPlaylist.PLAYLIST_TYPE_VOD, mediaPlaylist.playlistType);
|
||||
assertEquals(mediaPlaylist.durationUs - 25000000, mediaPlaylist.startOffsetUs);
|
||||
|
||||
assertEquals(2679, mediaPlaylist.mediaSequence);
|
||||
assertEquals(3, mediaPlaylist.version);
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ import android.test.InstrumentationTestCase;
|
|||
import android.test.MoreAsserts;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSource;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSource.Builder;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.upstream.DataSpec;
|
||||
import com.google.android.exoplayer2.upstream.FileDataSource;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
|
@ -42,13 +42,13 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
|
|||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
|
||||
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
|
||||
simpleCache = new SimpleCache(cacheDir, new NoOpCacheEvictor());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
TestUtil.recursiveDelete(cacheDir);
|
||||
Util.recursiveDelete(cacheDir);
|
||||
}
|
||||
|
||||
public void testMaxCacheFileSize() throws Exception {
|
||||
|
|
@ -126,9 +126,15 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
|
|||
MoreAsserts.assertEmpty(simpleCache.getKeys());
|
||||
}
|
||||
|
||||
public void testReadOnlyCache() throws Exception {
|
||||
CacheDataSource cacheDataSource = createCacheDataSource(false, false, 0, null);
|
||||
assertReadDataContentLength(cacheDataSource, false, false);
|
||||
assertEquals(0, cacheDir.list().length);
|
||||
}
|
||||
|
||||
private void assertCacheAndRead(boolean unboundedRequest, boolean simulateUnknownLength)
|
||||
throws IOException {
|
||||
// Read all data from upstream and cache
|
||||
// Read all data from upstream and write to cache
|
||||
CacheDataSource cacheDataSource = createCacheDataSource(false, simulateUnknownLength);
|
||||
assertReadDataContentLength(cacheDataSource, unboundedRequest, simulateUnknownLength);
|
||||
|
||||
|
|
@ -184,14 +190,21 @@ public class CacheDataSourceTest extends InstrumentationTestCase {
|
|||
|
||||
private CacheDataSource createCacheDataSource(boolean setReadException,
|
||||
boolean simulateUnknownLength, @CacheDataSource.Flags int flags) {
|
||||
Builder builder = new Builder();
|
||||
return createCacheDataSource(setReadException, simulateUnknownLength, flags,
|
||||
new CacheDataSink(simpleCache, MAX_CACHE_FILE_SIZE));
|
||||
}
|
||||
|
||||
private CacheDataSource createCacheDataSource(boolean setReadException,
|
||||
boolean simulateUnknownLength, @CacheDataSource.Flags int flags,
|
||||
CacheDataSink cacheWriteDataSink) {
|
||||
FakeDataSource.Builder builder = new FakeDataSource.Builder();
|
||||
if (setReadException) {
|
||||
builder.appendReadError(new IOException("Shouldn't read from upstream"));
|
||||
}
|
||||
builder.setSimulateUnknownLength(simulateUnknownLength);
|
||||
builder.appendReadData(TEST_DATA);
|
||||
FakeDataSource upstream = builder.build();
|
||||
return new CacheDataSource(simpleCache, upstream, flags, MAX_CACHE_FILE_SIZE);
|
||||
FakeDataSource upstream =
|
||||
builder.setSimulateUnknownLength(simulateUnknownLength).appendReadData(TEST_DATA).build();
|
||||
return new CacheDataSource(simpleCache, upstream, new FileDataSource(), cacheWriteDataSink,
|
||||
flags, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import android.test.InstrumentationTestCase;
|
|||
import android.test.MoreAsserts;
|
||||
import android.util.SparseArray;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
|
|
@ -36,13 +36,13 @@ public class CachedContentIndexTest extends InstrumentationTestCase {
|
|||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
|
||||
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
|
||||
index = new CachedContentIndex(cacheDir);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
TestUtil.recursiveDelete(cacheDir);
|
||||
Util.recursiveDelete(cacheDir);
|
||||
}
|
||||
|
||||
public void testAddGetRemove() throws Exception {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ 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 com.google.android.exoplayer2.util.Util;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import org.mockito.Mock;
|
||||
|
|
@ -49,13 +50,13 @@ public final class CachedRegionTrackerTest extends InstrumentationTestCase {
|
|||
|
||||
tracker = new CachedRegionTracker(cache, CACHE_KEY, CHUNK_INDEX);
|
||||
|
||||
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
|
||||
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
|
||||
index = new CachedContentIndex(cacheDir);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
TestUtil.recursiveDelete(cacheDir);
|
||||
Util.recursiveDelete(cacheDir);
|
||||
}
|
||||
|
||||
public void testGetRegion_noSpansInCache() {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
package com.google.android.exoplayer2.upstream.cache;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
|
@ -48,13 +48,13 @@ public class SimpleCacheSpanTest extends InstrumentationTestCase {
|
|||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
|
||||
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
|
||||
index = new CachedContentIndex(cacheDir);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
TestUtil.recursiveDelete(cacheDir);
|
||||
Util.recursiveDelete(cacheDir);
|
||||
}
|
||||
|
||||
public void testCacheFile() throws Exception {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ 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;
|
||||
|
|
@ -39,12 +38,12 @@ public class SimpleCacheTest extends InstrumentationTestCase {
|
|||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
this.cacheDir = TestUtil.createTempFolder(getInstrumentation().getContext());
|
||||
cacheDir = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
TestUtil.recursiveDelete(cacheDir);
|
||||
Util.recursiveDelete(cacheDir);
|
||||
}
|
||||
|
||||
public void testCommittingOneFile() throws Exception {
|
||||
|
|
@ -192,6 +191,41 @@ public class SimpleCacheTest extends InstrumentationTestCase {
|
|||
assertEquals(0, cacheDir.listFiles().length);
|
||||
}
|
||||
|
||||
|
||||
public void testGetCachedBytes() throws Exception {
|
||||
SimpleCache simpleCache = getSimpleCache();
|
||||
CacheSpan cacheSpan = simpleCache.startReadWrite(KEY_1, 0);
|
||||
|
||||
// No cached bytes, returns -'length'
|
||||
assertEquals(-100, simpleCache.getCachedBytes(KEY_1, 0, 100));
|
||||
|
||||
// Position value doesn't affect the return value
|
||||
assertEquals(-100, simpleCache.getCachedBytes(KEY_1, 20, 100));
|
||||
|
||||
addCache(simpleCache, KEY_1, 0, 15);
|
||||
|
||||
// Returns the length of a single span
|
||||
assertEquals(15, simpleCache.getCachedBytes(KEY_1, 0, 100));
|
||||
|
||||
// Value is capped by the 'length'
|
||||
assertEquals(10, simpleCache.getCachedBytes(KEY_1, 0, 10));
|
||||
|
||||
addCache(simpleCache, KEY_1, 15, 35);
|
||||
|
||||
// Returns the length of two adjacent spans
|
||||
assertEquals(50, simpleCache.getCachedBytes(KEY_1, 0, 100));
|
||||
|
||||
addCache(simpleCache, KEY_1, 60, 10);
|
||||
|
||||
// Not adjacent span doesn't affect return value
|
||||
assertEquals(50, simpleCache.getCachedBytes(KEY_1, 0, 100));
|
||||
|
||||
// Returns length of hole up to the next cached span
|
||||
assertEquals(-5, simpleCache.getCachedBytes(KEY_1, 55, 100));
|
||||
|
||||
simpleCache.releaseHoleSpan(cacheSpan);
|
||||
}
|
||||
|
||||
private SimpleCache getSimpleCache() {
|
||||
return new SimpleCache(cacheDir, new NoOpCacheEvictor());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
package com.google.android.exoplayer2.util;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
|
@ -34,14 +33,14 @@ public class AtomicFileTest extends InstrumentationTestCase {
|
|||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
tempFolder = TestUtil.createTempFolder(getInstrumentation().getContext());
|
||||
tempFolder = Util.createTempDirectory(getInstrumentation().getContext(), "ExoPlayerTest");
|
||||
file = new File(tempFolder, "atomicFile");
|
||||
atomicFile = new AtomicFile(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDown() throws Exception {
|
||||
TestUtil.recursiveDelete(tempFolder);
|
||||
Util.recursiveDelete(tempFolder);
|
||||
}
|
||||
|
||||
public void testDelete() throws Exception {
|
||||
|
|
|
|||
|
|
@ -142,8 +142,10 @@ public class UtilTest extends TestCase {
|
|||
public void testParseXsDateTime() throws Exception {
|
||||
assertEquals(1403219262000L, Util.parseXsDateTime("2014-06-19T23:07:42"));
|
||||
assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00Z"));
|
||||
assertEquals(1407322800000L, Util.parseXsDateTime("2014-08-06T11:00:00,000Z"));
|
||||
assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-08:00"));
|
||||
assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55-0800"));
|
||||
assertEquals(1411161535000L, Util.parseXsDateTime("2014-09-19T13:18:55.000-0800"));
|
||||
}
|
||||
|
||||
public void testUnescapeInvalidFileName() {
|
||||
|
|
|
|||
|
|
@ -254,6 +254,14 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
|||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use {@link #readSource(FormatHolder, DecoderInputBuffer, boolean)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer) {
|
||||
return readSource(formatHolder, buffer, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads from the enabled upstream source. If the upstream source has been read to the end then
|
||||
* {@link C#RESULT_BUFFER_READ} is only returned if {@link #setCurrentStreamFinal()} has been
|
||||
|
|
@ -262,13 +270,16 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
|
|||
* @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. May be null if the
|
||||
* caller requires that the format of the stream be read even if it's not changing.
|
||||
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.
|
||||
* @param formatRequired Whether the caller requires that the format of the stream be read even if
|
||||
* it's not changing. A sample will never be read if set to true, however it is still possible
|
||||
* for the end of stream or nothing to be read.
|
||||
* @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
|
||||
* {@link C#RESULT_BUFFER_READ}.
|
||||
*/
|
||||
protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer) {
|
||||
int result = stream.readData(formatHolder, buffer);
|
||||
protected final int readSource(FormatHolder formatHolder, DecoderInputBuffer buffer,
|
||||
boolean formatRequired) {
|
||||
int result = stream.readData(formatHolder, buffer, formatRequired);
|
||||
if (result == C.RESULT_BUFFER_READ) {
|
||||
if (buffer.isEndOfStream()) {
|
||||
readEndOfStream = true;
|
||||
|
|
|
|||
|
|
@ -443,9 +443,16 @@ public final class C {
|
|||
*/
|
||||
public static final UUID UUID_NIL = new UUID(0L, 0L);
|
||||
|
||||
/**
|
||||
* UUID for the ClearKey DRM scheme.
|
||||
* <p>
|
||||
* ClearKey is supported on Android devices running Android 5.0 (API Level 21) and up.
|
||||
*/
|
||||
public static final UUID CLEARKEY_UUID = new UUID(0x1077EFECC0B24D02L, 0xACE33C1E52E2FB4BL);
|
||||
|
||||
/**
|
||||
* UUID for the Widevine DRM scheme.
|
||||
* <p></p>
|
||||
* <p>
|
||||
* Widevine is supported on Android devices running Android 4.3 (API Level 18) and up.
|
||||
*/
|
||||
public static final UUID WIDEVINE_UUID = new UUID(0xEDEF8BA979D64ACEL, 0xA3C827DCD51D21EDL);
|
||||
|
|
@ -477,7 +484,7 @@ public final class C {
|
|||
* {@link ExoPlayer#sendMessages} or {@link ExoPlayer#blockingSendMessages}. The message object
|
||||
* should be a {@link android.media.PlaybackParams}, or null, which will be used to configure the
|
||||
* underlying {@link android.media.AudioTrack}. The message object should not be modified by the
|
||||
* caller after it has been passed
|
||||
* caller after it has been passed.
|
||||
*/
|
||||
public static final int MSG_SET_PLAYBACK_PARAMS = 3;
|
||||
|
||||
|
|
@ -515,7 +522,13 @@ public final class C {
|
|||
* The stereo mode for 360/3D/VR videos.
|
||||
*/
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef({Format.NO_VALUE, STEREO_MODE_MONO, STEREO_MODE_TOP_BOTTOM, STEREO_MODE_LEFT_RIGHT})
|
||||
@IntDef({
|
||||
Format.NO_VALUE,
|
||||
STEREO_MODE_MONO,
|
||||
STEREO_MODE_TOP_BOTTOM,
|
||||
STEREO_MODE_LEFT_RIGHT,
|
||||
STEREO_MODE_STEREO_MESH
|
||||
})
|
||||
public @interface StereoMode {}
|
||||
/**
|
||||
* Indicates Monoscopic stereo layout, used with 360/3D/VR videos.
|
||||
|
|
@ -529,6 +542,16 @@ public final class C {
|
|||
* Indicates Left-Right stereo layout, used with 360/3D/VR videos.
|
||||
*/
|
||||
public static final int STEREO_MODE_LEFT_RIGHT = 2;
|
||||
/**
|
||||
* Indicates a stereo layout where the left and right eyes have separate meshes,
|
||||
* used with 360/3D/VR videos.
|
||||
*/
|
||||
public static final int STEREO_MODE_STEREO_MESH = 3;
|
||||
|
||||
/**
|
||||
* Priority for media playback.
|
||||
*/
|
||||
public static final int PRIORITY_PLAYBACK = 0;
|
||||
|
||||
/**
|
||||
* Converts a time in microseconds to the corresponding time in milliseconds, preserving
|
||||
|
|
|
|||
|
|
@ -51,11 +51,6 @@ 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;
|
||||
|
|
@ -122,7 +117,7 @@ public final class DefaultLoadControl implements LoadControl {
|
|||
* 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
|
||||
* {@link C#PRIORITY_PLAYBACK} during loading periods, and unregisters itself during draining
|
||||
* periods.
|
||||
*/
|
||||
public DefaultLoadControl(DefaultAllocator allocator, int minBufferMs, int maxBufferMs,
|
||||
|
|
@ -183,9 +178,9 @@ public final class DefaultLoadControl implements LoadControl {
|
|||
|| (bufferTimeState == BETWEEN_WATERMARKS && isBuffering && !targetBufferSizeReached);
|
||||
if (priorityTaskManager != null && isBuffering != wasBuffering) {
|
||||
if (isBuffering) {
|
||||
priorityTaskManager.add(LOADING_PRIORITY);
|
||||
priorityTaskManager.add(C.PRIORITY_PLAYBACK);
|
||||
} else {
|
||||
priorityTaskManager.remove(LOADING_PRIORITY);
|
||||
priorityTaskManager.remove(C.PRIORITY_PLAYBACK);
|
||||
}
|
||||
}
|
||||
return isBuffering;
|
||||
|
|
@ -199,7 +194,7 @@ public final class DefaultLoadControl implements LoadControl {
|
|||
private void reset(boolean resetAllocator) {
|
||||
targetBufferSize = 0;
|
||||
if (priorityTaskManager != null && isBuffering) {
|
||||
priorityTaskManager.remove(LOADING_PRIORITY);
|
||||
priorityTaskManager.remove(C.PRIORITY_PLAYBACK);
|
||||
}
|
||||
isBuffering = false;
|
||||
if (resetAllocator) {
|
||||
|
|
|
|||
|
|
@ -56,8 +56,7 @@ public final class ExoPlaybackException extends Exception {
|
|||
* The type of the playback failure. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER} and
|
||||
* {@link #TYPE_UNEXPECTED}.
|
||||
*/
|
||||
@Type
|
||||
public final int type;
|
||||
@Type public final int type;
|
||||
|
||||
/**
|
||||
* If {@link #type} is {@link #TYPE_RENDERER}, this is the index of the renderer.
|
||||
|
|
|
|||
|
|
@ -455,6 +455,8 @@ import java.io.IOException;
|
|||
TraceUtil.beginSection("doSomeWork");
|
||||
|
||||
updatePlaybackPositions();
|
||||
playingPeriodHolder.mediaPeriod.discardBuffer(playbackInfo.positionUs);
|
||||
|
||||
boolean allRenderersEnded = true;
|
||||
boolean allRenderersReadyOrEnded = true;
|
||||
for (Renderer renderer : enabledRenderers) {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ public interface ExoPlayerLibraryInfo {
|
|||
/**
|
||||
* The version of the library, expressed as a string.
|
||||
*/
|
||||
String VERSION = "2.2.0";
|
||||
String VERSION = "2.3.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 = 2002000;
|
||||
int VERSION_INT = 2003000;
|
||||
|
||||
/**
|
||||
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ public final class Format implements Parcelable {
|
|||
/**
|
||||
* The stereo layout for 360/3D/VR video, or {@link #NO_VALUE} if not applicable. Valid stereo
|
||||
* modes are {@link C#STEREO_MODE_MONO}, {@link C#STEREO_MODE_TOP_BOTTOM}, {@link
|
||||
* C#STEREO_MODE_LEFT_RIGHT}.
|
||||
* C#STEREO_MODE_LEFT_RIGHT}, {@link C#STEREO_MODE_STEREO_MESH}.
|
||||
*/
|
||||
@C.StereoMode
|
||||
public final int stereoMode;
|
||||
|
|
@ -438,16 +438,19 @@ public final class Format implements Parcelable {
|
|||
drmInitData, metadata);
|
||||
}
|
||||
|
||||
public Format copyWithManifestFormatInfo(Format manifestFormat,
|
||||
boolean preferManifestDrmInitData) {
|
||||
public Format copyWithManifestFormatInfo(Format manifestFormat) {
|
||||
if (this == manifestFormat) {
|
||||
// No need to copy from ourselves.
|
||||
return this;
|
||||
}
|
||||
String id = manifestFormat.id;
|
||||
String codecs = this.codecs == null ? manifestFormat.codecs : this.codecs;
|
||||
int bitrate = this.bitrate == NO_VALUE ? manifestFormat.bitrate : this.bitrate;
|
||||
float frameRate = this.frameRate == NO_VALUE ? manifestFormat.frameRate : this.frameRate;
|
||||
@C.SelectionFlags int selectionFlags = this.selectionFlags | manifestFormat.selectionFlags;
|
||||
String language = this.language == null ? manifestFormat.language : this.language;
|
||||
DrmInitData drmInitData = (preferManifestDrmInitData && manifestFormat.drmInitData != null)
|
||||
|| this.drmInitData == null ? manifestFormat.drmInitData : this.drmInitData;
|
||||
DrmInitData drmInitData = manifestFormat.drmInitData != null ? manifestFormat.drmInitData
|
||||
: this.drmInitData;
|
||||
return new Format(id, containerMimeType, sampleMimeType, codecs, bitrate, maxInputSize, width,
|
||||
height, frameRate, rotationDegrees, pixelWidthHeightRatio, projectionData, stereoMode,
|
||||
channelCount, sampleRate, pcmEncoding, encoderDelay, encoderPadding, selectionFlags,
|
||||
|
|
@ -672,9 +675,6 @@ public final class Format implements Parcelable {
|
|||
dest.writeParcelable(metadata, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link Creator} implementation.
|
||||
*/
|
||||
public static final Creator<Format> CREATOR = new Creator<Format>() {
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import android.view.SurfaceHolder;
|
|||
import android.view.SurfaceView;
|
||||
import android.view.TextureView;
|
||||
import com.google.android.exoplayer2.audio.AudioCapabilities;
|
||||
import com.google.android.exoplayer2.audio.AudioProcessor;
|
||||
import com.google.android.exoplayer2.audio.AudioRendererEventListener;
|
||||
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
|
|
@ -624,7 +625,7 @@ public class SimpleExoPlayer implements ExoPlayer {
|
|||
buildVideoRenderers(context, mainHandler, drmSessionManager, extensionRendererMode,
|
||||
componentListener, allowedVideoJoiningTimeMs, out);
|
||||
buildAudioRenderers(context, mainHandler, drmSessionManager, extensionRendererMode,
|
||||
componentListener, out);
|
||||
componentListener, buildAudioProcessors(), out);
|
||||
buildTextRenderers(context, mainHandler, extensionRendererMode, componentListener, out);
|
||||
buildMetadataRenderers(context, mainHandler, extensionRendererMode, componentListener, out);
|
||||
buildMiscellaneousRenderers(context, mainHandler, extensionRendererMode, out);
|
||||
|
|
@ -636,7 +637,7 @@ public class SimpleExoPlayer implements ExoPlayer {
|
|||
* @param context The {@link Context} associated with the player.
|
||||
* @param mainHandler A handler associated with the main thread's looper.
|
||||
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will
|
||||
* not be used for DRM protected playbacks.
|
||||
* not be used for DRM protected playbacks.
|
||||
* @param extensionRendererMode The extension renderer mode.
|
||||
* @param eventListener An event listener.
|
||||
* @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video renderers
|
||||
|
|
@ -681,17 +682,19 @@ public class SimpleExoPlayer implements ExoPlayer {
|
|||
* @param context The {@link Context} associated with the player.
|
||||
* @param mainHandler A handler associated with the main thread's looper.
|
||||
* @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will
|
||||
* not be used for DRM protected playbacks.
|
||||
* not be used for DRM protected playbacks.
|
||||
* @param extensionRendererMode The extension renderer mode.
|
||||
* @param eventListener An event listener.
|
||||
* @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio buffers
|
||||
* before output. May be empty.
|
||||
* @param out An array to which the built renderers should be appended.
|
||||
*/
|
||||
protected void buildAudioRenderers(Context context, Handler mainHandler,
|
||||
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||
@ExtensionRendererMode int extensionRendererMode, AudioRendererEventListener eventListener,
|
||||
ArrayList<Renderer> out) {
|
||||
AudioProcessor[] audioProcessors, ArrayList<Renderer> out) {
|
||||
out.add(new MediaCodecAudioRenderer(MediaCodecSelector.DEFAULT, drmSessionManager, true,
|
||||
mainHandler, eventListener, AudioCapabilities.getCapabilities(context)));
|
||||
mainHandler, eventListener, AudioCapabilities.getCapabilities(context), audioProcessors));
|
||||
|
||||
if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
|
||||
return;
|
||||
|
|
@ -705,8 +708,9 @@ public class SimpleExoPlayer implements ExoPlayer {
|
|||
Class<?> clazz =
|
||||
Class.forName("com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer");
|
||||
Constructor<?> constructor = clazz.getConstructor(Handler.class,
|
||||
AudioRendererEventListener.class);
|
||||
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener);
|
||||
AudioRendererEventListener.class, AudioProcessor[].class);
|
||||
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener,
|
||||
audioProcessors);
|
||||
out.add(extensionRendererIndex++, renderer);
|
||||
Log.i(TAG, "Loaded LibopusAudioRenderer.");
|
||||
} catch (ClassNotFoundException e) {
|
||||
|
|
@ -719,8 +723,9 @@ public class SimpleExoPlayer implements ExoPlayer {
|
|||
Class<?> clazz =
|
||||
Class.forName("com.google.android.exoplayer2.ext.flac.LibflacAudioRenderer");
|
||||
Constructor<?> constructor = clazz.getConstructor(Handler.class,
|
||||
AudioRendererEventListener.class);
|
||||
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener);
|
||||
AudioRendererEventListener.class, AudioProcessor[].class);
|
||||
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener,
|
||||
audioProcessors);
|
||||
out.add(extensionRendererIndex++, renderer);
|
||||
Log.i(TAG, "Loaded LibflacAudioRenderer.");
|
||||
} catch (ClassNotFoundException e) {
|
||||
|
|
@ -733,8 +738,9 @@ public class SimpleExoPlayer implements ExoPlayer {
|
|||
Class<?> clazz =
|
||||
Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegAudioRenderer");
|
||||
Constructor<?> constructor = clazz.getConstructor(Handler.class,
|
||||
AudioRendererEventListener.class);
|
||||
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener);
|
||||
AudioRendererEventListener.class, AudioProcessor[].class);
|
||||
Renderer renderer = (Renderer) constructor.newInstance(mainHandler, componentListener,
|
||||
audioProcessors);
|
||||
out.add(extensionRendererIndex++, renderer);
|
||||
Log.i(TAG, "Loaded FfmpegAudioRenderer.");
|
||||
} catch (ClassNotFoundException e) {
|
||||
|
|
@ -787,6 +793,13 @@ public class SimpleExoPlayer implements ExoPlayer {
|
|||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an array of {@link AudioProcessor}s that will process PCM audio before output.
|
||||
*/
|
||||
protected AudioProcessor[] buildAudioProcessors() {
|
||||
return new AudioProcessor[0];
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private void removeSurfaceCallbacks() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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.audio;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* Interface for audio processors.
|
||||
*/
|
||||
public interface AudioProcessor {
|
||||
|
||||
/**
|
||||
* Exception thrown when a processor can't be configured for a given input audio format.
|
||||
*/
|
||||
final class UnhandledFormatException extends Exception {
|
||||
|
||||
public UnhandledFormatException(int sampleRateHz, int channelCount, @C.Encoding int encoding) {
|
||||
super("Unhandled format: " + sampleRateHz + " Hz, " + channelCount + " channels in encoding "
|
||||
+ encoding);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* An empty, direct {@link ByteBuffer}.
|
||||
*/
|
||||
ByteBuffer EMPTY_BUFFER = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder());
|
||||
|
||||
/**
|
||||
* Configures the processor to process input audio with the specified format. After calling this
|
||||
* method, {@link #isActive()} returns whether the processor needs to handle buffers; if not, the
|
||||
* processor will not accept any buffers until it is reconfigured. Returns {@code true} if the
|
||||
* processor must be flushed, or if the value returned by {@link #isActive()} has changed as a
|
||||
* result of the call. If it's active, {@link #getOutputChannelCount()} and
|
||||
* {@link #getOutputEncoding()} return the processor's output format.
|
||||
*
|
||||
* @param sampleRateHz The sample rate of input audio in Hz.
|
||||
* @param channelCount The number of interleaved channels in input audio.
|
||||
* @param encoding The encoding of input audio.
|
||||
* @return {@code true} if the processor must be flushed or the value returned by
|
||||
* {@link #isActive()} has changed as a result of the call.
|
||||
* @throws UnhandledFormatException Thrown if the specified format can't be handled as input.
|
||||
*/
|
||||
boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding)
|
||||
throws UnhandledFormatException;
|
||||
|
||||
/**
|
||||
* Returns whether the processor is configured and active.
|
||||
*/
|
||||
boolean isActive();
|
||||
|
||||
/**
|
||||
* Returns the number of audio channels in the data output by the processor.
|
||||
*/
|
||||
int getOutputChannelCount();
|
||||
|
||||
/**
|
||||
* Returns the audio encoding used in the data output by the processor.
|
||||
*/
|
||||
@C.Encoding
|
||||
int getOutputEncoding();
|
||||
|
||||
/**
|
||||
* Queues audio data between the position and limit of the input {@code buffer} for processing.
|
||||
* {@code buffer} must be a direct byte buffer with native byte order. Its contents are treated as
|
||||
* read-only. Its position will be advanced by the number of bytes consumed (which may be zero).
|
||||
* The caller retains ownership of the provided buffer. Calling this method invalidates any
|
||||
* previous buffer returned by {@link #getOutput()}.
|
||||
*
|
||||
* @param buffer The input buffer to process.
|
||||
*/
|
||||
void queueInput(ByteBuffer buffer);
|
||||
|
||||
/**
|
||||
* Queues an end of stream signal. After this method has been called,
|
||||
* {@link #queueInput(ByteBuffer)} may not be called until after the next call to
|
||||
* {@link #flush()}. Calling {@link #getOutput()} will return any remaining output data. Multiple
|
||||
* calls may be required to read all of the remaining output data. {@link #isEnded()} will return
|
||||
* {@code true} once all remaining output data has been read.
|
||||
*/
|
||||
void queueEndOfStream();
|
||||
|
||||
/**
|
||||
* Returns a buffer containing processed output data between its position and limit. The buffer
|
||||
* will always be a direct byte buffer with native byte order. Calling this method invalidates any
|
||||
* previously returned buffer. The buffer will be empty if no output is available.
|
||||
*
|
||||
* @return A buffer containing processed output data between its position and limit.
|
||||
*/
|
||||
ByteBuffer getOutput();
|
||||
|
||||
/**
|
||||
* Returns whether this processor will return no more output from {@link #getOutput()} until it
|
||||
* has been {@link #flush()}ed and more input has been queued.
|
||||
*/
|
||||
boolean isEnded();
|
||||
|
||||
/**
|
||||
* Clears any state in preparation for receiving a new stream of input buffers.
|
||||
*/
|
||||
void flush();
|
||||
|
||||
/**
|
||||
* Releases any resources associated with this instance.
|
||||
*/
|
||||
void release();
|
||||
|
||||
}
|
||||
|
|
@ -25,13 +25,13 @@ import android.os.ConditionVariable;
|
|||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Plays audio data. The implementation delegates to an {@link android.media.AudioTrack} and handles
|
||||
|
|
@ -54,8 +54,8 @@ import java.nio.ByteOrder;
|
|||
* safe to call {@link #handleBuffer(ByteBuffer, long)} after {@link #reset()} without calling
|
||||
* {@link #configure(String, int, int, int, int)}.
|
||||
* <p>
|
||||
* 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
|
||||
* Call {@link #playToEndOfStream()} repeatedly 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 {
|
||||
|
|
@ -91,6 +91,21 @@ public final class AudioTrack {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when a failure occurs configuring the track.
|
||||
*/
|
||||
public static final class ConfigurationException extends Exception {
|
||||
|
||||
public ConfigurationException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public ConfigurationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when a failure occurs initializing an {@link android.media.AudioTrack}.
|
||||
*/
|
||||
|
|
@ -255,6 +270,8 @@ public final class AudioTrack {
|
|||
public static boolean failOnSpuriousAudioTimestamp = false;
|
||||
|
||||
private final AudioCapabilities audioCapabilities;
|
||||
private final ChannelMappingAudioProcessor channelMappingAudioProcessor;
|
||||
private final AudioProcessor[] availableAudioProcessors;
|
||||
private final Listener listener;
|
||||
private final ConditionVariable releasingConditionVariable;
|
||||
private final long[] playheadOffsets;
|
||||
|
|
@ -268,14 +285,13 @@ public final class AudioTrack {
|
|||
private android.media.AudioTrack audioTrack;
|
||||
private int sampleRate;
|
||||
private int channelConfig;
|
||||
@C.Encoding
|
||||
private int encoding;
|
||||
@C.Encoding
|
||||
private int outputEncoding;
|
||||
@C.StreamType
|
||||
private int streamType;
|
||||
@C.Encoding
|
||||
private int sourceEncoding;
|
||||
@C.Encoding
|
||||
private int targetEncoding;
|
||||
private boolean passthrough;
|
||||
private int pcmFrameSize;
|
||||
private int bufferSize;
|
||||
private long bufferSizeUs;
|
||||
|
||||
|
|
@ -290,8 +306,12 @@ public final class AudioTrack {
|
|||
private long lastTimestampSampleTimeUs;
|
||||
|
||||
private Method getLatencyMethod;
|
||||
private int pcmFrameSize;
|
||||
private long submittedPcmBytes;
|
||||
private long submittedEncodedFrames;
|
||||
private int outputPcmFrameSize;
|
||||
private long writtenPcmBytes;
|
||||
private long writtenEncodedFrames;
|
||||
private int framesPerEncodedSample;
|
||||
private int startMediaTimeState;
|
||||
private long startMediaTimeUs;
|
||||
|
|
@ -299,12 +319,14 @@ public final class AudioTrack {
|
|||
private long latencyUs;
|
||||
private float volume;
|
||||
|
||||
private byte[] temporaryBuffer;
|
||||
private int temporaryBufferOffset;
|
||||
private ByteBuffer currentSourceBuffer;
|
||||
|
||||
private ByteBuffer resampledBuffer;
|
||||
private boolean useResampledBuffer;
|
||||
private AudioProcessor[] audioProcessors;
|
||||
private ByteBuffer[] outputBuffers;
|
||||
private ByteBuffer inputBuffer;
|
||||
private ByteBuffer outputBuffer;
|
||||
private byte[] preV21OutputBuffer;
|
||||
private int preV21OutputBufferOffset;
|
||||
private int drainingAudioProcessorIndex;
|
||||
private boolean handledEndOfStream;
|
||||
|
||||
private boolean playing;
|
||||
private int audioSessionId;
|
||||
|
|
@ -313,11 +335,20 @@ public final class AudioTrack {
|
|||
private long lastFeedElapsedRealtimeMs;
|
||||
|
||||
/**
|
||||
* @param audioCapabilities The current audio capabilities.
|
||||
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
|
||||
* default capabilities (no encoded audio passthrough support) should be assumed.
|
||||
* @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before
|
||||
* output. May be empty.
|
||||
* @param listener Listener for audio track events.
|
||||
*/
|
||||
public AudioTrack(AudioCapabilities audioCapabilities, Listener listener) {
|
||||
public AudioTrack(AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors,
|
||||
Listener listener) {
|
||||
this.audioCapabilities = audioCapabilities;
|
||||
channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
|
||||
availableAudioProcessors = new AudioProcessor[audioProcessors.length + 2];
|
||||
availableAudioProcessors[0] = new ResamplingAudioProcessor();
|
||||
availableAudioProcessors[1] = channelMappingAudioProcessor;
|
||||
System.arraycopy(audioProcessors, 0, availableAudioProcessors, 2, audioProcessors.length);
|
||||
this.listener = listener;
|
||||
releasingConditionVariable = new ConditionVariable(true);
|
||||
if (Util.SDK_INT >= 18) {
|
||||
|
|
@ -340,6 +371,9 @@ public final class AudioTrack {
|
|||
startMediaTimeState = START_NOT_SET;
|
||||
streamType = C.STREAM_TYPE_DEFAULT;
|
||||
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
|
||||
drainingAudioProcessorIndex = C.INDEX_UNSET;
|
||||
this.audioProcessors = new AudioProcessor[0];
|
||||
outputBuffers = new ByteBuffer[0];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -414,9 +448,70 @@ public final class AudioTrack {
|
|||
* {@link C#ENCODING_PCM_32BIT}.
|
||||
* @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a
|
||||
* suitable buffer size automatically.
|
||||
* @throws ConfigurationException If an error occurs configuring the track.
|
||||
*/
|
||||
public void configure(String mimeType, int channelCount, int sampleRate,
|
||||
@C.PcmEncoding int pcmEncoding, int specifiedBufferSize) {
|
||||
@C.PcmEncoding int pcmEncoding, int specifiedBufferSize) throws ConfigurationException {
|
||||
configure(mimeType, channelCount, sampleRate, pcmEncoding, specifiedBufferSize, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures (or reconfigures) the audio track.
|
||||
*
|
||||
* @param mimeType The mime type.
|
||||
* @param channelCount The number of channels.
|
||||
* @param sampleRate The sample rate in Hz.
|
||||
* @param pcmEncoding For PCM formats, the encoding used. One of {@link C#ENCODING_PCM_16BIT},
|
||||
* {@link C#ENCODING_PCM_16BIT}, {@link C#ENCODING_PCM_24BIT} and
|
||||
* {@link C#ENCODING_PCM_32BIT}.
|
||||
* @param specifiedBufferSize A specific size for the playback buffer in bytes, or 0 to infer a
|
||||
* suitable buffer size automatically.
|
||||
* @param outputChannels A mapping from input to output channels that is applied to this track's
|
||||
* input as a preprocessing step, if handling PCM input. Specify {@code null} to leave the
|
||||
* input unchanged. Otherwise, the element at index {@code i} specifies index of the input
|
||||
* channel to map to output channel {@code i} when preprocessing input buffers. After the
|
||||
* map is applied the audio data will have {@code outputChannels.length} channels.
|
||||
* @throws ConfigurationException If an error occurs configuring the track.
|
||||
*/
|
||||
public void configure(String mimeType, int channelCount, int sampleRate,
|
||||
@C.PcmEncoding int pcmEncoding, int specifiedBufferSize, int[] outputChannels)
|
||||
throws ConfigurationException {
|
||||
boolean passthrough = !MimeTypes.AUDIO_RAW.equals(mimeType);
|
||||
@C.Encoding int encoding = passthrough ? getEncodingForMimeType(mimeType) : pcmEncoding;
|
||||
boolean flush = false;
|
||||
if (!passthrough) {
|
||||
pcmFrameSize = Util.getPcmFrameSize(pcmEncoding, channelCount);
|
||||
|
||||
// Reconfigure the audio processors.
|
||||
channelMappingAudioProcessor.setChannelMap(outputChannels);
|
||||
ArrayList<AudioProcessor> newAudioProcessors = new ArrayList<>();
|
||||
for (AudioProcessor audioProcessor : availableAudioProcessors) {
|
||||
try {
|
||||
flush |= audioProcessor.configure(sampleRate, channelCount, encoding);
|
||||
} catch (AudioProcessor.UnhandledFormatException e) {
|
||||
throw new ConfigurationException(e);
|
||||
}
|
||||
if (audioProcessor.isActive()) {
|
||||
newAudioProcessors.add(audioProcessor);
|
||||
channelCount = audioProcessor.getOutputChannelCount();
|
||||
encoding = audioProcessor.getOutputEncoding();
|
||||
} else {
|
||||
audioProcessor.flush();
|
||||
}
|
||||
}
|
||||
|
||||
if (flush) {
|
||||
int count = newAudioProcessors.size();
|
||||
audioProcessors = newAudioProcessors.toArray(new AudioProcessor[count]);
|
||||
outputBuffers = new ByteBuffer[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
AudioProcessor audioProcessor = audioProcessors[i];
|
||||
audioProcessor.flush();
|
||||
outputBuffers[i] = audioProcessor.getOutput();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int channelConfig;
|
||||
switch (channelCount) {
|
||||
case 1:
|
||||
|
|
@ -444,7 +539,7 @@ public final class AudioTrack {
|
|||
channelConfig = C.CHANNEL_OUT_7POINT1_SURROUND;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported channel count: " + channelCount);
|
||||
throw new ConfigurationException("Unsupported channel count: " + channelCount);
|
||||
}
|
||||
|
||||
// Workaround for overly strict channel configuration checks on nVidia Shield.
|
||||
|
|
@ -462,25 +557,13 @@ public final class AudioTrack {
|
|||
}
|
||||
}
|
||||
|
||||
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);
|
||||
} else if (pcmEncoding == C.ENCODING_PCM_8BIT || pcmEncoding == C.ENCODING_PCM_16BIT
|
||||
|| pcmEncoding == C.ENCODING_PCM_24BIT || pcmEncoding == C.ENCODING_PCM_32BIT) {
|
||||
sourceEncoding = pcmEncoding;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported PCM encoding: " + pcmEncoding);
|
||||
}
|
||||
|
||||
if (isInitialized() && this.sourceEncoding == sourceEncoding && this.sampleRate == sampleRate
|
||||
if (!flush && isInitialized() && this.encoding == encoding && this.sampleRate == sampleRate
|
||||
&& this.channelConfig == channelConfig) {
|
||||
// We already have an audio track with the correct sample rate, channel config and encoding.
|
||||
return;
|
||||
|
|
@ -488,38 +571,38 @@ public final class AudioTrack {
|
|||
|
||||
reset();
|
||||
|
||||
this.sourceEncoding = sourceEncoding;
|
||||
this.encoding = encoding;
|
||||
this.passthrough = passthrough;
|
||||
this.sampleRate = sampleRate;
|
||||
this.channelConfig = channelConfig;
|
||||
targetEncoding = passthrough ? sourceEncoding : C.ENCODING_PCM_16BIT;
|
||||
pcmFrameSize = 2 * channelCount; // 2 bytes per 16-bit sample * number of channels.
|
||||
outputEncoding = passthrough ? encoding : C.ENCODING_PCM_16BIT;
|
||||
outputPcmFrameSize = Util.getPcmFrameSize(C.ENCODING_PCM_16BIT, channelCount);
|
||||
|
||||
if (specifiedBufferSize != 0) {
|
||||
bufferSize = specifiedBufferSize;
|
||||
} else if (passthrough) {
|
||||
// TODO: Set the minimum buffer size using getMinBufferSize when it takes the encoding into
|
||||
// account. [Internal: b/25181305]
|
||||
if (targetEncoding == C.ENCODING_AC3 || targetEncoding == C.ENCODING_E_AC3) {
|
||||
if (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3) {
|
||||
// AC-3 allows bitrates up to 640 kbit/s.
|
||||
bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 80 * 1024 / C.MICROS_PER_SECOND);
|
||||
} else /* (targetEncoding == C.ENCODING_DTS || targetEncoding == C.ENCODING_DTS_HD */ {
|
||||
} else /* (outputEncoding == C.ENCODING_DTS || outputEncoding == C.ENCODING_DTS_HD */ {
|
||||
// DTS allows an 'open' bitrate, but we assume the maximum listed value: 1536 kbit/s.
|
||||
bufferSize = (int) (PASSTHROUGH_BUFFER_DURATION_US * 192 * 1024 / C.MICROS_PER_SECOND);
|
||||
}
|
||||
} else {
|
||||
int minBufferSize =
|
||||
android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, targetEncoding);
|
||||
android.media.AudioTrack.getMinBufferSize(sampleRate, channelConfig, outputEncoding);
|
||||
Assertions.checkState(minBufferSize != ERROR_BAD_VALUE);
|
||||
int multipliedBufferSize = minBufferSize * BUFFER_MULTIPLICATION_FACTOR;
|
||||
int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * pcmFrameSize;
|
||||
int minAppBufferSize = (int) durationUsToFrames(MIN_BUFFER_DURATION_US) * outputPcmFrameSize;
|
||||
int maxAppBufferSize = (int) Math.max(minBufferSize,
|
||||
durationUsToFrames(MAX_BUFFER_DURATION_US) * pcmFrameSize);
|
||||
durationUsToFrames(MAX_BUFFER_DURATION_US) * outputPcmFrameSize);
|
||||
bufferSize = multipliedBufferSize < minAppBufferSize ? minAppBufferSize
|
||||
: multipliedBufferSize > maxAppBufferSize ? maxAppBufferSize
|
||||
: multipliedBufferSize;
|
||||
}
|
||||
bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(pcmBytesToFrames(bufferSize));
|
||||
bufferSizeUs = passthrough ? C.TIME_UNSET : framesToDurationUs(bufferSize / outputPcmFrameSize);
|
||||
}
|
||||
|
||||
private void initialize() throws InitializationException {
|
||||
|
|
@ -531,15 +614,15 @@ public final class AudioTrack {
|
|||
releasingConditionVariable.block();
|
||||
|
||||
if (tunneling) {
|
||||
audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, targetEncoding,
|
||||
audioTrack = createHwAvSyncAudioTrackV21(sampleRate, channelConfig, outputEncoding,
|
||||
bufferSize, audioSessionId);
|
||||
} else if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
|
||||
audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
|
||||
targetEncoding, bufferSize, MODE_STREAM);
|
||||
outputEncoding, bufferSize, MODE_STREAM);
|
||||
} else {
|
||||
// Re-attach to the same audio session.
|
||||
audioTrack = new android.media.AudioTrack(streamType, sampleRate, channelConfig,
|
||||
targetEncoding, bufferSize, MODE_STREAM, audioSessionId);
|
||||
outputEncoding, bufferSize, MODE_STREAM, audioSessionId);
|
||||
}
|
||||
checkAudioTrackInitialized();
|
||||
|
||||
|
|
@ -594,25 +677,26 @@ 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.
|
||||
* Attempts to process data from a {@link ByteBuffer}, 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 handled. {@link Listener#onPositionDiscontinuity()} will be called if
|
||||
* {@code presentationTimeUs} is discontinuous with the last buffer handled since the last reset.
|
||||
* <p>
|
||||
* Returns whether the data was written in full. If the data was not written in full then the same
|
||||
* Returns whether the data was handled in full. If the data was not handled 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 Whether the buffer was consumed fully.
|
||||
* @param buffer The buffer containing audio data.
|
||||
* @param presentationTimeUs The presentation timestamp of the buffer in microseconds.
|
||||
* @return Whether the buffer was handled fully.
|
||||
* @throws InitializationException If an error occurs initializing the track.
|
||||
* @throws WriteException If an error occurs writing the audio data.
|
||||
*/
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
public boolean handleBuffer(ByteBuffer buffer, long presentationTimeUs)
|
||||
throws InitializationException, WriteException {
|
||||
Assertions.checkArgument(inputBuffer == null || buffer == inputBuffer);
|
||||
if (!isInitialized()) {
|
||||
initialize();
|
||||
if (playing) {
|
||||
|
|
@ -620,27 +704,12 @@ public final class AudioTrack {
|
|||
}
|
||||
}
|
||||
|
||||
boolean hadData = hasData;
|
||||
hasData = hasPendingData();
|
||||
if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) {
|
||||
long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs;
|
||||
listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs);
|
||||
}
|
||||
boolean result = writeBuffer(buffer, presentationTimeUs);
|
||||
lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime();
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
private boolean writeBuffer(ByteBuffer buffer, long presentationTimeUs) throws WriteException {
|
||||
boolean isNewSourceBuffer = currentSourceBuffer == null;
|
||||
Assertions.checkState(isNewSourceBuffer || currentSourceBuffer == buffer);
|
||||
currentSourceBuffer = buffer;
|
||||
|
||||
if (needsPassthroughWorkarounds()) {
|
||||
// An AC-3 audio track continues to play data written while it is paused. Stop writing so its
|
||||
// buffer empties. See [Internal: b/18899620].
|
||||
if (audioTrack.getPlayState() == PLAYSTATE_PAUSED) {
|
||||
// We force an underrun to pause the track, so don't notify the listener in this case.
|
||||
hasData = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -653,27 +722,25 @@ public final class AudioTrack {
|
|||
}
|
||||
}
|
||||
|
||||
if (isNewSourceBuffer) {
|
||||
// We're seeing this buffer for the first time.
|
||||
boolean hadData = hasData;
|
||||
hasData = hasPendingData();
|
||||
if (hadData && !hasData && audioTrack.getPlayState() != PLAYSTATE_STOPPED) {
|
||||
long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs;
|
||||
listener.onUnderrun(bufferSize, C.usToMs(bufferSizeUs), elapsedSinceLastFeedMs);
|
||||
}
|
||||
|
||||
if (!currentSourceBuffer.hasRemaining()) {
|
||||
if (inputBuffer == null) {
|
||||
// We are seeing this buffer for the first time.
|
||||
if (!buffer.hasRemaining()) {
|
||||
// The buffer is empty.
|
||||
currentSourceBuffer = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
useResampledBuffer = targetEncoding != sourceEncoding;
|
||||
if (useResampledBuffer) {
|
||||
Assertions.checkState(targetEncoding == C.ENCODING_PCM_16BIT);
|
||||
// Resample the buffer to get the data in the target encoding.
|
||||
resampledBuffer = resampleTo16BitPcm(currentSourceBuffer, sourceEncoding, resampledBuffer);
|
||||
buffer = resampledBuffer;
|
||||
}
|
||||
|
||||
if (passthrough && framesPerEncodedSample == 0) {
|
||||
// If this is the first encoded sample, calculate the sample size in frames.
|
||||
framesPerEncodedSample = getFramesPerEncodedSample(targetEncoding, buffer);
|
||||
framesPerEncodedSample = getFramesPerEncodedSample(outputEncoding, buffer);
|
||||
}
|
||||
|
||||
if (startMediaTimeState == START_NOT_SET) {
|
||||
startMediaTimeUs = Math.max(0, presentationTimeUs);
|
||||
startMediaTimeState = START_IN_SYNC;
|
||||
|
|
@ -695,66 +762,172 @@ public final class AudioTrack {
|
|||
listener.onPositionDiscontinuity();
|
||||
}
|
||||
}
|
||||
if (Util.SDK_INT < 21) {
|
||||
// Copy {@code buffer} into {@code temporaryBuffer}.
|
||||
int bytesRemaining = buffer.remaining();
|
||||
if (temporaryBuffer == null || temporaryBuffer.length < bytesRemaining) {
|
||||
temporaryBuffer = new byte[bytesRemaining];
|
||||
}
|
||||
int originalPosition = buffer.position();
|
||||
buffer.get(temporaryBuffer, 0, bytesRemaining);
|
||||
buffer.position(originalPosition);
|
||||
temporaryBufferOffset = 0;
|
||||
|
||||
if (passthrough) {
|
||||
submittedEncodedFrames += framesPerEncodedSample;
|
||||
} else {
|
||||
submittedPcmBytes += buffer.remaining();
|
||||
}
|
||||
|
||||
inputBuffer = buffer;
|
||||
}
|
||||
|
||||
buffer = useResampledBuffer ? resampledBuffer : buffer;
|
||||
if (passthrough) {
|
||||
// Passthrough buffers are not processed.
|
||||
writeBuffer(inputBuffer, presentationTimeUs);
|
||||
} else {
|
||||
processBuffers(presentationTimeUs);
|
||||
}
|
||||
|
||||
if (!inputBuffer.hasRemaining()) {
|
||||
inputBuffer = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void processBuffers(long avSyncPresentationTimeUs) throws WriteException {
|
||||
int count = audioProcessors.length;
|
||||
int index = count;
|
||||
while (index >= 0) {
|
||||
ByteBuffer input = index > 0 ? outputBuffers[index - 1]
|
||||
: (inputBuffer != null ? inputBuffer : AudioProcessor.EMPTY_BUFFER);
|
||||
if (index == count) {
|
||||
writeBuffer(input, avSyncPresentationTimeUs);
|
||||
} else {
|
||||
AudioProcessor audioProcessor = audioProcessors[index];
|
||||
audioProcessor.queueInput(input);
|
||||
ByteBuffer output = audioProcessor.getOutput();
|
||||
outputBuffers[index] = output;
|
||||
if (output.hasRemaining()) {
|
||||
// Handle the output as input to the next audio processor or the AudioTrack.
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (input.hasRemaining()) {
|
||||
// The input wasn't consumed and no output was produced, so give up for now.
|
||||
return;
|
||||
}
|
||||
|
||||
// Get more input from upstream.
|
||||
index--;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
private boolean writeBuffer(ByteBuffer buffer, long avSyncPresentationTimeUs)
|
||||
throws WriteException {
|
||||
if (!buffer.hasRemaining()) {
|
||||
return true;
|
||||
}
|
||||
if (outputBuffer != null) {
|
||||
Assertions.checkArgument(outputBuffer == buffer);
|
||||
} else {
|
||||
outputBuffer = buffer;
|
||||
if (Util.SDK_INT < 21) {
|
||||
int bytesRemaining = buffer.remaining();
|
||||
if (preV21OutputBuffer == null || preV21OutputBuffer.length < bytesRemaining) {
|
||||
preV21OutputBuffer = new byte[bytesRemaining];
|
||||
}
|
||||
int originalPosition = buffer.position();
|
||||
buffer.get(preV21OutputBuffer, 0, bytesRemaining);
|
||||
buffer.position(originalPosition);
|
||||
preV21OutputBufferOffset = 0;
|
||||
}
|
||||
}
|
||||
int bytesRemaining = buffer.remaining();
|
||||
int bytesWritten = 0;
|
||||
if (Util.SDK_INT < 21) { // passthrough == false
|
||||
// Work out how many bytes we can write without the risk of blocking.
|
||||
int bytesPending =
|
||||
(int) (submittedPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * pcmFrameSize));
|
||||
(int) (writtenPcmBytes - (audioTrackUtil.getPlaybackHeadPosition() * outputPcmFrameSize));
|
||||
int bytesToWrite = bufferSize - bytesPending;
|
||||
if (bytesToWrite > 0) {
|
||||
bytesToWrite = Math.min(bytesRemaining, bytesToWrite);
|
||||
bytesWritten = audioTrack.write(temporaryBuffer, temporaryBufferOffset, bytesToWrite);
|
||||
if (bytesWritten >= 0) {
|
||||
temporaryBufferOffset += bytesWritten;
|
||||
bytesWritten = audioTrack.write(preV21OutputBuffer, preV21OutputBufferOffset, bytesToWrite);
|
||||
if (bytesWritten > 0) {
|
||||
preV21OutputBufferOffset += bytesWritten;
|
||||
buffer.position(buffer.position() + bytesWritten);
|
||||
}
|
||||
buffer.position(buffer.position() + bytesWritten);
|
||||
}
|
||||
} else if (tunneling) {
|
||||
Assertions.checkState(avSyncPresentationTimeUs != C.TIME_UNSET);
|
||||
bytesWritten = writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining,
|
||||
avSyncPresentationTimeUs);
|
||||
} else {
|
||||
bytesWritten = tunneling
|
||||
? writeNonBlockingWithAvSyncV21(audioTrack, buffer, bytesRemaining, presentationTimeUs)
|
||||
: writeNonBlockingV21(audioTrack, buffer, bytesRemaining);
|
||||
bytesWritten = writeNonBlockingV21(audioTrack, buffer, bytesRemaining);
|
||||
}
|
||||
|
||||
lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime();
|
||||
|
||||
if (bytesWritten < 0) {
|
||||
throw new WriteException(bytesWritten);
|
||||
}
|
||||
|
||||
if (!passthrough) {
|
||||
submittedPcmBytes += bytesWritten;
|
||||
writtenPcmBytes += bytesWritten;
|
||||
}
|
||||
if (bytesWritten == bytesRemaining) {
|
||||
if (passthrough) {
|
||||
submittedEncodedFrames += framesPerEncodedSample;
|
||||
writtenEncodedFrames += framesPerEncodedSample;
|
||||
}
|
||||
currentSourceBuffer = null;
|
||||
outputBuffer = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the last data passed to {@link #handleBuffer(ByteBuffer, long)} is played in full.
|
||||
* Plays out remaining audio. {@link #isEnded()} will return {@code true} when playback has ended.
|
||||
*
|
||||
* @throws WriteException If an error occurs draining data to the track.
|
||||
*/
|
||||
public void handleEndOfStream() {
|
||||
if (isInitialized()) {
|
||||
audioTrackUtil.handleEndOfStream(getSubmittedFrames());
|
||||
bytesUntilNextAvSync = 0;
|
||||
public void playToEndOfStream() throws WriteException {
|
||||
if (handledEndOfStream || !isInitialized()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Drain the audio processors.
|
||||
boolean audioProcessorNeedsEndOfStream = false;
|
||||
if (drainingAudioProcessorIndex == C.INDEX_UNSET) {
|
||||
drainingAudioProcessorIndex = passthrough ? audioProcessors.length : 0;
|
||||
audioProcessorNeedsEndOfStream = true;
|
||||
}
|
||||
while (drainingAudioProcessorIndex < audioProcessors.length) {
|
||||
AudioProcessor audioProcessor = audioProcessors[drainingAudioProcessorIndex];
|
||||
if (audioProcessorNeedsEndOfStream) {
|
||||
audioProcessor.queueEndOfStream();
|
||||
}
|
||||
processBuffers(C.TIME_UNSET);
|
||||
if (!audioProcessor.isEnded()) {
|
||||
return;
|
||||
}
|
||||
audioProcessorNeedsEndOfStream = true;
|
||||
drainingAudioProcessorIndex++;
|
||||
}
|
||||
|
||||
// Finish writing any remaining output to the track.
|
||||
if (outputBuffer != null) {
|
||||
writeBuffer(outputBuffer, C.TIME_UNSET);
|
||||
if (outputBuffer != null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Drain the track.
|
||||
audioTrackUtil.handleEndOfStream(getWrittenFrames());
|
||||
bytesUntilNextAvSync = 0;
|
||||
handledEndOfStream = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether all buffers passed to {@link #handleBuffer(ByteBuffer, long)} have been
|
||||
* completely processed and played.
|
||||
*/
|
||||
public boolean isEnded() {
|
||||
return !isInitialized() || (handledEndOfStream && !hasPendingData());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -762,7 +935,7 @@ public final class AudioTrack {
|
|||
*/
|
||||
public boolean hasPendingData() {
|
||||
return isInitialized()
|
||||
&& (getSubmittedFrames() > audioTrackUtil.getPlaybackHeadPosition()
|
||||
&& (getWrittenFrames() > audioTrackUtil.getPlaybackHeadPosition()
|
||||
|| overrideHasPendingData());
|
||||
}
|
||||
|
||||
|
|
@ -815,9 +988,14 @@ public final class AudioTrack {
|
|||
/**
|
||||
* 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.
|
||||
* <p>
|
||||
* If this instance has {@link AudioProcessor}s and tunneling is enabled, care must be taken that
|
||||
* audio processors do not output buffers with a different duration than their input, and buffer
|
||||
* processors must produce output corresponding to their last input immediately after that input
|
||||
* is queued.
|
||||
*
|
||||
* @param tunnelingAudioSessionId The audio session id to use.
|
||||
* @throws IllegalStateException Thrown if enabling tunneling on platform API version < 21.
|
||||
* @throws IllegalStateException Thrown if enabling tunneling on platform API version < 21.
|
||||
*/
|
||||
public void enableTunnelingV21(int tunnelingAudioSessionId) {
|
||||
Assertions.checkState(Util.SDK_INT >= 21);
|
||||
|
|
@ -884,8 +1062,18 @@ public final class AudioTrack {
|
|||
if (isInitialized()) {
|
||||
submittedPcmBytes = 0;
|
||||
submittedEncodedFrames = 0;
|
||||
writtenPcmBytes = 0;
|
||||
writtenEncodedFrames = 0;
|
||||
framesPerEncodedSample = 0;
|
||||
currentSourceBuffer = null;
|
||||
inputBuffer = null;
|
||||
outputBuffer = null;
|
||||
for (int i = 0; i < audioProcessors.length; i++) {
|
||||
AudioProcessor audioProcessor = audioProcessors[i];
|
||||
audioProcessor.flush();
|
||||
outputBuffers[i] = audioProcessor.getOutput();
|
||||
}
|
||||
handledEndOfStream = false;
|
||||
drainingAudioProcessorIndex = C.INDEX_UNSET;
|
||||
avSyncHeader = null;
|
||||
bytesUntilNextAvSync = 0;
|
||||
startMediaTimeState = START_NOT_SET;
|
||||
|
|
@ -920,6 +1108,9 @@ public final class AudioTrack {
|
|||
public void release() {
|
||||
reset();
|
||||
releaseKeepSessionIdAudioTrack();
|
||||
for (AudioProcessor audioProcessor : availableAudioProcessors) {
|
||||
audioProcessor.release();
|
||||
}
|
||||
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
|
||||
playing = false;
|
||||
}
|
||||
|
|
@ -1063,10 +1254,6 @@ public final class AudioTrack {
|
|||
return audioTrack != null;
|
||||
}
|
||||
|
||||
private long pcmBytesToFrames(long byteCount) {
|
||||
return byteCount / pcmFrameSize;
|
||||
}
|
||||
|
||||
private long framesToDurationUs(long frameCount) {
|
||||
return (frameCount * C.MICROS_PER_SECOND) / sampleRate;
|
||||
}
|
||||
|
|
@ -1076,7 +1263,11 @@ public final class AudioTrack {
|
|||
}
|
||||
|
||||
private long getSubmittedFrames() {
|
||||
return passthrough ? submittedEncodedFrames : pcmBytesToFrames(submittedPcmBytes);
|
||||
return passthrough ? submittedEncodedFrames : (submittedPcmBytes / pcmFrameSize);
|
||||
}
|
||||
|
||||
private long getWrittenFrames() {
|
||||
return passthrough ? writtenEncodedFrames : (writtenPcmBytes / outputPcmFrameSize);
|
||||
}
|
||||
|
||||
private void resetSyncParams() {
|
||||
|
|
@ -1094,7 +1285,7 @@ public final class AudioTrack {
|
|||
*/
|
||||
private boolean needsPassthroughWorkarounds() {
|
||||
return Util.SDK_INT < 23
|
||||
&& (targetEncoding == C.ENCODING_AC3 || targetEncoding == C.ENCODING_E_AC3);
|
||||
&& (outputEncoding == C.ENCODING_AC3 || outputEncoding == C.ENCODING_E_AC3);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1129,82 +1320,6 @@ public final class AudioTrack {
|
|||
sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the provided buffer into 16-bit PCM.
|
||||
*
|
||||
* @param buffer The buffer containing the data to convert.
|
||||
* @param sourceEncoding The data encoding.
|
||||
* @param out A buffer into which the output should be written, if its capacity is sufficient.
|
||||
* @return The 16-bit PCM output. Different to the out parameter if null was passed, or if the
|
||||
* capacity was insufficient for the output.
|
||||
*/
|
||||
private static ByteBuffer resampleTo16BitPcm(ByteBuffer buffer, @C.PcmEncoding int sourceEncoding,
|
||||
ByteBuffer out) {
|
||||
int offset = buffer.position();
|
||||
int limit = buffer.limit();
|
||||
int size = limit - offset;
|
||||
|
||||
int resampledSize;
|
||||
switch (sourceEncoding) {
|
||||
case C.ENCODING_PCM_8BIT:
|
||||
resampledSize = size * 2;
|
||||
break;
|
||||
case C.ENCODING_PCM_24BIT:
|
||||
resampledSize = (size / 3) * 2;
|
||||
break;
|
||||
case C.ENCODING_PCM_32BIT:
|
||||
resampledSize = size / 2;
|
||||
break;
|
||||
case C.ENCODING_PCM_16BIT:
|
||||
case C.ENCODING_INVALID:
|
||||
case Format.NO_VALUE:
|
||||
default:
|
||||
// Never happens.
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
ByteBuffer resampledBuffer = out;
|
||||
if (resampledBuffer == null || resampledBuffer.capacity() < resampledSize) {
|
||||
resampledBuffer = ByteBuffer.allocateDirect(resampledSize);
|
||||
}
|
||||
resampledBuffer.position(0);
|
||||
resampledBuffer.limit(resampledSize);
|
||||
|
||||
// Samples are little endian.
|
||||
switch (sourceEncoding) {
|
||||
case C.ENCODING_PCM_8BIT:
|
||||
// 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up.
|
||||
for (int i = offset; i < limit; i++) {
|
||||
resampledBuffer.put((byte) 0);
|
||||
resampledBuffer.put((byte) ((buffer.get(i) & 0xFF) - 128));
|
||||
}
|
||||
break;
|
||||
case C.ENCODING_PCM_24BIT:
|
||||
// 24->16 bit resampling. Drop the least significant byte.
|
||||
for (int i = offset; i < limit; i += 3) {
|
||||
resampledBuffer.put(buffer.get(i + 1));
|
||||
resampledBuffer.put(buffer.get(i + 2));
|
||||
}
|
||||
break;
|
||||
case C.ENCODING_PCM_32BIT:
|
||||
// 32->16 bit resampling. Drop the two least significant bytes.
|
||||
for (int i = offset; i < limit; i += 4) {
|
||||
resampledBuffer.put(buffer.get(i + 2));
|
||||
resampledBuffer.put(buffer.get(i + 3));
|
||||
}
|
||||
break;
|
||||
case C.ENCODING_PCM_16BIT:
|
||||
case C.ENCODING_INVALID:
|
||||
case Format.NO_VALUE:
|
||||
default:
|
||||
// Never happens.
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
resampledBuffer.position(0);
|
||||
return resampledBuffer;
|
||||
}
|
||||
|
||||
@C.Encoding
|
||||
private static int getEncodingForMimeType(String mimeType) {
|
||||
switch (mimeType) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* 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.audio;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.C.Encoding;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* An {@link AudioProcessor} that applies a mapping from input channels onto specified output
|
||||
* channels. This can be used to reorder, duplicate or discard channels.
|
||||
*/
|
||||
/* package */ final class ChannelMappingAudioProcessor implements AudioProcessor {
|
||||
|
||||
private int channelCount;
|
||||
private int sampleRateHz;
|
||||
private int[] pendingOutputChannels;
|
||||
|
||||
private boolean active;
|
||||
private int[] outputChannels;
|
||||
private ByteBuffer buffer;
|
||||
private ByteBuffer outputBuffer;
|
||||
private boolean inputEnded;
|
||||
|
||||
/**
|
||||
* Creates a new processor that applies a channel mapping.
|
||||
*/
|
||||
public ChannelMappingAudioProcessor() {
|
||||
buffer = EMPTY_BUFFER;
|
||||
outputBuffer = EMPTY_BUFFER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the channel mapping. After calling this method, call {@link #configure(int, int, int)}
|
||||
* to start using the new channel map.
|
||||
*
|
||||
* @see AudioTrack#configure(String, int, int, int, int, int[])
|
||||
*/
|
||||
public void setChannelMap(int[] outputChannels) {
|
||||
pendingOutputChannels = outputChannels;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configure(int sampleRateHz, int channelCount, @Encoding int encoding)
|
||||
throws UnhandledFormatException {
|
||||
boolean outputChannelsChanged = !Arrays.equals(pendingOutputChannels, outputChannels);
|
||||
outputChannels = pendingOutputChannels;
|
||||
if (outputChannels == null) {
|
||||
active = false;
|
||||
return outputChannelsChanged;
|
||||
}
|
||||
if (encoding != C.ENCODING_PCM_16BIT) {
|
||||
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
|
||||
}
|
||||
if (!outputChannelsChanged && this.sampleRateHz == sampleRateHz
|
||||
&& this.channelCount == channelCount) {
|
||||
return false;
|
||||
}
|
||||
this.sampleRateHz = sampleRateHz;
|
||||
this.channelCount = channelCount;
|
||||
|
||||
active = channelCount != outputChannels.length;
|
||||
for (int i = 0; i < outputChannels.length; i++) {
|
||||
int channelIndex = outputChannels[i];
|
||||
if (channelIndex >= channelCount) {
|
||||
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
|
||||
}
|
||||
active |= (channelIndex != i);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return active;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOutputChannelCount() {
|
||||
return outputChannels == null ? channelCount : outputChannels.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOutputEncoding() {
|
||||
return C.ENCODING_PCM_16BIT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueInput(ByteBuffer inputBuffer) {
|
||||
int position = inputBuffer.position();
|
||||
int limit = inputBuffer.limit();
|
||||
int frameCount = (limit - position) / (2 * channelCount);
|
||||
int outputSize = frameCount * outputChannels.length * 2;
|
||||
if (buffer.capacity() < outputSize) {
|
||||
buffer = ByteBuffer.allocateDirect(outputSize).order(ByteOrder.nativeOrder());
|
||||
} else {
|
||||
buffer.clear();
|
||||
}
|
||||
while (position < limit) {
|
||||
for (int channelIndex : outputChannels) {
|
||||
buffer.putShort(inputBuffer.getShort(position + 2 * channelIndex));
|
||||
}
|
||||
position += channelCount * 2;
|
||||
}
|
||||
inputBuffer.position(limit);
|
||||
buffer.flip();
|
||||
outputBuffer = buffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueEndOfStream() {
|
||||
inputEnded = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer getOutput() {
|
||||
ByteBuffer outputBuffer = this.outputBuffer;
|
||||
this.outputBuffer = EMPTY_BUFFER;
|
||||
return outputBuffer;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
@Override
|
||||
public boolean isEnded() {
|
||||
return inputEnded && outputBuffer == EMPTY_BUFFER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
outputBuffer = EMPTY_BUFFER;
|
||||
inputEnded = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
flush();
|
||||
buffer = EMPTY_BUFFER;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -47,8 +47,10 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
private final AudioTrack audioTrack;
|
||||
|
||||
private boolean passthroughEnabled;
|
||||
private boolean codecNeedsDiscardChannelsWorkaround;
|
||||
private android.media.MediaFormat passthroughMediaFormat;
|
||||
private int pcmEncoding;
|
||||
private int channelCount;
|
||||
private long currentPositionUs;
|
||||
private boolean allowPositionDiscontinuity;
|
||||
|
||||
|
|
@ -121,13 +123,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @param audioCapabilities The audio capabilities for playback on this device. May be null if the
|
||||
* default capabilities (no encoded audio passthrough support) should be assumed.
|
||||
* @param audioProcessors Optional {@link AudioProcessor}s that will process PCM audio before
|
||||
* output.
|
||||
*/
|
||||
public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector,
|
||||
DrmSessionManager<FrameworkMediaCrypto> drmSessionManager,
|
||||
boolean playClearSamplesWithoutKeys, Handler eventHandler,
|
||||
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities) {
|
||||
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities,
|
||||
AudioProcessor... audioProcessors) {
|
||||
super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys);
|
||||
audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener());
|
||||
audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener());
|
||||
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
||||
}
|
||||
|
||||
|
|
@ -185,6 +190,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
@Override
|
||||
protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format,
|
||||
MediaCrypto crypto) {
|
||||
codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name);
|
||||
if (passthroughEnabled) {
|
||||
// Override the MIME type used to configure the codec if we are using a passthrough decoder.
|
||||
passthroughMediaFormat = format.getFrameworkMediaFormatV16();
|
||||
|
|
@ -216,17 +222,33 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
// output 16-bit PCM.
|
||||
pcmEncoding = MimeTypes.AUDIO_RAW.equals(newFormat.sampleMimeType) ? newFormat.pcmEncoding
|
||||
: C.ENCODING_PCM_16BIT;
|
||||
channelCount = newFormat.channelCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat) {
|
||||
protected void onOutputFormatChanged(MediaCodec codec, MediaFormat outputFormat)
|
||||
throws ExoPlaybackException {
|
||||
boolean passthrough = passthroughMediaFormat != null;
|
||||
String mimeType = passthrough ? passthroughMediaFormat.getString(MediaFormat.KEY_MIME)
|
||||
: MimeTypes.AUDIO_RAW;
|
||||
MediaFormat format = passthrough ? passthroughMediaFormat : outputFormat;
|
||||
int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
|
||||
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
|
||||
audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0);
|
||||
int[] channelMap;
|
||||
if (codecNeedsDiscardChannelsWorkaround && channelCount == 6 && this.channelCount < 6) {
|
||||
channelMap = new int[this.channelCount];
|
||||
for (int i = 0; i < this.channelCount; i++) {
|
||||
channelMap[i] = i;
|
||||
}
|
||||
} else {
|
||||
channelMap = null;
|
||||
}
|
||||
|
||||
try {
|
||||
audioTrack.configure(mimeType, channelCount, sampleRate, pcmEncoding, 0, channelMap);
|
||||
} catch (AudioTrack.ConfigurationException e) {
|
||||
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -304,7 +326,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
|
||||
@Override
|
||||
public boolean isEnded() {
|
||||
return super.isEnded() && !audioTrack.hasPendingData();
|
||||
return super.isEnded() && audioTrack.isEnded();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -353,8 +375,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void onOutputStreamEnded() {
|
||||
audioTrack.handleEndOfStream();
|
||||
protected void renderToEndOfStream() throws ExoPlaybackException {
|
||||
try {
|
||||
audioTrack.playToEndOfStream();
|
||||
} catch (AudioTrack.WriteException e) {
|
||||
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -376,6 +402,20 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the decoder is known to output six audio channels when provided with input with
|
||||
* fewer than six channels.
|
||||
* <p>
|
||||
* See [Internal: b/35655036].
|
||||
*/
|
||||
private static boolean codecNeedsDiscardChannelsWorkaround(String codecName) {
|
||||
// The workaround applies to Samsung Galaxy S6 and Samsung Galaxy S7.
|
||||
return Util.SDK_INT < 24 && "OMX.SEC.aac.dec".equals(codecName)
|
||||
&& "samsung".equals(Util.MANUFACTURER)
|
||||
&& (Util.DEVICE.startsWith("zeroflte") || Util.DEVICE.startsWith("herolte")
|
||||
|| Util.DEVICE.startsWith("heroqlte"));
|
||||
}
|
||||
|
||||
private final class AudioTrackListener implements AudioTrack.Listener {
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
* 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.audio;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* An {@link AudioProcessor} that converts audio data to {@link C#ENCODING_PCM_16BIT}.
|
||||
*/
|
||||
/* package */ final class ResamplingAudioProcessor implements AudioProcessor {
|
||||
|
||||
private int sampleRateHz;
|
||||
private int channelCount;
|
||||
@C.PcmEncoding
|
||||
private int encoding;
|
||||
private ByteBuffer buffer;
|
||||
private ByteBuffer outputBuffer;
|
||||
private boolean inputEnded;
|
||||
|
||||
/**
|
||||
* Creates a new audio processor that converts audio data to {@link C#ENCODING_PCM_16BIT}.
|
||||
*/
|
||||
public ResamplingAudioProcessor() {
|
||||
sampleRateHz = Format.NO_VALUE;
|
||||
channelCount = Format.NO_VALUE;
|
||||
encoding = C.ENCODING_INVALID;
|
||||
buffer = EMPTY_BUFFER;
|
||||
outputBuffer = EMPTY_BUFFER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configure(int sampleRateHz, int channelCount, @C.Encoding int encoding)
|
||||
throws UnhandledFormatException {
|
||||
if (encoding != C.ENCODING_PCM_8BIT && encoding != C.ENCODING_PCM_16BIT
|
||||
&& encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_32BIT) {
|
||||
throw new UnhandledFormatException(sampleRateHz, channelCount, encoding);
|
||||
}
|
||||
if (this.sampleRateHz == sampleRateHz && this.channelCount == channelCount
|
||||
&& this.encoding == encoding) {
|
||||
return false;
|
||||
}
|
||||
this.sampleRateHz = sampleRateHz;
|
||||
this.channelCount = channelCount;
|
||||
this.encoding = encoding;
|
||||
if (encoding == C.ENCODING_PCM_16BIT) {
|
||||
buffer = EMPTY_BUFFER;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return encoding != C.ENCODING_INVALID && encoding != C.ENCODING_PCM_16BIT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOutputChannelCount() {
|
||||
return channelCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOutputEncoding() {
|
||||
return C.ENCODING_PCM_16BIT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueInput(ByteBuffer inputBuffer) {
|
||||
// Prepare the output buffer.
|
||||
int position = inputBuffer.position();
|
||||
int limit = inputBuffer.limit();
|
||||
int size = limit - position;
|
||||
int resampledSize;
|
||||
switch (encoding) {
|
||||
case C.ENCODING_PCM_8BIT:
|
||||
resampledSize = size * 2;
|
||||
break;
|
||||
case C.ENCODING_PCM_24BIT:
|
||||
resampledSize = (size / 3) * 2;
|
||||
break;
|
||||
case C.ENCODING_PCM_32BIT:
|
||||
resampledSize = size / 2;
|
||||
break;
|
||||
case C.ENCODING_PCM_16BIT:
|
||||
case C.ENCODING_INVALID:
|
||||
case Format.NO_VALUE:
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
if (buffer.capacity() < resampledSize) {
|
||||
buffer = ByteBuffer.allocateDirect(resampledSize).order(ByteOrder.nativeOrder());
|
||||
} else {
|
||||
buffer.clear();
|
||||
}
|
||||
|
||||
// Resample the little endian input and update the input/output buffers.
|
||||
switch (encoding) {
|
||||
case C.ENCODING_PCM_8BIT:
|
||||
// 8->16 bit resampling. Shift each byte from [0, 256) to [-128, 128) and scale up.
|
||||
for (int i = position; i < limit; i++) {
|
||||
buffer.put((byte) 0);
|
||||
buffer.put((byte) ((inputBuffer.get(i) & 0xFF) - 128));
|
||||
}
|
||||
break;
|
||||
case C.ENCODING_PCM_24BIT:
|
||||
// 24->16 bit resampling. Drop the least significant byte.
|
||||
for (int i = position; i < limit; i += 3) {
|
||||
buffer.put(inputBuffer.get(i + 1));
|
||||
buffer.put(inputBuffer.get(i + 2));
|
||||
}
|
||||
break;
|
||||
case C.ENCODING_PCM_32BIT:
|
||||
// 32->16 bit resampling. Drop the two least significant bytes.
|
||||
for (int i = position; i < limit; i += 4) {
|
||||
buffer.put(inputBuffer.get(i + 2));
|
||||
buffer.put(inputBuffer.get(i + 3));
|
||||
}
|
||||
break;
|
||||
case C.ENCODING_PCM_16BIT:
|
||||
case C.ENCODING_INVALID:
|
||||
case Format.NO_VALUE:
|
||||
default:
|
||||
// Never happens.
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
inputBuffer.position(inputBuffer.limit());
|
||||
buffer.flip();
|
||||
outputBuffer = buffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void queueEndOfStream() {
|
||||
inputEnded = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer getOutput() {
|
||||
ByteBuffer outputBuffer = this.outputBuffer;
|
||||
this.outputBuffer = EMPTY_BUFFER;
|
||||
return outputBuffer;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
@Override
|
||||
public boolean isEnded() {
|
||||
return inputEnded && outputBuffer == EMPTY_BUFFER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
outputBuffer = EMPTY_BUFFER;
|
||||
inputEnded = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
flush();
|
||||
buffer = EMPTY_BUFFER;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@ import com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
|
|||
import com.google.android.exoplayer2.drm.DrmSession;
|
||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||
import com.google.android.exoplayer2.drm.ExoMediaCrypto;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.MediaClock;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.TraceUtil;
|
||||
|
|
@ -67,12 +68,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
*/
|
||||
private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2;
|
||||
|
||||
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
|
||||
private final boolean playClearSamplesWithoutKeys;
|
||||
|
||||
private final EventDispatcher eventDispatcher;
|
||||
private final AudioTrack audioTrack;
|
||||
private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
|
||||
private final FormatHolder formatHolder;
|
||||
private final DecoderInputBuffer flagsOnlyBuffer;
|
||||
|
||||
private DecoderCounters decoderCounters;
|
||||
private Format inputFormat;
|
||||
|
|
@ -83,8 +84,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
private DrmSession<ExoMediaCrypto> drmSession;
|
||||
private DrmSession<ExoMediaCrypto> pendingDrmSession;
|
||||
|
||||
@ReinitializationState
|
||||
private int decoderReinitializationState;
|
||||
@ReinitializationState private int decoderReinitializationState;
|
||||
private boolean decoderReceivedBuffers;
|
||||
private boolean audioTrackNeedsConfigure;
|
||||
|
||||
|
|
@ -102,10 +102,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
||||
* null if delivery of events is not required.
|
||||
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
||||
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
|
||||
*/
|
||||
public SimpleDecoderAudioRenderer(Handler eventHandler,
|
||||
AudioRendererEventListener eventListener) {
|
||||
this(eventHandler, eventListener, null);
|
||||
AudioRendererEventListener eventListener, AudioProcessor... audioProcessors) {
|
||||
this(eventHandler, eventListener, null, null, false, audioProcessors);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -133,16 +134,19 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
* begin in parallel with key acquisition. This parameter specifies whether the renderer is
|
||||
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
|
||||
* has obtained the keys necessary to decrypt encrypted regions of the media.
|
||||
* @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
|
||||
*/
|
||||
public SimpleDecoderAudioRenderer(Handler eventHandler,
|
||||
AudioRendererEventListener eventListener, AudioCapabilities audioCapabilities,
|
||||
DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys) {
|
||||
DrmSessionManager<ExoMediaCrypto> drmSessionManager, boolean playClearSamplesWithoutKeys,
|
||||
AudioProcessor... audioProcessors) {
|
||||
super(C.TRACK_TYPE_AUDIO);
|
||||
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
||||
audioTrack = new AudioTrack(audioCapabilities, new AudioTrackListener());
|
||||
this.drmSessionManager = drmSessionManager;
|
||||
formatHolder = new FormatHolder();
|
||||
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
|
||||
eventDispatcher = new EventDispatcher(eventHandler, eventListener);
|
||||
audioTrack = new AudioTrack(audioCapabilities, audioProcessors, new AudioTrackListener());
|
||||
formatHolder = new FormatHolder();
|
||||
flagsOnlyBuffer = new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
|
||||
decoderReinitializationState = REINITIALIZATION_STATE_NONE;
|
||||
audioTrackNeedsConfigure = true;
|
||||
}
|
||||
|
|
@ -174,13 +178,31 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
@Override
|
||||
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
|
||||
if (outputStreamEnded) {
|
||||
try {
|
||||
audioTrack.playToEndOfStream();
|
||||
} catch (AudioTrack.WriteException e) {
|
||||
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Try and read a format if we don't have one already.
|
||||
if (inputFormat == null && !readFormat()) {
|
||||
// We can't make progress without one.
|
||||
return;
|
||||
if (inputFormat == null) {
|
||||
// We don't have a format yet, so try and read one.
|
||||
flagsOnlyBuffer.clear();
|
||||
int result = readSource(formatHolder, flagsOnlyBuffer, true);
|
||||
if (result == C.RESULT_FORMAT_READ) {
|
||||
onInputFormatChanged(formatHolder.format);
|
||||
} else if (result == C.RESULT_BUFFER_READ) {
|
||||
// End of stream read having not read a format.
|
||||
Assertions.checkState(flagsOnlyBuffer.isEndOfStream());
|
||||
inputStreamEnded = true;
|
||||
processEndOfStream();
|
||||
return;
|
||||
} else {
|
||||
// We still don't have a format and can't make progress without one.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have a decoder yet, we need to instantiate one.
|
||||
|
|
@ -193,8 +215,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
while (drainOutputBuffer()) {}
|
||||
while (feedInputBuffer()) {}
|
||||
TraceUtil.endSection();
|
||||
} catch (AudioTrack.InitializationException | AudioTrack.WriteException
|
||||
| AudioDecoderException e) {
|
||||
} catch (AudioDecoderException | AudioTrack.ConfigurationException
|
||||
| AudioTrack.InitializationException | AudioTrack.WriteException e) {
|
||||
throw ExoPlaybackException.createForRenderer(e, getIndex());
|
||||
}
|
||||
decoderCounters.ensureUpdated();
|
||||
|
|
@ -255,7 +277,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
}
|
||||
|
||||
private boolean drainOutputBuffer() throws ExoPlaybackException, AudioDecoderException,
|
||||
AudioTrack.InitializationException, AudioTrack.WriteException {
|
||||
AudioTrack.ConfigurationException, AudioTrack.InitializationException,
|
||||
AudioTrack.WriteException {
|
||||
if (outputBuffer == null) {
|
||||
outputBuffer = decoder.dequeueOutputBuffer();
|
||||
if (outputBuffer == null) {
|
||||
|
|
@ -274,8 +297,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
} else {
|
||||
outputBuffer.release();
|
||||
outputBuffer = null;
|
||||
outputStreamEnded = true;
|
||||
audioTrack.handleEndOfStream();
|
||||
processEndOfStream();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -324,7 +346,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
// We've already read an encrypted sample into buffer, and are waiting for keys.
|
||||
result = C.RESULT_BUFFER_READ;
|
||||
} else {
|
||||
result = readSource(formatHolder, inputBuffer);
|
||||
result = readSource(formatHolder, inputBuffer, false);
|
||||
}
|
||||
|
||||
if (result == C.RESULT_NOTHING_READ) {
|
||||
|
|
@ -365,6 +387,15 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
&& (bufferEncrypted || !playClearSamplesWithoutKeys);
|
||||
}
|
||||
|
||||
private void processEndOfStream() throws ExoPlaybackException {
|
||||
outputStreamEnded = true;
|
||||
try {
|
||||
audioTrack.playToEndOfStream();
|
||||
} catch (AudioTrack.WriteException e) {
|
||||
throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex());
|
||||
}
|
||||
}
|
||||
|
||||
private void flushDecoder() throws ExoPlaybackException {
|
||||
waitingForKeys = false;
|
||||
if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) {
|
||||
|
|
@ -383,7 +414,7 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
|
||||
@Override
|
||||
public boolean isEnded() {
|
||||
return outputStreamEnded && !audioTrack.hasPendingData();
|
||||
return outputStreamEnded && audioTrack.isEnded();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -513,15 +544,6 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements
|
|||
decoderReceivedBuffers = false;
|
||||
}
|
||||
|
||||
private boolean readFormat() throws ExoPlaybackException {
|
||||
int result = readSource(formatHolder, null);
|
||||
if (result == C.RESULT_FORMAT_READ) {
|
||||
onInputFormatChanged(formatHolder.format);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException {
|
||||
Format oldFormat = inputFormat;
|
||||
inputFormat = newFormat;
|
||||
|
|
|
|||
|
|
@ -61,8 +61,7 @@ public class DecoderInputBuffer extends Buffer {
|
|||
*/
|
||||
public long timeUs;
|
||||
|
||||
@BufferReplacementMode
|
||||
private final int bufferReplacementMode;
|
||||
@BufferReplacementMode private final int bufferReplacementMode;
|
||||
|
||||
/**
|
||||
* @param bufferReplacementMode Determines the behavior of {@link #ensureSpaceForWrite(int)}. One
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener;
|
|||
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.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
|
@ -103,6 +104,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
|
|||
public static final int MODE_RELEASE = 3;
|
||||
|
||||
private static final String TAG = "OfflineDrmSessionMngr";
|
||||
private static final String CENC_SCHEME_MIME_TYPE = "cenc";
|
||||
|
||||
private static final int MSG_PROVISION = 0;
|
||||
private static final int MSG_KEYS = 1;
|
||||
|
|
@ -280,6 +282,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
|
|||
* required.
|
||||
*
|
||||
* <p>{@code mode} must be one of these:
|
||||
* <ul>
|
||||
* <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
|
||||
|
|
@ -288,6 +291,7 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
|
|||
* requested otherwise the offline license is renewed.
|
||||
* <li>{@link #MODE_RELEASE}: {@code offlineLicenseKeySetId} can not be null. The offline license
|
||||
* is released.
|
||||
* </ul>
|
||||
*
|
||||
* @param mode The mode to be set.
|
||||
* @param offlineLicenseKeySetId The key set id of the license to be used with the given mode.
|
||||
|
|
@ -337,6 +341,12 @@ public class DefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSe
|
|||
schemeInitData = psshData;
|
||||
}
|
||||
}
|
||||
if (Util.SDK_INT < 26 && C.CLEARKEY_UUID.equals(uuid)
|
||||
&& (MimeTypes.VIDEO_MP4.equals(schemeMimeType)
|
||||
|| MimeTypes.AUDIO_MP4.equals(schemeMimeType))) {
|
||||
// Prior to API level 26 the ClearKey CDM only accepted "cenc" as the scheme for MP4.
|
||||
schemeMimeType = CENC_SCHEME_MIME_TYPE;
|
||||
}
|
||||
}
|
||||
state = STATE_OPENING;
|
||||
openInternal(true);
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ public interface DrmSession<T extends ExoMediaCrypto> {
|
|||
/** Wraps the exception which is the cause of the error state. */
|
||||
class DrmSessionException extends Exception {
|
||||
|
||||
DrmSessionException(Exception e) {
|
||||
public DrmSessionException(Exception e) {
|
||||
super(e);
|
||||
}
|
||||
|
||||
|
|
@ -70,8 +70,7 @@ public interface DrmSession<T extends ExoMediaCrypto> {
|
|||
* @return One of {@link #STATE_ERROR}, {@link #STATE_CLOSED}, {@link #STATE_OPENING},
|
||||
* {@link #STATE_OPENED} and {@link #STATE_OPENED_WITH_KEYS}.
|
||||
*/
|
||||
@State
|
||||
int getState();
|
||||
@State int getState();
|
||||
|
||||
/**
|
||||
* Returns a {@link ExoMediaCrypto} for the open session.
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import com.google.android.exoplayer2.drm.ExoMediaDrm.ProvisionRequest;
|
|||
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.Util;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
|
|
@ -57,21 +59,62 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
|
|||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link HttpMediaDrmCallback#HttpMediaDrmCallback(String, Factory)}. Request
|
||||
* properties can be set by calling {@link #setKeyRequestProperty(String, String)}.
|
||||
* @param defaultUrl The default license URL.
|
||||
* @param dataSourceFactory A factory from which to obtain {@link HttpDataSource} instances.
|
||||
* @param keyRequestProperties Request properties to set when making key requests, or null.
|
||||
*/
|
||||
@Deprecated
|
||||
public HttpMediaDrmCallback(String defaultUrl, HttpDataSource.Factory dataSourceFactory,
|
||||
Map<String, String> keyRequestProperties) {
|
||||
this.dataSourceFactory = dataSourceFactory;
|
||||
this.defaultUrl = defaultUrl;
|
||||
this.keyRequestProperties = keyRequestProperties;
|
||||
this.keyRequestProperties = new HashMap<>();
|
||||
if (keyRequestProperties != null) {
|
||||
this.keyRequestProperties.putAll(keyRequestProperties);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a header for key requests made by the callback.
|
||||
*
|
||||
* @param name The name of the header field.
|
||||
* @param value The value of the field.
|
||||
*/
|
||||
public void setKeyRequestProperty(String name, String value) {
|
||||
Assertions.checkNotNull(name);
|
||||
Assertions.checkNotNull(value);
|
||||
synchronized (keyRequestProperties) {
|
||||
keyRequestProperties.put(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears a header for key requests made by the callback.
|
||||
*
|
||||
* @param name The name of the header field.
|
||||
*/
|
||||
public void clearKeyRequestProperty(String name) {
|
||||
Assertions.checkNotNull(name);
|
||||
synchronized (keyRequestProperties) {
|
||||
keyRequestProperties.remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all headers for key requests made by the callback.
|
||||
*/
|
||||
public void clearAllKeyRequestProperties() {
|
||||
synchronized (keyRequestProperties) {
|
||||
keyRequestProperties.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] executeProvisionRequest(UUID uuid, ProvisionRequest request) throws IOException {
|
||||
String url = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData());
|
||||
return executePost(url, new byte[0], null);
|
||||
return executePost(dataSourceFactory, url, new byte[0], null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -85,14 +128,14 @@ public final class HttpMediaDrmCallback implements MediaDrmCallback {
|
|||
if (C.PLAYREADY_UUID.equals(uuid)) {
|
||||
requestProperties.putAll(PLAYREADY_KEY_REQUEST_PROPERTIES);
|
||||
}
|
||||
if (keyRequestProperties != null) {
|
||||
synchronized (keyRequestProperties) {
|
||||
requestProperties.putAll(keyRequestProperties);
|
||||
}
|
||||
return executePost(url, request.getData(), requestProperties);
|
||||
return executePost(dataSourceFactory, url, request.getData(), requestProperties);
|
||||
}
|
||||
|
||||
private byte[] executePost(String url, byte[] data, Map<String, String> requestProperties)
|
||||
throws IOException {
|
||||
private static byte[] executePost(HttpDataSource.Factory dataSourceFactory, String url,
|
||||
byte[] data, Map<String, String> requestProperties) throws IOException {
|
||||
HttpDataSource dataSource = dataSourceFactory.createDataSource();
|
||||
if (requestProperties != null) {
|
||||
for (Map.Entry<String, String> requestProperty : requestProperties.entrySet()) {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
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;
|
||||
|
|
@ -27,24 +26,14 @@ 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.DashUtil;
|
||||
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;
|
||||
|
||||
|
|
@ -58,28 +47,6 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
|
|||
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.
|
||||
|
|
@ -93,7 +60,7 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
|
|||
public static OfflineLicenseHelper<FrameworkMediaCrypto> newWidevineInstance(
|
||||
String licenseUrl, Factory httpDataSourceFactory) throws UnsupportedDrmException {
|
||||
return newWidevineInstance(
|
||||
new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory, null), null);
|
||||
new HttpMediaDrmCallback(licenseUrl, httpDataSourceFactory), null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -174,7 +141,7 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
|
|||
*/
|
||||
public byte[] download(HttpDataSource dataSource, String manifestUriString)
|
||||
throws IOException, InterruptedException, DrmSessionException {
|
||||
return download(dataSource, downloadManifest(dataSource, manifestUriString));
|
||||
return download(dataSource, DashUtil.loadManifest(dataSource, manifestUriString));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -210,11 +177,7 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
|
|||
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();
|
||||
Format sampleFormat = DashUtil.loadSampleFormat(dataSource, representation);
|
||||
if (sampleFormat != null) {
|
||||
drmInitData = sampleFormat.drmInitData;
|
||||
}
|
||||
|
|
@ -288,28 +251,4 @@ public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
|
|||
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 */);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,8 +43,7 @@ public final class UnsupportedDrmException extends Exception {
|
|||
/**
|
||||
* Either {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.
|
||||
*/
|
||||
@Reason
|
||||
public final int reason;
|
||||
@Reason public final int reason;
|
||||
|
||||
/**
|
||||
* @param reason {@link #REASON_UNSUPPORTED_SCHEME} or {@link #REASON_INSTANTIATION_ERROR}.
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor;
|
|||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
|
@ -27,6 +28,8 @@ import java.util.Arrays;
|
|||
*/
|
||||
public final class DefaultExtractorInput implements ExtractorInput {
|
||||
|
||||
private static final int PEEK_MIN_FREE_SPACE_AFTER_RESIZE = 64 * 1024;
|
||||
private static final int PEEK_MAX_FREE_SPACE = 512 * 1024;
|
||||
private static final byte[] SCRATCH_SPACE = new byte[4096];
|
||||
|
||||
private final DataSource dataSource;
|
||||
|
|
@ -46,7 +49,7 @@ public final class DefaultExtractorInput implements ExtractorInput {
|
|||
this.dataSource = dataSource;
|
||||
this.position = position;
|
||||
this.streamLength = length;
|
||||
peekBuffer = new byte[8 * 1024];
|
||||
peekBuffer = new byte[PEEK_MIN_FREE_SPACE_AFTER_RESIZE];
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -176,7 +179,9 @@ public final class DefaultExtractorInput implements ExtractorInput {
|
|||
private void ensureSpaceForPeek(int length) {
|
||||
int requiredLength = peekBufferPosition + length;
|
||||
if (requiredLength > peekBuffer.length) {
|
||||
peekBuffer = Arrays.copyOf(peekBuffer, Math.max(peekBuffer.length * 2, requiredLength));
|
||||
int newPeekCapacity = Util.constrainValue(peekBuffer.length * 2,
|
||||
requiredLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE, requiredLength + PEEK_MAX_FREE_SPACE);
|
||||
peekBuffer = Arrays.copyOf(peekBuffer, newPeekCapacity);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -218,7 +223,12 @@ public final class DefaultExtractorInput implements ExtractorInput {
|
|||
private void updatePeekBuffer(int bytesConsumed) {
|
||||
peekBufferLength -= bytesConsumed;
|
||||
peekBufferPosition = 0;
|
||||
System.arraycopy(peekBuffer, bytesConsumed, peekBuffer, 0, peekBufferLength);
|
||||
byte[] newPeekBuffer = peekBuffer;
|
||||
if (peekBufferLength < peekBuffer.length - PEEK_MAX_FREE_SPACE) {
|
||||
newPeekBuffer = new byte[peekBufferLength + PEEK_MIN_FREE_SPACE_AFTER_RESIZE];
|
||||
}
|
||||
System.arraycopy(peekBuffer, bytesConsumed, newPeekBuffer, 0, peekBufferLength);
|
||||
peekBuffer = newPeekBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ public final class DefaultTrackOutput implements TrackOutput {
|
|||
private Format downstreamFormat;
|
||||
|
||||
// Accessed only by the loading thread (or the consuming thread when there is no loading thread).
|
||||
private boolean pendingFormatAdjustment;
|
||||
private Format lastUnadjustedFormat;
|
||||
private long sampleOffsetUs;
|
||||
private long totalBytesWritten;
|
||||
private Allocation lastAllocation;
|
||||
|
|
@ -265,40 +267,42 @@ 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. May be null if the
|
||||
* caller requires that the format of the stream be read even if it's not changing.
|
||||
* {@link C#BUFFER_FLAG_END_OF_STREAM} flag will be set on the buffer.
|
||||
* @param formatRequired Whether the caller requires that the format of the stream be read even if
|
||||
* it's not changing. A sample will never be read if set to true, however it is still possible
|
||||
* for the end of stream or nothing to be read.
|
||||
* @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.
|
||||
* @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ} or
|
||||
* {@link C#RESULT_BUFFER_READ}.
|
||||
*/
|
||||
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean loadingFinished,
|
||||
long decodeOnlyUntilUs) {
|
||||
switch (infoQueue.readData(formatHolder, buffer, downstreamFormat, extrasHolder)) {
|
||||
case C.RESULT_NOTHING_READ:
|
||||
if (loadingFinished) {
|
||||
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
|
||||
return C.RESULT_BUFFER_READ;
|
||||
}
|
||||
return C.RESULT_NOTHING_READ;
|
||||
public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired,
|
||||
boolean loadingFinished, long decodeOnlyUntilUs) {
|
||||
int result = infoQueue.readData(formatHolder, buffer, formatRequired, loadingFinished,
|
||||
downstreamFormat, extrasHolder);
|
||||
switch (result) {
|
||||
case C.RESULT_FORMAT_READ:
|
||||
downstreamFormat = formatHolder.format;
|
||||
return C.RESULT_FORMAT_READ;
|
||||
case C.RESULT_BUFFER_READ:
|
||||
if (buffer.timeUs < decodeOnlyUntilUs) {
|
||||
buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
|
||||
if (!buffer.isEndOfStream()) {
|
||||
if (buffer.timeUs < decodeOnlyUntilUs) {
|
||||
buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
|
||||
}
|
||||
// Read encryption data if the sample is encrypted.
|
||||
if (buffer.isEncrypted()) {
|
||||
readEncryptionData(buffer, extrasHolder);
|
||||
}
|
||||
// Write the sample data into the holder.
|
||||
buffer.ensureSpaceForWrite(extrasHolder.size);
|
||||
readData(extrasHolder.offset, buffer.data, extrasHolder.size);
|
||||
// Advance the read head.
|
||||
dropDownstreamTo(extrasHolder.nextOffset);
|
||||
}
|
||||
// Read encryption data if the sample is encrypted.
|
||||
if (buffer.isEncrypted()) {
|
||||
readEncryptionData(buffer, extrasHolder);
|
||||
}
|
||||
// Write the sample data into the holder.
|
||||
buffer.ensureSpaceForWrite(extrasHolder.size);
|
||||
readData(extrasHolder.offset, buffer.data, extrasHolder.size);
|
||||
// Advance the read head.
|
||||
dropDownstreamTo(extrasHolder.nextOffset);
|
||||
return C.RESULT_BUFFER_READ;
|
||||
case C.RESULT_NOTHING_READ:
|
||||
return C.RESULT_NOTHING_READ;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
|
@ -445,23 +449,24 @@ public final class DefaultTrackOutput implements TrackOutput {
|
|||
}
|
||||
|
||||
/**
|
||||
* Like {@link #format(Format)}, but with an offset that will be added to the timestamps of
|
||||
* samples subsequently queued to the buffer. The offset is also used to adjust
|
||||
* {@link Format#subsampleOffsetUs} for both the {@link Format} passed and those subsequently
|
||||
* passed to {@link #format(Format)}.
|
||||
* Sets an offset that will be added to the timestamps (and sub-sample timestamps) of samples
|
||||
* subsequently queued to the buffer.
|
||||
*
|
||||
* @param format The format.
|
||||
* @param sampleOffsetUs The timestamp offset in microseconds.
|
||||
*/
|
||||
public void formatWithOffset(Format format, long sampleOffsetUs) {
|
||||
this.sampleOffsetUs = sampleOffsetUs;
|
||||
format(format);
|
||||
public void setSampleOffsetUs(long sampleOffsetUs) {
|
||||
if (this.sampleOffsetUs != sampleOffsetUs) {
|
||||
this.sampleOffsetUs = sampleOffsetUs;
|
||||
pendingFormatAdjustment = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void format(Format format) {
|
||||
Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs);
|
||||
boolean formatChanged = infoQueue.format(adjustedFormat);
|
||||
lastUnadjustedFormat = format;
|
||||
pendingFormatAdjustment = false;
|
||||
if (upstreamFormatChangeListener != null && formatChanged) {
|
||||
upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat);
|
||||
}
|
||||
|
|
@ -518,6 +523,9 @@ public final class DefaultTrackOutput implements TrackOutput {
|
|||
@Override
|
||||
public void sampleMetadata(long timeUs, @C.BufferFlags int flags, int size, int offset,
|
||||
byte[] encryptionKey) {
|
||||
if (pendingFormatAdjustment) {
|
||||
format(lastUnadjustedFormat);
|
||||
}
|
||||
if (!startWriteOperation()) {
|
||||
infoQueue.commitSampleTimestamp(timeUs);
|
||||
return;
|
||||
|
|
@ -754,23 +762,34 @@ public final class DefaultTrackOutput implements TrackOutput {
|
|||
* and the absolute position of the first byte that may still be required after the current
|
||||
* 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 formatRequired Whether the caller requires that the format of the stream be read even
|
||||
* if it's not changing. A sample will never be read if set to true, however it is still
|
||||
* possible for the end of stream or nothing to be read.
|
||||
* @param loadingFinished True if an empty queue should be considered the end of the stream.
|
||||
* @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.
|
||||
* @return The result, which can be {@link C#RESULT_NOTHING_READ}, {@link C#RESULT_FORMAT_READ}
|
||||
* or {@link C#RESULT_BUFFER_READ}.
|
||||
*/
|
||||
@SuppressWarnings("ReferenceEquality")
|
||||
public synchronized int readData(FormatHolder formatHolder, DecoderInputBuffer buffer,
|
||||
Format downstreamFormat, BufferExtrasHolder extrasHolder) {
|
||||
boolean formatRequired, boolean loadingFinished, Format downstreamFormat,
|
||||
BufferExtrasHolder extrasHolder) {
|
||||
if (queueSize == 0) {
|
||||
if (upstreamFormat != null && (buffer == null || upstreamFormat != downstreamFormat)) {
|
||||
if (loadingFinished) {
|
||||
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
|
||||
return C.RESULT_BUFFER_READ;
|
||||
} else if (upstreamFormat != null
|
||||
&& (formatRequired || upstreamFormat != downstreamFormat)) {
|
||||
formatHolder.format = upstreamFormat;
|
||||
return C.RESULT_FORMAT_READ;
|
||||
} else {
|
||||
return C.RESULT_NOTHING_READ;
|
||||
}
|
||||
return C.RESULT_NOTHING_READ;
|
||||
}
|
||||
|
||||
if (buffer == null || formats[relativeReadIndex] != downstreamFormat) {
|
||||
if (formatRequired || formats[relativeReadIndex] != downstreamFormat) {
|
||||
formatHolder.format = formats[relativeReadIndex];
|
||||
return C.RESULT_FORMAT_READ;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,4 +102,5 @@ public interface Extractor {
|
|||
* Releases all kept resources.
|
||||
*/
|
||||
void release();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,17 +23,18 @@ public interface ExtractorOutput {
|
|||
/**
|
||||
* Called by the {@link Extractor} to get the {@link TrackOutput} for a specific track.
|
||||
* <p>
|
||||
* The same {@link TrackOutput} is returned if multiple calls are made with the same
|
||||
* {@code trackId}.
|
||||
* The same {@link TrackOutput} is returned if multiple calls are made with the same {@code id}.
|
||||
*
|
||||
* @param trackId A track identifier.
|
||||
* @param id A track identifier.
|
||||
* @param type The type of the track. Typically one of the {@link com.google.android.exoplayer2.C}
|
||||
* {@code TRACK_TYPE_*} constants.
|
||||
* @return The {@link TrackOutput} for the given track identifier.
|
||||
*/
|
||||
TrackOutput track(int trackId);
|
||||
TrackOutput track(int id, int type);
|
||||
|
||||
/**
|
||||
* Called when all tracks have been identified, meaning no new {@code trackId} values will be
|
||||
* passed to {@link #track(int)}.
|
||||
* passed to {@link #track(int, int)}.
|
||||
*/
|
||||
void endTracks();
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package com.google.android.exoplayer2.extractor;
|
|||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.metadata.Metadata;
|
||||
import com.google.android.exoplayer2.metadata.id3.CommentFrame;
|
||||
import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
|
@ -26,6 +27,18 @@ import java.util.regex.Pattern;
|
|||
*/
|
||||
public final class GaplessInfoHolder {
|
||||
|
||||
/**
|
||||
* A {@link FramePredicate} suitable for use when decoding {@link Metadata} that will be passed
|
||||
* to {@link #setFromMetadata(Metadata)}. Only frames that might contain gapless playback
|
||||
* information are decoded.
|
||||
*/
|
||||
public static final FramePredicate GAPLESS_INFO_ID3_FRAME_PREDICATE = new FramePredicate() {
|
||||
@Override
|
||||
public boolean evaluate(int majorVersion, int id0, int id1, int id2, int id3) {
|
||||
return id0 == 'C' && id1 == 'O' && id2 == 'M' && (id3 == 'M' || majorVersion == 2);
|
||||
}
|
||||
};
|
||||
|
||||
private static final String GAPLESS_COMMENT_ID = "iTunSMPB";
|
||||
private static final Pattern GAPLESS_COMMENT_PATTERN =
|
||||
Pattern.compile("^ [0-9a-fA-F]{8} ([0-9a-fA-F]{8}) ([0-9a-fA-F]{8})");
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.extractor.flv;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorOutput;
|
||||
|
|
@ -183,10 +184,12 @@ public final class FlvExtractor implements Extractor, SeekMap {
|
|||
boolean hasAudio = (flags & 0x04) != 0;
|
||||
boolean hasVideo = (flags & 0x01) != 0;
|
||||
if (hasAudio && audioReader == null) {
|
||||
audioReader = new AudioTagPayloadReader(extractorOutput.track(TAG_TYPE_AUDIO));
|
||||
audioReader = new AudioTagPayloadReader(
|
||||
extractorOutput.track(TAG_TYPE_AUDIO, C.TRACK_TYPE_AUDIO));
|
||||
}
|
||||
if (hasVideo && videoReader == null) {
|
||||
videoReader = new VideoTagPayloadReader(extractorOutput.track(TAG_TYPE_VIDEO));
|
||||
videoReader = new VideoTagPayloadReader(
|
||||
extractorOutput.track(TAG_TYPE_VIDEO, C.TRACK_TYPE_VIDEO));
|
||||
}
|
||||
if (metadataReader == null) {
|
||||
metadataReader = new ScriptTagPayloadReader(null);
|
||||
|
|
|
|||
|
|
@ -673,6 +673,9 @@ public final class MatroskaExtractor implements Extractor {
|
|||
case 3:
|
||||
currentTrack.stereoMode = C.STEREO_MODE_TOP_BOTTOM;
|
||||
break;
|
||||
case 15:
|
||||
currentTrack.stereoMode = C.STEREO_MODE_STEREO_MESH;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
@ -1462,6 +1465,7 @@ public final class MatroskaExtractor implements Extractor {
|
|||
throw new ParserException("Unrecognized codec identifier.");
|
||||
}
|
||||
|
||||
int type;
|
||||
Format format;
|
||||
@C.SelectionFlags int selectionFlags = 0;
|
||||
selectionFlags |= flagDefault ? C.SELECTION_FLAG_DEFAULT : 0;
|
||||
|
|
@ -1469,10 +1473,12 @@ public final class MatroskaExtractor implements Extractor {
|
|||
// TODO: Consider reading the name elements of the tracks and, if present, incorporating them
|
||||
// into the trackId passed when creating the formats.
|
||||
if (MimeTypes.isAudio(mimeType)) {
|
||||
type = C.TRACK_TYPE_AUDIO;
|
||||
format = Format.createAudioSampleFormat(Integer.toString(trackId), mimeType, null,
|
||||
Format.NO_VALUE, maxInputSize, channelCount, sampleRate, pcmEncoding,
|
||||
initializationData, drmInitData, selectionFlags, language);
|
||||
} else if (MimeTypes.isVideo(mimeType)) {
|
||||
type = C.TRACK_TYPE_VIDEO;
|
||||
if (displayUnit == Track.DISPLAY_UNIT_PIXELS) {
|
||||
displayWidth = displayWidth == Format.NO_VALUE ? width : displayWidth;
|
||||
displayHeight = displayHeight == Format.NO_VALUE ? height : displayHeight;
|
||||
|
|
@ -1485,17 +1491,19 @@ public final class MatroskaExtractor implements Extractor {
|
|||
Format.NO_VALUE, maxInputSize, width, height, Format.NO_VALUE, initializationData,
|
||||
Format.NO_VALUE, pixelWidthHeightRatio, projectionData, stereoMode, drmInitData);
|
||||
} else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)) {
|
||||
type = C.TRACK_TYPE_TEXT;
|
||||
format = Format.createTextSampleFormat(Integer.toString(trackId), mimeType, null,
|
||||
Format.NO_VALUE, selectionFlags, language, drmInitData);
|
||||
} else if (MimeTypes.APPLICATION_VOBSUB.equals(mimeType)
|
||||
|| MimeTypes.APPLICATION_PGS.equals(mimeType)) {
|
||||
type = C.TRACK_TYPE_TEXT;
|
||||
format = Format.createImageSampleFormat(Integer.toString(trackId), mimeType, null,
|
||||
Format.NO_VALUE, initializationData, language, drmInitData);
|
||||
} else {
|
||||
throw new ParserException("Unexpected MIME type.");
|
||||
}
|
||||
|
||||
this.output = output.track(number);
|
||||
this.output = output.track(number, type);
|
||||
this.output.format(format);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.extractor.mp3;
|
||||
|
||||
import android.support.annotation.IntDef;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
|
|
@ -33,6 +34,8 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
|||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* Extracts data from an MP3 file.
|
||||
|
|
@ -51,6 +54,23 @@ public final class Mp3Extractor implements Extractor {
|
|||
|
||||
};
|
||||
|
||||
/**
|
||||
* Flags controlling the behavior of the extractor.
|
||||
*/
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@IntDef(flag = true, value = {FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, FLAG_DISABLE_ID3_METADATA})
|
||||
public @interface Flags {}
|
||||
/**
|
||||
* Flag to force enable seeking using a constant bitrate assumption in cases where seeking would
|
||||
* otherwise not be possible.
|
||||
*/
|
||||
public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1;
|
||||
/**
|
||||
* Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not
|
||||
* required.
|
||||
*/
|
||||
public static final int FLAG_DISABLE_ID3_METADATA = 2;
|
||||
|
||||
/**
|
||||
* The maximum number of bytes to search when synchronizing, before giving up.
|
||||
*/
|
||||
|
|
@ -72,6 +92,7 @@ public final class Mp3Extractor implements Extractor {
|
|||
private static final int INFO_HEADER = Util.getIntegerCodeForString("Info");
|
||||
private static final int VBRI_HEADER = Util.getIntegerCodeForString("VBRI");
|
||||
|
||||
@Flags private final int flags;
|
||||
private final long forcedFirstSampleTimestampUs;
|
||||
private final ParsableByteArray scratch;
|
||||
private final MpegAudioHeader synchronizedHeader;
|
||||
|
|
@ -93,16 +114,27 @@ public final class Mp3Extractor implements Extractor {
|
|||
* Constructs a new {@link Mp3Extractor}.
|
||||
*/
|
||||
public Mp3Extractor() {
|
||||
this(C.TIME_UNSET);
|
||||
this(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Mp3Extractor}.
|
||||
*
|
||||
* @param flags Flags that control the extractor's behavior.
|
||||
*/
|
||||
public Mp3Extractor(@Flags int flags) {
|
||||
this(flags, C.TIME_UNSET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Mp3Extractor}.
|
||||
*
|
||||
* @param flags Flags that control the extractor's behavior.
|
||||
* @param forcedFirstSampleTimestampUs A timestamp to force for the first sample, or
|
||||
* {@link C#TIME_UNSET} if forcing is not required.
|
||||
*/
|
||||
public Mp3Extractor(long forcedFirstSampleTimestampUs) {
|
||||
public Mp3Extractor(@Flags int flags, long forcedFirstSampleTimestampUs) {
|
||||
this.flags = flags;
|
||||
this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs;
|
||||
scratch = new ParsableByteArray(SCRATCH_LENGTH);
|
||||
synchronizedHeader = new MpegAudioHeader();
|
||||
|
|
@ -118,7 +150,7 @@ public final class Mp3Extractor implements Extractor {
|
|||
@Override
|
||||
public void init(ExtractorOutput output) {
|
||||
extractorOutput = output;
|
||||
trackOutput = extractorOutput.track(0);
|
||||
trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO);
|
||||
extractorOutput.endTracks();
|
||||
}
|
||||
|
||||
|
|
@ -151,7 +183,8 @@ public final class Mp3Extractor implements Extractor {
|
|||
trackOutput.format(Format.createAudioSampleFormat(null, synchronizedHeader.mimeType, null,
|
||||
Format.NO_VALUE, MpegAudioHeader.MAX_FRAME_SIZE_BYTES, synchronizedHeader.channels,
|
||||
synchronizedHeader.sampleRate, Format.NO_VALUE, gaplessInfoHolder.encoderDelay,
|
||||
gaplessInfoHolder.encoderPadding, null, null, 0, null, metadata));
|
||||
gaplessInfoHolder.encoderPadding, null, null, 0, null,
|
||||
(flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata));
|
||||
}
|
||||
return readSample(input);
|
||||
}
|
||||
|
|
@ -284,7 +317,11 @@ public final class Mp3Extractor implements Extractor {
|
|||
byte[] id3Data = new byte[tagLength];
|
||||
System.arraycopy(scratch.data, 0, id3Data, 0, Id3Decoder.ID3_HEADER_LENGTH);
|
||||
input.peekFully(id3Data, Id3Decoder.ID3_HEADER_LENGTH, framesLength);
|
||||
metadata = new Id3Decoder().decode(id3Data, tagLength);
|
||||
// We need to parse enough ID3 metadata to retrieve any gapless playback information even
|
||||
// if ID3 metadata parsing is disabled.
|
||||
Id3Decoder.FramePredicate id3FramePredicate = (flags & FLAG_DISABLE_ID3_METADATA) != 0
|
||||
? GaplessInfoHolder.GAPLESS_INFO_ID3_FRAME_PREDICATE : null;
|
||||
metadata = new Id3Decoder(id3FramePredicate).decode(id3Data, tagLength);
|
||||
if (metadata != null) {
|
||||
gaplessInfoHolder.setFromMetadata(metadata);
|
||||
}
|
||||
|
|
@ -350,7 +387,8 @@ public final class Mp3Extractor implements Extractor {
|
|||
}
|
||||
}
|
||||
|
||||
if (seeker == null) {
|
||||
if (seeker == null || (!seeker.isSeekable()
|
||||
&& (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) {
|
||||
// Repopulate the synchronized header in case we had to skip an invalid seeking header, which
|
||||
// would give an invalid CBR bitrate.
|
||||
input.resetPeekPosition();
|
||||
|
|
|
|||
|
|
@ -332,6 +332,9 @@ import java.util.List;
|
|||
return new TrackSampleTable(offsets, sizes, maximumSize, timestamps, flags);
|
||||
}
|
||||
|
||||
// Omit any sample at the end point of an edit for audio tracks.
|
||||
boolean omitClippedSample = track.type == C.TRACK_TYPE_AUDIO;
|
||||
|
||||
// Count the number of samples after applying edits.
|
||||
int editedSampleCount = 0;
|
||||
int nextSampleIndex = 0;
|
||||
|
|
@ -342,7 +345,8 @@ import java.util.List;
|
|||
long duration = Util.scaleLargeTimestamp(track.editListDurations[i], track.timescale,
|
||||
track.movieTimescale);
|
||||
int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true);
|
||||
int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, true, false);
|
||||
int endIndex = Util.binarySearchCeil(timestamps, mediaTime + duration, omitClippedSample,
|
||||
false);
|
||||
editedSampleCount += endIndex - startIndex;
|
||||
copyMetadata |= nextSampleIndex != startIndex;
|
||||
nextSampleIndex = endIndex;
|
||||
|
|
@ -365,7 +369,7 @@ import java.util.List;
|
|||
long endMediaTime = mediaTime + Util.scaleLargeTimestamp(duration, track.timescale,
|
||||
track.movieTimescale);
|
||||
int startIndex = Util.binarySearchCeil(timestamps, mediaTime, true, true);
|
||||
int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, true, false);
|
||||
int endIndex = Util.binarySearchCeil(timestamps, endMediaTime, omitClippedSample, false);
|
||||
if (copyMetadata) {
|
||||
int count = endIndex - startIndex;
|
||||
System.arraycopy(offsets, startIndex, editedOffsets, sampleIndex, count);
|
||||
|
|
@ -716,6 +720,9 @@ import java.util.List;
|
|||
case 2:
|
||||
stereoMode = C.STEREO_MODE_LEFT_RIGHT;
|
||||
break;
|
||||
case 3:
|
||||
stereoMode = C.STEREO_MODE_STEREO_MESH;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,7 +106,6 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
|
||||
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};
|
||||
|
||||
|
|
@ -118,8 +117,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
private static final int STATE_READING_SAMPLE_CONTINUE = 4;
|
||||
|
||||
// Workarounds.
|
||||
@Flags
|
||||
private final int flags;
|
||||
@Flags private final int flags;
|
||||
private final Track sideloadedTrack;
|
||||
|
||||
// Track-linked data bundle, accessible as a whole through trackID.
|
||||
|
|
@ -127,8 +125,8 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
|
||||
// Temporary arrays.
|
||||
private final ParsableByteArray nalStartCode;
|
||||
private final ParsableByteArray nalLength;
|
||||
private final ParsableByteArray nalPayload;
|
||||
private final ParsableByteArray nalPrefix;
|
||||
private final ParsableByteArray nalBuffer;
|
||||
private final ParsableByteArray encryptionSignalByte;
|
||||
|
||||
// Adjusts sample timestamps.
|
||||
|
|
@ -154,17 +152,25 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
private int sampleSize;
|
||||
private int sampleBytesWritten;
|
||||
private int sampleCurrentNalBytesRemaining;
|
||||
private boolean processSeiNalUnitPayload;
|
||||
|
||||
// Extractor output.
|
||||
private ExtractorOutput extractorOutput;
|
||||
private TrackOutput eventMessageTrackOutput;
|
||||
private TrackOutput cea608TrackOutput;
|
||||
private TrackOutput[] cea608TrackOutputs;
|
||||
|
||||
// Whether extractorOutput.seekMap has been called.
|
||||
private boolean haveOutputSeekMap;
|
||||
|
||||
public FragmentedMp4Extractor() {
|
||||
this(0, null);
|
||||
this(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param flags Flags that control the extractor's behavior.
|
||||
*/
|
||||
public FragmentedMp4Extractor(@Flags int flags) {
|
||||
this(flags, null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -172,24 +178,24 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
|
||||
*/
|
||||
public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster) {
|
||||
this(flags, null, timestampAdjuster);
|
||||
this(flags, timestampAdjuster, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param flags Flags that control the extractor's behavior.
|
||||
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
|
||||
* @param sideloadedTrack Sideloaded track information, in the case that the extractor
|
||||
* will not receive a moov box in the input data.
|
||||
* @param timestampAdjuster Adjusts sample timestamps. May be null if no adjustment is needed.
|
||||
*/
|
||||
public FragmentedMp4Extractor(@Flags int flags, Track sideloadedTrack,
|
||||
TimestampAdjuster timestampAdjuster) {
|
||||
this.sideloadedTrack = sideloadedTrack;
|
||||
public FragmentedMp4Extractor(@Flags int flags, TimestampAdjuster timestampAdjuster,
|
||||
Track sideloadedTrack) {
|
||||
this.flags = flags | (sideloadedTrack != null ? FLAG_SIDELOADED : 0);
|
||||
this.timestampAdjuster = timestampAdjuster;
|
||||
this.sideloadedTrack = sideloadedTrack;
|
||||
atomHeader = new ParsableByteArray(Atom.LONG_HEADER_SIZE);
|
||||
nalStartCode = new ParsableByteArray(NalUnitUtil.NAL_START_CODE);
|
||||
nalLength = new ParsableByteArray(4);
|
||||
nalPayload = new ParsableByteArray(1);
|
||||
nalPrefix = new ParsableByteArray(5);
|
||||
nalBuffer = new ParsableByteArray();
|
||||
encryptionSignalByte = new ParsableByteArray(1);
|
||||
extendedTypeScratch = new byte[16];
|
||||
containerAtoms = new Stack<>();
|
||||
|
|
@ -209,7 +215,7 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
public void init(ExtractorOutput output) {
|
||||
extractorOutput = output;
|
||||
if (sideloadedTrack != null) {
|
||||
TrackBundle bundle = new TrackBundle(output.track(0));
|
||||
TrackBundle bundle = new TrackBundle(output.track(0, sideloadedTrack.type));
|
||||
bundle.init(sideloadedTrack, new DefaultSampleValues(0, 0, 0, 0));
|
||||
trackBundles.put(0, bundle);
|
||||
maybeInitExtraTracks();
|
||||
|
|
@ -420,7 +426,7 @@ 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);
|
||||
TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i));
|
||||
TrackBundle trackBundle = new TrackBundle(extractorOutput.track(i, track.type));
|
||||
trackBundle.init(track, defaultSampleValuesArray.get(track.id));
|
||||
trackBundles.put(track.id, trackBundle);
|
||||
durationUs = Math.max(durationUs, track.durationUs);
|
||||
|
|
@ -449,14 +455,16 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
|
||||
private void maybeInitExtraTracks() {
|
||||
if ((flags & FLAG_ENABLE_EMSG_TRACK) != 0 && eventMessageTrackOutput == null) {
|
||||
eventMessageTrackOutput = extractorOutput.track(trackBundles.size());
|
||||
eventMessageTrackOutput = extractorOutput.track(trackBundles.size(), C.TRACK_TYPE_METADATA);
|
||||
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);
|
||||
if ((flags & FLAG_ENABLE_CEA608_TRACK) != 0 && cea608TrackOutputs == null) {
|
||||
TrackOutput cea608TrackOutput = extractorOutput.track(trackBundles.size() + 1,
|
||||
C.TRACK_TYPE_TEXT);
|
||||
cea608TrackOutput.format(Format.createTextSampleFormat(null, MimeTypes.APPLICATION_CEA608,
|
||||
null, Format.NO_VALUE, 0, null, null));
|
||||
cea608TrackOutputs = new TrackOutput[] {cea608TrackOutput};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1059,49 +1067,49 @@ public final class FragmentedMp4Extractor implements Extractor {
|
|||
if (track.nalUnitLengthFieldLength != 0) {
|
||||
// Zero the top three bytes of the array that we'll use to decode nal unit lengths, in case
|
||||
// they're only 1 or 2 bytes long.
|
||||
byte[] nalLengthData = nalLength.data;
|
||||
nalLengthData[0] = 0;
|
||||
nalLengthData[1] = 0;
|
||||
nalLengthData[2] = 0;
|
||||
int nalUnitLengthFieldLength = track.nalUnitLengthFieldLength;
|
||||
byte[] nalPrefixData = nalPrefix.data;
|
||||
nalPrefixData[0] = 0;
|
||||
nalPrefixData[1] = 0;
|
||||
nalPrefixData[2] = 0;
|
||||
int nalUnitPrefixLength = track.nalUnitLengthFieldLength + 1;
|
||||
int nalUnitLengthFieldLengthDiff = 4 - track.nalUnitLengthFieldLength;
|
||||
// NAL units are length delimited, but the decoder requires start code delimited units.
|
||||
// Loop until we've written the sample to the track output, replacing length delimiters with
|
||||
// start codes as we encounter them.
|
||||
while (sampleBytesWritten < sampleSize) {
|
||||
if (sampleCurrentNalBytesRemaining == 0) {
|
||||
// Read the NAL length so that we know where we find the next one.
|
||||
input.readFully(nalLength.data, nalUnitLengthFieldLengthDiff, nalUnitLengthFieldLength);
|
||||
nalLength.setPosition(0);
|
||||
sampleCurrentNalBytesRemaining = nalLength.readUnsignedIntToInt();
|
||||
// Read the NAL length so that we know where we find the next one, and its type.
|
||||
input.readFully(nalPrefixData, nalUnitLengthFieldLengthDiff, nalUnitPrefixLength);
|
||||
nalPrefix.setPosition(0);
|
||||
sampleCurrentNalBytesRemaining = nalPrefix.readUnsignedIntToInt() - 1;
|
||||
// Write a start code for the current NAL unit.
|
||||
nalStartCode.setPosition(0);
|
||||
output.sampleData(nalStartCode, 4);
|
||||
sampleBytesWritten += 4;
|
||||
// Write the NAL unit type byte.
|
||||
output.sampleData(nalPrefix, 1);
|
||||
processSeiNalUnitPayload = cea608TrackOutputs != null
|
||||
&& NalUnitUtil.isNalUnitSei(track.format.sampleMimeType, nalPrefixData[4]);
|
||||
sampleBytesWritten += 5;
|
||||
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);
|
||||
int writtenBytes;
|
||||
if (processSeiNalUnitPayload) {
|
||||
// Read and write the payload of the SEI NAL unit.
|
||||
nalBuffer.reset(sampleCurrentNalBytesRemaining);
|
||||
input.readFully(nalBuffer.data, 0, sampleCurrentNalBytesRemaining);
|
||||
output.sampleData(nalBuffer, sampleCurrentNalBytesRemaining);
|
||||
writtenBytes = sampleCurrentNalBytesRemaining;
|
||||
// Unescape and process the SEI NAL unit.
|
||||
int unescapedLength = NalUnitUtil.unescapeStream(nalBuffer.data, nalBuffer.limit());
|
||||
// If the format is H.265/HEVC the NAL unit header has two bytes so skip one more byte.
|
||||
nalBuffer.setPosition(MimeTypes.VIDEO_H265.equals(track.format.sampleMimeType) ? 1 : 0);
|
||||
nalBuffer.setLimit(unescapedLength);
|
||||
CeaUtil.consume(fragment.getSamplePresentationTime(sampleIndex) * 1000L, nalBuffer,
|
||||
cea608TrackOutputs);
|
||||
} else {
|
||||
// Write the payload of the NAL unit.
|
||||
writtenBytes = output.sampleData(input, sampleCurrentNalBytesRemaining, false);
|
||||
}
|
||||
sampleBytesWritten += writtenBytes;
|
||||
sampleCurrentNalBytesRemaining -= writtenBytes;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,8 +83,7 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
|||
private final ParsableByteArray atomHeader;
|
||||
private final Stack<ContainerAtom> containerAtoms;
|
||||
|
||||
@State
|
||||
private int parserState;
|
||||
@State private int parserState;
|
||||
private int atomType;
|
||||
private long atomSize;
|
||||
private int atomHeaderBytesRead;
|
||||
|
|
@ -344,7 +343,8 @@ public final class Mp4Extractor implements Extractor, SeekMap {
|
|||
continue;
|
||||
}
|
||||
|
||||
Mp4Track mp4Track = new Mp4Track(track, trackSampleTable, extractorOutput.track(i));
|
||||
Mp4Track mp4Track = new Mp4Track(track, trackSampleTable,
|
||||
extractorOutput.track(i, track.type));
|
||||
// Each sample has up to three bytes of overhead for the start code that replaces its length.
|
||||
// Allow ten source samples per output sample, like the platform extractor.
|
||||
int maxInputSize = trackSampleTable.maximumSize + 3 * 10;
|
||||
|
|
|
|||
|
|
@ -75,8 +75,7 @@ public final class Track {
|
|||
* One of {@code TRANSFORMATION_*}. Defines the transformation to apply before outputting each
|
||||
* sample.
|
||||
*/
|
||||
@Transformation
|
||||
public final int sampleTransformation;
|
||||
@Transformation public final int sampleTransformation;
|
||||
|
||||
/**
|
||||
* Track encryption boxes for the different track sample descriptions. Entries may be null.
|
||||
|
|
@ -94,7 +93,7 @@ public final class Track {
|
|||
public final long[] editListMediaTimes;
|
||||
|
||||
/**
|
||||
* For H264 video tracks, the length in bytes of the NALUnitLength field in each sample. -1 for
|
||||
* For H264 video tracks, the length in bytes of the NALUnitLength field in each sample. 0 for
|
||||
* other track types.
|
||||
*/
|
||||
public final int nalUnitLengthFieldLength;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.extractor.ogg;
|
||||
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.extractor.ExtractorInput;
|
||||
|
|
@ -75,7 +76,7 @@ public class OggExtractor implements Extractor {
|
|||
|
||||
@Override
|
||||
public void init(ExtractorOutput output) {
|
||||
TrackOutput trackOutput = output.track(0);
|
||||
TrackOutput trackOutput = output.track(0, C.TRACK_TYPE_AUDIO);
|
||||
output.endTracks();
|
||||
// TODO: fix the case if sniff() isn't called
|
||||
streamReader.init(output, trackOutput);
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue