Commit b2333c86 by Ian Baker Committed by GitHub

Merge pull request #9045 from google/dev-v2-r2.14.1

r2.14.1
parents 9be5ece8 c1b7c47a
Showing with 2616 additions and 572 deletions
...@@ -25,6 +25,8 @@ and extend, and can be updated through Play Store application updates. ...@@ -25,6 +25,8 @@ and extend, and can be updated through Play Store application updates.
ExoPlayer modules can be obtained from [the Google Maven repository][]. It's ExoPlayer modules can be obtained from [the Google Maven repository][]. It's
also possible to clone the repository and depend on the modules locally. also possible to clone the repository and depend on the modules locally.
[the Google Maven repository]: https://developer.android.com/studio/build/dependencies#google-maven
### From the Google Maven repository ### From the Google Maven repository
#### 1. Add ExoPlayer module dependencies #### #### 1. Add ExoPlayer module dependencies ####
...@@ -39,13 +41,10 @@ implementation 'com.google.android.exoplayer:exoplayer:2.X.X' ...@@ -39,13 +41,10 @@ implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
where `2.X.X` is your preferred version. where `2.X.X` is your preferred version.
Note: old versions of ExoPlayer are available via JCenter. To use them, you need
to add `jcenter()` to your project's root build.gradle `repositories` block.
As an alternative to the full library, you can depend on only the library As an alternative to the full library, you can depend on only the library
modules that you actually need. For example the following will add dependencies modules that you actually need. For example the following will add dependencies
on the Core, DASH and UI library modules, as might be required for an app that on the Core, DASH and UI library modules, as might be required for an app that
plays DASH content: only plays DASH content:
```gradle ```gradle
implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X' implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X'
...@@ -54,13 +53,15 @@ implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X' ...@@ -54,13 +53,15 @@ implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X'
``` ```
The available library modules are listed below. Adding a dependency to the full The available library modules are listed below. Adding a dependency to the full
library is equivalent to adding dependencies on all of the library modules ExoPlayer library is equivalent to adding dependencies on all of the library
individually. modules individually.
* `exoplayer-core`: Core functionality (required). * `exoplayer-core`: Core functionality (required).
* `exoplayer-dash`: Support for DASH content. * `exoplayer-dash`: Support for DASH content.
* `exoplayer-hls`: Support for HLS content. * `exoplayer-hls`: Support for HLS content.
* `exoplayer-rtsp`: Support for RTSP content.
* `exoplayer-smoothstreaming`: Support for SmoothStreaming content. * `exoplayer-smoothstreaming`: Support for SmoothStreaming content.
* `exoplayer-transformer`: Media transformation functionality.
* `exoplayer-ui`: UI components and resources for use with ExoPlayer. * `exoplayer-ui`: UI components and resources for use with ExoPlayer.
In addition to library modules, ExoPlayer has extension modules that depend on In addition to library modules, ExoPlayer has extension modules that depend on
...@@ -72,7 +73,6 @@ More information on the library and extension modules that are available can be ...@@ -72,7 +73,6 @@ More information on the library and extension modules that are available can be
found on the [Google Maven ExoPlayer page][]. found on the [Google Maven ExoPlayer page][].
[extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/ [extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/
[the Google Maven repository]: https://developer.android.com/studio/build/dependencies#google-maven
[Google Maven ExoPlayer page]: https://maven.google.com/web/index.html#com.google.android.exoplayer [Google Maven ExoPlayer page]: https://maven.google.com/web/index.html#com.google.android.exoplayer
#### 2. Turn on Java 8 support #### #### 2. Turn on Java 8 support ####
...@@ -87,6 +87,12 @@ compileOptions { ...@@ -87,6 +87,12 @@ compileOptions {
} }
``` ```
#### 3. Enable multidex ####
If your Gradle `minSdkVersion` is 20 or lower, you should
[enable multidex](https://developer.android.com/studio/build/multidex) in order
to prevent build errors.
### Locally ### ### Locally ###
Cloning the repository and depending on the modules locally is required when Cloning the repository and depending on the modules locally is required when
...@@ -104,12 +110,12 @@ git checkout release-v2 ...@@ -104,12 +110,12 @@ git checkout release-v2
``` ```
Next, add the following to your project's `settings.gradle` file, replacing Next, add the following to your project's `settings.gradle` file, replacing
`/absolute/path/to/exoplayer` with the absolute path to your local copy: `path/to/exoplayer` with the path to your local copy:
```gradle ```gradle
gradle.ext.exoplayerRoot = '/absolute/path/to/exoplayer' gradle.ext.exoplayerRoot = 'path/to/exoplayer'
gradle.ext.exoplayerModulePrefix = 'exoplayer-' gradle.ext.exoplayerModulePrefix = 'exoplayer-'
apply from: new File(gradle.ext.exoplayerRoot, 'core_settings.gradle') apply from: file("$gradle.ext.exoplayerRoot/core_settings.gradle")
``` ```
You should now see the ExoPlayer modules appear as part of your project. You can You should now see the ExoPlayer modules appear as part of your project. You can
......
# Release notes # Release notes
### 2.14.1 (2021-06-11)
* Core Library:
* Fix gradle config to allow specifying a relative path for
`exoplayerRoot` when [depending on ExoPlayer locally](README.md#locally)
([#8927](https://github.com/google/ExoPlayer/issues/8927)).
* Update `MediaItem.Builder` javadoc to discourage calling setters that
will be (currently) ignored if another setter is not also called.
* Extractors:
* Add support for MPEG-H 3D Audio in MP4 extractors
([#8860](https://github.com/google/ExoPlayer/pull/8860)).
* Video:
* Fix bug that could cause `CodecException: Error 0xffffffff` to be thrown
from `MediaCodec.native_setSurface` in use cases that involve both
swapping the output `Surface` and a mixture of secure and non-secure
content being played
([#8776](https://github.com/google/ExoPlayer/issues/8776)).
* HLS:
* Use the `PRECISE` attribute in `EXT-X-START` to select the default start
position.
* Fix a bug where skipping into spliced-in chunks triggered an assertion
error ([#8937](https://github.com/google/ExoPlayer/issues/8937)).
* DRM:
* Keep secure `MediaCodec` instances initialized when disabling (but not
resetting) `MediaCodecRenderer`. This helps re-use secure decoders in
more contexts, which avoids the 'black flash' caused by detaching a
`Surface` from a secure decoder on some devices
([#8842](https://github.com/google/ExoPlayer/issues/8842)). It will also
result in DRM license refresh network requests while the player is
stopped if `Player#setForegroundMode` is true.
* Fix issue where offline keys were unnecessarily (and incorrectly)
restored into a session before being released. This call sequence is
explicitly disallowed in OEMCrypto v16.
* UI:
* Keep subtitle language features embedded (e.g. rubies & tate-chu-yoko)
in `Cue.text` even when `SubtitleView#setApplyEmbeddedStyles()` is
`false`.
* Fix `NullPointerException` in `StyledPlayerView` that could occur after
calling `StyledPlayerView.setPlayer(null)`
([#8985](https://github.com/google/ExoPlayer/issues/8985)).
* RTSP:
* Add support for RTSP basic and digest authentication
([#8941](https://github.com/google/ExoPlayer/issues/8941)).
* Enable using repeat mode and playlist with RTSP
([#8994](https://github.com/google/ExoPlayer/issues/8994)).
* Add `RtspMediaSource.Factory` option to set the RTSP user agent.
* Add `RtspMediaSource.Factory` option to force using TCP for streaming.
* GL demo app:
* Fix texture transformation to avoid green bars shown on some videos
([#8992](https://github.com/google/ExoPlayer/issues/8992)).
### 2.14.0 (2021-05-13) ### 2.14.0 (2021-05-13)
* Core Library: * Core Library:
...@@ -1023,6 +1074,7 @@ To learn more about what's new in 2.12, read the corresponding ...@@ -1023,6 +1074,7 @@ To learn more about what's new in 2.12, read the corresponding
and the range of API levels for which they are supported is too small to and the range of API levels for which they are supported is too small to
be useful. be useful.
* Remove generic types from DRM components. * Remove generic types from DRM components.
* Rename `DefaultDrmSessionEventListener` to `DrmSessionEventListener`.
* Track selection: * Track selection:
* Add `TrackSelection.shouldCancelMediaChunkLoad` to check whether an * Add `TrackSelection.shouldCancelMediaChunkLoad` to check whether an
ongoing load should be canceled ongoing load should be canceled
......
...@@ -13,8 +13,8 @@ ...@@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.14.0' releaseVersion = '2.14.1'
releaseVersionCode = 2014000 releaseVersionCode = 2014001
minSdkVersion = 16 minSdkVersion = 16
appTargetSdkVersion = 29 appTargetSdkVersion = 29
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest. targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest.
......
...@@ -11,10 +11,9 @@ ...@@ -11,10 +11,9 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
def rootDir = gradle.ext.exoplayerRoot def rootDir = file(gradle.ext.exoplayerRoot)
if (!gradle.ext.has('exoplayerSettingsDir')) { if (!gradle.ext.has('exoplayerSettingsDir')) {
gradle.ext.exoplayerSettingsDir = gradle.ext.exoplayerSettingsDir = rootDir.getCanonicalPath()
new File(rootDir.toString()).getCanonicalPath()
} }
def modulePrefix = ':' def modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) { if (gradle.ext.has('exoplayerModulePrefix')) {
......
...@@ -11,10 +11,11 @@ ...@@ -11,10 +11,11 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
attribute vec2 a_position; attribute vec4 a_position;
attribute vec2 a_texcoord; attribute vec4 a_texcoord;
uniform mat4 tex_transform;
varying vec2 v_texcoord; varying vec2 v_texcoord;
void main() { void main() {
gl_Position = vec4(a_position.x, a_position.y, 0, 1); gl_Position = a_position;
v_texcoord = a_texcoord; v_texcoord = (tex_transform * a_texcoord).xy;
} }
...@@ -88,9 +88,9 @@ import javax.microedition.khronos.opengles.GL10; ...@@ -88,9 +88,9 @@ import javax.microedition.khronos.opengles.GL10;
GlUtil.Uniform[] uniforms = GlUtil.getUniforms(program); GlUtil.Uniform[] uniforms = GlUtil.getUniforms(program);
for (GlUtil.Attribute attribute : attributes) { for (GlUtil.Attribute attribute : attributes) {
if (attribute.name.equals("a_position")) { if (attribute.name.equals("a_position")) {
attribute.setBuffer(new float[] {-1, -1, 1, -1, -1, 1, 1, 1}, 2); attribute.setBuffer(new float[] {-1, -1, 0, 1, 1, -1, 0, 1, -1, 1, 0, 1, 1, 1, 0, 1}, 4);
} else if (attribute.name.equals("a_texcoord")) { } else if (attribute.name.equals("a_texcoord")) {
attribute.setBuffer(new float[] {0, 1, 1, 1, 0, 0, 1, 0}, 2); attribute.setBuffer(new float[] {0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1}, 4);
} }
} }
this.attributes = attributes; this.attributes = attributes;
...@@ -111,7 +111,7 @@ import javax.microedition.khronos.opengles.GL10; ...@@ -111,7 +111,7 @@ import javax.microedition.khronos.opengles.GL10;
} }
@Override @Override
public void draw(int frameTexture, long frameTimestampUs) { public void draw(int frameTexture, long frameTimestampUs, float[] transformMatrix) {
// Draw to the canvas and store it in a texture. // Draw to the canvas and store it in a texture.
String text = String.format(Locale.US, "%.02f", frameTimestampUs / (float) C.MICROS_PER_SECOND); String text = String.format(Locale.US, "%.02f", frameTimestampUs / (float) C.MICROS_PER_SECOND);
overlayBitmap.eraseColor(Color.TRANSPARENT); overlayBitmap.eraseColor(Color.TRANSPARENT);
...@@ -140,6 +140,9 @@ import javax.microedition.khronos.opengles.GL10; ...@@ -140,6 +140,9 @@ import javax.microedition.khronos.opengles.GL10;
case "scaleY": case "scaleY":
uniform.setFloat(bitmapScaleY); uniform.setFloat(bitmapScaleY);
break; break;
case "tex_transform":
uniform.setFloats(transformMatrix);
break;
default: // fall out default: // fall out
} }
} }
......
...@@ -25,6 +25,7 @@ import android.os.Handler; ...@@ -25,6 +25,7 @@ import android.os.Handler;
import android.view.Surface; import android.view.Surface;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
...@@ -61,8 +62,9 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView { ...@@ -61,8 +62,9 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
* *
* @param frameTexture The ID of a GL texture containing a video frame. * @param frameTexture The ID of a GL texture containing a video frame.
* @param frameTimestampUs The presentation timestamp of the frame, in microseconds. * @param frameTimestampUs The presentation timestamp of the frame, in microseconds.
* @param transformMatrix The 4 * 4 transform matrix to be applied to the texture.
*/ */
void draw(int frameTexture, long frameTimestampUs); void draw(int frameTexture, long frameTimestampUs, float[] transformMatrix);
} }
private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0; private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
...@@ -214,6 +216,7 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView { ...@@ -214,6 +216,7 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
private final VideoProcessor videoProcessor; private final VideoProcessor videoProcessor;
private final AtomicBoolean frameAvailable; private final AtomicBoolean frameAvailable;
private final TimedValueQueue<Long> sampleTimestampQueue; private final TimedValueQueue<Long> sampleTimestampQueue;
private final float[] transformMatrix;
private int texture; private int texture;
@Nullable private SurfaceTexture surfaceTexture; @Nullable private SurfaceTexture surfaceTexture;
...@@ -229,6 +232,8 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView { ...@@ -229,6 +232,8 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
sampleTimestampQueue = new TimedValueQueue<>(); sampleTimestampQueue = new TimedValueQueue<>();
width = -1; width = -1;
height = -1; height = -1;
frameTimestampUs = C.TIME_UNSET;
transformMatrix = new float[16];
} }
@Override @Override
...@@ -271,13 +276,14 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView { ...@@ -271,13 +276,14 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
SurfaceTexture surfaceTexture = Assertions.checkNotNull(this.surfaceTexture); SurfaceTexture surfaceTexture = Assertions.checkNotNull(this.surfaceTexture);
surfaceTexture.updateTexImage(); surfaceTexture.updateTexImage();
long lastFrameTimestampNs = surfaceTexture.getTimestamp(); long lastFrameTimestampNs = surfaceTexture.getTimestamp();
Long frameTimestampUs = sampleTimestampQueue.poll(lastFrameTimestampNs); @Nullable Long frameTimestampUs = sampleTimestampQueue.poll(lastFrameTimestampNs);
if (frameTimestampUs != null) { if (frameTimestampUs != null) {
this.frameTimestampUs = frameTimestampUs; this.frameTimestampUs = frameTimestampUs;
} }
surfaceTexture.getTransformMatrix(transformMatrix);
} }
videoProcessor.draw(texture, frameTimestampUs); videoProcessor.draw(texture, frameTimestampUs, transformMatrix);
} }
@Override @Override
......
This diff could not be displayed because it is too large.
...@@ -642,6 +642,7 @@ ...@@ -642,6 +642,7 @@
<li><a href="com/google/android/exoplayer2/metadata/id3/InternalFrame.html" title="class in com.google.android.exoplayer2.metadata.id3">InternalFrame</a></li> <li><a href="com/google/android/exoplayer2/metadata/id3/InternalFrame.html" title="class in com.google.android.exoplayer2.metadata.id3">InternalFrame</a></li>
<li><a href="com/google/android/exoplayer2/extractor/jpeg/JpegExtractor.html" title="class in com.google.android.exoplayer2.extractor.jpeg">JpegExtractor</a></li> <li><a href="com/google/android/exoplayer2/extractor/jpeg/JpegExtractor.html" title="class in com.google.android.exoplayer2.extractor.jpeg">JpegExtractor</a></li>
<li><a href="com/google/android/exoplayer2/drm/KeysExpiredException.html" title="class in com.google.android.exoplayer2.drm">KeysExpiredException</a></li> <li><a href="com/google/android/exoplayer2/drm/KeysExpiredException.html" title="class in com.google.android.exoplayer2.drm">KeysExpiredException</a></li>
<li><a href="com/google/android/exoplayer2/text/span/LanguageFeatureSpan.html" title="interface in com.google.android.exoplayer2.text.span"><span class="interfaceName">LanguageFeatureSpan</span></a></li>
<li><a href="com/google/android/exoplayer2/extractor/ts/LatmReader.html" title="class in com.google.android.exoplayer2.extractor.ts">LatmReader</a></li> <li><a href="com/google/android/exoplayer2/extractor/ts/LatmReader.html" title="class in com.google.android.exoplayer2.extractor.ts">LatmReader</a></li>
<li><a href="com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.html" title="class in com.google.android.exoplayer2.ext.leanback">LeanbackPlayerAdapter</a></li> <li><a href="com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.html" title="class in com.google.android.exoplayer2.ext.leanback">LeanbackPlayerAdapter</a></li>
<li><a href="com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.html" title="class in com.google.android.exoplayer2.upstream.cache">LeastRecentlyUsedCacheEvictor</a></li> <li><a href="com/google/android/exoplayer2/upstream/cache/LeastRecentlyUsedCacheEvictor.html" title="class in com.google.android.exoplayer2.upstream.cache">LeastRecentlyUsedCacheEvictor</a></li>
...@@ -713,6 +714,7 @@ ...@@ -713,6 +714,7 @@
<li><a href="com/google/android/exoplayer2/source/MediaLoadData.html" title="class in com.google.android.exoplayer2.source">MediaLoadData</a></li> <li><a href="com/google/android/exoplayer2/source/MediaLoadData.html" title="class in com.google.android.exoplayer2.source">MediaLoadData</a></li>
<li><a href="com/google/android/exoplayer2/MediaMetadata.html" title="class in com.google.android.exoplayer2">MediaMetadata</a></li> <li><a href="com/google/android/exoplayer2/MediaMetadata.html" title="class in com.google.android.exoplayer2">MediaMetadata</a></li>
<li><a href="com/google/android/exoplayer2/MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></li> <li><a href="com/google/android/exoplayer2/MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></li>
<li><a href="com/google/android/exoplayer2/MediaMetadata.FolderType.html" title="annotation in com.google.android.exoplayer2">MediaMetadata.FolderType</a></li>
<li><a href="com/google/android/exoplayer2/source/chunk/MediaParserChunkExtractor.html" title="class in com.google.android.exoplayer2.source.chunk">MediaParserChunkExtractor</a></li> <li><a href="com/google/android/exoplayer2/source/chunk/MediaParserChunkExtractor.html" title="class in com.google.android.exoplayer2.source.chunk">MediaParserChunkExtractor</a></li>
<li><a href="com/google/android/exoplayer2/source/MediaParserExtractorAdapter.html" title="class in com.google.android.exoplayer2.source">MediaParserExtractorAdapter</a></li> <li><a href="com/google/android/exoplayer2/source/MediaParserExtractorAdapter.html" title="class in com.google.android.exoplayer2.source">MediaParserExtractorAdapter</a></li>
<li><a href="com/google/android/exoplayer2/source/hls/MediaParserHlsMediaChunkExtractor.html" title="class in com.google.android.exoplayer2.source.hls">MediaParserHlsMediaChunkExtractor</a></li> <li><a href="com/google/android/exoplayer2/source/hls/MediaParserHlsMediaChunkExtractor.html" title="class in com.google.android.exoplayer2.source.hls">MediaParserHlsMediaChunkExtractor</a></li>
......
<!DOCTYPE HTML>
<!-- NewPage -->
<html lang="en">
<head><!-- start favicons snippet, use https://realfavicongenerator.net/ --><link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png"><link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png"><link rel="manifest" href="/assets/site.webmanifest"><link rel="mask-icon" href="/assets/safari-pinned-tab.svg" color="#fc4d50"><link rel="shortcut icon" href="/assets/favicon.ico"><meta name="msapplication-TileColor" content="#ffc40d"><meta name="msapplication-config" content="/assets/browserconfig.xml"><meta name="theme-color" content="#ffffff"><!-- end favicons snippet -->
<title>MediaMetadata.FolderType (ExoPlayer library)</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="../../../../stylesheet.css" title="Style">
<link rel="stylesheet" type="text/css" href="../../../../jquery/jquery-ui.css" title="Style">
<script type="text/javascript" src="../../../../script.js"></script>
<script type="text/javascript" src="../../../../jquery/jszip/dist/jszip.min.js"></script>
<script type="text/javascript" src="../../../../jquery/jszip-utils/dist/jszip-utils.min.js"></script>
<!--[if IE]>
<script type="text/javascript" src="../../../../jquery/jszip-utils/dist/jszip-utils-ie.min.js"></script>
<![endif]-->
<script type="text/javascript" src="../../../../jquery/jquery-3.5.1.js"></script>
<script type="text/javascript" src="../../../../jquery/jquery-ui.js"></script>
</head>
<body>
<script type="text/javascript"><!--
try {
if (location.href.indexOf('is-external=true') == -1) {
parent.document.title="MediaMetadata.FolderType (ExoPlayer library)";
}
}
catch(err) {
}
//-->
var pathtoroot = "../../../../";
var useModuleDirectories = false;
loadScripts(document, 'script');</script>
<noscript>
<div>JavaScript is disabled on your browser.</div>
</noscript>
<header role="banner">
<nav role="navigation">
<div class="fixedNav">
<!-- ========= START OF TOP NAVBAR ======= -->
<div class="topNav"><a id="navbar.top">
<!-- -->
</a>
<div class="skipNav"><a href="#skip.navbar.top" title="Skip navigation links">Skip navigation links</a></div>
<a id="navbar.top.firstrow">
<!-- -->
</a>
<ul class="navList" title="Navigation">
<li><a href="../../../../index.html">Overview</a></li>
<li><a href="package-summary.html">Package</a></li>
<li class="navBarCell1Rev">Class</li>
<li><a href="package-tree.html">Tree</a></li>
<li><a href="../../../../deprecated-list.html">Deprecated</a></li>
<li><a href="../../../../index-all.html">Index</a></li>
<li><a href="../../../../help-doc.html">Help</a></li>
</ul>
</div>
<div class="subNav">
<ul class="navList" id="allclasses_navbar_top">
<li><a href="../../../../allclasses.html">All&nbsp;Classes</a></li>
</ul>
<ul class="navListSearch">
<li><label for="search">SEARCH:</label>
<input type="text" id="search" value="search" disabled="disabled">
<input type="reset" id="reset" value="reset" disabled="disabled">
</li>
</ul>
<div>
<script type="text/javascript"><!--
allClassesLink = document.getElementById("allclasses_navbar_top");
if(window==top) {
allClassesLink.style.display = "block";
}
else {
allClassesLink.style.display = "none";
}
//-->
</script>
<noscript>
<div>JavaScript is disabled on your browser.</div>
</noscript>
</div>
<div>
<ul class="subNavList">
<li>Summary:&nbsp;</li>
<li>Field&nbsp;|&nbsp;</li>
<li>Required&nbsp;|&nbsp;</li>
<li>Optional</li>
</ul>
<ul class="subNavList">
<li>Detail:&nbsp;</li>
<li>Field&nbsp;|&nbsp;</li>
<li>Element</li>
</ul>
</div>
<a id="skip.navbar.top">
<!-- -->
</a></div>
<!-- ========= END OF TOP NAVBAR ========= -->
</div>
<div class="navPadding">&nbsp;</div>
<script type="text/javascript"><!--
$('.navPadding').css('padding-top', $('.fixedNav').css("height"));
//-->
</script>
</nav>
</header>
<!-- ======== START OF CLASS DATA ======== -->
<main role="main">
<div class="header">
<div class="subTitle"><span class="packageLabelInType">Package</span>&nbsp;<a href="package-summary.html">com.google.android.exoplayer2</a></div>
<h2 title="Annotation Type MediaMetadata.FolderType" class="title">Annotation Type MediaMetadata.FolderType</h2>
</div>
<div class="contentContainer">
<div class="description">
<ul class="blockList">
<li class="blockList">
<hr>
<pre><a href="https://developer.android.com/reference/java/lang/annotation/Documented.html" title="class or interface in java.lang.annotation" class="externalLink" target="_top">@Documented</a>
<a href="https://developer.android.com/reference/java/lang/annotation/Retention.html" title="class or interface in java.lang.annotation" class="externalLink">@Retention</a>(<a href="https://developer.android.com/reference/java/lang/annotation/RetentionPolicy.html?is-external=true#SOURCE" title="class or interface in java.lang.annotation" class="externalLink" target="_top">SOURCE</a>)
public static @interface <span class="memberNameLabel">MediaMetadata.FolderType</span></pre>
<div class="block">The folder type of the media item.
<p>This can be used as the type of a browsable bluetooth folder (see section 6.10.2.2 of the <a href="https://www.bluetooth.com/specifications/specs/a-v-remote-control-profile-1-6-2/">Bluetooth
AVRCP 1.6.2</a>).</div>
</li>
</ul>
</div>
</div>
</main>
<!-- ========= END OF CLASS DATA ========= -->
<footer role="contentinfo">
<nav role="navigation">
<!-- ======= START OF BOTTOM NAVBAR ====== -->
<div class="bottomNav"><a id="navbar.bottom">
<!-- -->
</a>
<div class="skipNav"><a href="#skip.navbar.bottom" title="Skip navigation links">Skip navigation links</a></div>
<a id="navbar.bottom.firstrow">
<!-- -->
</a>
<ul class="navList" title="Navigation">
<li><a href="../../../../index.html">Overview</a></li>
<li><a href="package-summary.html">Package</a></li>
<li class="navBarCell1Rev">Class</li>
<li><a href="package-tree.html">Tree</a></li>
<li><a href="../../../../deprecated-list.html">Deprecated</a></li>
<li><a href="../../../../index-all.html">Index</a></li>
<li><a href="../../../../help-doc.html">Help</a></li>
</ul>
</div>
<div class="subNav">
<ul class="navList" id="allclasses_navbar_bottom">
<li><a href="../../../../allclasses.html">All&nbsp;Classes</a></li>
</ul>
<div>
<script type="text/javascript"><!--
allClassesLink = document.getElementById("allclasses_navbar_bottom");
if(window==top) {
allClassesLink.style.display = "block";
}
else {
allClassesLink.style.display = "none";
}
//-->
</script>
<noscript>
<div>JavaScript is disabled on your browser.</div>
</noscript>
</div>
<div>
<ul class="subNavList">
<li>Summary:&nbsp;</li>
<li>Field&nbsp;|&nbsp;</li>
<li>Required&nbsp;|&nbsp;</li>
<li>Optional</li>
</ul>
<ul class="subNavList">
<li>Detail:&nbsp;</li>
<li>Field&nbsp;|&nbsp;</li>
<li>Element</li>
</ul>
</div>
<a id="skip.navbar.bottom">
<!-- -->
</a></div>
<!-- ======== END OF BOTTOM NAVBAR ======= -->
</nav>
</footer>
</body>
</html>
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
catch(err) { catch(err) {
} }
//--> //-->
var data = {"i0":10,"i1":10,"i2":10,"i3":10}; var data = {"i0":10,"i1":10,"i2":10,"i3":10,"i4":10};
var tabs = {65535:["t0","All Methods"],2:["t2","Instance Methods"],8:["t4","Concrete Methods"]}; var tabs = {65535:["t0","All Methods"],2:["t2","Instance Methods"],8:["t4","Concrete Methods"]};
var altColor = "altColor"; var altColor = "altColor";
var rowColor = "rowColor"; var rowColor = "rowColor";
...@@ -275,11 +275,18 @@ extends <a href="Id3Frame.html" title="class in com.google.android.exoplayer2.me ...@@ -275,11 +275,18 @@ extends <a href="Id3Frame.html" title="class in com.google.android.exoplayer2.me
<td class="colLast">&nbsp;</td> <td class="colLast">&nbsp;</td>
</tr> </tr>
<tr id="i2" class="altColor"> <tr id="i2" class="altColor">
<td class="colFirst"><code>void</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#populateMediaMetadata(com.google.android.exoplayer2.MediaMetadata.Builder)">populateMediaMetadata</a></span>&#8203;(<a href="../../MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a>&nbsp;builder)</code></th>
<td class="colLast">
<div class="block">Updates the <a href="../../MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2"><code>MediaMetadata.Builder</code></a> with the type specific values stored in this Entry.</div>
</td>
</tr>
<tr id="i3" class="rowColor">
<td class="colFirst"><code><a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td> <td class="colFirst"><code><a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#toString()">toString</a></span>()</code></th> <th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#toString()">toString</a></span>()</code></th>
<td class="colLast">&nbsp;</td> <td class="colLast">&nbsp;</td>
</tr> </tr>
<tr id="i3" class="rowColor"> <tr id="i4" class="altColor">
<td class="colFirst"><code>void</code></td> <td class="colFirst"><code>void</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#writeToParcel(android.os.Parcel,int)">writeToParcel</a></span>&#8203;(<a href="https://developer.android.com/reference/android/os/Parcel.html" title="class or interface in android.os" class="externalLink" target="_top">Parcel</a>&nbsp;dest, <th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#writeToParcel(android.os.Parcel,int)">writeToParcel</a></span>&#8203;(<a href="https://developer.android.com/reference/android/os/Parcel.html" title="class or interface in android.os" class="externalLink" target="_top">Parcel</a>&nbsp;dest,
int&nbsp;flags)</code></th> int&nbsp;flags)</code></th>
...@@ -305,7 +312,7 @@ extends <a href="Id3Frame.html" title="class in com.google.android.exoplayer2.me ...@@ -305,7 +312,7 @@ extends <a href="Id3Frame.html" title="class in com.google.android.exoplayer2.me
<!-- --> <!-- -->
</a> </a>
<h3>Methods inherited from interface&nbsp;com.google.android.exoplayer2.metadata.<a href="../Metadata.Entry.html" title="interface in com.google.android.exoplayer2.metadata">Metadata.Entry</a></h3> <h3>Methods inherited from interface&nbsp;com.google.android.exoplayer2.metadata.<a href="../Metadata.Entry.html" title="interface in com.google.android.exoplayer2.metadata">Metadata.Entry</a></h3>
<code><a href="../Metadata.Entry.html#getWrappedMetadataBytes()">getWrappedMetadataBytes</a>, <a href="../Metadata.Entry.html#getWrappedMetadataFormat()">getWrappedMetadataFormat</a>, <a href="../Metadata.Entry.html#populateMediaMetadata(com.google.android.exoplayer2.MediaMetadata.Builder)">populateMediaMetadata</a></code></li> <code><a href="../Metadata.Entry.html#getWrappedMetadataBytes()">getWrappedMetadataBytes</a>, <a href="../Metadata.Entry.html#getWrappedMetadataFormat()">getWrappedMetadataFormat</a></code></li>
</ul> </ul>
</li> </li>
</ul> </ul>
...@@ -415,6 +422,24 @@ public final&nbsp;<a href="https://developer.android.com/reference/java/lang/Str ...@@ -415,6 +422,24 @@ public final&nbsp;<a href="https://developer.android.com/reference/java/lang/Str
<!-- --> <!-- -->
</a> </a>
<h3>Method Detail</h3> <h3>Method Detail</h3>
<a id="populateMediaMetadata(com.google.android.exoplayer2.MediaMetadata.Builder)">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>populateMediaMetadata</h4>
<pre class="methodSignature">public&nbsp;void&nbsp;populateMediaMetadata&#8203;(<a href="../../MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a>&nbsp;builder)</pre>
<div class="block"><span class="descfrmTypeLabel">Description copied from interface:&nbsp;<code><a href="../Metadata.Entry.html#populateMediaMetadata(com.google.android.exoplayer2.MediaMetadata.Builder)">Metadata.Entry</a></code></span></div>
<div class="block">Updates the <a href="../../MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2"><code>MediaMetadata.Builder</code></a> with the type specific values stored in this Entry.
<p>The order of the <a href="../Metadata.Entry.html" title="interface in com.google.android.exoplayer2.metadata"><code>Metadata.Entry</code></a> objects in the <a href="../Metadata.html" title="class in com.google.android.exoplayer2.metadata"><code>Metadata</code></a> matters. If two <a href="../Metadata.Entry.html" title="interface in com.google.android.exoplayer2.metadata"><code>Metadata.Entry</code></a> entries attempt to populate the same <a href="../../MediaMetadata.html" title="class in com.google.android.exoplayer2"><code>MediaMetadata</code></a> field, then the last one in
the list is used.</div>
<dl>
<dt><span class="paramLabel">Parameters:</span></dt>
<dd><code>builder</code> - The builder to be updated.</dd>
</dl>
</li>
</ul>
<a id="equals(java.lang.Object)"> <a id="equals(java.lang.Object)">
<!-- --> <!-- -->
</a> </a>
......
...@@ -732,90 +732,96 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height")); ...@@ -732,90 +732,96 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
</td> </td>
</tr> </tr>
<tr class="altColor"> <tr class="altColor">
<th class="colFirst" scope="row"><a href="MediaMetadata.FolderType.html" title="annotation in com.google.android.exoplayer2">MediaMetadata.FolderType</a></th>
<td class="colLast">
<div class="block">The folder type of the media item.</div>
</td>
</tr>
<tr class="rowColor">
<th class="colFirst" scope="row"><a href="Player.Command.html" title="annotation in com.google.android.exoplayer2">Player.Command</a></th> <th class="colFirst" scope="row"><a href="Player.Command.html" title="annotation in com.google.android.exoplayer2">Player.Command</a></th>
<td class="colLast"> <td class="colLast">
<div class="block">Commands that can be executed on a <code>Player</code>.</div> <div class="block">Commands that can be executed on a <code>Player</code>.</div>
</td> </td>
</tr> </tr>
<tr class="rowColor"> <tr class="altColor">
<th class="colFirst" scope="row"><a href="Player.DiscontinuityReason.html" title="annotation in com.google.android.exoplayer2">Player.DiscontinuityReason</a></th> <th class="colFirst" scope="row"><a href="Player.DiscontinuityReason.html" title="annotation in com.google.android.exoplayer2">Player.DiscontinuityReason</a></th>
<td class="colLast"> <td class="colLast">
<div class="block">Reasons for position discontinuities.</div> <div class="block">Reasons for position discontinuities.</div>
</td> </td>
</tr> </tr>
<tr class="altColor"> <tr class="rowColor">
<th class="colFirst" scope="row"><a href="Player.EventFlags.html" title="annotation in com.google.android.exoplayer2">Player.EventFlags</a></th> <th class="colFirst" scope="row"><a href="Player.EventFlags.html" title="annotation in com.google.android.exoplayer2">Player.EventFlags</a></th>
<td class="colLast"> <td class="colLast">
<div class="block">Events that can be reported via <a href="Player.EventListener.html#onEvents(com.google.android.exoplayer2.Player,com.google.android.exoplayer2.Player.Events)"><code>Player.EventListener.onEvents(Player, Events)</code></a>.</div> <div class="block">Events that can be reported via <a href="Player.EventListener.html#onEvents(com.google.android.exoplayer2.Player,com.google.android.exoplayer2.Player.Events)"><code>Player.EventListener.onEvents(Player, Events)</code></a>.</div>
</td> </td>
</tr> </tr>
<tr class="rowColor"> <tr class="altColor">
<th class="colFirst" scope="row"><a href="Player.MediaItemTransitionReason.html" title="annotation in com.google.android.exoplayer2">Player.MediaItemTransitionReason</a></th> <th class="colFirst" scope="row"><a href="Player.MediaItemTransitionReason.html" title="annotation in com.google.android.exoplayer2">Player.MediaItemTransitionReason</a></th>
<td class="colLast"> <td class="colLast">
<div class="block">Reasons for media item transitions.</div> <div class="block">Reasons for media item transitions.</div>
</td> </td>
</tr> </tr>
<tr class="altColor"> <tr class="rowColor">
<th class="colFirst" scope="row"><a href="Player.PlaybackSuppressionReason.html" title="annotation in com.google.android.exoplayer2">Player.PlaybackSuppressionReason</a></th> <th class="colFirst" scope="row"><a href="Player.PlaybackSuppressionReason.html" title="annotation in com.google.android.exoplayer2">Player.PlaybackSuppressionReason</a></th>
<td class="colLast"> <td class="colLast">
<div class="block">Reason why playback is suppressed even though <a href="Player.html#getPlayWhenReady()"><code>Player.getPlayWhenReady()</code></a> is <code>true</code>.</div> <div class="block">Reason why playback is suppressed even though <a href="Player.html#getPlayWhenReady()"><code>Player.getPlayWhenReady()</code></a> is <code>true</code>.</div>
</td> </td>
</tr> </tr>
<tr class="rowColor"> <tr class="altColor">
<th class="colFirst" scope="row"><a href="Player.PlayWhenReadyChangeReason.html" title="annotation in com.google.android.exoplayer2">Player.PlayWhenReadyChangeReason</a></th> <th class="colFirst" scope="row"><a href="Player.PlayWhenReadyChangeReason.html" title="annotation in com.google.android.exoplayer2">Player.PlayWhenReadyChangeReason</a></th>
<td class="colLast"> <td class="colLast">
<div class="block">Reasons for <a href="Player.html#getPlayWhenReady()"><code>playWhenReady</code></a> changes.</div> <div class="block">Reasons for <a href="Player.html#getPlayWhenReady()"><code>playWhenReady</code></a> changes.</div>
</td> </td>
</tr> </tr>
<tr class="altColor"> <tr class="rowColor">
<th class="colFirst" scope="row"><a href="Player.RepeatMode.html" title="annotation in com.google.android.exoplayer2">Player.RepeatMode</a></th> <th class="colFirst" scope="row"><a href="Player.RepeatMode.html" title="annotation in com.google.android.exoplayer2">Player.RepeatMode</a></th>
<td class="colLast"> <td class="colLast">
<div class="block">Repeat modes for playback.</div> <div class="block">Repeat modes for playback.</div>
</td> </td>
</tr> </tr>
<tr class="rowColor"> <tr class="altColor">
<th class="colFirst" scope="row"><a href="Player.State.html" title="annotation in com.google.android.exoplayer2">Player.State</a></th> <th class="colFirst" scope="row"><a href="Player.State.html" title="annotation in com.google.android.exoplayer2">Player.State</a></th>
<td class="colLast"> <td class="colLast">
<div class="block">Playback state.</div> <div class="block">Playback state.</div>
</td> </td>
</tr> </tr>
<tr class="altColor"> <tr class="rowColor">
<th class="colFirst" scope="row"><a href="Player.TimelineChangeReason.html" title="annotation in com.google.android.exoplayer2">Player.TimelineChangeReason</a></th> <th class="colFirst" scope="row"><a href="Player.TimelineChangeReason.html" title="annotation in com.google.android.exoplayer2">Player.TimelineChangeReason</a></th>
<td class="colLast"> <td class="colLast">
<div class="block">Reasons for timeline changes.</div> <div class="block">Reasons for timeline changes.</div>
</td> </td>
</tr> </tr>
<tr class="rowColor"> <tr class="altColor">
<th class="colFirst" scope="row"><a href="Renderer.State.html" title="annotation in com.google.android.exoplayer2">Renderer.State</a></th> <th class="colFirst" scope="row"><a href="Renderer.State.html" title="annotation in com.google.android.exoplayer2">Renderer.State</a></th>
<td class="colLast"> <td class="colLast">
<div class="block">The renderer states.</div> <div class="block">The renderer states.</div>
</td> </td>
</tr> </tr>
<tr class="altColor"> <tr class="rowColor">
<th class="colFirst" scope="row"><a href="Renderer.VideoScalingMode.html" title="annotation in com.google.android.exoplayer2">Renderer.VideoScalingMode</a></th> <th class="colFirst" scope="row"><a href="Renderer.VideoScalingMode.html" title="annotation in com.google.android.exoplayer2">Renderer.VideoScalingMode</a></th>
<td class="colLast">Deprecated. <td class="colLast">Deprecated.
<div class="deprecationComment">Use <a href="C.VideoScalingMode.html" title="annotation in com.google.android.exoplayer2"><code>C.VideoScalingMode</code></a>.</div> <div class="deprecationComment">Use <a href="C.VideoScalingMode.html" title="annotation in com.google.android.exoplayer2"><code>C.VideoScalingMode</code></a>.</div>
</td> </td>
</tr> </tr>
<tr class="rowColor"> <tr class="altColor">
<th class="colFirst" scope="row"><a href="RendererCapabilities.AdaptiveSupport.html" title="annotation in com.google.android.exoplayer2">RendererCapabilities.AdaptiveSupport</a></th> <th class="colFirst" scope="row"><a href="RendererCapabilities.AdaptiveSupport.html" title="annotation in com.google.android.exoplayer2">RendererCapabilities.AdaptiveSupport</a></th>
<td class="colLast"> <td class="colLast">
<div class="block">Level of renderer support for adaptive format switches.</div> <div class="block">Level of renderer support for adaptive format switches.</div>
</td> </td>
</tr> </tr>
<tr class="altColor"> <tr class="rowColor">
<th class="colFirst" scope="row"><a href="RendererCapabilities.Capabilities.html" title="annotation in com.google.android.exoplayer2">RendererCapabilities.Capabilities</a></th> <th class="colFirst" scope="row"><a href="RendererCapabilities.Capabilities.html" title="annotation in com.google.android.exoplayer2">RendererCapabilities.Capabilities</a></th>
<td class="colLast"> <td class="colLast">
<div class="block">Combined renderer capabilities.</div> <div class="block">Combined renderer capabilities.</div>
</td> </td>
</tr> </tr>
<tr class="rowColor"> <tr class="altColor">
<th class="colFirst" scope="row"><a href="RendererCapabilities.FormatSupport.html" title="annotation in com.google.android.exoplayer2">RendererCapabilities.FormatSupport</a></th> <th class="colFirst" scope="row"><a href="RendererCapabilities.FormatSupport.html" title="annotation in com.google.android.exoplayer2">RendererCapabilities.FormatSupport</a></th>
<td class="colLast">Deprecated. <td class="colLast">Deprecated.
<div class="deprecationComment">Use <a href="C.FormatSupport.html" title="annotation in com.google.android.exoplayer2"><code>C.FormatSupport</code></a> instead.</div> <div class="deprecationComment">Use <a href="C.FormatSupport.html" title="annotation in com.google.android.exoplayer2"><code>C.FormatSupport</code></a> instead.</div>
</td> </td>
</tr> </tr>
<tr class="altColor"> <tr class="rowColor">
<th class="colFirst" scope="row"><a href="RendererCapabilities.TunnelingSupport.html" title="annotation in com.google.android.exoplayer2">RendererCapabilities.TunnelingSupport</a></th> <th class="colFirst" scope="row"><a href="RendererCapabilities.TunnelingSupport.html" title="annotation in com.google.android.exoplayer2">RendererCapabilities.TunnelingSupport</a></th>
<td class="colLast"> <td class="colLast">
<div class="block">Level of renderer support for tunneling.</div> <div class="block">Level of renderer support for tunneling.</div>
......
...@@ -280,6 +280,7 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height")); ...@@ -280,6 +280,7 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<li class="circle">com.google.android.exoplayer2.<a href="DefaultRenderersFactory.ExtensionRendererMode.html" title="annotation in com.google.android.exoplayer2"><span class="typeNameLink">DefaultRenderersFactory.ExtensionRendererMode</span></a> (implements java.lang.annotation.<a href="https://developer.android.com/reference/java/lang/annotation/Annotation.html" title="class or interface in java.lang.annotation" class="externalLink" target="_top">Annotation</a>)</li> <li class="circle">com.google.android.exoplayer2.<a href="DefaultRenderersFactory.ExtensionRendererMode.html" title="annotation in com.google.android.exoplayer2"><span class="typeNameLink">DefaultRenderersFactory.ExtensionRendererMode</span></a> (implements java.lang.annotation.<a href="https://developer.android.com/reference/java/lang/annotation/Annotation.html" title="class or interface in java.lang.annotation" class="externalLink" target="_top">Annotation</a>)</li>
<li class="circle">com.google.android.exoplayer2.<a href="ExoPlaybackException.Type.html" title="annotation in com.google.android.exoplayer2"><span class="typeNameLink">ExoPlaybackException.Type</span></a> (implements java.lang.annotation.<a href="https://developer.android.com/reference/java/lang/annotation/Annotation.html" title="class or interface in java.lang.annotation" class="externalLink" target="_top">Annotation</a>)</li> <li class="circle">com.google.android.exoplayer2.<a href="ExoPlaybackException.Type.html" title="annotation in com.google.android.exoplayer2"><span class="typeNameLink">ExoPlaybackException.Type</span></a> (implements java.lang.annotation.<a href="https://developer.android.com/reference/java/lang/annotation/Annotation.html" title="class or interface in java.lang.annotation" class="externalLink" target="_top">Annotation</a>)</li>
<li class="circle">com.google.android.exoplayer2.<a href="ExoTimeoutException.TimeoutOperation.html" title="annotation in com.google.android.exoplayer2"><span class="typeNameLink">ExoTimeoutException.TimeoutOperation</span></a> (implements java.lang.annotation.<a href="https://developer.android.com/reference/java/lang/annotation/Annotation.html" title="class or interface in java.lang.annotation" class="externalLink" target="_top">Annotation</a>)</li> <li class="circle">com.google.android.exoplayer2.<a href="ExoTimeoutException.TimeoutOperation.html" title="annotation in com.google.android.exoplayer2"><span class="typeNameLink">ExoTimeoutException.TimeoutOperation</span></a> (implements java.lang.annotation.<a href="https://developer.android.com/reference/java/lang/annotation/Annotation.html" title="class or interface in java.lang.annotation" class="externalLink" target="_top">Annotation</a>)</li>
<li class="circle">com.google.android.exoplayer2.<a href="MediaMetadata.FolderType.html" title="annotation in com.google.android.exoplayer2"><span class="typeNameLink">MediaMetadata.FolderType</span></a> (implements java.lang.annotation.<a href="https://developer.android.com/reference/java/lang/annotation/Annotation.html" title="class or interface in java.lang.annotation" class="externalLink" target="_top">Annotation</a>)</li>
<li class="circle">com.google.android.exoplayer2.<a href="Player.Command.html" title="annotation in com.google.android.exoplayer2"><span class="typeNameLink">Player.Command</span></a> (implements java.lang.annotation.<a href="https://developer.android.com/reference/java/lang/annotation/Annotation.html" title="class or interface in java.lang.annotation" class="externalLink" target="_top">Annotation</a>)</li> <li class="circle">com.google.android.exoplayer2.<a href="Player.Command.html" title="annotation in com.google.android.exoplayer2"><span class="typeNameLink">Player.Command</span></a> (implements java.lang.annotation.<a href="https://developer.android.com/reference/java/lang/annotation/Annotation.html" title="class or interface in java.lang.annotation" class="externalLink" target="_top">Annotation</a>)</li>
<li class="circle">com.google.android.exoplayer2.<a href="Player.DiscontinuityReason.html" title="annotation in com.google.android.exoplayer2"><span class="typeNameLink">Player.DiscontinuityReason</span></a> (implements java.lang.annotation.<a href="https://developer.android.com/reference/java/lang/annotation/Annotation.html" title="class or interface in java.lang.annotation" class="externalLink" target="_top">Annotation</a>)</li> <li class="circle">com.google.android.exoplayer2.<a href="Player.DiscontinuityReason.html" title="annotation in com.google.android.exoplayer2"><span class="typeNameLink">Player.DiscontinuityReason</span></a> (implements java.lang.annotation.<a href="https://developer.android.com/reference/java/lang/annotation/Annotation.html" title="class or interface in java.lang.annotation" class="externalLink" target="_top">Annotation</a>)</li>
<li class="circle">com.google.android.exoplayer2.<a href="Player.EventFlags.html" title="annotation in com.google.android.exoplayer2"><span class="typeNameLink">Player.EventFlags</span></a> (implements java.lang.annotation.<a href="https://developer.android.com/reference/java/lang/annotation/Annotation.html" title="class or interface in java.lang.annotation" class="externalLink" target="_top">Annotation</a>)</li> <li class="circle">com.google.android.exoplayer2.<a href="Player.EventFlags.html" title="annotation in com.google.android.exoplayer2"><span class="typeNameLink">Player.EventFlags</span></a> (implements java.lang.annotation.<a href="https://developer.android.com/reference/java/lang/annotation/Annotation.html" title="class or interface in java.lang.annotation" class="externalLink" target="_top">Annotation</a>)</li>
......
...@@ -296,62 +296,70 @@ extends <a href="HlsPlaylist.html" title="class in com.google.android.exoplayer2 ...@@ -296,62 +296,70 @@ extends <a href="HlsPlaylist.html" title="class in com.google.android.exoplayer2
</td> </td>
</tr> </tr>
<tr class="rowColor"> <tr class="rowColor">
<td class="colFirst"><code>boolean</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#preciseStart">preciseStart</a></span></code></th>
<td class="colLast">
<div class="block">Whether the start position should be precise, as defined by #EXT-X-START.</div>
</td>
</tr>
<tr class="altColor">
<td class="colFirst"><code><a href="../../../drm/DrmInitData.html" title="class in com.google.android.exoplayer2.drm">DrmInitData</a></code></td> <td class="colFirst"><code><a href="../../../drm/DrmInitData.html" title="class in com.google.android.exoplayer2.drm">DrmInitData</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#protectionSchemes">protectionSchemes</a></span></code></th> <th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#protectionSchemes">protectionSchemes</a></span></code></th>
<td class="colLast"> <td class="colLast">
<div class="block">Contains the CDM protection schemes used by segments in this playlist.</div> <div class="block">Contains the CDM protection schemes used by segments in this playlist.</div>
</td> </td>
</tr> </tr>
<tr class="altColor"> <tr class="rowColor">
<td class="colFirst"><code><a href="https://developer.android.com/reference/java/util/Map.html" title="class or interface in java.util" class="externalLink">Map</a>&lt;<a href="https://developer.android.com/reference/android/net/Uri.html?is-external=true" title="class or interface in android.net" class="externalLink">Uri</a>,&#8203;<a href="HlsMediaPlaylist.RenditionReport.html" title="class in com.google.android.exoplayer2.source.hls.playlist" target="_top">HlsMediaPlaylist.RenditionReport</a>&gt;</code></td> <td class="colFirst"><code><a href="https://developer.android.com/reference/java/util/Map.html" title="class or interface in java.util" class="externalLink">Map</a>&lt;<a href="https://developer.android.com/reference/android/net/Uri.html?is-external=true" title="class or interface in android.net" class="externalLink">Uri</a>,&#8203;<a href="HlsMediaPlaylist.RenditionReport.html" title="class in com.google.android.exoplayer2.source.hls.playlist" target="_top">HlsMediaPlaylist.RenditionReport</a>&gt;</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#renditionReports">renditionReports</a></span></code></th> <th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#renditionReports">renditionReports</a></span></code></th>
<td class="colLast"> <td class="colLast">
<div class="block">The rendition reports of alternative rendition playlists.</div> <div class="block">The rendition reports of alternative rendition playlists.</div>
</td> </td>
</tr> </tr>
<tr class="rowColor"> <tr class="altColor">
<td class="colFirst"><code><a href="https://developer.android.com/reference/java/util/List.html" title="class or interface in java.util" class="externalLink">List</a>&lt;<a href="HlsMediaPlaylist.Segment.html" title="class in com.google.android.exoplayer2.source.hls.playlist" target="_top">HlsMediaPlaylist.Segment</a>&gt;</code></td> <td class="colFirst"><code><a href="https://developer.android.com/reference/java/util/List.html" title="class or interface in java.util" class="externalLink">List</a>&lt;<a href="HlsMediaPlaylist.Segment.html" title="class in com.google.android.exoplayer2.source.hls.playlist" target="_top">HlsMediaPlaylist.Segment</a>&gt;</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#segments">segments</a></span></code></th> <th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#segments">segments</a></span></code></th>
<td class="colLast"> <td class="colLast">
<div class="block">The list of segments in the playlist.</div> <div class="block">The list of segments in the playlist.</div>
</td> </td>
</tr> </tr>
<tr class="altColor"> <tr class="rowColor">
<td class="colFirst"><code><a href="HlsMediaPlaylist.ServerControl.html" title="class in com.google.android.exoplayer2.source.hls.playlist">HlsMediaPlaylist.ServerControl</a></code></td> <td class="colFirst"><code><a href="HlsMediaPlaylist.ServerControl.html" title="class in com.google.android.exoplayer2.source.hls.playlist">HlsMediaPlaylist.ServerControl</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#serverControl">serverControl</a></span></code></th> <th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#serverControl">serverControl</a></span></code></th>
<td class="colLast"> <td class="colLast">
<div class="block">The attributes of the #EXT-X-SERVER-CONTROL header.</div> <div class="block">The attributes of the #EXT-X-SERVER-CONTROL header.</div>
</td> </td>
</tr> </tr>
<tr class="rowColor"> <tr class="altColor">
<td class="colFirst"><code>long</code></td> <td class="colFirst"><code>long</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#startOffsetUs">startOffsetUs</a></span></code></th> <th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#startOffsetUs">startOffsetUs</a></span></code></th>
<td class="colLast"> <td class="colLast">
<div class="block">The start offset in microseconds, as defined by #EXT-X-START.</div> <div class="block">The start offset in microseconds from the beginning of the playlist, as defined by
#EXT-X-START, or <a href="../../../C.html#TIME_UNSET"><code>C.TIME_UNSET</code></a> if undefined.</div>
</td> </td>
</tr> </tr>
<tr class="altColor"> <tr class="rowColor">
<td class="colFirst"><code>long</code></td> <td class="colFirst"><code>long</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#startTimeUs">startTimeUs</a></span></code></th> <th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#startTimeUs">startTimeUs</a></span></code></th>
<td class="colLast"> <td class="colLast">
<div class="block">If <a href="#hasProgramDateTime"><code>hasProgramDateTime</code></a> is true, contains the datetime as microseconds since epoch.</div> <div class="block">If <a href="#hasProgramDateTime"><code>hasProgramDateTime</code></a> is true, contains the datetime as microseconds since epoch.</div>
</td> </td>
</tr> </tr>
<tr class="rowColor"> <tr class="altColor">
<td class="colFirst"><code>long</code></td> <td class="colFirst"><code>long</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#targetDurationUs">targetDurationUs</a></span></code></th> <th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#targetDurationUs">targetDurationUs</a></span></code></th>
<td class="colLast"> <td class="colLast">
<div class="block">The target duration in microseconds, as defined by #EXT-X-TARGETDURATION.</div> <div class="block">The target duration in microseconds, as defined by #EXT-X-TARGETDURATION.</div>
</td> </td>
</tr> </tr>
<tr class="altColor"> <tr class="rowColor">
<td class="colFirst"><code><a href="https://developer.android.com/reference/java/util/List.html" title="class or interface in java.util" class="externalLink">List</a>&lt;<a href="HlsMediaPlaylist.Part.html" title="class in com.google.android.exoplayer2.source.hls.playlist" target="_top">HlsMediaPlaylist.Part</a>&gt;</code></td> <td class="colFirst"><code><a href="https://developer.android.com/reference/java/util/List.html" title="class or interface in java.util" class="externalLink">List</a>&lt;<a href="HlsMediaPlaylist.Part.html" title="class in com.google.android.exoplayer2.source.hls.playlist" target="_top">HlsMediaPlaylist.Part</a>&gt;</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#trailingParts">trailingParts</a></span></code></th> <th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#trailingParts">trailingParts</a></span></code></th>
<td class="colLast"> <td class="colLast">
<div class="block">The list of parts at the end of the playlist for which the segment is not in the playlist yet.</div> <div class="block">The list of parts at the end of the playlist for which the segment is not in the playlist yet.</div>
</td> </td>
</tr> </tr>
<tr class="rowColor"> <tr class="altColor">
<td class="colFirst"><code>int</code></td> <td class="colFirst"><code>int</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#version">version</a></span></code></th> <th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#version">version</a></span></code></th>
<td class="colLast"> <td class="colLast">
...@@ -383,10 +391,11 @@ extends <a href="HlsPlaylist.html" title="class in com.google.android.exoplayer2 ...@@ -383,10 +391,11 @@ extends <a href="HlsPlaylist.html" title="class in com.google.android.exoplayer2
<th class="colLast" scope="col">Description</th> <th class="colLast" scope="col">Description</th>
</tr> </tr>
<tr class="altColor"> <tr class="altColor">
<th class="colConstructorName" scope="row"><code><span class="memberNameLink"><a href="#%3Cinit%3E(int,java.lang.String,java.util.List,long,long,boolean,int,long,int,long,long,boolean,boolean,boolean,com.google.android.exoplayer2.drm.DrmInitData,java.util.List,java.util.List,com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.ServerControl,java.util.Map)">HlsMediaPlaylist</a></span>&#8203;(int&nbsp;playlistType, <th class="colConstructorName" scope="row"><code><span class="memberNameLink"><a href="#%3Cinit%3E(int,java.lang.String,java.util.List,long,boolean,long,boolean,int,long,int,long,long,boolean,boolean,boolean,com.google.android.exoplayer2.drm.DrmInitData,java.util.List,java.util.List,com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.ServerControl,java.util.Map)">HlsMediaPlaylist</a></span>&#8203;(int&nbsp;playlistType,
<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a>&nbsp;baseUri, <a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a>&nbsp;baseUri,
<a href="https://developer.android.com/reference/java/util/List.html" title="class or interface in java.util" class="externalLink">List</a>&lt;<a href="https://developer.android.com/reference/java/lang/String.html?is-external=true" title="class or interface in java.lang" class="externalLink" target="_top">String</a>&gt;&nbsp;tags, <a href="https://developer.android.com/reference/java/util/List.html" title="class or interface in java.util" class="externalLink">List</a>&lt;<a href="https://developer.android.com/reference/java/lang/String.html?is-external=true" title="class or interface in java.lang" class="externalLink" target="_top">String</a>&gt;&nbsp;tags,
long&nbsp;startOffsetUs, long&nbsp;startOffsetUs,
boolean&nbsp;preciseStart,
long&nbsp;startTimeUs, long&nbsp;startTimeUs,
boolean&nbsp;hasDiscontinuitySequence, boolean&nbsp;hasDiscontinuitySequence,
int&nbsp;discontinuitySequence, int&nbsp;discontinuitySequence,
...@@ -540,7 +549,19 @@ public final&nbsp;int playlistType</pre> ...@@ -540,7 +549,19 @@ public final&nbsp;int playlistType</pre>
<li class="blockList"> <li class="blockList">
<h4>startOffsetUs</h4> <h4>startOffsetUs</h4>
<pre>public final&nbsp;long startOffsetUs</pre> <pre>public final&nbsp;long startOffsetUs</pre>
<div class="block">The start offset in microseconds, as defined by #EXT-X-START.</div> <div class="block">The start offset in microseconds from the beginning of the playlist, as defined by
#EXT-X-START, or <a href="../../../C.html#TIME_UNSET"><code>C.TIME_UNSET</code></a> if undefined. The value is guaranteed to be between 0 and
<a href="#durationUs"><code>durationUs</code></a>, inclusive.</div>
</li>
</ul>
<a id="preciseStart">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>preciseStart</h4>
<pre>public final&nbsp;boolean preciseStart</pre>
<div class="block">Whether the start position should be precise, as defined by #EXT-X-START.</div>
</li> </li>
</ul> </ul>
<a id="startTimeUs"> <a id="startTimeUs">
...@@ -710,7 +731,7 @@ public final&nbsp;<a href="../../../drm/DrmInitData.html" title="class in com.go ...@@ -710,7 +731,7 @@ public final&nbsp;<a href="../../../drm/DrmInitData.html" title="class in com.go
<!-- --> <!-- -->
</a> </a>
<h3>Constructor Detail</h3> <h3>Constructor Detail</h3>
<a id="&lt;init&gt;(int,java.lang.String,java.util.List,long,long,boolean,int,long,int,long,long,boolean,boolean,boolean,com.google.android.exoplayer2.drm.DrmInitData,java.util.List,java.util.List,com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.ServerControl,java.util.Map)"> <a id="&lt;init&gt;(int,java.lang.String,java.util.List,long,boolean,long,boolean,int,long,int,long,long,boolean,boolean,boolean,com.google.android.exoplayer2.drm.DrmInitData,java.util.List,java.util.List,com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.ServerControl,java.util.Map)">
<!-- --> <!-- -->
</a> </a>
<ul class="blockListLast"> <ul class="blockListLast">
...@@ -721,6 +742,7 @@ public final&nbsp;<a href="../../../drm/DrmInitData.html" title="class in com.go ...@@ -721,6 +742,7 @@ public final&nbsp;<a href="../../../drm/DrmInitData.html" title="class in com.go
<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a>&nbsp;baseUri, <a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a>&nbsp;baseUri,
<a href="https://developer.android.com/reference/java/util/List.html" title="class or interface in java.util" class="externalLink">List</a>&lt;<a href="https://developer.android.com/reference/java/lang/String.html?is-external=true" title="class or interface in java.lang" class="externalLink" target="_top">String</a>&gt;&nbsp;tags, <a href="https://developer.android.com/reference/java/util/List.html" title="class or interface in java.util" class="externalLink">List</a>&lt;<a href="https://developer.android.com/reference/java/lang/String.html?is-external=true" title="class or interface in java.lang" class="externalLink" target="_top">String</a>&gt;&nbsp;tags,
long&nbsp;startOffsetUs, long&nbsp;startOffsetUs,
boolean&nbsp;preciseStart,
long&nbsp;startTimeUs, long&nbsp;startTimeUs,
boolean&nbsp;hasDiscontinuitySequence, boolean&nbsp;hasDiscontinuitySequence,
int&nbsp;discontinuitySequence, int&nbsp;discontinuitySequence,
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
catch(err) { catch(err) {
} }
//--> //-->
var data = {"i0":10,"i1":10,"i2":42,"i3":42,"i4":10,"i5":42,"i6":10}; var data = {"i0":10,"i1":10,"i2":42,"i3":42,"i4":10,"i5":42,"i6":10,"i7":10,"i8":10};
var tabs = {65535:["t0","All Methods"],2:["t2","Instance Methods"],8:["t4","Concrete Methods"],32:["t6","Deprecated Methods"]}; var tabs = {65535:["t0","All Methods"],2:["t2","Instance Methods"],8:["t4","Concrete Methods"],32:["t6","Deprecated Methods"]};
var altColor = "altColor"; var altColor = "altColor";
var rowColor = "rowColor"; var rowColor = "rowColor";
...@@ -243,11 +243,25 @@ implements <a href="../MediaSourceFactory.html" title="interface in com.google.a ...@@ -243,11 +243,25 @@ implements <a href="../MediaSourceFactory.html" title="interface in com.google.a
</tr> </tr>
<tr id="i6" class="altColor"> <tr id="i6" class="altColor">
<td class="colFirst"><code><a href="RtspMediaSource.Factory.html" title="class in com.google.android.exoplayer2.source.rtsp">RtspMediaSource.Factory</a></code></td> <td class="colFirst"><code><a href="RtspMediaSource.Factory.html" title="class in com.google.android.exoplayer2.source.rtsp">RtspMediaSource.Factory</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setForceUseRtpTcp(boolean)">setForceUseRtpTcp</a></span>&#8203;(boolean&nbsp;forceUseRtpTcp)</code></th>
<td class="colLast">
<div class="block">Sets whether to force using TCP as the default RTP transport.</div>
</td>
</tr>
<tr id="i7" class="rowColor">
<td class="colFirst"><code><a href="RtspMediaSource.Factory.html" title="class in com.google.android.exoplayer2.source.rtsp">RtspMediaSource.Factory</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setLoadErrorHandlingPolicy(com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy)">setLoadErrorHandlingPolicy</a></span>&#8203;(<a href="../../upstream/LoadErrorHandlingPolicy.html" title="interface in com.google.android.exoplayer2.upstream">LoadErrorHandlingPolicy</a>&nbsp;loadErrorHandlingPolicy)</code></th> <th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setLoadErrorHandlingPolicy(com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy)">setLoadErrorHandlingPolicy</a></span>&#8203;(<a href="../../upstream/LoadErrorHandlingPolicy.html" title="interface in com.google.android.exoplayer2.upstream">LoadErrorHandlingPolicy</a>&nbsp;loadErrorHandlingPolicy)</code></th>
<td class="colLast"> <td class="colLast">
<div class="block">Does nothing.</div> <div class="block">Does nothing.</div>
</td> </td>
</tr> </tr>
<tr id="i8" class="altColor">
<td class="colFirst"><code><a href="RtspMediaSource.Factory.html" title="class in com.google.android.exoplayer2.source.rtsp">RtspMediaSource.Factory</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setUserAgent(java.lang.String)">setUserAgent</a></span>&#8203;(<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a>&nbsp;userAgent)</code></th>
<td class="colLast">
<div class="block">Sets the user agent, the default value is <a href="../../ExoPlayerLibraryInfo.html#VERSION_SLASHY"><code>ExoPlayerLibraryInfo.VERSION_SLASHY</code></a>.</div>
</td>
</tr>
</table> </table>
<ul class="blockList"> <ul class="blockList">
<li class="blockList"><a id="methods.inherited.from.class.java.lang.Object"> <li class="blockList"><a id="methods.inherited.from.class.java.lang.Object">
...@@ -298,6 +312,43 @@ implements <a href="../MediaSourceFactory.html" title="interface in com.google.a ...@@ -298,6 +312,43 @@ implements <a href="../MediaSourceFactory.html" title="interface in com.google.a
<!-- --> <!-- -->
</a> </a>
<h3>Method Detail</h3> <h3>Method Detail</h3>
<a id="setForceUseRtpTcp(boolean)">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>setForceUseRtpTcp</h4>
<pre class="methodSignature">public&nbsp;<a href="RtspMediaSource.Factory.html" title="class in com.google.android.exoplayer2.source.rtsp">RtspMediaSource.Factory</a>&nbsp;setForceUseRtpTcp&#8203;(boolean&nbsp;forceUseRtpTcp)</pre>
<div class="block">Sets whether to force using TCP as the default RTP transport.
<p>The default value is <code>false</code>, the source will first try streaming RTSP with UDP. If
no data is received on the UDP channel (for instance, when streaming behind a NAT) for a
while, the source will switch to streaming using TCP. If this value is set to <code>true</code>,
the source will always use TCP for streaming.</div>
<dl>
<dt><span class="paramLabel">Parameters:</span></dt>
<dd><code>forceUseRtpTcp</code> - Whether force to use TCP for streaming.</dd>
<dt><span class="returnLabel">Returns:</span></dt>
<dd>This Factory, for convenience.</dd>
</dl>
</li>
</ul>
<a id="setUserAgent(java.lang.String)">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>setUserAgent</h4>
<pre class="methodSignature">public&nbsp;<a href="RtspMediaSource.Factory.html" title="class in com.google.android.exoplayer2.source.rtsp">RtspMediaSource.Factory</a>&nbsp;setUserAgent&#8203;(<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a>&nbsp;userAgent)</pre>
<div class="block">Sets the user agent, the default value is <a href="../../ExoPlayerLibraryInfo.html#VERSION_SLASHY"><code>ExoPlayerLibraryInfo.VERSION_SLASHY</code></a>.</div>
<dl>
<dt><span class="paramLabel">Parameters:</span></dt>
<dd><code>userAgent</code> - The user agent.</dd>
<dt><span class="returnLabel">Returns:</span></dt>
<dd>This Factory, for convenience.</dd>
</dl>
</li>
</ul>
<a id="setDrmSessionManagerProvider(com.google.android.exoplayer2.drm.DrmSessionManagerProvider)"> <a id="setDrmSessionManagerProvider(com.google.android.exoplayer2.drm.DrmSessionManagerProvider)">
<!-- --> <!-- -->
</a> </a>
......
...@@ -338,18 +338,13 @@ extends <a href="../BaseMediaSource.html" title="class in com.google.android.exo ...@@ -338,18 +338,13 @@ extends <a href="../BaseMediaSource.html" title="class in com.google.android.exo
<ul class="blockList"> <ul class="blockList">
<li class="blockList"> <li class="blockList">
<h4>maybeThrowSourceInfoRefreshError</h4> <h4>maybeThrowSourceInfoRefreshError</h4>
<pre class="methodSignature">public&nbsp;void&nbsp;maybeThrowSourceInfoRefreshError() <pre class="methodSignature">public&nbsp;void&nbsp;maybeThrowSourceInfoRefreshError()</pre>
throws <a href="https://developer.android.com/reference/java/io/IOException.html" title="class or interface in java.io" class="externalLink" target="_top">IOException</a></pre>
<div class="block"><span class="descfrmTypeLabel">Description copied from interface:&nbsp;<code><a href="../MediaSource.html#maybeThrowSourceInfoRefreshError()">MediaSource</a></code></span></div> <div class="block"><span class="descfrmTypeLabel">Description copied from interface:&nbsp;<code><a href="../MediaSource.html#maybeThrowSourceInfoRefreshError()">MediaSource</a></code></span></div>
<div class="block">Throws any pending error encountered while loading or refreshing source information. <div class="block">Throws any pending error encountered while loading or refreshing source information.
<p>Should not be called directly from application code. <p>Should not be called directly from application code.
<p>Must only be called after <a href="../MediaSource.html#prepareSource(com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller,com.google.android.exoplayer2.upstream.TransferListener)"><code>MediaSource.prepareSource(MediaSourceCaller, TransferListener)</code></a>.</div> <p>Must only be called after <a href="../MediaSource.html#prepareSource(com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller,com.google.android.exoplayer2.upstream.TransferListener)"><code>MediaSource.prepareSource(MediaSourceCaller, TransferListener)</code></a>.</div>
<dl>
<dt><span class="throwsLabel">Throws:</span></dt>
<dd><code><a href="https://developer.android.com/reference/java/io/IOException.html" title="class or interface in java.io" class="externalLink" target="_top">IOException</a></code></dd>
</dl>
</li> </li>
</ul> </ul>
<a id="createPeriod(com.google.android.exoplayer2.source.MediaSource.MediaPeriodId,com.google.android.exoplayer2.upstream.Allocator,long)"> <a id="createPeriod(com.google.android.exoplayer2.source.MediaSource.MediaPeriodId,com.google.android.exoplayer2.upstream.Allocator,long)">
......
...@@ -122,9 +122,14 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height")); ...@@ -122,9 +122,14 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<div class="description"> <div class="description">
<ul class="blockList"> <ul class="blockList">
<li class="blockList"> <li class="blockList">
<dl>
<dt>All Implemented Interfaces:</dt>
<dd><code><a href="LanguageFeatureSpan.html" title="interface in com.google.android.exoplayer2.text.span">LanguageFeatureSpan</a></code></dd>
</dl>
<hr> <hr>
<pre>public final class <span class="typeNameLabel">HorizontalTextInVerticalContextSpan</span> <pre>public final class <span class="typeNameLabel">HorizontalTextInVerticalContextSpan</span>
extends <a href="https://developer.android.com/reference/java/lang/Object.html" title="class or interface in java.lang" class="externalLink" target="_top">Object</a></pre> extends <a href="https://developer.android.com/reference/java/lang/Object.html" title="class or interface in java.lang" class="externalLink" target="_top">Object</a>
implements <a href="LanguageFeatureSpan.html" title="interface in com.google.android.exoplayer2.text.span">LanguageFeatureSpan</a></pre>
<div class="block">A styling span for horizontal text in a vertical context. <div class="block">A styling span for horizontal text in a vertical context.
<p>This is used in vertical text to write some characters in a horizontal orientation, known in <p>This is used in vertical text to write some characters in a horizontal orientation, known in
......
<!DOCTYPE HTML>
<!-- NewPage -->
<html lang="en">
<head><!-- start favicons snippet, use https://realfavicongenerator.net/ --><link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png"><link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png"><link rel="manifest" href="/assets/site.webmanifest"><link rel="mask-icon" href="/assets/safari-pinned-tab.svg" color="#fc4d50"><link rel="shortcut icon" href="/assets/favicon.ico"><meta name="msapplication-TileColor" content="#ffc40d"><meta name="msapplication-config" content="/assets/browserconfig.xml"><meta name="theme-color" content="#ffffff"><!-- end favicons snippet -->
<title>LanguageFeatureSpan (ExoPlayer library)</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="../../../../../../stylesheet.css" title="Style">
<link rel="stylesheet" type="text/css" href="../../../../../../jquery/jquery-ui.css" title="Style">
<script type="text/javascript" src="../../../../../../script.js"></script>
<script type="text/javascript" src="../../../../../../jquery/jszip/dist/jszip.min.js"></script>
<script type="text/javascript" src="../../../../../../jquery/jszip-utils/dist/jszip-utils.min.js"></script>
<!--[if IE]>
<script type="text/javascript" src="../../../../../../jquery/jszip-utils/dist/jszip-utils-ie.min.js"></script>
<![endif]-->
<script type="text/javascript" src="../../../../../../jquery/jquery-3.5.1.js"></script>
<script type="text/javascript" src="../../../../../../jquery/jquery-ui.js"></script>
</head>
<body>
<script type="text/javascript"><!--
try {
if (location.href.indexOf('is-external=true') == -1) {
parent.document.title="LanguageFeatureSpan (ExoPlayer library)";
}
}
catch(err) {
}
//-->
var pathtoroot = "../../../../../../";
var useModuleDirectories = false;
loadScripts(document, 'script');</script>
<noscript>
<div>JavaScript is disabled on your browser.</div>
</noscript>
<header role="banner">
<nav role="navigation">
<div class="fixedNav">
<!-- ========= START OF TOP NAVBAR ======= -->
<div class="topNav"><a id="navbar.top">
<!-- -->
</a>
<div class="skipNav"><a href="#skip.navbar.top" title="Skip navigation links">Skip navigation links</a></div>
<a id="navbar.top.firstrow">
<!-- -->
</a>
<ul class="navList" title="Navigation">
<li><a href="../../../../../../index.html">Overview</a></li>
<li><a href="package-summary.html">Package</a></li>
<li class="navBarCell1Rev">Class</li>
<li><a href="package-tree.html">Tree</a></li>
<li><a href="../../../../../../deprecated-list.html">Deprecated</a></li>
<li><a href="../../../../../../index-all.html">Index</a></li>
<li><a href="../../../../../../help-doc.html">Help</a></li>
</ul>
</div>
<div class="subNav">
<ul class="navList" id="allclasses_navbar_top">
<li><a href="../../../../../../allclasses.html">All&nbsp;Classes</a></li>
</ul>
<ul class="navListSearch">
<li><label for="search">SEARCH:</label>
<input type="text" id="search" value="search" disabled="disabled">
<input type="reset" id="reset" value="reset" disabled="disabled">
</li>
</ul>
<div>
<script type="text/javascript"><!--
allClassesLink = document.getElementById("allclasses_navbar_top");
if(window==top) {
allClassesLink.style.display = "block";
}
else {
allClassesLink.style.display = "none";
}
//-->
</script>
<noscript>
<div>JavaScript is disabled on your browser.</div>
</noscript>
</div>
<div>
<ul class="subNavList">
<li>Summary:&nbsp;</li>
<li>Nested&nbsp;|&nbsp;</li>
<li>Field&nbsp;|&nbsp;</li>
<li>Constr&nbsp;|&nbsp;</li>
<li>Method</li>
</ul>
<ul class="subNavList">
<li>Detail:&nbsp;</li>
<li>Field&nbsp;|&nbsp;</li>
<li>Constr&nbsp;|&nbsp;</li>
<li>Method</li>
</ul>
</div>
<a id="skip.navbar.top">
<!-- -->
</a></div>
<!-- ========= END OF TOP NAVBAR ========= -->
</div>
<div class="navPadding">&nbsp;</div>
<script type="text/javascript"><!--
$('.navPadding').css('padding-top', $('.fixedNav').css("height"));
//-->
</script>
</nav>
</header>
<!-- ======== START OF CLASS DATA ======== -->
<main role="main">
<div class="header">
<div class="subTitle"><span class="packageLabelInType">Package</span>&nbsp;<a href="package-summary.html">com.google.android.exoplayer2.text.span</a></div>
<h2 title="Interface LanguageFeatureSpan" class="title">Interface LanguageFeatureSpan</h2>
</div>
<div class="contentContainer">
<div class="description">
<ul class="blockList">
<li class="blockList">
<dl>
<dt>All Known Implementing Classes:</dt>
<dd><code><a href="HorizontalTextInVerticalContextSpan.html" title="class in com.google.android.exoplayer2.text.span">HorizontalTextInVerticalContextSpan</a></code>, <code><a href="RubySpan.html" title="class in com.google.android.exoplayer2.text.span">RubySpan</a></code>, <code><a href="TextEmphasisSpan.html" title="class in com.google.android.exoplayer2.text.span">TextEmphasisSpan</a></code></dd>
</dl>
<hr>
<pre>public interface <span class="typeNameLabel">LanguageFeatureSpan</span></pre>
<div class="block">Marker interface for span classes that carry language features rather than style information.</div>
</li>
</ul>
</div>
</div>
</main>
<!-- ========= END OF CLASS DATA ========= -->
<footer role="contentinfo">
<nav role="navigation">
<!-- ======= START OF BOTTOM NAVBAR ====== -->
<div class="bottomNav"><a id="navbar.bottom">
<!-- -->
</a>
<div class="skipNav"><a href="#skip.navbar.bottom" title="Skip navigation links">Skip navigation links</a></div>
<a id="navbar.bottom.firstrow">
<!-- -->
</a>
<ul class="navList" title="Navigation">
<li><a href="../../../../../../index.html">Overview</a></li>
<li><a href="package-summary.html">Package</a></li>
<li class="navBarCell1Rev">Class</li>
<li><a href="package-tree.html">Tree</a></li>
<li><a href="../../../../../../deprecated-list.html">Deprecated</a></li>
<li><a href="../../../../../../index-all.html">Index</a></li>
<li><a href="../../../../../../help-doc.html">Help</a></li>
</ul>
</div>
<div class="subNav">
<ul class="navList" id="allclasses_navbar_bottom">
<li><a href="../../../../../../allclasses.html">All&nbsp;Classes</a></li>
</ul>
<div>
<script type="text/javascript"><!--
allClassesLink = document.getElementById("allclasses_navbar_bottom");
if(window==top) {
allClassesLink.style.display = "block";
}
else {
allClassesLink.style.display = "none";
}
//-->
</script>
<noscript>
<div>JavaScript is disabled on your browser.</div>
</noscript>
</div>
<div>
<ul class="subNavList">
<li>Summary:&nbsp;</li>
<li>Nested&nbsp;|&nbsp;</li>
<li>Field&nbsp;|&nbsp;</li>
<li>Constr&nbsp;|&nbsp;</li>
<li>Method</li>
</ul>
<ul class="subNavList">
<li>Detail:&nbsp;</li>
<li>Field&nbsp;|&nbsp;</li>
<li>Constr&nbsp;|&nbsp;</li>
<li>Method</li>
</ul>
</div>
<a id="skip.navbar.bottom">
<!-- -->
</a></div>
<!-- ======== END OF BOTTOM NAVBAR ======= -->
</nav>
</footer>
</body>
</html>
...@@ -122,9 +122,14 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height")); ...@@ -122,9 +122,14 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<div class="description"> <div class="description">
<ul class="blockList"> <ul class="blockList">
<li class="blockList"> <li class="blockList">
<dl>
<dt>All Implemented Interfaces:</dt>
<dd><code><a href="LanguageFeatureSpan.html" title="interface in com.google.android.exoplayer2.text.span">LanguageFeatureSpan</a></code></dd>
</dl>
<hr> <hr>
<pre>public final class <span class="typeNameLabel">RubySpan</span> <pre>public final class <span class="typeNameLabel">RubySpan</span>
extends <a href="https://developer.android.com/reference/java/lang/Object.html" title="class or interface in java.lang" class="externalLink" target="_top">Object</a></pre> extends <a href="https://developer.android.com/reference/java/lang/Object.html" title="class or interface in java.lang" class="externalLink" target="_top">Object</a>
implements <a href="LanguageFeatureSpan.html" title="interface in com.google.android.exoplayer2.text.span">LanguageFeatureSpan</a></pre>
<div class="block">A styling span for ruby text. <div class="block">A styling span for ruby text.
<p>The text covered by this span is known as the "base text", and the ruby text is stored in <p>The text covered by this span is known as the "base text", and the ruby text is stored in
......
...@@ -122,9 +122,14 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height")); ...@@ -122,9 +122,14 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<div class="description"> <div class="description">
<ul class="blockList"> <ul class="blockList">
<li class="blockList"> <li class="blockList">
<dl>
<dt>All Implemented Interfaces:</dt>
<dd><code><a href="LanguageFeatureSpan.html" title="interface in com.google.android.exoplayer2.text.span">LanguageFeatureSpan</a></code></dd>
</dl>
<hr> <hr>
<pre>public final class <span class="typeNameLabel">TextEmphasisSpan</span> <pre>public final class <span class="typeNameLabel">TextEmphasisSpan</span>
extends <a href="https://developer.android.com/reference/java/lang/Object.html" title="class or interface in java.lang" class="externalLink" target="_top">Object</a></pre> extends <a href="https://developer.android.com/reference/java/lang/Object.html" title="class or interface in java.lang" class="externalLink" target="_top">Object</a>
implements <a href="LanguageFeatureSpan.html" title="interface in com.google.android.exoplayer2.text.span">LanguageFeatureSpan</a></pre>
<div class="block">A styling span for text emphasis marks. <div class="block">A styling span for text emphasis marks.
<p>These are pronunciation aids such as <a href="https://www.w3.org/TR/jlreq/?lang=en#term.emphasis-dots">Japanese boutens</a> which can be <p>These are pronunciation aids such as <a href="https://www.w3.org/TR/jlreq/?lang=en#term.emphasis-dots">Japanese boutens</a> which can be
......
...@@ -97,6 +97,23 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height")); ...@@ -97,6 +97,23 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<ul class="blockList"> <ul class="blockList">
<li class="blockList"> <li class="blockList">
<table class="typeSummary"> <table class="typeSummary">
<caption><span>Interface Summary</span><span class="tabEnd">&nbsp;</span></caption>
<tr>
<th class="colFirst" scope="col">Interface</th>
<th class="colLast" scope="col">Description</th>
</tr>
<tbody>
<tr class="altColor">
<th class="colFirst" scope="row"><a href="LanguageFeatureSpan.html" title="interface in com.google.android.exoplayer2.text.span">LanguageFeatureSpan</a></th>
<td class="colLast">
<div class="block">Marker interface for span classes that carry language features rather than style information.</div>
</td>
</tr>
</tbody>
</table>
</li>
<li class="blockList">
<table class="typeSummary">
<caption><span>Class Summary</span><span class="tabEnd">&nbsp;</span></caption> <caption><span>Class Summary</span><span class="tabEnd">&nbsp;</span></caption>
<tr> <tr>
<th class="colFirst" scope="col">Class</th> <th class="colFirst" scope="col">Class</th>
......
...@@ -103,16 +103,22 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height")); ...@@ -103,16 +103,22 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<ul> <ul>
<li class="circle">java.lang.<a href="https://developer.android.com/reference/java/lang/Object.html" title="class or interface in java.lang" class="externalLink"><span class="typeNameLink" target="_top">Object</span></a> <li class="circle">java.lang.<a href="https://developer.android.com/reference/java/lang/Object.html" title="class or interface in java.lang" class="externalLink"><span class="typeNameLink" target="_top">Object</span></a>
<ul> <ul>
<li class="circle">com.google.android.exoplayer2.text.span.<a href="HorizontalTextInVerticalContextSpan.html" title="class in com.google.android.exoplayer2.text.span"><span class="typeNameLink">HorizontalTextInVerticalContextSpan</span></a></li> <li class="circle">com.google.android.exoplayer2.text.span.<a href="HorizontalTextInVerticalContextSpan.html" title="class in com.google.android.exoplayer2.text.span"><span class="typeNameLink">HorizontalTextInVerticalContextSpan</span></a> (implements com.google.android.exoplayer2.text.span.<a href="LanguageFeatureSpan.html" title="interface in com.google.android.exoplayer2.text.span">LanguageFeatureSpan</a>)</li>
<li class="circle">com.google.android.exoplayer2.text.span.<a href="RubySpan.html" title="class in com.google.android.exoplayer2.text.span"><span class="typeNameLink">RubySpan</span></a></li> <li class="circle">com.google.android.exoplayer2.text.span.<a href="RubySpan.html" title="class in com.google.android.exoplayer2.text.span"><span class="typeNameLink">RubySpan</span></a> (implements com.google.android.exoplayer2.text.span.<a href="LanguageFeatureSpan.html" title="interface in com.google.android.exoplayer2.text.span">LanguageFeatureSpan</a>)</li>
<li class="circle">com.google.android.exoplayer2.text.span.<a href="SpanUtil.html" title="class in com.google.android.exoplayer2.text.span"><span class="typeNameLink">SpanUtil</span></a></li> <li class="circle">com.google.android.exoplayer2.text.span.<a href="SpanUtil.html" title="class in com.google.android.exoplayer2.text.span"><span class="typeNameLink">SpanUtil</span></a></li>
<li class="circle">com.google.android.exoplayer2.text.span.<a href="TextAnnotation.html" title="class in com.google.android.exoplayer2.text.span"><span class="typeNameLink">TextAnnotation</span></a></li> <li class="circle">com.google.android.exoplayer2.text.span.<a href="TextAnnotation.html" title="class in com.google.android.exoplayer2.text.span"><span class="typeNameLink">TextAnnotation</span></a></li>
<li class="circle">com.google.android.exoplayer2.text.span.<a href="TextEmphasisSpan.html" title="class in com.google.android.exoplayer2.text.span"><span class="typeNameLink">TextEmphasisSpan</span></a></li> <li class="circle">com.google.android.exoplayer2.text.span.<a href="TextEmphasisSpan.html" title="class in com.google.android.exoplayer2.text.span"><span class="typeNameLink">TextEmphasisSpan</span></a> (implements com.google.android.exoplayer2.text.span.<a href="LanguageFeatureSpan.html" title="interface in com.google.android.exoplayer2.text.span">LanguageFeatureSpan</a>)</li>
</ul> </ul>
</li> </li>
</ul> </ul>
</section> </section>
<section role="region"> <section role="region">
<h2 title="Interface Hierarchy">Interface Hierarchy</h2>
<ul>
<li class="circle">com.google.android.exoplayer2.text.span.<a href="LanguageFeatureSpan.html" title="interface in com.google.android.exoplayer2.text.span"><span class="typeNameLink">LanguageFeatureSpan</span></a></li>
</ul>
</section>
<section role="region">
<h2 title="Annotation Type Hierarchy">Annotation Type Hierarchy</h2> <h2 title="Annotation Type Hierarchy">Annotation Type Hierarchy</h2>
<ul> <ul>
<li class="circle">com.google.android.exoplayer2.text.span.<a href="TextAnnotation.Position.html" title="annotation in com.google.android.exoplayer2.text.span"><span class="typeNameLink">TextAnnotation.Position</span></a> (implements java.lang.annotation.<a href="https://developer.android.com/reference/java/lang/annotation/Annotation.html" title="class or interface in java.lang.annotation" class="externalLink" target="_top">Annotation</a>)</li> <li class="circle">com.google.android.exoplayer2.text.span.<a href="TextAnnotation.Position.html" title="annotation in com.google.android.exoplayer2.text.span"><span class="typeNameLink">TextAnnotation.Position</span></a> (implements java.lang.annotation.<a href="https://developer.android.com/reference/java/lang/annotation/Annotation.html" title="class or interface in java.lang.annotation" class="externalLink" target="_top">Annotation</a>)</li>
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
catch(err) { catch(err) {
} }
//--> //-->
var data = {"i0":10,"i1":10,"i2":10}; var data = {"i0":10,"i1":10,"i2":10,"i3":10};
var tabs = {65535:["t0","All Methods"],2:["t2","Instance Methods"],8:["t4","Concrete Methods"]}; var tabs = {65535:["t0","All Methods"],2:["t2","Instance Methods"],8:["t4","Concrete Methods"]};
var altColor = "altColor"; var altColor = "altColor";
var rowColor = "rowColor"; var rowColor = "rowColor";
...@@ -209,8 +209,7 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html" ...@@ -209,8 +209,7 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<td class="colFirst"><code>void</code></td> <td class="colFirst"><code>void</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#bind()">bind</a></span>()</code></th> <th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#bind()">bind</a></span>()</code></th>
<td class="colLast"> <td class="colLast">
<div class="block">Sets the uniform to whatever value was passed via <a href="#setSamplerTexId(int,int)"><code>setSamplerTexId(int, int)</code></a> or <div class="block">Sets the uniform to whatever value was passed via <a href="#setSamplerTexId(int,int)"><code>setSamplerTexId(int, int)</code></a>, <a href="#setFloat(float)"><code>setFloat(float)</code></a> or <a href="#setFloats(float%5B%5D)"><code>setFloats(float[])</code></a>.</div>
<a href="#setFloat(float)"><code>setFloat(float)</code></a>.</div>
</td> </td>
</tr> </tr>
<tr id="i1" class="rowColor"> <tr id="i1" class="rowColor">
...@@ -222,6 +221,13 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html" ...@@ -222,6 +221,13 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
</tr> </tr>
<tr id="i2" class="altColor"> <tr id="i2" class="altColor">
<td class="colFirst"><code>void</code></td> <td class="colFirst"><code>void</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setFloats(float%5B%5D)">setFloats</a></span>&#8203;(float[]&nbsp;value)</code></th>
<td class="colLast">
<div class="block">Configures <a href="#bind()"><code>bind()</code></a> to use the specified float[] <code>value</code> for this uniform.</div>
</td>
</tr>
<tr id="i3" class="rowColor">
<td class="colFirst"><code>void</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setSamplerTexId(int,int)">setSamplerTexId</a></span>&#8203;(int&nbsp;texId, <th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setSamplerTexId(int,int)">setSamplerTexId</a></span>&#8203;(int&nbsp;texId,
int&nbsp;unit)</code></th> int&nbsp;unit)</code></th>
<td class="colLast"> <td class="colLast">
...@@ -325,6 +331,16 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html" ...@@ -325,6 +331,16 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<div class="block">Configures <a href="#bind()"><code>bind()</code></a> to use the specified float <code>value</code> for this uniform.</div> <div class="block">Configures <a href="#bind()"><code>bind()</code></a> to use the specified float <code>value</code> for this uniform.</div>
</li> </li>
</ul> </ul>
<a id="setFloats(float[])">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>setFloats</h4>
<pre class="methodSignature">public&nbsp;void&nbsp;setFloats&#8203;(float[]&nbsp;value)</pre>
<div class="block">Configures <a href="#bind()"><code>bind()</code></a> to use the specified float[] <code>value</code> for this uniform.</div>
</li>
</ul>
<a id="bind()"> <a id="bind()">
<!-- --> <!-- -->
</a> </a>
...@@ -332,8 +348,7 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html" ...@@ -332,8 +348,7 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<li class="blockList"> <li class="blockList">
<h4>bind</h4> <h4>bind</h4>
<pre class="methodSignature">public&nbsp;void&nbsp;bind()</pre> <pre class="methodSignature">public&nbsp;void&nbsp;bind()</pre>
<div class="block">Sets the uniform to whatever value was passed via <a href="#setSamplerTexId(int,int)"><code>setSamplerTexId(int, int)</code></a> or <div class="block">Sets the uniform to whatever value was passed via <a href="#setSamplerTexId(int,int)"><code>setSamplerTexId(int, int)</code></a>, <a href="#setFloat(float)"><code>setFloat(float)</code></a> or <a href="#setFloats(float%5B%5D)"><code>setFloats(float[])</code></a>.
<a href="#setFloat(float)"><code>setFloat(float)</code></a>.
<p>Should be called before each drawing call.</div> <p>Should be called before each drawing call.</div>
</li> </li>
......
...@@ -379,6 +379,16 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html" ...@@ -379,6 +379,16 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
</tr> </tr>
<tr class="rowColor"> <tr class="rowColor">
<td class="colFirst"><code>static <a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td> <td class="colFirst"><code>static <a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#AUDIO_MPEGH_MHA1">AUDIO_MPEGH_MHA1</a></span></code></th>
<td class="colLast">&nbsp;</td>
</tr>
<tr class="altColor">
<td class="colFirst"><code>static <a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#AUDIO_MPEGH_MHM1">AUDIO_MPEGH_MHM1</a></span></code></th>
<td class="colLast">&nbsp;</td>
</tr>
<tr class="rowColor">
<td class="colFirst"><code>static <a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#AUDIO_MSGSM">AUDIO_MSGSM</a></span></code></th> <th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#AUDIO_MSGSM">AUDIO_MSGSM</a></span></code></th>
<td class="colLast">&nbsp;</td> <td class="colLast">&nbsp;</td>
</tr> </tr>
...@@ -1155,6 +1165,32 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html" ...@@ -1155,6 +1165,32 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
</dl> </dl>
</li> </li>
</ul> </ul>
<a id="AUDIO_MPEGH_MHA1">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>AUDIO_MPEGH_MHA1</h4>
<pre>public static final&nbsp;<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a> AUDIO_MPEGH_MHA1</pre>
<dl>
<dt><span class="seeLabel">See Also:</span></dt>
<dd><a href="../../../../../constant-values.html#com.google.android.exoplayer2.util.MimeTypes.AUDIO_MPEGH_MHA1">Constant Field Values</a></dd>
</dl>
</li>
</ul>
<a id="AUDIO_MPEGH_MHM1">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>AUDIO_MPEGH_MHM1</h4>
<pre>public static final&nbsp;<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a> AUDIO_MPEGH_MHM1</pre>
<dl>
<dt><span class="seeLabel">See Also:</span></dt>
<dd><a href="../../../../../constant-values.html#com.google.android.exoplayer2.util.MimeTypes.AUDIO_MPEGH_MHM1">Constant Field Values</a></dd>
</dl>
</li>
</ul>
<a id="AUDIO_RAW"> <a id="AUDIO_RAW">
<!-- --> <!-- -->
</a> </a>
......
...@@ -1855,21 +1855,21 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height")); ...@@ -1855,21 +1855,21 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<!-- --> <!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td> </a><code>public&nbsp;static&nbsp;final&nbsp;<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/ExoPlayerLibraryInfo.html#VERSION">VERSION</a></code></th> <th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/ExoPlayerLibraryInfo.html#VERSION">VERSION</a></code></th>
<td class="colLast"><code>"2.14.0"</code></td> <td class="colLast"><code>"2.14.1"</code></td>
</tr> </tr>
<tr class="rowColor"> <tr class="rowColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.ExoPlayerLibraryInfo.VERSION_INT"> <td class="colFirst"><a id="com.google.android.exoplayer2.ExoPlayerLibraryInfo.VERSION_INT">
<!-- --> <!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;int</code></td> </a><code>public&nbsp;static&nbsp;final&nbsp;int</code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/ExoPlayerLibraryInfo.html#VERSION_INT">VERSION_INT</a></code></th> <th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/ExoPlayerLibraryInfo.html#VERSION_INT">VERSION_INT</a></code></th>
<td class="colLast"><code>2014000</code></td> <td class="colLast"><code>2014001</code></td>
</tr> </tr>
<tr class="altColor"> <tr class="altColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.ExoPlayerLibraryInfo.VERSION_SLASHY"> <td class="colFirst"><a id="com.google.android.exoplayer2.ExoPlayerLibraryInfo.VERSION_SLASHY">
<!-- --> <!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td> </a><code>public&nbsp;static&nbsp;final&nbsp;<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/ExoPlayerLibraryInfo.html#VERSION_SLASHY">VERSION_SLASHY</a></code></th> <th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/ExoPlayerLibraryInfo.html#VERSION_SLASHY">VERSION_SLASHY</a></code></th>
<td class="colLast"><code>"ExoPlayerLib/2.14.0"</code></td> <td class="colLast"><code>"ExoPlayerLib/2.14.1"</code></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
...@@ -1961,6 +1961,67 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height")); ...@@ -1961,6 +1961,67 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
</li> </li>
<li class="blockList"> <li class="blockList">
<table class="constantsSummary"> <table class="constantsSummary">
<caption><span>com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.html" title="class in com.google.android.exoplayer2">MediaMetadata</a></span><span class="tabEnd">&nbsp;</span></caption>
<tr>
<th class="colFirst" scope="col">Modifier and Type</th>
<th class="colSecond" scope="col">Constant Field</th>
<th class="colLast" scope="col">Value</th>
</tr>
<tbody>
<tr class="altColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.MediaMetadata.FOLDER_TYPE_ALBUMS">
<!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;int</code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/MediaMetadata.html#FOLDER_TYPE_ALBUMS">FOLDER_TYPE_ALBUMS</a></code></th>
<td class="colLast"><code>2</code></td>
</tr>
<tr class="rowColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.MediaMetadata.FOLDER_TYPE_ARTISTS">
<!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;int</code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/MediaMetadata.html#FOLDER_TYPE_ARTISTS">FOLDER_TYPE_ARTISTS</a></code></th>
<td class="colLast"><code>3</code></td>
</tr>
<tr class="altColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.MediaMetadata.FOLDER_TYPE_GENRES">
<!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;int</code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/MediaMetadata.html#FOLDER_TYPE_GENRES">FOLDER_TYPE_GENRES</a></code></th>
<td class="colLast"><code>4</code></td>
</tr>
<tr class="rowColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.MediaMetadata.FOLDER_TYPE_MIXED">
<!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;int</code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/MediaMetadata.html#FOLDER_TYPE_MIXED">FOLDER_TYPE_MIXED</a></code></th>
<td class="colLast"><code>0</code></td>
</tr>
<tr class="altColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.MediaMetadata.FOLDER_TYPE_PLAYLISTS">
<!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;int</code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/MediaMetadata.html#FOLDER_TYPE_PLAYLISTS">FOLDER_TYPE_PLAYLISTS</a></code></th>
<td class="colLast"><code>5</code></td>
</tr>
<tr class="rowColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.MediaMetadata.FOLDER_TYPE_TITLES">
<!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;int</code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/MediaMetadata.html#FOLDER_TYPE_TITLES">FOLDER_TYPE_TITLES</a></code></th>
<td class="colLast"><code>1</code></td>
</tr>
<tr class="altColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.MediaMetadata.FOLDER_TYPE_YEARS">
<!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;int</code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/MediaMetadata.html#FOLDER_TYPE_YEARS">FOLDER_TYPE_YEARS</a></code></th>
<td class="colLast"><code>6</code></td>
</tr>
</tbody>
</table>
</li>
<li class="blockList">
<table class="constantsSummary">
<caption><span>com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/Player.html" title="interface in com.google.android.exoplayer2">Player</a></span><span class="tabEnd">&nbsp;</span></caption> <caption><span>com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/Player.html" title="interface in com.google.android.exoplayer2">Player</a></span><span class="tabEnd">&nbsp;</span></caption>
<tr> <tr>
<th class="colFirst" scope="col">Modifier and Type</th> <th class="colFirst" scope="col">Modifier and Type</th>
...@@ -8902,6 +8963,20 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height")); ...@@ -8902,6 +8963,20 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<td class="colLast"><code>"audio/mpeg-L2"</code></td> <td class="colLast"><code>"audio/mpeg-L2"</code></td>
</tr> </tr>
<tr class="rowColor"> <tr class="rowColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.util.MimeTypes.AUDIO_MPEGH_MHA1">
<!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/util/MimeTypes.html#AUDIO_MPEGH_MHA1">AUDIO_MPEGH_MHA1</a></code></th>
<td class="colLast"><code>"audio/mha1"</code></td>
</tr>
<tr class="altColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.util.MimeTypes.AUDIO_MPEGH_MHM1">
<!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/util/MimeTypes.html#AUDIO_MPEGH_MHM1">AUDIO_MPEGH_MHM1</a></code></th>
<td class="colLast"><code>"audio/mhm1"</code></td>
</tr>
<tr class="rowColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.util.MimeTypes.AUDIO_MSGSM"> <td class="colFirst"><a id="com.google.android.exoplayer2.util.MimeTypes.AUDIO_MSGSM">
<!-- --> <!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td> </a><code>public&nbsp;static&nbsp;final&nbsp;<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td>
......
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
...@@ -24,24 +24,11 @@ These steps are described in more detail below. For a complete example, refer to ...@@ -24,24 +24,11 @@ These steps are described in more detail below. For a complete example, refer to
## Adding ExoPlayer as a dependency ## ## Adding ExoPlayer as a dependency ##
### Add repositories ###
The first step to getting started is to make sure you have the Google and
JCenter repositories included in the `build.gradle` file in the root of your
project.
~~~
repositories {
google()
jcenter()
}
~~~
{: .language-gradle}
### Add ExoPlayer modules ### ### Add ExoPlayer modules ###
Next add a dependency in the `build.gradle` file of your app module. The The easiest way to get started using ExoPlayer is to add it as a gradle
following will add a dependency to the full ExoPlayer library: dependency in the `build.gradle` file of your app module. The following will add
a dependency to the full library:
~~~ ~~~
implementation 'com.google.android.exoplayer:exoplayer:2.X.X' implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
...@@ -75,9 +62,13 @@ modules individually. ...@@ -75,9 +62,13 @@ modules individually.
* `exoplayer-transformer`: Media transformation functionality. * `exoplayer-transformer`: Media transformation functionality.
* `exoplayer-ui`: UI components and resources for use with ExoPlayer. * `exoplayer-ui`: UI components and resources for use with ExoPlayer.
In addition to library modules, ExoPlayer has multiple extension modules that In addition to library modules, ExoPlayer has extension modules that depend on
depend on external libraries to provide additional functionality. Browse the external libraries to provide additional functionality. Some extensions are
[extensions directory][] and their individual READMEs for details. available from the Maven repository, whereas others must be built manually.
Browse the [extensions directory][] and their individual READMEs for details.
More information on the library and extension modules that are available can be
found on the [Google Maven ExoPlayer page][].
### Turn on Java 8 support ### ### Turn on Java 8 support ###
...@@ -239,4 +230,4 @@ can be done by calling `ExoPlayer.release`. ...@@ -239,4 +230,4 @@ can be done by calling `ExoPlayer.release`.
[Playlists page]: {{ site.baseurl }}/playlists.html [Playlists page]: {{ site.baseurl }}/playlists.html
[Media items page]: {{ site.baseurl }}/media-items.html [Media items page]: {{ site.baseurl }}/media-items.html
[Media sources page]: {{ site.baseurl }}/media-sources.html [Media sources page]: {{ site.baseurl }}/media-sources.html
[Google Maven ExoPlayer page]: https://maven.google.com/web/index.html#com.google.android.exoplayer
...@@ -32,8 +32,7 @@ dependencies { ...@@ -32,8 +32,7 @@ dependencies {
// Instrumentation tests assume that an app-packaged version of cronet is // Instrumentation tests assume that an app-packaged version of cronet is
// available. // available.
androidTestImplementation 'org.chromium.net:cronet-embedded:72.3626.96' androidTestImplementation 'org.chromium.net:cronet-embedded:72.3626.96'
androidTestImplementation(project(modulePrefix + 'testutils')) androidTestImplementation project(modulePrefix + 'testutils')
testImplementation project(modulePrefix + 'library')
testImplementation project(modulePrefix + 'testutils') testImplementation project(modulePrefix + 'testutils')
testImplementation 'com.squareup.okhttp3:mockwebserver:' + mockWebServerVersion testImplementation 'com.squareup.okhttp3:mockwebserver:' + mockWebServerVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion
......
...@@ -34,6 +34,7 @@ class CombinedJavadocPlugin implements Plugin<Project> { ...@@ -34,6 +34,7 @@ class CombinedJavadocPlugin implements Plugin<Project> {
"https://guava.dev/releases/$project.ext.guavaVersion/api/docs" "https://guava.dev/releases/$project.ext.guavaVersion/api/docs"
encoding = "UTF-8" encoding = "UTF-8"
} }
options.addBooleanOption "-no-module-directories", true
exclude "**/BuildConfig.java" exclude "**/BuildConfig.java"
exclude "**/R.java" exclude "**/R.java"
doFirst { doFirst {
......
...@@ -31,6 +31,7 @@ android.libraryVariants.all { variant -> ...@@ -31,6 +31,7 @@ android.libraryVariants.all { variant ->
"https://guava.dev/releases/$project.ext.guavaVersion/api/docs" "https://guava.dev/releases/$project.ext.guavaVersion/api/docs"
encoding = "UTF-8" encoding = "UTF-8"
} }
options.addBooleanOption "-no-module-directories", true
exclude "**/BuildConfig.java" exclude "**/BuildConfig.java"
exclude "**/R.java" exclude "**/R.java"
doFirst { doFirst {
......
...@@ -28,11 +28,11 @@ public final class ExoPlayerLibraryInfo { ...@@ -28,11 +28,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */ /** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.14.0"; public static final String VERSION = "2.14.1";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.14.0"; public static final String VERSION_SLASHY = "ExoPlayerLib/2.14.1";
/** /**
* The version of the library expressed as an integer, for example 1002003. * The version of the library expressed as an integer, for example 1002003.
...@@ -42,7 +42,7 @@ public final class ExoPlayerLibraryInfo { ...@@ -42,7 +42,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006). * integer version 123045006 (123-045-006).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2014000; public static final int VERSION_INT = 2014001;
/** /**
* The default user agent for requests made by the library. * The default user agent for requests made by the library.
......
...@@ -20,6 +20,7 @@ import static com.google.android.exoplayer2.util.Util.castNonNull; ...@@ -20,6 +20,7 @@ import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; import android.os.Parcelable;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.MediaMetadata;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.Arrays; import java.util.Arrays;
...@@ -51,6 +52,11 @@ public final class ApicFrame extends Id3Frame { ...@@ -51,6 +52,11 @@ public final class ApicFrame extends Id3Frame {
} }
@Override @Override
public void populateMediaMetadata(MediaMetadata.Builder builder) {
builder.setArtworkData(pictureData);
}
@Override
public boolean equals(@Nullable Object obj) { public boolean equals(@Nullable Object obj) {
if (this == obj) { if (this == obj) {
return true; return true;
......
...@@ -60,6 +60,27 @@ public final class TextInformationFrame extends Id3Frame { ...@@ -60,6 +60,27 @@ public final class TextInformationFrame extends Id3Frame {
case "TALB": case "TALB":
builder.setAlbumTitle(value); builder.setAlbumTitle(value);
break; break;
case "TRK":
case "TRCK":
String[] trackNumbers = Util.split(value, "/");
try {
int trackNumber = Integer.parseInt(trackNumbers[0]);
@Nullable
Integer totalTrackCount =
trackNumbers.length > 1 ? Integer.parseInt(trackNumbers[1]) : null;
builder.setTrackNumber(trackNumber).setTotalTrackCount(totalTrackCount);
} catch (NumberFormatException e) {
// Do nothing, invalid input.
}
break;
case "TYE":
case "TYER":
try {
builder.setYear(Integer.parseInt(value));
} catch (NumberFormatException e) {
// Do nothing, invalid input.
}
break;
default: default:
break; break;
} }
......
...@@ -19,6 +19,8 @@ import android.graphics.Bitmap; ...@@ -19,6 +19,8 @@ import android.graphics.Bitmap;
import android.graphics.Color; import android.graphics.Color;
import android.text.Layout; import android.text.Layout;
import android.text.Layout.Alignment; import android.text.Layout.Alignment;
import android.text.Spanned;
import android.text.SpannedString;
import androidx.annotation.ColorInt; import androidx.annotation.ColorInt;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
...@@ -458,7 +460,13 @@ public final class Cue { ...@@ -458,7 +460,13 @@ public final class Cue {
} else { } else {
Assertions.checkArgument(bitmap == null); Assertions.checkArgument(bitmap == null);
} }
this.text = text; if (text instanceof Spanned) {
this.text = SpannedString.valueOf(text);
} else if (text != null) {
this.text = text.toString();
} else {
this.text = null;
}
this.textAlignment = textAlignment; this.textAlignment = textAlignment;
this.multiRowAlignment = multiRowAlignment; this.multiRowAlignment = multiRowAlignment;
this.bitmap = bitmap; this.bitmap = bitmap;
......
...@@ -141,7 +141,7 @@ public final class GlUtil { ...@@ -141,7 +141,7 @@ public final class GlUtil {
location = GLES20.glGetUniformLocation(program, this.name); location = GLES20.glGetUniformLocation(program, this.name);
this.type = type[0]; this.type = type[0];
value = new float[1]; value = new float[16];
} }
/** /**
...@@ -160,9 +160,14 @@ public final class GlUtil { ...@@ -160,9 +160,14 @@ public final class GlUtil {
this.value[0] = value; this.value[0] = value;
} }
/** Configures {@link #bind()} to use the specified float[] {@code value} for this uniform. */
public void setFloats(float[] value) {
System.arraycopy(value, 0, this.value, 0, value.length);
}
/** /**
* Sets the uniform to whatever value was passed via {@link #setSamplerTexId(int, int)} or * Sets the uniform to whatever value was passed via {@link #setSamplerTexId(int, int)}, {@link
* {@link #setFloat(float)}. * #setFloat(float)} or {@link #setFloats(float[])}.
* *
* <p>Should be called before each drawing call. * <p>Should be called before each drawing call.
*/ */
...@@ -173,6 +178,12 @@ public final class GlUtil { ...@@ -173,6 +178,12 @@ public final class GlUtil {
return; return;
} }
if (type == GLES20.GL_FLOAT_MAT4) {
GLES20.glUniformMatrix4fv(location, 1, false, value, 0);
checkGlError();
return;
}
if (texId == 0) { if (texId == 0) {
throw new IllegalStateException("call setSamplerTexId before bind"); throw new IllegalStateException("call setSamplerTexId before bind");
} }
......
...@@ -62,6 +62,8 @@ public final class MimeTypes { ...@@ -62,6 +62,8 @@ public final class MimeTypes {
public static final String AUDIO_MPEG = BASE_TYPE_AUDIO + "/mpeg"; public static final String AUDIO_MPEG = BASE_TYPE_AUDIO + "/mpeg";
public static final String AUDIO_MPEG_L1 = BASE_TYPE_AUDIO + "/mpeg-L1"; public static final String AUDIO_MPEG_L1 = BASE_TYPE_AUDIO + "/mpeg-L1";
public static final String AUDIO_MPEG_L2 = BASE_TYPE_AUDIO + "/mpeg-L2"; public static final String AUDIO_MPEG_L2 = BASE_TYPE_AUDIO + "/mpeg-L2";
public static final String AUDIO_MPEGH_MHA1 = BASE_TYPE_AUDIO + "/mha1";
public static final String AUDIO_MPEGH_MHM1 = BASE_TYPE_AUDIO + "/mhm1";
public static final String AUDIO_RAW = BASE_TYPE_AUDIO + "/raw"; public static final String AUDIO_RAW = BASE_TYPE_AUDIO + "/raw";
public static final String AUDIO_ALAW = BASE_TYPE_AUDIO + "/g711-alaw"; public static final String AUDIO_ALAW = BASE_TYPE_AUDIO + "/g711-alaw";
public static final String AUDIO_MLAW = BASE_TYPE_AUDIO + "/g711-mlaw"; public static final String AUDIO_MLAW = BASE_TYPE_AUDIO + "/g711-mlaw";
...@@ -365,6 +367,10 @@ public final class MimeTypes { ...@@ -365,6 +367,10 @@ public final class MimeTypes {
} }
} }
return mimeType == null ? MimeTypes.AUDIO_AAC : mimeType; return mimeType == null ? MimeTypes.AUDIO_AAC : mimeType;
} else if (codec.startsWith("mha1")) {
return MimeTypes.AUDIO_MPEGH_MHA1;
} else if (codec.startsWith("mhm1")) {
return MimeTypes.AUDIO_MPEGH_MHM1;
} else if (codec.startsWith("ac-3") || codec.startsWith("dac3")) { } else if (codec.startsWith("ac-3") || codec.startsWith("dac3")) {
return MimeTypes.AUDIO_AC3; return MimeTypes.AUDIO_AC3;
} else if (codec.startsWith("ec-3") || codec.startsWith("dec3")) { } else if (codec.startsWith("ec-3") || codec.startsWith("dec3")) {
......
...@@ -17,9 +17,16 @@ package com.google.android.exoplayer2; ...@@ -17,9 +17,16 @@ package com.google.android.exoplayer2;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import android.os.Bundle;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.id3.ApicFrame;
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.metadata.id3.TextInformationFrame;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.common.collect.ImmutableList;
import java.util.Arrays;
import java.util.List;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
...@@ -32,6 +39,23 @@ public class MediaMetadataTest { ...@@ -32,6 +39,23 @@ public class MediaMetadataTest {
MediaMetadata mediaMetadata = new MediaMetadata.Builder().build(); MediaMetadata mediaMetadata = new MediaMetadata.Builder().build();
assertThat(mediaMetadata.title).isNull(); assertThat(mediaMetadata.title).isNull();
assertThat(mediaMetadata.artist).isNull();
assertThat(mediaMetadata.albumTitle).isNull();
assertThat(mediaMetadata.albumArtist).isNull();
assertThat(mediaMetadata.displayTitle).isNull();
assertThat(mediaMetadata.subtitle).isNull();
assertThat(mediaMetadata.description).isNull();
assertThat(mediaMetadata.mediaUri).isNull();
assertThat(mediaMetadata.userRating).isNull();
assertThat(mediaMetadata.overallRating).isNull();
assertThat(mediaMetadata.artworkData).isNull();
assertThat(mediaMetadata.artworkUri).isNull();
assertThat(mediaMetadata.trackNumber).isNull();
assertThat(mediaMetadata.totalTrackCount).isNull();
assertThat(mediaMetadata.folderType).isNull();
assertThat(mediaMetadata.isPlayable).isNull();
assertThat(mediaMetadata.year).isNull();
assertThat(mediaMetadata.extras).isNull();
} }
@Test @Test
...@@ -44,20 +68,96 @@ public class MediaMetadataTest { ...@@ -44,20 +68,96 @@ public class MediaMetadataTest {
} }
@Test @Test
public void builderSetArtworkData_setsArtworkData() {
byte[] bytes = new byte[] {35, 12, 6, 77};
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setArtworkData(bytes).build();
assertThat(Arrays.equals(mediaMetadata.artworkData, bytes)).isTrue();
}
@Test
public void builderSetArworkUri_setsArtworkUri() {
Uri uri = Uri.parse("https://www.google.com");
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setArtworkUri(uri).build();
assertThat(mediaMetadata.artworkUri).isEqualTo(uri);
}
@Test
public void roundTripViaBundle_yieldsEqualInstance() { public void roundTripViaBundle_yieldsEqualInstance() {
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build(); Bundle extras = new Bundle();
extras.putString("exampleKey", "exampleValue");
MediaMetadata mediaMetadata =
new MediaMetadata.Builder()
.setTitle("title")
.setAlbumArtist("the artist")
.setMediaUri(Uri.parse("https://www.google.com"))
.setUserRating(new HeartRating(false))
.setOverallRating(new PercentageRating(87.4f))
.setArtworkData(new byte[] {-88, 12, 3, 2, 124, -54, -33, 69})
.setTrackNumber(4)
.setTotalTrackCount(12)
.setFolderType(MediaMetadata.FOLDER_TYPE_PLAYLISTS)
.setIsPlayable(true)
.setYear(2000)
.setExtras(extras) // Extras is not implemented in MediaMetadata.equals(Object o).
.build();
assertThat(MediaMetadata.CREATOR.fromBundle(mediaMetadata.toBundle())).isEqualTo(mediaMetadata); MediaMetadata fromBundle = MediaMetadata.CREATOR.fromBundle(mediaMetadata.toBundle());
assertThat(fromBundle).isEqualTo(mediaMetadata);
assertThat(fromBundle.extras.getString("exampleKey")).isEqualTo("exampleValue");
} }
@Test @Test
public void builderPopulatedFromMetadataEntry_setsTitleCorrectly() { public void builderPopulatedFromTextInformationFrameEntry_setsValues() {
String title = "the title"; String title = "the title";
Metadata.Entry entry = String artist = "artist";
new TextInformationFrame(/* id= */ "TT2", /* description= */ null, /* value= */ title); String albumTitle = "album title";
String albumArtist = "album Artist";
String trackNumberInfo = "11/17";
String year = "2000";
List<Metadata.Entry> entries =
ImmutableList.of(
new TextInformationFrame(/* id= */ "TT2", /* description= */ null, /* value= */ title),
new TextInformationFrame(/* id= */ "TP1", /* description= */ null, /* value= */ artist),
new TextInformationFrame(
/* id= */ "TAL", /* description= */ null, /* value= */ albumTitle),
new TextInformationFrame(
/* id= */ "TP2", /* description= */ null, /* value= */ albumArtist),
new TextInformationFrame(
/* id= */ "TRK", /* description= */ null, /* value= */ trackNumberInfo),
new TextInformationFrame(/* id= */ "TYE", /* description= */ null, /* value= */ year));
MediaMetadata.Builder builder = MediaMetadata.EMPTY.buildUpon(); MediaMetadata.Builder builder = MediaMetadata.EMPTY.buildUpon();
entry.populateMediaMetadata(builder); for (Metadata.Entry entry : entries) {
entry.populateMediaMetadata(builder);
}
assertThat(builder.build().title.toString()).isEqualTo(title); assertThat(builder.build().title.toString()).isEqualTo(title);
assertThat(builder.build().artist.toString()).isEqualTo(artist);
assertThat(builder.build().albumTitle.toString()).isEqualTo(albumTitle);
assertThat(builder.build().albumArtist.toString()).isEqualTo(albumArtist);
assertThat(builder.build().trackNumber).isEqualTo(11);
assertThat(builder.build().totalTrackCount).isEqualTo(17);
assertThat(builder.build().year).isEqualTo(2000);
}
@Test
public void builderPopulatedFromApicFrameEntry_setsArtwork() {
byte[] pictureData = new byte[] {-12, 52, 33, 85, 34, 22, 1, -55};
Metadata.Entry entry =
new ApicFrame(
/* mimeType= */ MimeTypes.BASE_TYPE_IMAGE,
/* description= */ "an image",
/* pictureType= */ 0x03,
pictureData);
MediaMetadata.Builder builder = MediaMetadata.EMPTY.buildUpon();
entry.populateMediaMetadata(builder);
MediaMetadata mediaMetadata = builder.build();
assertThat(mediaMetadata.artworkData).isEqualTo(pictureData);
} }
} }
...@@ -430,11 +430,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -430,11 +430,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
case DefaultDrmSessionManager.MODE_RELEASE: case DefaultDrmSessionManager.MODE_RELEASE:
Assertions.checkNotNull(offlineLicenseKeySetId); Assertions.checkNotNull(offlineLicenseKeySetId);
Assertions.checkNotNull(this.sessionId); Assertions.checkNotNull(this.sessionId);
// It's not necessary to restore the key before releasing it but this serves as a good postKeyRequest(offlineLicenseKeySetId, ExoMediaDrm.KEY_TYPE_RELEASE, allowRetry);
// fast-failure check.
if (restoreKeys()) {
postKeyRequest(offlineLicenseKeySetId, ExoMediaDrm.KEY_TYPE_RELEASE, allowRetry);
}
break; break;
default: default:
break; break;
......
...@@ -457,9 +457,15 @@ public class DefaultDrmSessionManager implements DrmSessionManager { ...@@ -457,9 +457,15 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
if (prepareCallsCount++ != 0) { if (prepareCallsCount++ != 0) {
return; return;
} }
checkState(exoMediaDrm == null); if (exoMediaDrm == null) {
exoMediaDrm = exoMediaDrmProvider.acquireExoMediaDrm(uuid); exoMediaDrm = exoMediaDrmProvider.acquireExoMediaDrm(uuid);
exoMediaDrm.setOnEventListener(new MediaDrmEventListener()); exoMediaDrm.setOnEventListener(new MediaDrmEventListener());
} else if (sessionKeepaliveMs != C.TIME_UNSET) {
// Re-acquire the keepalive references for any sessions that are still active.
for (int i = 0; i < sessions.size(); i++) {
sessions.get(i).acquire(/* eventDispatcher= */ null);
}
}
} }
@Override @Override
...@@ -478,8 +484,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { ...@@ -478,8 +484,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
} }
releaseAllPreacquiredSessions(); releaseAllPreacquiredSessions();
checkNotNull(exoMediaDrm).release(); maybeReleaseMediaDrm();
exoMediaDrm = null;
} }
@Override @Override
...@@ -487,6 +492,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { ...@@ -487,6 +492,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
Looper playbackLooper, Looper playbackLooper,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
Format format) { Format format) {
checkState(prepareCallsCount > 0);
initPlaybackLooper(playbackLooper); initPlaybackLooper(playbackLooper);
PreacquiredSessionReference preacquiredSessionReference = PreacquiredSessionReference preacquiredSessionReference =
new PreacquiredSessionReference(eventDispatcher); new PreacquiredSessionReference(eventDispatcher);
...@@ -500,6 +506,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { ...@@ -500,6 +506,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
Looper playbackLooper, Looper playbackLooper,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher, @Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
Format format) { Format format) {
checkState(prepareCallsCount > 0);
initPlaybackLooper(playbackLooper); initPlaybackLooper(playbackLooper);
return acquireSession( return acquireSession(
playbackLooper, playbackLooper,
...@@ -774,6 +781,17 @@ public class DefaultDrmSessionManager implements DrmSessionManager { ...@@ -774,6 +781,17 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
return session; return session;
} }
private void maybeReleaseMediaDrm() {
if (exoMediaDrm != null
&& prepareCallsCount == 0
&& sessions.isEmpty()
&& preacquiredSessionReferences.isEmpty()) {
// This manager and all its sessions are fully released so we can release exoMediaDrm.
checkNotNull(exoMediaDrm).release();
exoMediaDrm = null;
}
}
/** /**
* Extracts {@link SchemeData} instances suitable for the given DRM scheme {@link UUID}. * Extracts {@link SchemeData} instances suitable for the given DRM scheme {@link UUID}.
* *
...@@ -895,6 +913,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager { ...@@ -895,6 +913,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
keepaliveSessions.remove(session); keepaliveSessions.remove(session);
} }
} }
maybeReleaseMediaDrm();
} }
} }
......
...@@ -758,12 +758,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -758,12 +758,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
outputStreamStartPositionUs = C.TIME_UNSET; outputStreamStartPositionUs = C.TIME_UNSET;
outputStreamOffsetUs = C.TIME_UNSET; outputStreamOffsetUs = C.TIME_UNSET;
pendingOutputStreamOffsetCount = 0; pendingOutputStreamOffsetCount = 0;
if (sourceDrmSession != null || codecDrmSession != null) { flushOrReleaseCodec();
// TODO: Do something better with this case.
onReset();
} else {
flushOrReleaseCodec();
}
} }
@Override @Override
......
...@@ -841,9 +841,9 @@ public final class MediaCodecUtil { ...@@ -841,9 +841,9 @@ public final class MediaCodecUtil {
/** /**
* Conversion values taken from ISO 14496-10 Table A-1. * Conversion values taken from ISO 14496-10 Table A-1.
* *
* @param avcLevel one of CodecProfileLevel.AVCLevel* constants. * @param avcLevel One of the {@link CodecProfileLevel} {@code AVCLevel*} constants.
* @return maximum frame size that can be decoded by a decoder with the specified avc level * @return The maximum frame size that can be decoded by a decoder with the specified AVC level,
* (or {@code -1} if the level is not recognized) * or {@code -1} if the level is not recognized.
*/ */
private static int avcLevelToMaxFrameSize(int avcLevel) { private static int avcLevelToMaxFrameSize(int avcLevel) {
switch (avcLevel) { switch (avcLevel) {
...@@ -873,6 +873,10 @@ public final class MediaCodecUtil { ...@@ -873,6 +873,10 @@ public final class MediaCodecUtil {
case CodecProfileLevel.AVCLevel51: case CodecProfileLevel.AVCLevel51:
case CodecProfileLevel.AVCLevel52: case CodecProfileLevel.AVCLevel52:
return 36864 * 16 * 16; return 36864 * 16 * 16;
case CodecProfileLevel.AVCLevel6:
case CodecProfileLevel.AVCLevel61:
case CodecProfileLevel.AVCLevel62:
return 139264 * 16 * 16;
default: default:
return -1; return -1;
} }
......
...@@ -29,4 +29,4 @@ package com.google.android.exoplayer2.text.span; ...@@ -29,4 +29,4 @@ package com.google.android.exoplayer2.text.span;
// NOTE: There's no Android layout support for this, so this span currently doesn't extend any // NOTE: There's no Android layout support for this, so this span currently doesn't extend any
// styling superclasses (e.g. MetricAffectingSpan). The only way to render this styling is to // styling superclasses (e.g. MetricAffectingSpan). The only way to render this styling is to
// extract the spans and do the layout manually. // extract the spans and do the layout manually.
public final class HorizontalTextInVerticalContextSpan {} public final class HorizontalTextInVerticalContextSpan implements LanguageFeatureSpan {}
/*
* Copyright 2021 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.text.span;
/** Marker interface for span classes that carry language features rather than style information. */
public interface LanguageFeatureSpan {}
...@@ -30,7 +30,7 @@ package com.google.android.exoplayer2.text.span; ...@@ -30,7 +30,7 @@ package com.google.android.exoplayer2.text.span;
// extract the spans and do the layout manually. // extract the spans and do the layout manually.
// TODO: Consider adding support for parenthetical text to be used when rendering doesn't support // TODO: Consider adding support for parenthetical text to be used when rendering doesn't support
// rubies (e.g. HTML <rp> tag). // rubies (e.g. HTML <rp> tag).
public final class RubySpan { public final class RubySpan implements LanguageFeatureSpan {
/** The ruby text, i.e. the smaller explanatory characters. */ /** The ruby text, i.e. the smaller explanatory characters. */
public final String rubyText; public final String rubyText;
......
...@@ -32,7 +32,7 @@ import java.lang.annotation.Retention; ...@@ -32,7 +32,7 @@ import java.lang.annotation.Retention;
// NOTE: There's no Android layout support for text emphasis, so this span currently doesn't extend // NOTE: There's no Android layout support for text emphasis, so this span currently doesn't extend
// any styling superclasses (e.g. MetricAffectingSpan). The only way to render this emphasis is to // any styling superclasses (e.g. MetricAffectingSpan). The only way to render this emphasis is to
// extract the spans and do the layout manually. // extract the spans and do the layout manually.
public final class TextEmphasisSpan { public final class TextEmphasisSpan implements LanguageFeatureSpan {
/** /**
* The possible mark shapes that can be used. * The possible mark shapes that can be used.
......
...@@ -124,7 +124,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -124,7 +124,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
private boolean codecHandlesHdr10PlusOutOfBandMetadata; private boolean codecHandlesHdr10PlusOutOfBandMetadata;
@Nullable private Surface surface; @Nullable private Surface surface;
@Nullable private Surface dummySurface; @Nullable private DummySurface dummySurface;
private boolean haveReportedFirstFrameRenderedForCurrentSurface; private boolean haveReportedFirstFrameRenderedForCurrentSurface;
@C.VideoScalingMode private int scalingMode; @C.VideoScalingMode private int scalingMode;
private boolean renderedFirstFrameAfterReset; private boolean renderedFirstFrameAfterReset;
...@@ -486,6 +486,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -486,6 +486,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
} }
} }
@TargetApi(17) // Needed for dummySurface usage. dummySurface is always null on API level 16.
@Override @Override
protected void onReset() { protected void onReset() {
try { try {
...@@ -596,12 +597,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -596,12 +597,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
return tunneling && Util.SDK_INT < 23; return tunneling && Util.SDK_INT < 23;
} }
@TargetApi(17) // Needed for dummySurface usage. dummySurface is always null on API level 16.
@Override @Override
protected MediaCodecAdapter.Configuration getMediaCodecConfiguration( protected MediaCodecAdapter.Configuration getMediaCodecConfiguration(
MediaCodecInfo codecInfo, MediaCodecInfo codecInfo,
Format format, Format format,
@Nullable MediaCrypto crypto, @Nullable MediaCrypto crypto,
float codecOperatingRate) { float codecOperatingRate) {
if (dummySurface != null && dummySurface.secure != codecInfo.secure) {
// We can't re-use the current DummySurface instance with the new decoder.
dummySurface.release();
dummySurface = null;
}
String codecMimeType = codecInfo.codecMimeType; String codecMimeType = codecInfo.codecMimeType;
codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats()); codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats());
MediaFormat mediaFormat = MediaFormat mediaFormat =
......
...@@ -18,18 +18,21 @@ package com.google.android.exoplayer2.drm; ...@@ -18,18 +18,21 @@ package com.google.android.exoplayer2.drm;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertThrows;
import android.os.Looper; import android.os.Looper;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.drm.ExoMediaDrm.AppManagedProvider;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.testutil.FakeExoMediaDrm; import com.google.android.exoplayer2.testutil.FakeExoMediaDrm;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test; import org.junit.Test;
...@@ -179,6 +182,49 @@ public class DefaultDrmSessionManagerTest { ...@@ -179,6 +182,49 @@ public class DefaultDrmSessionManagerTest {
} }
@Test(timeout = 10_000) @Test(timeout = 10_000)
public void managerRelease_mediaDrmNotReleasedUntilLastSessionReleased() throws Exception {
FakeExoMediaDrm.LicenseServer licenseServer =
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
FakeExoMediaDrm exoMediaDrm = new FakeExoMediaDrm();
DrmSessionManager drmSessionManager =
new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, new AppManagedProvider(exoMediaDrm))
.setSessionKeepaliveMs(10_000)
.build(/* mediaDrmCallback= */ licenseServer);
drmSessionManager.prepare();
DrmSession drmSession =
checkNotNull(
drmSessionManager.acquireSession(
/* playbackLooper= */ checkNotNull(Looper.myLooper()),
/* eventDispatcher= */ null,
FORMAT_WITH_DRM_INIT_DATA));
drmSessionManager.release();
// The manager is now in a 'releasing' state because the session is still active - so the
// ExoMediaDrm instance should still be active (with 1 reference held by this test, and 1 held
// by the manager).
assertThat(exoMediaDrm.getReferenceCount()).isEqualTo(2);
// And re-preparing the session shouldn't acquire another reference.
drmSessionManager.prepare();
assertThat(exoMediaDrm.getReferenceCount()).isEqualTo(2);
drmSessionManager.release();
drmSession.release(/* eventDispatcher= */ null);
// The final session has been released, so now the ExoMediaDrm should be released too.
assertThat(exoMediaDrm.getReferenceCount()).isEqualTo(1);
// Re-preparing the fully released manager should now acquire another ExoMediaDrm reference.
drmSessionManager.prepare();
assertThat(exoMediaDrm.getReferenceCount()).isEqualTo(2);
drmSessionManager.release();
exoMediaDrm.release();
}
@Test(timeout = 10_000)
public void maxConcurrentSessionsExceeded_allKeepAliveSessionsEagerlyReleased() throws Exception { public void maxConcurrentSessionsExceeded_allKeepAliveSessionsEagerlyReleased() throws Exception {
ImmutableList<DrmInitData.SchemeData> secondSchemeDatas = ImmutableList<DrmInitData.SchemeData> secondSchemeDatas =
ImmutableList.of(DRM_SCHEME_DATAS.get(0).copyWithData(TestUtil.createByteArray(4, 5, 6))); ImmutableList.of(DRM_SCHEME_DATAS.get(0).copyWithData(TestUtil.createByteArray(4, 5, 6)));
...@@ -407,6 +453,154 @@ public class DefaultDrmSessionManagerTest { ...@@ -407,6 +453,154 @@ public class DefaultDrmSessionManagerTest {
drmSessionManager.release(); drmSessionManager.release();
} }
@Test(timeout = 10_000)
public void keyRefreshEvent_triggersKeyRefresh() throws Exception {
FakeExoMediaDrm exoMediaDrm = new FakeExoMediaDrm();
FakeExoMediaDrm.LicenseServer licenseServer =
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
DrmSessionManager drmSessionManager =
new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, new AppManagedProvider(exoMediaDrm))
.build(/* mediaDrmCallback= */ licenseServer);
drmSessionManager.prepare();
DefaultDrmSession drmSession =
(DefaultDrmSession)
checkNotNull(
drmSessionManager.acquireSession(
/* playbackLooper= */ checkNotNull(Looper.myLooper()),
/* eventDispatcher= */ null,
FORMAT_WITH_DRM_INIT_DATA));
waitForOpenedWithKeys(drmSession);
assertThat(licenseServer.getReceivedSchemeDatas()).hasSize(1);
exoMediaDrm.triggerEvent(
drmSession::hasSessionId,
ExoMediaDrm.EVENT_KEY_REQUIRED,
/* extra= */ 0,
/* data= */ Util.EMPTY_BYTE_ARRAY);
while (licenseServer.getReceivedSchemeDatas().size() == 1) {
// Allow the key refresh event to be handled.
ShadowLooper.idleMainLooper();
}
assertThat(licenseServer.getReceivedSchemeDatas()).hasSize(2);
assertThat(ImmutableSet.copyOf(licenseServer.getReceivedSchemeDatas())).hasSize(1);
drmSession.release(/* eventDispatcher= */ null);
drmSessionManager.release();
exoMediaDrm.release();
}
@Test(timeout = 10_000)
public void keyRefreshEvent_whileManagerIsReleasing_triggersKeyRefresh() throws Exception {
FakeExoMediaDrm exoMediaDrm = new FakeExoMediaDrm();
FakeExoMediaDrm.LicenseServer licenseServer =
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
DrmSessionManager drmSessionManager =
new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, new AppManagedProvider(exoMediaDrm))
.build(/* mediaDrmCallback= */ licenseServer);
drmSessionManager.prepare();
DefaultDrmSession drmSession =
(DefaultDrmSession)
checkNotNull(
drmSessionManager.acquireSession(
/* playbackLooper= */ checkNotNull(Looper.myLooper()),
/* eventDispatcher= */ null,
FORMAT_WITH_DRM_INIT_DATA));
waitForOpenedWithKeys(drmSession);
assertThat(licenseServer.getReceivedSchemeDatas()).hasSize(1);
drmSessionManager.release();
exoMediaDrm.triggerEvent(
drmSession::hasSessionId,
ExoMediaDrm.EVENT_KEY_REQUIRED,
/* extra= */ 0,
/* data= */ Util.EMPTY_BYTE_ARRAY);
while (licenseServer.getReceivedSchemeDatas().size() == 1) {
// Allow the key refresh event to be handled.
ShadowLooper.idleMainLooper();
}
assertThat(licenseServer.getReceivedSchemeDatas()).hasSize(2);
assertThat(ImmutableSet.copyOf(licenseServer.getReceivedSchemeDatas())).hasSize(1);
drmSession.release(/* eventDispatcher= */ null);
exoMediaDrm.release();
}
@Test
public void managerNotPrepared_acquireSessionAndPreacquireSessionFail() throws Exception {
FakeExoMediaDrm.LicenseServer licenseServer =
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
DefaultDrmSessionManager drmSessionManager =
new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
.build(/* mediaDrmCallback= */ licenseServer);
assertThrows(
Exception.class,
() ->
drmSessionManager.acquireSession(
/* playbackLooper= */ checkNotNull(Looper.myLooper()),
/* eventDispatcher= */ null,
FORMAT_WITH_DRM_INIT_DATA));
assertThrows(
Exception.class,
() ->
drmSessionManager.preacquireSession(
/* playbackLooper= */ checkNotNull(Looper.myLooper()),
/* eventDispatcher= */ null,
FORMAT_WITH_DRM_INIT_DATA));
}
@Test
public void managerReleasing_acquireSessionAndPreacquireSessionFail() throws Exception {
FakeExoMediaDrm.LicenseServer licenseServer =
FakeExoMediaDrm.LicenseServer.allowingSchemeDatas(DRM_SCHEME_DATAS);
DefaultDrmSessionManager drmSessionManager =
new DefaultDrmSessionManager.Builder()
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
.build(/* mediaDrmCallback= */ licenseServer);
drmSessionManager.prepare();
DrmSession drmSession =
checkNotNull(
drmSessionManager.acquireSession(
/* playbackLooper= */ checkNotNull(Looper.myLooper()),
/* eventDispatcher= */ null,
FORMAT_WITH_DRM_INIT_DATA));
drmSessionManager.release();
// The manager's prepareCount is now zero, but the drmSession is keeping it in a 'releasing'
// state. acquireSession and preacquireSession should still fail.
assertThrows(
Exception.class,
() ->
drmSessionManager.acquireSession(
/* playbackLooper= */ checkNotNull(Looper.myLooper()),
/* eventDispatcher= */ null,
FORMAT_WITH_DRM_INIT_DATA));
assertThrows(
Exception.class,
() ->
drmSessionManager.preacquireSession(
/* playbackLooper= */ checkNotNull(Looper.myLooper()),
/* eventDispatcher= */ null,
FORMAT_WITH_DRM_INIT_DATA));
drmSession.release(/* eventDispatcher= */ null);
}
private static void waitForOpenedWithKeys(DrmSession drmSession) { private static void waitForOpenedWithKeys(DrmSession drmSession) {
// Check the error first, so we get a meaningful failure if there's been an error. // Check the error first, so we get a meaningful failure if there's been an error.
assertThat(drmSession.getError()).isNull(); assertThat(drmSession.getError()).isNull();
......
...@@ -122,6 +122,15 @@ import java.util.List; ...@@ -122,6 +122,15 @@ import java.util.List;
public static final int TYPE__mp3 = 0x2e6d7033; public static final int TYPE__mp3 = 0x2e6d7033;
@SuppressWarnings("ConstantCaseForConstants") @SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_mha1 = 0x6d686131;
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_mhm1 = 0x6d686d31;
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_mhaC = 0x6d686143;
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE_wave = 0x77617665; public static final int TYPE_wave = 0x77617665;
@SuppressWarnings("ConstantCaseForConstants") @SuppressWarnings("ConstantCaseForConstants")
......
...@@ -940,6 +940,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -940,6 +940,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|| childAtomType == Atom.TYPE_twos || childAtomType == Atom.TYPE_twos
|| childAtomType == Atom.TYPE__mp2 || childAtomType == Atom.TYPE__mp2
|| childAtomType == Atom.TYPE__mp3 || childAtomType == Atom.TYPE__mp3
|| childAtomType == Atom.TYPE_mha1
|| childAtomType == Atom.TYPE_mhm1
|| childAtomType == Atom.TYPE_alac || childAtomType == Atom.TYPE_alac
|| childAtomType == Atom.TYPE_alaw || childAtomType == Atom.TYPE_alaw
|| childAtomType == Atom.TYPE_ulaw || childAtomType == Atom.TYPE_ulaw
...@@ -1312,6 +1314,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -1312,6 +1314,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
pcmEncoding = C.ENCODING_PCM_16BIT_BIG_ENDIAN; pcmEncoding = C.ENCODING_PCM_16BIT_BIG_ENDIAN;
} else if (atomType == Atom.TYPE__mp2 || atomType == Atom.TYPE__mp3) { } else if (atomType == Atom.TYPE__mp2 || atomType == Atom.TYPE__mp3) {
mimeType = MimeTypes.AUDIO_MPEG; mimeType = MimeTypes.AUDIO_MPEG;
} else if (atomType == Atom.TYPE_mha1) {
mimeType = MimeTypes.AUDIO_MPEGH_MHA1;
} else if (atomType == Atom.TYPE_mhm1) {
mimeType = MimeTypes.AUDIO_MPEGH_MHM1;
} else if (atomType == Atom.TYPE_alac) { } else if (atomType == Atom.TYPE_alac) {
mimeType = MimeTypes.AUDIO_ALAC; mimeType = MimeTypes.AUDIO_ALAC;
} else if (atomType == Atom.TYPE_alaw) { } else if (atomType == Atom.TYPE_alaw) {
...@@ -1330,9 +1336,22 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -1330,9 +1336,22 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
int childAtomSize = parent.readInt(); int childAtomSize = parent.readInt();
Assertions.checkState(childAtomSize > 0, "childAtomSize should be positive"); Assertions.checkState(childAtomSize > 0, "childAtomSize should be positive");
int childAtomType = parent.readInt(); int childAtomType = parent.readInt();
if (childAtomType == Atom.TYPE_esds || (isQuickTime && childAtomType == Atom.TYPE_wave)) { if (childAtomType == Atom.TYPE_mhaC) {
int esdsAtomPosition = childAtomType == Atom.TYPE_esds ? childPosition // See ISO_IEC_23008-3;2019 MHADecoderConfigurationRecord
: findEsdsPosition(parent, childPosition, childAtomSize); // The header consists of: size (4), boxtype 'mhaC' (4), configurationVersion (1),
// mpegh3daProfileLevelIndication (1), referenceChannelLayout (1), mpegh3daConfigLength (2).
int mhacHeaderSize = 13;
int childAtomBodySize = childAtomSize - mhacHeaderSize;
byte[] initializationDataBytes = new byte[childAtomBodySize];
parent.setPosition(childPosition + mhacHeaderSize);
parent.readBytes(initializationDataBytes, 0, childAtomBodySize);
initializationData = ImmutableList.of(initializationDataBytes);
} else if (childAtomType == Atom.TYPE_esds
|| (isQuickTime && childAtomType == Atom.TYPE_wave)) {
int esdsAtomPosition =
childAtomType == Atom.TYPE_esds
? childPosition
: findEsdsPosition(parent, childPosition, childAtomSize);
if (esdsAtomPosition != C.POSITION_UNSET) { if (esdsAtomPosition != C.POSITION_UNSET) {
Pair<@NullableType String, byte @NullableType []> mimeTypeAndInitializationData = Pair<@NullableType String, byte @NullableType []> mimeTypeAndInitializationData =
parseEsdsFromParent(parent, esdsAtomPosition); parseEsdsFromParent(parent, esdsAtomPosition);
......
...@@ -84,4 +84,16 @@ public final class Mp4ExtractorTest { ...@@ -84,4 +84,16 @@ public final class Mp4ExtractorTest {
ExtractorAsserts.assertBehavior( ExtractorAsserts.assertBehavior(
Mp4Extractor::new, "media/mp4/sample_opus.mp4", simulationConfig); Mp4Extractor::new, "media/mp4/sample_opus.mp4", simulationConfig);
} }
@Test
public void mp4SampleWithMha1Track() throws Exception {
ExtractorAsserts.assertBehavior(
Mp4Extractor::new, "media/mp4/sample_mpegh_mha1.mp4", simulationConfig);
}
@Test
public void mp4SampleWithMhm1Track() throws Exception {
ExtractorAsserts.assertBehavior(
Mp4Extractor::new, "media/mp4/sample_mpegh_mhm1.mp4", simulationConfig);
}
} }
...@@ -352,70 +352,67 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -352,70 +352,67 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return; return;
} }
@Nullable @Nullable
HlsMediaPlaylist mediaPlaylist = HlsMediaPlaylist playlist =
playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true); playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true);
// playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be non-null. // playlistTracker snapshot is valid (checked by if() above), so playlist must be non-null.
checkNotNull(mediaPlaylist); checkNotNull(playlist);
independentSegments = mediaPlaylist.hasIndependentSegments; independentSegments = playlist.hasIndependentSegments;
updateLiveEdgeTimeUs(mediaPlaylist); updateLiveEdgeTimeUs(playlist);
// Select the chunk. // Select the chunk.
long startOfPlaylistInPeriodUs = long startOfPlaylistInPeriodUs = playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
Pair<Long, Integer> nextMediaSequenceAndPartIndex = Pair<Long, Integer> nextMediaSequenceAndPartIndex =
getNextMediaSequenceAndPartIndex( getNextMediaSequenceAndPartIndex(
previous, switchingTrack, mediaPlaylist, startOfPlaylistInPeriodUs, loadPositionUs); previous, switchingTrack, playlist, startOfPlaylistInPeriodUs, loadPositionUs);
long chunkMediaSequence = nextMediaSequenceAndPartIndex.first; long chunkMediaSequence = nextMediaSequenceAndPartIndex.first;
int partIndex = nextMediaSequenceAndPartIndex.second; int partIndex = nextMediaSequenceAndPartIndex.second;
if (chunkMediaSequence < mediaPlaylist.mediaSequence && previous != null && switchingTrack) { if (chunkMediaSequence < playlist.mediaSequence && previous != null && switchingTrack) {
// We try getting the next chunk without adapting in case that's the reason for falling // We try getting the next chunk without adapting in case that's the reason for falling
// behind the live window. // behind the live window.
selectedTrackIndex = oldTrackIndex; selectedTrackIndex = oldTrackIndex;
selectedPlaylistUrl = playlistUrls[selectedTrackIndex]; selectedPlaylistUrl = playlistUrls[selectedTrackIndex];
mediaPlaylist = playlist =
playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true); playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true);
// playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be // playlistTracker snapshot is valid (checked by if() above), so playlist must be non-null.
// non-null. checkNotNull(playlist);
checkNotNull(mediaPlaylist); startOfPlaylistInPeriodUs = playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
startOfPlaylistInPeriodUs =
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
// Get the next segment/part without switching tracks. // Get the next segment/part without switching tracks.
Pair<Long, Integer> nextMediaSequenceAndPartIndexWithoutAdapting = Pair<Long, Integer> nextMediaSequenceAndPartIndexWithoutAdapting =
getNextMediaSequenceAndPartIndex( getNextMediaSequenceAndPartIndex(
previous, previous,
/* switchingTrack= */ false, /* switchingTrack= */ false,
mediaPlaylist, playlist,
startOfPlaylistInPeriodUs, startOfPlaylistInPeriodUs,
loadPositionUs); loadPositionUs);
chunkMediaSequence = nextMediaSequenceAndPartIndexWithoutAdapting.first; chunkMediaSequence = nextMediaSequenceAndPartIndexWithoutAdapting.first;
partIndex = nextMediaSequenceAndPartIndexWithoutAdapting.second; partIndex = nextMediaSequenceAndPartIndexWithoutAdapting.second;
} }
if (chunkMediaSequence < mediaPlaylist.mediaSequence) { if (chunkMediaSequence < playlist.mediaSequence) {
fatalError = new BehindLiveWindowException(); fatalError = new BehindLiveWindowException();
return; return;
} }
@Nullable @Nullable
SegmentBaseHolder segmentBaseHolder = SegmentBaseHolder segmentBaseHolder =
getNextSegmentHolder(mediaPlaylist, chunkMediaSequence, partIndex); getNextSegmentHolder(playlist, chunkMediaSequence, partIndex);
if (segmentBaseHolder == null) { if (segmentBaseHolder == null) {
if (!mediaPlaylist.hasEndTag) { if (!playlist.hasEndTag) {
// Reload the playlist in case of a live stream. // Reload the playlist in case of a live stream.
out.playlistUrl = selectedPlaylistUrl; out.playlistUrl = selectedPlaylistUrl;
seenExpectedPlaylistError &= selectedPlaylistUrl.equals(expectedPlaylistUrl); seenExpectedPlaylistError &= selectedPlaylistUrl.equals(expectedPlaylistUrl);
expectedPlaylistUrl = selectedPlaylistUrl; expectedPlaylistUrl = selectedPlaylistUrl;
return; return;
} else if (allowEndOfStream || mediaPlaylist.segments.isEmpty()) { } else if (allowEndOfStream || playlist.segments.isEmpty()) {
out.endOfStream = true; out.endOfStream = true;
return; return;
} }
// Use the last segment available in case of a VOD stream. // Use the last segment available in case of a VOD stream.
segmentBaseHolder = segmentBaseHolder =
new SegmentBaseHolder( new SegmentBaseHolder(
Iterables.getLast(mediaPlaylist.segments), Iterables.getLast(playlist.segments),
mediaPlaylist.mediaSequence + mediaPlaylist.segments.size() - 1, playlist.mediaSequence + playlist.segments.size() - 1,
/* partIndex= */ C.INDEX_UNSET); /* partIndex= */ C.INDEX_UNSET);
} }
...@@ -426,24 +423,36 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -426,24 +423,36 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// Check if the media segment or its initialization segment are fully encrypted. // Check if the media segment or its initialization segment are fully encrypted.
@Nullable @Nullable
Uri initSegmentKeyUri = Uri initSegmentKeyUri =
getFullEncryptionKeyUri(mediaPlaylist, segmentBaseHolder.segmentBase.initializationSegment); getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase.initializationSegment);
out.chunk = maybeCreateEncryptionChunkFor(initSegmentKeyUri, selectedTrackIndex); out.chunk = maybeCreateEncryptionChunkFor(initSegmentKeyUri, selectedTrackIndex);
if (out.chunk != null) { if (out.chunk != null) {
return; return;
} }
@Nullable @Nullable
Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(mediaPlaylist, segmentBaseHolder.segmentBase); Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase);
out.chunk = maybeCreateEncryptionChunkFor(mediaSegmentKeyUri, selectedTrackIndex); out.chunk = maybeCreateEncryptionChunkFor(mediaSegmentKeyUri, selectedTrackIndex);
if (out.chunk != null) { if (out.chunk != null) {
return; return;
} }
boolean shouldSpliceIn =
HlsMediaChunk.shouldSpliceIn(
previous, selectedPlaylistUrl, playlist, segmentBaseHolder, startOfPlaylistInPeriodUs);
if (shouldSpliceIn && segmentBaseHolder.isPreload) {
// We don't support discarding spliced-in segments [internal: b/159904763], but preload
// parts may need to be discarded if they are removed before becoming permanently published.
// Hence, don't allow this combination and instead wait with loading the next part until it
// becomes fully available (or the track selection selects another track).
return;
}
out.chunk = out.chunk =
HlsMediaChunk.createInstance( HlsMediaChunk.createInstance(
extractorFactory, extractorFactory,
mediaDataSource, mediaDataSource,
playlistFormats[selectedTrackIndex], playlistFormats[selectedTrackIndex],
startOfPlaylistInPeriodUs, startOfPlaylistInPeriodUs,
mediaPlaylist, playlist,
segmentBaseHolder, segmentBaseHolder,
selectedPlaylistUrl, selectedPlaylistUrl,
muxedCaptionFormats, muxedCaptionFormats,
...@@ -453,7 +462,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -453,7 +462,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
timestampAdjusterProvider, timestampAdjusterProvider,
previous, previous,
/* mediaSegmentKey= */ keyCache.get(mediaSegmentKeyUri), /* mediaSegmentKey= */ keyCache.get(mediaSegmentKeyUri),
/* initSegmentKey= */ keyCache.get(initSegmentKeyUri)); /* initSegmentKey= */ keyCache.get(initSegmentKeyUri),
shouldSpliceIn);
} }
@Nullable @Nullable
......
...@@ -75,6 +75,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -75,6 +75,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* @param mediaSegmentKey The media segment decryption key, if fully encrypted. Null otherwise. * @param mediaSegmentKey The media segment decryption key, if fully encrypted. Null otherwise.
* @param initSegmentKey The initialization segment decryption key, if fully encrypted. Null * @param initSegmentKey The initialization segment decryption key, if fully encrypted. Null
* otherwise. * otherwise.
* @param shouldSpliceIn Whether samples for this chunk should be spliced into existing samples.
*/ */
public static HlsMediaChunk createInstance( public static HlsMediaChunk createInstance(
HlsExtractorFactory extractorFactory, HlsExtractorFactory extractorFactory,
...@@ -91,7 +92,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -91,7 +92,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
TimestampAdjusterProvider timestampAdjusterProvider, TimestampAdjusterProvider timestampAdjusterProvider,
@Nullable HlsMediaChunk previousChunk, @Nullable HlsMediaChunk previousChunk,
@Nullable byte[] mediaSegmentKey, @Nullable byte[] mediaSegmentKey,
@Nullable byte[] initSegmentKey) { @Nullable byte[] initSegmentKey,
boolean shouldSpliceIn) {
// Media segment. // Media segment.
HlsMediaPlaylist.SegmentBase mediaSegment = segmentBaseHolder.segmentBase; HlsMediaPlaylist.SegmentBase mediaSegment = segmentBaseHolder.segmentBase;
DataSpec dataSpec = DataSpec dataSpec =
...@@ -135,17 +137,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -135,17 +137,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@Nullable HlsMediaChunkExtractor previousExtractor = null; @Nullable HlsMediaChunkExtractor previousExtractor = null;
Id3Decoder id3Decoder; Id3Decoder id3Decoder;
ParsableByteArray scratchId3Data; ParsableByteArray scratchId3Data;
boolean shouldSpliceIn;
if (previousChunk != null) { if (previousChunk != null) {
boolean isFollowingChunk = boolean isFollowingChunk =
playlistUrl.equals(previousChunk.playlistUrl) && previousChunk.loadCompleted; playlistUrl.equals(previousChunk.playlistUrl) && previousChunk.loadCompleted;
id3Decoder = previousChunk.id3Decoder; id3Decoder = previousChunk.id3Decoder;
scratchId3Data = previousChunk.scratchId3Data; scratchId3Data = previousChunk.scratchId3Data;
boolean isIndependent = isIndependent(segmentBaseHolder, mediaPlaylist);
boolean canContinueWithoutSplice =
isFollowingChunk
|| (isIndependent && segmentStartTimeInPeriodUs >= previousChunk.endTimeUs);
shouldSpliceIn = !canContinueWithoutSplice;
previousExtractor = previousExtractor =
isFollowingChunk isFollowingChunk
&& !previousChunk.extractorInvalidated && !previousChunk.extractorInvalidated
...@@ -155,7 +152,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -155,7 +152,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
} else { } else {
id3Decoder = new Id3Decoder(); id3Decoder = new Id3Decoder();
scratchId3Data = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH); scratchId3Data = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH);
shouldSpliceIn = false;
} }
return new HlsMediaChunk( return new HlsMediaChunk(
extractorFactory, extractorFactory,
...@@ -186,6 +182,41 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -186,6 +182,41 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
shouldSpliceIn); shouldSpliceIn);
} }
/**
* Returns whether samples of a new HLS media chunk should be spliced into existing samples.
*
* @param previousChunk The previous existing media chunk, or null if the new chunk is the first
* in the queue.
* @param playlistUrl The URL of the playlist from which the new chunk will be obtained.
* @param mediaPlaylist The {@link HlsMediaPlaylist} containing the new chunk.
* @param segmentBaseHolder The {@link HlsChunkSource.SegmentBaseHolder} with information about
* the new chunk.
* @param startOfPlaylistInPeriodUs The start time of the playlist in the period, in microseconds.
* @return Whether samples of the new chunk should be spliced into existing samples.
*/
public static boolean shouldSpliceIn(
@Nullable HlsMediaChunk previousChunk,
Uri playlistUrl,
HlsMediaPlaylist mediaPlaylist,
HlsChunkSource.SegmentBaseHolder segmentBaseHolder,
long startOfPlaylistInPeriodUs) {
if (previousChunk == null) {
// First chunk doesn't require splicing.
return false;
}
if (playlistUrl.equals(previousChunk.playlistUrl) && previousChunk.loadCompleted) {
// Continuing with the next chunk in the same playlist after fully loading the previous chunk
// (i.e. the load wasn't cancelled or failed) is always possible.
return false;
}
// Changing playlists or continuing after a chunk cancellation/failure requires independent,
// non-overlapping segments to avoid the splice.
long segmentStartTimeInPeriodUs =
startOfPlaylistInPeriodUs + segmentBaseHolder.segmentBase.relativeStartTimeUs;
return !isIndependent(segmentBaseHolder, mediaPlaylist)
|| segmentStartTimeInPeriodUs < previousChunk.endTimeUs;
}
public static final String PRIV_TIMESTAMP_FRAME_OWNER = public static final String PRIV_TIMESTAMP_FRAME_OWNER =
"com.apple.streaming.transportStreamTimestamp"; "com.apple.streaming.transportStreamTimestamp";
......
...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.hls; ...@@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.hls;
import static com.google.android.exoplayer2.source.hls.HlsChunkSource.CHUNK_PUBLICATION_STATE_PUBLISHED; import static com.google.android.exoplayer2.source.hls.HlsChunkSource.CHUNK_PUBLICATION_STATE_PUBLISHED;
import static com.google.android.exoplayer2.source.hls.HlsChunkSource.CHUNK_PUBLICATION_STATE_REMOVED; import static com.google.android.exoplayer2.source.hls.HlsChunkSource.CHUNK_PUBLICATION_STATE_REMOVED;
import static java.lang.Math.max; import static java.lang.Math.max;
import static java.lang.Math.min;
import android.net.Uri; import android.net.Uri;
import android.os.Handler; import android.os.Handler;
...@@ -636,17 +637,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -636,17 +637,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
int skipCount = sampleQueue.getSkipCount(positionUs, loadingFinished); int skipCount = sampleQueue.getSkipCount(positionUs, loadingFinished);
// Ensure we don't skip into preload chunks until we can be sure they are permanently published. // Ensure we don't skip into preload chunks until we can be sure they are permanently published.
int readIndex = sampleQueue.getReadIndex(); @Nullable HlsMediaChunk lastChunk = Iterables.getLast(mediaChunks, /* defaultValue= */ null);
for (int i = 0; i < mediaChunks.size(); i++) { if (lastChunk != null && !lastChunk.isPublished()) {
HlsMediaChunk mediaChunk = mediaChunks.get(i); int readIndex = sampleQueue.getReadIndex();
int firstSampleIndex = mediaChunks.get(i).getFirstSampleIndex(sampleQueueIndex); int firstSampleIndex = lastChunk.getFirstSampleIndex(sampleQueueIndex);
if (readIndex + skipCount <= firstSampleIndex) { skipCount = min(skipCount, firstSampleIndex - readIndex);
break;
}
if (!mediaChunk.isPublished()) {
skipCount = firstSampleIndex - readIndex;
break;
}
} }
sampleQueue.skip(skipCount); sampleQueue.skip(skipCount);
...@@ -709,6 +704,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -709,6 +704,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
? lastMediaChunk.endTimeUs ? lastMediaChunk.endTimeUs
: max(lastSeekPositionUs, lastMediaChunk.startTimeUs); : max(lastSeekPositionUs, lastMediaChunk.startTimeUs);
} }
nextChunkHolder.clear();
chunkSource.getNextChunk( chunkSource.getNextChunk(
positionUs, positionUs,
loadPositionUs, loadPositionUs,
...@@ -718,7 +714,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -718,7 +714,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
boolean endOfStream = nextChunkHolder.endOfStream; boolean endOfStream = nextChunkHolder.endOfStream;
@Nullable Chunk loadable = nextChunkHolder.chunk; @Nullable Chunk loadable = nextChunkHolder.chunk;
@Nullable Uri playlistUrlToLoad = nextChunkHolder.playlistUrl; @Nullable Uri playlistUrlToLoad = nextChunkHolder.playlistUrl;
nextChunkHolder.clear();
if (endOfStream) { if (endOfStream) {
pendingResetPositionUs = C.TIME_UNSET; pendingResetPositionUs = C.TIME_UNSET;
......
...@@ -15,6 +15,9 @@ ...@@ -15,6 +15,9 @@
*/ */
package com.google.android.exoplayer2.source.hls.playlist; package com.google.android.exoplayer2.source.hls.playlist;
import static java.lang.Math.max;
import static java.lang.Math.min;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
...@@ -393,9 +396,13 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -393,9 +396,13 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
*/ */
@PlaylistType public final int playlistType; @PlaylistType public final int playlistType;
/** /**
* The start offset in microseconds, as defined by #EXT-X-START. * The start offset in microseconds from the beginning of the playlist, as defined by
* #EXT-X-START, or {@link C#TIME_UNSET} if undefined. The value is guaranteed to be between 0 and
* {@link #durationUs}, inclusive.
*/ */
public final long startOffsetUs; public final long startOffsetUs;
/** Whether the start position should be precise, as defined by #EXT-X-START. */
public final boolean preciseStart;
/** /**
* If {@link #hasProgramDateTime} is true, contains the datetime as microseconds since epoch. * If {@link #hasProgramDateTime} is true, contains the datetime as microseconds since epoch.
* Otherwise, contains the aggregated duration of removed segments up to this snapshot of the * Otherwise, contains the aggregated duration of removed segments up to this snapshot of the
...@@ -480,6 +487,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -480,6 +487,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
String baseUri, String baseUri,
List<String> tags, List<String> tags,
long startOffsetUs, long startOffsetUs,
boolean preciseStart,
long startTimeUs, long startTimeUs,
boolean hasDiscontinuitySequence, boolean hasDiscontinuitySequence,
int discontinuitySequence, int discontinuitySequence,
...@@ -498,6 +506,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -498,6 +506,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
super(baseUri, tags, hasIndependentSegments); super(baseUri, tags, hasIndependentSegments);
this.playlistType = playlistType; this.playlistType = playlistType;
this.startTimeUs = startTimeUs; this.startTimeUs = startTimeUs;
this.preciseStart = preciseStart;
this.hasDiscontinuitySequence = hasDiscontinuitySequence; this.hasDiscontinuitySequence = hasDiscontinuitySequence;
this.discontinuitySequence = discontinuitySequence; this.discontinuitySequence = discontinuitySequence;
this.mediaSequence = mediaSequence; this.mediaSequence = mediaSequence;
...@@ -519,8 +528,15 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -519,8 +528,15 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
} else { } else {
durationUs = 0; durationUs = 0;
} }
this.startOffsetUs = startOffsetUs == C.TIME_UNSET ? C.TIME_UNSET // From RFC 8216, section 4.4.2.2: If startOffsetUs is negative, it indicates the offset from
: startOffsetUs >= 0 ? startOffsetUs : durationUs + startOffsetUs; // the end of the playlist. If the absolute value exceeds the duration of the playlist, it
// indicates the beginning (if negative) or the end (if positive) of the playlist.
this.startOffsetUs =
startOffsetUs == C.TIME_UNSET
? C.TIME_UNSET
: startOffsetUs >= 0
? min(durationUs, startOffsetUs)
: max(0, durationUs + startOffsetUs);
this.serverControl = serverControl; this.serverControl = serverControl;
} }
...@@ -575,6 +591,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -575,6 +591,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
baseUri, baseUri,
tags, tags,
startOffsetUs, startOffsetUs,
preciseStart,
startTimeUs, startTimeUs,
/* hasDiscontinuitySequence= */ true, /* hasDiscontinuitySequence= */ true,
discontinuitySequence, discontinuitySequence,
...@@ -605,6 +622,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist { ...@@ -605,6 +622,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
baseUri, baseUri,
tags, tags,
startOffsetUs, startOffsetUs,
preciseStart,
startTimeUs, startTimeUs,
hasDiscontinuitySequence, hasDiscontinuitySequence,
discontinuitySequence, discontinuitySequence,
......
...@@ -208,6 +208,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -208,6 +208,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
private static final Pattern REGEX_FORCED = compileBooleanAttrPattern("FORCED"); private static final Pattern REGEX_FORCED = compileBooleanAttrPattern("FORCED");
private static final Pattern REGEX_INDEPENDENT = compileBooleanAttrPattern("INDEPENDENT"); private static final Pattern REGEX_INDEPENDENT = compileBooleanAttrPattern("INDEPENDENT");
private static final Pattern REGEX_GAP = compileBooleanAttrPattern("GAP"); private static final Pattern REGEX_GAP = compileBooleanAttrPattern("GAP");
private static final Pattern REGEX_PRECISE = compileBooleanAttrPattern("PRECISE");
private static final Pattern REGEX_VALUE = Pattern.compile("VALUE=\"(.+?)\""); private static final Pattern REGEX_VALUE = Pattern.compile("VALUE=\"(.+?)\"");
private static final Pattern REGEX_IMPORT = Pattern.compile("IMPORT=\"(.+?)\""); private static final Pattern REGEX_IMPORT = Pattern.compile("IMPORT=\"(.+?)\"");
private static final Pattern REGEX_VARIABLE_REFERENCE = private static final Pattern REGEX_VARIABLE_REFERENCE =
...@@ -643,6 +644,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -643,6 +644,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
int relativeDiscontinuitySequence = 0; int relativeDiscontinuitySequence = 0;
long playlistStartTimeUs = 0; long playlistStartTimeUs = 0;
long segmentStartTimeUs = 0; long segmentStartTimeUs = 0;
boolean preciseStart = false;
long segmentByteRangeOffset = 0; long segmentByteRangeOffset = 0;
long segmentByteRangeLength = C.LENGTH_UNSET; long segmentByteRangeLength = C.LENGTH_UNSET;
long partStartTimeUs = 0; long partStartTimeUs = 0;
...@@ -685,6 +687,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -685,6 +687,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
isIFrameOnly = true; isIFrameOnly = true;
} else if (line.startsWith(TAG_START)) { } else if (line.startsWith(TAG_START)) {
startOffsetUs = (long) (parseDoubleAttr(line, REGEX_TIME_OFFSET) * C.MICROS_PER_SECOND); startOffsetUs = (long) (parseDoubleAttr(line, REGEX_TIME_OFFSET) * C.MICROS_PER_SECOND);
preciseStart =
parseOptionalBooleanAttribute(line, REGEX_PRECISE, /* defaultValue= */ false);
} else if (line.startsWith(TAG_SERVER_CONTROL)) { } else if (line.startsWith(TAG_SERVER_CONTROL)) {
serverControl = parseServerControl(line); serverControl = parseServerControl(line);
} else if (line.startsWith(TAG_PART_INF)) { } else if (line.startsWith(TAG_PART_INF)) {
...@@ -1015,6 +1019,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -1015,6 +1019,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
baseUri, baseUri,
tags, tags,
startOffsetUs, startOffsetUs,
preciseStart,
playlistStartTimeUs, playlistStartTimeUs,
hasDiscontinuitySequence, hasDiscontinuitySequence,
playlistDiscontinuitySequence, playlistDiscontinuitySequence,
......
...@@ -15,7 +15,9 @@ ...@@ -15,7 +15,9 @@
*/ */
package com.google.android.exoplayer2.source.rtsp; package com.google.android.exoplayer2.source.rtsp;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.rtsp.RtspMessageChannel.InterleavedBinaryDataListener;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import java.io.IOException; import java.io.IOException;
...@@ -43,17 +45,9 @@ import java.io.IOException; ...@@ -43,17 +45,9 @@ import java.io.IOException;
int getLocalPort(); int getLocalPort();
/** /**
* Returns whether the data channel is using sideband binary data to transmit RTP packets. For * Returns a {@link InterleavedBinaryDataListener} if the implementation supports receiving RTP
* example, RTP-over-RTSP. * packets on a side-band protocol, for example RTP-over-RTSP; otherwise {@code null}.
*/ */
boolean usesSidebandBinaryData(); @Nullable
InterleavedBinaryDataListener getInterleavedBinaryDataListener();
/**
* Writes data to the channel.
*
* <p>The channel owns the written buffer, the user must not alter its content after writing.
*
* @param buffer The buffer from which data should be written. The buffer should be full.
*/
void write(byte[] buffer);
} }
/*
* Copyright 2021 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.rtsp;
import android.net.Uri;
import android.util.Base64;
import androidx.annotation.IntDef;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.source.rtsp.RtspMessageUtil.RtspAuthUserInfo;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/** Wraps RTSP authentication information. */
/* package */ final class RtspAuthenticationInfo {
/** The supported authentication methods. */
@Retention(RetentionPolicy.SOURCE)
@IntDef({BASIC, DIGEST})
@interface AuthenticationMechanism {}
/** HTTP basic authentication (RFC2068 Section 11.1). */
public static final int BASIC = 1;
/** HTTP digest authentication (RFC2069). */
public static final int DIGEST = 2;
private static final String DIGEST_FORMAT =
"Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\"";
private static final String DIGEST_FORMAT_WITH_OPAQUE =
"Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\","
+ " opaque=\"%s\"";
private static final String ALGORITHM = "MD5";
/** The authentication mechanism. */
@AuthenticationMechanism public final int authenticationMechanism;
/** The authentication realm. */
public final String realm;
/** The nonce used in digest authentication; empty if using {@link #BASIC} authentication. */
public final String nonce;
/** The opaque used in digest authentication; empty if using {@link #BASIC} authentication. */
public final String opaque;
/**
* Creates a new instance.
*
* @param authenticationMechanism The authentication mechanism, as defined by {@link
* AuthenticationMechanism}.
* @param realm The authentication realm.
* @param nonce The nonce in digest authentication; empty if using {@link #BASIC} authentication.
* @param opaque The opaque in digest authentication; empty if using {@link #BASIC}
* authentication.
*/
public RtspAuthenticationInfo(
@AuthenticationMechanism int authenticationMechanism,
String realm,
String nonce,
String opaque) {
this.authenticationMechanism = authenticationMechanism;
this.realm = realm;
this.nonce = nonce;
this.opaque = opaque;
}
/**
* Gets the string value for {@link RtspHeaders#AUTHORIZATION} header.
*
* @param authUserInfo The {@link RtspAuthUserInfo} for authentication.
* @param uri The request {@link Uri}.
* @param requestMethod The request method, defined in {@link RtspRequest.Method}.
* @return The string value for {@link RtspHeaders#AUTHORIZATION} header.
* @throws ParserException If the MD5 algorithm is not supported by {@link MessageDigest}.
*/
public String getAuthorizationHeaderValue(
RtspAuthUserInfo authUserInfo, Uri uri, @RtspRequest.Method int requestMethod)
throws ParserException {
switch (authenticationMechanism) {
case BASIC:
return getBasicAuthorizationHeaderValue(authUserInfo);
case DIGEST:
return getDigestAuthorizationHeaderValue(authUserInfo, uri, requestMethod);
default:
throw new ParserException(new UnsupportedOperationException());
}
}
private String getBasicAuthorizationHeaderValue(RtspAuthUserInfo authUserInfo) {
return Base64.encodeToString(
RtspMessageUtil.getStringBytes(authUserInfo.username + ":" + authUserInfo.password),
Base64.DEFAULT);
}
private String getDigestAuthorizationHeaderValue(
RtspAuthUserInfo authUserInfo, Uri uri, @RtspRequest.Method int requestMethod)
throws ParserException {
try {
MessageDigest md = MessageDigest.getInstance(ALGORITHM);
String methodName = RtspMessageUtil.toMethodString(requestMethod);
// From RFC2069 Section 2.1.2:
// response-digest = H( H(A1) ":" unquoted nonce-value ":" H(A2) )
// A1 = unquoted username-value ":" unquoted realm-value ":" password
// A2 = Method ":" request-uri
// H(x) = MD5(x)
String hashA1 =
Util.toHexString(
md.digest(
RtspMessageUtil.getStringBytes(
authUserInfo.username + ":" + realm + ":" + authUserInfo.password)));
String hashA2 =
Util.toHexString(md.digest(RtspMessageUtil.getStringBytes(methodName + ":" + uri)));
String response =
Util.toHexString(
md.digest(RtspMessageUtil.getStringBytes(hashA1 + ":" + nonce + ":" + hashA2)));
if (opaque.isEmpty()) {
return Util.formatInvariant(
DIGEST_FORMAT, authUserInfo.username, realm, nonce, uri, response);
} else {
return Util.formatInvariant(
DIGEST_FORMAT_WITH_OPAQUE, authUserInfo.username, realm, nonce, uri, response, opaque);
}
} catch (NoSuchAlgorithmException e) {
throw new ParserException(e);
}
}
}
...@@ -20,9 +20,8 @@ import androidx.annotation.Nullable; ...@@ -20,9 +20,8 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Ascii; import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableListMultimap;
import java.util.ArrayList; import com.google.common.collect.Iterables;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
...@@ -35,45 +34,45 @@ import java.util.Map; ...@@ -35,45 +34,45 @@ import java.util.Map;
*/ */
/* package */ final class RtspHeaders { /* package */ final class RtspHeaders {
public static final String ACCEPT = "Accept"; public static final String ACCEPT = "accept";
public static final String ALLOW = "Allow"; public static final String ALLOW = "allow";
public static final String AUTHORIZATION = "Authorization"; public static final String AUTHORIZATION = "authorization";
public static final String BANDWIDTH = "Bandwidth"; public static final String BANDWIDTH = "bandwidth";
public static final String BLOCKSIZE = "Blocksize"; public static final String BLOCKSIZE = "blocksize";
public static final String CACHE_CONTROL = "Cache-Control"; public static final String CACHE_CONTROL = "cache-control";
public static final String CONNECTION = "Connection"; public static final String CONNECTION = "connection";
public static final String CONTENT_BASE = "Content-Base"; public static final String CONTENT_BASE = "content-base";
public static final String CONTENT_ENCODING = "Content-Encoding"; public static final String CONTENT_ENCODING = "content-encoding";
public static final String CONTENT_LANGUAGE = "Content-Language"; public static final String CONTENT_LANGUAGE = "content-language";
public static final String CONTENT_LENGTH = "Content-Length"; public static final String CONTENT_LENGTH = "content-length";
public static final String CONTENT_LOCATION = "Content-Location"; public static final String CONTENT_LOCATION = "content-location";
public static final String CONTENT_TYPE = "Content-Type"; public static final String CONTENT_TYPE = "content-type";
public static final String CSEQ = "CSeq"; public static final String CSEQ = "cseq";
public static final String DATE = "Date"; public static final String DATE = "date";
public static final String EXPIRES = "Expires"; public static final String EXPIRES = "expires";
public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate"; public static final String PROXY_AUTHENTICATE = "proxy-authenticate";
public static final String PROXY_REQUIRE = "Proxy-Require"; public static final String PROXY_REQUIRE = "proxy-require";
public static final String PUBLIC = "Public"; public static final String PUBLIC = "public";
public static final String RANGE = "Range"; public static final String RANGE = "range";
public static final String RTP_INFO = "RTP-Info"; public static final String RTP_INFO = "rtp-info";
public static final String RTCP_INTERVAL = "RTCP-Interval"; public static final String RTCP_INTERVAL = "rtcp-interval";
public static final String SCALE = "Scale"; public static final String SCALE = "scale";
public static final String SESSION = "Session"; public static final String SESSION = "session";
public static final String SPEED = "Speed"; public static final String SPEED = "speed";
public static final String SUPPORTED = "Supported"; public static final String SUPPORTED = "supported";
public static final String TIMESTAMP = "Timestamp"; public static final String TIMESTAMP = "timestamp";
public static final String TRANSPORT = "Transport"; public static final String TRANSPORT = "transport";
public static final String USER_AGENT = "User-Agent"; public static final String USER_AGENT = "user-agent";
public static final String VIA = "Via"; public static final String VIA = "via";
public static final String WWW_AUTHENTICATE = "WWW-Authenticate"; public static final String WWW_AUTHENTICATE = "www-authenticate";
/** Builds {@link RtspHeaders} instances. */ /** Builds {@link RtspHeaders} instances. */
public static final class Builder { public static final class Builder {
private final List<String> namesAndValues; private final ImmutableListMultimap.Builder<String, String> namesAndValuesBuilder;
/** Creates a new instance. */ /** Creates a new instance. */
public Builder() { public Builder() {
namesAndValues = new ArrayList<>(); namesAndValuesBuilder = new ImmutableListMultimap.Builder<>();
} }
/** /**
...@@ -84,8 +83,7 @@ import java.util.Map; ...@@ -84,8 +83,7 @@ import java.util.Map;
* @return This builder. * @return This builder.
*/ */
public Builder add(String headerName, String headerValue) { public Builder add(String headerName, String headerValue) {
namesAndValues.add(headerName.trim()); namesAndValuesBuilder.put(Ascii.toLowerCase(headerName.trim()), headerValue.trim());
namesAndValues.add(headerValue.trim());
return this; return this;
} }
...@@ -130,37 +128,38 @@ import java.util.Map; ...@@ -130,37 +128,38 @@ import java.util.Map;
} }
} }
private final ImmutableList<String> namesAndValues; private final ImmutableListMultimap<String, String> namesAndValues;
/** /**
* Gets the headers as a map, where the keys are the header names and values are the header * Returns a map that associates header names to the list of values associated with the
* values. * corresponding header name.
*
* @return The headers as a map. The keys of the map have follows those that are used to build
* this {@link RtspHeaders} instance.
*/ */
public ImmutableMap<String, String> asMap() { public ImmutableListMultimap<String, String> asMultiMap() {
Map<String, String> headers = new LinkedHashMap<>(); return namesAndValues;
for (int i = 0; i < namesAndValues.size(); i += 2) {
headers.put(namesAndValues.get(i), namesAndValues.get(i + 1));
}
return ImmutableMap.copyOf(headers);
} }
/** /**
* Returns a header value mapped to the argument, {@code null} if the header name is not recorded. * Returns the most recent header value mapped to the argument, {@code null} if the header name is
* not recorded.
*/ */
@Nullable @Nullable
public String get(String headerName) { public String get(String headerName) {
for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) { ImmutableList<String> headerValues = values(headerName);
if (Ascii.equalsIgnoreCase(headerName, namesAndValues.get(i))) { if (headerValues.isEmpty()) {
return namesAndValues.get(i + 1); return null;
}
} }
return null; return Iterables.getLast(headerValues);
}
/**
* Returns a list of header values mapped to the argument, in the addition order. The returned
* list is empty if the header name is not recorded.
*/
public ImmutableList<String> values(String headerName) {
return namesAndValues.get(Ascii.toLowerCase(headerName));
} }
private RtspHeaders(Builder builder) { private RtspHeaders(Builder builder) {
this.namesAndValues = ImmutableList.copyOf(builder.namesAndValues); this.namesAndValues = builder.namesAndValuesBuilder.build();
} }
} }
...@@ -16,33 +16,35 @@ ...@@ -16,33 +16,35 @@
package com.google.android.exoplayer2.source.rtsp; package com.google.android.exoplayer2.source.rtsp;
import static com.google.android.exoplayer2.ExoPlayerLibraryInfo.VERSION_SLASHY;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManagerProvider; import com.google.android.exoplayer2.drm.DrmSessionManagerProvider;
import com.google.android.exoplayer2.source.BaseMediaSource; import com.google.android.exoplayer2.source.BaseMediaSource;
import com.google.android.exoplayer2.source.ForwardingTimeline;
import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.source.SinglePeriodTimeline; import com.google.android.exoplayer2.source.SinglePeriodTimeline;
import com.google.android.exoplayer2.source.rtsp.RtspClient.SessionInfoListener;
import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import java.io.IOException; import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** An Rtsp {@link MediaSource} */ /** An Rtsp {@link MediaSource} */
public final class RtspMediaSource extends BaseMediaSource { public final class RtspMediaSource extends BaseMediaSource {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.rtsp");
}
/** /**
* Factory for {@link RtspMediaSource} * Factory for {@link RtspMediaSource}
* *
...@@ -58,6 +60,40 @@ public final class RtspMediaSource extends BaseMediaSource { ...@@ -58,6 +60,40 @@ public final class RtspMediaSource extends BaseMediaSource {
*/ */
public static final class Factory implements MediaSourceFactory { public static final class Factory implements MediaSourceFactory {
private String userAgent;
private boolean forceUseRtpTcp;
public Factory() {
userAgent = ExoPlayerLibraryInfo.VERSION_SLASHY;
}
/**
* Sets whether to force using TCP as the default RTP transport.
*
* <p>The default value is {@code false}, the source will first try streaming RTSP with UDP. If
* no data is received on the UDP channel (for instance, when streaming behind a NAT) for a
* while, the source will switch to streaming using TCP. If this value is set to {@code true},
* the source will always use TCP for streaming.
*
* @param forceUseRtpTcp Whether force to use TCP for streaming.
* @return This Factory, for convenience.
*/
public Factory setForceUseRtpTcp(boolean forceUseRtpTcp) {
this.forceUseRtpTcp = forceUseRtpTcp;
return this;
}
/**
* Sets the user agent, the default value is {@link ExoPlayerLibraryInfo#VERSION_SLASHY}.
*
* @param userAgent The user agent.
* @return This Factory, for convenience.
*/
public Factory setUserAgent(String userAgent) {
this.userAgent = userAgent;
return this;
}
/** Does nothing. {@link RtspMediaSource} does not support DRM. */ /** Does nothing. {@link RtspMediaSource} does not support DRM. */
@Override @Override
public Factory setDrmSessionManagerProvider( public Factory setDrmSessionManagerProvider(
...@@ -122,7 +158,12 @@ public final class RtspMediaSource extends BaseMediaSource { ...@@ -122,7 +158,12 @@ public final class RtspMediaSource extends BaseMediaSource {
@Override @Override
public RtspMediaSource createMediaSource(MediaItem mediaItem) { public RtspMediaSource createMediaSource(MediaItem mediaItem) {
checkNotNull(mediaItem.playbackProperties); checkNotNull(mediaItem.playbackProperties);
return new RtspMediaSource(mediaItem); return new RtspMediaSource(
mediaItem,
forceUseRtpTcp
? new TransferRtpDataChannelFactory()
: new UdpDataSourceRtpDataChannelFactory(),
userAgent);
} }
} }
...@@ -143,34 +184,32 @@ public final class RtspMediaSource extends BaseMediaSource { ...@@ -143,34 +184,32 @@ public final class RtspMediaSource extends BaseMediaSource {
private final MediaItem mediaItem; private final MediaItem mediaItem;
private final RtpDataChannel.Factory rtpDataChannelFactory; private final RtpDataChannel.Factory rtpDataChannelFactory;
private @MonotonicNonNull RtspClient rtspClient; private final String userAgent;
private final Uri uri;
@Nullable private ImmutableList<RtspMediaTrack> rtspMediaTracks; private long timelineDurationUs;
@Nullable private IOException sourcePrepareException; private boolean timelineIsSeekable;
private boolean timelineIsLive;
private boolean timelineIsPlaceholder;
private RtspMediaSource(MediaItem mediaItem) { private RtspMediaSource(
MediaItem mediaItem, RtpDataChannel.Factory rtpDataChannelFactory, String userAgent) {
this.mediaItem = mediaItem; this.mediaItem = mediaItem;
rtpDataChannelFactory = new UdpDataSourceRtpDataChannelFactory(); this.rtpDataChannelFactory = rtpDataChannelFactory;
this.userAgent = userAgent;
this.uri = checkNotNull(this.mediaItem.playbackProperties).uri;
this.timelineDurationUs = C.TIME_UNSET;
this.timelineIsPlaceholder = true;
} }
@Override @Override
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
checkNotNull(mediaItem.playbackProperties); notifySourceInfoRefreshed();
try {
rtspClient =
new RtspClient(
new SessionInfoListenerImpl(),
/* userAgent= */ VERSION_SLASHY,
mediaItem.playbackProperties.uri);
rtspClient.start();
} catch (IOException e) {
sourcePrepareException = new RtspPlaybackException("RtspClient not opened.", e);
}
} }
@Override @Override
protected void releaseSourceInternal() { protected void releaseSourceInternal() {
Util.closeQuietly(rtspClient); // Do nothing.
} }
@Override @Override
...@@ -179,16 +218,24 @@ public final class RtspMediaSource extends BaseMediaSource { ...@@ -179,16 +218,24 @@ public final class RtspMediaSource extends BaseMediaSource {
} }
@Override @Override
public void maybeThrowSourceInfoRefreshError() throws IOException { public void maybeThrowSourceInfoRefreshError() {
if (sourcePrepareException != null) { // Do nothing.
throw sourcePrepareException;
}
} }
@Override @Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
return new RtspMediaPeriod( return new RtspMediaPeriod(
allocator, checkNotNull(rtspMediaTracks), checkNotNull(rtspClient), rtpDataChannelFactory); allocator,
rtpDataChannelFactory,
uri,
(timing) -> {
timelineDurationUs = C.msToUs(timing.getDurationMs());
timelineIsSeekable = !timing.isLive();
timelineIsLive = timing.isLive();
timelineIsPlaceholder = false;
notifySourceInfoRefreshed();
},
userAgent);
} }
@Override @Override
...@@ -196,28 +243,36 @@ public final class RtspMediaSource extends BaseMediaSource { ...@@ -196,28 +243,36 @@ public final class RtspMediaSource extends BaseMediaSource {
((RtspMediaPeriod) mediaPeriod).release(); ((RtspMediaPeriod) mediaPeriod).release();
} }
private final class SessionInfoListenerImpl implements SessionInfoListener { // Internal methods.
@Override
public void onSessionTimelineUpdated(
RtspSessionTiming timing, ImmutableList<RtspMediaTrack> tracks) {
rtspMediaTracks = tracks;
refreshSourceInfo(
new SinglePeriodTimeline(
/* durationUs= */ C.msToUs(timing.getDurationMs()),
/* isSeekable= */ !timing.isLive(),
/* isDynamic= */ false,
/* useLiveConfiguration= */ timing.isLive(),
/* manifest= */ null,
mediaItem));
}
@Override private void notifySourceInfoRefreshed() {
public void onSessionTimelineRequestFailed(String message, @Nullable Throwable cause) { Timeline timeline =
if (cause == null) { new SinglePeriodTimeline(
sourcePrepareException = new RtspPlaybackException(message); timelineDurationUs,
} else { timelineIsSeekable,
sourcePrepareException = new RtspPlaybackException(message, castNonNull(cause)); /* isDynamic= */ false,
} /* useLiveConfiguration= */ timelineIsLive,
/* manifest= */ null,
mediaItem);
if (timelineIsPlaceholder) {
timeline =
new ForwardingTimeline(timeline) {
@Override
public Window getWindow(
int windowIndex, Window window, long defaultPositionProjectionUs) {
super.getWindow(windowIndex, window, defaultPositionProjectionUs);
window.isPlaceholder = true;
return window;
}
@Override
public Period getPeriod(int periodIndex, Period period, boolean setIds) {
super.getPeriod(periodIndex, period, setIds);
period.isPlaceholder = true;
return period;
}
};
} }
refreshSourceInfo(timeline);
} }
} }
...@@ -31,6 +31,7 @@ import androidx.annotation.VisibleForTesting; ...@@ -31,6 +31,7 @@ import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.audio.AacUtil; import com.google.android.exoplayer2.audio.AacUtil;
import com.google.android.exoplayer2.util.CodecSpecificDataUtil;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.NalUnitUtil; import com.google.android.exoplayer2.util.NalUnitUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
...@@ -171,10 +172,6 @@ import com.google.common.collect.ImmutableMap; ...@@ -171,10 +172,6 @@ import com.google.common.collect.ImmutableMap;
private static void processH264FmtpAttribute( private static void processH264FmtpAttribute(
Format.Builder formatBuilder, ImmutableMap<String, String> fmtpAttributes) { Format.Builder formatBuilder, ImmutableMap<String, String> fmtpAttributes) {
checkArgument(fmtpAttributes.containsKey(PARAMETER_PROFILE_LEVEL_ID));
String profileLevel = checkNotNull(fmtpAttributes.get(PARAMETER_PROFILE_LEVEL_ID));
formatBuilder.setCodecs(H264_CODECS_PREFIX + profileLevel);
checkArgument(fmtpAttributes.containsKey(PARAMETER_SPROP_PARAMS)); checkArgument(fmtpAttributes.containsKey(PARAMETER_SPROP_PARAMS));
String spropParameterSets = checkNotNull(fmtpAttributes.get(PARAMETER_SPROP_PARAMS)); String spropParameterSets = checkNotNull(fmtpAttributes.get(PARAMETER_SPROP_PARAMS));
String[] parameterSets = Util.split(spropParameterSets, ","); String[] parameterSets = Util.split(spropParameterSets, ",");
...@@ -193,6 +190,15 @@ import com.google.common.collect.ImmutableMap; ...@@ -193,6 +190,15 @@ import com.google.common.collect.ImmutableMap;
formatBuilder.setPixelWidthHeightRatio(spsData.pixelWidthAspectRatio); formatBuilder.setPixelWidthHeightRatio(spsData.pixelWidthAspectRatio);
formatBuilder.setHeight(spsData.height); formatBuilder.setHeight(spsData.height);
formatBuilder.setWidth(spsData.width); formatBuilder.setWidth(spsData.width);
@Nullable String profileLevel = fmtpAttributes.get(PARAMETER_PROFILE_LEVEL_ID);
if (profileLevel != null) {
formatBuilder.setCodecs(H264_CODECS_PREFIX + profileLevel);
} else {
formatBuilder.setCodecs(
CodecSpecificDataUtil.buildAvcCodecString(
spsData.profileIdc, spsData.constraintsFlagsAndReservedZero2Bits, spsData.levelIdc));
}
} }
private static byte[] getH264InitializationDataFromParameterSet(String parameterSet) { private static byte[] getH264InitializationDataFromParameterSet(String parameterSet) {
......
...@@ -30,6 +30,7 @@ import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_TEARD ...@@ -30,6 +30,7 @@ import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_TEARD
import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_UNSET; import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_UNSET;
import static com.google.android.exoplayer2.util.Assertions.checkArgument; import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.common.base.Strings.nullToEmpty;
import static java.util.regex.Pattern.CASE_INSENSITIVE; import static java.util.regex.Pattern.CASE_INSENSITIVE;
import android.net.Uri; import android.net.Uri;
...@@ -37,10 +38,10 @@ import androidx.annotation.Nullable; ...@@ -37,10 +38,10 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Charsets; import com.google.common.base.Ascii;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableListMultimap;
import java.util.List; import java.util.List;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
...@@ -64,6 +65,20 @@ import java.util.regex.Pattern; ...@@ -64,6 +65,20 @@ import java.util.regex.Pattern;
} }
} }
/** Wraps username and password for authentication purposes. */
public static final class RtspAuthUserInfo {
/** The username. */
public final String username;
/** The password. */
public final String password;
/** Creates a new instance. */
public RtspAuthUserInfo(String username, String password) {
this.username = username;
this.password = password;
}
}
/** The default timeout, in milliseconds, defined for RTSP (RFC2326 Section 12.37). */ /** The default timeout, in milliseconds, defined for RTSP (RFC2326 Section 12.37). */
public static final long DEFAULT_RTSP_TIMEOUT_MS = 60_000; public static final long DEFAULT_RTSP_TIMEOUT_MS = 60_000;
...@@ -81,7 +96,20 @@ import java.util.regex.Pattern; ...@@ -81,7 +96,20 @@ import java.util.regex.Pattern;
private static final Pattern SESSION_HEADER_PATTERN = private static final Pattern SESSION_HEADER_PATTERN =
Pattern.compile("(\\w+)(?:;\\s?timeout=(\\d+))?"); Pattern.compile("(\\w+)(?:;\\s?timeout=(\\d+))?");
// WWW-Authenticate header pattern, see RFC2068 Sections 14.46 and RFC2069.
private static final Pattern WWW_AUTHENTICATION_HEADER_DIGEST_PATTERN =
Pattern.compile(
"Digest realm=\"([\\w\\s@.]+)\""
+ ",\\s?(?:domain=\"(.+)\",\\s?)?"
+ "nonce=\"(\\w+)\""
+ "(?:,\\s?opaque=\"(\\w+)\")?");
// WWW-Authenticate header pattern, see RFC2068 Section 11.1 and RFC2069.
private static final Pattern WWW_AUTHENTICATION_HEADER_BASIC_PATTERN =
Pattern.compile("Basic realm=\"([\\w\\s@.]+)\"");
private static final String RTSP_VERSION = "RTSP/1.0"; private static final String RTSP_VERSION = "RTSP/1.0";
private static final String LF = new String(new byte[] {Ascii.LF});
private static final String CRLF = new String(new byte[] {Ascii.CR, Ascii.LF});
/** /**
* Serializes an {@link RtspRequest} to an {@link ImmutableList} of strings. * Serializes an {@link RtspRequest} to an {@link ImmutableList} of strings.
...@@ -95,11 +123,13 @@ import java.util.regex.Pattern; ...@@ -95,11 +123,13 @@ import java.util.regex.Pattern;
builder.add( builder.add(
Util.formatInvariant( Util.formatInvariant(
"%s %s %s", toMethodString(request.method), request.uri, RTSP_VERSION)); "%s %s %s", toMethodString(request.method), request.uri, RTSP_VERSION));
ImmutableMap<String, String> headers = request.headers.asMap();
ImmutableListMultimap<String, String> headers = request.headers.asMultiMap();
for (String headerName : headers.keySet()) { for (String headerName : headers.keySet()) {
builder.add( ImmutableList<String> headerValuesForName = headers.get(headerName);
Util.formatInvariant( for (int i = 0; i < headerValuesForName.size(); i++) {
"%s: %s", headerName, checkNotNull(request.headers.get(headerName)))); builder.add(Util.formatInvariant("%s: %s", headerName, headerValuesForName.get(i)));
}
} }
// Empty line after headers. // Empty line after headers.
builder.add(""); builder.add("");
...@@ -120,11 +150,12 @@ import java.util.regex.Pattern; ...@@ -120,11 +150,12 @@ import java.util.regex.Pattern;
Util.formatInvariant( Util.formatInvariant(
"%s %s %s", RTSP_VERSION, response.status, getRtspStatusReasonPhrase(response.status))); "%s %s %s", RTSP_VERSION, response.status, getRtspStatusReasonPhrase(response.status)));
ImmutableMap<String, String> headers = response.headers.asMap(); ImmutableListMultimap<String, String> headers = response.headers.asMultiMap();
for (String headerName : headers.keySet()) { for (String headerName : headers.keySet()) {
builder.add( ImmutableList<String> headerValuesForName = headers.get(headerName);
Util.formatInvariant( for (int i = 0; i < headerValuesForName.size(); i++) {
"%s: %s", headerName, checkNotNull(response.headers.get(headerName)))); builder.add(Util.formatInvariant("%s: %s", headerName, headerValuesForName.get(i)));
}
} }
// Empty line after headers. // Empty line after headers.
builder.add(""); builder.add("");
...@@ -139,7 +170,7 @@ import java.util.regex.Pattern; ...@@ -139,7 +170,7 @@ import java.util.regex.Pattern;
* removed. * removed.
*/ */
public static byte[] convertMessageToByteArray(List<String> message) { public static byte[] convertMessageToByteArray(List<String> message) {
return Joiner.on("\r\n").join(message).getBytes(Charsets.UTF_8); return Joiner.on(CRLF).join(message).getBytes(RtspMessageChannel.CHARSET);
} }
/** Removes the user info from the supplied {@link Uri}. */ /** Removes the user info from the supplied {@link Uri}. */
...@@ -155,10 +186,35 @@ import java.util.regex.Pattern; ...@@ -155,10 +186,35 @@ import java.util.regex.Pattern;
return uri.buildUpon().encodedAuthority(authority).build(); return uri.buildUpon().encodedAuthority(authority).build();
} }
/**
* Parses the user info encapsulated in the RTSP {@link Uri}.
*
* @param uri The {@link Uri}.
* @return The extracted {@link RtspAuthUserInfo}, {@code null} if the argument {@link Uri} does
* not contain userinfo, or it's not properly formatted.
*/
@Nullable
public static RtspAuthUserInfo parseUserInfo(Uri uri) {
@Nullable String userInfo = uri.getUserInfo();
if (userInfo == null) {
return null;
}
if (userInfo.contains(":")) {
String[] userInfoStrings = Util.splitAtFirst(userInfo, ":");
return new RtspAuthUserInfo(userInfoStrings[0], userInfoStrings[1]);
}
return null;
}
/** Returns the byte array representation of a string, using RTSP's character encoding. */
public static byte[] getStringBytes(String s) {
return s.getBytes(RtspMessageChannel.CHARSET);
}
/** Returns the corresponding String representation of the {@link RtspRequest.Method} argument. */ /** Returns the corresponding String representation of the {@link RtspRequest.Method} argument. */
public static String toMethodString(@RtspRequest.Method int method) { public static String toMethodString(@RtspRequest.Method int method) {
switch (method) { switch (method) {
case RtspRequest.METHOD_ANNOUNCE: case METHOD_ANNOUNCE:
return "ANNOUNCE"; return "ANNOUNCE";
case METHOD_DESCRIBE: case METHOD_DESCRIBE:
return "DESCRIBE"; return "DESCRIBE";
...@@ -238,7 +294,7 @@ import java.util.regex.Pattern; ...@@ -238,7 +294,7 @@ import java.util.regex.Pattern;
List<String> headerLines = lines.subList(1, messageBodyOffset); List<String> headerLines = lines.subList(1, messageBodyOffset);
RtspHeaders headers = new RtspHeaders.Builder().addAll(headerLines).build(); RtspHeaders headers = new RtspHeaders.Builder().addAll(headerLines).build();
String messageBody = Joiner.on("\r\n").join(lines.subList(messageBodyOffset + 1, lines.size())); String messageBody = Joiner.on(CRLF).join(lines.subList(messageBodyOffset + 1, lines.size()));
return new RtspResponse(statusCode, headers, messageBody); return new RtspResponse(statusCode, headers, messageBody);
} }
...@@ -261,7 +317,7 @@ import java.util.regex.Pattern; ...@@ -261,7 +317,7 @@ import java.util.regex.Pattern;
List<String> headerLines = lines.subList(1, messageBodyOffset); List<String> headerLines = lines.subList(1, messageBodyOffset);
RtspHeaders headers = new RtspHeaders.Builder().addAll(headerLines).build(); RtspHeaders headers = new RtspHeaders.Builder().addAll(headerLines).build();
String messageBody = Joiner.on("\r\n").join(lines.subList(messageBodyOffset + 1, lines.size())); String messageBody = Joiner.on(CRLF).join(lines.subList(messageBodyOffset + 1, lines.size()));
return new RtspRequest(requestUri, method, headers, messageBody); return new RtspRequest(requestUri, method, headers, messageBody);
} }
...@@ -271,6 +327,11 @@ import java.util.regex.Pattern; ...@@ -271,6 +327,11 @@ import java.util.regex.Pattern;
|| STATUS_LINE_PATTERN.matcher(line).matches(); || STATUS_LINE_PATTERN.matcher(line).matches();
} }
/** Returns the lines in an RTSP message body split by the line terminator used in body. */
public static String[] splitRtspMessageBody(String body) {
return Util.split(body, body.contains(CRLF) ? CRLF : LF);
}
/** /**
* Returns the length in bytes if the line contains a Content-Length header, otherwise {@link * Returns the length in bytes if the line contains a Content-Length header, otherwise {@link
* C#LENGTH_UNSET}. * C#LENGTH_UNSET}.
...@@ -343,6 +404,39 @@ import java.util.regex.Pattern; ...@@ -343,6 +404,39 @@ import java.util.regex.Pattern;
return new RtspSessionHeader(sessionId, timeoutMs); return new RtspSessionHeader(sessionId, timeoutMs);
} }
/**
* Parses a WWW-Authenticate header.
*
* <p>Reference RFC2068 Section 14.46 for WWW-Authenticate header. Only digest and basic
* authentication mechanisms are supported.
*
* @param headerValue The string representation of the content, without the header name
* (WWW-Authenticate: ).
* @return The parsed {@link RtspAuthenticationInfo}.
* @throws ParserException When the input header value does not follow the WWW-Authenticate header
* format, or is not using either Basic or Digest mechanisms.
*/
public static RtspAuthenticationInfo parseWwwAuthenticateHeader(String headerValue)
throws ParserException {
Matcher matcher = WWW_AUTHENTICATION_HEADER_DIGEST_PATTERN.matcher(headerValue);
if (matcher.find()) {
return new RtspAuthenticationInfo(
RtspAuthenticationInfo.DIGEST,
/* realm= */ checkNotNull(matcher.group(1)),
/* nonce= */ checkNotNull(matcher.group(3)),
/* opaque= */ nullToEmpty(matcher.group(4)));
}
matcher = WWW_AUTHENTICATION_HEADER_BASIC_PATTERN.matcher(headerValue);
if (matcher.matches()) {
return new RtspAuthenticationInfo(
RtspAuthenticationInfo.BASIC,
/* realm= */ checkNotNull(matcher.group(1)),
/* nonce= */ "",
/* opaque= */ "");
}
throw new ParserException("Invalid WWW-Authenticate header " + headerValue);
}
private static String getRtspStatusReasonPhrase(int statusCode) { private static String getRtspStatusReasonPhrase(int statusCode) {
switch (statusCode) { switch (statusCode) {
case 200: case 200:
......
...@@ -41,8 +41,6 @@ import java.util.regex.Pattern; ...@@ -41,8 +41,6 @@ import java.util.regex.Pattern;
private static final Pattern MEDIA_DESCRIPTION_PATTERN = private static final Pattern MEDIA_DESCRIPTION_PATTERN =
Pattern.compile("(\\S+)\\s(\\S+)\\s(\\S+)\\s(\\S+)"); Pattern.compile("(\\S+)\\s(\\S+)\\s(\\S+)\\s(\\S+)");
private static final String CRLF = "\r\n";
private static final String VERSION_TYPE = "v"; private static final String VERSION_TYPE = "v";
private static final String ORIGIN_TYPE = "o"; private static final String ORIGIN_TYPE = "o";
private static final String SESSION_TYPE = "s"; private static final String SESSION_TYPE = "s";
...@@ -71,7 +69,7 @@ import java.util.regex.Pattern; ...@@ -71,7 +69,7 @@ import java.util.regex.Pattern;
@Nullable MediaDescription.Builder mediaDescriptionBuilder = null; @Nullable MediaDescription.Builder mediaDescriptionBuilder = null;
// Lines are separated by an CRLF. // Lines are separated by an CRLF.
for (String line : Util.split(sdpString, CRLF)) { for (String line : RtspMessageUtil.splitRtspMessageBody(sdpString)) {
if ("".equals(line)) { if ("".equals(line)) {
continue; continue;
} }
...@@ -188,7 +186,7 @@ import java.util.regex.Pattern; ...@@ -188,7 +186,7 @@ import java.util.regex.Pattern;
try { try {
return sessionDescriptionBuilder.build(); return sessionDescriptionBuilder.build();
} catch (IllegalStateException e) { } catch (IllegalArgumentException | IllegalStateException e) {
throw new ParserException(e); throw new ParserException(e);
} }
} }
...@@ -199,7 +197,7 @@ import java.util.regex.Pattern; ...@@ -199,7 +197,7 @@ import java.util.regex.Pattern;
throws ParserException { throws ParserException {
try { try {
sessionDescriptionBuilder.addMediaDescription(mediaDescriptionBuilder.build()); sessionDescriptionBuilder.addMediaDescription(mediaDescriptionBuilder.build());
} catch (IllegalStateException e) { } catch (IllegalArgumentException | IllegalStateException e) {
throw new ParserException(e); throw new ParserException(e);
} }
} }
......
...@@ -22,6 +22,7 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; ...@@ -22,6 +22,7 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.rtsp.RtspMessageChannel.InterleavedBinaryDataListener;
import com.google.android.exoplayer2.upstream.BaseDataSource; import com.google.android.exoplayer2.upstream.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
...@@ -31,7 +32,8 @@ import java.util.Arrays; ...@@ -31,7 +32,8 @@ import java.util.Arrays;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
/** An {@link RtpDataChannel} that transfers received data in-memory. */ /** An {@link RtpDataChannel} that transfers received data in-memory. */
/* package */ final class TransferRtpDataChannel extends BaseDataSource implements RtpDataChannel { /* package */ final class TransferRtpDataChannel extends BaseDataSource
implements RtpDataChannel, RtspMessageChannel.InterleavedBinaryDataListener {
private static final String DEFAULT_TCP_TRANSPORT_FORMAT = private static final String DEFAULT_TCP_TRANSPORT_FORMAT =
"RTP/AVP/TCP;unicast;interleaved=%d-%d"; "RTP/AVP/TCP;unicast;interleaved=%d-%d";
...@@ -62,8 +64,8 @@ import java.util.concurrent.LinkedBlockingQueue; ...@@ -62,8 +64,8 @@ import java.util.concurrent.LinkedBlockingQueue;
} }
@Override @Override
public boolean usesSidebandBinaryData() { public InterleavedBinaryDataListener getInterleavedBinaryDataListener() {
return true; return this;
} }
@Override @Override
...@@ -119,7 +121,7 @@ import java.util.concurrent.LinkedBlockingQueue; ...@@ -119,7 +121,7 @@ import java.util.concurrent.LinkedBlockingQueue;
} }
@Override @Override
public void write(byte[] buffer) { public void onInterleavedBinaryDataReceived(byte[] data) {
packetQueue.add(buffer); packetQueue.add(data);
} }
} }
...@@ -55,6 +55,12 @@ import java.io.IOException; ...@@ -55,6 +55,12 @@ import java.io.IOException;
return port == UdpDataSource.UDP_PORT_UNSET ? C.INDEX_UNSET : port; return port == UdpDataSource.UDP_PORT_UNSET ? C.INDEX_UNSET : port;
} }
@Nullable
@Override
public RtspMessageChannel.InterleavedBinaryDataListener getInterleavedBinaryDataListener() {
return null;
}
@Override @Override
public void addTransferListener(TransferListener transferListener) { public void addTransferListener(TransferListener transferListener) {
dataSource.addTransferListener(transferListener); dataSource.addTransferListener(transferListener);
...@@ -85,20 +91,6 @@ import java.io.IOException; ...@@ -85,20 +91,6 @@ import java.io.IOException;
return dataSource.read(target, offset, length); return dataSource.read(target, offset, length);
} }
@Override
public boolean usesSidebandBinaryData() {
return false;
}
/**
* Writing to a {@link UdpDataSource} backed {@link RtpDataChannel} is not supported at the
* moment.
*/
@Override
public void write(byte[] buffer) {
throw new UnsupportedOperationException();
}
public void setRtcpChannel(UdpDataSourceRtpDataChannel rtcpChannel) { public void setRtcpChannel(UdpDataSourceRtpDataChannel rtcpChannel) {
checkArgument(this != rtcpChannel); checkArgument(this != rtcpChannel);
this.rtcpChannel = rtcpChannel; this.rtcpChannel = rtcpChannel;
......
/*
* Copyright 2021 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.rtsp;
import com.google.android.exoplayer2.ParserException;
import com.google.common.collect.ImmutableList;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/** A value wrapper for a dumped RTP packet stream. */
/* package */ class RtpPacketStreamDump {
/** The name of the RTP track. */
public final String trackName;
/** The sequence number of the first RTP packet in the dump file. */
public final int firstSequenceNumber;
/** The timestamp of the first RTP packet in the dump file. */
public final long firstTimestamp;
/** The interval between transmitting two consecutive RTP packets, in milliseconds. */
public final long transmissionIntervalMs;
/** The description of the dumped media in SDP(RFC2327) format. */
public final String mediaDescription;
/** A list of hex strings. Each hex string represents a binary RTP packet. */
public final ImmutableList<String> packets;
/**
* Parses a JSON string into an {@code RtpPacketStreamDump}.
*
* <p>The input JSON must include the following key-value pairs:
*
* <ul>
* <li>Key: "trackName", Value type: String. The name of the RTP track.
* <li>Key: "firstSequenceNumber", Value type: int. The sequence number of the first RTP packet
* in the dump file.
* <li>Key: "firstTimestamp", Value type: long. The timestamp of the first RTP packet in the
* dump file.
* <li>Key: "transmissionIntervalMs", Value type: long. The interval between transmitting two
* consecutive RTP packets, in milliseconds.
* <li>Key: "mediaDescription", Value type: String. The description of the dumped media in
* SDP(RFC2327) format.
* <li>Key: "packets", Value type: Array of hex strings. Each element is a hex string
* representing an RTP packet's binary data.
* </ul>
*
* @param jsonString The JSON string that contains the dumped RTP packets and metadata.
* @return The parsed {@code RtpDumpFile}.
* @throws ParserException If the argument does not contain all required key-value pairs, or there
* are incorrect values.
*/
public static RtpPacketStreamDump parse(String jsonString) throws ParserException {
try {
JSONObject jsonObject = new JSONObject(jsonString);
String trackName = jsonObject.getString("trackName");
int firstSequenceNumber = jsonObject.getInt("firstSequenceNumber");
long firstTimestamp = jsonObject.getLong("firstTimestamp");
long transmissionIntervalMs = jsonObject.getLong("transmitIntervalMs");
String mediaDescription = jsonObject.getString("mediaDescription");
ImmutableList.Builder<String> packetsBuilder = new ImmutableList.Builder<>();
JSONArray jsonPackets = jsonObject.getJSONArray("packets");
for (int i = 0; i < jsonPackets.length(); i++) {
packetsBuilder.add(jsonPackets.getString(i));
}
return new RtpPacketStreamDump(
trackName,
firstSequenceNumber,
firstTimestamp,
transmissionIntervalMs,
mediaDescription,
packetsBuilder.build());
} catch (JSONException e) {
throw new ParserException(e);
}
}
private RtpPacketStreamDump(
String trackName,
int firstSequenceNumber,
long firstTimestamp,
long transmissionIntervalMs,
String mediaDescription,
ImmutableList<String> packets) {
this.trackName = trackName;
this.firstSequenceNumber = firstSequenceNumber;
this.firstTimestamp = firstTimestamp;
this.transmissionIntervalMs = transmissionIntervalMs;
this.mediaDescription = mediaDescription;
this.packets = ImmutableList.copyOf(packets);
}
}
/*
* Copyright 2021 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.rtsp;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.source.rtsp.RtspMessageUtil.RtspAuthUserInfo;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link RtspAuthenticationInfo}. */
@RunWith(AndroidJUnit4.class)
public class RtspAuthenticationInfoTest {
@Test
public void getAuthorizationHeaderValue_withBasicAuthenticationMechanism_getsCorrectHeaderValue()
throws Exception {
String authenticationRealm = "WallyWorld";
String username = "Aladdin";
String password = "open sesame";
String expectedAuthorizationHeaderValue = "QWxhZGRpbjpvcGVuIHNlc2FtZQ==\n";
RtspAuthenticationInfo authenticator =
new RtspAuthenticationInfo(
RtspAuthenticationInfo.BASIC, authenticationRealm, /* nonce= */ "", /* opaque= */ "");
assertThat(
authenticator.getAuthorizationHeaderValue(
new RtspAuthUserInfo(username, password), Uri.EMPTY, RtspRequest.METHOD_DESCRIBE))
.isEqualTo(expectedAuthorizationHeaderValue);
}
@Test
public void getAuthorizationHeaderValue_withDigestAuthenticationMechanism_getsCorrectHeaderValue()
throws Exception {
RtspAuthenticationInfo authenticator =
new RtspAuthenticationInfo(
RtspAuthenticationInfo.DIGEST,
/* realm= */ "LIVE555 Streaming Media",
/* nonce= */ "0cdfe9719e7373b7d5bb2913e2115f3f",
/* opaque= */ "5ccc069c403ebaf9f0171e9517f40e41");
assertThat(
authenticator.getAuthorizationHeaderValue(
new RtspAuthUserInfo("username", "password"),
Uri.parse("rtsp://localhost:554/imax_cd_2k_264_6ch.mkv"),
RtspRequest.METHOD_DESCRIBE))
.isEqualTo(
"Digest username=\"username\", realm=\"LIVE555 Streaming Media\","
+ " nonce=\"0cdfe9719e7373b7d5bb2913e2115f3f\","
+ " uri=\"rtsp://localhost:554/imax_cd_2k_264_6ch.mkv\","
+ " response=\"ba9433847439387776f7fb905db3fcae\","
+ " opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"");
}
}
...@@ -19,9 +19,13 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull; ...@@ -19,9 +19,13 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.net.Uri; import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.robolectric.RobolectricUtil; import com.google.android.exoplayer2.robolectric.RobolectricUtil;
import com.google.android.exoplayer2.source.rtsp.RtspClient.PlaybackEventListener;
import com.google.android.exoplayer2.source.rtsp.RtspClient.SessionInfoListener; import com.google.android.exoplayer2.source.rtsp.RtspClient.SessionInfoListener;
import com.google.android.exoplayer2.source.rtsp.RtspMediaSource.RtspPlaybackException;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
...@@ -39,8 +43,12 @@ public final class RtspClientTest { ...@@ -39,8 +43,12 @@ public final class RtspClientTest {
private @MonotonicNonNull RtspServer rtspServer; private @MonotonicNonNull RtspServer rtspServer;
@Before @Before
public void setUp() { public void setUp() throws Exception {
rtspServer = new RtspServer(); rtspServer =
new RtspServer(
RtpPacketStreamDump.parse(
TestUtil.getString(
ApplicationProvider.getApplicationContext(), "media/rtsp/aac-dump.json")));
} }
@After @After
...@@ -50,7 +58,7 @@ public final class RtspClientTest { ...@@ -50,7 +58,7 @@ public final class RtspClientTest {
} }
@Test @Test
public void connectServerAndClient_withServerSupportsOnlyOptions_sessionTimelineRequestFails() public void connectServerAndClient_withServerSupportsDescribe_updatesSessionTimeline()
throws Exception { throws Exception {
int serverRtspPortNumber = checkNotNull(rtspServer).startAndGetPortNumber(); int serverRtspPortNumber = checkNotNull(rtspServer).startAndGetPortNumber();
...@@ -60,13 +68,24 @@ public final class RtspClientTest { ...@@ -60,13 +68,24 @@ public final class RtspClientTest {
new SessionInfoListener() { new SessionInfoListener() {
@Override @Override
public void onSessionTimelineUpdated( public void onSessionTimelineUpdated(
RtspSessionTiming timing, ImmutableList<RtspMediaTrack> tracks) {} RtspSessionTiming timing, ImmutableList<RtspMediaTrack> tracks) {
sessionTimelineUpdateEventReceived.set(!tracks.isEmpty());
}
@Override @Override
public void onSessionTimelineRequestFailed( public void onSessionTimelineRequestFailed(
String message, @Nullable Throwable cause) { String message, @Nullable Throwable cause) {}
sessionTimelineUpdateEventReceived.set(true); },
} new PlaybackEventListener() {
@Override
public void onRtspSetupCompleted() {}
@Override
public void onPlaybackStarted(
long startPositionUs, ImmutableList<RtspTrackTiming> trackTimingList) {}
@Override
public void onPlaybackError(RtspPlaybackException error) {}
}, },
/* userAgent= */ "ExoPlayer:RtspClientTest", /* userAgent= */ "ExoPlayer:RtspClientTest",
/* uri= */ Uri.parse( /* uri= */ Uri.parse(
......
...@@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; ...@@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ListMultimap;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
...@@ -82,7 +83,47 @@ public final class RtspHeadersTest { ...@@ -82,7 +83,47 @@ public final class RtspHeadersTest {
} }
@Test @Test
public void asMap() { public void get_withMultipleValuesMappedToTheSameName_getsTheMostRecentValue() {
RtspHeaders headers =
new RtspHeaders.Builder()
.addAll(
ImmutableList.of(
"WWW-Authenticate: Digest realm=\"2857be52f47f\","
+ " nonce=\"f4cba07ad14b5bf181ac77c5a92ba65f\", stale=\"FALSE\"",
"WWW-Authenticate: Basic realm=\"2857be52f47f\""))
.build();
assertThat(headers.get("WWW-Authenticate")).isEqualTo("Basic realm=\"2857be52f47f\"");
}
@Test
public void values_withNoHeaders_returnsAnEmptyList() {
RtspHeaders headers = new RtspHeaders.Builder().build();
assertThat(headers.values("WWW-Authenticate")).isEmpty();
}
@Test
public void values_withMultipleValuesMappedToTheSameName_returnsAllMappedValues() {
RtspHeaders headers =
new RtspHeaders.Builder()
.addAll(
ImmutableList.of(
"WWW-Authenticate: Digest realm=\"2857be52f47f\","
+ " nonce=\"f4cba07ad14b5bf181ac77c5a92ba65f\", stale=\"FALSE\"",
"WWW-Authenticate: Basic realm=\"2857be52f47f\""))
.build();
assertThat(headers.values("WWW-Authenticate"))
.containsExactly(
"Digest realm=\"2857be52f47f\", nonce=\"f4cba07ad14b5bf181ac77c5a92ba65f\","
+ " stale=\"FALSE\"",
"Basic realm=\"2857be52f47f\"")
.inOrder();
}
@Test
public void asMultiMap_withoutValuesMappedToTheSameName_getsTheMappedValuesInAdditionOrder() {
RtspHeaders headers = RtspHeaders headers =
new RtspHeaders.Builder() new RtspHeaders.Builder()
.addAll( .addAll(
...@@ -92,11 +133,39 @@ public final class RtspHeadersTest { ...@@ -92,11 +133,39 @@ public final class RtspHeadersTest {
"Content-Length: 707", "Content-Length: 707",
"Transport: RTP/AVP;unicast;client_port=65458-65459\r\n")) "Transport: RTP/AVP;unicast;client_port=65458-65459\r\n"))
.build(); .build();
assertThat(headers.asMap()) assertThat(headers.asMultiMap())
.containsExactly(
"accept", "application/sdp",
"cseq", "3",
"content-length", "707",
"transport", "RTP/AVP;unicast;client_port=65458-65459");
}
@Test
public void asMap_withMultipleValuesMappedToTheSameName_getsTheMappedValuesInAdditionOrder() {
RtspHeaders headers =
new RtspHeaders.Builder()
.addAll(
ImmutableList.of(
"Accept: application/sdp ", // Extra space after header value.
"Accept: application/sip ", // Extra space after header value.
"CSeq:3", // No space after colon.
"CSeq:5", // No space after colon.
"Transport: RTP/AVP;unicast;client_port=65456-65457",
"Transport: RTP/AVP;unicast;client_port=65458-65459\r\n"))
.build();
ListMultimap<String, String> headersMap = headers.asMultiMap();
assertThat(headersMap.keySet()).containsExactly("accept", "cseq", "transport").inOrder();
assertThat(headersMap)
.valuesForKey("accept")
.containsExactly("application/sdp", "application/sip")
.inOrder();
assertThat(headersMap).valuesForKey("cseq").containsExactly("3", "5").inOrder();
assertThat(headersMap)
.valuesForKey("transport")
.containsExactly( .containsExactly(
"Accept", "application/sdp", "RTP/AVP;unicast;client_port=65456-65457", "RTP/AVP;unicast;client_port=65458-65459")
"CSeq", "3", .inOrder();
"Content-Length", "707",
"Transport", "RTP/AVP;unicast;client_port=65458-65459");
} }
} }
/*
* Copyright 2021 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.rtsp;
import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runMainLooperUntil;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
import com.google.common.collect.ImmutableList;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link RtspMediaPeriod}. */
@RunWith(AndroidJUnit4.class)
public class RtspMediaPeriodTest {
private static final RtspClient PLACEHOLDER_RTSP_CLIENT =
new RtspClient(
new RtspClient.SessionInfoListener() {
@Override
public void onSessionTimelineUpdated(
RtspSessionTiming timing, ImmutableList<RtspMediaTrack> tracks) {}
@Override
public void onSessionTimelineRequestFailed(String message, @Nullable Throwable cause) {}
},
/* userAgent= */ null,
Uri.EMPTY);
@Test
public void prepare_startsLoading() throws Exception {
RtspMediaPeriod rtspMediaPeriod =
new RtspMediaPeriod(
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
ImmutableList.of(
new RtspMediaTrack(
new MediaDescription.Builder(
/* mediaType= */ MediaDescription.MEDIA_TYPE_VIDEO,
/* port= */ 0,
/* transportProtocol= */ MediaDescription.RTP_AVP_PROFILE,
/* payloadType= */ 96)
.setConnection("IN IP4 0.0.0.0")
.setBitrate(500_000)
.addAttribute(SessionDescription.ATTR_RTPMAP, "96 H264/90000")
.addAttribute(
SessionDescription.ATTR_FMTP,
"96 packetization-mode=1;profile-level-id=64001F;sprop-parameter-sets=Z2QAH6zZQPARabIAAAMACAAAAwGcHjBjLA==,aOvjyyLA")
.addAttribute(SessionDescription.ATTR_CONTROL, "track1")
.build(),
Uri.parse("rtsp://localhost/test"))),
PLACEHOLDER_RTSP_CLIENT,
new UdpDataSourceRtpDataChannelFactory());
AtomicBoolean prepareCallbackCalled = new AtomicBoolean(false);
rtspMediaPeriod.prepare(
new MediaPeriod.Callback() {
@Override
public void onPrepared(MediaPeriod mediaPeriod) {
prepareCallbackCalled.set(true);
}
@Override
public void onContinueLoadingRequested(MediaPeriod source) {
source.continueLoading(/* positionUs= */ 0);
}
},
/* positionUs= */ 0);
runMainLooperUntil(prepareCallbackCalled::get);
rtspMediaPeriod.release();
}
@Test
public void getBufferedPositionUs_withNoRtspMediaTracks_returnsEndOfSource() {
RtspMediaPeriod rtspMediaPeriod =
new RtspMediaPeriod(
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
ImmutableList.of(),
PLACEHOLDER_RTSP_CLIENT,
new UdpDataSourceRtpDataChannelFactory());
assertThat(rtspMediaPeriod.getBufferedPositionUs()).isEqualTo(C.TIME_END_OF_SOURCE);
}
}
...@@ -176,6 +176,23 @@ public class RtspMediaTrackTest { ...@@ -176,6 +176,23 @@ public class RtspMediaTrackTest {
@Test @Test
public void public void
generatePayloadFormat_withH264MediaDescriptionMissingProfileLevel_generatesCorrectProfileLevel() {
MediaDescription mediaDescription =
new MediaDescription.Builder(MEDIA_TYPE_VIDEO, 0, RTP_AVP_PROFILE, 96)
.setConnection("IN IP4 0.0.0.0")
.setBitrate(500_000)
.addAttribute(ATTR_RTPMAP, "96 H264/90000")
.addAttribute(
ATTR_FMTP,
"96 packetization-mode=1;sprop-parameter-sets=Z2QAH6zZQPARabIAAAMACAAAAwGcHjBjLA==,aOvjyyLA")
.addAttribute(ATTR_CONTROL, "track1")
.build();
RtpPayloadFormat rtpPayloadFormat = RtspMediaTrack.generatePayloadFormat(mediaDescription);
assertThat(rtpPayloadFormat.format.codecs).isEqualTo("avc1.64001F");
}
@Test
public void
generatePayloadFormat_withAacMediaDescriptionMissingFmtpAttribute_throwsIllegalArgumentException() { generatePayloadFormat_withAacMediaDescriptionMissingFmtpAttribute_throwsIllegalArgumentException() {
MediaDescription mediaDescription = MediaDescription mediaDescription =
new MediaDescription.Builder(MEDIA_TYPE_AUDIO, 0, RTP_AVP_PROFILE, 97) new MediaDescription.Builder(MEDIA_TYPE_AUDIO, 0, RTP_AVP_PROFILE, 97)
...@@ -224,24 +241,6 @@ public class RtspMediaTrackTest { ...@@ -224,24 +241,6 @@ public class RtspMediaTrackTest {
@Test @Test
public void public void
generatePayloadFormat_withH264MediaDescriptionMissingProfileLevel_throwsIllegalArgumentException() {
MediaDescription mediaDescription =
new MediaDescription.Builder(MEDIA_TYPE_VIDEO, 0, RTP_AVP_PROFILE, 96)
.setConnection("IN IP4 0.0.0.0")
.setBitrate(500_000)
.addAttribute(ATTR_RTPMAP, "96 H264/90000")
.addAttribute(
ATTR_FMTP,
"96 packetization-mode=1;sprop-parameter-sets=Z2QAH6zZQPARabIAAAMACAAAAwGcHjBjLA==,aOvjyyLA")
.addAttribute(ATTR_CONTROL, "track1")
.build();
assertThrows(
IllegalArgumentException.class,
() -> RtspMediaTrack.generatePayloadFormat(mediaDescription));
}
@Test
public void
generatePayloadFormat_withH264MediaDescriptionMissingSpropParameter_throwsIllegalArgumentException() { generatePayloadFormat_withH264MediaDescriptionMissingSpropParameter_throwsIllegalArgumentException() {
MediaDescription mediaDescription = MediaDescription mediaDescription =
new MediaDescription.Builder(MEDIA_TYPE_VIDEO, 0, RTP_AVP_PROFILE, 96) new MediaDescription.Builder(MEDIA_TYPE_VIDEO, 0, RTP_AVP_PROFILE, 96)
......
...@@ -22,7 +22,6 @@ import static com.google.common.truth.Truth.assertThat; ...@@ -22,7 +22,6 @@ import static com.google.common.truth.Truth.assertThat;
import android.net.Uri; import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.robolectric.RobolectricUtil; import com.google.android.exoplayer2.robolectric.RobolectricUtil;
import com.google.android.exoplayer2.source.rtsp.RtspMessageChannel.MessageListener;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.LinkedListMultimap;
...@@ -67,11 +66,21 @@ public final class RtspMessageChannelTest { ...@@ -67,11 +66,21 @@ public final class RtspMessageChannelTest {
.build(), .build(),
"v=安卓アンドロイド\r\n"); "v=安卓アンドロイド\r\n");
RtspResponse describeResponse2 =
new RtspResponse(
200,
new RtspHeaders.Builder()
.add(RtspHeaders.CSEQ, "4")
.add(RtspHeaders.CONTENT_TYPE, "application/sdp")
.add(RtspHeaders.CONTENT_LENGTH, "73")
.build(),
"v=安卓アンドロイド\n" + "o=test 2890844526 2890842807 IN IP4 127.0.0.1\n");
RtspResponse setupResponse = RtspResponse setupResponse =
new RtspResponse( new RtspResponse(
200, 200,
new RtspHeaders.Builder() new RtspHeaders.Builder()
.add(RtspHeaders.CSEQ, "3") .add(RtspHeaders.CSEQ, "5")
.add(RtspHeaders.TRANSPORT, "RTP/AVP/TCP;unicast;interleaved=0-1") .add(RtspHeaders.TRANSPORT, "RTP/AVP/TCP;unicast;interleaved=0-1")
.build(), .build(),
""); "");
...@@ -84,6 +93,7 @@ public final class RtspMessageChannelTest { ...@@ -84,6 +93,7 @@ public final class RtspMessageChannelTest {
AtomicBoolean receivingFinished = new AtomicBoolean(); AtomicBoolean receivingFinished = new AtomicBoolean();
AtomicReference<Exception> sendingException = new AtomicReference<>(); AtomicReference<Exception> sendingException = new AtomicReference<>();
List<List<String>> receivedRtspResponses = new ArrayList<>(/* initialCapacity= */ 3); List<List<String>> receivedRtspResponses = new ArrayList<>(/* initialCapacity= */ 3);
// Key: channel number, Value: a list of received byte arrays.
Multimap<Integer, List<Byte>> receivedInterleavedData = LinkedListMultimap.create(); Multimap<Integer, List<Byte>> receivedInterleavedData = LinkedListMultimap.create();
ServerSocket serverSocket = ServerSocket serverSocket =
new ServerSocket(/* port= */ 0, /* backlog= */ 1, InetAddress.getByName(/* host= */ null)); new ServerSocket(/* port= */ 0, /* backlog= */ 1, InetAddress.getByName(/* host= */ null));
...@@ -97,6 +107,8 @@ public final class RtspMessageChannelTest { ...@@ -97,6 +107,8 @@ public final class RtspMessageChannelTest {
convertMessageToByteArray(serializeResponse(optionsResponse))); convertMessageToByteArray(serializeResponse(optionsResponse)));
serverOutputStream.write( serverOutputStream.write(
convertMessageToByteArray(serializeResponse(describeResponse))); convertMessageToByteArray(serializeResponse(describeResponse)));
serverOutputStream.write(
convertMessageToByteArray(serializeResponse(describeResponse2)));
serverOutputStream.write(Bytes.concat(new byte[] {'$'}, interleavedData1)); serverOutputStream.write(Bytes.concat(new byte[] {'$'}, interleavedData1));
serverOutputStream.write(Bytes.concat(new byte[] {'$'}, interleavedData2)); serverOutputStream.write(Bytes.concat(new byte[] {'$'}, interleavedData2));
serverOutputStream.write( serverOutputStream.write(
...@@ -116,21 +128,19 @@ public final class RtspMessageChannelTest { ...@@ -116,21 +128,19 @@ public final class RtspMessageChannelTest {
RtspMessageChannel rtspMessageChannel = RtspMessageChannel rtspMessageChannel =
new RtspMessageChannel( new RtspMessageChannel(
new MessageListener() { message -> {
@Override receivedRtspResponses.add(message);
public void onRtspMessageReceived(List<String> message) { if (receivedRtspResponses.size() == 4 && receivedInterleavedData.size() == 2) {
receivedRtspResponses.add(message); receivingFinished.set(true);
if (receivedRtspResponses.size() == 3 && receivedInterleavedData.size() == 2) {
receivingFinished.set(true);
}
}
@Override
public void onInterleavedBinaryDataReceived(byte[] data, int channel) {
receivedInterleavedData.put(channel, Bytes.asList(data));
} }
}); });
rtspMessageChannel.openSocket(clientSideSocket);
rtspMessageChannel.registerInterleavedBinaryDataListener(
/* channel= */ 0, data -> receivedInterleavedData.put(0, Bytes.asList(data)));
rtspMessageChannel.registerInterleavedBinaryDataListener(
/* channel= */ 1, data -> receivedInterleavedData.put(1, Bytes.asList(data)));
rtspMessageChannel.open(clientSideSocket);
RobolectricUtil.runMainLooperUntil(receivingFinished::get); RobolectricUtil.runMainLooperUntil(receivingFinished::get);
Util.closeQuietly(rtspMessageChannel); Util.closeQuietly(rtspMessageChannel);
...@@ -141,18 +151,26 @@ public final class RtspMessageChannelTest { ...@@ -141,18 +151,26 @@ public final class RtspMessageChannelTest {
assertThat(receivedRtspResponses) assertThat(receivedRtspResponses)
.containsExactly( .containsExactly(
/* optionsResponse */ /* optionsResponse */
ImmutableList.of("RTSP/1.0 200 OK", "CSeq: 2", "Public: OPTIONS", ""), ImmutableList.of("RTSP/1.0 200 OK", "cseq: 2", "public: OPTIONS", ""),
/* describeResponse */ /* describeResponse */
ImmutableList.of( ImmutableList.of(
"RTSP/1.0 200 OK", "RTSP/1.0 200 OK",
"CSeq: 3", "cseq: 3",
"Content-Type: application/sdp", "content-type: application/sdp",
"Content-Length: 28", "content-length: 28",
"", "",
"v=安卓アンドロイド"), "v=安卓アンドロイド"),
/* describeResponse2 */
ImmutableList.of(
"RTSP/1.0 200 OK",
"cseq: 4",
"content-type: application/sdp",
"content-length: 73",
"",
"v=安卓アンドロイド\n" + "o=test 2890844526 2890842807 IN IP4 127.0.0.1"),
/* setupResponse */ /* setupResponse */
ImmutableList.of( ImmutableList.of(
"RTSP/1.0 200 OK", "CSeq: 3", "Transport: RTP/AVP/TCP;unicast;interleaved=0-1", "")) "RTSP/1.0 200 OK", "cseq: 5", "transport: RTP/AVP/TCP;unicast;interleaved=0-1", ""))
.inOrder(); .inOrder();
assertThat(receivedInterleavedData) assertThat(receivedInterleavedData)
.containsExactly( .containsExactly(
......
...@@ -15,12 +15,15 @@ ...@@ -15,12 +15,15 @@
*/ */
package com.google.android.exoplayer2.source.rtsp; package com.google.android.exoplayer2.source.rtsp;
import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_DESCRIBE;
import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_OPTIONS; import static com.google.android.exoplayer2.source.rtsp.RtspRequest.METHOD_OPTIONS;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableMap;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
...@@ -28,19 +31,32 @@ import java.net.ServerSocket; ...@@ -28,19 +31,32 @@ import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.net.SocketException; import java.net.SocketException;
import java.util.List; import java.util.List;
import java.util.Map;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** The RTSP server. */ /** The RTSP server. */
public final class RtspServer implements Closeable { public final class RtspServer implements Closeable {
private static final String PUBLIC_SUPPORTED_METHODS = "OPTIONS"; private static final String PUBLIC_SUPPORTED_METHODS = "OPTIONS, DESCRIBE";
/** RTSP error Method Not Allowed (RFC2326 Section 7.1.1). */ /** RTSP error Method Not Allowed (RFC2326 Section 7.1.1). */
private static final int STATUS_OK = 200;
private static final int STATUS_METHOD_NOT_ALLOWED = 405; private static final int STATUS_METHOD_NOT_ALLOWED = 405;
private static final String SESSION_DESCRIPTION =
"v=0\r\n"
+ "o=- 1606776316530225 1 IN IP4 127.0.0.1\r\n"
+ "s=Exoplayer test\r\n"
+ "t=0 0\r\n"
+ "a=range:npt=0-50.46\r\n";
private final Thread listenerThread; private final Thread listenerThread;
/** Runs on the thread on which the constructor was called. */ /** Runs on the thread on which the constructor was called. */
private final Handler mainHandler; private final Handler mainHandler;
private final RtpPacketStreamDump rtpPacketStreamDump;
private @MonotonicNonNull ServerSocket serverSocket; private @MonotonicNonNull ServerSocket serverSocket;
private @MonotonicNonNull RtspMessageChannel connectedClient; private @MonotonicNonNull RtspMessageChannel connectedClient;
...@@ -51,7 +67,8 @@ public final class RtspServer implements Closeable { ...@@ -51,7 +67,8 @@ public final class RtspServer implements Closeable {
* *
* <p>The constructor must be called on a {@link Looper} thread. * <p>The constructor must be called on a {@link Looper} thread.
*/ */
public RtspServer() { public RtspServer(RtpPacketStreamDump rtpPacketStreamDump) {
this.rtpPacketStreamDump = rtpPacketStreamDump;
listenerThread = listenerThread =
new Thread(this::listenToIncomingRtspConnection, "ExoPlayerTest:RtspConnectionMonitor"); new Thread(this::listenToIncomingRtspConnection, "ExoPlayerTest:RtspConnectionMonitor");
mainHandler = Util.createHandlerForCurrentLooper(); mainHandler = Util.createHandlerForCurrentLooper();
...@@ -87,7 +104,7 @@ public final class RtspServer implements Closeable { ...@@ -87,7 +104,7 @@ public final class RtspServer implements Closeable {
private void handleNewClientConnected(Socket socket) { private void handleNewClientConnected(Socket socket) {
try { try {
connectedClient = new RtspMessageChannel(new MessageListener()); connectedClient = new RtspMessageChannel(new MessageListener());
connectedClient.openSocket(socket); connectedClient.open(socket);
} catch (IOException e) { } catch (IOException e) {
Util.closeQuietly(connectedClient); Util.closeQuietly(connectedClient);
// Log the error. // Log the error.
...@@ -98,34 +115,62 @@ public final class RtspServer implements Closeable { ...@@ -98,34 +115,62 @@ public final class RtspServer implements Closeable {
private final class MessageListener implements RtspMessageChannel.MessageListener { private final class MessageListener implements RtspMessageChannel.MessageListener {
@Override @Override
public void onRtspMessageReceived(List<String> message) { public void onRtspMessageReceived(List<String> message) {
mainHandler.post(() -> handleRtspMessage(message));
}
private void handleRtspMessage(List<String> message) {
RtspRequest request = RtspMessageUtil.parseRequest(message); RtspRequest request = RtspMessageUtil.parseRequest(message);
String cSeq = checkNotNull(request.headers.get(RtspHeaders.CSEQ));
switch (request.method) { switch (request.method) {
case METHOD_OPTIONS: case METHOD_OPTIONS:
onOptionsRequestReceived(request); onOptionsRequestReceived(cSeq);
break;
case METHOD_DESCRIBE:
onDescribeRequestReceived(request.uri, cSeq);
break; break;
default: default:
connectedClient.send( sendErrorResponse(STATUS_METHOD_NOT_ALLOWED, cSeq);
RtspMessageUtil.serializeResponse(
new RtspResponse(
/* status= */ STATUS_METHOD_NOT_ALLOWED,
/* headers= */ new RtspHeaders.Builder()
.add(
RtspHeaders.CSEQ, checkNotNull(request.headers.get(RtspHeaders.CSEQ)))
.build(),
/* messageBody= */ "")));
} }
} }
private void onOptionsRequestReceived(RtspRequest request) { private void onOptionsRequestReceived(String cSeq) {
sendResponseWithCommonHeaders(
/* status= */ STATUS_OK,
/* cSeq= */ cSeq,
/* additionalHeaders= */ ImmutableMap.of(RtspHeaders.PUBLIC, PUBLIC_SUPPORTED_METHODS),
/* messageBody= */ "");
}
private void onDescribeRequestReceived(Uri requestedUri, String cSeq) {
String sdpMessage = SESSION_DESCRIPTION + rtpPacketStreamDump.mediaDescription + "\r\n";
sendResponseWithCommonHeaders(
/* status= */ STATUS_OK,
/* cSeq= */ cSeq,
/* additionalHeaders= */ ImmutableMap.of(
RtspHeaders.CONTENT_BASE, requestedUri.toString(),
RtspHeaders.CONTENT_TYPE, "application/sdp",
RtspHeaders.CONTENT_LENGTH, String.valueOf(sdpMessage.length())),
/* messageBody= */ sdpMessage);
}
private void sendErrorResponse(int status, String cSeq) {
sendResponseWithCommonHeaders(
status, cSeq, /* additionalHeaders= */ ImmutableMap.of(), /* messageBody= */ "");
}
private void sendResponseWithCommonHeaders(
int status, String cSeq, Map<String, String> additionalHeaders, String messageBody) {
RtspHeaders.Builder headerBuilder = new RtspHeaders.Builder();
headerBuilder.add(RtspHeaders.CSEQ, cSeq);
headerBuilder.addAll(additionalHeaders);
connectedClient.send( connectedClient.send(
RtspMessageUtil.serializeResponse( RtspMessageUtil.serializeResponse(
new RtspResponse( new RtspResponse(
/* status= */ 200, /* status= */ status,
/* headers= */ new RtspHeaders.Builder() /* headers= */ headerBuilder.build(),
.add(RtspHeaders.CSEQ, checkNotNull(request.headers.get(RtspHeaders.CSEQ))) /* messageBody= */ messageBody)));
.add(RtspHeaders.PUBLIC, PUBLIC_SUPPORTED_METHODS)
.build(),
/* messageBody= */ "")));
} }
} }
......
...@@ -30,6 +30,7 @@ import static org.junit.Assert.assertThrows; ...@@ -30,6 +30,7 @@ import static org.junit.Assert.assertThrows;
import android.net.Uri; import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ParserException;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
...@@ -150,6 +151,35 @@ public class SessionDescriptionTest { ...@@ -150,6 +151,35 @@ public class SessionDescriptionTest {
} }
@Test @Test
public void parse_sdpStringWithDuplicatedMediaAttribute_throwsParserException() {
String testMediaSdpInfo =
"v=0\r\n"
+ "o=MNobody 2890844526 2890842807 IN IP4 192.0.2.46\r\n"
+ "s=SDP Seminar\r\n"
+ "i=A Seminar on the session description protocol\r\n"
+ "m=audio 3456 RTP/AVP 0\r\n"
+ "a=control:audio\r\n"
+ "a=control:audio\r\n";
assertThrows(ParserException.class, () -> SessionDescriptionParser.parse(testMediaSdpInfo));
}
@Test
public void parse_sdpStringWithDuplicatedSessionAttribute_throwsParserException() {
String testMediaSdpInfo =
"v=0\r\n"
+ "o=MNobody 2890844526 2890842807 IN IP4 192.0.2.46\r\n"
+ "s=SDP Seminar\r\n"
+ "a=control:*\r\n"
+ "a=control:*\r\n"
+ "i=A Seminar on the session description protocol\r\n"
+ "m=audio 3456 RTP/AVP 0\r\n"
+ "a=control:audio\r\n";
assertThrows(ParserException.class, () -> SessionDescriptionParser.parse(testMediaSdpInfo));
}
@Test
public void buildMediaDescription_withInvalidRtpmapAttribute_throwsIllegalStateException() { public void buildMediaDescription_withInvalidRtpmapAttribute_throwsIllegalStateException() {
assertThrows( assertThrows(
IllegalStateException.class, IllegalStateException.class,
......
...@@ -17,9 +17,11 @@ package com.google.android.exoplayer2.source.rtsp; ...@@ -17,9 +17,11 @@ package com.google.android.exoplayer2.source.rtsp;
import static com.google.android.exoplayer2.testutil.TestUtil.buildTestData; import static com.google.android.exoplayer2.testutil.TestUtil.buildTestData;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.primitives.Bytes; import com.google.common.primitives.Bytes;
import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
...@@ -29,11 +31,29 @@ import org.junit.runner.RunWith; ...@@ -29,11 +31,29 @@ import org.junit.runner.RunWith;
public class TransferRtpDataChannelTest { public class TransferRtpDataChannelTest {
@Test @Test
public void getInterleavedBinaryDataListener_returnsAnInterleavedBinaryDataListener() {
TransferRtpDataChannel transferRtpDataChannel = new TransferRtpDataChannel();
assertThat(transferRtpDataChannel.getInterleavedBinaryDataListener())
.isEqualTo(transferRtpDataChannel);
}
@Test
public void read_withoutReceivingInterleavedData_timesOut() {
TransferRtpDataChannel transferRtpDataChannel = new TransferRtpDataChannel();
byte[] buffer = new byte[1];
assertThrows(
IOException.class,
() -> transferRtpDataChannel.read(buffer, /* offset= */ 0, buffer.length));
}
@Test
public void read_withLargeEnoughBuffer_reads() throws Exception { public void read_withLargeEnoughBuffer_reads() throws Exception {
byte[] randomBytes = buildTestData(20); byte[] randomBytes = buildTestData(20);
byte[] buffer = new byte[40]; byte[] buffer = new byte[40];
TransferRtpDataChannel transferRtpDataChannel = new TransferRtpDataChannel(); TransferRtpDataChannel transferRtpDataChannel = new TransferRtpDataChannel();
transferRtpDataChannel.write(randomBytes); transferRtpDataChannel.onInterleavedBinaryDataReceived(randomBytes);
transferRtpDataChannel.read(buffer, /* offset= */ 0, buffer.length); transferRtpDataChannel.read(buffer, /* offset= */ 0, buffer.length);
...@@ -45,7 +65,7 @@ public class TransferRtpDataChannelTest { ...@@ -45,7 +65,7 @@ public class TransferRtpDataChannelTest {
byte[] randomBytes = buildTestData(20); byte[] randomBytes = buildTestData(20);
byte[] buffer = new byte[8]; byte[] buffer = new byte[8];
TransferRtpDataChannel transferRtpDataChannel = new TransferRtpDataChannel(); TransferRtpDataChannel transferRtpDataChannel = new TransferRtpDataChannel();
transferRtpDataChannel.write(randomBytes); transferRtpDataChannel.onInterleavedBinaryDataReceived(randomBytes);
transferRtpDataChannel.read(buffer, /* offset= */ 0, buffer.length); transferRtpDataChannel.read(buffer, /* offset= */ 0, buffer.length);
assertThat(buffer).isEqualTo(Arrays.copyOfRange(randomBytes, /* from= */ 0, /* to= */ 8)); assertThat(buffer).isEqualTo(Arrays.copyOfRange(randomBytes, /* from= */ 0, /* to= */ 8));
...@@ -61,7 +81,7 @@ public class TransferRtpDataChannelTest { ...@@ -61,7 +81,7 @@ public class TransferRtpDataChannelTest {
byte[] randomBytes = buildTestData(40); byte[] randomBytes = buildTestData(40);
byte[] buffer = new byte[20]; byte[] buffer = new byte[20];
TransferRtpDataChannel transferRtpDataChannel = new TransferRtpDataChannel(); TransferRtpDataChannel transferRtpDataChannel = new TransferRtpDataChannel();
transferRtpDataChannel.write(randomBytes); transferRtpDataChannel.onInterleavedBinaryDataReceived(randomBytes);
transferRtpDataChannel.read(buffer, /* offset= */ 0, buffer.length); transferRtpDataChannel.read(buffer, /* offset= */ 0, buffer.length);
assertThat(buffer).isEqualTo(Arrays.copyOfRange(randomBytes, /* from= */ 0, /* to= */ 20)); assertThat(buffer).isEqualTo(Arrays.copyOfRange(randomBytes, /* from= */ 0, /* to= */ 20));
...@@ -77,13 +97,13 @@ public class TransferRtpDataChannelTest { ...@@ -77,13 +97,13 @@ public class TransferRtpDataChannelTest {
byte[] smallBuffer = new byte[20]; byte[] smallBuffer = new byte[20];
byte[] bigBuffer = new byte[40]; byte[] bigBuffer = new byte[40];
TransferRtpDataChannel transferRtpDataChannel = new TransferRtpDataChannel(); TransferRtpDataChannel transferRtpDataChannel = new TransferRtpDataChannel();
transferRtpDataChannel.write(randomBytes1); transferRtpDataChannel.onInterleavedBinaryDataReceived(randomBytes1);
transferRtpDataChannel.read(smallBuffer, /* offset= */ 0, smallBuffer.length); transferRtpDataChannel.read(smallBuffer, /* offset= */ 0, smallBuffer.length);
assertThat(smallBuffer) assertThat(smallBuffer)
.isEqualTo(Arrays.copyOfRange(randomBytes1, /* from= */ 0, /* to= */ 20)); .isEqualTo(Arrays.copyOfRange(randomBytes1, /* from= */ 0, /* to= */ 20));
transferRtpDataChannel.write(randomBytes2); transferRtpDataChannel.onInterleavedBinaryDataReceived(randomBytes2);
// Read the remaining 20 bytes in randomBytes1, and 20 bytes from randomBytes2. // Read the remaining 20 bytes in randomBytes1, and 20 bytes from randomBytes2.
transferRtpDataChannel.read(bigBuffer, /* offset= */ 0, bigBuffer.length); transferRtpDataChannel.read(bigBuffer, /* offset= */ 0, bigBuffer.length);
...@@ -107,13 +127,13 @@ public class TransferRtpDataChannelTest { ...@@ -107,13 +127,13 @@ public class TransferRtpDataChannelTest {
byte[] smallBuffer = new byte[30]; byte[] smallBuffer = new byte[30];
byte[] bigBuffer = new byte[30]; byte[] bigBuffer = new byte[30];
TransferRtpDataChannel transferRtpDataChannel = new TransferRtpDataChannel(); TransferRtpDataChannel transferRtpDataChannel = new TransferRtpDataChannel();
transferRtpDataChannel.write(randomBytes1); transferRtpDataChannel.onInterleavedBinaryDataReceived(randomBytes1);
transferRtpDataChannel.read(smallBuffer, /* offset= */ 0, smallBuffer.length); transferRtpDataChannel.read(smallBuffer, /* offset= */ 0, smallBuffer.length);
assertThat(smallBuffer) assertThat(smallBuffer)
.isEqualTo(Arrays.copyOfRange(randomBytes1, /* from= */ 0, /* to= */ 30)); .isEqualTo(Arrays.copyOfRange(randomBytes1, /* from= */ 0, /* to= */ 30));
transferRtpDataChannel.write(randomBytes2); transferRtpDataChannel.onInterleavedBinaryDataReceived(randomBytes2);
// Read 30 bytes to big buffer. // Read 30 bytes to big buffer.
transferRtpDataChannel.read(bigBuffer, /* offset= */ 0, bigBuffer.length); transferRtpDataChannel.read(bigBuffer, /* offset= */ 0, bigBuffer.length);
...@@ -136,13 +156,13 @@ public class TransferRtpDataChannelTest { ...@@ -136,13 +156,13 @@ public class TransferRtpDataChannelTest {
byte[] smallBuffer = new byte[20]; byte[] smallBuffer = new byte[20];
byte[] bigBuffer = new byte[70]; byte[] bigBuffer = new byte[70];
TransferRtpDataChannel transferRtpDataChannel = new TransferRtpDataChannel(); TransferRtpDataChannel transferRtpDataChannel = new TransferRtpDataChannel();
transferRtpDataChannel.write(randomBytes1); transferRtpDataChannel.onInterleavedBinaryDataReceived(randomBytes1);
transferRtpDataChannel.read(smallBuffer, /* offset= */ 0, smallBuffer.length); transferRtpDataChannel.read(smallBuffer, /* offset= */ 0, smallBuffer.length);
assertThat(smallBuffer) assertThat(smallBuffer)
.isEqualTo(Arrays.copyOfRange(randomBytes1, /* from= */ 0, /* to= */ 20)); .isEqualTo(Arrays.copyOfRange(randomBytes1, /* from= */ 0, /* to= */ 20));
transferRtpDataChannel.write(randomBytes2); transferRtpDataChannel.onInterleavedBinaryDataReceived(randomBytes2);
transferRtpDataChannel.read(bigBuffer, /* offset= */ 0, bigBuffer.length); transferRtpDataChannel.read(bigBuffer, /* offset= */ 0, bigBuffer.length);
assertThat(Arrays.copyOfRange(bigBuffer, /* from= */ 0, /* to= */ 60)) assertThat(Arrays.copyOfRange(bigBuffer, /* from= */ 0, /* to= */ 60))
......
/*
* Copyright 2021 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.rtsp;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link UdpDataSourceRtpDataChannel}. */
@RunWith(AndroidJUnit4.class)
public class UdpDataSourceRtpDataChannelTest {
@Test
public void getInterleavedBinaryDataListener_returnsNull() {
UdpDataSourceRtpDataChannel udpDataSourceRtpDataChannel = new UdpDataSourceRtpDataChannel();
assertThat(udpDataSourceRtpDataChannel.getInterleavedBinaryDataListener()).isNull();
}
}
...@@ -570,6 +570,7 @@ public class StyledPlayerView extends FrameLayout implements AdViewProvider { ...@@ -570,6 +570,7 @@ public class StyledPlayerView extends FrameLayout implements AdViewProvider {
} }
@Nullable Player oldPlayer = this.player; @Nullable Player oldPlayer = this.player;
if (oldPlayer != null) { if (oldPlayer != null) {
oldPlayer.removeListener(componentListener);
if (surfaceView instanceof TextureView) { if (surfaceView instanceof TextureView) {
oldPlayer.clearVideoTextureView((TextureView) surfaceView); oldPlayer.clearVideoTextureView((TextureView) surfaceView);
} else if (surfaceView instanceof SurfaceView) { } else if (surfaceView instanceof SurfaceView) {
......
...@@ -21,10 +21,6 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; ...@@ -21,10 +21,6 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.RelativeSizeSpan;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.View; import android.view.View;
...@@ -380,37 +376,13 @@ public final class SubtitleView extends FrameLayout implements TextOutput { ...@@ -380,37 +376,13 @@ public final class SubtitleView extends FrameLayout implements TextOutput {
} }
private Cue removeEmbeddedStyling(Cue cue) { private Cue removeEmbeddedStyling(Cue cue) {
@Nullable CharSequence cueText = cue.text; Cue.Builder strippedCue = cue.buildUpon();
if (!applyEmbeddedStyles) { if (!applyEmbeddedStyles) {
Cue.Builder strippedCue = SubtitleViewUtils.removeAllEmbeddedStyling(strippedCue);
cue.buildUpon().setTextSize(Cue.DIMEN_UNSET, Cue.TYPE_UNSET).clearWindowColor();
if (cueText != null) {
// Remove all spans, regardless of type.
strippedCue.setText(cueText.toString());
}
return strippedCue.build();
} else if (!applyEmbeddedFontSizes) { } else if (!applyEmbeddedFontSizes) {
if (cueText == null) { SubtitleViewUtils.removeEmbeddedFontSizes(strippedCue);
return cue;
}
Cue.Builder strippedCue = cue.buildUpon().setTextSize(Cue.DIMEN_UNSET, Cue.TYPE_UNSET);
if (cueText instanceof Spanned) {
SpannableString spannable = SpannableString.valueOf(cueText);
AbsoluteSizeSpan[] absSpans =
spannable.getSpans(0, spannable.length(), AbsoluteSizeSpan.class);
for (AbsoluteSizeSpan absSpan : absSpans) {
spannable.removeSpan(absSpan);
}
RelativeSizeSpan[] relSpans =
spannable.getSpans(0, spannable.length(), RelativeSizeSpan.class);
for (RelativeSizeSpan relSpan : relSpans) {
spannable.removeSpan(relSpan);
}
strippedCue.setText(spannable);
}
return strippedCue.build();
} }
return cue; return strippedCue.build();
} }
} }
...@@ -16,7 +16,16 @@ ...@@ -16,7 +16,16 @@
*/ */
package com.google.android.exoplayer2.ui; package com.google.android.exoplayer2.ui;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.RelativeSizeSpan;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.span.LanguageFeatureSpan;
import com.google.common.base.Predicate;
/** Utility class for subtitle layout logic. */ /** Utility class for subtitle layout logic. */
/* package */ final class SubtitleViewUtils { /* package */ final class SubtitleViewUtils {
...@@ -48,5 +57,50 @@ import com.google.android.exoplayer2.text.Cue; ...@@ -48,5 +57,50 @@ import com.google.android.exoplayer2.text.Cue;
} }
} }
/** Removes all styling information from {@code cue}. */
public static void removeAllEmbeddedStyling(Cue.Builder cue) {
cue.clearWindowColor();
if (cue.getText() instanceof Spanned) {
if (!(cue.getText() instanceof Spannable)) {
cue.setText(SpannableString.valueOf(cue.getText()));
}
removeSpansIf(
(Spannable) checkNotNull(cue.getText()), span -> !(span instanceof LanguageFeatureSpan));
}
removeEmbeddedFontSizes(cue);
}
/**
* Removes all font size information from {@code cue}.
*
* <p>This involves:
*
* <ul>
* <li>Clearing {@link Cue.Builder#setTextSize(float, int)}.
* <li>Removing all {@link AbsoluteSizeSpan} and {@link RelativeSizeSpan} spans from {@link
* Cue#text}.
* </ul>
*/
public static void removeEmbeddedFontSizes(Cue.Builder cue) {
cue.setTextSize(Cue.DIMEN_UNSET, Cue.TYPE_UNSET);
if (cue.getText() instanceof Spanned) {
if (!(cue.getText() instanceof Spannable)) {
cue.setText(SpannableString.valueOf(cue.getText()));
}
removeSpansIf(
(Spannable) checkNotNull(cue.getText()),
span -> span instanceof AbsoluteSizeSpan || span instanceof RelativeSizeSpan);
}
}
private static void removeSpansIf(Spannable spannable, Predicate<Object> removeFilter) {
Object[] spans = spannable.getSpans(0, spannable.length(), Object.class);
for (Object span : spans) {
if (removeFilter.apply(span)) {
spannable.removeSpan(span);
}
}
}
private SubtitleViewUtils() {} private SubtitleViewUtils() {}
} }
/*
* Copyright 2021 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.ui;
import static com.google.android.exoplayer2.testutil.truth.SpannedSubject.assertThat;
import static com.google.common.truth.Truth.assertThat;
import android.graphics.Color;
import android.text.Layout;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.UnderlineSpan;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
import com.google.android.exoplayer2.text.span.RubySpan;
import com.google.android.exoplayer2.text.span.TextAnnotation;
import com.google.android.exoplayer2.text.span.TextEmphasisSpan;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link SubtitleView}. */
@RunWith(AndroidJUnit4.class)
public class SubtitleViewUtilsTest {
private static final Cue CUE = buildCue();
@Test
public void testRemoveAllEmbeddedStyling() {
Cue.Builder cueBuilder = CUE.buildUpon();
SubtitleViewUtils.removeAllEmbeddedStyling(cueBuilder);
Cue strippedCue = cueBuilder.build();
Spanned originalText = (Spanned) CUE.text;
Spanned strippedText = (Spanned) strippedCue.text;
// Assert all non styling properties and spans are kept
assertThat(strippedCue.textAlignment).isEqualTo(CUE.textAlignment);
assertThat(strippedCue.multiRowAlignment).isEqualTo(CUE.multiRowAlignment);
assertThat(strippedCue.line).isEqualTo(CUE.line);
assertThat(strippedCue.lineType).isEqualTo(CUE.lineType);
assertThat(strippedCue.position).isEqualTo(CUE.position);
assertThat(strippedCue.positionAnchor).isEqualTo(CUE.positionAnchor);
assertThat(strippedCue.textSize).isEqualTo(Cue.DIMEN_UNSET);
assertThat(strippedCue.textSizeType).isEqualTo(Cue.TYPE_UNSET);
assertThat(strippedCue.size).isEqualTo(CUE.size);
assertThat(strippedCue.verticalType).isEqualTo(CUE.verticalType);
assertThat(strippedCue.shearDegrees).isEqualTo(CUE.shearDegrees);
TextEmphasisSpan expectedTextEmphasisSpan =
originalText.getSpans(0, originalText.length(), TextEmphasisSpan.class)[0];
assertThat(strippedText)
.hasTextEmphasisSpanBetween(
originalText.getSpanStart(expectedTextEmphasisSpan),
originalText.getSpanEnd(expectedTextEmphasisSpan));
RubySpan expectedRubySpan = originalText.getSpans(0, originalText.length(), RubySpan.class)[0];
assertThat(strippedText)
.hasRubySpanBetween(
originalText.getSpanStart(expectedRubySpan), originalText.getSpanEnd(expectedRubySpan));
HorizontalTextInVerticalContextSpan expectedHorizontalTextInVerticalContextSpan =
originalText
.getSpans(0, originalText.length(), HorizontalTextInVerticalContextSpan.class)[0];
assertThat(strippedText)
.hasHorizontalTextInVerticalContextSpanBetween(
originalText.getSpanStart(expectedHorizontalTextInVerticalContextSpan),
originalText.getSpanEnd(expectedHorizontalTextInVerticalContextSpan));
// Assert all styling properties and spans are removed
assertThat(strippedCue.windowColorSet).isFalse();
assertThat(strippedText).hasNoUnderlineSpanBetween(0, strippedText.length());
assertThat(strippedText).hasNoRelativeSizeSpanBetween(0, strippedText.length());
assertThat(strippedText).hasNoAbsoluteSizeSpanBetween(0, strippedText.length());
}
@Test
public void testRemoveEmbeddedFontSizes() {
Cue.Builder cueBuilder = CUE.buildUpon();
SubtitleViewUtils.removeEmbeddedFontSizes(cueBuilder);
Cue strippedCue = cueBuilder.build();
Spanned originalText = (Spanned) CUE.text;
Spanned strippedText = (Spanned) strippedCue.text;
// Assert all non text-size properties and spans are kept
assertThat(strippedCue.textAlignment).isEqualTo(CUE.textAlignment);
assertThat(strippedCue.multiRowAlignment).isEqualTo(CUE.multiRowAlignment);
assertThat(strippedCue.line).isEqualTo(CUE.line);
assertThat(strippedCue.lineType).isEqualTo(CUE.lineType);
assertThat(strippedCue.position).isEqualTo(CUE.position);
assertThat(strippedCue.positionAnchor).isEqualTo(CUE.positionAnchor);
assertThat(strippedCue.size).isEqualTo(CUE.size);
assertThat(strippedCue.windowColor).isEqualTo(CUE.windowColor);
assertThat(strippedCue.windowColorSet).isEqualTo(CUE.windowColorSet);
assertThat(strippedCue.verticalType).isEqualTo(CUE.verticalType);
assertThat(strippedCue.shearDegrees).isEqualTo(CUE.shearDegrees);
TextEmphasisSpan expectedTextEmphasisSpan =
originalText.getSpans(0, originalText.length(), TextEmphasisSpan.class)[0];
assertThat(strippedText)
.hasTextEmphasisSpanBetween(
originalText.getSpanStart(expectedTextEmphasisSpan),
originalText.getSpanEnd(expectedTextEmphasisSpan));
RubySpan expectedRubySpan = originalText.getSpans(0, originalText.length(), RubySpan.class)[0];
assertThat(strippedText)
.hasRubySpanBetween(
originalText.getSpanStart(expectedRubySpan), originalText.getSpanEnd(expectedRubySpan));
HorizontalTextInVerticalContextSpan expectedHorizontalTextInVerticalContextSpan =
originalText
.getSpans(0, originalText.length(), HorizontalTextInVerticalContextSpan.class)[0];
assertThat(strippedText)
.hasHorizontalTextInVerticalContextSpanBetween(
originalText.getSpanStart(expectedHorizontalTextInVerticalContextSpan),
originalText.getSpanEnd(expectedHorizontalTextInVerticalContextSpan));
UnderlineSpan expectedUnderlineSpan =
originalText.getSpans(0, originalText.length(), UnderlineSpan.class)[0];
assertThat(strippedText)
.hasUnderlineSpanBetween(
originalText.getSpanStart(expectedUnderlineSpan),
originalText.getSpanEnd(expectedUnderlineSpan));
// Assert the text-size properties and spans are removed
assertThat(strippedCue.textSize).isEqualTo(Cue.DIMEN_UNSET);
assertThat(strippedCue.textSizeType).isEqualTo(Cue.TYPE_UNSET);
assertThat(strippedText).hasNoRelativeSizeSpanBetween(0, strippedText.length());
assertThat(strippedText).hasNoAbsoluteSizeSpanBetween(0, strippedText.length());
}
private static Cue buildCue() {
SpannableString spanned =
new SpannableString("TextEmphasis おはよ Ruby ございます 123 Underline RelativeSize AbsoluteSize");
spanned.setSpan(
new TextEmphasisSpan(
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_BEFORE),
"Text emphasis ".length(),
"Text emphasis おはよ".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spanned.setSpan(
new RubySpan("おはよ", TextAnnotation.POSITION_BEFORE),
"TextEmphasis おはよ Ruby ".length(),
"TextEmphasis おはよ Ruby ございます".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spanned.setSpan(
new HorizontalTextInVerticalContextSpan(),
"TextEmphasis おはよ Ruby ございます ".length(),
"TextEmphasis おはよ Ruby ございます 123".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spanned.setSpan(
new UnderlineSpan(),
"TextEmphasis おはよ Ruby ございます 123 ".length(),
"TextEmphasis おはよ Ruby ございます 123 Underline".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spanned.setSpan(
new RelativeSizeSpan(1f),
"TextEmphasis おはよ Ruby ございます 123 Underline ".length(),
"TextEmphasis おはよ Ruby ございます 123 Underline RelativeSize".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spanned.setSpan(
new AbsoluteSizeSpan(10),
"TextEmphasis おはよ Ruby ございます 123 Underline RelativeSize ".length(),
"TextEmphasis おはよ Ruby ございます 123 Underline RelativeSize AbsoluteSize".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return new Cue.Builder()
.setText(spanned)
.setTextAlignment(Layout.Alignment.ALIGN_CENTER)
.setMultiRowAlignment(Layout.Alignment.ALIGN_NORMAL)
.setLine(5, Cue.LINE_TYPE_NUMBER)
.setLineAnchor(Cue.ANCHOR_TYPE_END)
.setPosition(0.4f)
.setPositionAnchor(Cue.ANCHOR_TYPE_MIDDLE)
.setTextSize(0.2f, Cue.TEXT_SIZE_TYPE_FRACTIONAL)
.setSize(0.8f)
.setWindowColor(Color.CYAN)
.setVerticalType(Cue.VERTICAL_TYPE_RL)
.setShearDegrees(-15f)
.build();
}
}
This diff could not be displayed because it is too large.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment