mirror of
https://github.com/androidx/media.git
synced 2025-05-21 23:56:32 +08:00
commit
b2333c86c1
26
README.md
26
README.md
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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')) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
@ -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>
|
||||
|
@ -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> 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> 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> 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> 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 <a href="MediaItem.Builder.html" title="class in com.google.android.exoplayer2">MediaItem.Builder</a> setDrmUuid​(@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> 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 <a href="MediaItem.Builder.html" title="class in com.google.android.exoplayer2">MediaItem.Builder</a> setDrmMultiSession​(boolean 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 <a href="MediaItem.Builder.html" title="class in com.google.android.exoplayer2">MediaItem.Builder</a> setDrmPlayClearContentWithoutKey​(boolean 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> 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> 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> 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> 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)">
|
||||
|
@ -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>​(byte[] 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>​(<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)</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>​(<a href="https://developer.android.com/reference/java/lang/CharSequence.html" title="class or interface in java.lang" class="externalLink" target="_top">CharSequence</a> 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>​(<a href="https://developer.android.com/reference/java/lang/CharSequence.html" title="class or interface in java.lang" class="externalLink" target="_top">CharSequence</a> 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>​(<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)</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>​(<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)</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>​(<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)</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>​(<a href="https://developer.android.com/reference/android/net/Uri.html" title="class or interface in android.net" class="externalLink" target="_top">Uri</a> 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>​(<a href="Rating.html" title="class in com.google.android.exoplayer2">Rating</a> 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>​(<a href="https://developer.android.com/reference/java/lang/CharSequence.html" title="class or interface in java.lang" class="externalLink" target="_top">CharSequence</a> 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>​(<a href="https://developer.android.com/reference/java/lang/CharSequence.html" title="class or interface in java.lang" class="externalLink" target="_top">CharSequence</a> 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>​(<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)</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>​(<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)</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>​(<a href="Rating.html" title="class in com.google.android.exoplayer2">Rating</a> 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>​(<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)</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 <a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a> setArtworkData​(@Nullable
|
||||
byte[] 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 <a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a> setArtworkUri​(@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> 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 <a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a> setTrackNumber​(@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> 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 <a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a> setTotalTrackCount​(@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> 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 <a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a> setFolderType​(@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> 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 <a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a> setIsPlayable​(@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> 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 <a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a> setYear​(@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> 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 <a href="MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a> setExtras​(@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> 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>
|
||||
|
@ -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 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: </li>
|
||||
<li>Field | </li>
|
||||
<li>Required | </li>
|
||||
<li>Optional</li>
|
||||
</ul>
|
||||
<ul class="subNavList">
|
||||
<li>Detail: </li>
|
||||
<li>Field | </li>
|
||||
<li>Element</li>
|
||||
</ul>
|
||||
</div>
|
||||
<a id="skip.navbar.top">
|
||||
<!-- -->
|
||||
</a></div>
|
||||
<!-- ========= END OF TOP NAVBAR ========= -->
|
||||
</div>
|
||||
<div class="navPadding"> </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> <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 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: </li>
|
||||
<li>Field | </li>
|
||||
<li>Required | </li>
|
||||
<li>Optional</li>
|
||||
</ul>
|
||||
<ul class="subNavList">
|
||||
<li>Detail: </li>
|
||||
<li>Field | </li>
|
||||
<li>Element</li>
|
||||
</ul>
|
||||
</div>
|
||||
<a id="skip.navbar.bottom">
|
||||
<!-- -->
|
||||
</a></div>
|
||||
<!-- ======== END OF BOTTOM NAVBAR ======= -->
|
||||
</nav>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
@ -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 </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><<a href="MediaMetadata.html" title="class in com.google.android.exoplayer2">MediaMetadata</a>></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 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 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 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 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 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 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 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 <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 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 <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 <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 <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 <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 <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 <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 <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>
|
||||
|
@ -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"> </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>​(<a href="../../MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a> 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"> </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>​(<a href="https://developer.android.com/reference/android/os/Parcel.html" title="class or interface in android.os" class="externalLink" target="_top">Parcel</a> dest,
|
||||
int 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 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 <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 void populateMediaMetadata​(<a href="../../MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a> builder)</pre>
|
||||
<div class="block"><span class="descfrmTypeLabel">Description copied from interface: <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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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><<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>,​<a href="HlsMediaPlaylist.RenditionReport.html" title="class in com.google.android.exoplayer2.source.hls.playlist" target="_top">HlsMediaPlaylist.RenditionReport</a>></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><<a href="HlsMediaPlaylist.Segment.html" title="class in com.google.android.exoplayer2.source.hls.playlist" target="_top">HlsMediaPlaylist.Segment</a>></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><<a href="HlsMediaPlaylist.Part.html" title="class in com.google.android.exoplayer2.source.hls.playlist" target="_top">HlsMediaPlaylist.Part</a>></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>​(int 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>​(int 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> baseUri,
|
||||
<a href="https://developer.android.com/reference/java/util/List.html" title="class or interface in java.util" class="externalLink">List</a><<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>> tags,
|
||||
long startOffsetUs,
|
||||
boolean preciseStart,
|
||||
long startTimeUs,
|
||||
boolean hasDiscontinuitySequence,
|
||||
int discontinuitySequence,
|
||||
@ -540,7 +549,19 @@ public final int playlistType</pre>
|
||||
<li class="blockList">
|
||||
<h4>startOffsetUs</h4>
|
||||
<pre>public final 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 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 <a href="../../../drm/DrmInitData.html" title="class in com.go
|
||||
<!-- -->
|
||||
</a>
|
||||
<h3>Constructor Detail</h3>
|
||||
<a id="<init>(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="<init>(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 <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> baseUri,
|
||||
<a href="https://developer.android.com/reference/java/util/List.html" title="class or interface in java.util" class="externalLink">List</a><<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>> tags,
|
||||
long startOffsetUs,
|
||||
boolean preciseStart,
|
||||
long startTimeUs,
|
||||
boolean hasDiscontinuitySequence,
|
||||
int discontinuitySequence,
|
||||
|
@ -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>​(boolean 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>​(<a href="../../upstream/LoadErrorHandlingPolicy.html" title="interface in com.google.android.exoplayer2.upstream">LoadErrorHandlingPolicy</a> 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>​(<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a> 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 <a href="RtspMediaSource.Factory.html" title="class in com.google.android.exoplayer2.source.rtsp">RtspMediaSource.Factory</a> setForceUseRtpTcp​(boolean 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 <a href="RtspMediaSource.Factory.html" title="class in com.google.android.exoplayer2.source.rtsp">RtspMediaSource.Factory</a> setUserAgent​(<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a> 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>
|
||||
|
@ -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 void 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 void maybeThrowSourceInfoRefreshError()</pre>
|
||||
<div class="block"><span class="descfrmTypeLabel">Description copied from interface: <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)">
|
||||
|
@ -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"> </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>​(byte[] scope,
|
||||
byte[] 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<com.google.android.exoplayer2.drm.DrmInitData.SchemeData>, int, java.util.HashMap<java.lang.String, java.lang.String>)</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>​(byte[] 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><<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>,​<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>></code></td>
|
||||
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#queryKeyStatus(byte%5B%5D)">queryKeyStatus</a></span>​(byte[] 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>​(byte[] sessionId,
|
||||
byte[] 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>​(<a href="../drm/ExoMediaDrm.OnEventListener.html" title="interface in com.google.android.exoplayer2.drm">ExoMediaDrm.OnEventListener</a> 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>​(<a href="../drm/ExoMediaDrm.OnExpirationUpdateListener.html" title="interface in com.google.android.exoplayer2.drm">ExoMediaDrm.OnExpirationUpdateListener</a> 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>​(<a href="../drm/ExoMediaDrm.OnKeyStatusChangeListener.html" title="interface in com.google.android.exoplayer2.drm">ExoMediaDrm.OnKeyStatusChangeListener</a> 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>​(<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a> propertyName,
|
||||
byte[] 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>​(<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a> 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> 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>​(<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><byte[]> sessionIdPredicate,
|
||||
int event,
|
||||
int extra,
|
||||
byte[] 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 <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 <a href="https://developer.android.com/reference/java/lang/Class.html" title="class or interface in java.lang" class="externalLink" target="_top">Class</a><com.google.android.exoplayer2.testutil.FakeExoMediaDrm.FakeExoMediaCrypto> getExoMediaCryptoType()</pre>
|
||||
@ -959,6 +976,31 @@ public <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 int 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 void triggerEvent​(<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><byte[]> sessionIdPredicate,
|
||||
int event,
|
||||
int extra,
|
||||
@Nullable
|
||||
byte[] 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>
|
||||
|
@ -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
|
||||
|
@ -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 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: </li>
|
||||
<li>Nested | </li>
|
||||
<li>Field | </li>
|
||||
<li>Constr | </li>
|
||||
<li>Method</li>
|
||||
</ul>
|
||||
<ul class="subNavList">
|
||||
<li>Detail: </li>
|
||||
<li>Field | </li>
|
||||
<li>Constr | </li>
|
||||
<li>Method</li>
|
||||
</ul>
|
||||
</div>
|
||||
<a id="skip.navbar.top">
|
||||
<!-- -->
|
||||
</a></div>
|
||||
<!-- ========= END OF TOP NAVBAR ========= -->
|
||||
</div>
|
||||
<div class="navPadding"> </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> <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 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: </li>
|
||||
<li>Nested | </li>
|
||||
<li>Field | </li>
|
||||
<li>Constr | </li>
|
||||
<li>Method</li>
|
||||
</ul>
|
||||
<ul class="subNavList">
|
||||
<li>Detail: </li>
|
||||
<li>Field | </li>
|
||||
<li>Constr | </li>
|
||||
<li>Method</li>
|
||||
</ul>
|
||||
</div>
|
||||
<a id="skip.navbar.bottom">
|
||||
<!-- -->
|
||||
</a></div>
|
||||
<!-- ======== END OF BOTTOM NAVBAR ======= -->
|
||||
</nav>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
@ -122,9 +122,14 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
|
||||
<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
|
||||
|
@ -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
|
||||
|
@ -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"> </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"> </span></caption>
|
||||
<tr>
|
||||
<th class="colFirst" scope="col">Class</th>
|
||||
|
@ -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>
|
||||
|
@ -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>​(float[] 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>​(int texId,
|
||||
int 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 void setFloats​(float[] 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 void 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>
|
||||
|
@ -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"> </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"> </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"> </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 <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 <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>
|
||||
|
@ -1855,21 +1855,21 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
|
||||
<!-- -->
|
||||
</a><code>public static final <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 static final 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 static final <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"> </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 static final 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 static final 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 static final 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 static final 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 static final 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 static final 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 static final 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"> </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 static final <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 static final <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 static final <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>
|
||||
|
@ -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> </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> </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> </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> </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> </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> </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> </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<String>, long, long, boolean, int, long, int, long, long, boolean, boolean, boolean, DrmInitData, List<HlsMediaPlaylist.Segment>, List<HlsMediaPlaylist.Part>, HlsMediaPlaylist.ServerControl, Map<Uri, HlsMediaPlaylist.RenditionReport>)</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<String>, long, boolean, long, boolean, int, long, int, long, long, boolean, boolean, boolean, DrmInitData, List<HlsMediaPlaylist.Segment>, List<HlsMediaPlaylist.Part>, HlsMediaPlaylist.ServerControl, Map<Uri, HlsMediaPlaylist.RenditionReport>)</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> </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> </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> </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> </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> </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> </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> </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> </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> </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> </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<Format>)</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> </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> </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<byte[]>, 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
Binary file not shown.
@ -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><T>)</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><R,​E> (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><V>)</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><T></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><T></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>
|
||||
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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")) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {}
|
||||
|
@ -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 {}
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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 =
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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. */
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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\"");
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
@ -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(
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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))
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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() {}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
255
testdata/src/test/assets/extractordumps/mp4/sample_mpegh_mha1.mp4.0.dump
vendored
Normal file
255
testdata/src/test/assets/extractordumps/mp4/sample_mpegh_mha1.mp4.0.dump
vendored
Normal 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
|
255
testdata/src/test/assets/extractordumps/mp4/sample_mpegh_mha1.mp4.1.dump
vendored
Normal file
255
testdata/src/test/assets/extractordumps/mp4/sample_mpegh_mha1.mp4.1.dump
vendored
Normal 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
|
155
testdata/src/test/assets/extractordumps/mp4/sample_mpegh_mha1.mp4.2.dump
vendored
Normal file
155
testdata/src/test/assets/extractordumps/mp4/sample_mpegh_mha1.mp4.2.dump
vendored
Normal 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
Loading…
x
Reference in New Issue
Block a user