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

r2.14.1
This commit is contained in:
Ian Baker 2021-06-14 12:23:15 +01:00 committed by GitHub
commit b2333c86c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
111 changed files with 7324 additions and 1580 deletions

View File

@ -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
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
#### 1. Add ExoPlayer module dependencies ####
@ -39,13 +41,10 @@ implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
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
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
plays DASH content:
only plays DASH content:
```gradle
implementation 'com.google.android.exoplayer:exoplayer-core: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
library is equivalent to adding dependencies on all of the library modules
individually.
ExoPlayer library is equivalent to adding dependencies on all of the library
modules individually.
* `exoplayer-core`: Core functionality (required).
* `exoplayer-dash`: Support for DASH content.
* `exoplayer-hls`: Support for HLS content.
* `exoplayer-rtsp`: Support for RTSP content.
* `exoplayer-smoothstreaming`: Support for SmoothStreaming content.
* `exoplayer-transformer`: Media transformation functionality.
* `exoplayer-ui`: UI components and resources for use with ExoPlayer.
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
found on the [Google Maven ExoPlayer page][].
[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
#### 2. Turn on Java 8 support ####
@ -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 ###
Cloning the repository and depending on the modules locally is required when
@ -104,12 +110,12 @@ git checkout release-v2
```
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.ext.exoplayerRoot = '/absolute/path/to/exoplayer'
gradle.ext.exoplayerRoot = 'path/to/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

View File

@ -1,5 +1,56 @@
# 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)
* Core Library:
@ -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
be useful.
* Remove generic types from DRM components.
* Rename `DefaultDrmSessionEventListener` to `DrmSessionEventListener`.
* Track selection:
* Add `TrackSelection.shouldCancelMediaChunkLoad` to check whether an
ongoing load should be canceled

View File

@ -13,8 +13,8 @@
// limitations under the License.
project.ext {
// ExoPlayer version and version code.
releaseVersion = '2.14.0'
releaseVersionCode = 2014000
releaseVersion = '2.14.1'
releaseVersionCode = 2014001
minSdkVersion = 16
appTargetSdkVersion = 29
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest.

View File

@ -11,10 +11,9 @@
// 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.
def rootDir = gradle.ext.exoplayerRoot
def rootDir = file(gradle.ext.exoplayerRoot)
if (!gradle.ext.has('exoplayerSettingsDir')) {
gradle.ext.exoplayerSettingsDir =
new File(rootDir.toString()).getCanonicalPath()
gradle.ext.exoplayerSettingsDir = rootDir.getCanonicalPath()
}
def modulePrefix = ':'
if (gradle.ext.has('exoplayerModulePrefix')) {

View File

@ -11,10 +11,11 @@
// 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.
attribute vec2 a_position;
attribute vec2 a_texcoord;
attribute vec4 a_position;
attribute vec4 a_texcoord;
uniform mat4 tex_transform;
varying vec2 v_texcoord;
void main() {
gl_Position = vec4(a_position.x, a_position.y, 0, 1);
v_texcoord = a_texcoord;
gl_Position = a_position;
v_texcoord = (tex_transform * a_texcoord).xy;
}

View File

@ -88,9 +88,9 @@ import javax.microedition.khronos.opengles.GL10;
GlUtil.Uniform[] uniforms = GlUtil.getUniforms(program);
for (GlUtil.Attribute attribute : attributes) {
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")) {
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;
@ -111,7 +111,7 @@ import javax.microedition.khronos.opengles.GL10;
}
@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.
String text = String.format(Locale.US, "%.02f", frameTimestampUs / (float) C.MICROS_PER_SECOND);
overlayBitmap.eraseColor(Color.TRANSPARENT);
@ -140,6 +140,9 @@ import javax.microedition.khronos.opengles.GL10;
case "scaleY":
uniform.setFloat(bitmapScaleY);
break;
case "tex_transform":
uniform.setFloats(transformMatrix);
break;
default: // fall out
}
}

View File

@ -25,6 +25,7 @@ import android.os.Handler;
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Assertions;
@ -61,8 +62,9 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
*
* @param frameTexture The ID of a GL texture containing a video frame.
* @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;
@ -214,6 +216,7 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
private final VideoProcessor videoProcessor;
private final AtomicBoolean frameAvailable;
private final TimedValueQueue<Long> sampleTimestampQueue;
private final float[] transformMatrix;
private int texture;
@Nullable private SurfaceTexture surfaceTexture;
@ -229,6 +232,8 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
sampleTimestampQueue = new TimedValueQueue<>();
width = -1;
height = -1;
frameTimestampUs = C.TIME_UNSET;
transformMatrix = new float[16];
}
@Override
@ -271,13 +276,14 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
SurfaceTexture surfaceTexture = Assertions.checkNotNull(this.surfaceTexture);
surfaceTexture.updateTexImage();
long lastFrameTimestampNs = surfaceTexture.getTimestamp();
Long frameTimestampUs = sampleTimestampQueue.poll(lastFrameTimestampNs);
@Nullable Long frameTimestampUs = sampleTimestampQueue.poll(lastFrameTimestampNs);
if (frameTimestampUs != null) {
this.frameTimestampUs = frameTimestampUs;
}
surfaceTexture.getTransformMatrix(transformMatrix);
}
videoProcessor.draw(texture, frameTimestampUs);
videoProcessor.draw(texture, frameTimestampUs, transformMatrix);
}
@Override

File diff suppressed because one or more lines are too long

View File

@ -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/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/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/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>
@ -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/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.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/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>

View File

@ -486,8 +486,8 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<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;uri)</pre>
<div class="block">Sets the optional URI.
<p>If <code>uri</code> is null or unset no <a href="MediaItem.PlaybackProperties.html" title="class in com.google.android.exoplayer2"><code>MediaItem.PlaybackProperties</code></a> object is created during
<a href="#build()"><code>build()</code></a> and any other <code>Builder</code> methods that would populate <a href="MediaItem.html#playbackProperties"><code>MediaItem.playbackProperties</code></a> are ignored.</div>
<p>If <code>uri</code> is null or unset then no <a href="MediaItem.PlaybackProperties.html" title="class in com.google.android.exoplayer2"><code>MediaItem.PlaybackProperties</code></a> object is created
during <a href="#build()"><code>build()</code></a> and no other <code>Builder</code> methods that would populate <a href="MediaItem.html#playbackProperties"><code>MediaItem.playbackProperties</code></a> should be called.</div>
</li>
</ul>
<a id="setUri(android.net.Uri)">
@ -500,8 +500,8 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<a href="https://developer.android.com/reference/android/net/Uri.html" title="class or interface in android.net" class="externalLink" target="_top">Uri</a>&nbsp;uri)</pre>
<div class="block">Sets the optional URI.
<p>If <code>uri</code> is null or unset no <a href="MediaItem.PlaybackProperties.html" title="class in com.google.android.exoplayer2"><code>MediaItem.PlaybackProperties</code></a> object is created during
<a href="#build()"><code>build()</code></a> and any other <code>Builder</code> methods that would populate <a href="MediaItem.html#playbackProperties"><code>MediaItem.playbackProperties</code></a> are ignored.</div>
<p>If <code>uri</code> is null or unset then no <a href="MediaItem.PlaybackProperties.html" title="class in com.google.android.exoplayer2"><code>MediaItem.PlaybackProperties</code></a> object is created
during <a href="#build()"><code>build()</code></a> and no other <code>Builder</code> methods that would populate <a href="MediaItem.html#playbackProperties"><code>MediaItem.playbackProperties</code></a> should be called.</div>
</li>
</ul>
<a id="setMimeType(java.lang.String)">
@ -516,8 +516,7 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<p>The MIME type may be used as a hint for inferring the type of the media item.
<p>If <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> is passed a non-null <code>uri</code>, the MIME type is used to create a
<a href="MediaItem.PlaybackProperties.html" title="class in com.google.android.exoplayer2"><code>MediaItem.PlaybackProperties</code></a> object. Otherwise it will be ignored.</div>
<p>This method should only be called if <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> is passed a non-null value.</div>
<dl>
<dt><span class="paramLabel">Parameters:</span></dt>
<dd><code>mimeType</code> - The MIME type.</dd>
@ -591,8 +590,8 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<a href="https://developer.android.com/reference/android/net/Uri.html" title="class or interface in android.net" class="externalLink" target="_top">Uri</a>&nbsp;licenseUri)</pre>
<div class="block">Sets the optional default DRM license server URI. If this URI is set, the <a href="MediaItem.DrmConfiguration.html#uuid"><code>MediaItem.DrmConfiguration.uuid</code></a> needs to be specified as well.
<p>If <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> is passed a non-null <code>uri</code>, the DRM license server URI is used to
create a <a href="MediaItem.PlaybackProperties.html" title="class in com.google.android.exoplayer2"><code>MediaItem.PlaybackProperties</code></a> object. Otherwise it will be ignored.</div>
<p>This method should only be called if both <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> and <a href="#setDrmUuid(java.util.UUID)"><code>setDrmUuid(UUID)</code></a>
are passed non-null values.</div>
</li>
</ul>
<a id="setDrmLicenseUri(java.lang.String)">
@ -605,8 +604,8 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<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;licenseUri)</pre>
<div class="block">Sets the optional default DRM license server URI. If this URI is set, the <a href="MediaItem.DrmConfiguration.html#uuid"><code>MediaItem.DrmConfiguration.uuid</code></a> needs to be specified as well.
<p>If <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> is passed a non-null <code>uri</code>, the DRM license server URI is used to
create a <a href="MediaItem.PlaybackProperties.html" title="class in com.google.android.exoplayer2"><code>MediaItem.PlaybackProperties</code></a> object. Otherwise it will be ignored.</div>
<p>This method should only be called if both <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> and <a href="#setDrmUuid(java.util.UUID)"><code>setDrmUuid(UUID)</code></a>
are passed non-null values.</div>
</li>
</ul>
<a id="setDrmLicenseRequestHeaders(java.util.Map)">
@ -621,7 +620,8 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<p><code>null</code> or an empty <a href="https://developer.android.com/reference/java/util/Map.html" title="class or interface in java.util" class="externalLink" target="_top"><code>Map</code></a> can be used for a reset.
<p>If no valid DRM configuration is specified, the DRM license request headers are ignored.</div>
<p>This method should only be called if both <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> and <a href="#setDrmUuid(java.util.UUID)"><code>setDrmUuid(UUID)</code></a>
are passed non-null values.</div>
</li>
</ul>
<a id="setDrmUuid(java.util.UUID)">
@ -632,10 +632,12 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<h4>setDrmUuid</h4>
<pre class="methodSignature">public&nbsp;<a href="MediaItem.Builder.html" title="class in com.google.android.exoplayer2">MediaItem.Builder</a>&nbsp;setDrmUuid&#8203;(@Nullable
<a href="https://developer.android.com/reference/java/util/UUID.html" title="class or interface in java.util" class="externalLink" target="_top">UUID</a>&nbsp;uuid)</pre>
<div class="block">Sets the <a href="https://developer.android.com/reference/java/util/UUID.html" title="class or interface in java.util" class="externalLink"><code>UUID</code></a> of the protection scheme. If a DRM system UUID is set, the <a href="MediaItem.DrmConfiguration.html#licenseUri" target="_top"><code>MediaItem.DrmConfiguration.licenseUri</code></a> needs to be set as well.
<div class="block">Sets the <a href="https://developer.android.com/reference/java/util/UUID.html" title="class or interface in java.util" class="externalLink" target="_top"><code>UUID</code></a> of the protection scheme.
<p>If <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> is passed a non-null <code>uri</code>, the DRM system UUID is used to create
a <a href="MediaItem.PlaybackProperties.html" title="class in com.google.android.exoplayer2"><code>MediaItem.PlaybackProperties</code></a> object. Otherwise it will be ignored.</div>
<p>If <code>uuid</code> is null or unset then no <a href="MediaItem.DrmConfiguration.html" title="class in com.google.android.exoplayer2"><code>MediaItem.DrmConfiguration</code></a> object is created during
<a href="#build()"><code>build()</code></a> and no other <code>Builder</code> methods that would populate <a href="MediaItem.PlaybackProperties.html#drmConfiguration"><code>MediaItem.PlaybackProperties.drmConfiguration</code></a> should be called.
<p>This method should only be called if <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> is passed a non-null value.</div>
</li>
</ul>
<a id="setDrmMultiSession(boolean)">
@ -647,8 +649,8 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<pre class="methodSignature">public&nbsp;<a href="MediaItem.Builder.html" title="class in com.google.android.exoplayer2">MediaItem.Builder</a>&nbsp;setDrmMultiSession&#8203;(boolean&nbsp;multiSession)</pre>
<div class="block">Sets whether the DRM configuration is multi session enabled.
<p>If <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> is passed a non-null <code>uri</code>, the DRM multi session flag is used to
create a <a href="MediaItem.PlaybackProperties.html" title="class in com.google.android.exoplayer2"><code>MediaItem.PlaybackProperties</code></a> object. Otherwise it will be ignored.</div>
<p>This method should only be called if both <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> and <a href="#setDrmUuid(java.util.UUID)"><code>setDrmUuid(UUID)</code></a>
are passed non-null values.</div>
</li>
</ul>
<a id="setDrmForceDefaultLicenseUri(boolean)">
@ -661,8 +663,8 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<div class="block">Sets whether to force use the default DRM license server URI even if the media specifies its
own DRM license server URI.
<p>If <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> is passed a non-null <code>uri</code>, the DRM force default license flag is
used to create a <a href="MediaItem.PlaybackProperties.html" title="class in com.google.android.exoplayer2"><code>MediaItem.PlaybackProperties</code></a> object. Otherwise it will be ignored.</div>
<p>This method should only be called if both <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> and <a href="#setDrmUuid(java.util.UUID)"><code>setDrmUuid(UUID)</code></a>
are passed non-null values.</div>
</li>
</ul>
<a id="setDrmPlayClearContentWithoutKey(boolean)">
@ -673,7 +675,10 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<h4>setDrmPlayClearContentWithoutKey</h4>
<pre class="methodSignature">public&nbsp;<a href="MediaItem.Builder.html" title="class in com.google.android.exoplayer2">MediaItem.Builder</a>&nbsp;setDrmPlayClearContentWithoutKey&#8203;(boolean&nbsp;playClearContentWithoutKey)</pre>
<div class="block">Sets whether clear samples within protected content should be played when keys for the
encrypted part of the content have yet to be loaded.</div>
encrypted part of the content have yet to be loaded.
<p>This method should only be called if both <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> and <a href="#setDrmUuid(java.util.UUID)"><code>setDrmUuid(UUID)</code></a>
are passed non-null values.</div>
</li>
</ul>
<a id="setDrmSessionForClearPeriods(boolean)">
@ -686,7 +691,10 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<div class="block">Sets whether a DRM session should be used for clear tracks of type <a href="C.html#TRACK_TYPE_VIDEO"><code>C.TRACK_TYPE_VIDEO</code></a>
and <a href="C.html#TRACK_TYPE_AUDIO"><code>C.TRACK_TYPE_AUDIO</code></a>.
<p>This method overrides what has been set by previously calling <a href="#setDrmSessionForClearTypes(java.util.List)"><code>setDrmSessionForClearTypes(List)</code></a>.</div>
<p>This method overrides what has been set by previously calling <a href="#setDrmSessionForClearTypes(java.util.List)"><code>setDrmSessionForClearTypes(List)</code></a>.
<p>This method should only be called if both <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> and <a href="#setDrmUuid(java.util.UUID)"><code>setDrmUuid(UUID)</code></a>
are passed non-null values.</div>
</li>
</ul>
<a id="setDrmSessionForClearTypes(java.util.List)">
@ -704,7 +712,10 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<p>This method overrides what has been set by previously calling <a href="#setDrmSessionForClearPeriods(boolean)"><code>setDrmSessionForClearPeriods(boolean)</code></a>.
<p><code>null</code> or an empty <a href="https://developer.android.com/reference/java/util/List.html" title="class or interface in java.util" class="externalLink" target="_top"><code>List</code></a> can be used for a reset.</div>
<p><code>null</code> or an empty <a href="https://developer.android.com/reference/java/util/List.html" title="class or interface in java.util" class="externalLink" target="_top"><code>List</code></a> can be used for a reset.
<p>This method should only be called if both <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> and <a href="#setDrmUuid(java.util.UUID)"><code>setDrmUuid(UUID)</code></a>
are passed non-null values.</div>
</li>
</ul>
<a id="setDrmKeySetId(byte[])">
@ -721,7 +732,8 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
release an existing offline license (see <code>DefaultDrmSessionManager#setMode(int
mode,byte[] offlineLicenseKeySetId)</code>).
<p>If no valid DRM configuration is specified, the key set ID is ignored.</div>
<p>This method should only be called if both <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> and <a href="#setDrmUuid(java.util.UUID)"><code>setDrmUuid(UUID)</code></a>
are passed non-null values.</div>
</li>
</ul>
<a id="setStreamKeys(java.util.List)">
@ -751,8 +763,7 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<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;customCacheKey)</pre>
<div class="block">Sets the optional custom cache key (only used for progressive streams).
<p>If <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> is passed a non-null <code>uri</code>, the custom cache key is used to
create a <a href="MediaItem.PlaybackProperties.html" title="class in com.google.android.exoplayer2"><code>MediaItem.PlaybackProperties</code></a> object. Otherwise it will be ignored.</div>
<p>This method should only be called if <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> is passed a non-null value.</div>
</li>
</ul>
<a id="setSubtitles(java.util.List)">
@ -767,8 +778,7 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<p><code>null</code> or an empty <a href="https://developer.android.com/reference/java/util/List.html" title="class or interface in java.util" class="externalLink" target="_top"><code>List</code></a> can be used for a reset.
<p>If <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> is passed a non-null <code>uri</code>, the subtitles are used to create a
<a href="MediaItem.PlaybackProperties.html" title="class in com.google.android.exoplayer2"><code>MediaItem.PlaybackProperties</code></a> object. Otherwise they will be ignored.</div>
<p>This method should only be called if <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> is passed a non-null value.</div>
</li>
</ul>
<a id="setAdTagUri(java.lang.String)">
@ -781,12 +791,11 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<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;adTagUri)</pre>
<div class="block">Sets the optional ad tag <a href="https://developer.android.com/reference/android/net/Uri.html" title="class or interface in android.net" class="externalLink" target="_top"><code>Uri</code></a>.
<p>If <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> is passed a non-null <code>uri</code>, the ad tag URI is used to create a
<a href="MediaItem.PlaybackProperties.html" title="class in com.google.android.exoplayer2"><code>MediaItem.PlaybackProperties</code></a> object. Otherwise it will be ignored.
<p>Media items in the playlist with the same ad tag URI, media ID and ads loader will share
the same ad playback state. To resume ad playback when recreating the playlist on returning
from the background, pass media items with the same ad tag URIs and media IDs to the player.</div>
from the background, pass media items with the same ad tag URIs and media IDs to the player.
<p>This method should only be called if <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> is passed a non-null value.</div>
<dl>
<dt><span class="paramLabel">Parameters:</span></dt>
<dd><code>adTagUri</code> - The ad tag URI to load.</dd>
@ -803,12 +812,11 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<a href="https://developer.android.com/reference/android/net/Uri.html" title="class or interface in android.net" class="externalLink" target="_top">Uri</a>&nbsp;adTagUri)</pre>
<div class="block">Sets the optional ad tag <a href="https://developer.android.com/reference/android/net/Uri.html" title="class or interface in android.net" class="externalLink" target="_top"><code>Uri</code></a>.
<p>If <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> is passed a non-null <code>uri</code>, the ad tag URI is used to create a
<a href="MediaItem.PlaybackProperties.html" title="class in com.google.android.exoplayer2"><code>MediaItem.PlaybackProperties</code></a> object. Otherwise it will be ignored.
<p>Media items in the playlist with the same ad tag URI, media ID and ads loader will share
the same ad playback state. To resume ad playback when recreating the playlist on returning
from the background, pass media items with the same ad tag URIs and media IDs to the player.</div>
from the background, pass media items with the same ad tag URIs and media IDs to the player.
<p>This method should only be called if <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> is passed a non-null value.</div>
<dl>
<dt><span class="paramLabel">Parameters:</span></dt>
<dd><code>adTagUri</code> - The ad tag URI to load.</dd>
@ -827,12 +835,11 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<a href="https://developer.android.com/reference/java/lang/Object.html" title="class or interface in java.lang" class="externalLink" target="_top">Object</a>&nbsp;adsId)</pre>
<div class="block">Sets the optional ad tag <a href="https://developer.android.com/reference/android/net/Uri.html" title="class or interface in android.net" class="externalLink" target="_top"><code>Uri</code></a> and ads identifier.
<p>If <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> is passed a non-null <code>uri</code>, the ad tag URI is used to create a
<a href="MediaItem.PlaybackProperties.html" title="class in com.google.android.exoplayer2"><code>MediaItem.PlaybackProperties</code></a> object. Otherwise it will be ignored.
<p>Media items in the playlist that have the same ads identifier and ads loader share the
same ad playback state. To resume ad playback when recreating the playlist on returning from
the background, pass the same ads IDs to the player.</div>
the background, pass the same ads IDs to the player.
<p>This method should only be called if <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> is passed a non-null value.</div>
<dl>
<dt><span class="paramLabel">Parameters:</span></dt>
<dd><code>adTagUri</code> - The ad tag URI to load.</dd>
@ -940,7 +947,7 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
published in the <code>com.google.android.exoplayer2.Timeline</code> of the source as <code>
com.google.android.exoplayer2.Timeline.Window#tag</code>.
<p>If <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> is passed a non-null <code>uri</code>, the tag is used to create a <a href="MediaItem.PlaybackProperties.html" title="class in com.google.android.exoplayer2"><code>MediaItem.PlaybackProperties</code></a> object. Otherwise it will be ignored.</div>
<p>This method should only be called if <a href="#setUri(java.lang.String)"><code>setUri(java.lang.String)</code></a> is passed a non-null value.</div>
</li>
</ul>
<a id="setMediaMetadata(com.google.android.exoplayer2.MediaMetadata)">

View File

@ -25,7 +25,7 @@
catch(err) {
}
//-->
var data = {"i0":10,"i1":10,"i2":10,"i3":10,"i4":10,"i5":10,"i6":10,"i7":10,"i8":10,"i9":10,"i10":10,"i11":10,"i12":10};
var data = {"i0":10,"i1":10,"i2":10,"i3":10,"i4":10,"i5":10,"i6":10,"i7":10,"i8":10,"i9":10,"i10":10,"i11":10,"i12":10,"i13":10,"i14":10,"i15":10,"i16":10,"i17":10,"i18":10,"i19":10,"i20":10};
var tabs = {65535:["t0","All Methods"],2:["t2","Instance Methods"],8:["t4","Concrete Methods"]};
var altColor = "altColor";
var rowColor = "rowColor";
@ -221,53 +221,109 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
</tr>
<tr id="i6" class="altColor">
<td class="colFirst"><code><a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setArtworkData(byte%5B%5D)">setArtworkData</a></span>&#8203;(byte[]&nbsp;artworkData)</code></th>
<td class="colLast">
<div class="block">Sets the artwork data as a compressed byte array.</div>
</td>
</tr>
<tr id="i7" class="rowColor">
<td class="colFirst"><code><a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setArtworkUri(android.net.Uri)">setArtworkUri</a></span>&#8203;(<a href="https://developer.android.com/reference/android/net/Uri.html" title="class or interface in android.net" class="externalLink" target="_top">Uri</a>&nbsp;artworkUri)</code></th>
<td class="colLast">
<div class="block">Sets the artwork <a href="https://developer.android.com/reference/android/net/Uri.html" title="class or interface in android.net" class="externalLink" target="_top"><code>Uri</code></a>.</div>
</td>
</tr>
<tr id="i8" class="altColor">
<td class="colFirst"><code><a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setDescription(java.lang.CharSequence)">setDescription</a></span>&#8203;(<a href="https://developer.android.com/reference/java/lang/CharSequence.html" title="class or interface in java.lang" class="externalLink" target="_top">CharSequence</a>&nbsp;description)</code></th>
<td class="colLast">
<div class="block">Sets the description.</div>
</td>
</tr>
<tr id="i7" class="rowColor">
<tr id="i9" class="rowColor">
<td class="colFirst"><code><a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setDisplayTitle(java.lang.CharSequence)">setDisplayTitle</a></span>&#8203;(<a href="https://developer.android.com/reference/java/lang/CharSequence.html" title="class or interface in java.lang" class="externalLink" target="_top">CharSequence</a>&nbsp;displayTitle)</code></th>
<td class="colLast">
<div class="block">Sets the display title.</div>
</td>
</tr>
<tr id="i8" class="altColor">
<tr id="i10" class="altColor">
<td class="colFirst"><code><a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setExtras(android.os.Bundle)">setExtras</a></span>&#8203;(<a href="https://developer.android.com/reference/android/os/Bundle.html" title="class or interface in android.os" class="externalLink" target="_top">Bundle</a>&nbsp;extras)</code></th>
<td class="colLast">
<div class="block">Sets the extras <a href="https://developer.android.com/reference/android/os/Bundle.html" title="class or interface in android.os" class="externalLink" target="_top"><code>Bundle</code></a>.</div>
</td>
</tr>
<tr id="i11" class="rowColor">
<td class="colFirst"><code><a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setFolderType(java.lang.Integer)">setFolderType</a></span>&#8203;(<a href="https://developer.android.com/reference/java/lang/Integer.html" title="class or interface in java.lang" class="externalLink" target="_top">Integer</a>&nbsp;folderType)</code></th>
<td class="colLast">
<div class="block">Sets the <a href="MediaMetadata.FolderType.html" title="annotation in com.google.android.exoplayer2"><code>MediaMetadata.FolderType</code></a>.</div>
</td>
</tr>
<tr id="i12" class="altColor">
<td class="colFirst"><code><a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setIsPlayable(java.lang.Boolean)">setIsPlayable</a></span>&#8203;(<a href="https://developer.android.com/reference/java/lang/Boolean.html" title="class or interface in java.lang" class="externalLink" target="_top">Boolean</a>&nbsp;isPlayable)</code></th>
<td class="colLast">
<div class="block">Sets whether the media is playable.</div>
</td>
</tr>
<tr id="i13" class="rowColor">
<td class="colFirst"><code><a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setMediaUri(android.net.Uri)">setMediaUri</a></span>&#8203;(<a href="https://developer.android.com/reference/android/net/Uri.html" title="class or interface in android.net" class="externalLink" target="_top">Uri</a>&nbsp;mediaUri)</code></th>
<td class="colLast">
<div class="block">Sets the media <a href="https://developer.android.com/reference/android/net/Uri.html" title="class or interface in android.net" class="externalLink" target="_top"><code>Uri</code></a>.</div>
</td>
</tr>
<tr id="i9" class="rowColor">
<tr id="i14" class="altColor">
<td class="colFirst"><code><a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setOverallRating(com.google.android.exoplayer2.Rating)">setOverallRating</a></span>&#8203;(<a href="Rating.html" title="class in com.google.android.exoplayer2">Rating</a>&nbsp;overallRating)</code></th>
<td class="colLast">
<div class="block">Sets the overall <a href="Rating.html" title="class in com.google.android.exoplayer2"><code>Rating</code></a>.</div>
</td>
</tr>
<tr id="i10" class="altColor">
<tr id="i15" class="rowColor">
<td class="colFirst"><code><a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setSubtitle(java.lang.CharSequence)">setSubtitle</a></span>&#8203;(<a href="https://developer.android.com/reference/java/lang/CharSequence.html" title="class or interface in java.lang" class="externalLink" target="_top">CharSequence</a>&nbsp;subtitle)</code></th>
<td class="colLast">
<div class="block">Sets the subtitle.</div>
</td>
</tr>
<tr id="i11" class="rowColor">
<tr id="i16" class="altColor">
<td class="colFirst"><code><a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setTitle(java.lang.CharSequence)">setTitle</a></span>&#8203;(<a href="https://developer.android.com/reference/java/lang/CharSequence.html" title="class or interface in java.lang" class="externalLink" target="_top">CharSequence</a>&nbsp;title)</code></th>
<td class="colLast">
<div class="block">Sets the title.</div>
</td>
</tr>
<tr id="i12" class="altColor">
<tr id="i17" class="rowColor">
<td class="colFirst"><code><a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setTotalTrackCount(java.lang.Integer)">setTotalTrackCount</a></span>&#8203;(<a href="https://developer.android.com/reference/java/lang/Integer.html" title="class or interface in java.lang" class="externalLink" target="_top">Integer</a>&nbsp;totalTrackCount)</code></th>
<td class="colLast">
<div class="block">Sets the total number of tracks.</div>
</td>
</tr>
<tr id="i18" class="altColor">
<td class="colFirst"><code><a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setTrackNumber(java.lang.Integer)">setTrackNumber</a></span>&#8203;(<a href="https://developer.android.com/reference/java/lang/Integer.html" title="class or interface in java.lang" class="externalLink" target="_top">Integer</a>&nbsp;trackNumber)</code></th>
<td class="colLast">
<div class="block">Sets the track number.</div>
</td>
</tr>
<tr id="i19" class="rowColor">
<td class="colFirst"><code><a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setUserRating(com.google.android.exoplayer2.Rating)">setUserRating</a></span>&#8203;(<a href="Rating.html" title="class in com.google.android.exoplayer2">Rating</a>&nbsp;userRating)</code></th>
<td class="colLast">
<div class="block">Sets the user <a href="Rating.html" title="class in com.google.android.exoplayer2"><code>Rating</code></a>.</div>
</td>
</tr>
<tr id="i20" class="altColor">
<td class="colFirst"><code><a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setYear(java.lang.Integer)">setYear</a></span>&#8203;(<a href="https://developer.android.com/reference/java/lang/Integer.html" title="class or interface in java.lang" class="externalLink" target="_top">Integer</a>&nbsp;year)</code></th>
<td class="colLast">
<div class="block">Sets the year.</div>
</td>
</tr>
</table>
<ul class="blockList">
<li class="blockList"><a id="methods.inherited.from.class.java.lang.Object">
@ -423,6 +479,94 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<div class="block">Sets the overall <a href="Rating.html" title="class in com.google.android.exoplayer2"><code>Rating</code></a>.</div>
</li>
</ul>
<a id="setArtworkData(byte[])">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>setArtworkData</h4>
<pre class="methodSignature">public&nbsp;<a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a>&nbsp;setArtworkData&#8203;(@Nullable
byte[]&nbsp;artworkData)</pre>
<div class="block">Sets the artwork data as a compressed byte array.</div>
</li>
</ul>
<a id="setArtworkUri(android.net.Uri)">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>setArtworkUri</h4>
<pre class="methodSignature">public&nbsp;<a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a>&nbsp;setArtworkUri&#8203;(@Nullable
<a href="https://developer.android.com/reference/android/net/Uri.html" title="class or interface in android.net" class="externalLink" target="_top">Uri</a>&nbsp;artworkUri)</pre>
<div class="block">Sets the artwork <a href="https://developer.android.com/reference/android/net/Uri.html" title="class or interface in android.net" class="externalLink" target="_top"><code>Uri</code></a>.</div>
</li>
</ul>
<a id="setTrackNumber(java.lang.Integer)">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>setTrackNumber</h4>
<pre class="methodSignature">public&nbsp;<a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a>&nbsp;setTrackNumber&#8203;(@Nullable
<a href="https://developer.android.com/reference/java/lang/Integer.html" title="class or interface in java.lang" class="externalLink" target="_top">Integer</a>&nbsp;trackNumber)</pre>
<div class="block">Sets the track number.</div>
</li>
</ul>
<a id="setTotalTrackCount(java.lang.Integer)">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>setTotalTrackCount</h4>
<pre class="methodSignature">public&nbsp;<a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a>&nbsp;setTotalTrackCount&#8203;(@Nullable
<a href="https://developer.android.com/reference/java/lang/Integer.html" title="class or interface in java.lang" class="externalLink" target="_top">Integer</a>&nbsp;totalTrackCount)</pre>
<div class="block">Sets the total number of tracks.</div>
</li>
</ul>
<a id="setFolderType(java.lang.Integer)">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>setFolderType</h4>
<pre class="methodSignature">public&nbsp;<a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a>&nbsp;setFolderType&#8203;(@Nullable <a href="MediaMetadata.FolderType.html" title="annotation in com.google.android.exoplayer2">@FolderType</a>
<a href="https://developer.android.com/reference/java/lang/Integer.html" title="class or interface in java.lang" class="externalLink" target="_top">Integer</a>&nbsp;folderType)</pre>
<div class="block">Sets the <a href="MediaMetadata.FolderType.html" title="annotation in com.google.android.exoplayer2"><code>MediaMetadata.FolderType</code></a>.</div>
</li>
</ul>
<a id="setIsPlayable(java.lang.Boolean)">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>setIsPlayable</h4>
<pre class="methodSignature">public&nbsp;<a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a>&nbsp;setIsPlayable&#8203;(@Nullable
<a href="https://developer.android.com/reference/java/lang/Boolean.html" title="class or interface in java.lang" class="externalLink" target="_top">Boolean</a>&nbsp;isPlayable)</pre>
<div class="block">Sets whether the media is playable.</div>
</li>
</ul>
<a id="setYear(java.lang.Integer)">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>setYear</h4>
<pre class="methodSignature">public&nbsp;<a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a>&nbsp;setYear&#8203;(@Nullable
<a href="https://developer.android.com/reference/java/lang/Integer.html" title="class or interface in java.lang" class="externalLink" target="_top">Integer</a>&nbsp;year)</pre>
<div class="block">Sets the year.</div>
</li>
</ul>
<a id="setExtras(android.os.Bundle)">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>setExtras</h4>
<pre class="methodSignature">public&nbsp;<a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a>&nbsp;setExtras&#8203;(@Nullable
<a href="https://developer.android.com/reference/android/os/Bundle.html" title="class or interface in android.os" class="externalLink" target="_top">Bundle</a>&nbsp;extras)</pre>
<div class="block">Sets the extras <a href="https://developer.android.com/reference/android/os/Bundle.html" title="class or interface in android.os" class="externalLink" target="_top"><code>Bundle</code></a>.</div>
</li>
</ul>
<a id="populateFromMetadata(com.google.android.exoplayer2.metadata.Metadata)">
<!-- -->
</a>

View File

@ -0,0 +1,188 @@
<!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>

View File

@ -164,6 +164,13 @@ implements <a href="Bundleable.html" title="interface in com.google.android.exop
<div class="block">A builder for <a href="MediaMetadata.html" title="class in com.google.android.exoplayer2"><code>MediaMetadata</code></a> instances.</div>
</td>
</tr>
<tr class="rowColor">
<td class="colFirst"><code>static interface&nbsp;</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="MediaMetadata.FolderType.html" title="annotation in com.google.android.exoplayer2">MediaMetadata.FolderType</a></span></code></th>
<td class="colLast">
<div class="block">The folder type of the media item.</div>
</td>
</tr>
</table>
<ul class="blockList">
<li class="blockList"><a id="nested.classes.inherited.from.class.com.google.android.exoplayer2.Bundleable">
@ -211,6 +218,20 @@ implements <a href="Bundleable.html" title="interface in com.google.android.exop
</td>
</tr>
<tr class="rowColor">
<td class="colFirst"><code>byte[]</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#artworkData">artworkData</a></span></code></th>
<td class="colLast">
<div class="block">Optional artwork data as a compressed byte array.</div>
</td>
</tr>
<tr class="altColor">
<td class="colFirst"><code><a href="https://developer.android.com/reference/android/net/Uri.html" title="class or interface in android.net" class="externalLink" target="_top">Uri</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#artworkUri">artworkUri</a></span></code></th>
<td class="colLast">
<div class="block">Optional artwork <a href="https://developer.android.com/reference/android/net/Uri.html" title="class or interface in android.net" class="externalLink" target="_top"><code>Uri</code></a>.</div>
</td>
</tr>
<tr class="rowColor">
<td class="colFirst"><code>static <a href="Bundleable.Creator.html" title="interface in com.google.android.exoplayer2">Bundleable.Creator</a>&lt;<a href="MediaMetadata.html" title="class in com.google.android.exoplayer2">MediaMetadata</a>&gt;</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#CREATOR">CREATOR</a></span></code></th>
<td class="colLast">
@ -239,6 +260,76 @@ implements <a href="Bundleable.html" title="interface in com.google.android.exop
</td>
</tr>
<tr class="rowColor">
<td class="colFirst"><code><a href="https://developer.android.com/reference/android/os/Bundle.html" title="class or interface in android.os" class="externalLink" target="_top">Bundle</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#extras">extras</a></span></code></th>
<td class="colLast">
<div class="block">Optional extras <a href="https://developer.android.com/reference/android/os/Bundle.html" title="class or interface in android.os" class="externalLink" target="_top"><code>Bundle</code></a>.</div>
</td>
</tr>
<tr class="altColor">
<td class="colFirst"><code>static int</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#FOLDER_TYPE_ALBUMS">FOLDER_TYPE_ALBUMS</a></span></code></th>
<td class="colLast">
<div class="block">Type for a folder containing media categorized by album.</div>
</td>
</tr>
<tr class="rowColor">
<td class="colFirst"><code>static int</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#FOLDER_TYPE_ARTISTS">FOLDER_TYPE_ARTISTS</a></span></code></th>
<td class="colLast">
<div class="block">Type for a folder containing media categorized by artist.</div>
</td>
</tr>
<tr class="altColor">
<td class="colFirst"><code>static int</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#FOLDER_TYPE_GENRES">FOLDER_TYPE_GENRES</a></span></code></th>
<td class="colLast">
<div class="block">Type for a folder containing media categorized by genre.</div>
</td>
</tr>
<tr class="rowColor">
<td class="colFirst"><code>static int</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#FOLDER_TYPE_MIXED">FOLDER_TYPE_MIXED</a></span></code></th>
<td class="colLast">
<div class="block">Type for a folder containing media of mixed types.</div>
</td>
</tr>
<tr class="altColor">
<td class="colFirst"><code>static int</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#FOLDER_TYPE_PLAYLISTS">FOLDER_TYPE_PLAYLISTS</a></span></code></th>
<td class="colLast">
<div class="block">Type for a folder containing a playlist.</div>
</td>
</tr>
<tr class="rowColor">
<td class="colFirst"><code>static int</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#FOLDER_TYPE_TITLES">FOLDER_TYPE_TITLES</a></span></code></th>
<td class="colLast">
<div class="block">Type for a folder containing only playable media.</div>
</td>
</tr>
<tr class="altColor">
<td class="colFirst"><code>static int</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#FOLDER_TYPE_YEARS">FOLDER_TYPE_YEARS</a></span></code></th>
<td class="colLast">
<div class="block">Type for a folder containing media categorized by year.</div>
</td>
</tr>
<tr class="rowColor">
<td class="colFirst"><code><a href="https://developer.android.com/reference/java/lang/Integer.html" title="class or interface in java.lang" class="externalLink" target="_top">Integer</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#folderType">folderType</a></span></code></th>
<td class="colLast">
<div class="block">Optional <a href="MediaMetadata.FolderType.html" title="annotation in com.google.android.exoplayer2"><code>MediaMetadata.FolderType</code></a>.</div>
</td>
</tr>
<tr class="altColor">
<td class="colFirst"><code><a href="https://developer.android.com/reference/java/lang/Boolean.html" title="class or interface in java.lang" class="externalLink" target="_top">Boolean</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#isPlayable">isPlayable</a></span></code></th>
<td class="colLast">
<div class="block">Optional boolean for media playability.</div>
</td>
</tr>
<tr class="rowColor">
<td class="colFirst"><code><a href="https://developer.android.com/reference/android/net/Uri.html" title="class or interface in android.net" class="externalLink" target="_top">Uri</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#mediaUri">mediaUri</a></span></code></th>
<td class="colLast">
@ -267,12 +358,33 @@ implements <a href="Bundleable.html" title="interface in com.google.android.exop
</td>
</tr>
<tr class="rowColor">
<td class="colFirst"><code><a href="https://developer.android.com/reference/java/lang/Integer.html" title="class or interface in java.lang" class="externalLink" target="_top">Integer</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#totalTrackCount">totalTrackCount</a></span></code></th>
<td class="colLast">
<div class="block">Optional total number of tracks.</div>
</td>
</tr>
<tr class="altColor">
<td class="colFirst"><code><a href="https://developer.android.com/reference/java/lang/Integer.html" title="class or interface in java.lang" class="externalLink" target="_top">Integer</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#trackNumber">trackNumber</a></span></code></th>
<td class="colLast">
<div class="block">Optional track number.</div>
</td>
</tr>
<tr class="rowColor">
<td class="colFirst"><code><a href="Rating.html" title="class in com.google.android.exoplayer2">Rating</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#userRating">userRating</a></span></code></th>
<td class="colLast">
<div class="block">Optional user <a href="Rating.html" title="class in com.google.android.exoplayer2"><code>Rating</code></a>.</div>
</td>
</tr>
<tr class="altColor">
<td class="colFirst"><code><a href="https://developer.android.com/reference/java/lang/Integer.html" title="class or interface in java.lang" class="externalLink" target="_top">Integer</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#year">year</a></span></code></th>
<td class="colLast">
<div class="block">Optional year.</div>
</td>
</tr>
</table>
</li>
</ul>
@ -339,6 +451,104 @@ implements <a href="Bundleable.html" title="interface in com.google.android.exop
<!-- -->
</a>
<h3>Field Detail</h3>
<a id="FOLDER_TYPE_MIXED">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>FOLDER_TYPE_MIXED</h4>
<pre>public static final&nbsp;int FOLDER_TYPE_MIXED</pre>
<div class="block">Type for a folder containing media of mixed types.</div>
<dl>
<dt><span class="seeLabel">See Also:</span></dt>
<dd><a href="../../../../constant-values.html#com.google.android.exoplayer2.MediaMetadata.FOLDER_TYPE_MIXED">Constant Field Values</a></dd>
</dl>
</li>
</ul>
<a id="FOLDER_TYPE_TITLES">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>FOLDER_TYPE_TITLES</h4>
<pre>public static final&nbsp;int FOLDER_TYPE_TITLES</pre>
<div class="block">Type for a folder containing only playable media.</div>
<dl>
<dt><span class="seeLabel">See Also:</span></dt>
<dd><a href="../../../../constant-values.html#com.google.android.exoplayer2.MediaMetadata.FOLDER_TYPE_TITLES">Constant Field Values</a></dd>
</dl>
</li>
</ul>
<a id="FOLDER_TYPE_ALBUMS">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>FOLDER_TYPE_ALBUMS</h4>
<pre>public static final&nbsp;int FOLDER_TYPE_ALBUMS</pre>
<div class="block">Type for a folder containing media categorized by album.</div>
<dl>
<dt><span class="seeLabel">See Also:</span></dt>
<dd><a href="../../../../constant-values.html#com.google.android.exoplayer2.MediaMetadata.FOLDER_TYPE_ALBUMS">Constant Field Values</a></dd>
</dl>
</li>
</ul>
<a id="FOLDER_TYPE_ARTISTS">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>FOLDER_TYPE_ARTISTS</h4>
<pre>public static final&nbsp;int FOLDER_TYPE_ARTISTS</pre>
<div class="block">Type for a folder containing media categorized by artist.</div>
<dl>
<dt><span class="seeLabel">See Also:</span></dt>
<dd><a href="../../../../constant-values.html#com.google.android.exoplayer2.MediaMetadata.FOLDER_TYPE_ARTISTS">Constant Field Values</a></dd>
</dl>
</li>
</ul>
<a id="FOLDER_TYPE_GENRES">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>FOLDER_TYPE_GENRES</h4>
<pre>public static final&nbsp;int FOLDER_TYPE_GENRES</pre>
<div class="block">Type for a folder containing media categorized by genre.</div>
<dl>
<dt><span class="seeLabel">See Also:</span></dt>
<dd><a href="../../../../constant-values.html#com.google.android.exoplayer2.MediaMetadata.FOLDER_TYPE_GENRES">Constant Field Values</a></dd>
</dl>
</li>
</ul>
<a id="FOLDER_TYPE_PLAYLISTS">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>FOLDER_TYPE_PLAYLISTS</h4>
<pre>public static final&nbsp;int FOLDER_TYPE_PLAYLISTS</pre>
<div class="block">Type for a folder containing a playlist.</div>
<dl>
<dt><span class="seeLabel">See Also:</span></dt>
<dd><a href="../../../../constant-values.html#com.google.android.exoplayer2.MediaMetadata.FOLDER_TYPE_PLAYLISTS">Constant Field Values</a></dd>
</dl>
</li>
</ul>
<a id="FOLDER_TYPE_YEARS">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>FOLDER_TYPE_YEARS</h4>
<pre>public static final&nbsp;int FOLDER_TYPE_YEARS</pre>
<div class="block">Type for a folder containing media categorized by year.</div>
<dl>
<dt><span class="seeLabel">See Also:</span></dt>
<dd><a href="../../../../constant-values.html#com.google.android.exoplayer2.MediaMetadata.FOLDER_TYPE_YEARS">Constant Field Values</a></dd>
</dl>
</li>
</ul>
<a id="EMPTY">
<!-- -->
</a>
@ -461,6 +671,98 @@ public final&nbsp;<a href="Rating.html" title="class in com.google.android.exopl
<div class="block">Optional overall <a href="Rating.html" title="class in com.google.android.exoplayer2"><code>Rating</code></a>.</div>
</li>
</ul>
<a id="artworkData">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>artworkData</h4>
<pre>@Nullable
public final&nbsp;byte[] artworkData</pre>
<div class="block">Optional artwork data as a compressed byte array.</div>
</li>
</ul>
<a id="artworkUri">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>artworkUri</h4>
<pre>@Nullable
public final&nbsp;<a href="https://developer.android.com/reference/android/net/Uri.html" title="class or interface in android.net" class="externalLink" target="_top">Uri</a> artworkUri</pre>
<div class="block">Optional artwork <a href="https://developer.android.com/reference/android/net/Uri.html" title="class or interface in android.net" class="externalLink" target="_top"><code>Uri</code></a>.</div>
</li>
</ul>
<a id="trackNumber">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>trackNumber</h4>
<pre>@Nullable
public final&nbsp;<a href="https://developer.android.com/reference/java/lang/Integer.html" title="class or interface in java.lang" class="externalLink" target="_top">Integer</a> trackNumber</pre>
<div class="block">Optional track number.</div>
</li>
</ul>
<a id="totalTrackCount">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>totalTrackCount</h4>
<pre>@Nullable
public final&nbsp;<a href="https://developer.android.com/reference/java/lang/Integer.html" title="class or interface in java.lang" class="externalLink" target="_top">Integer</a> totalTrackCount</pre>
<div class="block">Optional total number of tracks.</div>
</li>
</ul>
<a id="folderType">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>folderType</h4>
<pre>@Nullable
<a href="MediaMetadata.FolderType.html" title="annotation in com.google.android.exoplayer2">@FolderType</a>
public final&nbsp;<a href="https://developer.android.com/reference/java/lang/Integer.html" title="class or interface in java.lang" class="externalLink" target="_top">Integer</a> folderType</pre>
<div class="block">Optional <a href="MediaMetadata.FolderType.html" title="annotation in com.google.android.exoplayer2"><code>MediaMetadata.FolderType</code></a>.</div>
</li>
</ul>
<a id="isPlayable">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>isPlayable</h4>
<pre>@Nullable
public final&nbsp;<a href="https://developer.android.com/reference/java/lang/Boolean.html" title="class or interface in java.lang" class="externalLink" target="_top">Boolean</a> isPlayable</pre>
<div class="block">Optional boolean for media playability.</div>
</li>
</ul>
<a id="year">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>year</h4>
<pre>@Nullable
public final&nbsp;<a href="https://developer.android.com/reference/java/lang/Integer.html" title="class or interface in java.lang" class="externalLink" target="_top">Integer</a> year</pre>
<div class="block">Optional year.</div>
</li>
</ul>
<a id="extras">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>extras</h4>
<pre>@Nullable
public final&nbsp;<a href="https://developer.android.com/reference/android/os/Bundle.html" title="class or interface in android.os" class="externalLink" target="_top">Bundle</a> extras</pre>
<div class="block">Optional extras <a href="https://developer.android.com/reference/android/os/Bundle.html" title="class or interface in android.os" class="externalLink" target="_top"><code>Bundle</code></a>.
<p>Given the complexities of checking the equality of two <a href="https://developer.android.com/reference/android/os/Bundle.html" title="class or interface in android.os" class="externalLink" target="_top"><code>Bundle</code></a>s, this is not
considered in the <a href="#equals(java.lang.Object)"><code>equals(Object)</code></a> or <a href="#hashCode()"><code>hashCode()</code></a>.</div>
</li>
</ul>
<a id="CREATOR">
<!-- -->
</a>

View File

@ -25,7 +25,7 @@
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 altColor = "altColor";
var rowColor = "rowColor";
@ -275,11 +275,18 @@ extends <a href="Id3Frame.html" title="class in com.google.android.exoplayer2.me
<td class="colLast">&nbsp;</td>
</tr>
<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>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#toString()">toString</a></span>()</code></th>
<td class="colLast">&nbsp;</td>
</tr>
<tr id="i3" class="rowColor">
<tr id="i4" class="altColor">
<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,
int&nbsp;flags)</code></th>
@ -305,7 +312,7 @@ extends <a href="Id3Frame.html" title="class in com.google.android.exoplayer2.me
<!-- -->
</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>
<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>
</li>
</ul>
@ -415,6 +422,24 @@ public final&nbsp;<a href="https://developer.android.com/reference/java/lang/Str
<!-- -->
</a>
<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>

View File

@ -732,90 +732,96 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
</td>
</tr>
<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>
<td class="colLast">
<div class="block">Commands that can be executed on a <code>Player</code>.</div>
</td>
</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>
<td class="colLast">
<div class="block">Reasons for position discontinuities.</div>
</td>
</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>
<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>
</td>
</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>
<td class="colLast">
<div class="block">Reasons for media item transitions.</div>
</td>
</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>
<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>
</td>
</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>
<td class="colLast">
<div class="block">Reasons for <a href="Player.html#getPlayWhenReady()"><code>playWhenReady</code></a> changes.</div>
</td>
</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>
<td class="colLast">
<div class="block">Repeat modes for playback.</div>
</td>
</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>
<td class="colLast">
<div class="block">Playback state.</div>
</td>
</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>
<td class="colLast">
<div class="block">Reasons for timeline changes.</div>
</td>
</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>
<td class="colLast">
<div class="block">The renderer states.</div>
</td>
</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>
<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>
</td>
</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>
<td class="colLast">
<div class="block">Level of renderer support for adaptive format switches.</div>
</td>
</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>
<td class="colLast">
<div class="block">Combined renderer capabilities.</div>
</td>
</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>
<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>
</td>
</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>
<td class="colLast">
<div class="block">Level of renderer support for tunneling.</div>

View File

@ -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="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="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.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>

View File

@ -296,62 +296,70 @@ extends <a href="HlsPlaylist.html" title="class in com.google.android.exoplayer2
</td>
</tr>
<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>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#protectionSchemes">protectionSchemes</a></span></code></th>
<td class="colLast">
<div class="block">Contains the CDM protection schemes used by segments in this playlist.</div>
</td>
</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>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#renditionReports">renditionReports</a></span></code></th>
<td class="colLast">
<div class="block">The rendition reports of alternative rendition playlists.</div>
</td>
</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>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#segments">segments</a></span></code></th>
<td class="colLast">
<div class="block">The list of segments in the playlist.</div>
</td>
</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>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#serverControl">serverControl</a></span></code></th>
<td class="colLast">
<div class="block">The attributes of the #EXT-X-SERVER-CONTROL header.</div>
</td>
</tr>
<tr class="rowColor">
<tr class="altColor">
<td class="colFirst"><code>long</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#startOffsetUs">startOffsetUs</a></span></code></th>
<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>
</tr>
<tr class="altColor">
<tr class="rowColor">
<td class="colFirst"><code>long</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#startTimeUs">startTimeUs</a></span></code></th>
<td class="colLast">
<div class="block">If <a href="#hasProgramDateTime"><code>hasProgramDateTime</code></a> is true, contains the datetime as microseconds since epoch.</div>
</td>
</tr>
<tr class="rowColor">
<tr class="altColor">
<td class="colFirst"><code>long</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#targetDurationUs">targetDurationUs</a></span></code></th>
<td class="colLast">
<div class="block">The target duration in microseconds, as defined by #EXT-X-TARGETDURATION.</div>
</td>
</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>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#trailingParts">trailingParts</a></span></code></th>
<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>
</td>
</tr>
<tr class="rowColor">
<tr class="altColor">
<td class="colFirst"><code>int</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#version">version</a></span></code></th>
<td class="colLast">
@ -383,10 +391,11 @@ extends <a href="HlsPlaylist.html" title="class in com.google.android.exoplayer2
<th class="colLast" scope="col">Description</th>
</tr>
<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/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,
boolean&nbsp;preciseStart,
long&nbsp;startTimeUs,
boolean&nbsp;hasDiscontinuitySequence,
int&nbsp;discontinuitySequence,
@ -540,7 +549,19 @@ public final&nbsp;int playlistType</pre>
<li class="blockList">
<h4>startOffsetUs</h4>
<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>
</ul>
<a id="startTimeUs">
@ -710,7 +731,7 @@ public final&nbsp;<a href="../../../drm/DrmInitData.html" title="class in com.go
<!-- -->
</a>
<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>
<ul class="blockListLast">
@ -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/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,
boolean&nbsp;preciseStart,
long&nbsp;startTimeUs,
boolean&nbsp;hasDiscontinuitySequence,
int&nbsp;discontinuitySequence,

View File

@ -25,7 +25,7 @@
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 altColor = "altColor";
var rowColor = "rowColor";
@ -243,11 +243,25 @@ implements <a href="../MediaSourceFactory.html" title="interface in com.google.a
</tr>
<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>
<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>
<td class="colLast">
<div class="block">Does nothing.</div>
</td>
</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>
<ul class="blockList">
<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
<!-- -->
</a>
<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>

View File

@ -338,18 +338,13 @@ extends <a href="../BaseMediaSource.html" title="class in com.google.android.exo
<ul class="blockList">
<li class="blockList">
<h4>maybeThrowSourceInfoRefreshError</h4>
<pre class="methodSignature">public&nbsp;void&nbsp;maybeThrowSourceInfoRefreshError()
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>
<pre class="methodSignature">public&nbsp;void&nbsp;maybeThrowSourceInfoRefreshError()</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">Throws any pending error encountered while loading or refreshing source information.
<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>
<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>
</ul>
<a id="createPeriod(com.google.android.exoplayer2.source.MediaSource.MediaPeriodId,com.google.android.exoplayer2.upstream.Allocator,long)">

View File

@ -25,7 +25,7 @@
catch(err) {
}
//-->
var data = {"i0":10,"i1":10,"i2":10,"i3":10,"i4":10,"i5":10,"i6":10,"i7":10,"i8":10,"i9":10,"i10":10,"i11":10,"i12":10,"i13":10,"i14":10,"i15":10,"i16":10,"i17":10,"i18":10,"i19":10};
var data = {"i0":10,"i1":10,"i2":10,"i3":10,"i4":10,"i5":10,"i6":10,"i7":10,"i8":10,"i9":10,"i10":10,"i11":10,"i12":10,"i13":10,"i14":10,"i15":10,"i16":10,"i17":10,"i18":10,"i19":10,"i20":10,"i21":10};
var tabs = {65535:["t0","All Methods"],2:["t2","Instance Methods"],8:["t4","Concrete Methods"]};
var altColor = "altColor";
var rowColor = "rowColor";
@ -342,13 +342,18 @@ implements <a href="../drm/ExoMediaDrm.html" title="interface in com.google.andr
</td>
</tr>
<tr id="i9" class="rowColor">
<td class="colFirst"><code>int</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#getReferenceCount()">getReferenceCount</a></span>()</code></th>
<td class="colLast">&nbsp;</td>
</tr>
<tr id="i10" class="altColor">
<td class="colFirst"><code>byte[]</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#openSession()">openSession</a></span>()</code></th>
<td class="colLast">
<div class="block">Opens a new DRM session.</div>
</td>
</tr>
<tr id="i10" class="altColor">
<tr id="i11" class="rowColor">
<td class="colFirst"><code>byte[]</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#provideKeyResponse(byte%5B%5D,byte%5B%5D)">provideKeyResponse</a></span>&#8203;(byte[]&nbsp;scope,
byte[]&nbsp;response)</code></th>
@ -356,28 +361,28 @@ implements <a href="../drm/ExoMediaDrm.html" title="interface in com.google.andr
<div class="block">Provides a key response for the last request to be generated using <a href="../drm/ExoMediaDrm.html#getKeyRequest(byte%5B%5D,java.util.List,int,java.util.HashMap)"><code>ExoMediaDrm.getKeyRequest(byte[], java.util.List&lt;com.google.android.exoplayer2.drm.DrmInitData.SchemeData&gt;, int, java.util.HashMap&lt;java.lang.String, java.lang.String&gt;)</code></a>.</div>
</td>
</tr>
<tr id="i11" class="rowColor">
<tr id="i12" class="altColor">
<td class="colFirst"><code>void</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#provideProvisionResponse(byte%5B%5D)">provideProvisionResponse</a></span>&#8203;(byte[]&nbsp;response)</code></th>
<td class="colLast">
<div class="block">Provides a provisioning response for the last request to be generated using <a href="../drm/ExoMediaDrm.html#getProvisionRequest()"><code>ExoMediaDrm.getProvisionRequest()</code></a>.</div>
</td>
</tr>
<tr id="i12" class="altColor">
<tr id="i13" 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/java/lang/String.html?is-external=true" title="class or interface in java.lang" class="externalLink">String</a>,&#8203;<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;</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#queryKeyStatus(byte%5B%5D)">queryKeyStatus</a></span>&#8203;(byte[]&nbsp;sessionId)</code></th>
<td class="colLast">
<div class="block">Returns the key status for a given session, as {name, value} pairs.</div>
</td>
</tr>
<tr id="i13" class="rowColor">
<tr id="i14" class="altColor">
<td class="colFirst"><code>void</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#release()">release</a></span>()</code></th>
<td class="colLast">
<div class="block">Decrements the reference count.</div>
</td>
</tr>
<tr id="i14" class="altColor">
<tr id="i15" class="rowColor">
<td class="colFirst"><code>void</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#restoreKeys(byte%5B%5D,byte%5B%5D)">restoreKeys</a></span>&#8203;(byte[]&nbsp;sessionId,
byte[]&nbsp;keySetId)</code></th>
@ -385,28 +390,28 @@ implements <a href="../drm/ExoMediaDrm.html" title="interface in com.google.andr
<div class="block">Restores persisted offline keys into a session.</div>
</td>
</tr>
<tr id="i15" class="rowColor">
<tr id="i16" class="altColor">
<td class="colFirst"><code>void</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setOnEventListener(com.google.android.exoplayer2.drm.ExoMediaDrm.OnEventListener)">setOnEventListener</a></span>&#8203;(<a href="../drm/ExoMediaDrm.OnEventListener.html" title="interface in com.google.android.exoplayer2.drm">ExoMediaDrm.OnEventListener</a>&nbsp;listener)</code></th>
<td class="colLast">
<div class="block">Sets the listener for DRM events.</div>
</td>
</tr>
<tr id="i16" class="altColor">
<tr id="i17" class="rowColor">
<td class="colFirst"><code>void</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setOnExpirationUpdateListener(com.google.android.exoplayer2.drm.ExoMediaDrm.OnExpirationUpdateListener)">setOnExpirationUpdateListener</a></span>&#8203;(<a href="../drm/ExoMediaDrm.OnExpirationUpdateListener.html" title="interface in com.google.android.exoplayer2.drm">ExoMediaDrm.OnExpirationUpdateListener</a>&nbsp;listener)</code></th>
<td class="colLast">
<div class="block">Sets the listener for session expiration events.</div>
</td>
</tr>
<tr id="i17" class="rowColor">
<tr id="i18" class="altColor">
<td class="colFirst"><code>void</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setOnKeyStatusChangeListener(com.google.android.exoplayer2.drm.ExoMediaDrm.OnKeyStatusChangeListener)">setOnKeyStatusChangeListener</a></span>&#8203;(<a href="../drm/ExoMediaDrm.OnKeyStatusChangeListener.html" title="interface in com.google.android.exoplayer2.drm">ExoMediaDrm.OnKeyStatusChangeListener</a>&nbsp;listener)</code></th>
<td class="colLast">
<div class="block">Sets the listener for key status change events.</div>
</td>
</tr>
<tr id="i18" class="altColor">
<tr id="i19" class="rowColor">
<td class="colFirst"><code>void</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setPropertyByteArray(java.lang.String,byte%5B%5D)">setPropertyByteArray</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;propertyName,
byte[]&nbsp;value)</code></th>
@ -414,7 +419,7 @@ implements <a href="../drm/ExoMediaDrm.html" title="interface in com.google.andr
<div class="block">Sets the value of a byte array property.</div>
</td>
</tr>
<tr id="i19" class="rowColor">
<tr id="i20" class="altColor">
<td class="colFirst"><code>void</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setPropertyString(java.lang.String,java.lang.String)">setPropertyString</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;propertyName,
<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;value)</code></th>
@ -422,6 +427,18 @@ implements <a href="../drm/ExoMediaDrm.html" title="interface in com.google.andr
<div class="block">Sets the value of a string property.</div>
</td>
</tr>
<tr id="i21" class="rowColor">
<td class="colFirst"><code>void</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#triggerEvent(com.google.common.base.Predicate,int,int,byte%5B%5D)">triggerEvent</a></span>&#8203;(<a href="https://guava.dev/releases/27.1-android/api/docs/com/google/common/base/Predicate.html?is-external=true" title="class or interface in com.google.common.base" class="externalLink">Predicate</a>&lt;byte[]&gt;&nbsp;sessionIdPredicate,
int&nbsp;event,
int&nbsp;extra,
byte[]&nbsp;data)</code></th>
<td class="colLast">
<div class="block">Calls <a href="../drm/ExoMediaDrm.OnEventListener.html#onEvent(com.google.android.exoplayer2.drm.ExoMediaDrm,byte%5B%5D,int,int,byte%5B%5D)"><code>ExoMediaDrm.OnEventListener.onEvent(ExoMediaDrm, byte[], int, int, byte[])</code></a> on the attached
listener (if present) once for each open session ID which passes <code>sessionIdPredicate</code>,
passing the provided values for <code>event</code>, <code>extra</code> and <code>data</code>.</div>
</td>
</tr>
</table>
<ul class="blockList">
<li class="blockList"><a id="methods.inherited.from.class.java.lang.Object">
@ -947,7 +964,7 @@ public&nbsp;<a href="https://developer.android.com/reference/android/os/Persista
<a id="getExoMediaCryptoType()">
<!-- -->
</a>
<ul class="blockListLast">
<ul class="blockList">
<li class="blockList">
<h4>getExoMediaCryptoType</h4>
<pre class="methodSignature">public&nbsp;<a href="https://developer.android.com/reference/java/lang/Class.html" title="class or interface in java.lang" class="externalLink" target="_top">Class</a>&lt;com.google.android.exoplayer2.testutil.FakeExoMediaDrm.FakeExoMediaCrypto&gt;&nbsp;getExoMediaCryptoType()</pre>
@ -959,6 +976,31 @@ public&nbsp;<a href="https://developer.android.com/reference/android/os/Persista
</dl>
</li>
</ul>
<a id="getReferenceCount()">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>getReferenceCount</h4>
<pre class="methodSignature">public&nbsp;int&nbsp;getReferenceCount()</pre>
</li>
</ul>
<a id="triggerEvent(com.google.common.base.Predicate,int,int,byte[])">
<!-- -->
</a>
<ul class="blockListLast">
<li class="blockList">
<h4>triggerEvent</h4>
<pre class="methodSignature">public&nbsp;void&nbsp;triggerEvent&#8203;(<a href="https://guava.dev/releases/27.1-android/api/docs/com/google/common/base/Predicate.html?is-external=true" title="class or interface in com.google.common.base" class="externalLink">Predicate</a>&lt;byte[]&gt;&nbsp;sessionIdPredicate,
int&nbsp;event,
int&nbsp;extra,
@Nullable
byte[]&nbsp;data)</pre>
<div class="block">Calls <a href="../drm/ExoMediaDrm.OnEventListener.html#onEvent(com.google.android.exoplayer2.drm.ExoMediaDrm,byte%5B%5D,int,int,byte%5B%5D)"><code>ExoMediaDrm.OnEventListener.onEvent(ExoMediaDrm, byte[], int, int, byte[])</code></a> on the attached
listener (if present) once for each open session ID which passes <code>sessionIdPredicate</code>,
passing the provided values for <code>event</code>, <code>extra</code> and <code>data</code>.</div>
</li>
</ul>
</li>
</ul>
</section>

View File

@ -122,9 +122,14 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<div class="description">
<ul 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>
<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.
<p>This is used in vertical text to write some characters in a horizontal orientation, known in

View File

@ -0,0 +1,191 @@
<!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>

View File

@ -122,9 +122,14 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<div class="description">
<ul 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>
<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.
<p>The text covered by this span is known as the "base text", and the ruby text is stored in

View File

@ -122,9 +122,14 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<div class="description">
<ul 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>
<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.
<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

View File

@ -97,6 +97,23 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<ul class="blockList">
<li class="blockList">
<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>
<tr>
<th class="colFirst" scope="col">Class</th>

View File

@ -103,16 +103,22 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<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>
<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="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="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> (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="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>
</li>
</ul>
</section>
<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>
<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>

View File

@ -25,7 +25,7 @@
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 altColor = "altColor";
var rowColor = "rowColor";
@ -209,8 +209,7 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<td class="colFirst"><code>void</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#bind()">bind</a></span>()</code></th>
<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
<a href="#setFloat(float)"><code>setFloat(float)</code></a>.</div>
<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>
</td>
</tr>
<tr id="i1" class="rowColor">
@ -222,6 +221,13 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
</tr>
<tr id="i2" class="altColor">
<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,
int&nbsp;unit)</code></th>
<td class="colLast">
@ -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>
</li>
</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>
@ -332,8 +348,7 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<li class="blockList">
<h4>bind</h4>
<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
<a href="#setFloat(float)"><code>setFloat(float)</code></a>.
<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>.
<p>Should be called before each drawing call.</div>
</li>

View File

@ -379,6 +379,16 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
</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_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>
<td class="colLast">&nbsp;</td>
</tr>
@ -1155,6 +1165,32 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
</dl>
</li>
</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>

View File

@ -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>
<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 class="rowColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.ExoPlayerLibraryInfo.VERSION_INT">
<!-- -->
</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>
<td class="colLast"><code>2014000</code></td>
<td class="colLast"><code>2014001</code></td>
</tr>
<tr class="altColor">
<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>
<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>
</tbody>
</table>
@ -1961,6 +1961,67 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
</li>
<li class="blockList">
<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>
<tr>
<th class="colFirst" scope="col">Modifier and Type</th>
@ -8902,6 +8963,20 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<td class="colLast"><code>"audio/mpeg-L2"</code></td>
</tr>
<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">
<!-- -->
</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>

View File

@ -1485,6 +1485,14 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<dd>
<div class="block">Optional artist.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.html#artworkData">artworkData</a></span> - Variable in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.html" title="class in com.google.android.exoplayer2">MediaMetadata</a></dt>
<dd>
<div class="block">Optional artwork data as a compressed byte array.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.html#artworkUri">artworkUri</a></span> - Variable in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.html" title="class in com.google.android.exoplayer2">MediaMetadata</a></dt>
<dd>
<div class="block">Optional artwork <a href="https://developer.android.com/reference/android/net/Uri.html" title="class or interface in android.net" class="externalLink" target="_top"><code>Uri</code></a>.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/C.html#ASCII_NAME">ASCII_NAME</a></span> - Static variable in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/C.html" title="class in com.google.android.exoplayer2">C</a></dt>
<dd>
<div class="deprecationBlock"><span class="deprecatedLabel">Deprecated.</span>
@ -1862,6 +1870,10 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<dd>&nbsp;</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/util/MimeTypes.html#AUDIO_MPEG_L2">AUDIO_MPEG_L2</a></span> - Static variable in class com.google.android.exoplayer2.util.<a href="com/google/android/exoplayer2/util/MimeTypes.html" title="class in com.google.android.exoplayer2.util">MimeTypes</a></dt>
<dd>&nbsp;</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/util/MimeTypes.html#AUDIO_MPEGH_MHA1">AUDIO_MPEGH_MHA1</a></span> - Static variable in class com.google.android.exoplayer2.util.<a href="com/google/android/exoplayer2/util/MimeTypes.html" title="class in com.google.android.exoplayer2.util">MimeTypes</a></dt>
<dd>&nbsp;</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/util/MimeTypes.html#AUDIO_MPEGH_MHM1">AUDIO_MPEGH_MHM1</a></span> - Static variable in class com.google.android.exoplayer2.util.<a href="com/google/android/exoplayer2/util/MimeTypes.html" title="class in com.google.android.exoplayer2.util">MimeTypes</a></dt>
<dd>&nbsp;</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/util/MimeTypes.html#AUDIO_MSGSM">AUDIO_MSGSM</a></span> - Static variable in class com.google.android.exoplayer2.util.<a href="com/google/android/exoplayer2/util/MimeTypes.html" title="class in com.google.android.exoplayer2.util">MimeTypes</a></dt>
<dd>&nbsp;</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/audio/AacUtil.html#AUDIO_OBJECT_TYPE_AAC_ELD">AUDIO_OBJECT_TYPE_AAC_ELD</a></span> - Static variable in class com.google.android.exoplayer2.audio.<a href="com/google/android/exoplayer2/audio/AacUtil.html" title="class in com.google.android.exoplayer2.audio">AacUtil</a></dt>
@ -2314,8 +2326,7 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/util/GlUtil.Uniform.html#bind()">bind()</a></span> - Method in class com.google.android.exoplayer2.util.<a href="com/google/android/exoplayer2/util/GlUtil.Uniform.html" title="class in com.google.android.exoplayer2.util">GlUtil.Uniform</a></dt>
<dd>
<div class="block">Sets the uniform to whatever value was passed via <a href="com/google/android/exoplayer2/util/GlUtil.Uniform.html#setSamplerTexId(int,int)"><code>GlUtil.Uniform.setSamplerTexId(int, int)</code></a> or
<a href="com/google/android/exoplayer2/util/GlUtil.Uniform.html#setFloat(float)"><code>GlUtil.Uniform.setFloat(float)</code></a>.</div>
<div class="block">Sets the uniform to whatever value was passed via <a href="com/google/android/exoplayer2/util/GlUtil.Uniform.html#setSamplerTexId(int,int)"><code>GlUtil.Uniform.setSamplerTexId(int, int)</code></a>, <a href="com/google/android/exoplayer2/util/GlUtil.Uniform.html#setFloat(float)"><code>GlUtil.Uniform.setFloat(float)</code></a> or <a href="com/google/android/exoplayer2/util/GlUtil.Uniform.html#setFloats(float%5B%5D)"><code>GlUtil.Uniform.setFloats(float[])</code></a>.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/text/Cue.html#bitmap">bitmap</a></span> - Variable in class com.google.android.exoplayer2.text.<a href="com/google/android/exoplayer2/text/Cue.html" title="class in com.google.android.exoplayer2.text">Cue</a></dt>
<dd>
@ -10721,6 +10732,10 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<div class="block">Reads from the given input using the given <a href="com/google/android/exoplayer2/extractor/Extractor.html" title="interface in com.google.android.exoplayer2.extractor"><code>Extractor</code></a>, until it can produce the <a href="com/google/android/exoplayer2/extractor/SeekMap.html" title="interface in com.google.android.exoplayer2.extractor"><code>SeekMap</code></a> and all of the track formats have been identified, or until the extractor encounters
EOF.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.html#extras">extras</a></span> - Variable in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.html" title="class in com.google.android.exoplayer2">MediaMetadata</a></dt>
<dd>
<div class="block">Optional extras <a href="https://developer.android.com/reference/android/os/Bundle.html" title="class or interface in android.os" class="externalLink" target="_top"><code>Bundle</code></a>.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.html#EXTRAS_SPEED">EXTRAS_SPEED</a></span> - Static variable in class com.google.android.exoplayer2.ext.mediasession.<a href="com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.html" title="class in com.google.android.exoplayer2.ext.mediasession">MediaSessionConnector</a></dt>
<dd>
<div class="block">The name of the <code>PlaybackStateCompat</code> float extra with the value of <code>
@ -11801,6 +11816,38 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<dd>
<div class="block">Moves UI focus to the skip button (or other interactive elements), if currently shown.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.html#FOLDER_TYPE_ALBUMS">FOLDER_TYPE_ALBUMS</a></span> - Static variable in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.html" title="class in com.google.android.exoplayer2">MediaMetadata</a></dt>
<dd>
<div class="block">Type for a folder containing media categorized by album.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.html#FOLDER_TYPE_ARTISTS">FOLDER_TYPE_ARTISTS</a></span> - Static variable in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.html" title="class in com.google.android.exoplayer2">MediaMetadata</a></dt>
<dd>
<div class="block">Type for a folder containing media categorized by artist.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.html#FOLDER_TYPE_GENRES">FOLDER_TYPE_GENRES</a></span> - Static variable in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.html" title="class in com.google.android.exoplayer2">MediaMetadata</a></dt>
<dd>
<div class="block">Type for a folder containing media categorized by genre.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.html#FOLDER_TYPE_MIXED">FOLDER_TYPE_MIXED</a></span> - Static variable in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.html" title="class in com.google.android.exoplayer2">MediaMetadata</a></dt>
<dd>
<div class="block">Type for a folder containing media of mixed types.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.html#FOLDER_TYPE_PLAYLISTS">FOLDER_TYPE_PLAYLISTS</a></span> - Static variable in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.html" title="class in com.google.android.exoplayer2">MediaMetadata</a></dt>
<dd>
<div class="block">Type for a folder containing a playlist.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.html#FOLDER_TYPE_TITLES">FOLDER_TYPE_TITLES</a></span> - Static variable in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.html" title="class in com.google.android.exoplayer2">MediaMetadata</a></dt>
<dd>
<div class="block">Type for a folder containing only playable media.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.html#FOLDER_TYPE_YEARS">FOLDER_TYPE_YEARS</a></span> - Static variable in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.html" title="class in com.google.android.exoplayer2">MediaMetadata</a></dt>
<dd>
<div class="block">Type for a folder containing media categorized by year.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.html#folderType">folderType</a></span> - Variable in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.html" title="class in com.google.android.exoplayer2">MediaMetadata</a></dt>
<dd>
<div class="block">Optional <a href="com/google/android/exoplayer2/MediaMetadata.FolderType.html" title="annotation in com.google.android.exoplayer2"><code>MediaMetadata.FolderType</code></a>.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.html#FONT_SIZE_UNIT_EM">FONT_SIZE_UNIT_EM</a></span> - Static variable in class com.google.android.exoplayer2.text.webvtt.<a href="com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.html" title="class in com.google.android.exoplayer2.text.webvtt">WebvttCssStyle</a></dt>
<dd>&nbsp;</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.html#FONT_SIZE_UNIT_PERCENT">FONT_SIZE_UNIT_PERCENT</a></span> - Static variable in class com.google.android.exoplayer2.text.webvtt.<a href="com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.html" title="class in com.google.android.exoplayer2.text.webvtt">WebvttCssStyle</a></dt>
@ -15408,6 +15455,8 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<div class="block">Returns the value stored under <a href="com/google/android/exoplayer2/upstream/cache/ContentMetadata.html#KEY_REDIRECTED_URI"><code>ContentMetadata.KEY_REDIRECTED_URI</code></a> as a <a href="https://developer.android.com/reference/android/net/Uri.html" title="class or interface in android.net" class="externalLink" target="_top"><code>Uri</code></a>, or {code null} if
not set.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/testutil/FakeExoMediaDrm.html#getReferenceCount()">getReferenceCount()</a></span> - Method in class com.google.android.exoplayer2.testutil.<a href="com/google/android/exoplayer2/testutil/FakeExoMediaDrm.html" title="class in com.google.android.exoplayer2.testutil">FakeExoMediaDrm</a></dt>
<dd>&nbsp;</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.html#getRegionEndTimeMs(long)">getRegionEndTimeMs(long)</a></span> - Method in class com.google.android.exoplayer2.upstream.cache.<a href="com/google/android/exoplayer2/upstream/cache/CachedRegionTracker.html" title="class in com.google.android.exoplayer2.upstream.cache">CachedRegionTracker</a></dt>
<dd>
<div class="block">When provided with a byte offset, this method locates the cached region within which the
@ -17493,7 +17542,7 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<dd>
<div class="block">Represents an HLS media playlist.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.html#%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(int, String, List&lt;String&gt;, long, long, boolean, int, long, int, long, long, boolean, boolean, boolean, DrmInitData, List&lt;HlsMediaPlaylist.Segment&gt;, List&lt;HlsMediaPlaylist.Part&gt;, HlsMediaPlaylist.ServerControl, Map&lt;Uri, HlsMediaPlaylist.RenditionReport&gt;)</a></span> - Constructor for class com.google.android.exoplayer2.source.hls.playlist.<a href="com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.html" title="class in com.google.android.exoplayer2.source.hls.playlist">HlsMediaPlaylist</a></dt>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.html#%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(int, String, List&lt;String&gt;, long, boolean, long, boolean, int, long, int, long, long, boolean, boolean, boolean, DrmInitData, List&lt;HlsMediaPlaylist.Segment&gt;, List&lt;HlsMediaPlaylist.Part&gt;, HlsMediaPlaylist.ServerControl, Map&lt;Uri, HlsMediaPlaylist.RenditionReport&gt;)</a></span> - Constructor for class com.google.android.exoplayer2.source.hls.playlist.<a href="com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.html" title="class in com.google.android.exoplayer2.source.hls.playlist">HlsMediaPlaylist</a></dt>
<dd>&nbsp;</dd>
<dt><a href="com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.Part.html" title="class in com.google.android.exoplayer2.source.hls.playlist"><span class="typeNameLink">HlsMediaPlaylist.Part</span></a> - Class in <a href="com/google/android/exoplayer2/source/hls/playlist/package-summary.html">com.google.android.exoplayer2.source.hls.playlist</a></dt>
<dd>
@ -18818,6 +18867,10 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<div class="block">Whether this window contains placeholder information because the real information has yet to
be loaded.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.html#isPlayable">isPlayable</a></span> - Variable in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.html" title="class in com.google.android.exoplayer2">MediaMetadata</a></dt>
<dd>
<div class="block">Optional boolean for media playability.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/BasePlayer.html#isPlaying()">isPlaying()</a></span> - Method in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/BasePlayer.html" title="class in com.google.android.exoplayer2">BasePlayer</a></dt>
<dd>&nbsp;</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.html#isPlaying()">isPlaying()</a></span> - Method in class com.google.android.exoplayer2.ext.leanback.<a href="com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.html" title="class in com.google.android.exoplayer2.ext.leanback">LeanbackPlayerAdapter</a></dt>
@ -19303,6 +19356,10 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<dd>
<div class="block">Represents an undetermined language as an ISO 639-2 language code.</div>
</dd>
<dt><a href="com/google/android/exoplayer2/text/span/LanguageFeatureSpan.html" title="interface in com.google.android.exoplayer2.text.span"><span class="typeNameLink">LanguageFeatureSpan</span></a> - Interface in <a href="com/google/android/exoplayer2/text/span/package-summary.html">com.google.android.exoplayer2.text.span</a></dt>
<dd>
<div class="block">Marker interface for span classes that carry language features rather than style information.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/testutil/FakeTrackOutput.html#lastFormat">lastFormat</a></span> - Variable in class com.google.android.exoplayer2.testutil.<a href="com/google/android/exoplayer2/testutil/FakeTrackOutput.html" title="class in com.google.android.exoplayer2.testutil">FakeTrackOutput</a></dt>
<dd>&nbsp;</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.RenditionReport.html#lastMediaSequence">lastMediaSequence</a></span> - Variable in class com.google.android.exoplayer2.source.hls.playlist.<a href="com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.RenditionReport.html" title="class in com.google.android.exoplayer2.source.hls.playlist">HlsMediaPlaylist.RenditionReport</a></dt>
@ -20481,6 +20538,10 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<dd>
<div class="block">A builder for <a href="com/google/android/exoplayer2/MediaMetadata.html" title="class in com.google.android.exoplayer2"><code>MediaMetadata</code></a> instances.</div>
</dd>
<dt><a href="com/google/android/exoplayer2/MediaMetadata.FolderType.html" title="annotation in com.google.android.exoplayer2"><span class="typeNameLink">MediaMetadata.FolderType</span></a> - Annotation Type in <a href="com/google/android/exoplayer2/package-summary.html">com.google.android.exoplayer2</a></dt>
<dd>
<div class="block">The folder type of the media item.</div>
</dd>
<dt><a href="com/google/android/exoplayer2/source/chunk/MediaParserChunkExtractor.html" title="class in com.google.android.exoplayer2.source.chunk"><span class="typeNameLink">MediaParserChunkExtractor</span></a> - Class in <a href="com/google/android/exoplayer2/source/chunk/package-summary.html">com.google.android.exoplayer2.source.chunk</a></dt>
<dd>
<div class="block"><a href="com/google/android/exoplayer2/source/chunk/ChunkExtractor.html" title="interface in com.google.android.exoplayer2.source.chunk"><code>ChunkExtractor</code></a> implemented on top of the platform's <a href="https://developer.android.com/reference/android/media/MediaParser.html" title="class or interface in android.media" class="externalLink" target="_top"><code>MediaParser</code></a>.</div>
@ -25318,6 +25379,8 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/metadata/icy/IcyInfo.html#populateMediaMetadata(com.google.android.exoplayer2.MediaMetadata.Builder)">populateMediaMetadata(MediaMetadata.Builder)</a></span> - Method in class com.google.android.exoplayer2.metadata.icy.<a href="com/google/android/exoplayer2/metadata/icy/IcyInfo.html" title="class in com.google.android.exoplayer2.metadata.icy">IcyInfo</a></dt>
<dd>&nbsp;</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/metadata/id3/ApicFrame.html#populateMediaMetadata(com.google.android.exoplayer2.MediaMetadata.Builder)">populateMediaMetadata(MediaMetadata.Builder)</a></span> - Method in class com.google.android.exoplayer2.metadata.id3.<a href="com/google/android/exoplayer2/metadata/id3/ApicFrame.html" title="class in com.google.android.exoplayer2.metadata.id3">ApicFrame</a></dt>
<dd>&nbsp;</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/metadata/id3/TextInformationFrame.html#populateMediaMetadata(com.google.android.exoplayer2.MediaMetadata.Builder)">populateMediaMetadata(MediaMetadata.Builder)</a></span> - Method in class com.google.android.exoplayer2.metadata.id3.<a href="com/google/android/exoplayer2/metadata/id3/TextInformationFrame.html" title="class in com.google.android.exoplayer2.metadata.id3">TextInformationFrame</a></dt>
<dd>&nbsp;</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/metadata/Metadata.Entry.html#populateMediaMetadata(com.google.android.exoplayer2.MediaMetadata.Builder)">populateMediaMetadata(MediaMetadata.Builder)</a></span> - Method in interface com.google.android.exoplayer2.metadata.<a href="com/google/android/exoplayer2/metadata/Metadata.Entry.html" title="interface in com.google.android.exoplayer2.metadata">Metadata.Entry</a></dt>
@ -25436,6 +25499,10 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<dd>
<div class="block">Pre-acquires a DRM session for the specified <a href="com/google/android/exoplayer2/Format.html" title="class in com.google.android.exoplayer2"><code>Format</code></a>.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.html#preciseStart">preciseStart</a></span> - Variable in class com.google.android.exoplayer2.source.hls.playlist.<a href="com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.html" title="class in com.google.android.exoplayer2.source.hls.playlist">HlsMediaPlaylist</a></dt>
<dd>
<div class="block">Whether the start position should be precise, as defined by #EXT-X-START.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/trackselection/TrackSelectionParameters.html#preferredAudioLanguages">preferredAudioLanguages</a></span> - Variable in class com.google.android.exoplayer2.trackselection.<a href="com/google/android/exoplayer2/trackselection/TrackSelectionParameters.html" title="class in com.google.android.exoplayer2.trackselection">TrackSelectionParameters</a></dt>
<dd>
<div class="block">The preferred languages for audio and forced text tracks as IETF BCP 47 conformant tags in
@ -29458,6 +29525,14 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<dd>
<div class="block">Sets the artist.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.Builder.html#setArtworkData(byte%5B%5D)">setArtworkData(byte[])</a></span> - Method in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></dt>
<dd>
<div class="block">Sets the artwork data as a compressed byte array.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.Builder.html#setArtworkUri(android.net.Uri)">setArtworkUri(Uri)</a></span> - Method in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></dt>
<dd>
<div class="block">Sets the artwork <a href="https://developer.android.com/reference/android/net/Uri.html" title="class or interface in android.net" class="externalLink" target="_top"><code>Uri</code></a>.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/ui/AspectRatioFrameLayout.html#setAspectRatio(float)">setAspectRatio(float)</a></span> - Method in class com.google.android.exoplayer2.ui.<a href="com/google/android/exoplayer2/ui/AspectRatioFrameLayout.html" title="class in com.google.android.exoplayer2.ui">AspectRatioFrameLayout</a></dt>
<dd>
<div class="block">Sets the aspect ratio that this view should satisfy.</div>
@ -30380,6 +30455,10 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
factory as unused.</div>
</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.Builder.html#setExtras(android.os.Bundle)">setExtras(Bundle)</a></span> - Method in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></dt>
<dd>
<div class="block">Sets the extras <a href="https://developer.android.com/reference/android/os/Bundle.html" title="class or interface in android.os" class="externalLink" target="_top"><code>Bundle</code></a>.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/testutil/DownloadBuilder.html#setFailureReason(int)">setFailureReason(int)</a></span> - Method in class com.google.android.exoplayer2.testutil.<a href="com/google/android/exoplayer2/testutil/DownloadBuilder.html" title="class in com.google.android.exoplayer2.testutil">DownloadBuilder</a></dt>
<dd>&nbsp;</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/testutil/FakeDataSource.Factory.html#setFakeDataSet(com.google.android.exoplayer2.testutil.FakeDataSet)">setFakeDataSet(FakeDataSet)</a></span> - Method in class com.google.android.exoplayer2.testutil.<a href="com/google/android/exoplayer2/testutil/FakeDataSource.Factory.html" title="class in com.google.android.exoplayer2.testutil">FakeDataSource.Factory</a></dt>
@ -30476,10 +30555,18 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<dd>
<div class="block">Configures <a href="com/google/android/exoplayer2/util/GlUtil.Uniform.html#bind()"><code>GlUtil.Uniform.bind()</code></a> to use the specified float <code>value</code> for this uniform.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/util/GlUtil.Uniform.html#setFloats(float%5B%5D)">setFloats(float[])</a></span> - Method in class com.google.android.exoplayer2.util.<a href="com/google/android/exoplayer2/util/GlUtil.Uniform.html" title="class in com.google.android.exoplayer2.util">GlUtil.Uniform</a></dt>
<dd>
<div class="block">Configures <a href="com/google/android/exoplayer2/util/GlUtil.Uniform.html#bind()"><code>GlUtil.Uniform.bind()</code></a> to use the specified float[] <code>value</code> for this uniform.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/ext/ima/ImaAdsLoader.Builder.html#setFocusSkipButtonWhenAvailable(boolean)">setFocusSkipButtonWhenAvailable(boolean)</a></span> - Method in class com.google.android.exoplayer2.ext.ima.<a href="com/google/android/exoplayer2/ext/ima/ImaAdsLoader.Builder.html" title="class in com.google.android.exoplayer2.ext.ima">ImaAdsLoader.Builder</a></dt>
<dd>
<div class="block">Sets whether to focus the skip button (when available) on Android TV devices.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.Builder.html#setFolderType(java.lang.Integer)">setFolderType(Integer)</a></span> - Method in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></dt>
<dd>
<div class="block">Sets the <a href="com/google/android/exoplayer2/MediaMetadata.FolderType.html" title="annotation in com.google.android.exoplayer2"><code>MediaMetadata.FolderType</code></a>.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.html#setFontColor(int)">setFontColor(int)</a></span> - Method in class com.google.android.exoplayer2.text.webvtt.<a href="com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.html" title="class in com.google.android.exoplayer2.text.webvtt">WebvttCssStyle</a></dt>
<dd>&nbsp;</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.html#setFontFamily(java.lang.String)">setFontFamily(String)</a></span> - Method in class com.google.android.exoplayer2.text.webvtt.<a href="com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.html" title="class in com.google.android.exoplayer2.text.webvtt">WebvttCssStyle</a></dt>
@ -30498,6 +30585,10 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<div class="block">Sets whether to force selection of the single lowest bitrate audio and video tracks that
comply with all other constraints.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/source/rtsp/RtspMediaSource.Factory.html#setForceUseRtpTcp(boolean)">setForceUseRtpTcp(boolean)</a></span> - Method in class com.google.android.exoplayer2.source.rtsp.<a href="com/google/android/exoplayer2/source/rtsp/RtspMediaSource.Factory.html" title="class in com.google.android.exoplayer2.source.rtsp">RtspMediaSource.Factory</a></dt>
<dd>
<div class="block">Sets whether to force using TCP as the default RTP transport.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/ExoPlayer.html#setForegroundMode(boolean)">setForegroundMode(boolean)</a></span> - Method in interface com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/ExoPlayer.html" title="interface in com.google.android.exoplayer2">ExoPlayer</a></dt>
<dd>
<div class="block">Sets whether the player is allowed to keep holding limited resources such as video decoders,
@ -30644,6 +30735,10 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/testutil/FakeDataSource.Factory.html#setIsNetwork(boolean)">setIsNetwork(boolean)</a></span> - Method in class com.google.android.exoplayer2.testutil.<a href="com/google/android/exoplayer2/testutil/FakeDataSource.Factory.html" title="class in com.google.android.exoplayer2.testutil">FakeDataSource.Factory</a></dt>
<dd>&nbsp;</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.Builder.html#setIsPlayable(java.lang.Boolean)">setIsPlayable(Boolean)</a></span> - Method in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></dt>
<dd>
<div class="block">Sets whether the media is playable.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.html#setItalic(boolean)">setItalic(boolean)</a></span> - Method in class com.google.android.exoplayer2.text.webvtt.<a href="com/google/android/exoplayer2/text/webvtt/WebvttCssStyle.html" title="class in com.google.android.exoplayer2.text.webvtt">WebvttCssStyle</a></dt>
<dd>&nbsp;</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/ui/PlayerView.html#setKeepContentOnPlayerReset(boolean)">setKeepContentOnPlayerReset(boolean)</a></span> - Method in class com.google.android.exoplayer2.ui.<a href="com/google/android/exoplayer2/ui/PlayerView.html" title="class in com.google.android.exoplayer2.ui">PlayerView</a></dt>
@ -32550,6 +32645,10 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<dd>
<div class="block">Sets the title.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.Builder.html#setTotalTrackCount(java.lang.Integer)">setTotalTrackCount(Integer)</a></span> - Method in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></dt>
<dd>
<div class="block">Sets the total number of tracks.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.html#setTrackFormatComparator(java.util.Comparator)">setTrackFormatComparator(Comparator&lt;Format&gt;)</a></span> - Method in class com.google.android.exoplayer2.ui.<a href="com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.html" title="class in com.google.android.exoplayer2.ui">TrackSelectionDialogBuilder</a></dt>
<dd>
<div class="block">Sets a <a href="https://developer.android.com/reference/java/util/Comparator.html" title="class or interface in java.util" class="externalLink" target="_top"><code>Comparator</code></a> used to determine the display order of the tracks within each track
@ -32569,6 +32668,10 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<div class="block">Sets the <a href="com/google/android/exoplayer2/ui/TrackNameProvider.html" title="interface in com.google.android.exoplayer2.ui"><code>TrackNameProvider</code></a> used to generate the user visible name of each track and
updates the view with track names queried from the specified provider.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.Builder.html#setTrackNumber(java.lang.Integer)">setTrackNumber(Integer)</a></span> - Method in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></dt>
<dd>
<div class="block">Sets the track number.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.Builder.html#setTrackSelector(com.google.android.exoplayer2.trackselection.DefaultTrackSelector)">setTrackSelector(DefaultTrackSelector)</a></span> - Method in class com.google.android.exoplayer2.testutil.<a href="com/google/android/exoplayer2/testutil/ExoPlayerTestRunner.Builder.html" title="class in com.google.android.exoplayer2.testutil">ExoPlayerTestRunner.Builder</a></dt>
<dd>&nbsp;</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/testutil/TestExoPlayerBuilder.html#setTrackSelector(com.google.android.exoplayer2.trackselection.DefaultTrackSelector)">setTrackSelector(DefaultTrackSelector)</a></span> - Method in class com.google.android.exoplayer2.testutil.<a href="com/google/android/exoplayer2/testutil/TestExoPlayerBuilder.html" title="class in com.google.android.exoplayer2.testutil">TestExoPlayerBuilder</a></dt>
@ -32768,6 +32871,10 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<dd>
<div class="block">Sets the user agent that will be used.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/source/rtsp/RtspMediaSource.Factory.html#setUserAgent(java.lang.String)">setUserAgent(String)</a></span> - Method in class com.google.android.exoplayer2.source.rtsp.<a href="com/google/android/exoplayer2/source/rtsp/RtspMediaSource.Factory.html" title="class in com.google.android.exoplayer2.source.rtsp">RtspMediaSource.Factory</a></dt>
<dd>
<div class="block">Sets the user agent, the default value is <a href="com/google/android/exoplayer2/ExoPlayerLibraryInfo.html#VERSION_SLASHY"><code>ExoPlayerLibraryInfo.VERSION_SLASHY</code></a>.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/upstream/DefaultHttpDataSource.Factory.html#setUserAgent(java.lang.String)">setUserAgent(String)</a></span> - Method in class com.google.android.exoplayer2.upstream.<a href="com/google/android/exoplayer2/upstream/DefaultHttpDataSource.Factory.html" title="class in com.google.android.exoplayer2.upstream">DefaultHttpDataSource.Factory</a></dt>
<dd>
<div class="block">Sets the user agent that will be used.</div>
@ -32988,6 +33095,10 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<dd>
<div class="block">Sets the fill color of the window.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.Builder.html#setYear(java.lang.Integer)">setYear(Integer)</a></span> - Method in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a></dt>
<dd>
<div class="block">Sets the year.</div>
</dd>
<dt><a href="com/google/android/exoplayer2/robolectric/ShadowMediaCodecConfig.html" title="class in com.google.android.exoplayer2.robolectric"><span class="typeNameLink">ShadowMediaCodecConfig</span></a> - Class in <a href="com/google/android/exoplayer2/robolectric/package-summary.html">com.google.android.exoplayer2.robolectric</a></dt>
<dd>
<div class="block">A JUnit @Rule to configure Roboelectric's <code>ShadowMediaCodec</code>.</div>
@ -33981,7 +34092,8 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.html#startOffsetUs">startOffsetUs</a></span> - Variable in class com.google.android.exoplayer2.source.hls.playlist.<a href="com/google/android/exoplayer2/source/hls/playlist/HlsMediaPlaylist.html" title="class in com.google.android.exoplayer2.source.hls.playlist">HlsMediaPlaylist</a></dt>
<dd>
<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="com/google/android/exoplayer2/C.html#TIME_UNSET"><code>C.TIME_UNSET</code></a> if undefined.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaItem.ClippingProperties.html#startPositionMs">startPositionMs</a></span> - Variable in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaItem.ClippingProperties.html" title="class in com.google.android.exoplayer2">MediaItem.ClippingProperties</a></dt>
<dd>
@ -35322,6 +35434,10 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<dd>
<div class="block">The total number of times a seek occurred.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.html#totalTrackCount">totalTrackCount</a></span> - Variable in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.html" title="class in com.google.android.exoplayer2">MediaMetadata</a></dt>
<dd>
<div class="block">Optional total number of tracks.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/analytics/PlaybackStats.html#totalValidJoinTimeMs">totalValidJoinTimeMs</a></span> - Variable in class com.google.android.exoplayer2.analytics.<a href="com/google/android/exoplayer2/analytics/PlaybackStats.html" title="class in com.google.android.exoplayer2.analytics">PlaybackStats</a></dt>
<dd>
<div class="block">The total time spent joining the playback, in milliseconds, or <a href="com/google/android/exoplayer2/C.html#TIME_UNSET"><code>C.TIME_UNSET</code></a> if no valid
@ -35472,6 +35588,10 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<dd>
<div class="block">Converts <a href="com/google/android/exoplayer2/Format.html" title="class in com.google.android.exoplayer2"><code>Format</code></a>s to user readable track names.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.html#trackNumber">trackNumber</a></span> - Variable in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.html" title="class in com.google.android.exoplayer2">MediaMetadata</a></dt>
<dd>
<div class="block">Optional track number.</div>
</dd>
<dt><a href="com/google/android/exoplayer2/extractor/TrackOutput.html" title="interface in com.google.android.exoplayer2.extractor"><span class="typeNameLink">TrackOutput</span></a> - Interface in <a href="com/google/android/exoplayer2/extractor/package-summary.html">com.google.android.exoplayer2.extractor</a></dt>
<dd>
<div class="block">Receives track level data extracted by an <a href="com/google/android/exoplayer2/extractor/Extractor.html" title="interface in com.google.android.exoplayer2.extractor"><code>Extractor</code></a>.</div>
@ -35646,6 +35766,12 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/extractor/VorbisUtil.Mode.html#transformType">transformType</a></span> - Variable in class com.google.android.exoplayer2.extractor.<a href="com/google/android/exoplayer2/extractor/VorbisUtil.Mode.html" title="class in com.google.android.exoplayer2.extractor">VorbisUtil.Mode</a></dt>
<dd>&nbsp;</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/testutil/FakeExoMediaDrm.html#triggerEvent(com.google.common.base.Predicate,int,int,byte%5B%5D)">triggerEvent(Predicate&lt;byte[]&gt;, int, int, byte[])</a></span> - Method in class com.google.android.exoplayer2.testutil.<a href="com/google/android/exoplayer2/testutil/FakeExoMediaDrm.html" title="class in com.google.android.exoplayer2.testutil">FakeExoMediaDrm</a></dt>
<dd>
<div class="block">Calls <a href="com/google/android/exoplayer2/drm/ExoMediaDrm.OnEventListener.html#onEvent(com.google.android.exoplayer2.drm.ExoMediaDrm,byte%5B%5D,int,int,byte%5B%5D)"><code>ExoMediaDrm.OnEventListener.onEvent(ExoMediaDrm, byte[], int, int, byte[])</code></a> on the attached
listener (if present) once for each open session ID which passes <code>sessionIdPredicate</code>,
passing the provided values for <code>event</code>, <code>extra</code> and <code>data</code>.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/upstream/Allocator.html#trim()">trim()</a></span> - Method in interface com.google.android.exoplayer2.upstream.<a href="com/google/android/exoplayer2/upstream/Allocator.html" title="interface in com.google.android.exoplayer2.upstream">Allocator</a></dt>
<dd>
<div class="block">Hints to the allocator that it should make a best effort to release any excess
@ -37381,6 +37507,10 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
</a>
<h2 class="title">Y</h2>
<dl>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/MediaMetadata.html#year">year</a></span> - Variable in class com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.html" title="class in com.google.android.exoplayer2">MediaMetadata</a></dt>
<dd>
<div class="block">Optional year.</div>
</dd>
<dt><span class="memberNameLink"><a href="com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.html#yuvPlanes">yuvPlanes</a></span> - Variable in class com.google.android.exoplayer2.video.<a href="com/google/android/exoplayer2/video/VideoDecoderOutputBuffer.html" title="class in com.google.android.exoplayer2.video">VideoDecoderOutputBuffer</a></dt>
<dd>
<div class="block">YUV planes for YUV mode.</div>

File diff suppressed because one or more lines are too long

View File

@ -699,7 +699,7 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<li class="circle">com.google.android.exoplayer2.source.hls.playlist.<a href="com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.html" title="class in com.google.android.exoplayer2.source.hls.playlist"><span class="typeNameLink">HlsPlaylistParser</span></a> (implements com.google.android.exoplayer2.upstream.<a href="com/google/android/exoplayer2/upstream/ParsingLoadable.Parser.html" title="interface in com.google.android.exoplayer2.upstream">ParsingLoadable.Parser</a>&lt;T&gt;)</li>
<li class="circle">com.google.android.exoplayer2.source.hls.<a href="com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.html" title="class in com.google.android.exoplayer2.source.hls"><span class="typeNameLink">HlsTrackMetadataEntry</span></a> (implements com.google.android.exoplayer2.metadata.<a href="com/google/android/exoplayer2/metadata/Metadata.Entry.html" title="interface in com.google.android.exoplayer2.metadata">Metadata.Entry</a>)</li>
<li class="circle">com.google.android.exoplayer2.source.hls.<a href="com/google/android/exoplayer2/source/hls/HlsTrackMetadataEntry.VariantInfo.html" title="class in com.google.android.exoplayer2.source.hls"><span class="typeNameLink">HlsTrackMetadataEntry.VariantInfo</span></a> (implements android.os.<a href="https://developer.android.com/reference/android/os/Parcelable.html" title="class or interface in android.os" class="externalLink" target="_top">Parcelable</a>)</li>
<li class="circle">com.google.android.exoplayer2.text.span.<a href="com/google/android/exoplayer2/text/span/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="com/google/android/exoplayer2/text/span/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="com/google/android/exoplayer2/text/span/LanguageFeatureSpan.html" title="interface in com.google.android.exoplayer2.text.span">LanguageFeatureSpan</a>)</li>
<li class="circle">com.google.android.exoplayer2.upstream.<a href="com/google/android/exoplayer2/upstream/HttpDataSource.BaseFactory.html" title="class in com.google.android.exoplayer2.upstream"><span class="typeNameLink">HttpDataSource.BaseFactory</span></a> (implements com.google.android.exoplayer2.upstream.<a href="com/google/android/exoplayer2/upstream/HttpDataSource.Factory.html" title="interface in com.google.android.exoplayer2.upstream">HttpDataSource.Factory</a>)
<ul>
<li class="circle">com.google.android.exoplayer2.ext.cronet.<a href="com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.html" title="class in com.google.android.exoplayer2.ext.cronet"><span class="typeNameLink">CronetDataSourceFactory</span></a></li>
@ -904,7 +904,7 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<li class="circle">com.google.android.exoplayer2.source.rtsp.<a href="com/google/android/exoplayer2/source/rtsp/RtpPayloadFormat.html" title="class in com.google.android.exoplayer2.source.rtsp"><span class="typeNameLink">RtpPayloadFormat</span></a></li>
<li class="circle">com.google.android.exoplayer2.source.rtsp.<a href="com/google/android/exoplayer2/source/rtsp/RtpUtils.html" title="class in com.google.android.exoplayer2.source.rtsp"><span class="typeNameLink">RtpUtils</span></a></li>
<li class="circle">com.google.android.exoplayer2.source.rtsp.<a href="com/google/android/exoplayer2/source/rtsp/RtspMediaSource.Factory.html" title="class in com.google.android.exoplayer2.source.rtsp"><span class="typeNameLink">RtspMediaSource.Factory</span></a> (implements com.google.android.exoplayer2.source.<a href="com/google/android/exoplayer2/source/MediaSourceFactory.html" title="interface in com.google.android.exoplayer2.source">MediaSourceFactory</a>)</li>
<li class="circle">com.google.android.exoplayer2.text.span.<a href="com/google/android/exoplayer2/text/span/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="com/google/android/exoplayer2/text/span/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="com/google/android/exoplayer2/text/span/LanguageFeatureSpan.html" title="interface in com.google.android.exoplayer2.text.span">LanguageFeatureSpan</a>)</li>
<li class="circle">com.google.android.exoplayer2.util.<a href="com/google/android/exoplayer2/util/RunnableFutureTask.html" title="class in com.google.android.exoplayer2.util"><span class="typeNameLink">RunnableFutureTask</span></a>&lt;R,&#8203;E&gt; (implements java.util.concurrent.<a href="https://developer.android.com/reference/java/util/concurrent/RunnableFuture.html" title="class or interface in java.util.concurrent" class="externalLink" target="_top">RunnableFuture</a>&lt;V&gt;)</li>
<li class="circle">com.google.android.exoplayer2.source.<a href="com/google/android/exoplayer2/source/SampleQueue.html" title="class in com.google.android.exoplayer2.source"><span class="typeNameLink">SampleQueue</span></a> (implements com.google.android.exoplayer2.extractor.<a href="com/google/android/exoplayer2/extractor/TrackOutput.html" title="interface in com.google.android.exoplayer2.extractor">TrackOutput</a>)</li>
<li class="circle">com.google.android.exoplayer2.extractor.ts.<a href="com/google/android/exoplayer2/extractor/ts/SectionReader.html" title="class in com.google.android.exoplayer2.extractor.ts"><span class="typeNameLink">SectionReader</span></a> (implements com.google.android.exoplayer2.extractor.ts.<a href="com/google/android/exoplayer2/extractor/ts/TsPayloadReader.html" title="interface in com.google.android.exoplayer2.extractor.ts">TsPayloadReader</a>)</li>
@ -1030,7 +1030,7 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<li class="circle">com.google.android.exoplayer2.robolectric.<a href="com/google/android/exoplayer2/robolectric/TestPlayerRunHelper.html" title="class in com.google.android.exoplayer2.robolectric"><span class="typeNameLink">TestPlayerRunHelper</span></a></li>
<li class="circle">com.google.android.exoplayer2.testutil.<a href="com/google/android/exoplayer2/testutil/TestUtil.html" title="class in com.google.android.exoplayer2.testutil"><span class="typeNameLink">TestUtil</span></a></li>
<li class="circle">com.google.android.exoplayer2.text.span.<a href="com/google/android/exoplayer2/text/span/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="com/google/android/exoplayer2/text/span/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="com/google/android/exoplayer2/text/span/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="com/google/android/exoplayer2/text/span/LanguageFeatureSpan.html" title="interface in com.google.android.exoplayer2.text.span">LanguageFeatureSpan</a>)</li>
<li class="circle">java.lang.<a href="https://developer.android.com/reference/java/lang/Throwable.html" title="class or interface in java.lang" class="externalLink"><span class="typeNameLink">Throwable</span></a> (implements java.io.<a href="https://developer.android.com/reference/java/io/Serializable.html?is-external=true" title="class or interface in java.io" class="externalLink" target="_top">Serializable</a>)
<ul>
<li class="circle">java.lang.<a href="https://developer.android.com/reference/java/lang/Exception.html" title="class or interface in java.lang" class="externalLink"><span class="typeNameLink" target="_top">Exception</span></a>
@ -1404,6 +1404,7 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<li class="circle">com.google.android.exoplayer2.source.hls.playlist.<a href="com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistTracker.PrimaryPlaylistListener.html" title="interface in com.google.android.exoplayer2.source.hls.playlist"><span class="typeNameLink">HlsPlaylistTracker.PrimaryPlaylistListener</span></a></li>
<li class="circle">com.google.android.exoplayer2.testutil.<a href="com/google/android/exoplayer2/testutil/HostActivity.HostedTest.html" title="interface in com.google.android.exoplayer2.testutil"><span class="typeNameLink">HostActivity.HostedTest</span></a></li>
<li class="circle">com.google.android.exoplayer2.metadata.id3.<a href="com/google/android/exoplayer2/metadata/id3/Id3Decoder.FramePredicate.html" title="interface in com.google.android.exoplayer2.metadata.id3"><span class="typeNameLink">Id3Decoder.FramePredicate</span></a></li>
<li class="circle">com.google.android.exoplayer2.text.span.<a href="com/google/android/exoplayer2/text/span/LanguageFeatureSpan.html" title="interface in com.google.android.exoplayer2.text.span"><span class="typeNameLink">LanguageFeatureSpan</span></a></li>
<li class="circle">com.google.android.exoplayer2.util.<a href="com/google/android/exoplayer2/util/ListenerSet.Event.html" title="interface in com.google.android.exoplayer2.util"><span class="typeNameLink">ListenerSet.Event</span></a>&lt;T&gt;</li>
<li class="circle">com.google.android.exoplayer2.util.<a href="com/google/android/exoplayer2/util/ListenerSet.IterationFinishedEvent.html" title="interface in com.google.android.exoplayer2.util"><span class="typeNameLink">ListenerSet.IterationFinishedEvent</span></a>&lt;T&gt;</li>
<li class="circle">com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/LivePlaybackSpeedControl.html" title="interface in com.google.android.exoplayer2"><span class="typeNameLink">LivePlaybackSpeedControl</span></a></li>
@ -1639,6 +1640,7 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<li class="circle">com.google.android.exoplayer2.source.hls.<a href="com/google/android/exoplayer2/source/hls/HlsMediaSource.MetadataType.html" title="annotation in com.google.android.exoplayer2.source.hls"><span class="typeNameLink">HlsMediaSource.MetadataType</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.upstream.<a href="com/google/android/exoplayer2/upstream/HttpDataSource.HttpDataSourceException.Type.html" title="annotation in com.google.android.exoplayer2.upstream"><span class="typeNameLink">HttpDataSource.HttpDataSourceException.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.extractor.mkv.<a href="com/google/android/exoplayer2/extractor/mkv/MatroskaExtractor.Flags.html" title="annotation in com.google.android.exoplayer2.extractor.mkv"><span class="typeNameLink">MatroskaExtractor.Flags</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="com/google/android/exoplayer2/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.ext.mediasession.<a href="com/google/android/exoplayer2/ext/mediasession/MediaSessionConnector.PlaybackActions.html" title="annotation in com.google.android.exoplayer2.ext.mediasession"><span class="typeNameLink">MediaSessionConnector.PlaybackActions</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.source.<a href="com/google/android/exoplayer2/source/MergingMediaSource.IllegalMergeException.Reason.html" title="annotation in com.google.android.exoplayer2.source"><span class="typeNameLink">MergingMediaSource.IllegalMergeException.Reason</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.extractor.mp3.<a href="com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.Flags.html" title="annotation in com.google.android.exoplayer2.extractor.mp3"><span class="typeNameLink">Mp3Extractor.Flags</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>

File diff suppressed because one or more lines are too long

View File

@ -24,24 +24,11 @@ These steps are described in more detail below. For a complete example, refer to
## 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 ###
Next add a dependency in the `build.gradle` file of your app module. The
following will add a dependency to the full ExoPlayer library:
The easiest way to get started using ExoPlayer is to add it as a gradle
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'
@ -75,9 +62,13 @@ modules individually.
* `exoplayer-transformer`: Media transformation functionality.
* `exoplayer-ui`: UI components and resources for use with ExoPlayer.
In addition to library modules, ExoPlayer has multiple extension modules that
depend on external libraries to provide additional functionality. Browse the
[extensions directory][] and their individual READMEs for details.
In addition to library modules, ExoPlayer has extension modules that depend on
external libraries to provide additional functionality. Some extensions are
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 ###
@ -239,4 +230,4 @@ can be done by calling `ExoPlayer.release`.
[Playlists page]: {{ site.baseurl }}/playlists.html
[Media items page]: {{ site.baseurl }}/media-items.html
[Media sources page]: {{ site.baseurl }}/media-sources.html
[Google Maven ExoPlayer page]: https://maven.google.com/web/index.html#com.google.android.exoplayer

View File

@ -32,8 +32,7 @@ dependencies {
// Instrumentation tests assume that an app-packaged version of cronet is
// available.
androidTestImplementation 'org.chromium.net:cronet-embedded:72.3626.96'
androidTestImplementation(project(modulePrefix + 'testutils'))
testImplementation project(modulePrefix + 'library')
androidTestImplementation project(modulePrefix + 'testutils')
testImplementation project(modulePrefix + 'testutils')
testImplementation 'com.squareup.okhttp3:mockwebserver:' + mockWebServerVersion
testImplementation 'org.robolectric:robolectric:' + robolectricVersion

View File

@ -34,6 +34,7 @@ class CombinedJavadocPlugin implements Plugin<Project> {
"https://guava.dev/releases/$project.ext.guavaVersion/api/docs"
encoding = "UTF-8"
}
options.addBooleanOption "-no-module-directories", true
exclude "**/BuildConfig.java"
exclude "**/R.java"
doFirst {

View File

@ -31,6 +31,7 @@ android.libraryVariants.all { variant ->
"https://guava.dev/releases/$project.ext.guavaVersion/api/docs"
encoding = "UTF-8"
}
options.addBooleanOption "-no-module-directories", true
exclude "**/BuildConfig.java"
exclude "**/R.java"
doFirst {

View File

@ -28,11 +28,11 @@ public final class ExoPlayerLibraryInfo {
/** 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.
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}. */
// 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.
@ -42,7 +42,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006).
*/
// 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.

View File

@ -159,9 +159,9 @@ public final class MediaItem implements Bundleable {
/**
* Sets the optional URI.
*
* <p>If {@code uri} is null or unset no {@link PlaybackProperties} object is created during
* {@link #build()} and any other {@code Builder} methods that would populate {@link
* MediaItem#playbackProperties} are ignored.
* <p>If {@code uri} is null or unset then no {@link PlaybackProperties} object is created
* during {@link #build()} and no other {@code Builder} methods that would populate {@link
* MediaItem#playbackProperties} should be called.
*/
public Builder setUri(@Nullable String uri) {
return setUri(uri == null ? null : Uri.parse(uri));
@ -170,9 +170,9 @@ public final class MediaItem implements Bundleable {
/**
* Sets the optional URI.
*
* <p>If {@code uri} is null or unset no {@link PlaybackProperties} object is created during
* {@link #build()} and any other {@code Builder} methods that would populate {@link
* MediaItem#playbackProperties} are ignored.
* <p>If {@code uri} is null or unset then no {@link PlaybackProperties} object is created
* during {@link #build()} and no other {@code Builder} methods that would populate {@link
* MediaItem#playbackProperties} should be called.
*/
public Builder setUri(@Nullable Uri uri) {
this.uri = uri;
@ -184,8 +184,7 @@ public final class MediaItem implements Bundleable {
*
* <p>The MIME type may be used as a hint for inferring the type of the media item.
*
* <p>If {@link #setUri} is passed a non-null {@code uri}, the MIME type is used to create a
* {@link PlaybackProperties} object. Otherwise it will be ignored.
* <p>This method should only be called if {@link #setUri} is passed a non-null value.
*
* @param mimeType The MIME type.
*/
@ -247,8 +246,8 @@ public final class MediaItem implements Bundleable {
* Sets the optional default DRM license server URI. If this URI is set, the {@link
* DrmConfiguration#uuid} needs to be specified as well.
*
* <p>If {@link #setUri} is passed a non-null {@code uri}, the DRM license server URI is used to
* create a {@link PlaybackProperties} object. Otherwise it will be ignored.
* <p>This method should only be called if both {@link #setUri} and {@link #setDrmUuid(UUID)}
* are passed non-null values.
*/
public Builder setDrmLicenseUri(@Nullable Uri licenseUri) {
drmLicenseUri = licenseUri;
@ -259,8 +258,8 @@ public final class MediaItem implements Bundleable {
* Sets the optional default DRM license server URI. If this URI is set, the {@link
* DrmConfiguration#uuid} needs to be specified as well.
*
* <p>If {@link #setUri} is passed a non-null {@code uri}, the DRM license server URI is used to
* create a {@link PlaybackProperties} object. Otherwise it will be ignored.
* <p>This method should only be called if both {@link #setUri} and {@link #setDrmUuid(UUID)}
* are passed non-null values.
*/
public Builder setDrmLicenseUri(@Nullable String licenseUri) {
drmLicenseUri = licenseUri == null ? null : Uri.parse(licenseUri);
@ -272,7 +271,8 @@ public final class MediaItem implements Bundleable {
*
* <p>{@code null} or an empty {@link Map} can be used for a reset.
*
* <p>If no valid DRM configuration is specified, the DRM license request headers are ignored.
* <p>This method should only be called if both {@link #setUri} and {@link #setDrmUuid(UUID)}
* are passed non-null values.
*/
public Builder setDrmLicenseRequestHeaders(
@Nullable Map<String, String> licenseRequestHeaders) {
@ -284,11 +284,13 @@ public final class MediaItem implements Bundleable {
}
/**
* Sets the {@link UUID} of the protection scheme. If a DRM system UUID is set, the {@link
* DrmConfiguration#licenseUri} needs to be set as well.
* Sets the {@link UUID} of the protection scheme.
*
* <p>If {@link #setUri} is passed a non-null {@code uri}, the DRM system UUID is used to create
* a {@link PlaybackProperties} object. Otherwise it will be ignored.
* <p>If {@code uuid} is null or unset then no {@link DrmConfiguration} object is created during
* {@link #build()} and no other {@code Builder} methods that would populate {@link
* MediaItem.PlaybackProperties#drmConfiguration} should be called.
*
* <p>This method should only be called if {@link #setUri} is passed a non-null value.
*/
public Builder setDrmUuid(@Nullable UUID uuid) {
drmUuid = uuid;
@ -298,8 +300,8 @@ public final class MediaItem implements Bundleable {
/**
* Sets whether the DRM configuration is multi session enabled.
*
* <p>If {@link #setUri} is passed a non-null {@code uri}, the DRM multi session flag is used to
* create a {@link PlaybackProperties} object. Otherwise it will be ignored.
* <p>This method should only be called if both {@link #setUri} and {@link #setDrmUuid(UUID)}
* are passed non-null values.
*/
public Builder setDrmMultiSession(boolean multiSession) {
drmMultiSession = multiSession;
@ -310,8 +312,8 @@ public final class MediaItem implements Bundleable {
* Sets whether to force use the default DRM license server URI even if the media specifies its
* own DRM license server URI.
*
* <p>If {@link #setUri} is passed a non-null {@code uri}, the DRM force default license flag is
* used to create a {@link PlaybackProperties} object. Otherwise it will be ignored.
* <p>This method should only be called if both {@link #setUri} and {@link #setDrmUuid(UUID)}
* are passed non-null values.
*/
public Builder setDrmForceDefaultLicenseUri(boolean forceDefaultLicenseUri) {
this.drmForceDefaultLicenseUri = forceDefaultLicenseUri;
@ -321,6 +323,9 @@ public final class MediaItem implements Bundleable {
/**
* Sets whether clear samples within protected content should be played when keys for the
* encrypted part of the content have yet to be loaded.
*
* <p>This method should only be called if both {@link #setUri} and {@link #setDrmUuid(UUID)}
* are passed non-null values.
*/
public Builder setDrmPlayClearContentWithoutKey(boolean playClearContentWithoutKey) {
this.drmPlayClearContentWithoutKey = playClearContentWithoutKey;
@ -333,6 +338,9 @@ public final class MediaItem implements Bundleable {
*
* <p>This method overrides what has been set by previously calling {@link
* #setDrmSessionForClearTypes(List)}.
*
* <p>This method should only be called if both {@link #setUri} and {@link #setDrmUuid(UUID)}
* are passed non-null values.
*/
public Builder setDrmSessionForClearPeriods(boolean sessionForClearPeriods) {
this.setDrmSessionForClearTypes(
@ -353,6 +361,9 @@ public final class MediaItem implements Bundleable {
* #setDrmSessionForClearPeriods(boolean)}.
*
* <p>{@code null} or an empty {@link List} can be used for a reset.
*
* <p>This method should only be called if both {@link #setUri} and {@link #setDrmUuid(UUID)}
* are passed non-null values.
*/
public Builder setDrmSessionForClearTypes(@Nullable List<Integer> sessionForClearTypes) {
this.drmSessionForClearTypes =
@ -369,7 +380,8 @@ public final class MediaItem implements Bundleable {
* release an existing offline license (see {@code DefaultDrmSessionManager#setMode(int
* mode,byte[] offlineLicenseKeySetId)}).
*
* <p>If no valid DRM configuration is specified, the key set ID is ignored.
* <p>This method should only be called if both {@link #setUri} and {@link #setDrmUuid(UUID)}
* are passed non-null values.
*/
public Builder setDrmKeySetId(@Nullable byte[] keySetId) {
this.drmKeySetId = keySetId != null ? Arrays.copyOf(keySetId, keySetId.length) : null;
@ -396,8 +408,7 @@ public final class MediaItem implements Bundleable {
/**
* Sets the optional custom cache key (only used for progressive streams).
*
* <p>If {@link #setUri} is passed a non-null {@code uri}, the custom cache key is used to
* create a {@link PlaybackProperties} object. Otherwise it will be ignored.
* <p>This method should only be called if {@link #setUri} is passed a non-null value.
*/
public Builder setCustomCacheKey(@Nullable String customCacheKey) {
this.customCacheKey = customCacheKey;
@ -409,8 +420,7 @@ public final class MediaItem implements Bundleable {
*
* <p>{@code null} or an empty {@link List} can be used for a reset.
*
* <p>If {@link #setUri} is passed a non-null {@code uri}, the subtitles are used to create a
* {@link PlaybackProperties} object. Otherwise they will be ignored.
* <p>This method should only be called if {@link #setUri} is passed a non-null value.
*/
public Builder setSubtitles(@Nullable List<Subtitle> subtitles) {
this.subtitles =
@ -423,13 +433,12 @@ public final class MediaItem implements Bundleable {
/**
* Sets the optional ad tag {@link Uri}.
*
* <p>If {@link #setUri} is passed a non-null {@code uri}, the ad tag URI is used to create a
* {@link PlaybackProperties} object. Otherwise it will be ignored.
*
* <p>Media items in the playlist with the same ad tag URI, media ID and ads loader will share
* the same ad playback state. To resume ad playback when recreating the playlist on returning
* from the background, pass media items with the same ad tag URIs and media IDs to the player.
*
* <p>This method should only be called if {@link #setUri} is passed a non-null value.
*
* @param adTagUri The ad tag URI to load.
*/
public Builder setAdTagUri(@Nullable String adTagUri) {
@ -439,13 +448,12 @@ public final class MediaItem implements Bundleable {
/**
* Sets the optional ad tag {@link Uri}.
*
* <p>If {@link #setUri} is passed a non-null {@code uri}, the ad tag URI is used to create a
* {@link PlaybackProperties} object. Otherwise it will be ignored.
*
* <p>Media items in the playlist with the same ad tag URI, media ID and ads loader will share
* the same ad playback state. To resume ad playback when recreating the playlist on returning
* from the background, pass media items with the same ad tag URIs and media IDs to the player.
*
* <p>This method should only be called if {@link #setUri} is passed a non-null value.
*
* @param adTagUri The ad tag URI to load.
*/
public Builder setAdTagUri(@Nullable Uri adTagUri) {
@ -455,13 +463,12 @@ public final class MediaItem implements Bundleable {
/**
* Sets the optional ad tag {@link Uri} and ads identifier.
*
* <p>If {@link #setUri} is passed a non-null {@code uri}, the ad tag URI is used to create a
* {@link PlaybackProperties} object. Otherwise it will be ignored.
*
* <p>Media items in the playlist that have the same ads identifier and ads loader share the
* same ad playback state. To resume ad playback when recreating the playlist on returning from
* the background, pass the same ads IDs to the player.
*
* <p>This method should only be called if {@link #setUri} is passed a non-null value.
*
* @param adTagUri The ad tag URI to load.
* @param adsId An opaque identifier for ad playback state associated with this item. Ad loading
* and playback state is shared among all media items that have the same ads ID (by {@link
@ -545,8 +552,7 @@ public final class MediaItem implements Bundleable {
* published in the {@code com.google.android.exoplayer2.Timeline} of the source as {@code
* com.google.android.exoplayer2.Timeline.Window#tag}.
*
* <p>If {@link #setUri} is passed a non-null {@code uri}, the tag is used to create a {@link
* PlaybackProperties} object. Otherwise it will be ignored.
* <p>This method should only be called if {@link #setUri} is passed a non-null value.
*/
public Builder setTag(@Nullable Object tag) {
this.tag = tag;

View File

@ -25,6 +25,7 @@ import com.google.common.base.Objects;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.List;
/**
@ -46,6 +47,14 @@ public final class MediaMetadata implements Bundleable {
@Nullable private Uri mediaUri;
@Nullable private Rating userRating;
@Nullable private Rating overallRating;
@Nullable private byte[] artworkData;
@Nullable private Uri artworkUri;
@Nullable private Integer trackNumber;
@Nullable private Integer totalTrackCount;
@Nullable @FolderType private Integer folderType;
@Nullable private Boolean isPlayable;
@Nullable private Integer year;
@Nullable private Bundle extras;
public Builder() {}
@ -60,6 +69,14 @@ public final class MediaMetadata implements Bundleable {
this.mediaUri = mediaMetadata.mediaUri;
this.userRating = mediaMetadata.userRating;
this.overallRating = mediaMetadata.overallRating;
this.artworkData = mediaMetadata.artworkData;
this.artworkUri = mediaMetadata.artworkUri;
this.trackNumber = mediaMetadata.trackNumber;
this.totalTrackCount = mediaMetadata.totalTrackCount;
this.folderType = mediaMetadata.folderType;
this.isPlayable = mediaMetadata.isPlayable;
this.year = mediaMetadata.year;
this.extras = mediaMetadata.extras;
}
/** Sets the title. */
@ -126,6 +143,54 @@ public final class MediaMetadata implements Bundleable {
return this;
}
/** Sets the artwork data as a compressed byte array. */
public Builder setArtworkData(@Nullable byte[] artworkData) {
this.artworkData = artworkData == null ? null : artworkData.clone();
return this;
}
/** Sets the artwork {@link Uri}. */
public Builder setArtworkUri(@Nullable Uri artworkUri) {
this.artworkUri = artworkUri;
return this;
}
/** Sets the track number. */
public Builder setTrackNumber(@Nullable Integer trackNumber) {
this.trackNumber = trackNumber;
return this;
}
/** Sets the total number of tracks. */
public Builder setTotalTrackCount(@Nullable Integer totalTrackCount) {
this.totalTrackCount = totalTrackCount;
return this;
}
/** Sets the {@link FolderType}. */
public Builder setFolderType(@Nullable @FolderType Integer folderType) {
this.folderType = folderType;
return this;
}
/** Sets whether the media is playable. */
public Builder setIsPlayable(@Nullable Boolean isPlayable) {
this.isPlayable = isPlayable;
return this;
}
/** Sets the year. */
public Builder setYear(@Nullable Integer year) {
this.year = year;
return this;
}
/** Sets the extras {@link Bundle}. */
public Builder setExtras(@Nullable Bundle extras) {
this.extras = extras;
return this;
}
/**
* Sets all fields supported by the {@link Metadata.Entry entries} within the {@link Metadata}.
*
@ -170,6 +235,41 @@ public final class MediaMetadata implements Bundleable {
}
}
/**
* 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>).
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
FOLDER_TYPE_MIXED,
FOLDER_TYPE_TITLES,
FOLDER_TYPE_ALBUMS,
FOLDER_TYPE_ARTISTS,
FOLDER_TYPE_GENRES,
FOLDER_TYPE_PLAYLISTS,
FOLDER_TYPE_YEARS
})
public @interface FolderType {}
/** Type for a folder containing media of mixed types. */
public static final int FOLDER_TYPE_MIXED = 0;
/** Type for a folder containing only playable media. */
public static final int FOLDER_TYPE_TITLES = 1;
/** Type for a folder containing media categorized by album. */
public static final int FOLDER_TYPE_ALBUMS = 2;
/** Type for a folder containing media categorized by artist. */
public static final int FOLDER_TYPE_ARTISTS = 3;
/** Type for a folder containing media categorized by genre. */
public static final int FOLDER_TYPE_GENRES = 4;
/** Type for a folder containing a playlist. */
public static final int FOLDER_TYPE_PLAYLISTS = 5;
/** Type for a folder containing media categorized by year. */
public static final int FOLDER_TYPE_YEARS = 6;
/** Empty {@link MediaMetadata}. */
public static final MediaMetadata EMPTY = new MediaMetadata.Builder().build();
@ -197,6 +297,27 @@ public final class MediaMetadata implements Bundleable {
@Nullable public final Rating userRating;
/** Optional overall {@link Rating}. */
@Nullable public final Rating overallRating;
/** Optional artwork data as a compressed byte array. */
@Nullable public final byte[] artworkData;
/** Optional artwork {@link Uri}. */
@Nullable public final Uri artworkUri;
/** Optional track number. */
@Nullable public final Integer trackNumber;
/** Optional total number of tracks. */
@Nullable public final Integer totalTrackCount;
/** Optional {@link FolderType}. */
@Nullable @FolderType public final Integer folderType;
/** Optional boolean for media playability. */
@Nullable public final Boolean isPlayable;
/** Optional year. */
@Nullable public final Integer year;
/**
* Optional extras {@link Bundle}.
*
* <p>Given the complexities of checking the equality of two {@link Bundle}s, this is not
* considered in the {@link #equals(Object)} or {@link #hashCode()}.
*/
@Nullable public final Bundle extras;
private MediaMetadata(Builder builder) {
this.title = builder.title;
@ -209,6 +330,14 @@ public final class MediaMetadata implements Bundleable {
this.mediaUri = builder.mediaUri;
this.userRating = builder.userRating;
this.overallRating = builder.overallRating;
this.artworkData = builder.artworkData;
this.artworkUri = builder.artworkUri;
this.trackNumber = builder.trackNumber;
this.totalTrackCount = builder.totalTrackCount;
this.folderType = builder.folderType;
this.isPlayable = builder.isPlayable;
this.year = builder.year;
this.extras = builder.extras;
}
/** Returns a new {@link Builder} instance with the current {@link MediaMetadata} fields. */
@ -234,7 +363,14 @@ public final class MediaMetadata implements Bundleable {
&& Util.areEqual(description, that.description)
&& Util.areEqual(mediaUri, that.mediaUri)
&& Util.areEqual(userRating, that.userRating)
&& Util.areEqual(overallRating, that.overallRating);
&& Util.areEqual(overallRating, that.overallRating)
&& Arrays.equals(artworkData, that.artworkData)
&& Util.areEqual(artworkUri, that.artworkUri)
&& Util.areEqual(trackNumber, that.trackNumber)
&& Util.areEqual(totalTrackCount, that.totalTrackCount)
&& Util.areEqual(folderType, that.folderType)
&& Util.areEqual(isPlayable, that.isPlayable)
&& Util.areEqual(year, that.year);
}
@Override
@ -249,7 +385,14 @@ public final class MediaMetadata implements Bundleable {
description,
mediaUri,
userRating,
overallRating);
overallRating,
Arrays.hashCode(artworkData),
artworkUri,
trackNumber,
totalTrackCount,
folderType,
isPlayable,
year);
}
// Bundleable implementation.
@ -267,6 +410,14 @@ public final class MediaMetadata implements Bundleable {
FIELD_MEDIA_URI,
FIELD_USER_RATING,
FIELD_OVERALL_RATING,
FIELD_ARTWORK_DATA,
FIELD_ARTWORK_URI,
FIELD_TRACK_NUMBER,
FIELD_TOTAL_TRACK_COUNT,
FIELD_FOLDER_TYPE,
FIELD_IS_PLAYABLE,
FIELD_YEAR,
FIELD_EXTRAS
})
private @interface FieldNumber {}
@ -280,6 +431,14 @@ public final class MediaMetadata implements Bundleable {
private static final int FIELD_MEDIA_URI = 7;
private static final int FIELD_USER_RATING = 8;
private static final int FIELD_OVERALL_RATING = 9;
private static final int FIELD_ARTWORK_DATA = 10;
private static final int FIELD_ARTWORK_URI = 11;
private static final int FIELD_TRACK_NUMBER = 12;
private static final int FIELD_TOTAL_TRACK_COUNT = 13;
private static final int FIELD_FOLDER_TYPE = 14;
private static final int FIELD_IS_PLAYABLE = 15;
private static final int FIELD_YEAR = 16;
private static final int FIELD_EXTRAS = 1000;
@Override
public Bundle toBundle() {
@ -292,6 +451,8 @@ public final class MediaMetadata implements Bundleable {
bundle.putCharSequence(keyForField(FIELD_SUBTITLE), subtitle);
bundle.putCharSequence(keyForField(FIELD_DESCRIPTION), description);
bundle.putParcelable(keyForField(FIELD_MEDIA_URI), mediaUri);
bundle.putByteArray(keyForField(FIELD_ARTWORK_DATA), artworkData);
bundle.putParcelable(keyForField(FIELD_ARTWORK_URI), artworkUri);
if (userRating != null) {
bundle.putBundle(keyForField(FIELD_USER_RATING), userRating.toBundle());
@ -299,7 +460,24 @@ public final class MediaMetadata implements Bundleable {
if (overallRating != null) {
bundle.putBundle(keyForField(FIELD_OVERALL_RATING), overallRating.toBundle());
}
if (trackNumber != null) {
bundle.putInt(keyForField(FIELD_TRACK_NUMBER), trackNumber);
}
if (totalTrackCount != null) {
bundle.putInt(keyForField(FIELD_TOTAL_TRACK_COUNT), totalTrackCount);
}
if (folderType != null) {
bundle.putInt(keyForField(FIELD_FOLDER_TYPE), folderType);
}
if (isPlayable != null) {
bundle.putBoolean(keyForField(FIELD_IS_PLAYABLE), isPlayable);
}
if (year != null) {
bundle.putInt(keyForField(FIELD_YEAR), year);
}
if (extras != null) {
bundle.putBundle(keyForField(FIELD_EXTRAS), extras);
}
return bundle;
}
@ -316,7 +494,10 @@ public final class MediaMetadata implements Bundleable {
.setDisplayTitle(bundle.getCharSequence(keyForField(FIELD_DISPLAY_TITLE)))
.setSubtitle(bundle.getCharSequence(keyForField(FIELD_SUBTITLE)))
.setDescription(bundle.getCharSequence(keyForField(FIELD_DESCRIPTION)))
.setMediaUri(bundle.getParcelable(keyForField(FIELD_MEDIA_URI)));
.setMediaUri(bundle.getParcelable(keyForField(FIELD_MEDIA_URI)))
.setArtworkData(bundle.getByteArray(keyForField(FIELD_ARTWORK_DATA)))
.setArtworkUri(bundle.getParcelable(keyForField(FIELD_ARTWORK_URI)))
.setExtras(bundle.getBundle(keyForField(FIELD_EXTRAS)));
if (bundle.containsKey(keyForField(FIELD_USER_RATING))) {
@Nullable Bundle fieldBundle = bundle.getBundle(keyForField(FIELD_USER_RATING));
@ -327,9 +508,24 @@ public final class MediaMetadata implements Bundleable {
if (bundle.containsKey(keyForField(FIELD_OVERALL_RATING))) {
@Nullable Bundle fieldBundle = bundle.getBundle(keyForField(FIELD_OVERALL_RATING));
if (fieldBundle != null) {
builder.setUserRating(Rating.CREATOR.fromBundle(fieldBundle));
builder.setOverallRating(Rating.CREATOR.fromBundle(fieldBundle));
}
}
if (bundle.containsKey(keyForField(FIELD_TRACK_NUMBER))) {
builder.setTrackNumber(bundle.getInt(keyForField(FIELD_TRACK_NUMBER)));
}
if (bundle.containsKey(keyForField(FIELD_TOTAL_TRACK_COUNT))) {
builder.setTotalTrackCount(bundle.getInt(keyForField(FIELD_TOTAL_TRACK_COUNT)));
}
if (bundle.containsKey(keyForField(FIELD_FOLDER_TYPE))) {
builder.setFolderType(bundle.getInt(keyForField(FIELD_FOLDER_TYPE)));
}
if (bundle.containsKey(keyForField(FIELD_IS_PLAYABLE))) {
builder.setIsPlayable(bundle.getBoolean(keyForField(FIELD_IS_PLAYABLE)));
}
if (bundle.containsKey(keyForField(FIELD_YEAR))) {
builder.setYear(bundle.getInt(keyForField(FIELD_YEAR)));
}
return builder.build();
}

View File

@ -20,6 +20,7 @@ import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.MediaMetadata;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
@ -50,6 +51,11 @@ public final class ApicFrame extends Id3Frame {
pictureData = castNonNull(in.createByteArray());
}
@Override
public void populateMediaMetadata(MediaMetadata.Builder builder) {
builder.setArtworkData(pictureData);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {

View File

@ -60,6 +60,27 @@ public final class TextInformationFrame extends Id3Frame {
case "TALB":
builder.setAlbumTitle(value);
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:
break;
}

View File

@ -19,6 +19,8 @@ import android.graphics.Bitmap;
import android.graphics.Color;
import android.text.Layout;
import android.text.Layout.Alignment;
import android.text.Spanned;
import android.text.SpannedString;
import androidx.annotation.ColorInt;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
@ -458,7 +460,13 @@ public final class Cue {
} else {
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.multiRowAlignment = multiRowAlignment;
this.bitmap = bitmap;

View File

@ -141,7 +141,7 @@ public final class GlUtil {
location = GLES20.glGetUniformLocation(program, this.name);
this.type = type[0];
value = new float[1];
value = new float[16];
}
/**
@ -160,9 +160,14 @@ public final class GlUtil {
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
* {@link #setFloat(float)}.
* Sets the uniform to whatever value was passed via {@link #setSamplerTexId(int, int)}, {@link
* #setFloat(float)} or {@link #setFloats(float[])}.
*
* <p>Should be called before each drawing call.
*/
@ -173,6 +178,12 @@ public final class GlUtil {
return;
}
if (type == GLES20.GL_FLOAT_MAT4) {
GLES20.glUniformMatrix4fv(location, 1, false, value, 0);
checkGlError();
return;
}
if (texId == 0) {
throw new IllegalStateException("call setSamplerTexId before bind");
}

View File

@ -62,6 +62,8 @@ public final class MimeTypes {
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_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_ALAW = BASE_TYPE_AUDIO + "/g711-alaw";
public static final String AUDIO_MLAW = BASE_TYPE_AUDIO + "/g711-mlaw";
@ -365,6 +367,10 @@ public final class MimeTypes {
}
}
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")) {
return MimeTypes.AUDIO_AC3;
} else if (codec.startsWith("ec-3") || codec.startsWith("dec3")) {

View File

@ -17,9 +17,16 @@ package com.google.android.exoplayer2;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import android.os.Bundle;
import androidx.test.ext.junit.runners.AndroidJUnit4;
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.util.MimeTypes;
import com.google.common.collect.ImmutableList;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -32,6 +39,23 @@ public class MediaMetadataTest {
MediaMetadata mediaMetadata = new MediaMetadata.Builder().build();
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
@ -44,20 +68,96 @@ public class MediaMetadataTest {
}
@Test
public void roundTripViaBundle_yieldsEqualInstance() {
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build();
public void builderSetArtworkData_setsArtworkData() {
byte[] bytes = new byte[] {35, 12, 6, 77};
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setArtworkData(bytes).build();
assertThat(MediaMetadata.CREATOR.fromBundle(mediaMetadata.toBundle())).isEqualTo(mediaMetadata);
assertThat(Arrays.equals(mediaMetadata.artworkData, bytes)).isTrue();
}
@Test
public void builderPopulatedFromMetadataEntry_setsTitleCorrectly() {
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() {
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();
MediaMetadata fromBundle = MediaMetadata.CREATOR.fromBundle(mediaMetadata.toBundle());
assertThat(fromBundle).isEqualTo(mediaMetadata);
assertThat(fromBundle.extras.getString("exampleKey")).isEqualTo("exampleValue");
}
@Test
public void builderPopulatedFromTextInformationFrameEntry_setsValues() {
String title = "the title";
Metadata.Entry entry =
new TextInformationFrame(/* id= */ "TT2", /* description= */ null, /* value= */ title);
String artist = "artist";
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();
entry.populateMediaMetadata(builder);
for (Metadata.Entry entry : entries) {
entry.populateMediaMetadata(builder);
}
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);
}
}

View File

@ -430,11 +430,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
case DefaultDrmSessionManager.MODE_RELEASE:
Assertions.checkNotNull(offlineLicenseKeySetId);
Assertions.checkNotNull(this.sessionId);
// It's not necessary to restore the key before releasing it but this serves as a good
// fast-failure check.
if (restoreKeys()) {
postKeyRequest(offlineLicenseKeySetId, ExoMediaDrm.KEY_TYPE_RELEASE, allowRetry);
}
postKeyRequest(offlineLicenseKeySetId, ExoMediaDrm.KEY_TYPE_RELEASE, allowRetry);
break;
default:
break;

View File

@ -457,9 +457,15 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
if (prepareCallsCount++ != 0) {
return;
}
checkState(exoMediaDrm == null);
exoMediaDrm = exoMediaDrmProvider.acquireExoMediaDrm(uuid);
exoMediaDrm.setOnEventListener(new MediaDrmEventListener());
if (exoMediaDrm == null) {
exoMediaDrm = exoMediaDrmProvider.acquireExoMediaDrm(uuid);
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
@ -478,8 +484,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
}
releaseAllPreacquiredSessions();
checkNotNull(exoMediaDrm).release();
exoMediaDrm = null;
maybeReleaseMediaDrm();
}
@Override
@ -487,6 +492,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
Looper playbackLooper,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
Format format) {
checkState(prepareCallsCount > 0);
initPlaybackLooper(playbackLooper);
PreacquiredSessionReference preacquiredSessionReference =
new PreacquiredSessionReference(eventDispatcher);
@ -500,6 +506,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
Looper playbackLooper,
@Nullable DrmSessionEventListener.EventDispatcher eventDispatcher,
Format format) {
checkState(prepareCallsCount > 0);
initPlaybackLooper(playbackLooper);
return acquireSession(
playbackLooper,
@ -774,6 +781,17 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
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}.
*
@ -895,6 +913,7 @@ public class DefaultDrmSessionManager implements DrmSessionManager {
keepaliveSessions.remove(session);
}
}
maybeReleaseMediaDrm();
}
}

View File

@ -758,12 +758,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
outputStreamStartPositionUs = C.TIME_UNSET;
outputStreamOffsetUs = C.TIME_UNSET;
pendingOutputStreamOffsetCount = 0;
if (sourceDrmSession != null || codecDrmSession != null) {
// TODO: Do something better with this case.
onReset();
} else {
flushOrReleaseCodec();
}
flushOrReleaseCodec();
}
@Override

View File

@ -841,9 +841,9 @@ public final class MediaCodecUtil {
/**
* Conversion values taken from ISO 14496-10 Table A-1.
*
* @param avcLevel one of CodecProfileLevel.AVCLevel* constants.
* @return maximum frame size that can be decoded by a decoder with the specified avc level
* (or {@code -1} if the level is not recognized)
* @param avcLevel One of the {@link CodecProfileLevel} {@code AVCLevel*} constants.
* @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.
*/
private static int avcLevelToMaxFrameSize(int avcLevel) {
switch (avcLevel) {
@ -873,6 +873,10 @@ public final class MediaCodecUtil {
case CodecProfileLevel.AVCLevel51:
case CodecProfileLevel.AVCLevel52:
return 36864 * 16 * 16;
case CodecProfileLevel.AVCLevel6:
case CodecProfileLevel.AVCLevel61:
case CodecProfileLevel.AVCLevel62:
return 139264 * 16 * 16;
default:
return -1;
}

View File

@ -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
// styling superclasses (e.g. MetricAffectingSpan). The only way to render this styling is to
// extract the spans and do the layout manually.
public final class HorizontalTextInVerticalContextSpan {}
public final class HorizontalTextInVerticalContextSpan implements LanguageFeatureSpan {}

View File

@ -0,0 +1,19 @@
/*
* 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 {}

View File

@ -30,7 +30,7 @@ package com.google.android.exoplayer2.text.span;
// extract the spans and do the layout manually.
// TODO: Consider adding support for parenthetical text to be used when rendering doesn't support
// 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. */
public final String rubyText;

View File

@ -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
// any styling superclasses (e.g. MetricAffectingSpan). The only way to render this emphasis is to
// 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.

View File

@ -124,7 +124,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
private boolean codecHandlesHdr10PlusOutOfBandMetadata;
@Nullable private Surface surface;
@Nullable private Surface dummySurface;
@Nullable private DummySurface dummySurface;
private boolean haveReportedFirstFrameRenderedForCurrentSurface;
@C.VideoScalingMode private int scalingMode;
private boolean renderedFirstFrameAfterReset;
@ -486,6 +486,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
}
@TargetApi(17) // Needed for dummySurface usage. dummySurface is always null on API level 16.
@Override
protected void onReset() {
try {
@ -596,12 +597,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
return tunneling && Util.SDK_INT < 23;
}
@TargetApi(17) // Needed for dummySurface usage. dummySurface is always null on API level 16.
@Override
protected MediaCodecAdapter.Configuration getMediaCodecConfiguration(
MediaCodecInfo codecInfo,
Format format,
@Nullable MediaCrypto crypto,
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;
codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats());
MediaFormat mediaFormat =

View File

@ -18,18 +18,21 @@ package com.google.android.exoplayer2.drm;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertThrows;
import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
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.testutil.FakeExoMediaDrm;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Test;
@ -178,6 +181,49 @@ public class DefaultDrmSessionManagerTest {
assertThat(drmSession.getState()).isEqualTo(DrmSession.STATE_OPENED_WITH_KEYS);
}
@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 {
ImmutableList<DrmInitData.SchemeData> secondSchemeDatas =
@ -407,6 +453,154 @@ public class DefaultDrmSessionManagerTest {
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) {
// Check the error first, so we get a meaningful failure if there's been an error.
assertThat(drmSession.getError()).isNull();

View File

@ -121,6 +121,15 @@ import java.util.List;
@SuppressWarnings("ConstantCaseForConstants")
public static final int TYPE__mp3 = 0x2e6d7033;
@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;

View File

@ -940,6 +940,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|| childAtomType == Atom.TYPE_twos
|| childAtomType == Atom.TYPE__mp2
|| childAtomType == Atom.TYPE__mp3
|| childAtomType == Atom.TYPE_mha1
|| childAtomType == Atom.TYPE_mhm1
|| childAtomType == Atom.TYPE_alac
|| childAtomType == Atom.TYPE_alaw
|| childAtomType == Atom.TYPE_ulaw
@ -1312,6 +1314,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
pcmEncoding = C.ENCODING_PCM_16BIT_BIG_ENDIAN;
} else if (atomType == Atom.TYPE__mp2 || atomType == Atom.TYPE__mp3) {
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) {
mimeType = MimeTypes.AUDIO_ALAC;
} else if (atomType == Atom.TYPE_alaw) {
@ -1330,9 +1336,22 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
int childAtomSize = parent.readInt();
Assertions.checkState(childAtomSize > 0, "childAtomSize should be positive");
int childAtomType = parent.readInt();
if (childAtomType == Atom.TYPE_esds || (isQuickTime && childAtomType == Atom.TYPE_wave)) {
int esdsAtomPosition = childAtomType == Atom.TYPE_esds ? childPosition
: findEsdsPosition(parent, childPosition, childAtomSize);
if (childAtomType == Atom.TYPE_mhaC) {
// See ISO_IEC_23008-3;2019 MHADecoderConfigurationRecord
// 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) {
Pair<@NullableType String, byte @NullableType []> mimeTypeAndInitializationData =
parseEsdsFromParent(parent, esdsAtomPosition);

View File

@ -84,4 +84,16 @@ public final class Mp4ExtractorTest {
ExtractorAsserts.assertBehavior(
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);
}
}

View File

@ -352,70 +352,67 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return;
}
@Nullable
HlsMediaPlaylist mediaPlaylist =
HlsMediaPlaylist playlist =
playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true);
// playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be non-null.
checkNotNull(mediaPlaylist);
independentSegments = mediaPlaylist.hasIndependentSegments;
// playlistTracker snapshot is valid (checked by if() above), so playlist must be non-null.
checkNotNull(playlist);
independentSegments = playlist.hasIndependentSegments;
updateLiveEdgeTimeUs(mediaPlaylist);
updateLiveEdgeTimeUs(playlist);
// Select the chunk.
long startOfPlaylistInPeriodUs =
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
long startOfPlaylistInPeriodUs = playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
Pair<Long, Integer> nextMediaSequenceAndPartIndex =
getNextMediaSequenceAndPartIndex(
previous, switchingTrack, mediaPlaylist, startOfPlaylistInPeriodUs, loadPositionUs);
previous, switchingTrack, playlist, startOfPlaylistInPeriodUs, loadPositionUs);
long chunkMediaSequence = nextMediaSequenceAndPartIndex.first;
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
// behind the live window.
selectedTrackIndex = oldTrackIndex;
selectedPlaylistUrl = playlistUrls[selectedTrackIndex];
mediaPlaylist =
playlist =
playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */ true);
// playlistTracker snapshot is valid (checked by if() above), so mediaPlaylist must be
// non-null.
checkNotNull(mediaPlaylist);
startOfPlaylistInPeriodUs =
mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
// playlistTracker snapshot is valid (checked by if() above), so playlist must be non-null.
checkNotNull(playlist);
startOfPlaylistInPeriodUs = playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
// Get the next segment/part without switching tracks.
Pair<Long, Integer> nextMediaSequenceAndPartIndexWithoutAdapting =
getNextMediaSequenceAndPartIndex(
previous,
/* switchingTrack= */ false,
mediaPlaylist,
playlist,
startOfPlaylistInPeriodUs,
loadPositionUs);
chunkMediaSequence = nextMediaSequenceAndPartIndexWithoutAdapting.first;
partIndex = nextMediaSequenceAndPartIndexWithoutAdapting.second;
}
if (chunkMediaSequence < mediaPlaylist.mediaSequence) {
if (chunkMediaSequence < playlist.mediaSequence) {
fatalError = new BehindLiveWindowException();
return;
}
@Nullable
SegmentBaseHolder segmentBaseHolder =
getNextSegmentHolder(mediaPlaylist, chunkMediaSequence, partIndex);
getNextSegmentHolder(playlist, chunkMediaSequence, partIndex);
if (segmentBaseHolder == null) {
if (!mediaPlaylist.hasEndTag) {
if (!playlist.hasEndTag) {
// Reload the playlist in case of a live stream.
out.playlistUrl = selectedPlaylistUrl;
seenExpectedPlaylistError &= selectedPlaylistUrl.equals(expectedPlaylistUrl);
expectedPlaylistUrl = selectedPlaylistUrl;
return;
} else if (allowEndOfStream || mediaPlaylist.segments.isEmpty()) {
} else if (allowEndOfStream || playlist.segments.isEmpty()) {
out.endOfStream = true;
return;
}
// Use the last segment available in case of a VOD stream.
segmentBaseHolder =
new SegmentBaseHolder(
Iterables.getLast(mediaPlaylist.segments),
mediaPlaylist.mediaSequence + mediaPlaylist.segments.size() - 1,
Iterables.getLast(playlist.segments),
playlist.mediaSequence + playlist.segments.size() - 1,
/* partIndex= */ C.INDEX_UNSET);
}
@ -426,24 +423,36 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// Check if the media segment or its initialization segment are fully encrypted.
@Nullable
Uri initSegmentKeyUri =
getFullEncryptionKeyUri(mediaPlaylist, segmentBaseHolder.segmentBase.initializationSegment);
getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase.initializationSegment);
out.chunk = maybeCreateEncryptionChunkFor(initSegmentKeyUri, selectedTrackIndex);
if (out.chunk != null) {
return;
}
@Nullable
Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(mediaPlaylist, segmentBaseHolder.segmentBase);
Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase);
out.chunk = maybeCreateEncryptionChunkFor(mediaSegmentKeyUri, selectedTrackIndex);
if (out.chunk != null) {
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 =
HlsMediaChunk.createInstance(
extractorFactory,
mediaDataSource,
playlistFormats[selectedTrackIndex],
startOfPlaylistInPeriodUs,
mediaPlaylist,
playlist,
segmentBaseHolder,
selectedPlaylistUrl,
muxedCaptionFormats,
@ -453,7 +462,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
timestampAdjusterProvider,
previous,
/* mediaSegmentKey= */ keyCache.get(mediaSegmentKeyUri),
/* initSegmentKey= */ keyCache.get(initSegmentKeyUri));
/* initSegmentKey= */ keyCache.get(initSegmentKeyUri),
shouldSpliceIn);
}
@Nullable

View File

@ -75,6 +75,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* @param mediaSegmentKey The media segment decryption key, if fully encrypted. Null otherwise.
* @param initSegmentKey The initialization segment decryption key, if fully encrypted. Null
* otherwise.
* @param shouldSpliceIn Whether samples for this chunk should be spliced into existing samples.
*/
public static HlsMediaChunk createInstance(
HlsExtractorFactory extractorFactory,
@ -91,7 +92,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
TimestampAdjusterProvider timestampAdjusterProvider,
@Nullable HlsMediaChunk previousChunk,
@Nullable byte[] mediaSegmentKey,
@Nullable byte[] initSegmentKey) {
@Nullable byte[] initSegmentKey,
boolean shouldSpliceIn) {
// Media segment.
HlsMediaPlaylist.SegmentBase mediaSegment = segmentBaseHolder.segmentBase;
DataSpec dataSpec =
@ -135,17 +137,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@Nullable HlsMediaChunkExtractor previousExtractor = null;
Id3Decoder id3Decoder;
ParsableByteArray scratchId3Data;
boolean shouldSpliceIn;
if (previousChunk != null) {
boolean isFollowingChunk =
playlistUrl.equals(previousChunk.playlistUrl) && previousChunk.loadCompleted;
id3Decoder = previousChunk.id3Decoder;
scratchId3Data = previousChunk.scratchId3Data;
boolean isIndependent = isIndependent(segmentBaseHolder, mediaPlaylist);
boolean canContinueWithoutSplice =
isFollowingChunk
|| (isIndependent && segmentStartTimeInPeriodUs >= previousChunk.endTimeUs);
shouldSpliceIn = !canContinueWithoutSplice;
previousExtractor =
isFollowingChunk
&& !previousChunk.extractorInvalidated
@ -155,7 +152,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
} else {
id3Decoder = new Id3Decoder();
scratchId3Data = new ParsableByteArray(Id3Decoder.ID3_HEADER_LENGTH);
shouldSpliceIn = false;
}
return new HlsMediaChunk(
extractorFactory,
@ -186,6 +182,41 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
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 =
"com.apple.streaming.transportStreamTimestamp";

View File

@ -514,9 +514,8 @@ public final class HlsMediaSource extends BaseMediaSource
@Override
public void onPrimaryPlaylistRefreshed(HlsMediaPlaylist playlist) {
SinglePeriodTimeline timeline;
long windowStartTimeMs = playlist.hasProgramDateTime ? C.usToMs(playlist.startTimeUs)
: C.TIME_UNSET;
long windowStartTimeMs =
playlist.hasProgramDateTime ? C.usToMs(playlist.startTimeUs) : C.TIME_UNSET;
// For playlist types EVENT and VOD we know segments are never removed, so the presentation
// started at the same time as the window. Otherwise, we don't know the presentation start time.
long presentationStartTimeMs =
@ -524,87 +523,123 @@ public final class HlsMediaSource extends BaseMediaSource
|| playlist.playlistType == HlsMediaPlaylist.PLAYLIST_TYPE_VOD
? windowStartTimeMs
: C.TIME_UNSET;
long windowDefaultStartPositionUs = playlist.startOffsetUs;
// masterPlaylist is non-null because the first playlist has been fetched by now.
// The master playlist is non-null because the first playlist has been fetched by now.
HlsManifest manifest =
new HlsManifest(checkNotNull(playlistTracker.getMasterPlaylist()), playlist);
if (playlistTracker.isLive()) {
long liveEdgeOffsetUs = getLiveEdgeOffsetUs(playlist);
long targetLiveOffsetUs =
liveConfiguration.targetOffsetMs != C.TIME_UNSET
? C.msToUs(liveConfiguration.targetOffsetMs)
: getTargetLiveOffsetUs(playlist, liveEdgeOffsetUs);
// Ensure target live offset is within the live window and greater than the live edge offset.
targetLiveOffsetUs =
Util.constrainValue(
targetLiveOffsetUs, liveEdgeOffsetUs, playlist.durationUs + liveEdgeOffsetUs);
maybeUpdateMediaItem(targetLiveOffsetUs);
long offsetFromInitialStartTimeUs =
playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
long periodDurationUs =
playlist.hasEndTag ? offsetFromInitialStartTimeUs + playlist.durationUs : C.TIME_UNSET;
List<HlsMediaPlaylist.Segment> segments = playlist.segments;
if (!segments.isEmpty()) {
windowDefaultStartPositionUs = getWindowDefaultStartPosition(playlist, liveEdgeOffsetUs);
} else if (windowDefaultStartPositionUs == C.TIME_UNSET) {
windowDefaultStartPositionUs = 0;
}
timeline =
new SinglePeriodTimeline(
presentationStartTimeMs,
windowStartTimeMs,
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
periodDurationUs,
/* windowDurationUs= */ playlist.durationUs,
/* windowPositionInPeriodUs= */ offsetFromInitialStartTimeUs,
windowDefaultStartPositionUs,
/* isSeekable= */ true,
/* isDynamic= */ !playlist.hasEndTag,
manifest,
mediaItem,
liveConfiguration);
} else /* not live */ {
if (windowDefaultStartPositionUs == C.TIME_UNSET) {
windowDefaultStartPositionUs = 0;
}
timeline =
new SinglePeriodTimeline(
presentationStartTimeMs,
windowStartTimeMs,
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
/* periodDurationUs= */ playlist.durationUs,
/* windowDurationUs= */ playlist.durationUs,
/* windowPositionInPeriodUs= */ 0,
windowDefaultStartPositionUs,
/* isSeekable= */ true,
/* isDynamic= */ false,
manifest,
mediaItem,
/* liveConfiguration= */ null);
}
SinglePeriodTimeline timeline =
playlistTracker.isLive()
? createTimelineForLive(playlist, presentationStartTimeMs, windowStartTimeMs, manifest)
: createTimelineForOnDemand(
playlist, presentationStartTimeMs, windowStartTimeMs, manifest);
refreshSourceInfo(timeline);
}
private SinglePeriodTimeline createTimelineForLive(
HlsMediaPlaylist playlist,
long presentationStartTimeMs,
long windowStartTimeMs,
HlsManifest manifest) {
long offsetFromInitialStartTimeUs =
playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
long periodDurationUs =
playlist.hasEndTag ? offsetFromInitialStartTimeUs + playlist.durationUs : C.TIME_UNSET;
long liveEdgeOffsetUs = getLiveEdgeOffsetUs(playlist);
long targetLiveOffsetUs;
if (liveConfiguration.targetOffsetMs != C.TIME_UNSET) {
// Media item has a defined target offset.
targetLiveOffsetUs = C.msToUs(liveConfiguration.targetOffsetMs);
} else {
// Decide target offset from playlist.
targetLiveOffsetUs = getTargetLiveOffsetUs(playlist, liveEdgeOffsetUs);
}
// Ensure target live offset is within the live window and greater than the live edge offset.
targetLiveOffsetUs =
Util.constrainValue(
targetLiveOffsetUs, liveEdgeOffsetUs, playlist.durationUs + liveEdgeOffsetUs);
maybeUpdateLiveConfiguration(targetLiveOffsetUs);
long windowDefaultStartPositionUs =
getLiveWindowDefaultStartPositionUs(playlist, liveEdgeOffsetUs);
return new SinglePeriodTimeline(
presentationStartTimeMs,
windowStartTimeMs,
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
periodDurationUs,
/* windowDurationUs= */ playlist.durationUs,
/* windowPositionInPeriodUs= */ offsetFromInitialStartTimeUs,
windowDefaultStartPositionUs,
/* isSeekable= */ true,
/* isDynamic= */ !playlist.hasEndTag,
manifest,
mediaItem,
liveConfiguration);
}
private SinglePeriodTimeline createTimelineForOnDemand(
HlsMediaPlaylist playlist,
long presentationStartTimeMs,
long windowStartTimeMs,
HlsManifest manifest) {
long windowDefaultStartPositionUs;
if (playlist.startOffsetUs == C.TIME_UNSET || playlist.segments.isEmpty()) {
windowDefaultStartPositionUs = 0;
} else {
if (playlist.preciseStart || playlist.startOffsetUs == playlist.durationUs) {
windowDefaultStartPositionUs = playlist.startOffsetUs;
} else {
windowDefaultStartPositionUs =
findClosestPrecedingSegment(playlist.segments, playlist.startOffsetUs)
.relativeStartTimeUs;
}
}
return new SinglePeriodTimeline(
presentationStartTimeMs,
windowStartTimeMs,
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
/* periodDurationUs= */ playlist.durationUs,
/* windowDurationUs= */ playlist.durationUs,
/* windowPositionInPeriodUs= */ 0,
windowDefaultStartPositionUs,
/* isSeekable= */ true,
/* isDynamic= */ false,
manifest,
mediaItem,
/* liveConfiguration= */ null);
}
private long getLiveEdgeOffsetUs(HlsMediaPlaylist playlist) {
return playlist.hasProgramDateTime
? C.msToUs(Util.getNowUnixTimeMs(elapsedRealTimeOffsetMs)) - playlist.getEndTimeUs()
: 0;
}
private long getWindowDefaultStartPosition(HlsMediaPlaylist playlist, long liveEdgeOffsetUs) {
List<HlsMediaPlaylist.Segment> segments = playlist.segments;
int segmentIndex = segments.size() - 1;
long minStartPositionUs =
playlist.durationUs + liveEdgeOffsetUs - C.msToUs(liveConfiguration.targetOffsetMs);
while (segmentIndex > 0
&& segments.get(segmentIndex).relativeStartTimeUs > minStartPositionUs) {
segmentIndex--;
private long getLiveWindowDefaultStartPositionUs(
HlsMediaPlaylist playlist, long liveEdgeOffsetUs) {
long startPositionUs =
playlist.startOffsetUs != C.TIME_UNSET
? playlist.startOffsetUs
: playlist.durationUs + liveEdgeOffsetUs - C.msToUs(liveConfiguration.targetOffsetMs);
if (playlist.preciseStart) {
return startPositionUs;
}
return segments.get(segmentIndex).relativeStartTimeUs;
@Nullable
HlsMediaPlaylist.Part part =
findClosestPrecedingIndependentPart(playlist.trailingParts, startPositionUs);
if (part != null) {
return part.relativeStartTimeUs;
}
if (playlist.segments.isEmpty()) {
return 0;
}
HlsMediaPlaylist.Segment segment =
findClosestPrecedingSegment(playlist.segments, startPositionUs);
part = findClosestPrecedingIndependentPart(segment.parts, startPositionUs);
if (part != null) {
return part.relativeStartTimeUs;
}
return segment.relativeStartTimeUs;
}
private void maybeUpdateMediaItem(long targetLiveOffsetUs) {
private void maybeUpdateLiveConfiguration(long targetLiveOffsetUs) {
long targetLiveOffsetMs = C.usToMs(targetLiveOffsetUs);
if (targetLiveOffsetMs != liveConfiguration.targetOffsetMs) {
liveConfiguration =
@ -612,21 +647,64 @@ public final class HlsMediaSource extends BaseMediaSource
}
}
/**
* Gets the target live offset, in microseconds, for a live playlist.
*
* <p>The target offset is derived by checking the following in this order:
*
* <ol>
* <li>The playlist defines a start offset.
* <li>The playlist defines a part hold back in server control and has part duration.
* <li>The playlist defines a hold back in server control.
* <li>Fallback to {@code 3 x target duration}.
* </ol>
*
* @param playlist The playlist.
* @param liveEdgeOffsetUs The current live edge offset.
* @return The selected target live offset, in microseconds.
*/
private static long getTargetLiveOffsetUs(HlsMediaPlaylist playlist, long liveEdgeOffsetUs) {
HlsMediaPlaylist.ServerControl serverControl = playlist.serverControl;
// Select part hold back only if the playlist has a part target duration.
long offsetToEndOfPlaylistUs;
long targetOffsetUs;
if (playlist.startOffsetUs != C.TIME_UNSET) {
offsetToEndOfPlaylistUs = playlist.durationUs - playlist.startOffsetUs;
targetOffsetUs = playlist.durationUs - playlist.startOffsetUs;
} else if (serverControl.partHoldBackUs != C.TIME_UNSET
&& playlist.partTargetDurationUs != C.TIME_UNSET) {
offsetToEndOfPlaylistUs = serverControl.partHoldBackUs;
// Select part hold back only if the playlist has a part target duration.
targetOffsetUs = serverControl.partHoldBackUs;
} else if (serverControl.holdBackUs != C.TIME_UNSET) {
offsetToEndOfPlaylistUs = serverControl.holdBackUs;
targetOffsetUs = serverControl.holdBackUs;
} else {
// Fallback, see RFC 8216, Section 4.4.3.8.
offsetToEndOfPlaylistUs = 3 * playlist.targetDurationUs;
targetOffsetUs = 3 * playlist.targetDurationUs;
}
return offsetToEndOfPlaylistUs + liveEdgeOffsetUs;
return targetOffsetUs + liveEdgeOffsetUs;
}
@Nullable
private static HlsMediaPlaylist.Part findClosestPrecedingIndependentPart(
List<HlsMediaPlaylist.Part> parts, long positionUs) {
@Nullable HlsMediaPlaylist.Part closestPart = null;
for (int i = 0; i < parts.size(); i++) {
HlsMediaPlaylist.Part part = parts.get(i);
if (part.relativeStartTimeUs <= positionUs && part.isIndependent) {
closestPart = part;
} else if (part.relativeStartTimeUs > positionUs) {
break;
}
}
return closestPart;
}
/**
* Gets the segment that contains {@code positionUs}, or the last segment if the position is
* beyond the segments list.
*/
private static HlsMediaPlaylist.Segment findClosestPrecedingSegment(
List<HlsMediaPlaylist.Segment> segments, long positionUs) {
int segmentIndex =
Util.binarySearchFloor(
segments, positionUs, /* inclusive= */ true, /* stayInBounds= */ true);
return segments.get(segmentIndex);
}
}

View File

@ -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_REMOVED;
import static java.lang.Math.max;
import static java.lang.Math.min;
import android.net.Uri;
import android.os.Handler;
@ -636,17 +637,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
int skipCount = sampleQueue.getSkipCount(positionUs, loadingFinished);
// Ensure we don't skip into preload chunks until we can be sure they are permanently published.
int readIndex = sampleQueue.getReadIndex();
for (int i = 0; i < mediaChunks.size(); i++) {
HlsMediaChunk mediaChunk = mediaChunks.get(i);
int firstSampleIndex = mediaChunks.get(i).getFirstSampleIndex(sampleQueueIndex);
if (readIndex + skipCount <= firstSampleIndex) {
break;
}
if (!mediaChunk.isPublished()) {
skipCount = firstSampleIndex - readIndex;
break;
}
@Nullable HlsMediaChunk lastChunk = Iterables.getLast(mediaChunks, /* defaultValue= */ null);
if (lastChunk != null && !lastChunk.isPublished()) {
int readIndex = sampleQueue.getReadIndex();
int firstSampleIndex = lastChunk.getFirstSampleIndex(sampleQueueIndex);
skipCount = min(skipCount, firstSampleIndex - readIndex);
}
sampleQueue.skip(skipCount);
@ -709,6 +704,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
? lastMediaChunk.endTimeUs
: max(lastSeekPositionUs, lastMediaChunk.startTimeUs);
}
nextChunkHolder.clear();
chunkSource.getNextChunk(
positionUs,
loadPositionUs,
@ -718,7 +714,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
boolean endOfStream = nextChunkHolder.endOfStream;
@Nullable Chunk loadable = nextChunkHolder.chunk;
@Nullable Uri playlistUrlToLoad = nextChunkHolder.playlistUrl;
nextChunkHolder.clear();
if (endOfStream) {
pendingResetPositionUs = C.TIME_UNSET;

View File

@ -15,6 +15,9 @@
*/
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 androidx.annotation.IntDef;
import androidx.annotation.Nullable;
@ -393,9 +396,13 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
*/
@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;
/** 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.
* Otherwise, contains the aggregated duration of removed segments up to this snapshot of the
@ -480,6 +487,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
String baseUri,
List<String> tags,
long startOffsetUs,
boolean preciseStart,
long startTimeUs,
boolean hasDiscontinuitySequence,
int discontinuitySequence,
@ -498,6 +506,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
super(baseUri, tags, hasIndependentSegments);
this.playlistType = playlistType;
this.startTimeUs = startTimeUs;
this.preciseStart = preciseStart;
this.hasDiscontinuitySequence = hasDiscontinuitySequence;
this.discontinuitySequence = discontinuitySequence;
this.mediaSequence = mediaSequence;
@ -519,8 +528,15 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
} else {
durationUs = 0;
}
this.startOffsetUs = startOffsetUs == C.TIME_UNSET ? C.TIME_UNSET
: startOffsetUs >= 0 ? startOffsetUs : durationUs + startOffsetUs;
// From RFC 8216, section 4.4.2.2: If startOffsetUs is negative, it indicates the offset from
// 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;
}
@ -575,6 +591,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
baseUri,
tags,
startOffsetUs,
preciseStart,
startTimeUs,
/* hasDiscontinuitySequence= */ true,
discontinuitySequence,
@ -605,6 +622,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
baseUri,
tags,
startOffsetUs,
preciseStart,
startTimeUs,
hasDiscontinuitySequence,
discontinuitySequence,

View File

@ -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_INDEPENDENT = compileBooleanAttrPattern("INDEPENDENT");
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_IMPORT = Pattern.compile("IMPORT=\"(.+?)\"");
private static final Pattern REGEX_VARIABLE_REFERENCE =
@ -643,6 +644,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
int relativeDiscontinuitySequence = 0;
long playlistStartTimeUs = 0;
long segmentStartTimeUs = 0;
boolean preciseStart = false;
long segmentByteRangeOffset = 0;
long segmentByteRangeLength = C.LENGTH_UNSET;
long partStartTimeUs = 0;
@ -685,6 +687,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
isIFrameOnly = true;
} else if (line.startsWith(TAG_START)) {
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)) {
serverControl = parseServerControl(line);
} else if (line.startsWith(TAG_PART_INF)) {
@ -1015,6 +1019,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
baseUri,
tags,
startOffsetUs,
preciseStart,
playlistStartTimeUs,
hasDiscontinuitySequence,
playlistDiscontinuitySequence,

View File

@ -152,7 +152,7 @@ public class HlsMediaSourceTest {
}
@Test
public void loadPlaylist_noTargetLiveOffsetDefined_fallbackToThreeTargetDuration()
public void loadLivePlaylist_noTargetLiveOffsetDefined_fallbackToThreeTargetDuration()
throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a duration of 16 seconds but not hold back or part hold back.
@ -188,7 +188,7 @@ public class HlsMediaSourceTest {
}
@Test
public void loadPlaylist_holdBackInPlaylist_targetLiveOffsetFromHoldBack()
public void loadLivePlaylist_holdBackInPlaylist_targetLiveOffsetFromHoldBack()
throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a duration of 16 seconds and a hold back of 12 seconds.
@ -225,7 +225,7 @@ public class HlsMediaSourceTest {
@Test
public void
loadPlaylist_partHoldBackWithoutPartInformationInPlaylist_targetLiveOffsetFromHoldBack()
loadLivePlaylist_partHoldBackWithoutPartInformationInPlaylist_targetLiveOffsetFromHoldBack()
throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a part hold back but not EXT-X-PART-INF. We should pick up the hold back.
@ -263,7 +263,7 @@ public class HlsMediaSourceTest {
@Test
public void
loadPlaylist_partHoldBackWithPartInformationInPlaylist_targetLiveOffsetFromPartHoldBack()
loadLivePlaylist_partHoldBackWithPartInformationInPlaylist_targetLiveOffsetFromPartHoldBack()
throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a duration of 4 seconds, part hold back and EXT-X-PART-INF defined.
@ -293,7 +293,44 @@ public class HlsMediaSourceTest {
}
@Test
public void loadPlaylist_withPlaylistStartTime_targetLiveOffsetFromStartTime()
public void loadLivePlaylist_withParts_defaultPositionPointsAtClosestIndependentPart()
throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a duration of 7 seconds, part hold back and EXT-X-PART-INF defined.
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-01T00:00:00.0+00:00\n"
+ "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-VERSION:3\n"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence0.ts\n"
+ "#EXT-X-SERVER-CONTROL:HOLD-BACK=12,PART-HOLD-BACK=2\n"
+ "#EXT-X-PART-INF:PART-TARGET=0.5\n"
+ "#EXT-X-PART:DURATION=0.5000,URI=\"fileSequence1.0.ts\",INDEPENDENT=YES\n"
+ "#EXT-X-PART:DURATION=0.5000,URI=\"fileSequence1.1.ts\"\n"
+ "#EXT-X-PART:DURATION=0.5000,URI=\"fileSequence1.2.ts\",INDEPENDENT=YES\n"
+ "#EXT-X-PART:DURATION=0.5000,URI=\"fileSequence1.3.ts\"\n"
+ "#EXT-X-PART:DURATION=0.5000,URI=\"fileSequence1.4.ts\",INDEPENDENT=YES\n"
+ "#EXT-X-PART:DURATION=0.5000,URI=\"fileSequence1.5.ts\"";
// The playlist finishes 1 second before the current time.
SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:08.0+00:00"));
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
MediaItem mediaItem = MediaItem.fromUri(playlistUri);
HlsMediaSource mediaSource = factory.createMediaSource(mediaItem);
Timeline timeline = prepareAndWaitForTimeline(mediaSource);
Timeline.Window window = timeline.getWindow(0, new Timeline.Window());
// The target live offset is picked from part hold back and then expressed in relation to the
// live edge (+1 seconds).
assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(3000);
// The default position points the closest preceding independent part.
assertThat(window.defaultPositionUs).isEqualTo(5000000);
}
@Test
public void loadLivePlaylist_withNonPreciseStartTime_targetLiveOffsetFromStartTime()
throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a duration of 16 seconds, and part hold back, hold back and start time
@ -303,7 +340,83 @@ public class HlsMediaSourceTest {
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-01T00:00:00.0+00:00\n"
+ "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-VERSION:3\n"
+ "#EXT-X-START:TIME-OFFSET=-15"
+ "#EXT-X-START:TIME-OFFSET=-10\n"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence0.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence1.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence2.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence3.ts\n"
+ "#EXT-X-SERVER-CONTROL:HOLD-BACK=12,PART-HOLD-BACK=3\n";
// The playlist finishes 1 second before the current time.
SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:17.0+00:00"));
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
MediaItem mediaItem = MediaItem.fromUri(playlistUri);
HlsMediaSource mediaSource = factory.createMediaSource(mediaItem);
Timeline timeline = prepareAndWaitForTimeline(mediaSource);
Timeline.Window window = timeline.getWindow(0, new Timeline.Window());
// The target live offset is picked from start time (16 - 10 = 6) and then expressed in relation
// to the live edge (17 - 6 = 11 seconds).
assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(11000);
// The default position points to the segment containing the start time.
assertThat(window.defaultPositionUs).isEqualTo(4000000);
}
@Test
public void
loadLivePlaylist_withNonPreciseStartTimeAndUserDefinedLiveOffset_startsFromPrecedingSegment()
throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a duration of 16 seconds, and part hold back, hold back and start time
// defined.
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-01T00:00:00.0+00:00\n"
+ "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-VERSION:3\n"
+ "#EXT-X-START:TIME-OFFSET=-10\n"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence0.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence1.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence2.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence3.ts\n"
+ "#EXT-X-SERVER-CONTROL:HOLD-BACK=12,PART-HOLD-BACK=3\n";
// The playlist finishes 1 second before the current time.
SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:17.0+00:00"));
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
MediaItem mediaItem =
new MediaItem.Builder().setUri(playlistUri).setLiveTargetOffsetMs(3000).build();
HlsMediaSource mediaSource = factory.createMediaSource(mediaItem);
Timeline timeline = prepareAndWaitForTimeline(mediaSource);
Timeline.Window window = timeline.getWindow(0, new Timeline.Window());
assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(3000);
// The default position points to the segment containing the start time.
assertThat(window.defaultPositionUs).isEqualTo(4000000);
}
@Test
public void loadLivePlaylist_withPreciseStartTime_targetLiveOffsetFromStartTime()
throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a duration of 16 seconds, and part hold back, hold back and start time
// defined.
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-01T00:00:00.0+00:00\n"
+ "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-VERSION:3\n"
+ "#EXT-X-START:TIME-OFFSET=-10,PRECISE=YES\n"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence0.ts\n"
@ -313,7 +426,6 @@ public class HlsMediaSourceTest {
+ "fileSequence2.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence3.ts\n"
+ "#EXT-X-PART-INF:PART-TARGET=0.5\n"
+ "#EXT-X-SERVER-CONTROL:HOLD-BACK=12,PART-HOLD-BACK=3";
// The playlist finishes 1 second before the current time.
SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:17.0+00:00"));
@ -324,14 +436,52 @@ public class HlsMediaSourceTest {
Timeline timeline = prepareAndWaitForTimeline(mediaSource);
Timeline.Window window = timeline.getWindow(0, new Timeline.Window());
// The target live offset is picked from start time and then expressed in relation to the live
// edge (+1 seconds).
assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(16000);
assertThat(window.defaultPositionUs).isEqualTo(0);
// The target live offset is picked from start time (16 - 10 = 6) and then expressed in relation
// to the live edge (17 - 7 = 11 seconds).
assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(11000);
// The default position points to the start time.
assertThat(window.defaultPositionUs).isEqualTo(6000000);
}
@Test
public void loadPlaylist_targetLiveOffsetInMediaItem_targetLiveOffsetPickedFromMediaItem()
public void loadLivePlaylist_withPreciseStartTimeAndUserDefinedLiveOffset_startsFromStartTime()
throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a duration of 16 seconds, and part hold back, hold back and start time
// defined.
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-01T00:00:00.0+00:00\n"
+ "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-VERSION:3\n"
+ "#EXT-X-START:TIME-OFFSET=-10,PRECISE=YES\n"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence0.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence1.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence2.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence3.ts\n"
+ "#EXT-X-SERVER-CONTROL:HOLD-BACK=12,PART-HOLD-BACK=3";
// The playlist finishes 1 second before the current time.
SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:17.0+00:00"));
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
MediaItem mediaItem =
new MediaItem.Builder().setUri(playlistUri).setLiveTargetOffsetMs(3000).build();
HlsMediaSource mediaSource = factory.createMediaSource(mediaItem);
Timeline timeline = prepareAndWaitForTimeline(mediaSource);
Timeline.Window window = timeline.getWindow(0, new Timeline.Window());
assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(3000);
// The default position points to the start time.
assertThat(window.defaultPositionUs).isEqualTo(6000000);
}
@Test
public void loadLivePlaylist_targetLiveOffsetInMediaItem_targetLiveOffsetPickedFromMediaItem()
throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a hold back of 12 seconds and a part hold back of 3 seconds.
@ -361,8 +511,9 @@ public class HlsMediaSourceTest {
}
@Test
public void loadPlaylist_targetLiveOffsetLargerThanLiveWindow_targetLiveOffsetIsWithinLiveWindow()
throws TimeoutException, ParserException {
public void
loadLivePlaylist_targetLiveOffsetLargerThanLiveWindow_targetLiveOffsetIsWithinLiveWindow()
throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a duration of 8 seconds and a hold back of 12 seconds.
String playlist =
@ -394,7 +545,7 @@ public class HlsMediaSourceTest {
@Test
public void
loadPlaylist_withoutProgramDateTime_targetLiveOffsetFromPlaylistNotAdjustedToLiveEdge()
loadLivePlaylist_withoutProgramDateTime_targetLiveOffsetFromPlaylistNotAdjustedToLiveEdge()
throws TimeoutException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a duration of 16 seconds and a hold back of 12 seconds.
@ -427,6 +578,119 @@ public class HlsMediaSourceTest {
assertThat(window.defaultPositionUs).isEqualTo(4000000);
}
@Test
public void loadOnDemandPlaylist_withPreciseStartTime_setsDefaultPosition()
throws TimeoutException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-TARGETDURATION:10\n"
+ "#EXT-X-VERSION:4\n"
+ "#EXT-X-START:TIME-OFFSET=15.000,PRECISE=YES"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXTINF:10.0,\n"
+ "fileSequence1.ts\n"
+ "#EXTINF:10.0,\n"
+ "fileSequence2.ts\n"
+ "#EXT-X-ENDLIST";
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
MediaItem mediaItem = new MediaItem.Builder().setUri(playlistUri).build();
HlsMediaSource mediaSource = factory.createMediaSource(mediaItem);
Timeline timeline = prepareAndWaitForTimeline(mediaSource);
Timeline.Window window = timeline.getWindow(0, new Timeline.Window());
// The target live offset is not adjusted to the live edge because the list does not have
// program date time.
assertThat(window.liveConfiguration).isNull();
assertThat(window.defaultPositionUs).isEqualTo(15000000);
}
@Test
public void loadOnDemandPlaylist_withNonPreciseStartTime_setsDefaultPosition()
throws TimeoutException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-TARGETDURATION:10\n"
+ "#EXT-X-VERSION:4\n"
+ "#EXT-X-START:TIME-OFFSET=15.000"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXTINF:10.0,\n"
+ "fileSequence1.ts\n"
+ "#EXTINF:10.0,\n"
+ "fileSequence2.ts\n"
+ "#EXT-X-ENDLIST";
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
MediaItem mediaItem = new MediaItem.Builder().setUri(playlistUri).build();
HlsMediaSource mediaSource = factory.createMediaSource(mediaItem);
Timeline timeline = prepareAndWaitForTimeline(mediaSource);
Timeline.Window window = timeline.getWindow(0, new Timeline.Window());
// The target live offset is not adjusted to the live edge because the list does not have
// program date time.
assertThat(window.liveConfiguration).isNull();
assertThat(window.defaultPositionUs).isEqualTo(10000000);
}
@Test
public void
loadOnDemandPlaylist_withStartTimeBeforeTheBeginning_setsDefaultPositionToTheBeginning()
throws TimeoutException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-TARGETDURATION:10\n"
+ "#EXT-X-VERSION:4\n"
+ "#EXT-X-START:TIME-OFFSET=-35.000"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXTINF:10.0,\n"
+ "fileSequence1.ts\n"
+ "#EXTINF:10.0,\n"
+ "fileSequence2.ts\n"
+ "#EXT-X-ENDLIST";
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
MediaItem mediaItem = new MediaItem.Builder().setUri(playlistUri).build();
HlsMediaSource mediaSource = factory.createMediaSource(mediaItem);
Timeline timeline = prepareAndWaitForTimeline(mediaSource);
Timeline.Window window = timeline.getWindow(0, new Timeline.Window());
assertThat(window.liveConfiguration).isNull();
assertThat(window.defaultPositionUs).isEqualTo(0);
}
@Test
public void loadOnDemandPlaylist_withStartTimeAfterTheNed_setsDefaultPositionToTheEnd()
throws TimeoutException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PLAYLIST-TYPE:VOD\n"
+ "#EXT-X-TARGETDURATION:10\n"
+ "#EXT-X-VERSION:4\n"
+ "#EXT-X-START:TIME-OFFSET=35.000"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXTINF:10.0,\n"
+ "fileSequence1.ts\n"
+ "#EXTINF:10.0,\n"
+ "fileSequence2.ts\n"
+ "#EXT-X-ENDLIST";
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
MediaItem mediaItem = new MediaItem.Builder().setUri(playlistUri).build();
HlsMediaSource mediaSource = factory.createMediaSource(mediaItem);
Timeline timeline = prepareAndWaitForTimeline(mediaSource);
Timeline.Window window = timeline.getWindow(0, new Timeline.Window());
assertThat(window.liveConfiguration).isNull();
assertThat(window.defaultPositionUs).isEqualTo(20000000);
}
@Test
public void refreshPlaylist_targetLiveOffsetRemainsInWindow()
throws TimeoutException, IOException {

View File

@ -15,7 +15,9 @@
*/
package com.google.android.exoplayer2.source.rtsp;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.rtsp.RtspMessageChannel.InterleavedBinaryDataListener;
import com.google.android.exoplayer2.upstream.DataSource;
import java.io.IOException;
@ -43,17 +45,9 @@ import java.io.IOException;
int getLocalPort();
/**
* Returns whether the data channel is using sideband binary data to transmit RTP packets. For
* example, RTP-over-RTSP.
* Returns a {@link InterleavedBinaryDataListener} if the implementation supports receiving RTP
* packets on a side-band protocol, for example RTP-over-RTSP; otherwise {@code null}.
*/
boolean usesSidebandBinaryData();
/**
* 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);
@Nullable
InterleavedBinaryDataListener getInterleavedBinaryDataListener();
}

View File

@ -0,0 +1,142 @@
/*
* 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);
}
}
}

View File

@ -32,24 +32,31 @@ 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.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
import static com.google.common.base.Strings.nullToEmpty;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.util.SparseArray;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.source.rtsp.RtspMediaPeriod.RtpLoadInfo;
import com.google.android.exoplayer2.source.rtsp.RtspMediaSource.RtspPlaybackException;
import com.google.android.exoplayer2.source.rtsp.RtspMessageChannel.InterleavedBinaryDataListener;
import com.google.android.exoplayer2.source.rtsp.RtspMessageUtil.RtspAuthUserInfo;
import com.google.android.exoplayer2.source.rtsp.RtspMessageUtil.RtspSessionHeader;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import java.io.Closeable;
import java.io.IOException;
import java.net.Socket;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.net.SocketFactory;
@ -89,19 +96,21 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
private final SessionInfoListener sessionInfoListener;
private final PlaybackEventListener playbackEventListener;
private final Uri uri;
@Nullable private final String userAgent;
@Nullable private final RtspAuthUserInfo rtspAuthUserInfo;
private final String userAgent;
private final ArrayDeque<RtpLoadInfo> pendingSetupRtpLoadInfos;
// TODO(b/172331505) Add a timeout monitor for pending requests.
private final SparseArray<RtspRequest> pendingRequests;
private final MessageSender messageSender;
private final SparseArray<RtpDataChannel> transferRtpDataChannelMap;
private RtspMessageChannel messageChannel;
private @MonotonicNonNull PlaybackEventListener playbackEventListener;
@Nullable private String sessionId;
@Nullable private KeepAliveMonitor keepAliveMonitor;
@Nullable private RtspAuthenticationInfo rtspAuthenticationInfo;
private boolean hasUpdatedTimelineAndTracks;
private boolean receivedAuthorizationRequest;
private long pendingSeekPositionUs;
/**
@ -114,18 +123,23 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* <p>Note: all method invocations must be made from the playback thread.
*
* @param sessionInfoListener The {@link SessionInfoListener}.
* @param userAgent The user agent that will be used if needed, or {@code null} for the fallback
* to use the default user agent of the underlying platform.
* @param playbackEventListener The {@link PlaybackEventListener}.
* @param userAgent The user agent.
* @param uri The RTSP playback URI.
*/
public RtspClient(SessionInfoListener sessionInfoListener, @Nullable String userAgent, Uri uri) {
public RtspClient(
SessionInfoListener sessionInfoListener,
PlaybackEventListener playbackEventListener,
String userAgent,
Uri uri) {
this.sessionInfoListener = sessionInfoListener;
this.playbackEventListener = playbackEventListener;
this.uri = RtspMessageUtil.removeUserInfo(uri);
this.rtspAuthUserInfo = RtspMessageUtil.parseUserInfo(uri);
this.userAgent = userAgent;
pendingSetupRtpLoadInfos = new ArrayDeque<>();
pendingRequests = new SparseArray<>();
messageSender = new MessageSender();
transferRtpDataChannelMap = new SparseArray<>();
pendingSeekPositionUs = C.TIME_UNSET;
messageChannel = new RtspMessageChannel(new MessageListener());
}
@ -140,7 +154,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*/
public void start() throws IOException {
try {
messageChannel.openSocket(openSocket());
messageChannel.open(getSocket(uri));
} catch (IOException e) {
Util.closeQuietly(messageChannel);
throw e;
@ -148,24 +162,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
messageSender.sendOptionsRequest(uri, sessionId);
}
/** Opens a {@link Socket} to the session {@link #uri}. */
private Socket openSocket() throws IOException {
checkArgument(uri.getHost() != null);
int rtspPort = uri.getPort() > 0 ? uri.getPort() : DEFAULT_RTSP_PORT;
return SocketFactory.getDefault().createSocket(checkNotNull(uri.getHost()), rtspPort);
}
/** Sets the {@link PlaybackEventListener} to receive playback events. */
public void setPlaybackEventListener(PlaybackEventListener playbackEventListener) {
this.playbackEventListener = playbackEventListener;
}
/**
* Triggers RTSP SETUP requests after track selection.
*
* <p>A {@link PlaybackEventListener} must be set via {@link #setPlaybackEventListener} before
* calling this method. All selected tracks (represented by {@link RtpLoadInfo}) must have valid
* transport.
* <p>All selected tracks (represented by {@link RtpLoadInfo}) must have valid transport.
*
* @param loadInfos A list of selected tracks represented by {@link RtpLoadInfo}.
*/
@ -217,27 +217,51 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
try {
close();
messageChannel = new RtspMessageChannel(new MessageListener());
messageChannel.openSocket(openSocket());
messageChannel.open(getSocket(uri));
sessionId = null;
receivedAuthorizationRequest = false;
rtspAuthenticationInfo = null;
} catch (IOException e) {
checkNotNull(playbackEventListener).onPlaybackError(new RtspPlaybackException(e));
playbackEventListener.onPlaybackError(new RtspPlaybackException(e));
}
}
/** Registers an {@link RtpDataChannel} to receive RTSP interleaved data. */
public void registerInterleavedDataChannel(RtpDataChannel rtpDataChannel) {
transferRtpDataChannelMap.put(rtpDataChannel.getLocalPort(), rtpDataChannel);
/** Registers an {@link InterleavedBinaryDataListener} to receive RTSP interleaved data. */
public void registerInterleavedDataChannel(
int channel, InterleavedBinaryDataListener interleavedBinaryDataListener) {
messageChannel.registerInterleavedBinaryDataListener(channel, interleavedBinaryDataListener);
}
private void continueSetupRtspTrack() {
@Nullable RtpLoadInfo loadInfo = pendingSetupRtpLoadInfos.pollFirst();
if (loadInfo == null) {
checkNotNull(playbackEventListener).onRtspSetupCompleted();
playbackEventListener.onRtspSetupCompleted();
return;
}
messageSender.sendSetupRequest(loadInfo.getTrackUri(), loadInfo.getTransport(), sessionId);
}
/** Returns a {@link Socket} that is connected to the {@code uri}. */
private static Socket getSocket(Uri uri) throws IOException {
checkArgument(uri.getHost() != null);
int rtspPort = uri.getPort() > 0 ? uri.getPort() : DEFAULT_RTSP_PORT;
return SocketFactory.getDefault().createSocket(checkNotNull(uri.getHost()), rtspPort);
}
private void dispatchRtspError(Throwable error) {
RtspPlaybackException playbackException =
error instanceof RtspPlaybackException
? (RtspPlaybackException) error
: new RtspPlaybackException(error);
if (hasUpdatedTimelineAndTracks) {
// Playback event listener must be non-null after timeline has been updated.
playbackEventListener.onPlaybackError(playbackException);
} else {
sessionInfoListener.onSessionTimelineRequestFailed(nullToEmpty(error.getMessage()), error);
}
}
/**
* Returns whether the RTSP server supports the DESCRIBE method.
*
@ -273,6 +297,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final class MessageSender {
private int cSeq;
private @MonotonicNonNull RtspRequest lastRequest;
public void sendOptionsRequest(Uri uri, @Nullable String sessionId) {
sendRequest(
@ -317,6 +342,28 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
METHOD_PAUSE, sessionId, /* additionalHeaders= */ ImmutableMap.of(), uri));
}
public void retryLastRequest() {
checkStateNotNull(lastRequest);
Multimap<String, String> headersMultiMap = lastRequest.headers.asMultiMap();
Map<String, String> lastRequestHeaders = new HashMap<>();
for (String headerName : headersMultiMap.keySet()) {
if (headerName.equals(RtspHeaders.CSEQ)
|| headerName.equals(RtspHeaders.USER_AGENT)
|| headerName.equals(RtspHeaders.SESSION)
|| headerName.equals(RtspHeaders.AUTHORIZATION)) {
// Clear session-specific header values.
continue;
}
// Only include the header value that is written most recently.
lastRequestHeaders.put(headerName, Iterables.getLast(headersMultiMap.get(headerName)));
}
sendRequest(
getRequestWithCommonHeaders(
lastRequest.method, sessionId, lastRequestHeaders, lastRequest.uri));
}
private RtspRequest getRequestWithCommonHeaders(
@RtspRequest.Method int method,
@Nullable String sessionId,
@ -324,15 +371,23 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
Uri uri) {
RtspHeaders.Builder headersBuilder = new RtspHeaders.Builder();
headersBuilder.add(RtspHeaders.CSEQ, String.valueOf(cSeq++));
if (userAgent != null) {
headersBuilder.add(RtspHeaders.USER_AGENT, userAgent);
}
headersBuilder.add(RtspHeaders.USER_AGENT, userAgent);
if (sessionId != null) {
headersBuilder.add(RtspHeaders.SESSION, sessionId);
}
if (rtspAuthenticationInfo != null) {
checkStateNotNull(rtspAuthUserInfo);
try {
headersBuilder.add(
RtspHeaders.AUTHORIZATION,
rtspAuthenticationInfo.getAuthorizationHeaderValue(rtspAuthUserInfo, uri, method));
} catch (ParserException e) {
dispatchRtspError(new RtspPlaybackException(e));
}
}
headersBuilder.addAll(additionalHeaders);
return new RtspRequest(uri, method, headersBuilder.build(), /* messageBody= */ "");
}
@ -342,13 +397,30 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
checkState(pendingRequests.get(cSeq) == null);
pendingRequests.append(cSeq, request);
messageChannel.send(RtspMessageUtil.serializeRequest(request));
lastRequest = request;
}
}
private final class MessageListener implements RtspMessageChannel.MessageListener {
private final Handler messageHandler;
/**
* Creates a new instance.
*
* <p>The constructor must be called on a {@link Looper} thread, on which all the received RTSP
* messages are processed.
*/
public MessageListener() {
messageHandler = Util.createHandlerForCurrentLooper();
}
@Override
public void onRtspMessageReceived(List<String> message) {
messageHandler.post(() -> handleRtspMessage(message));
}
private void handleRtspMessage(List<String> message) {
RtspResponse response = RtspMessageUtil.parseResponse(message);
int cSeq = Integer.parseInt(checkNotNull(response.headers.get(RtspHeaders.CSEQ)));
@ -362,14 +434,33 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@RtspRequest.Method int requestMethod = matchingRequest.method;
if (response.status != 200) {
dispatchRtspError(
new RtspPlaybackException(
RtspMessageUtil.toMethodString(requestMethod) + " " + response.status));
return;
}
try {
switch (response.status) {
case 200:
break;
case 401:
if (rtspAuthUserInfo != null && !receivedAuthorizationRequest) {
// Unauthorized.
@Nullable
String wwwAuthenticateHeader = response.headers.get(RtspHeaders.WWW_AUTHENTICATE);
if (wwwAuthenticateHeader == null) {
throw new ParserException("Missing WWW-Authenticate header in a 401 response.");
}
rtspAuthenticationInfo =
RtspMessageUtil.parseWwwAuthenticateHeader(wwwAuthenticateHeader);
messageSender.retryLastRequest();
receivedAuthorizationRequest = true;
return;
}
// fall through: if unauthorized and no userInfo present, or previous authentication
// unsuccessful.
default:
dispatchRtspError(
new RtspPlaybackException(
RtspMessageUtil.toMethodString(requestMethod) + " " + response.status));
return;
}
switch (requestMethod) {
case METHOD_OPTIONS:
onOptionsResponseReceived(
@ -412,24 +503,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
onPlayResponseReceived(new RtspPlayResponse(response.status, timing, trackTimingList));
break;
case METHOD_GET_PARAMETER:
onGetParameterResponseReceived(response);
break;
case METHOD_TEARDOWN:
onTeardownResponseReceived(response);
break;
case METHOD_PAUSE:
onPauseResponseReceived(response);
onPauseResponseReceived();
break;
case METHOD_GET_PARAMETER:
case METHOD_TEARDOWN:
case METHOD_PLAY_NOTIFY:
case METHOD_RECORD:
case METHOD_REDIRECT:
case METHOD_ANNOUNCE:
case METHOD_SET_PARAMETER:
onUnsupportedResponseReceived(response);
break;
case METHOD_UNSET:
default:
@ -440,17 +524,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
}
@Override
public void onInterleavedBinaryDataReceived(byte[] data, int channel) {
@Nullable RtpDataChannel dataChannel = transferRtpDataChannelMap.get(channel);
if (dataChannel != null) {
dataChannel.write(data);
}
}
// Response handlers must only be called only on 200 (OK) responses.
public void onOptionsResponseReceived(RtspOptionsResponse response) {
private void onOptionsResponseReceived(RtspOptionsResponse response) {
if (keepAliveMonitor != null) {
// Ignores the OPTIONS requests that are sent to keep RTSP connection alive.
return;
@ -464,7 +540,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
}
public void onDescribeResponseReceived(RtspDescribeResponse response) {
private void onDescribeResponseReceived(RtspDescribeResponse response) {
@Nullable
String sessionRangeAttributeString =
response.sessionDescription.attributes.get(SessionDescription.ATTR_RANGE);
@ -481,54 +557,27 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
}
public void onSetupResponseReceived(RtspSetupResponse response) {
private void onSetupResponseReceived(RtspSetupResponse response) {
sessionId = response.sessionHeader.sessionId;
continueSetupRtspTrack();
}
public void onPlayResponseReceived(RtspPlayResponse response) {
private void onPlayResponseReceived(RtspPlayResponse response) {
if (keepAliveMonitor == null) {
keepAliveMonitor = new KeepAliveMonitor(DEFAULT_RTSP_KEEP_ALIVE_INTERVAL_MS);
keepAliveMonitor.start();
}
checkNotNull(playbackEventListener)
.onPlaybackStarted(
C.msToUs(response.sessionTiming.startTimeMs), response.trackTimingList);
playbackEventListener.onPlaybackStarted(
C.msToUs(response.sessionTiming.startTimeMs), response.trackTimingList);
pendingSeekPositionUs = C.TIME_UNSET;
}
public void onPauseResponseReceived(RtspResponse response) {
private void onPauseResponseReceived() {
if (pendingSeekPositionUs != C.TIME_UNSET) {
startPlayback(C.usToMs(pendingSeekPositionUs));
}
}
public void onGetParameterResponseReceived(RtspResponse response) {
// Do nothing.
}
public void onTeardownResponseReceived(RtspResponse response) {
// Do nothing.
}
public void onUnsupportedResponseReceived(RtspResponse response) {
// Do nothing.
}
private void dispatchRtspError(Throwable error) {
RtspPlaybackException playbackException =
error instanceof RtspPlaybackException
? (RtspPlaybackException) error
: new RtspPlaybackException(error);
if (hasUpdatedTimelineAndTracks) {
// Playback event listener must be non-null after timeline has been updated.
checkNotNull(playbackEventListener).onPlaybackError(playbackException);
} else {
sessionInfoListener.onSessionTimelineRequestFailed(nullToEmpty(error.getMessage()), error);
}
}
}
/** Sends periodic OPTIONS requests to keep RTSP connection alive. */

View File

@ -20,9 +20,8 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Iterables;
import java.util.List;
import java.util.Map;
@ -35,45 +34,45 @@ import java.util.Map;
*/
/* package */ final class RtspHeaders {
public static final String ACCEPT = "Accept";
public static final String ALLOW = "Allow";
public static final String AUTHORIZATION = "Authorization";
public static final String BANDWIDTH = "Bandwidth";
public static final String BLOCKSIZE = "Blocksize";
public static final String CACHE_CONTROL = "Cache-Control";
public static final String CONNECTION = "Connection";
public static final String CONTENT_BASE = "Content-Base";
public static final String CONTENT_ENCODING = "Content-Encoding";
public static final String CONTENT_LANGUAGE = "Content-Language";
public static final String CONTENT_LENGTH = "Content-Length";
public static final String CONTENT_LOCATION = "Content-Location";
public static final String CONTENT_TYPE = "Content-Type";
public static final String CSEQ = "CSeq";
public static final String DATE = "Date";
public static final String EXPIRES = "Expires";
public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate";
public static final String PROXY_REQUIRE = "Proxy-Require";
public static final String PUBLIC = "Public";
public static final String RANGE = "Range";
public static final String RTP_INFO = "RTP-Info";
public static final String RTCP_INTERVAL = "RTCP-Interval";
public static final String SCALE = "Scale";
public static final String SESSION = "Session";
public static final String SPEED = "Speed";
public static final String SUPPORTED = "Supported";
public static final String TIMESTAMP = "Timestamp";
public static final String TRANSPORT = "Transport";
public static final String USER_AGENT = "User-Agent";
public static final String VIA = "Via";
public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
public static final String ACCEPT = "accept";
public static final String ALLOW = "allow";
public static final String AUTHORIZATION = "authorization";
public static final String BANDWIDTH = "bandwidth";
public static final String BLOCKSIZE = "blocksize";
public static final String CACHE_CONTROL = "cache-control";
public static final String CONNECTION = "connection";
public static final String CONTENT_BASE = "content-base";
public static final String CONTENT_ENCODING = "content-encoding";
public static final String CONTENT_LANGUAGE = "content-language";
public static final String CONTENT_LENGTH = "content-length";
public static final String CONTENT_LOCATION = "content-location";
public static final String CONTENT_TYPE = "content-type";
public static final String CSEQ = "cseq";
public static final String DATE = "date";
public static final String EXPIRES = "expires";
public static final String PROXY_AUTHENTICATE = "proxy-authenticate";
public static final String PROXY_REQUIRE = "proxy-require";
public static final String PUBLIC = "public";
public static final String RANGE = "range";
public static final String RTP_INFO = "rtp-info";
public static final String RTCP_INTERVAL = "rtcp-interval";
public static final String SCALE = "scale";
public static final String SESSION = "session";
public static final String SPEED = "speed";
public static final String SUPPORTED = "supported";
public static final String TIMESTAMP = "timestamp";
public static final String TRANSPORT = "transport";
public static final String USER_AGENT = "user-agent";
public static final String VIA = "via";
public static final String WWW_AUTHENTICATE = "www-authenticate";
/** Builds {@link RtspHeaders} instances. */
public static final class Builder {
private final List<String> namesAndValues;
private final ImmutableListMultimap.Builder<String, String> namesAndValuesBuilder;
/** Creates a new instance. */
public Builder() {
namesAndValues = new ArrayList<>();
namesAndValuesBuilder = new ImmutableListMultimap.Builder<>();
}
/**
@ -84,8 +83,7 @@ import java.util.Map;
* @return This builder.
*/
public Builder add(String headerName, String headerValue) {
namesAndValues.add(headerName.trim());
namesAndValues.add(headerValue.trim());
namesAndValuesBuilder.put(Ascii.toLowerCase(headerName.trim()), headerValue.trim());
return this;
}
@ -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
* values.
*
* @return The headers as a map. The keys of the map have follows those that are used to build
* this {@link RtspHeaders} instance.
* Returns a map that associates header names to the list of values associated with the
* corresponding header name.
*/
public ImmutableMap<String, String> asMap() {
Map<String, String> headers = new LinkedHashMap<>();
for (int i = 0; i < namesAndValues.size(); i += 2) {
headers.put(namesAndValues.get(i), namesAndValues.get(i + 1));
}
return ImmutableMap.copyOf(headers);
public ImmutableListMultimap<String, String> asMultiMap() {
return namesAndValues;
}
/**
* 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
public String get(String headerName) {
for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) {
if (Ascii.equalsIgnoreCase(headerName, namesAndValues.get(i))) {
return namesAndValues.get(i + 1);
}
ImmutableList<String> headerValues = values(headerName);
if (headerValues.isEmpty()) {
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) {
this.namesAndValues = ImmutableList.copyOf(builder.namesAndValues);
this.namesAndValues = builder.namesAndValuesBuilder.build();
}
}

View File

@ -42,6 +42,7 @@ import com.google.android.exoplayer2.source.SampleStream.ReadFlags;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
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.RtspMediaSource.RtspPlaybackException;
import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelection;
@ -62,57 +63,68 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** A {@link MediaPeriod} that loads an RTSP stream. */
/* package */ final class RtspMediaPeriod implements MediaPeriod {
/** Listener for information about the period. */
interface Listener {
/** Called when the {@link RtspSessionTiming} is available. */
void onSourceInfoRefreshed(RtspSessionTiming timing);
}
/** The maximum times to retry if the underlying data channel failed to bind. */
private static final int PORT_BINDING_MAX_RETRY_COUNT = 3;
private final Allocator allocator;
private final Handler handler;
private final InternalListener internalListener;
private final RtspClient rtspClient;
private final List<RtspLoaderWrapper> rtspLoaderWrappers;
private final List<RtpLoadInfo> selectedLoadInfos;
private final Listener listener;
private final RtpDataChannel.Factory rtpDataChannelFactory;
private @MonotonicNonNull Callback callback;
private @MonotonicNonNull ImmutableList<TrackGroup> trackGroups;
@Nullable private IOException preparationError;
@Nullable private RtspPlaybackException playbackException;
private long lastSeekPositionUs;
private long pendingSeekPositionUs;
private boolean loadingFinished;
private boolean released;
private boolean prepared;
private boolean trackSelected;
private int portBindingRetryCount;
private boolean hasRetriedWithRtpTcp;
private boolean isUsingRtpTcp;
/**
* Creates an RTSP media period.
*
* @param allocator An {@link Allocator} from which to obtain media buffer allocations.
* @param rtspTracks A list of tracks in an RTSP playback session.
* @param rtspClient The {@link RtspClient} for the current RTSP playback.
* @param rtpDataChannelFactory A {@link RtpDataChannel.Factory} for {@link RtpDataChannel}.
* @param uri The RTSP playback {@link Uri}.
* @param listener A {@link Listener} to receive session information updates.
*/
public RtspMediaPeriod(
Allocator allocator,
List<RtspMediaTrack> rtspTracks,
RtspClient rtspClient,
RtpDataChannel.Factory rtpDataChannelFactory) {
RtpDataChannel.Factory rtpDataChannelFactory,
Uri uri,
Listener listener,
String userAgent) {
this.allocator = allocator;
this.rtpDataChannelFactory = rtpDataChannelFactory;
this.listener = listener;
handler = Util.createHandlerForCurrentLooper();
internalListener = new InternalListener();
rtspLoaderWrappers = new ArrayList<>(rtspTracks.size());
this.rtspClient = rtspClient;
this.rtspClient.setPlaybackEventListener(internalListener);
rtspClient =
new RtspClient(
/* sessionInfoListener= */ internalListener,
/* playbackEventListener= */ internalListener,
/* userAgent= */ userAgent,
/* uri= */ uri);
rtspLoaderWrappers = new ArrayList<>();
selectedLoadInfos = new ArrayList<>();
for (int i = 0; i < rtspTracks.size(); i++) {
RtspMediaTrack rtspMediaTrack = rtspTracks.get(i);
rtspLoaderWrappers.add(
new RtspLoaderWrapper(rtspMediaTrack, /* trackId= */ i, rtpDataChannelFactory));
}
selectedLoadInfos = new ArrayList<>(rtspTracks.size());
pendingSeekPositionUs = C.TIME_UNSET;
}
@ -121,6 +133,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
rtspLoaderWrappers.get(i).release();
}
Util.closeQuietly(rtspClient);
released = true;
}
@ -128,8 +141,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public void prepare(Callback callback, long positionUs) {
this.callback = callback;
for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
rtspLoaderWrappers.get(i).startLoading();
try {
rtspClient.start();
} catch (IOException e) {
preparationError = e;
Util.closeQuietly(rtspClient);
}
}
@ -233,6 +249,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return positionUs;
}
lastSeekPositionUs = positionUs;
pendingSeekPositionUs = positionUs;
rtspClient.seekToUs(positionUs);
for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
@ -256,14 +273,19 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return pendingSeekPositionUs;
}
long bufferedPositionUs = rtspLoaderWrappers.get(0).sampleQueue.getLargestQueuedTimestampUs();
for (int i = 1; i < rtspLoaderWrappers.size(); i++) {
bufferedPositionUs =
min(
bufferedPositionUs,
checkNotNull(rtspLoaderWrappers.get(i)).sampleQueue.getLargestQueuedTimestampUs());
boolean allLoaderWrappersAreCanceled = true;
long bufferedPositionUs = Long.MAX_VALUE;
for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
RtspLoaderWrapper loaderWrapper = rtspLoaderWrappers.get(i);
if (!loaderWrapper.canceled) {
bufferedPositionUs = min(bufferedPositionUs, loaderWrapper.getBufferedPositionUs());
allLoaderWrappersAreCanceled = false;
}
}
return bufferedPositionUs;
return allLoaderWrappersAreCanceled || bufferedPositionUs == Long.MIN_VALUE
? lastSeekPositionUs
: bufferedPositionUs;
}
@Override
@ -306,9 +328,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Nullable
private RtpDataLoadable getLoadableByTrackUri(Uri trackUri) {
for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
RtpLoadInfo loadInfo = rtspLoaderWrappers.get(i).loadInfo;
if (loadInfo.getTrackUri().equals(trackUri)) {
return loadInfo.loadable;
if (!rtspLoaderWrappers.get(i).canceled) {
RtpLoadInfo loadInfo = rtspLoaderWrappers.get(i).loadInfo;
if (loadInfo.getTrackUri().equals(trackUri)) {
return loadInfo.loadable;
}
}
}
return null;
@ -384,6 +408,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
implements ExtractorOutput,
Loader.Callback<RtpDataLoadable>,
UpstreamFormatChangedListener,
SessionInfoListener,
PlaybackEventListener {
// ExtractorOutput implementation.
@ -513,13 +538,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Handles the {@link Loadable} whose {@link RtpDataChannel} timed out. */
private LoadErrorAction handleSocketTimeout(RtpDataLoadable loadable) {
// TODO(b/172331505) Allow for retry when loading is not ending.
if (getBufferedPositionUs() == Long.MIN_VALUE) {
// Retry playback with TCP if no sample has been received so far.
if (!hasRetriedWithRtpTcp) {
if (getBufferedPositionUs() == 0) {
if (!isUsingRtpTcp) {
// Retry playback with TCP if no sample has been received so far, and we are not already
// using TCP. Retrying will setup new loadables, so will not retry with the current
// loadables.
retryWithRtpTcp();
hasRetriedWithRtpTcp = true;
isUsingRtpTcp = true;
}
// Don't retry with the current UDP backed loadables.
return Loader.DONT_RETRY;
}
@ -530,9 +556,27 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
break;
}
}
playbackException = new RtspPlaybackException("Unknown loadable timed out.");
return Loader.DONT_RETRY;
}
@Override
public void onSessionTimelineUpdated(
RtspSessionTiming timing, ImmutableList<RtspMediaTrack> tracks) {
for (int i = 0; i < tracks.size(); i++) {
RtspMediaTrack rtspMediaTrack = tracks.get(i);
RtspLoaderWrapper loaderWrapper =
new RtspLoaderWrapper(rtspMediaTrack, /* trackId= */ i, rtpDataChannelFactory);
loaderWrapper.startLoading();
rtspLoaderWrappers.add(loaderWrapper);
}
listener.onSourceInfoRefreshed(timing);
}
@Override
public void onSessionTimelineRequestFailed(String message, @Nullable Throwable cause) {
preparationError = cause == null ? new IOException(message) : new IOException(message, cause);
}
}
private void retryWithRtpTcp() {
@ -542,17 +586,21 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
ArrayList<RtspLoaderWrapper> newLoaderWrappers = new ArrayList<>(rtspLoaderWrappers.size());
ArrayList<RtpLoadInfo> newSelectedLoadInfos = new ArrayList<>(selectedLoadInfos.size());
// newLoaderWrappers' elements and orders must match those of rtspLoaderWrappers'.
for (int i = 0; i < rtspLoaderWrappers.size(); i++) {
RtspLoaderWrapper loaderWrapper = rtspLoaderWrappers.get(i);
RtspLoaderWrapper newLoaderWrapper =
new RtspLoaderWrapper(
loaderWrapper.loadInfo.mediaTrack, /* trackId= */ i, rtpDataChannelFactory);
newLoaderWrappers.add(newLoaderWrapper);
newLoaderWrapper.startLoading();
if (selectedLoadInfos.contains(loaderWrapper.loadInfo)) {
newSelectedLoadInfos.add(newLoaderWrapper.loadInfo);
if (!loaderWrapper.canceled) {
RtspLoaderWrapper newLoaderWrapper =
new RtspLoaderWrapper(
loaderWrapper.loadInfo.mediaTrack, /* trackId= */ i, rtpDataChannelFactory);
newLoaderWrappers.add(newLoaderWrapper);
newLoaderWrapper.startLoading();
if (selectedLoadInfos.contains(loaderWrapper.loadInfo)) {
newSelectedLoadInfos.add(newLoaderWrapper.loadInfo);
}
} else {
newLoaderWrappers.add(loaderWrapper);
}
}
@ -625,6 +673,14 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
sampleQueue.setUpstreamFormatChangeListener(internalListener);
}
/**
* Returns the largest buffered position in microseconds; or {@link Long#MIN_VALUE} if no sample
* has been queued.
*/
public long getBufferedPositionUs() {
return sampleQueue.getLargestQueuedTimestampUs();
}
/** Starts loading. */
public void startLoading() {
loader.startLoading(
@ -643,21 +699,22 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Cancels loading. */
public void cancelLoad() {
if (canceled) {
return;
}
loadInfo.loadable.cancelLoad();
canceled = true;
if (!canceled) {
loadInfo.loadable.cancelLoad();
canceled = true;
// Update loadingFinished every time loading is canceled.
updateLoadingFinished();
// Update loadingFinished every time loading is canceled.
updateLoadingFinished();
}
}
/** Resets the {@link Loadable} and {@link SampleQueue} to prepare for an RTSP seek. */
public void seekTo(long positionUs) {
loadInfo.loadable.resetForSeek();
sampleQueue.reset();
sampleQueue.setStartTimeUs(positionUs);
if (!canceled) {
loadInfo.loadable.resetForSeek();
sampleQueue.reset();
sampleQueue.setStartTimeUs(positionUs);
}
}
/** Releases the instance. */
@ -685,14 +742,19 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
RtspMediaTrack mediaTrack, int trackId, RtpDataChannel.Factory rtpDataChannelFactory) {
this.mediaTrack = mediaTrack;
// This listener runs on the playback thread, posted by the Loader thread.
RtpDataLoadable.EventListener transportEventListener =
(transport, rtpDataChannel) -> {
RtpLoadInfo.this.transport = transport;
if (rtpDataChannel.usesSidebandBinaryData()) {
rtspClient.registerInterleavedDataChannel(rtpDataChannel);
@Nullable
RtspMessageChannel.InterleavedBinaryDataListener interleavedBinaryDataListener =
rtpDataChannel.getInterleavedBinaryDataListener();
if (interleavedBinaryDataListener != null) {
rtspClient.registerInterleavedDataChannel(
rtpDataChannel.getLocalPort(), interleavedBinaryDataListener);
isUsingRtpTcp = true;
}
maybeSetupTracks();
};

View File

@ -16,33 +16,35 @@
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.Util.castNonNull;
import android.net.Uri;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
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.DrmSessionManagerProvider;
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.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceFactory;
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.HttpDataSource;
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
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 org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** An Rtsp {@link MediaSource} */
public final class RtspMediaSource extends BaseMediaSource {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.rtsp");
}
/**
* Factory for {@link RtspMediaSource}
*
@ -58,6 +60,40 @@ public final class RtspMediaSource extends BaseMediaSource {
*/
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. */
@Override
public Factory setDrmSessionManagerProvider(
@ -122,7 +158,12 @@ public final class RtspMediaSource extends BaseMediaSource {
@Override
public RtspMediaSource createMediaSource(MediaItem mediaItem) {
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 {
private final MediaItem mediaItem;
private final RtpDataChannel.Factory rtpDataChannelFactory;
private @MonotonicNonNull RtspClient rtspClient;
private final String userAgent;
private final Uri uri;
@Nullable private ImmutableList<RtspMediaTrack> rtspMediaTracks;
@Nullable private IOException sourcePrepareException;
private long timelineDurationUs;
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;
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
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
checkNotNull(mediaItem.playbackProperties);
try {
rtspClient =
new RtspClient(
new SessionInfoListenerImpl(),
/* userAgent= */ VERSION_SLASHY,
mediaItem.playbackProperties.uri);
rtspClient.start();
} catch (IOException e) {
sourcePrepareException = new RtspPlaybackException("RtspClient not opened.", e);
}
notifySourceInfoRefreshed();
}
@Override
protected void releaseSourceInternal() {
Util.closeQuietly(rtspClient);
// Do nothing.
}
@Override
@ -179,16 +218,24 @@ public final class RtspMediaSource extends BaseMediaSource {
}
@Override
public void maybeThrowSourceInfoRefreshError() throws IOException {
if (sourcePrepareException != null) {
throw sourcePrepareException;
}
public void maybeThrowSourceInfoRefreshError() {
// Do nothing.
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
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
@ -196,28 +243,36 @@ public final class RtspMediaSource extends BaseMediaSource {
((RtspMediaPeriod) mediaPeriod).release();
}
private final class SessionInfoListenerImpl implements SessionInfoListener {
@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));
}
// Internal methods.
@Override
public void onSessionTimelineRequestFailed(String message, @Nullable Throwable cause) {
if (cause == null) {
sourcePrepareException = new RtspPlaybackException(message);
} else {
sourcePrepareException = new RtspPlaybackException(message, castNonNull(cause));
}
private void notifySourceInfoRefreshed() {
Timeline timeline =
new SinglePeriodTimeline(
timelineDurationUs,
timelineIsSeekable,
/* 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);
}
}

View File

@ -31,6 +31,7 @@ import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
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.NalUnitUtil;
import com.google.android.exoplayer2.util.Util;
@ -171,10 +172,6 @@ import com.google.common.collect.ImmutableMap;
private static void processH264FmtpAttribute(
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));
String spropParameterSets = checkNotNull(fmtpAttributes.get(PARAMETER_SPROP_PARAMS));
String[] parameterSets = Util.split(spropParameterSets, ",");
@ -193,6 +190,15 @@ import com.google.common.collect.ImmutableMap;
formatBuilder.setPixelWidthHeightRatio(spsData.pixelWidthAspectRatio);
formatBuilder.setHeight(spsData.height);
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) {

View File

@ -17,11 +17,11 @@ package com.google.android.exoplayer2.source.rtsp;
import static com.google.android.exoplayer2.source.rtsp.RtspMessageUtil.isRtspStartLine;
import static com.google.android.exoplayer2.util.Assertions.checkArgument;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
@ -29,10 +29,10 @@ import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.upstream.Loader;
import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction;
import com.google.android.exoplayer2.upstream.Loader.Loadable;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Ascii;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.DataInputStream;
@ -40,13 +40,20 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Sends and receives RTSP messages. */
/* package */ final class RtspMessageChannel implements Closeable {
/** RTSP uses UTF-8 (RFC2326 Section 1.1). */
public static final Charset CHARSET = Charsets.UTF_8;
private static final String TAG = "RtspMessageChannel";
/** A listener for received RTSP messages and possible failures. */
@ -59,15 +66,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*/
void onRtspMessageReceived(List<String> message);
/**
* Called when interleaved binary data is received on RTSP.
*
* @param data The received binary data. The byte array will not be reused by {@link
* RtspMessageChannel}, and will always be full.
* @param channel The channel on which the data is received.
*/
default void onInterleavedBinaryDataReceived(byte[] data, int channel) {}
/**
* Called when failed to send an RTSP message.
*
@ -84,20 +82,27 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
default void onReceivingFailed(Exception e) {}
}
/** A listener for received interleaved binary data from RTSP. */
public interface InterleavedBinaryDataListener {
/**
* Called when interleaved binary data is received on RTSP.
*
* @param data The received binary data. The byte array will not be reused by {@link
* RtspMessageChannel}, and will always be full.
*/
void onInterleavedBinaryDataReceived(byte[] data);
}
/**
* The IANA-registered default port for RTSP. See <a
* href="https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml">here</a>
*/
public static final int DEFAULT_RTSP_PORT = 554;
/**
* The handler for all {@code messageListener} interactions. Backed by the thread on which this
* class is constructed.
*/
private final Handler messageListenerHandler;
private final MessageListener messageListener;
private final Loader receiverLoader;
private final Map<Integer, InterleavedBinaryDataListener> interleavedBinaryDataListeners;
private @MonotonicNonNull Sender sender;
private @MonotonicNonNull Socket socket;
@ -106,19 +111,20 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Constructs a new instance.
*
* <p>The constructor must be called on a {@link Looper} thread. The thread is also where {@link
* MessageListener} events are sent. User must construct a socket for RTSP and call {@link
* #openSocket} to open the connection before being able to send and receive, and {@link #close}
* it when done.
* <p>A connected {@link Socket} must be provided in {@link #open} in order to send and receive
* RTSP messages. {@link #close} must be called when done, which would also close the socket.
*
* <p>{@link MessageListener} and {@link InterleavedBinaryDataListener} implementations must not
* make assumptions about which thread called their listener methods; and must be thread-safe.
*
* <p>Note: all method invocations must be made from the thread on which this class is created.
*
* @param messageListener The {@link MessageListener} to receive events.
*/
public RtspMessageChannel(MessageListener messageListener) {
this.messageListenerHandler = Util.createHandlerForCurrentLooper();
this.messageListener = messageListener;
this.receiverLoader = new Loader("ExoPlayer:RtspMessageChannel:ReceiverLoader");
this.interleavedBinaryDataListeners = Collections.synchronizedMap(new HashMap<>());
}
/**
@ -127,9 +133,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* <p>Note: If an {@link IOException} is thrown, callers must still call {@link #close()} to
* ensure that any partial effects of the invocation are cleaned up.
*
* @param socket An accepted {@link Socket}.
* @param socket A connected {@link Socket}.
*/
public void openSocket(Socket socket) throws IOException {
public void open(Socket socket) throws IOException {
this.socket = socket;
sender = new Sender(socket.getOutputStream());
@ -159,7 +165,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
sender.close();
}
receiverLoader.release();
messageListenerHandler.removeCallbacksAndMessages(/* token= */ null);
if (socket != null) {
socket.close();
@ -179,6 +184,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
sender.send(message);
}
/**
* Registers an {@link InterleavedBinaryDataListener} to receive RTSP interleaved data.
*
* <p>The listener method {@link InterleavedBinaryDataListener#onInterleavedBinaryDataReceived} is
* called on {@link RtspMessageChannel}'s internal thread for receiving RTSP messages.
*/
public void registerInterleavedBinaryDataListener(
int channel, InterleavedBinaryDataListener listener) {
interleavedBinaryDataListeners.put(channel, listener);
}
private final class Sender implements Closeable {
private final OutputStream outputStream;
@ -214,12 +230,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
try {
outputStream.write(data);
} catch (Exception e) {
messageListenerHandler.post(
() -> {
if (!closed) {
messageListener.onSendingFailed(message, e);
}
});
if (!closed) {
messageListener.onSendingFailed(message, e);
}
}
});
}
@ -240,10 +253,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final class Receiver implements Loadable {
/** ASCII dollar encapsulates the RTP packets in interleaved mode (RFC2326 Section 10.12). */
private static final byte RTSP_INTERLEAVED_MESSAGE_MARKER = '$';
private static final byte INTERLEAVED_MESSAGE_MARKER = '$';
private final DataInputStream dataInputStream;
private final RtspMessageBuilder messageBuilder;
private final MessageParser messageParser;
private volatile boolean loadCanceled;
/**
@ -255,7 +268,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*/
public Receiver(InputStream inputStream) {
dataInputStream = new DataInputStream(inputStream);
messageBuilder = new RtspMessageBuilder();
messageParser = new MessageParser();
}
@Override
@ -268,7 +281,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
while (!loadCanceled) {
// TODO(internal b/172331505) Use a buffered read.
byte firstByte = dataInputStream.readByte();
if (firstByte == RTSP_INTERLEAVED_MESSAGE_MARKER) {
if (firstByte == INTERLEAVED_MESSAGE_MARKER) {
handleInterleavedBinaryData();
} else {
handleRtspMessage(firstByte);
@ -278,38 +291,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** Handles an entire RTSP message. */
private void handleRtspMessage(byte firstByte) throws IOException {
@Nullable
ImmutableList<String> messageLines = messageBuilder.addLine(handleRtspMessageLine(firstByte));
while (messageLines == null) {
messageLines = messageBuilder.addLine(handleRtspMessageLine(dataInputStream.readByte()));
if (!closed) {
messageListener.onRtspMessageReceived(messageParser.parseNext(firstByte, dataInputStream));
}
ImmutableList<String> messageLinesToPost = ImmutableList.copyOf(messageLines);
messageListenerHandler.post(
() -> {
if (!closed) {
messageListener.onRtspMessageReceived(messageLinesToPost);
}
});
}
/** Returns the byte representation of a complete RTSP line, with CRLF line terminator. */
private byte[] handleRtspMessageLine(byte firstByte) throws IOException {
ByteArrayOutputStream messageByteStream = new ByteArrayOutputStream();
byte[] peekedBytes = new byte[2];
peekedBytes[0] = firstByte;
peekedBytes[1] = dataInputStream.readByte();
messageByteStream.write(peekedBytes);
while (peekedBytes[0] != Ascii.CR || peekedBytes[1] != Ascii.LF) {
// Shift the CRLF buffer.
peekedBytes[0] = peekedBytes[1];
peekedBytes[1] = dataInputStream.readByte();
messageByteStream.write(peekedBytes[1]);
}
return messageByteStream.toByteArray();
}
private void handleInterleavedBinaryData() throws IOException {
@ -318,12 +302,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
byte[] data = new byte[size];
dataInputStream.readFully(data, /* off= */ 0, size);
messageListenerHandler.post(
() -> {
if (!closed) {
messageListener.onInterleavedBinaryDataReceived(data, channel);
}
});
@Nullable
InterleavedBinaryDataListener listener = interleavedBinaryDataListeners.get(channel);
if (listener != null && !closed) {
listener.onInterleavedBinaryDataReceived(data);
}
}
}
@ -342,59 +325,113 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
long loadDurationMs,
IOException error,
int errorCount) {
messageListener.onReceivingFailed(error);
if (!closed) {
messageListener.onReceivingFailed(error);
}
return Loader.DONT_RETRY;
}
}
/** Processes RTSP messages line-by-line. */
private static final class RtspMessageBuilder {
@IntDef({STATE_READING_FIRST_LINE, STATE_READING_RTSP_HEADER, STATE_READING_RTSP_BODY})
/** Processes RTSP messages line-by-line. */
private static final class MessageParser {
@IntDef({STATE_READING_FIRST_LINE, STATE_READING_HEADER, STATE_READING_BODY})
@interface ReadingState {}
private static final int STATE_READING_FIRST_LINE = 1;
private static final int STATE_READING_RTSP_HEADER = 2;
private static final int STATE_READING_RTSP_BODY = 3;
private static final int STATE_READING_HEADER = 2;
private static final int STATE_READING_BODY = 3;
private final List<String> messageLines;
@ReadingState private int state;
private long messageBodyLength;
private long receivedMessageBodyLength;
/** Creates a new instance. */
public RtspMessageBuilder() {
public MessageParser() {
messageLines = new ArrayList<>();
state = STATE_READING_FIRST_LINE;
}
/**
* Add a line to the builder.
* Receives and parses an entire RTSP message.
*
* @param lineBytes The complete RTSP message line in UTF-8 byte array, including CRLF.
* @return A list of completed RTSP message lines, without the CRLF line terminators; or {@code
* null} if the message is not yet complete.
* @param firstByte The first byte received for the RTSP message.
* @param dataInputStream The {@link DataInputStream} on which RTSP messages are received.
* @return An {@link ImmutableList} of the lines that make up an RTSP message.
*/
public ImmutableList<String> parseNext(byte firstByte, DataInputStream dataInputStream)
throws IOException {
@Nullable
ImmutableList<String> parsedMessageLines =
addMessageLine(parseNextLine(firstByte, dataInputStream));
while (parsedMessageLines == null) {
if (state == STATE_READING_BODY) {
if (messageBodyLength > 0) {
// Message body's format is not regulated under RTSP, so it could use LF (instead of
// RTSP's CRLF) as line ending. The length of the message body is included in the RTSP
// Content-Length header.
// Assume the message body length is within a 32-bit integer.
int messageBodyLengthInt = Ints.checkedCast(messageBodyLength);
checkState(messageBodyLengthInt != C.LENGTH_UNSET);
byte[] messageBodyBytes = new byte[messageBodyLengthInt];
dataInputStream.readFully(messageBodyBytes, /* off= */ 0, messageBodyLengthInt);
parsedMessageLines = addMessageBody(messageBodyBytes);
} else {
throw new IllegalStateException("Expects a greater than zero Content-Length.");
}
} else {
parsedMessageLines =
addMessageLine(parseNextLine(dataInputStream.readByte(), dataInputStream));
}
}
return parsedMessageLines;
}
/** Returns the byte representation of a complete RTSP line, with CRLF line terminator. */
private static byte[] parseNextLine(byte firstByte, DataInputStream dataInputStream)
throws IOException {
ByteArrayOutputStream messageByteStream = new ByteArrayOutputStream();
byte[] peekedBytes = new byte[2];
peekedBytes[0] = firstByte;
peekedBytes[1] = dataInputStream.readByte();
messageByteStream.write(peekedBytes);
while (peekedBytes[0] != Ascii.CR || peekedBytes[1] != Ascii.LF) {
// Shift the CRLF buffer.
peekedBytes[0] = peekedBytes[1];
peekedBytes[1] = dataInputStream.readByte();
messageByteStream.write(peekedBytes[1]);
}
return messageByteStream.toByteArray();
}
/**
* Returns a list of completed RTSP message lines, without the CRLF line terminators; or {@code
* null} if the message is not yet complete.
*/
@Nullable
public ImmutableList<String> addLine(byte[] lineBytes) throws ParserException {
// Trim CRLF.
private ImmutableList<String> addMessageLine(byte[] lineBytes) throws ParserException {
// Trim CRLF. RTSP lists are terminated by a CRLF.
checkArgument(
lineBytes.length >= 2
&& lineBytes[lineBytes.length - 2] == Ascii.CR
&& lineBytes[lineBytes.length - 1] == Ascii.LF);
String line =
new String(
lineBytes, /* offset= */ 0, /* length= */ lineBytes.length - 2, Charsets.UTF_8);
new String(lineBytes, /* offset= */ 0, /* length= */ lineBytes.length - 2, CHARSET);
messageLines.add(line);
switch (state) {
case STATE_READING_FIRST_LINE:
if (isRtspStartLine(line)) {
state = STATE_READING_RTSP_HEADER;
state = STATE_READING_HEADER;
}
break;
case STATE_READING_RTSP_HEADER:
case STATE_READING_HEADER:
// Check if the line contains RTSP Content-Length header.
long contentLength = RtspMessageUtil.parseContentLengthHeader(line);
if (contentLength != C.LENGTH_UNSET) {
@ -404,7 +441,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
if (line.isEmpty()) {
// An empty line signals the end of the header section.
if (messageBodyLength > 0) {
state = STATE_READING_RTSP_BODY;
state = STATE_READING_BODY;
} else {
ImmutableList<String> linesToReturn = ImmutableList.copyOf(messageLines);
reset();
@ -413,14 +450,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
break;
case STATE_READING_RTSP_BODY:
receivedMessageBodyLength += lineBytes.length;
if (receivedMessageBodyLength >= messageBodyLength) {
ImmutableList<String> linesToReturn = ImmutableList.copyOf(messageLines);
reset();
return linesToReturn;
}
break;
case STATE_READING_BODY:
// Message body must be handled by addMessageBody().
default:
throw new IllegalStateException();
@ -428,11 +459,45 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return null;
}
/** Returns a list of completed RTSP message lines, without the line terminators. */
private ImmutableList<String> addMessageBody(byte[] messageBodyBytes) {
checkState(state == STATE_READING_BODY);
String messageBody;
if (messageBodyBytes.length > 0
&& messageBodyBytes[messageBodyBytes.length - 1] == Ascii.LF) {
if (messageBodyBytes.length > 1
&& messageBodyBytes[messageBodyBytes.length - 2] == Ascii.CR) {
// Line ends with CRLF.
messageBody =
new String(
messageBodyBytes,
/* offset= */ 0,
/* length= */ messageBodyBytes.length - 2,
CHARSET);
} else {
// Line ends with LF.
messageBody =
new String(
messageBodyBytes,
/* offset= */ 0,
/* length= */ messageBodyBytes.length - 1,
CHARSET);
}
} else {
throw new IllegalArgumentException("Message body is empty or does not end with a LF.");
}
messageLines.add(messageBody);
ImmutableList<String> linesToReturn = ImmutableList.copyOf(messageLines);
reset();
return linesToReturn;
}
private void reset() {
messageLines.clear();
state = STATE_READING_FIRST_LINE;
messageBodyLength = 0;
receivedMessageBodyLength = 0;
}
}
}

View File

@ -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.util.Assertions.checkArgument;
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 android.net.Uri;
@ -37,10 +38,10 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ParserException;
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.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableListMultimap;
import java.util.List;
import java.util.regex.Matcher;
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). */
public static final long DEFAULT_RTSP_TIMEOUT_MS = 60_000;
@ -81,7 +96,20 @@ import java.util.regex.Pattern;
private static final Pattern SESSION_HEADER_PATTERN =
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 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.
@ -95,11 +123,13 @@ import java.util.regex.Pattern;
builder.add(
Util.formatInvariant(
"%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()) {
builder.add(
Util.formatInvariant(
"%s: %s", headerName, checkNotNull(request.headers.get(headerName))));
ImmutableList<String> headerValuesForName = headers.get(headerName);
for (int i = 0; i < headerValuesForName.size(); i++) {
builder.add(Util.formatInvariant("%s: %s", headerName, headerValuesForName.get(i)));
}
}
// Empty line after headers.
builder.add("");
@ -120,11 +150,12 @@ import java.util.regex.Pattern;
Util.formatInvariant(
"%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()) {
builder.add(
Util.formatInvariant(
"%s: %s", headerName, checkNotNull(response.headers.get(headerName))));
ImmutableList<String> headerValuesForName = headers.get(headerName);
for (int i = 0; i < headerValuesForName.size(); i++) {
builder.add(Util.formatInvariant("%s: %s", headerName, headerValuesForName.get(i)));
}
}
// Empty line after headers.
builder.add("");
@ -139,7 +170,7 @@ import java.util.regex.Pattern;
* removed.
*/
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}. */
@ -155,10 +186,35 @@ import java.util.regex.Pattern;
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. */
public static String toMethodString(@RtspRequest.Method int method) {
switch (method) {
case RtspRequest.METHOD_ANNOUNCE:
case METHOD_ANNOUNCE:
return "ANNOUNCE";
case METHOD_DESCRIBE:
return "DESCRIBE";
@ -238,7 +294,7 @@ import java.util.regex.Pattern;
List<String> headerLines = lines.subList(1, messageBodyOffset);
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);
}
@ -261,7 +317,7 @@ import java.util.regex.Pattern;
List<String> headerLines = lines.subList(1, messageBodyOffset);
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);
}
@ -271,6 +327,11 @@ import java.util.regex.Pattern;
|| 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
* C#LENGTH_UNSET}.
@ -343,6 +404,39 @@ import java.util.regex.Pattern;
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) {
switch (statusCode) {
case 200:

View File

@ -41,8 +41,6 @@ import java.util.regex.Pattern;
private static final Pattern MEDIA_DESCRIPTION_PATTERN =
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 ORIGIN_TYPE = "o";
private static final String SESSION_TYPE = "s";
@ -71,7 +69,7 @@ import java.util.regex.Pattern;
@Nullable MediaDescription.Builder mediaDescriptionBuilder = null;
// Lines are separated by an CRLF.
for (String line : Util.split(sdpString, CRLF)) {
for (String line : RtspMessageUtil.splitRtspMessageBody(sdpString)) {
if ("".equals(line)) {
continue;
}
@ -188,7 +186,7 @@ import java.util.regex.Pattern;
try {
return sessionDescriptionBuilder.build();
} catch (IllegalStateException e) {
} catch (IllegalArgumentException | IllegalStateException e) {
throw new ParserException(e);
}
}
@ -199,7 +197,7 @@ import java.util.regex.Pattern;
throws ParserException {
try {
sessionDescriptionBuilder.addMediaDescription(mediaDescriptionBuilder.build());
} catch (IllegalStateException e) {
} catch (IllegalArgumentException | IllegalStateException e) {
throw new ParserException(e);
}
}

View File

@ -22,6 +22,7 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.net.Uri;
import androidx.annotation.Nullable;
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.DataSpec;
import com.google.android.exoplayer2.util.Util;
@ -31,7 +32,8 @@ import java.util.Arrays;
import java.util.concurrent.LinkedBlockingQueue;
/** 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 =
"RTP/AVP/TCP;unicast;interleaved=%d-%d";
@ -62,8 +64,8 @@ import java.util.concurrent.LinkedBlockingQueue;
}
@Override
public boolean usesSidebandBinaryData() {
return true;
public InterleavedBinaryDataListener getInterleavedBinaryDataListener() {
return this;
}
@Override
@ -119,7 +121,7 @@ import java.util.concurrent.LinkedBlockingQueue;
}
@Override
public void write(byte[] buffer) {
packetQueue.add(buffer);
public void onInterleavedBinaryDataReceived(byte[] data) {
packetQueue.add(data);
}
}

View File

@ -55,6 +55,12 @@ import java.io.IOException;
return port == UdpDataSource.UDP_PORT_UNSET ? C.INDEX_UNSET : port;
}
@Nullable
@Override
public RtspMessageChannel.InterleavedBinaryDataListener getInterleavedBinaryDataListener() {
return null;
}
@Override
public void addTransferListener(TransferListener transferListener) {
dataSource.addTransferListener(transferListener);
@ -85,20 +91,6 @@ import java.io.IOException;
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) {
checkArgument(this != rtcpChannel);
this.rtcpChannel = rtcpChannel;

View File

@ -0,0 +1,104 @@
/*
* 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);
}
}

View File

@ -0,0 +1,69 @@
/*
* 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\"");
}
}

View File

@ -19,9 +19,13 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
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.RtspMediaSource.RtspPlaybackException;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import java.util.concurrent.atomic.AtomicBoolean;
@ -39,8 +43,12 @@ public final class RtspClientTest {
private @MonotonicNonNull RtspServer rtspServer;
@Before
public void setUp() {
rtspServer = new RtspServer();
public void setUp() throws Exception {
rtspServer =
new RtspServer(
RtpPacketStreamDump.parse(
TestUtil.getString(
ApplicationProvider.getApplicationContext(), "media/rtsp/aac-dump.json")));
}
@After
@ -50,7 +58,7 @@ public final class RtspClientTest {
}
@Test
public void connectServerAndClient_withServerSupportsOnlyOptions_sessionTimelineRequestFails()
public void connectServerAndClient_withServerSupportsDescribe_updatesSessionTimeline()
throws Exception {
int serverRtspPortNumber = checkNotNull(rtspServer).startAndGetPortNumber();
@ -60,13 +68,24 @@ public final class RtspClientTest {
new SessionInfoListener() {
@Override
public void onSessionTimelineUpdated(
RtspSessionTiming timing, ImmutableList<RtspMediaTrack> tracks) {}
RtspSessionTiming timing, ImmutableList<RtspMediaTrack> tracks) {
sessionTimelineUpdateEventReceived.set(!tracks.isEmpty());
}
@Override
public void onSessionTimelineRequestFailed(
String message, @Nullable Throwable cause) {
sessionTimelineUpdateEventReceived.set(true);
}
String message, @Nullable Throwable cause) {}
},
new PlaybackEventListener() {
@Override
public void onRtspSetupCompleted() {}
@Override
public void onPlaybackStarted(
long startPositionUs, ImmutableList<RtspTrackTiming> trackTimingList) {}
@Override
public void onPlaybackError(RtspPlaybackException error) {}
},
/* userAgent= */ "ExoPlayer:RtspClientTest",
/* uri= */ Uri.parse(

View File

@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ListMultimap;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -82,7 +83,47 @@ public final class RtspHeadersTest {
}
@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 =
new RtspHeaders.Builder()
.addAll(
@ -92,11 +133,39 @@ public final class RtspHeadersTest {
"Content-Length: 707",
"Transport: RTP/AVP;unicast;client_port=65458-65459\r\n"))
.build();
assertThat(headers.asMap())
assertThat(headers.asMultiMap())
.containsExactly(
"Accept", "application/sdp",
"CSeq", "3",
"Content-Length", "707",
"Transport", "RTP/AVP;unicast;client_port=65458-65459");
"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(
"RTP/AVP;unicast;client_port=65456-65457", "RTP/AVP;unicast;client_port=65458-65459")
.inOrder();
}
}

View File

@ -1,102 +0,0 @@
/*
* 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);
}
}

View File

@ -174,6 +174,23 @@ public class RtspMediaTrackTest {
assertThat(format).isEqualTo(expectedFormat);
}
@Test
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() {
@ -222,24 +239,6 @@ public class RtspMediaTrackTest {
() -> RtspMediaTrack.generatePayloadFormat(mediaDescription));
}
@Test
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() {

View File

@ -22,7 +22,6 @@ import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
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.common.collect.ImmutableList;
import com.google.common.collect.LinkedListMultimap;
@ -67,11 +66,21 @@ public final class RtspMessageChannelTest {
.build(),
"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 =
new RtspResponse(
200,
new RtspHeaders.Builder()
.add(RtspHeaders.CSEQ, "3")
.add(RtspHeaders.CSEQ, "5")
.add(RtspHeaders.TRANSPORT, "RTP/AVP/TCP;unicast;interleaved=0-1")
.build(),
"");
@ -84,6 +93,7 @@ public final class RtspMessageChannelTest {
AtomicBoolean receivingFinished = new AtomicBoolean();
AtomicReference<Exception> sendingException = new AtomicReference<>();
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();
ServerSocket serverSocket =
new ServerSocket(/* port= */ 0, /* backlog= */ 1, InetAddress.getByName(/* host= */ null));
@ -97,6 +107,8 @@ public final class RtspMessageChannelTest {
convertMessageToByteArray(serializeResponse(optionsResponse)));
serverOutputStream.write(
convertMessageToByteArray(serializeResponse(describeResponse)));
serverOutputStream.write(
convertMessageToByteArray(serializeResponse(describeResponse2)));
serverOutputStream.write(Bytes.concat(new byte[] {'$'}, interleavedData1));
serverOutputStream.write(Bytes.concat(new byte[] {'$'}, interleavedData2));
serverOutputStream.write(
@ -116,21 +128,19 @@ public final class RtspMessageChannelTest {
RtspMessageChannel rtspMessageChannel =
new RtspMessageChannel(
new MessageListener() {
@Override
public void onRtspMessageReceived(List<String> message) {
receivedRtspResponses.add(message);
if (receivedRtspResponses.size() == 3 && receivedInterleavedData.size() == 2) {
receivingFinished.set(true);
}
}
@Override
public void onInterleavedBinaryDataReceived(byte[] data, int channel) {
receivedInterleavedData.put(channel, Bytes.asList(data));
message -> {
receivedRtspResponses.add(message);
if (receivedRtspResponses.size() == 4 && receivedInterleavedData.size() == 2) {
receivingFinished.set(true);
}
});
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);
Util.closeQuietly(rtspMessageChannel);
@ -141,18 +151,26 @@ public final class RtspMessageChannelTest {
assertThat(receivedRtspResponses)
.containsExactly(
/* optionsResponse */
ImmutableList.of("RTSP/1.0 200 OK", "CSeq: 2", "Public: OPTIONS", ""),
ImmutableList.of("RTSP/1.0 200 OK", "cseq: 2", "public: OPTIONS", ""),
/* describeResponse */
ImmutableList.of(
"RTSP/1.0 200 OK",
"CSeq: 3",
"Content-Type: application/sdp",
"Content-Length: 28",
"cseq: 3",
"content-type: application/sdp",
"content-length: 28",
"",
"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 */
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();
assertThat(receivedInterleavedData)
.containsExactly(

View File

@ -19,9 +19,11 @@ package com.google.android.exoplayer2.source.rtsp;
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.common.base.Charsets;
import com.google.android.exoplayer2.source.rtsp.RtspMessageUtil.RtspAuthUserInfo;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ListMultimap;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
@ -42,8 +44,10 @@ public final class RtspMessageUtilTest {
RtspRequest request = RtspMessageUtil.parseRequest(requestLines);
assertThat(request.method).isEqualTo(RtspRequest.METHOD_OPTIONS);
assertThat(request.headers.asMap())
.containsExactly(RtspHeaders.CSEQ, "2", RtspHeaders.USER_AGENT, "LibVLC/3.0.11");
assertThat(request.headers.asMultiMap())
.containsExactly(
RtspHeaders.CSEQ, "2",
RtspHeaders.USER_AGENT, "LibVLC/3.0.11");
assertThat(request.messageBody).isEmpty();
}
@ -58,12 +62,12 @@ public final class RtspMessageUtilTest {
RtspResponse response = RtspMessageUtil.parseResponse(responseLines);
assertThat(response.status).isEqualTo(200);
assertThat(response.headers.asMap())
assertThat(response.headers.asMultiMap())
.containsExactly(
RtspHeaders.CSEQ,
"2",
RtspHeaders.PUBLIC,
"OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER");
"OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER," + " SET_PARAMETER");
assertThat(response.messageBody).isEmpty();
}
@ -79,14 +83,11 @@ public final class RtspMessageUtilTest {
RtspRequest request = RtspMessageUtil.parseRequest(requestLines);
assertThat(request.method).isEqualTo(RtspRequest.METHOD_DESCRIBE);
assertThat(request.headers.asMap())
assertThat(request.headers.asMultiMap())
.containsExactly(
RtspHeaders.CSEQ,
"3",
RtspHeaders.USER_AGENT,
"LibVLC/3.0.11",
RtspHeaders.ACCEPT,
"application/sdp");
RtspHeaders.CSEQ, "3",
RtspHeaders.USER_AGENT, "LibVLC/3.0.11",
RtspHeaders.ACCEPT, "application/sdp");
assertThat(request.messageBody).isEmpty();
}
@ -112,16 +113,12 @@ public final class RtspMessageUtilTest {
RtspResponse response = RtspMessageUtil.parseResponse(responseLines);
assertThat(response.status).isEqualTo(200);
assertThat(response.headers.asMap())
assertThat(response.headers.asMultiMap())
.containsExactly(
RtspHeaders.CSEQ,
"3",
RtspHeaders.CONTENT_BASE,
"rtsp://127.0.0.1/test.mkv/",
RtspHeaders.CONTENT_TYPE,
"application/sdp",
RtspHeaders.CONTENT_LENGTH,
"707");
RtspHeaders.CSEQ, "3",
RtspHeaders.CONTENT_BASE, "rtsp://127.0.0.1/test.mkv/",
RtspHeaders.CONTENT_TYPE, "application/sdp",
RtspHeaders.CONTENT_LENGTH, "707");
assertThat(response.messageBody)
.isEqualTo(
@ -136,6 +133,30 @@ public final class RtspMessageUtilTest {
+ "a=control:track2");
}
@Test
public void parseResponse_with401DescribeResponse_succeeds() {
List<String> responseLines =
Arrays.asList(
"RTSP/1.0 401 Unauthorized",
"CSeq: 3",
"WWW-Authenticate: BASIC realm=\"wow\"",
"WWW-Authenticate: DIGEST realm=\"wow\", nonce=\"nonce\"",
"");
RtspResponse response = RtspMessageUtil.parseResponse(responseLines);
ListMultimap<String, String> headersMap = response.headers.asMultiMap();
assertThat(response.status).isEqualTo(401);
assertThat(headersMap.keySet()).containsExactly("cseq", "www-authenticate").inOrder();
assertThat(headersMap).valuesForKey("cseq").containsExactly("3");
assertThat(headersMap)
.valuesForKey("www-authenticate")
.containsExactly("BASIC realm=\"wow\"", "DIGEST realm=\"wow\", nonce=\"nonce\"")
.inOrder();
assertThat(response.messageBody).isEmpty();
}
@Test
public void parseRequest_withSetParameterRequest_succeeds() {
List<String> requestLines =
@ -150,16 +171,12 @@ public final class RtspMessageUtilTest {
RtspRequest request = RtspMessageUtil.parseRequest(requestLines);
assertThat(request.method).isEqualTo(RtspRequest.METHOD_SET_PARAMETER);
assertThat(request.headers.asMap())
assertThat(request.headers.asMultiMap())
.containsExactly(
RtspHeaders.CSEQ,
"3",
RtspHeaders.USER_AGENT,
"LibVLC/3.0.11",
RtspHeaders.CONTENT_LENGTH,
"20",
RtspHeaders.CONTENT_TYPE,
"text/parameters");
RtspHeaders.CSEQ, "3",
RtspHeaders.USER_AGENT, "LibVLC/3.0.11",
RtspHeaders.CONTENT_LENGTH, "20",
RtspHeaders.CONTENT_TYPE, "text/parameters");
assertThat(request.messageBody).isEqualTo("param: stuff");
}
@ -177,14 +194,11 @@ public final class RtspMessageUtilTest {
RtspResponse response = RtspMessageUtil.parseResponse(responseLines);
assertThat(response.status).isEqualTo(200);
assertThat(response.headers.asMap())
assertThat(response.headers.asMultiMap())
.containsExactly(
RtspHeaders.CSEQ,
"431",
RtspHeaders.CONTENT_LENGTH,
"46",
RtspHeaders.CONTENT_TYPE,
"text/parameters");
RtspHeaders.CSEQ, "431",
RtspHeaders.CONTENT_LENGTH, "46",
RtspHeaders.CONTENT_TYPE, "text/parameters");
assertThat(response.messageBody).isEqualTo("packets_received: 10\r\n" + "jitter: 0.3838");
}
@ -207,19 +221,19 @@ public final class RtspMessageUtilTest {
List<String> expectedLines =
Arrays.asList(
"SETUP rtsp://127.0.0.1/test.mkv/track1 RTSP/1.0",
"CSeq: 4",
"Transport: RTP/AVP;unicast;client_port=65458-65459",
"cseq: 4",
"transport: RTP/AVP;unicast;client_port=65458-65459",
"",
"");
String expectedRtspMessage =
"SETUP rtsp://127.0.0.1/test.mkv/track1 RTSP/1.0\r\n"
+ "CSeq: 4\r\n"
+ "Transport: RTP/AVP;unicast;client_port=65458-65459\r\n"
+ "cseq: 4\r\n"
+ "transport: RTP/AVP;unicast;client_port=65458-65459\r\n"
+ "\r\n";
assertThat(messageLines).isEqualTo(expectedLines);
assertThat(RtspMessageUtil.convertMessageToByteArray(messageLines))
.isEqualTo(expectedRtspMessage.getBytes(Charsets.UTF_8));
.isEqualTo(expectedRtspMessage.getBytes(RtspMessageChannel.CHARSET));
}
@Test
@ -241,18 +255,18 @@ public final class RtspMessageUtilTest {
List<String> expectedLines =
Arrays.asList(
"RTSP/1.0 200 OK",
"CSeq: 4",
"Transport: RTP/AVP;unicast;client_port=65458-65459;server_port=5354-5355",
"cseq: 4",
"transport: RTP/AVP;unicast;client_port=65458-65459;server_port=5354-5355",
"",
"");
String expectedRtspMessage =
"RTSP/1.0 200 OK\r\n"
+ "CSeq: 4\r\n"
+ "Transport: RTP/AVP;unicast;client_port=65458-65459;server_port=5354-5355\r\n"
+ "cseq: 4\r\n"
+ "transport: RTP/AVP;unicast;client_port=65458-65459;server_port=5354-5355\r\n"
+ "\r\n";
assertThat(messageLines).isEqualTo(expectedLines);
assertThat(RtspMessageUtil.convertMessageToByteArray(messageLines))
.isEqualTo(expectedRtspMessage.getBytes(Charsets.UTF_8));
.isEqualTo(expectedRtspMessage.getBytes(RtspMessageChannel.CHARSET));
}
@Test
@ -283,10 +297,10 @@ public final class RtspMessageUtilTest {
List<String> expectedLines =
Arrays.asList(
"RTSP/1.0 200 OK",
"CSeq: 4",
"Content-Base: rtsp://127.0.0.1/test.mkv/",
"Content-Type: application/sdp",
"Content-Length: 707",
"cseq: 4",
"content-base: rtsp://127.0.0.1/test.mkv/",
"content-type: application/sdp",
"content-length: 707",
"",
"v=0\r\n"
+ "o=- 1606776316530225 1 IN IP4 192.168.2.176\r\n"
@ -300,10 +314,10 @@ public final class RtspMessageUtilTest {
String expectedRtspMessage =
"RTSP/1.0 200 OK\r\n"
+ "CSeq: 4\r\n"
+ "Content-Base: rtsp://127.0.0.1/test.mkv/\r\n"
+ "Content-Type: application/sdp\r\n"
+ "Content-Length: 707\r\n"
+ "cseq: 4\r\n"
+ "content-base: rtsp://127.0.0.1/test.mkv/\r\n"
+ "content-type: application/sdp\r\n"
+ "content-length: 707\r\n"
+ "\r\n"
+ "v=0\r\n"
+ "o=- 1606776316530225 1 IN IP4 192.168.2.176\r\n"
@ -317,7 +331,7 @@ public final class RtspMessageUtilTest {
assertThat(messageLines).isEqualTo(expectedLines);
assertThat(RtspMessageUtil.convertMessageToByteArray(messageLines))
.isEqualTo(expectedRtspMessage.getBytes(Charsets.UTF_8));
.isEqualTo(expectedRtspMessage.getBytes(RtspMessageChannel.CHARSET));
}
@Test
@ -329,12 +343,12 @@ public final class RtspMessageUtilTest {
/* messageBody= */ "");
List<String> messageLines = RtspMessageUtil.serializeResponse(response);
List<String> expectedLines = Arrays.asList("RTSP/1.0 454 Session Not Found", "CSeq: 4", "", "");
String expectedRtspMessage = "RTSP/1.0 454 Session Not Found\r\n" + "CSeq: 4\r\n" + "\r\n";
List<String> expectedLines = Arrays.asList("RTSP/1.0 454 Session Not Found", "cseq: 4", "", "");
String expectedRtspMessage = "RTSP/1.0 454 Session Not Found\r\n" + "cseq: 4\r\n" + "\r\n";
assertThat(RtspMessageUtil.serializeResponse(response)).isEqualTo(expectedLines);
assertThat(RtspMessageUtil.convertMessageToByteArray(messageLines))
.isEqualTo(expectedRtspMessage.getBytes(Charsets.UTF_8));
.isEqualTo(expectedRtspMessage.getBytes(RtspMessageChannel.CHARSET));
}
@Test
@ -387,4 +401,97 @@ public final class RtspMessageUtilTest {
assertThat(RtspMessageUtil.isRtspStartLine("Transport: RTP/AVP;unicast;client_port=1000-1001"))
.isFalse();
}
@Test
public void extractUserInfo_withoutPassword_returnsNull() {
@Nullable
RtspAuthUserInfo authUserInfo =
RtspMessageUtil.parseUserInfo(Uri.parse("rtsp://username@mediaserver.com/stream1"));
assertThat(authUserInfo).isNull();
}
@Test
public void extractUserInfo_withoutUserInfo_returnsNull() {
@Nullable
RtspAuthUserInfo authUserInfo =
RtspMessageUtil.parseUserInfo(Uri.parse("rtsp://mediaserver.com/stream1"));
assertThat(authUserInfo).isNull();
}
@Test
public void extractUserInfo_withProperlyFormattedUri_succeeds() {
@Nullable
RtspAuthUserInfo authUserInfo =
RtspMessageUtil.parseUserInfo(
Uri.parse("rtsp://username:pass:word@mediaserver.com/stream1"));
assertThat(authUserInfo).isNotNull();
assertThat(authUserInfo.username).isEqualTo("username");
assertThat(authUserInfo.password).isEqualTo("pass:word");
}
@Test
public void parseWWWAuthenticateHeader_withBasicAuthentication_succeeds() throws Exception {
RtspAuthenticationInfo authenticationInfo =
RtspMessageUtil.parseWwwAuthenticateHeader("Basic realm=\"WallyWorld\"");
assertThat(authenticationInfo.authenticationMechanism).isEqualTo(RtspAuthenticationInfo.BASIC);
assertThat(authenticationInfo.nonce).isEmpty();
assertThat(authenticationInfo.realm).isEqualTo("WallyWorld");
}
@Test
public void parseWWWAuthenticateHeader_withDigestAuthenticationWithDomain_succeeds()
throws Exception {
RtspAuthenticationInfo authenticationInfo =
RtspMessageUtil.parseWwwAuthenticateHeader(
"Digest realm=\"testrealm@host.com\", domain=\"host.com\","
+ " nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", "
+ " opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"");
assertThat(authenticationInfo.authenticationMechanism).isEqualTo(RtspAuthenticationInfo.DIGEST);
assertThat(authenticationInfo.nonce).isEqualTo("dcd98b7102dd2f0e8b11d0f600bfb0c093");
assertThat(authenticationInfo.realm).isEqualTo("testrealm@host.com");
assertThat(authenticationInfo.opaque).isEmpty();
}
@Test
public void parseWWWAuthenticateHeader_withDigestAuthenticationWithOptionalParameters_succeeds()
throws Exception {
RtspAuthenticationInfo authenticationInfo =
RtspMessageUtil.parseWwwAuthenticateHeader(
"Digest realm=\"testrealm@host.com\", nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\","
+ " opaque=\"5ccc069c403ebaf9f0171e9517f40e41\", stale=\"stalev\","
+ " algorithm=\"md5\"");
assertThat(authenticationInfo.authenticationMechanism).isEqualTo(RtspAuthenticationInfo.DIGEST);
assertThat(authenticationInfo.nonce).isEqualTo("dcd98b7102dd2f0e8b11d0f600bfb0c093");
assertThat(authenticationInfo.realm).isEqualTo("testrealm@host.com");
assertThat(authenticationInfo.opaque).isEqualTo("5ccc069c403ebaf9f0171e9517f40e41");
}
@Test
public void parseWWWAuthenticateHeader_withDigestAuthentication_succeeds() throws Exception {
RtspAuthenticationInfo authenticationInfo =
RtspMessageUtil.parseWwwAuthenticateHeader(
"Digest realm=\"LIVE555 Streaming Media\", nonce=\"0cdfe9719e7373b7d5bb2913e2115f3f\"");
assertThat(authenticationInfo.authenticationMechanism).isEqualTo(RtspAuthenticationInfo.DIGEST);
assertThat(authenticationInfo.nonce).isEqualTo("0cdfe9719e7373b7d5bb2913e2115f3f");
assertThat(authenticationInfo.realm).isEqualTo("LIVE555 Streaming Media");
assertThat(authenticationInfo.opaque).isEmpty();
}
@Test
public void splitRtspMessageBody_withCrLfLineTerminatorMessageBody_splitsMessageBody() {
String[] lines = RtspMessageUtil.splitRtspMessageBody("line1\r\nline2\r\nline3");
assertThat(lines).asList().containsExactly("line1", "line2", "line3").inOrder();
}
@Test
public void splitRtspMessageBody_withLfLineTerminatorMessageBody_splitsMessageBody() {
String[] lines = RtspMessageUtil.splitRtspMessageBody("line1\nline2\nline3");
assertThat(lines).asList().containsExactly("line1", "line2", "line3").inOrder();
}
}

View File

@ -15,12 +15,15 @@
*/
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.util.Assertions.checkNotNull;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableMap;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetAddress;
@ -28,19 +31,32 @@ import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.List;
import java.util.Map;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** The RTSP server. */
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). */
private static final int STATUS_OK = 200;
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;
/** Runs on the thread on which the constructor was called. */
private final Handler mainHandler;
private final RtpPacketStreamDump rtpPacketStreamDump;
private @MonotonicNonNull ServerSocket serverSocket;
private @MonotonicNonNull RtspMessageChannel connectedClient;
@ -51,7 +67,8 @@ public final class RtspServer implements Closeable {
*
* <p>The constructor must be called on a {@link Looper} thread.
*/
public RtspServer() {
public RtspServer(RtpPacketStreamDump rtpPacketStreamDump) {
this.rtpPacketStreamDump = rtpPacketStreamDump;
listenerThread =
new Thread(this::listenToIncomingRtspConnection, "ExoPlayerTest:RtspConnectionMonitor");
mainHandler = Util.createHandlerForCurrentLooper();
@ -87,7 +104,7 @@ public final class RtspServer implements Closeable {
private void handleNewClientConnected(Socket socket) {
try {
connectedClient = new RtspMessageChannel(new MessageListener());
connectedClient.openSocket(socket);
connectedClient.open(socket);
} catch (IOException e) {
Util.closeQuietly(connectedClient);
// Log the error.
@ -98,34 +115,62 @@ public final class RtspServer implements Closeable {
private final class MessageListener implements RtspMessageChannel.MessageListener {
@Override
public void onRtspMessageReceived(List<String> message) {
mainHandler.post(() -> handleRtspMessage(message));
}
private void handleRtspMessage(List<String> message) {
RtspRequest request = RtspMessageUtil.parseRequest(message);
String cSeq = checkNotNull(request.headers.get(RtspHeaders.CSEQ));
switch (request.method) {
case METHOD_OPTIONS:
onOptionsRequestReceived(request);
onOptionsRequestReceived(cSeq);
break;
case METHOD_DESCRIBE:
onDescribeRequestReceived(request.uri, cSeq);
break;
default:
connectedClient.send(
RtspMessageUtil.serializeResponse(
new RtspResponse(
/* status= */ STATUS_METHOD_NOT_ALLOWED,
/* headers= */ new RtspHeaders.Builder()
.add(
RtspHeaders.CSEQ, checkNotNull(request.headers.get(RtspHeaders.CSEQ)))
.build(),
/* messageBody= */ "")));
sendErrorResponse(STATUS_METHOD_NOT_ALLOWED, cSeq);
}
}
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(
RtspMessageUtil.serializeResponse(
new RtspResponse(
/* status= */ 200,
/* headers= */ new RtspHeaders.Builder()
.add(RtspHeaders.CSEQ, checkNotNull(request.headers.get(RtspHeaders.CSEQ)))
.add(RtspHeaders.PUBLIC, PUBLIC_SUPPORTED_METHODS)
.build(),
/* messageBody= */ "")));
/* status= */ status,
/* headers= */ headerBuilder.build(),
/* messageBody= */ messageBody)));
}
}

View File

@ -30,6 +30,7 @@ import static org.junit.Assert.assertThrows;
import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.ParserException;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -149,6 +150,35 @@ public class SessionDescriptionTest {
assertThat(sessionDescription).isEqualTo(expectedSession);
}
@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() {
assertThrows(

View File

@ -17,9 +17,11 @@ package com.google.android.exoplayer2.source.rtsp;
import static com.google.android.exoplayer2.testutil.TestUtil.buildTestData;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.primitives.Bytes;
import java.io.IOException;
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -28,12 +30,30 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class TransferRtpDataChannelTest {
@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 {
byte[] randomBytes = buildTestData(20);
byte[] buffer = new byte[40];
TransferRtpDataChannel transferRtpDataChannel = new TransferRtpDataChannel();
transferRtpDataChannel.write(randomBytes);
transferRtpDataChannel.onInterleavedBinaryDataReceived(randomBytes);
transferRtpDataChannel.read(buffer, /* offset= */ 0, buffer.length);
@ -45,7 +65,7 @@ public class TransferRtpDataChannelTest {
byte[] randomBytes = buildTestData(20);
byte[] buffer = new byte[8];
TransferRtpDataChannel transferRtpDataChannel = new TransferRtpDataChannel();
transferRtpDataChannel.write(randomBytes);
transferRtpDataChannel.onInterleavedBinaryDataReceived(randomBytes);
transferRtpDataChannel.read(buffer, /* offset= */ 0, buffer.length);
assertThat(buffer).isEqualTo(Arrays.copyOfRange(randomBytes, /* from= */ 0, /* to= */ 8));
@ -61,7 +81,7 @@ public class TransferRtpDataChannelTest {
byte[] randomBytes = buildTestData(40);
byte[] buffer = new byte[20];
TransferRtpDataChannel transferRtpDataChannel = new TransferRtpDataChannel();
transferRtpDataChannel.write(randomBytes);
transferRtpDataChannel.onInterleavedBinaryDataReceived(randomBytes);
transferRtpDataChannel.read(buffer, /* offset= */ 0, buffer.length);
assertThat(buffer).isEqualTo(Arrays.copyOfRange(randomBytes, /* from= */ 0, /* to= */ 20));
@ -77,13 +97,13 @@ public class TransferRtpDataChannelTest {
byte[] smallBuffer = new byte[20];
byte[] bigBuffer = new byte[40];
TransferRtpDataChannel transferRtpDataChannel = new TransferRtpDataChannel();
transferRtpDataChannel.write(randomBytes1);
transferRtpDataChannel.onInterleavedBinaryDataReceived(randomBytes1);
transferRtpDataChannel.read(smallBuffer, /* offset= */ 0, smallBuffer.length);
assertThat(smallBuffer)
.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.
transferRtpDataChannel.read(bigBuffer, /* offset= */ 0, bigBuffer.length);
@ -107,13 +127,13 @@ public class TransferRtpDataChannelTest {
byte[] smallBuffer = new byte[30];
byte[] bigBuffer = new byte[30];
TransferRtpDataChannel transferRtpDataChannel = new TransferRtpDataChannel();
transferRtpDataChannel.write(randomBytes1);
transferRtpDataChannel.onInterleavedBinaryDataReceived(randomBytes1);
transferRtpDataChannel.read(smallBuffer, /* offset= */ 0, smallBuffer.length);
assertThat(smallBuffer)
.isEqualTo(Arrays.copyOfRange(randomBytes1, /* from= */ 0, /* to= */ 30));
transferRtpDataChannel.write(randomBytes2);
transferRtpDataChannel.onInterleavedBinaryDataReceived(randomBytes2);
// Read 30 bytes to big buffer.
transferRtpDataChannel.read(bigBuffer, /* offset= */ 0, bigBuffer.length);
@ -136,13 +156,13 @@ public class TransferRtpDataChannelTest {
byte[] smallBuffer = new byte[20];
byte[] bigBuffer = new byte[70];
TransferRtpDataChannel transferRtpDataChannel = new TransferRtpDataChannel();
transferRtpDataChannel.write(randomBytes1);
transferRtpDataChannel.onInterleavedBinaryDataReceived(randomBytes1);
transferRtpDataChannel.read(smallBuffer, /* offset= */ 0, smallBuffer.length);
assertThat(smallBuffer)
.isEqualTo(Arrays.copyOfRange(randomBytes1, /* from= */ 0, /* to= */ 20));
transferRtpDataChannel.write(randomBytes2);
transferRtpDataChannel.onInterleavedBinaryDataReceived(randomBytes2);
transferRtpDataChannel.read(bigBuffer, /* offset= */ 0, bigBuffer.length);
assertThat(Arrays.copyOfRange(bigBuffer, /* from= */ 0, /* to= */ 60))

View File

@ -0,0 +1,34 @@
/*
* 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();
}
}

View File

@ -570,6 +570,7 @@ public class StyledPlayerView extends FrameLayout implements AdViewProvider {
}
@Nullable Player oldPlayer = this.player;
if (oldPlayer != null) {
oldPlayer.removeListener(componentListener);
if (surfaceView instanceof TextureView) {
oldPlayer.clearVideoTextureView((TextureView) surfaceView);
} else if (surfaceView instanceof SurfaceView) {

View File

@ -21,10 +21,6 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.content.Context;
import android.content.res.Resources;
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.TypedValue;
import android.view.View;
@ -380,37 +376,13 @@ public final class SubtitleView extends FrameLayout implements TextOutput {
}
private Cue removeEmbeddedStyling(Cue cue) {
@Nullable CharSequence cueText = cue.text;
Cue.Builder strippedCue = cue.buildUpon();
if (!applyEmbeddedStyles) {
Cue.Builder 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();
SubtitleViewUtils.removeAllEmbeddedStyling(strippedCue);
} else if (!applyEmbeddedFontSizes) {
if (cueText == null) {
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();
SubtitleViewUtils.removeEmbeddedFontSizes(strippedCue);
}
return cue;
return strippedCue.build();
}
}

View File

@ -16,7 +16,16 @@
*/
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.span.LanguageFeatureSpan;
import com.google.common.base.Predicate;
/** Utility class for subtitle layout logic. */
/* package */ final class SubtitleViewUtils {
@ -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() {}
}

View File

@ -0,0 +1,199 @@
/*
* 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();
}
}

View File

@ -0,0 +1,255 @@
seekMap:
isSeekable = true
duration = 1168020
getPosition(0) = [[timeUs=0, position=1256]]
getPosition(1) = [[timeUs=1, position=1256]]
getPosition(584010) = [[timeUs=584010, position=46570]]
getPosition(1168020) = [[timeUs=1168020, position=91172]]
numberOfTracks = 1
track 0:
total output bytes = 104853
sample count = 58
format 0:
id = 1
sampleMimeType = audio/mha1
maxInputSize = 3528
channelCount = 0
sampleRate = 48000
encoderDelay = 3072
encoderPadding = 255
language = und
initializationData:
data = length 26, hash 4E58F6C7
sample 0:
time = 0
flags = 1
data = length 1706, hash F69B88EF
sample 1:
time = 21333
flags = 0
data = length 1705, hash 20F7927C
sample 2:
time = 42666
flags = 0
data = length 1900, hash D986BAA7
sample 3:
time = 64000
flags = 0
data = length 2224, hash DC056DA8
sample 4:
time = 85333
flags = 0
data = length 2157, hash D9587B29
sample 5:
time = 106666
flags = 0
data = length 2252, hash 9935FAC5
sample 6:
time = 128000
flags = 0
data = length 2025, hash 388EC449
sample 7:
time = 149333
flags = 0
data = length 1818, hash 148B1B72
sample 8:
time = 170666
flags = 0
data = length 1844, hash E997B535
sample 9:
time = 192000
flags = 0
data = length 1717, hash 2A9D93FB
sample 10:
time = 213333
flags = 0
data = length 1701, hash 40238DB5
sample 11:
time = 234666
flags = 0
data = length 1684, hash C46763BF
sample 12:
time = 256000
flags = 0
data = length 1747, hash 5FC3C86E
sample 13:
time = 277333
flags = 0
data = length 1750, hash 7F164780
sample 14:
time = 298666
flags = 0
data = length 1742, hash B853D24D
sample 15:
time = 320000
flags = 0
data = length 1839, hash FBFCDC4A
sample 16:
time = 341333
flags = 0
data = length 1809, hash C4722031
sample 17:
time = 362666
flags = 0
data = length 1746, hash 5990EC1F
sample 18:
time = 384000
flags = 0
data = length 1657, hash 1973E3C4
sample 19:
time = 405333
flags = 0
data = length 1862, hash 47500487
sample 20:
time = 426666
flags = 0
data = length 1687, hash 6789C2B4
sample 21:
time = 448000
flags = 0
data = length 1661, hash 26AB63E4
sample 22:
time = 469333
flags = 0
data = length 1671, hash 85AA94AD
sample 23:
time = 490666
flags = 0
data = length 1666, hash 60AF02C
sample 24:
time = 512000
flags = 0
data = length 1744, hash 93B89CC9
sample 25:
time = 533333
flags = 1
data = length 3498, hash 3013997F
sample 26:
time = 554666
flags = 0
data = length 1645, hash 912C7F11
sample 27:
time = 576000
flags = 0
data = length 1679, hash 5C1D5E4B
sample 28:
time = 597333
flags = 0
data = length 1653, hash AAFDF0C4
sample 29:
time = 618666
flags = 0
data = length 1793, hash 7DEDF0E7
sample 30:
time = 640000
flags = 0
data = length 1673, hash 5123A069
sample 31:
time = 661333
flags = 0
data = length 1631, hash 668D073B
sample 32:
time = 682666
flags = 0
data = length 1645, hash 4BA09D58
sample 33:
time = 704000
flags = 0
data = length 1715, hash 6696D08A
sample 34:
time = 725333
flags = 0
data = length 1913, hash F22841A5
sample 35:
time = 746666
flags = 0
data = length 1808, hash 3EB2EF9A
sample 36:
time = 768000
flags = 0
data = length 1703, hash A76E92E6
sample 37:
time = 789333
flags = 0
data = length 1663, hash D60564B6
sample 38:
time = 810666
flags = 0
data = length 1691, hash 3E9DB1D0
sample 39:
time = 832000
flags = 0
data = length 1682, hash 66EF509A
sample 40:
time = 853333
flags = 0
data = length 1661, hash 1F1114BE
sample 41:
time = 874666
flags = 0
data = length 1651, hash 37C9B7B6
sample 42:
time = 896000
flags = 0
data = length 1675, hash 94616355
sample 43:
time = 917333
flags = 0
data = length 1671, hash DA7F4549
sample 44:
time = 938666
flags = 0
data = length 1799, hash 49EF8B35
sample 45:
time = 960000
flags = 0
data = length 1807, hash C8BDF3C1
sample 46:
time = 981333
flags = 0
data = length 1674, hash 36EA2B2F
sample 47:
time = 1002666
flags = 0
data = length 1662, hash A865F92C
sample 48:
time = 1024000
flags = 0
data = length 1856, hash D20294BC
sample 49:
time = 1045333
flags = 0
data = length 1754, hash 54C7681A
sample 50:
time = 1066666
flags = 1
data = length 3408, hash 48774BB2
sample 51:
time = 1088000
flags = 0
data = length 1602, hash 8E895F43
sample 52:
time = 1109333
flags = 0
data = length 1624, hash 9E0AD8CF
sample 53:
time = 1130666
flags = 0
data = length 1616, hash 1F123433
sample 54:
time = 1152000
flags = 0
data = length 1671, hash 29644955
sample 55:
time = 1173333
flags = 0
data = length 1641, hash 55E8050C
sample 56:
time = 1194666
flags = 0
data = length 1670, hash CC133185
sample 57:
time = 1216000
flags = 536870912
data = length 1705, hash 35C3F104
tracksEnded = true

View File

@ -0,0 +1,255 @@
seekMap:
isSeekable = true
duration = 1168020
getPosition(0) = [[timeUs=0, position=1256]]
getPosition(1) = [[timeUs=1, position=1256]]
getPosition(584010) = [[timeUs=584010, position=46570]]
getPosition(1168020) = [[timeUs=1168020, position=91172]]
numberOfTracks = 1
track 0:
total output bytes = 104853
sample count = 58
format 0:
id = 1
sampleMimeType = audio/mha1
maxInputSize = 3528
channelCount = 0
sampleRate = 48000
encoderDelay = 3072
encoderPadding = 255
language = und
initializationData:
data = length 26, hash 4E58F6C7
sample 0:
time = 0
flags = 1
data = length 1706, hash F69B88EF
sample 1:
time = 21333
flags = 0
data = length 1705, hash 20F7927C
sample 2:
time = 42666
flags = 0
data = length 1900, hash D986BAA7
sample 3:
time = 64000
flags = 0
data = length 2224, hash DC056DA8
sample 4:
time = 85333
flags = 0
data = length 2157, hash D9587B29
sample 5:
time = 106666
flags = 0
data = length 2252, hash 9935FAC5
sample 6:
time = 128000
flags = 0
data = length 2025, hash 388EC449
sample 7:
time = 149333
flags = 0
data = length 1818, hash 148B1B72
sample 8:
time = 170666
flags = 0
data = length 1844, hash E997B535
sample 9:
time = 192000
flags = 0
data = length 1717, hash 2A9D93FB
sample 10:
time = 213333
flags = 0
data = length 1701, hash 40238DB5
sample 11:
time = 234666
flags = 0
data = length 1684, hash C46763BF
sample 12:
time = 256000
flags = 0
data = length 1747, hash 5FC3C86E
sample 13:
time = 277333
flags = 0
data = length 1750, hash 7F164780
sample 14:
time = 298666
flags = 0
data = length 1742, hash B853D24D
sample 15:
time = 320000
flags = 0
data = length 1839, hash FBFCDC4A
sample 16:
time = 341333
flags = 0
data = length 1809, hash C4722031
sample 17:
time = 362666
flags = 0
data = length 1746, hash 5990EC1F
sample 18:
time = 384000
flags = 0
data = length 1657, hash 1973E3C4
sample 19:
time = 405333
flags = 0
data = length 1862, hash 47500487
sample 20:
time = 426666
flags = 0
data = length 1687, hash 6789C2B4
sample 21:
time = 448000
flags = 0
data = length 1661, hash 26AB63E4
sample 22:
time = 469333
flags = 0
data = length 1671, hash 85AA94AD
sample 23:
time = 490666
flags = 0
data = length 1666, hash 60AF02C
sample 24:
time = 512000
flags = 0
data = length 1744, hash 93B89CC9
sample 25:
time = 533333
flags = 1
data = length 3498, hash 3013997F
sample 26:
time = 554666
flags = 0
data = length 1645, hash 912C7F11
sample 27:
time = 576000
flags = 0
data = length 1679, hash 5C1D5E4B
sample 28:
time = 597333
flags = 0
data = length 1653, hash AAFDF0C4
sample 29:
time = 618666
flags = 0
data = length 1793, hash 7DEDF0E7
sample 30:
time = 640000
flags = 0
data = length 1673, hash 5123A069
sample 31:
time = 661333
flags = 0
data = length 1631, hash 668D073B
sample 32:
time = 682666
flags = 0
data = length 1645, hash 4BA09D58
sample 33:
time = 704000
flags = 0
data = length 1715, hash 6696D08A
sample 34:
time = 725333
flags = 0
data = length 1913, hash F22841A5
sample 35:
time = 746666
flags = 0
data = length 1808, hash 3EB2EF9A
sample 36:
time = 768000
flags = 0
data = length 1703, hash A76E92E6
sample 37:
time = 789333
flags = 0
data = length 1663, hash D60564B6
sample 38:
time = 810666
flags = 0
data = length 1691, hash 3E9DB1D0
sample 39:
time = 832000
flags = 0
data = length 1682, hash 66EF509A
sample 40:
time = 853333
flags = 0
data = length 1661, hash 1F1114BE
sample 41:
time = 874666
flags = 0
data = length 1651, hash 37C9B7B6
sample 42:
time = 896000
flags = 0
data = length 1675, hash 94616355
sample 43:
time = 917333
flags = 0
data = length 1671, hash DA7F4549
sample 44:
time = 938666
flags = 0
data = length 1799, hash 49EF8B35
sample 45:
time = 960000
flags = 0
data = length 1807, hash C8BDF3C1
sample 46:
time = 981333
flags = 0
data = length 1674, hash 36EA2B2F
sample 47:
time = 1002666
flags = 0
data = length 1662, hash A865F92C
sample 48:
time = 1024000
flags = 0
data = length 1856, hash D20294BC
sample 49:
time = 1045333
flags = 0
data = length 1754, hash 54C7681A
sample 50:
time = 1066666
flags = 1
data = length 3408, hash 48774BB2
sample 51:
time = 1088000
flags = 0
data = length 1602, hash 8E895F43
sample 52:
time = 1109333
flags = 0
data = length 1624, hash 9E0AD8CF
sample 53:
time = 1130666
flags = 0
data = length 1616, hash 1F123433
sample 54:
time = 1152000
flags = 0
data = length 1671, hash 29644955
sample 55:
time = 1173333
flags = 0
data = length 1641, hash 55E8050C
sample 56:
time = 1194666
flags = 0
data = length 1670, hash CC133185
sample 57:
time = 1216000
flags = 536870912
data = length 1705, hash 35C3F104
tracksEnded = true

View File

@ -0,0 +1,155 @@
seekMap:
isSeekable = true
duration = 1168020
getPosition(0) = [[timeUs=0, position=1256]]
getPosition(1) = [[timeUs=1, position=1256]]
getPosition(584010) = [[timeUs=584010, position=46570]]
getPosition(1168020) = [[timeUs=1168020, position=91172]]
numberOfTracks = 1
track 0:
total output bytes = 59539
sample count = 33
format 0:
id = 1
sampleMimeType = audio/mha1
maxInputSize = 3528
channelCount = 0
sampleRate = 48000
encoderDelay = 3072
encoderPadding = 255
language = und
initializationData:
data = length 26, hash 4E58F6C7
sample 0:
time = 533333
flags = 1
data = length 3498, hash 3013997F
sample 1:
time = 554666
flags = 0
data = length 1645, hash 912C7F11
sample 2:
time = 576000
flags = 0
data = length 1679, hash 5C1D5E4B
sample 3:
time = 597333
flags = 0
data = length 1653, hash AAFDF0C4
sample 4:
time = 618666
flags = 0
data = length 1793, hash 7DEDF0E7
sample 5:
time = 640000
flags = 0
data = length 1673, hash 5123A069
sample 6:
time = 661333
flags = 0
data = length 1631, hash 668D073B
sample 7:
time = 682666
flags = 0
data = length 1645, hash 4BA09D58
sample 8:
time = 704000
flags = 0
data = length 1715, hash 6696D08A
sample 9:
time = 725333
flags = 0
data = length 1913, hash F22841A5
sample 10:
time = 746666
flags = 0
data = length 1808, hash 3EB2EF9A
sample 11:
time = 768000
flags = 0
data = length 1703, hash A76E92E6
sample 12:
time = 789333
flags = 0
data = length 1663, hash D60564B6
sample 13:
time = 810666
flags = 0
data = length 1691, hash 3E9DB1D0
sample 14:
time = 832000
flags = 0
data = length 1682, hash 66EF509A
sample 15:
time = 853333
flags = 0
data = length 1661, hash 1F1114BE
sample 16:
time = 874666
flags = 0
data = length 1651, hash 37C9B7B6
sample 17:
time = 896000
flags = 0
data = length 1675, hash 94616355
sample 18:
time = 917333
flags = 0
data = length 1671, hash DA7F4549
sample 19:
time = 938666
flags = 0
data = length 1799, hash 49EF8B35
sample 20:
time = 960000
flags = 0
data = length 1807, hash C8BDF3C1
sample 21:
time = 981333
flags = 0
data = length 1674, hash 36EA2B2F
sample 22:
time = 1002666
flags = 0
data = length 1662, hash A865F92C
sample 23:
time = 1024000
flags = 0
data = length 1856, hash D20294BC
sample 24:
time = 1045333
flags = 0
data = length 1754, hash 54C7681A
sample 25:
time = 1066666
flags = 1
data = length 3408, hash 48774BB2
sample 26:
time = 1088000
flags = 0
data = length 1602, hash 8E895F43
sample 27:
time = 1109333
flags = 0
data = length 1624, hash 9E0AD8CF
sample 28:
time = 1130666
flags = 0
data = length 1616, hash 1F123433
sample 29:
time = 1152000
flags = 0
data = length 1671, hash 29644955
sample 30:
time = 1173333
flags = 0
data = length 1641, hash 55E8050C
sample 31:
time = 1194666
flags = 0
data = length 1670, hash CC133185
sample 32:
time = 1216000
flags = 536870912
data = length 1705, hash 35C3F104
tracksEnded = true

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