Commit b2333c86 by Ian Baker Committed by GitHub

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

r2.14.1
parents 9be5ece8 c1b7c47a
Showing with 2616 additions and 572 deletions
......@@ -25,6 +25,8 @@ and extend, and can be updated through Play Store application updates.
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
......
# 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
......
This diff could not be displayed because it is too large.
......@@ -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>
......
<!DOCTYPE HTML>
<!-- NewPage -->
<html lang="en">
<head><!-- start favicons snippet, use https://realfavicongenerator.net/ --><link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png"><link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png"><link rel="manifest" href="/assets/site.webmanifest"><link rel="mask-icon" href="/assets/safari-pinned-tab.svg" color="#fc4d50"><link rel="shortcut icon" href="/assets/favicon.ico"><meta name="msapplication-TileColor" content="#ffc40d"><meta name="msapplication-config" content="/assets/browserconfig.xml"><meta name="theme-color" content="#ffffff"><!-- end favicons snippet -->
<title>MediaMetadata.FolderType (ExoPlayer library)</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="../../../../stylesheet.css" title="Style">
<link rel="stylesheet" type="text/css" href="../../../../jquery/jquery-ui.css" title="Style">
<script type="text/javascript" src="../../../../script.js"></script>
<script type="text/javascript" src="../../../../jquery/jszip/dist/jszip.min.js"></script>
<script type="text/javascript" src="../../../../jquery/jszip-utils/dist/jszip-utils.min.js"></script>
<!--[if IE]>
<script type="text/javascript" src="../../../../jquery/jszip-utils/dist/jszip-utils-ie.min.js"></script>
<![endif]-->
<script type="text/javascript" src="../../../../jquery/jquery-3.5.1.js"></script>
<script type="text/javascript" src="../../../../jquery/jquery-ui.js"></script>
</head>
<body>
<script type="text/javascript"><!--
try {
if (location.href.indexOf('is-external=true') == -1) {
parent.document.title="MediaMetadata.FolderType (ExoPlayer library)";
}
}
catch(err) {
}
//-->
var pathtoroot = "../../../../";
var useModuleDirectories = false;
loadScripts(document, 'script');</script>
<noscript>
<div>JavaScript is disabled on your browser.</div>
</noscript>
<header role="banner">
<nav role="navigation">
<div class="fixedNav">
<!-- ========= START OF TOP NAVBAR ======= -->
<div class="topNav"><a id="navbar.top">
<!-- -->
</a>
<div class="skipNav"><a href="#skip.navbar.top" title="Skip navigation links">Skip navigation links</a></div>
<a id="navbar.top.firstrow">
<!-- -->
</a>
<ul class="navList" title="Navigation">
<li><a href="../../../../index.html">Overview</a></li>
<li><a href="package-summary.html">Package</a></li>
<li class="navBarCell1Rev">Class</li>
<li><a href="package-tree.html">Tree</a></li>
<li><a href="../../../../deprecated-list.html">Deprecated</a></li>
<li><a href="../../../../index-all.html">Index</a></li>
<li><a href="../../../../help-doc.html">Help</a></li>
</ul>
</div>
<div class="subNav">
<ul class="navList" id="allclasses_navbar_top">
<li><a href="../../../../allclasses.html">All&nbsp;Classes</a></li>
</ul>
<ul class="navListSearch">
<li><label for="search">SEARCH:</label>
<input type="text" id="search" value="search" disabled="disabled">
<input type="reset" id="reset" value="reset" disabled="disabled">
</li>
</ul>
<div>
<script type="text/javascript"><!--
allClassesLink = document.getElementById("allclasses_navbar_top");
if(window==top) {
allClassesLink.style.display = "block";
}
else {
allClassesLink.style.display = "none";
}
//-->
</script>
<noscript>
<div>JavaScript is disabled on your browser.</div>
</noscript>
</div>
<div>
<ul class="subNavList">
<li>Summary:&nbsp;</li>
<li>Field&nbsp;|&nbsp;</li>
<li>Required&nbsp;|&nbsp;</li>
<li>Optional</li>
</ul>
<ul class="subNavList">
<li>Detail:&nbsp;</li>
<li>Field&nbsp;|&nbsp;</li>
<li>Element</li>
</ul>
</div>
<a id="skip.navbar.top">
<!-- -->
</a></div>
<!-- ========= END OF TOP NAVBAR ========= -->
</div>
<div class="navPadding">&nbsp;</div>
<script type="text/javascript"><!--
$('.navPadding').css('padding-top', $('.fixedNav').css("height"));
//-->
</script>
</nav>
</header>
<!-- ======== START OF CLASS DATA ======== -->
<main role="main">
<div class="header">
<div class="subTitle"><span class="packageLabelInType">Package</span>&nbsp;<a href="package-summary.html">com.google.android.exoplayer2</a></div>
<h2 title="Annotation Type MediaMetadata.FolderType" class="title">Annotation Type MediaMetadata.FolderType</h2>
</div>
<div class="contentContainer">
<div class="description">
<ul class="blockList">
<li class="blockList">
<hr>
<pre><a href="https://developer.android.com/reference/java/lang/annotation/Documented.html" title="class or interface in java.lang.annotation" class="externalLink" target="_top">@Documented</a>
<a href="https://developer.android.com/reference/java/lang/annotation/Retention.html" title="class or interface in java.lang.annotation" class="externalLink">@Retention</a>(<a href="https://developer.android.com/reference/java/lang/annotation/RetentionPolicy.html?is-external=true#SOURCE" title="class or interface in java.lang.annotation" class="externalLink" target="_top">SOURCE</a>)
public static @interface <span class="memberNameLabel">MediaMetadata.FolderType</span></pre>
<div class="block">The folder type of the media item.
<p>This can be used as the type of a browsable bluetooth folder (see section 6.10.2.2 of the <a href="https://www.bluetooth.com/specifications/specs/a-v-remote-control-profile-1-6-2/">Bluetooth
AVRCP 1.6.2</a>).</div>
</li>
</ul>
</div>
</div>
</main>
<!-- ========= END OF CLASS DATA ========= -->
<footer role="contentinfo">
<nav role="navigation">
<!-- ======= START OF BOTTOM NAVBAR ====== -->
<div class="bottomNav"><a id="navbar.bottom">
<!-- -->
</a>
<div class="skipNav"><a href="#skip.navbar.bottom" title="Skip navigation links">Skip navigation links</a></div>
<a id="navbar.bottom.firstrow">
<!-- -->
</a>
<ul class="navList" title="Navigation">
<li><a href="../../../../index.html">Overview</a></li>
<li><a href="package-summary.html">Package</a></li>
<li class="navBarCell1Rev">Class</li>
<li><a href="package-tree.html">Tree</a></li>
<li><a href="../../../../deprecated-list.html">Deprecated</a></li>
<li><a href="../../../../index-all.html">Index</a></li>
<li><a href="../../../../help-doc.html">Help</a></li>
</ul>
</div>
<div class="subNav">
<ul class="navList" id="allclasses_navbar_bottom">
<li><a href="../../../../allclasses.html">All&nbsp;Classes</a></li>
</ul>
<div>
<script type="text/javascript"><!--
allClassesLink = document.getElementById("allclasses_navbar_bottom");
if(window==top) {
allClassesLink.style.display = "block";
}
else {
allClassesLink.style.display = "none";
}
//-->
</script>
<noscript>
<div>JavaScript is disabled on your browser.</div>
</noscript>
</div>
<div>
<ul class="subNavList">
<li>Summary:&nbsp;</li>
<li>Field&nbsp;|&nbsp;</li>
<li>Required&nbsp;|&nbsp;</li>
<li>Optional</li>
</ul>
<ul class="subNavList">
<li>Detail:&nbsp;</li>
<li>Field&nbsp;|&nbsp;</li>
<li>Element</li>
</ul>
</div>
<a id="skip.navbar.bottom">
<!-- -->
</a></div>
<!-- ======== END OF BOTTOM NAVBAR ======= -->
</nav>
</footer>
</body>
</html>
......@@ -25,7 +25,7 @@
catch(err) {
}
//-->
var data = {"i0":10,"i1":10,"i2":10,"i3":10};
var data = {"i0":10,"i1":10,"i2":10,"i3":10,"i4":10};
var tabs = {65535:["t0","All Methods"],2:["t2","Instance Methods"],8:["t4","Concrete Methods"]};
var altColor = "altColor";
var rowColor = "rowColor";
......@@ -275,11 +275,18 @@ extends <a href="Id3Frame.html" title="class in com.google.android.exoplayer2.me
<td class="colLast">&nbsp;</td>
</tr>
<tr id="i2" class="altColor">
<td class="colFirst"><code>void</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#populateMediaMetadata(com.google.android.exoplayer2.MediaMetadata.Builder)">populateMediaMetadata</a></span>&#8203;(<a href="../../MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a>&nbsp;builder)</code></th>
<td class="colLast">
<div class="block">Updates the <a href="../../MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2"><code>MediaMetadata.Builder</code></a> with the type specific values stored in this Entry.</div>
</td>
</tr>
<tr id="i3" class="rowColor">
<td class="colFirst"><code><a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#toString()">toString</a></span>()</code></th>
<td class="colLast">&nbsp;</td>
</tr>
<tr id="i3" class="rowColor">
<tr id="i4" class="altColor">
<td class="colFirst"><code>void</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#writeToParcel(android.os.Parcel,int)">writeToParcel</a></span>&#8203;(<a href="https://developer.android.com/reference/android/os/Parcel.html" title="class or interface in android.os" class="externalLink" target="_top">Parcel</a>&nbsp;dest,
int&nbsp;flags)</code></th>
......@@ -305,7 +312,7 @@ extends <a href="Id3Frame.html" title="class in com.google.android.exoplayer2.me
<!-- -->
</a>
<h3>Methods inherited from interface&nbsp;com.google.android.exoplayer2.metadata.<a href="../Metadata.Entry.html" title="interface in com.google.android.exoplayer2.metadata">Metadata.Entry</a></h3>
<code><a href="../Metadata.Entry.html#getWrappedMetadataBytes()">getWrappedMetadataBytes</a>, <a href="../Metadata.Entry.html#getWrappedMetadataFormat()">getWrappedMetadataFormat</a>, <a href="../Metadata.Entry.html#populateMediaMetadata(com.google.android.exoplayer2.MediaMetadata.Builder)">populateMediaMetadata</a></code></li>
<code><a href="../Metadata.Entry.html#getWrappedMetadataBytes()">getWrappedMetadataBytes</a>, <a href="../Metadata.Entry.html#getWrappedMetadataFormat()">getWrappedMetadataFormat</a></code></li>
</ul>
</li>
</ul>
......@@ -415,6 +422,24 @@ public final&nbsp;<a href="https://developer.android.com/reference/java/lang/Str
<!-- -->
</a>
<h3>Method Detail</h3>
<a id="populateMediaMetadata(com.google.android.exoplayer2.MediaMetadata.Builder)">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>populateMediaMetadata</h4>
<pre class="methodSignature">public&nbsp;void&nbsp;populateMediaMetadata&#8203;(<a href="../../MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2">MediaMetadata.Builder</a>&nbsp;builder)</pre>
<div class="block"><span class="descfrmTypeLabel">Description copied from interface:&nbsp;<code><a href="../Metadata.Entry.html#populateMediaMetadata(com.google.android.exoplayer2.MediaMetadata.Builder)">Metadata.Entry</a></code></span></div>
<div class="block">Updates the <a href="../../MediaMetadata.Builder.html" title="class in com.google.android.exoplayer2"><code>MediaMetadata.Builder</code></a> with the type specific values stored in this Entry.
<p>The order of the <a href="../Metadata.Entry.html" title="interface in com.google.android.exoplayer2.metadata"><code>Metadata.Entry</code></a> objects in the <a href="../Metadata.html" title="class in com.google.android.exoplayer2.metadata"><code>Metadata</code></a> matters. If two <a href="../Metadata.Entry.html" title="interface in com.google.android.exoplayer2.metadata"><code>Metadata.Entry</code></a> entries attempt to populate the same <a href="../../MediaMetadata.html" title="class in com.google.android.exoplayer2"><code>MediaMetadata</code></a> field, then the last one in
the list is used.</div>
<dl>
<dt><span class="paramLabel">Parameters:</span></dt>
<dd><code>builder</code> - The builder to be updated.</dd>
</dl>
</li>
</ul>
<a id="equals(java.lang.Object)">
<!-- -->
</a>
......
......@@ -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>&lt;<a href="https://developer.android.com/reference/android/net/Uri.html?is-external=true" title="class or interface in android.net" class="externalLink">Uri</a>,&#8203;<a href="HlsMediaPlaylist.RenditionReport.html" title="class in com.google.android.exoplayer2.source.hls.playlist" target="_top">HlsMediaPlaylist.RenditionReport</a>&gt;</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#renditionReports">renditionReports</a></span></code></th>
<td class="colLast">
<div class="block">The rendition reports of alternative rendition playlists.</div>
</td>
</tr>
<tr class="rowColor">
<tr class="altColor">
<td class="colFirst"><code><a href="https://developer.android.com/reference/java/util/List.html" title="class or interface in java.util" class="externalLink">List</a>&lt;<a href="HlsMediaPlaylist.Segment.html" title="class in com.google.android.exoplayer2.source.hls.playlist" target="_top">HlsMediaPlaylist.Segment</a>&gt;</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#segments">segments</a></span></code></th>
<td class="colLast">
<div class="block">The list of segments in the playlist.</div>
</td>
</tr>
<tr class="altColor">
<tr class="rowColor">
<td class="colFirst"><code><a href="HlsMediaPlaylist.ServerControl.html" title="class in com.google.android.exoplayer2.source.hls.playlist">HlsMediaPlaylist.ServerControl</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#serverControl">serverControl</a></span></code></th>
<td class="colLast">
<div class="block">The attributes of the #EXT-X-SERVER-CONTROL header.</div>
</td>
</tr>
<tr class="rowColor">
<tr class="altColor">
<td class="colFirst"><code>long</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#startOffsetUs">startOffsetUs</a></span></code></th>
<td class="colLast">
<div class="block">The start offset in microseconds, as defined by #EXT-X-START.</div>
<div class="block">The start offset in microseconds from the beginning of the playlist, as defined by
#EXT-X-START, or <a href="../../../C.html#TIME_UNSET"><code>C.TIME_UNSET</code></a> if undefined.</div>
</td>
</tr>
<tr class="altColor">
<tr class="rowColor">
<td class="colFirst"><code>long</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#startTimeUs">startTimeUs</a></span></code></th>
<td class="colLast">
<div class="block">If <a href="#hasProgramDateTime"><code>hasProgramDateTime</code></a> is true, contains the datetime as microseconds since epoch.</div>
</td>
</tr>
<tr class="rowColor">
<tr class="altColor">
<td class="colFirst"><code>long</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#targetDurationUs">targetDurationUs</a></span></code></th>
<td class="colLast">
<div class="block">The target duration in microseconds, as defined by #EXT-X-TARGETDURATION.</div>
</td>
</tr>
<tr class="altColor">
<tr class="rowColor">
<td class="colFirst"><code><a href="https://developer.android.com/reference/java/util/List.html" title="class or interface in java.util" class="externalLink">List</a>&lt;<a href="HlsMediaPlaylist.Part.html" title="class in com.google.android.exoplayer2.source.hls.playlist" target="_top">HlsMediaPlaylist.Part</a>&gt;</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#trailingParts">trailingParts</a></span></code></th>
<td class="colLast">
<div class="block">The list of parts at the end of the playlist for which the segment is not in the playlist yet.</div>
</td>
</tr>
<tr class="rowColor">
<tr class="altColor">
<td class="colFirst"><code>int</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#version">version</a></span></code></th>
<td class="colLast">
......@@ -383,10 +391,11 @@ extends <a href="HlsPlaylist.html" title="class in com.google.android.exoplayer2
<th class="colLast" scope="col">Description</th>
</tr>
<tr class="altColor">
<th class="colConstructorName" scope="row"><code><span class="memberNameLink"><a href="#%3Cinit%3E(int,java.lang.String,java.util.List,long,long,boolean,int,long,int,long,long,boolean,boolean,boolean,com.google.android.exoplayer2.drm.DrmInitData,java.util.List,java.util.List,com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.ServerControl,java.util.Map)">HlsMediaPlaylist</a></span>&#8203;(int&nbsp;playlistType,
<th class="colConstructorName" scope="row"><code><span class="memberNameLink"><a href="#%3Cinit%3E(int,java.lang.String,java.util.List,long,boolean,long,boolean,int,long,int,long,long,boolean,boolean,boolean,com.google.android.exoplayer2.drm.DrmInitData,java.util.List,java.util.List,com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.ServerControl,java.util.Map)">HlsMediaPlaylist</a></span>&#8203;(int&nbsp;playlistType,
<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a>&nbsp;baseUri,
<a href="https://developer.android.com/reference/java/util/List.html" title="class or interface in java.util" class="externalLink">List</a>&lt;<a href="https://developer.android.com/reference/java/lang/String.html?is-external=true" title="class or interface in java.lang" class="externalLink" target="_top">String</a>&gt;&nbsp;tags,
long&nbsp;startOffsetUs,
boolean&nbsp;preciseStart,
long&nbsp;startTimeUs,
boolean&nbsp;hasDiscontinuitySequence,
int&nbsp;discontinuitySequence,
......@@ -540,7 +549,19 @@ public final&nbsp;int playlistType</pre>
<li class="blockList">
<h4>startOffsetUs</h4>
<pre>public final&nbsp;long startOffsetUs</pre>
<div class="block">The start offset in microseconds, as defined by #EXT-X-START.</div>
<div class="block">The start offset in microseconds from the beginning of the playlist, as defined by
#EXT-X-START, or <a href="../../../C.html#TIME_UNSET"><code>C.TIME_UNSET</code></a> if undefined. The value is guaranteed to be between 0 and
<a href="#durationUs"><code>durationUs</code></a>, inclusive.</div>
</li>
</ul>
<a id="preciseStart">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>preciseStart</h4>
<pre>public final&nbsp;boolean preciseStart</pre>
<div class="block">Whether the start position should be precise, as defined by #EXT-X-START.</div>
</li>
</ul>
<a id="startTimeUs">
......@@ -710,7 +731,7 @@ public final&nbsp;<a href="../../../drm/DrmInitData.html" title="class in com.go
<!-- -->
</a>
<h3>Constructor Detail</h3>
<a id="&lt;init&gt;(int,java.lang.String,java.util.List,long,long,boolean,int,long,int,long,long,boolean,boolean,boolean,com.google.android.exoplayer2.drm.DrmInitData,java.util.List,java.util.List,com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.ServerControl,java.util.Map)">
<a id="&lt;init&gt;(int,java.lang.String,java.util.List,long,boolean,long,boolean,int,long,int,long,long,boolean,boolean,boolean,com.google.android.exoplayer2.drm.DrmInitData,java.util.List,java.util.List,com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.ServerControl,java.util.Map)">
<!-- -->
</a>
<ul class="blockListLast">
......@@ -721,6 +742,7 @@ public final&nbsp;<a href="../../../drm/DrmInitData.html" title="class in com.go
<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a>&nbsp;baseUri,
<a href="https://developer.android.com/reference/java/util/List.html" title="class or interface in java.util" class="externalLink">List</a>&lt;<a href="https://developer.android.com/reference/java/lang/String.html?is-external=true" title="class or interface in java.lang" class="externalLink" target="_top">String</a>&gt;&nbsp;tags,
long&nbsp;startOffsetUs,
boolean&nbsp;preciseStart,
long&nbsp;startTimeUs,
boolean&nbsp;hasDiscontinuitySequence,
int&nbsp;discontinuitySequence,
......
......@@ -25,7 +25,7 @@
catch(err) {
}
//-->
var data = {"i0":10,"i1":10,"i2":42,"i3":42,"i4":10,"i5":42,"i6":10};
var data = {"i0":10,"i1":10,"i2":42,"i3":42,"i4":10,"i5":42,"i6":10,"i7":10,"i8":10};
var tabs = {65535:["t0","All Methods"],2:["t2","Instance Methods"],8:["t4","Concrete Methods"],32:["t6","Deprecated Methods"]};
var altColor = "altColor";
var rowColor = "rowColor";
......@@ -243,11 +243,25 @@ implements <a href="../MediaSourceFactory.html" title="interface in com.google.a
</tr>
<tr id="i6" class="altColor">
<td class="colFirst"><code><a href="RtspMediaSource.Factory.html" title="class in com.google.android.exoplayer2.source.rtsp">RtspMediaSource.Factory</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setForceUseRtpTcp(boolean)">setForceUseRtpTcp</a></span>&#8203;(boolean&nbsp;forceUseRtpTcp)</code></th>
<td class="colLast">
<div class="block">Sets whether to force using TCP as the default RTP transport.</div>
</td>
</tr>
<tr id="i7" class="rowColor">
<td class="colFirst"><code><a href="RtspMediaSource.Factory.html" title="class in com.google.android.exoplayer2.source.rtsp">RtspMediaSource.Factory</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setLoadErrorHandlingPolicy(com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy)">setLoadErrorHandlingPolicy</a></span>&#8203;(<a href="../../upstream/LoadErrorHandlingPolicy.html" title="interface in com.google.android.exoplayer2.upstream">LoadErrorHandlingPolicy</a>&nbsp;loadErrorHandlingPolicy)</code></th>
<td class="colLast">
<div class="block">Does nothing.</div>
</td>
</tr>
<tr id="i8" class="altColor">
<td class="colFirst"><code><a href="RtspMediaSource.Factory.html" title="class in com.google.android.exoplayer2.source.rtsp">RtspMediaSource.Factory</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setUserAgent(java.lang.String)">setUserAgent</a></span>&#8203;(<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a>&nbsp;userAgent)</code></th>
<td class="colLast">
<div class="block">Sets the user agent, the default value is <a href="../../ExoPlayerLibraryInfo.html#VERSION_SLASHY"><code>ExoPlayerLibraryInfo.VERSION_SLASHY</code></a>.</div>
</td>
</tr>
</table>
<ul class="blockList">
<li class="blockList"><a id="methods.inherited.from.class.java.lang.Object">
......@@ -298,6 +312,43 @@ implements <a href="../MediaSourceFactory.html" title="interface in com.google.a
<!-- -->
</a>
<h3>Method Detail</h3>
<a id="setForceUseRtpTcp(boolean)">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>setForceUseRtpTcp</h4>
<pre class="methodSignature">public&nbsp;<a href="RtspMediaSource.Factory.html" title="class in com.google.android.exoplayer2.source.rtsp">RtspMediaSource.Factory</a>&nbsp;setForceUseRtpTcp&#8203;(boolean&nbsp;forceUseRtpTcp)</pre>
<div class="block">Sets whether to force using TCP as the default RTP transport.
<p>The default value is <code>false</code>, the source will first try streaming RTSP with UDP. If
no data is received on the UDP channel (for instance, when streaming behind a NAT) for a
while, the source will switch to streaming using TCP. If this value is set to <code>true</code>,
the source will always use TCP for streaming.</div>
<dl>
<dt><span class="paramLabel">Parameters:</span></dt>
<dd><code>forceUseRtpTcp</code> - Whether force to use TCP for streaming.</dd>
<dt><span class="returnLabel">Returns:</span></dt>
<dd>This Factory, for convenience.</dd>
</dl>
</li>
</ul>
<a id="setUserAgent(java.lang.String)">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>setUserAgent</h4>
<pre class="methodSignature">public&nbsp;<a href="RtspMediaSource.Factory.html" title="class in com.google.android.exoplayer2.source.rtsp">RtspMediaSource.Factory</a>&nbsp;setUserAgent&#8203;(<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a>&nbsp;userAgent)</pre>
<div class="block">Sets the user agent, the default value is <a href="../../ExoPlayerLibraryInfo.html#VERSION_SLASHY"><code>ExoPlayerLibraryInfo.VERSION_SLASHY</code></a>.</div>
<dl>
<dt><span class="paramLabel">Parameters:</span></dt>
<dd><code>userAgent</code> - The user agent.</dd>
<dt><span class="returnLabel">Returns:</span></dt>
<dd>This Factory, for convenience.</dd>
</dl>
</li>
</ul>
<a id="setDrmSessionManagerProvider(com.google.android.exoplayer2.drm.DrmSessionManagerProvider)">
<!-- -->
</a>
......
......@@ -338,18 +338,13 @@ extends <a href="../BaseMediaSource.html" title="class in com.google.android.exo
<ul class="blockList">
<li class="blockList">
<h4>maybeThrowSourceInfoRefreshError</h4>
<pre class="methodSignature">public&nbsp;void&nbsp;maybeThrowSourceInfoRefreshError()
throws <a href="https://developer.android.com/reference/java/io/IOException.html" title="class or interface in java.io" class="externalLink" target="_top">IOException</a></pre>
<pre class="methodSignature">public&nbsp;void&nbsp;maybeThrowSourceInfoRefreshError()</pre>
<div class="block"><span class="descfrmTypeLabel">Description copied from interface:&nbsp;<code><a href="../MediaSource.html#maybeThrowSourceInfoRefreshError()">MediaSource</a></code></span></div>
<div class="block">Throws any pending error encountered while loading or refreshing source information.
<p>Should not be called directly from application code.
<p>Must only be called after <a href="../MediaSource.html#prepareSource(com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller,com.google.android.exoplayer2.upstream.TransferListener)"><code>MediaSource.prepareSource(MediaSourceCaller, TransferListener)</code></a>.</div>
<dl>
<dt><span class="throwsLabel">Throws:</span></dt>
<dd><code><a href="https://developer.android.com/reference/java/io/IOException.html" title="class or interface in java.io" class="externalLink" target="_top">IOException</a></code></dd>
</dl>
</li>
</ul>
<a id="createPeriod(com.google.android.exoplayer2.source.MediaSource.MediaPeriodId,com.google.android.exoplayer2.upstream.Allocator,long)">
......
......@@ -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
......
<!DOCTYPE HTML>
<!-- NewPage -->
<html lang="en">
<head><!-- start favicons snippet, use https://realfavicongenerator.net/ --><link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon.png"><link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16x16.png"><link rel="manifest" href="/assets/site.webmanifest"><link rel="mask-icon" href="/assets/safari-pinned-tab.svg" color="#fc4d50"><link rel="shortcut icon" href="/assets/favicon.ico"><meta name="msapplication-TileColor" content="#ffc40d"><meta name="msapplication-config" content="/assets/browserconfig.xml"><meta name="theme-color" content="#ffffff"><!-- end favicons snippet -->
<title>LanguageFeatureSpan (ExoPlayer library)</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="../../../../../../stylesheet.css" title="Style">
<link rel="stylesheet" type="text/css" href="../../../../../../jquery/jquery-ui.css" title="Style">
<script type="text/javascript" src="../../../../../../script.js"></script>
<script type="text/javascript" src="../../../../../../jquery/jszip/dist/jszip.min.js"></script>
<script type="text/javascript" src="../../../../../../jquery/jszip-utils/dist/jszip-utils.min.js"></script>
<!--[if IE]>
<script type="text/javascript" src="../../../../../../jquery/jszip-utils/dist/jszip-utils-ie.min.js"></script>
<![endif]-->
<script type="text/javascript" src="../../../../../../jquery/jquery-3.5.1.js"></script>
<script type="text/javascript" src="../../../../../../jquery/jquery-ui.js"></script>
</head>
<body>
<script type="text/javascript"><!--
try {
if (location.href.indexOf('is-external=true') == -1) {
parent.document.title="LanguageFeatureSpan (ExoPlayer library)";
}
}
catch(err) {
}
//-->
var pathtoroot = "../../../../../../";
var useModuleDirectories = false;
loadScripts(document, 'script');</script>
<noscript>
<div>JavaScript is disabled on your browser.</div>
</noscript>
<header role="banner">
<nav role="navigation">
<div class="fixedNav">
<!-- ========= START OF TOP NAVBAR ======= -->
<div class="topNav"><a id="navbar.top">
<!-- -->
</a>
<div class="skipNav"><a href="#skip.navbar.top" title="Skip navigation links">Skip navigation links</a></div>
<a id="navbar.top.firstrow">
<!-- -->
</a>
<ul class="navList" title="Navigation">
<li><a href="../../../../../../index.html">Overview</a></li>
<li><a href="package-summary.html">Package</a></li>
<li class="navBarCell1Rev">Class</li>
<li><a href="package-tree.html">Tree</a></li>
<li><a href="../../../../../../deprecated-list.html">Deprecated</a></li>
<li><a href="../../../../../../index-all.html">Index</a></li>
<li><a href="../../../../../../help-doc.html">Help</a></li>
</ul>
</div>
<div class="subNav">
<ul class="navList" id="allclasses_navbar_top">
<li><a href="../../../../../../allclasses.html">All&nbsp;Classes</a></li>
</ul>
<ul class="navListSearch">
<li><label for="search">SEARCH:</label>
<input type="text" id="search" value="search" disabled="disabled">
<input type="reset" id="reset" value="reset" disabled="disabled">
</li>
</ul>
<div>
<script type="text/javascript"><!--
allClassesLink = document.getElementById("allclasses_navbar_top");
if(window==top) {
allClassesLink.style.display = "block";
}
else {
allClassesLink.style.display = "none";
}
//-->
</script>
<noscript>
<div>JavaScript is disabled on your browser.</div>
</noscript>
</div>
<div>
<ul class="subNavList">
<li>Summary:&nbsp;</li>
<li>Nested&nbsp;|&nbsp;</li>
<li>Field&nbsp;|&nbsp;</li>
<li>Constr&nbsp;|&nbsp;</li>
<li>Method</li>
</ul>
<ul class="subNavList">
<li>Detail:&nbsp;</li>
<li>Field&nbsp;|&nbsp;</li>
<li>Constr&nbsp;|&nbsp;</li>
<li>Method</li>
</ul>
</div>
<a id="skip.navbar.top">
<!-- -->
</a></div>
<!-- ========= END OF TOP NAVBAR ========= -->
</div>
<div class="navPadding">&nbsp;</div>
<script type="text/javascript"><!--
$('.navPadding').css('padding-top', $('.fixedNav').css("height"));
//-->
</script>
</nav>
</header>
<!-- ======== START OF CLASS DATA ======== -->
<main role="main">
<div class="header">
<div class="subTitle"><span class="packageLabelInType">Package</span>&nbsp;<a href="package-summary.html">com.google.android.exoplayer2.text.span</a></div>
<h2 title="Interface LanguageFeatureSpan" class="title">Interface LanguageFeatureSpan</h2>
</div>
<div class="contentContainer">
<div class="description">
<ul class="blockList">
<li class="blockList">
<dl>
<dt>All Known Implementing Classes:</dt>
<dd><code><a href="HorizontalTextInVerticalContextSpan.html" title="class in com.google.android.exoplayer2.text.span">HorizontalTextInVerticalContextSpan</a></code>, <code><a href="RubySpan.html" title="class in com.google.android.exoplayer2.text.span">RubySpan</a></code>, <code><a href="TextEmphasisSpan.html" title="class in com.google.android.exoplayer2.text.span">TextEmphasisSpan</a></code></dd>
</dl>
<hr>
<pre>public interface <span class="typeNameLabel">LanguageFeatureSpan</span></pre>
<div class="block">Marker interface for span classes that carry language features rather than style information.</div>
</li>
</ul>
</div>
</div>
</main>
<!-- ========= END OF CLASS DATA ========= -->
<footer role="contentinfo">
<nav role="navigation">
<!-- ======= START OF BOTTOM NAVBAR ====== -->
<div class="bottomNav"><a id="navbar.bottom">
<!-- -->
</a>
<div class="skipNav"><a href="#skip.navbar.bottom" title="Skip navigation links">Skip navigation links</a></div>
<a id="navbar.bottom.firstrow">
<!-- -->
</a>
<ul class="navList" title="Navigation">
<li><a href="../../../../../../index.html">Overview</a></li>
<li><a href="package-summary.html">Package</a></li>
<li class="navBarCell1Rev">Class</li>
<li><a href="package-tree.html">Tree</a></li>
<li><a href="../../../../../../deprecated-list.html">Deprecated</a></li>
<li><a href="../../../../../../index-all.html">Index</a></li>
<li><a href="../../../../../../help-doc.html">Help</a></li>
</ul>
</div>
<div class="subNav">
<ul class="navList" id="allclasses_navbar_bottom">
<li><a href="../../../../../../allclasses.html">All&nbsp;Classes</a></li>
</ul>
<div>
<script type="text/javascript"><!--
allClassesLink = document.getElementById("allclasses_navbar_bottom");
if(window==top) {
allClassesLink.style.display = "block";
}
else {
allClassesLink.style.display = "none";
}
//-->
</script>
<noscript>
<div>JavaScript is disabled on your browser.</div>
</noscript>
</div>
<div>
<ul class="subNavList">
<li>Summary:&nbsp;</li>
<li>Nested&nbsp;|&nbsp;</li>
<li>Field&nbsp;|&nbsp;</li>
<li>Constr&nbsp;|&nbsp;</li>
<li>Method</li>
</ul>
<ul class="subNavList">
<li>Detail:&nbsp;</li>
<li>Field&nbsp;|&nbsp;</li>
<li>Constr&nbsp;|&nbsp;</li>
<li>Method</li>
</ul>
</div>
<a id="skip.navbar.bottom">
<!-- -->
</a></div>
<!-- ======== END OF BOTTOM NAVBAR ======= -->
</nav>
</footer>
</body>
</html>
......@@ -122,9 +122,14 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<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">&nbsp;</span></caption>
<tr>
<th class="colFirst" scope="col">Interface</th>
<th class="colLast" scope="col">Description</th>
</tr>
<tbody>
<tr class="altColor">
<th class="colFirst" scope="row"><a href="LanguageFeatureSpan.html" title="interface in com.google.android.exoplayer2.text.span">LanguageFeatureSpan</a></th>
<td class="colLast">
<div class="block">Marker interface for span classes that carry language features rather than style information.</div>
</td>
</tr>
</tbody>
</table>
</li>
<li class="blockList">
<table class="typeSummary">
<caption><span>Class Summary</span><span class="tabEnd">&nbsp;</span></caption>
<tr>
<th class="colFirst" scope="col">Class</th>
......
......@@ -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>&#8203;(float[]&nbsp;value)</code></th>
<td class="colLast">
<div class="block">Configures <a href="#bind()"><code>bind()</code></a> to use the specified float[] <code>value</code> for this uniform.</div>
</td>
</tr>
<tr id="i3" class="rowColor">
<td class="colFirst"><code>void</code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#setSamplerTexId(int,int)">setSamplerTexId</a></span>&#8203;(int&nbsp;texId,
int&nbsp;unit)</code></th>
<td class="colLast">
......@@ -325,6 +331,16 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<div class="block">Configures <a href="#bind()"><code>bind()</code></a> to use the specified float <code>value</code> for this uniform.</div>
</li>
</ul>
<a id="setFloats(float[])">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>setFloats</h4>
<pre class="methodSignature">public&nbsp;void&nbsp;setFloats&#8203;(float[]&nbsp;value)</pre>
<div class="block">Configures <a href="#bind()"><code>bind()</code></a> to use the specified float[] <code>value</code> for this uniform.</div>
</li>
</ul>
<a id="bind()">
<!-- -->
</a>
......@@ -332,8 +348,7 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
<li class="blockList">
<h4>bind</h4>
<pre class="methodSignature">public&nbsp;void&nbsp;bind()</pre>
<div class="block">Sets the uniform to whatever value was passed via <a href="#setSamplerTexId(int,int)"><code>setSamplerTexId(int, int)</code></a> or
<a href="#setFloat(float)"><code>setFloat(float)</code></a>.
<div class="block">Sets the uniform to whatever value was passed via <a href="#setSamplerTexId(int,int)"><code>setSamplerTexId(int, int)</code></a>, <a href="#setFloat(float)"><code>setFloat(float)</code></a> or <a href="#setFloats(float%5B%5D)"><code>setFloats(float[])</code></a>.
<p>Should be called before each drawing call.</div>
</li>
......
......@@ -379,6 +379,16 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
</tr>
<tr class="rowColor">
<td class="colFirst"><code>static <a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#AUDIO_MPEGH_MHA1">AUDIO_MPEGH_MHA1</a></span></code></th>
<td class="colLast">&nbsp;</td>
</tr>
<tr class="altColor">
<td class="colFirst"><code>static <a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#AUDIO_MPEGH_MHM1">AUDIO_MPEGH_MHM1</a></span></code></th>
<td class="colLast">&nbsp;</td>
</tr>
<tr class="rowColor">
<td class="colFirst"><code>static <a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td>
<th class="colSecond" scope="row"><code><span class="memberNameLink"><a href="#AUDIO_MSGSM">AUDIO_MSGSM</a></span></code></th>
<td class="colLast">&nbsp;</td>
</tr>
......@@ -1155,6 +1165,32 @@ extends <a href="https://developer.android.com/reference/java/lang/Object.html"
</dl>
</li>
</ul>
<a id="AUDIO_MPEGH_MHA1">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>AUDIO_MPEGH_MHA1</h4>
<pre>public static final&nbsp;<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a> AUDIO_MPEGH_MHA1</pre>
<dl>
<dt><span class="seeLabel">See Also:</span></dt>
<dd><a href="../../../../../constant-values.html#com.google.android.exoplayer2.util.MimeTypes.AUDIO_MPEGH_MHA1">Constant Field Values</a></dd>
</dl>
</li>
</ul>
<a id="AUDIO_MPEGH_MHM1">
<!-- -->
</a>
<ul class="blockList">
<li class="blockList">
<h4>AUDIO_MPEGH_MHM1</h4>
<pre>public static final&nbsp;<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a> AUDIO_MPEGH_MHM1</pre>
<dl>
<dt><span class="seeLabel">See Also:</span></dt>
<dd><a href="../../../../../constant-values.html#com.google.android.exoplayer2.util.MimeTypes.AUDIO_MPEGH_MHM1">Constant Field Values</a></dd>
</dl>
</li>
</ul>
<a id="AUDIO_RAW">
<!-- -->
</a>
......
......@@ -1855,21 +1855,21 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/ExoPlayerLibraryInfo.html#VERSION">VERSION</a></code></th>
<td class="colLast"><code>"2.14.0"</code></td>
<td class="colLast"><code>"2.14.1"</code></td>
</tr>
<tr class="rowColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.ExoPlayerLibraryInfo.VERSION_INT">
<!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;int</code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/ExoPlayerLibraryInfo.html#VERSION_INT">VERSION_INT</a></code></th>
<td class="colLast"><code>2014000</code></td>
<td class="colLast"><code>2014001</code></td>
</tr>
<tr class="altColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.ExoPlayerLibraryInfo.VERSION_SLASHY">
<!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/ExoPlayerLibraryInfo.html#VERSION_SLASHY">VERSION_SLASHY</a></code></th>
<td class="colLast"><code>"ExoPlayerLib/2.14.0"</code></td>
<td class="colLast"><code>"ExoPlayerLib/2.14.1"</code></td>
</tr>
</tbody>
</table>
......@@ -1961,6 +1961,67 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
</li>
<li class="blockList">
<table class="constantsSummary">
<caption><span>com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/MediaMetadata.html" title="class in com.google.android.exoplayer2">MediaMetadata</a></span><span class="tabEnd">&nbsp;</span></caption>
<tr>
<th class="colFirst" scope="col">Modifier and Type</th>
<th class="colSecond" scope="col">Constant Field</th>
<th class="colLast" scope="col">Value</th>
</tr>
<tbody>
<tr class="altColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.MediaMetadata.FOLDER_TYPE_ALBUMS">
<!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;int</code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/MediaMetadata.html#FOLDER_TYPE_ALBUMS">FOLDER_TYPE_ALBUMS</a></code></th>
<td class="colLast"><code>2</code></td>
</tr>
<tr class="rowColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.MediaMetadata.FOLDER_TYPE_ARTISTS">
<!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;int</code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/MediaMetadata.html#FOLDER_TYPE_ARTISTS">FOLDER_TYPE_ARTISTS</a></code></th>
<td class="colLast"><code>3</code></td>
</tr>
<tr class="altColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.MediaMetadata.FOLDER_TYPE_GENRES">
<!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;int</code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/MediaMetadata.html#FOLDER_TYPE_GENRES">FOLDER_TYPE_GENRES</a></code></th>
<td class="colLast"><code>4</code></td>
</tr>
<tr class="rowColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.MediaMetadata.FOLDER_TYPE_MIXED">
<!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;int</code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/MediaMetadata.html#FOLDER_TYPE_MIXED">FOLDER_TYPE_MIXED</a></code></th>
<td class="colLast"><code>0</code></td>
</tr>
<tr class="altColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.MediaMetadata.FOLDER_TYPE_PLAYLISTS">
<!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;int</code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/MediaMetadata.html#FOLDER_TYPE_PLAYLISTS">FOLDER_TYPE_PLAYLISTS</a></code></th>
<td class="colLast"><code>5</code></td>
</tr>
<tr class="rowColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.MediaMetadata.FOLDER_TYPE_TITLES">
<!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;int</code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/MediaMetadata.html#FOLDER_TYPE_TITLES">FOLDER_TYPE_TITLES</a></code></th>
<td class="colLast"><code>1</code></td>
</tr>
<tr class="altColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.MediaMetadata.FOLDER_TYPE_YEARS">
<!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;int</code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/MediaMetadata.html#FOLDER_TYPE_YEARS">FOLDER_TYPE_YEARS</a></code></th>
<td class="colLast"><code>6</code></td>
</tr>
</tbody>
</table>
</li>
<li class="blockList">
<table class="constantsSummary">
<caption><span>com.google.android.exoplayer2.<a href="com/google/android/exoplayer2/Player.html" title="interface in com.google.android.exoplayer2">Player</a></span><span class="tabEnd">&nbsp;</span></caption>
<tr>
<th class="colFirst" scope="col">Modifier and Type</th>
......@@ -8902,6 +8963,20 @@ $('.navPadding').css('padding-top', $('.fixedNav').css("height"));
<td class="colLast"><code>"audio/mpeg-L2"</code></td>
</tr>
<tr class="rowColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.util.MimeTypes.AUDIO_MPEGH_MHA1">
<!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/util/MimeTypes.html#AUDIO_MPEGH_MHA1">AUDIO_MPEGH_MHA1</a></code></th>
<td class="colLast"><code>"audio/mha1"</code></td>
</tr>
<tr class="altColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.util.MimeTypes.AUDIO_MPEGH_MHM1">
<!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td>
<th class="colSecond" scope="row"><code><a href="com/google/android/exoplayer2/util/MimeTypes.html#AUDIO_MPEGH_MHM1">AUDIO_MPEGH_MHM1</a></code></th>
<td class="colLast"><code>"audio/mhm1"</code></td>
</tr>
<tr class="rowColor">
<td class="colFirst"><a id="com.google.android.exoplayer2.util.MimeTypes.AUDIO_MSGSM">
<!-- -->
</a><code>public&nbsp;static&nbsp;final&nbsp;<a href="https://developer.android.com/reference/java/lang/String.html" title="class or interface in java.lang" class="externalLink" target="_top">String</a></code></td>
......
This diff could not be displayed because it is too large.
This diff could not be displayed because it is too large.
......@@ -24,24 +24,11 @@ These steps are described in more detail below. For a complete example, refer to
## 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.
......
......@@ -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;
......@@ -51,6 +52,11 @@ public final class ApicFrame extends Id3Frame {
}
@Override
public void populateMediaMetadata(MediaMetadata.Builder builder) {
builder.setArtworkData(pictureData);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
......
......@@ -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 builderSetArtworkData_setsArtworkData() {
byte[] bytes = new byte[] {35, 12, 6, 77};
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setArtworkData(bytes).build();
assertThat(Arrays.equals(mediaMetadata.artworkData, bytes)).isTrue();
}
@Test
public void builderSetArworkUri_setsArtworkUri() {
Uri uri = Uri.parse("https://www.google.com");
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setArtworkUri(uri).build();
assertThat(mediaMetadata.artworkUri).isEqualTo(uri);
}
@Test
public void roundTripViaBundle_yieldsEqualInstance() {
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setTitle("title").build();
Bundle extras = new Bundle();
extras.putString("exampleKey", "exampleValue");
MediaMetadata mediaMetadata =
new MediaMetadata.Builder()
.setTitle("title")
.setAlbumArtist("the artist")
.setMediaUri(Uri.parse("https://www.google.com"))
.setUserRating(new HeartRating(false))
.setOverallRating(new PercentageRating(87.4f))
.setArtworkData(new byte[] {-88, 12, 3, 2, 124, -54, -33, 69})
.setTrackNumber(4)
.setTotalTrackCount(12)
.setFolderType(MediaMetadata.FOLDER_TYPE_PLAYLISTS)
.setIsPlayable(true)
.setYear(2000)
.setExtras(extras) // Extras is not implemented in MediaMetadata.equals(Object o).
.build();
assertThat(MediaMetadata.CREATOR.fromBundle(mediaMetadata.toBundle())).isEqualTo(mediaMetadata);
MediaMetadata fromBundle = MediaMetadata.CREATOR.fromBundle(mediaMetadata.toBundle());
assertThat(fromBundle).isEqualTo(mediaMetadata);
assertThat(fromBundle.extras.getString("exampleKey")).isEqualTo("exampleValue");
}
@Test
public void builderPopulatedFromMetadataEntry_setsTitleCorrectly() {
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 {}
/*
* 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;
......@@ -179,6 +182,49 @@ public class DefaultDrmSessionManagerTest {
}
@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 =
ImmutableList.of(DRM_SCHEME_DATAS.get(0).copyWithData(TestUtil.createByteArray(4, 5, 6)));
......@@ -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();
......
......@@ -122,6 +122,15 @@ import java.util.List;
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;
@SuppressWarnings("ConstantCaseForConstants")
......
......@@ -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";
......
......@@ -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,
......
......@@ -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();
}
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source.rtsp;
import android.net.Uri;
import android.util.Base64;
import androidx.annotation.IntDef;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.source.rtsp.RtspMessageUtil.RtspAuthUserInfo;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/** Wraps RTSP authentication information. */
/* package */ final class RtspAuthenticationInfo {
/** The supported authentication methods. */
@Retention(RetentionPolicy.SOURCE)
@IntDef({BASIC, DIGEST})
@interface AuthenticationMechanism {}
/** HTTP basic authentication (RFC2068 Section 11.1). */
public static final int BASIC = 1;
/** HTTP digest authentication (RFC2069). */
public static final int DIGEST = 2;
private static final String DIGEST_FORMAT =
"Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\"";
private static final String DIGEST_FORMAT_WITH_OPAQUE =
"Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\","
+ " opaque=\"%s\"";
private static final String ALGORITHM = "MD5";
/** The authentication mechanism. */
@AuthenticationMechanism public final int authenticationMechanism;
/** The authentication realm. */
public final String realm;
/** The nonce used in digest authentication; empty if using {@link #BASIC} authentication. */
public final String nonce;
/** The opaque used in digest authentication; empty if using {@link #BASIC} authentication. */
public final String opaque;
/**
* Creates a new instance.
*
* @param authenticationMechanism The authentication mechanism, as defined by {@link
* AuthenticationMechanism}.
* @param realm The authentication realm.
* @param nonce The nonce in digest authentication; empty if using {@link #BASIC} authentication.
* @param opaque The opaque in digest authentication; empty if using {@link #BASIC}
* authentication.
*/
public RtspAuthenticationInfo(
@AuthenticationMechanism int authenticationMechanism,
String realm,
String nonce,
String opaque) {
this.authenticationMechanism = authenticationMechanism;
this.realm = realm;
this.nonce = nonce;
this.opaque = opaque;
}
/**
* Gets the string value for {@link RtspHeaders#AUTHORIZATION} header.
*
* @param authUserInfo The {@link RtspAuthUserInfo} for authentication.
* @param uri The request {@link Uri}.
* @param requestMethod The request method, defined in {@link RtspRequest.Method}.
* @return The string value for {@link RtspHeaders#AUTHORIZATION} header.
* @throws ParserException If the MD5 algorithm is not supported by {@link MessageDigest}.
*/
public String getAuthorizationHeaderValue(
RtspAuthUserInfo authUserInfo, Uri uri, @RtspRequest.Method int requestMethod)
throws ParserException {
switch (authenticationMechanism) {
case BASIC:
return getBasicAuthorizationHeaderValue(authUserInfo);
case DIGEST:
return getDigestAuthorizationHeaderValue(authUserInfo, uri, requestMethod);
default:
throw new ParserException(new UnsupportedOperationException());
}
}
private String getBasicAuthorizationHeaderValue(RtspAuthUserInfo authUserInfo) {
return Base64.encodeToString(
RtspMessageUtil.getStringBytes(authUserInfo.username + ":" + authUserInfo.password),
Base64.DEFAULT);
}
private String getDigestAuthorizationHeaderValue(
RtspAuthUserInfo authUserInfo, Uri uri, @RtspRequest.Method int requestMethod)
throws ParserException {
try {
MessageDigest md = MessageDigest.getInstance(ALGORITHM);
String methodName = RtspMessageUtil.toMethodString(requestMethod);
// From RFC2069 Section 2.1.2:
// response-digest = H( H(A1) ":" unquoted nonce-value ":" H(A2) )
// A1 = unquoted username-value ":" unquoted realm-value ":" password
// A2 = Method ":" request-uri
// H(x) = MD5(x)
String hashA1 =
Util.toHexString(
md.digest(
RtspMessageUtil.getStringBytes(
authUserInfo.username + ":" + realm + ":" + authUserInfo.password)));
String hashA2 =
Util.toHexString(md.digest(RtspMessageUtil.getStringBytes(methodName + ":" + uri)));
String response =
Util.toHexString(
md.digest(RtspMessageUtil.getStringBytes(hashA1 + ":" + nonce + ":" + hashA2)));
if (opaque.isEmpty()) {
return Util.formatInvariant(
DIGEST_FORMAT, authUserInfo.username, realm, nonce, uri, response);
} else {
return Util.formatInvariant(
DIGEST_FORMAT_WITH_OPAQUE, authUserInfo.username, realm, nonce, uri, response, opaque);
}
} catch (NoSuchAlgorithmException e) {
throw new ParserException(e);
}
}
}
......@@ -20,9 +20,8 @@ import androidx.annotation.Nullable;
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();
}
}
......@@ -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) {
......
......@@ -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;
......
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source.rtsp;
import com.google.android.exoplayer2.ParserException;
import com.google.common.collect.ImmutableList;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/** A value wrapper for a dumped RTP packet stream. */
/* package */ class RtpPacketStreamDump {
/** The name of the RTP track. */
public final String trackName;
/** The sequence number of the first RTP packet in the dump file. */
public final int firstSequenceNumber;
/** The timestamp of the first RTP packet in the dump file. */
public final long firstTimestamp;
/** The interval between transmitting two consecutive RTP packets, in milliseconds. */
public final long transmissionIntervalMs;
/** The description of the dumped media in SDP(RFC2327) format. */
public final String mediaDescription;
/** A list of hex strings. Each hex string represents a binary RTP packet. */
public final ImmutableList<String> packets;
/**
* Parses a JSON string into an {@code RtpPacketStreamDump}.
*
* <p>The input JSON must include the following key-value pairs:
*
* <ul>
* <li>Key: "trackName", Value type: String. The name of the RTP track.
* <li>Key: "firstSequenceNumber", Value type: int. The sequence number of the first RTP packet
* in the dump file.
* <li>Key: "firstTimestamp", Value type: long. The timestamp of the first RTP packet in the
* dump file.
* <li>Key: "transmissionIntervalMs", Value type: long. The interval between transmitting two
* consecutive RTP packets, in milliseconds.
* <li>Key: "mediaDescription", Value type: String. The description of the dumped media in
* SDP(RFC2327) format.
* <li>Key: "packets", Value type: Array of hex strings. Each element is a hex string
* representing an RTP packet's binary data.
* </ul>
*
* @param jsonString The JSON string that contains the dumped RTP packets and metadata.
* @return The parsed {@code RtpDumpFile}.
* @throws ParserException If the argument does not contain all required key-value pairs, or there
* are incorrect values.
*/
public static RtpPacketStreamDump parse(String jsonString) throws ParserException {
try {
JSONObject jsonObject = new JSONObject(jsonString);
String trackName = jsonObject.getString("trackName");
int firstSequenceNumber = jsonObject.getInt("firstSequenceNumber");
long firstTimestamp = jsonObject.getLong("firstTimestamp");
long transmissionIntervalMs = jsonObject.getLong("transmitIntervalMs");
String mediaDescription = jsonObject.getString("mediaDescription");
ImmutableList.Builder<String> packetsBuilder = new ImmutableList.Builder<>();
JSONArray jsonPackets = jsonObject.getJSONArray("packets");
for (int i = 0; i < jsonPackets.length(); i++) {
packetsBuilder.add(jsonPackets.getString(i));
}
return new RtpPacketStreamDump(
trackName,
firstSequenceNumber,
firstTimestamp,
transmissionIntervalMs,
mediaDescription,
packetsBuilder.build());
} catch (JSONException e) {
throw new ParserException(e);
}
}
private RtpPacketStreamDump(
String trackName,
int firstSequenceNumber,
long firstTimestamp,
long transmissionIntervalMs,
String mediaDescription,
ImmutableList<String> packets) {
this.trackName = trackName;
this.firstSequenceNumber = firstSequenceNumber;
this.firstTimestamp = firstTimestamp;
this.transmissionIntervalMs = transmissionIntervalMs;
this.mediaDescription = mediaDescription;
this.packets = ImmutableList.copyOf(packets);
}
}
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source.rtsp;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.source.rtsp.RtspMessageUtil.RtspAuthUserInfo;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link RtspAuthenticationInfo}. */
@RunWith(AndroidJUnit4.class)
public class RtspAuthenticationInfoTest {
@Test
public void getAuthorizationHeaderValue_withBasicAuthenticationMechanism_getsCorrectHeaderValue()
throws Exception {
String authenticationRealm = "WallyWorld";
String username = "Aladdin";
String password = "open sesame";
String expectedAuthorizationHeaderValue = "QWxhZGRpbjpvcGVuIHNlc2FtZQ==\n";
RtspAuthenticationInfo authenticator =
new RtspAuthenticationInfo(
RtspAuthenticationInfo.BASIC, authenticationRealm, /* nonce= */ "", /* opaque= */ "");
assertThat(
authenticator.getAuthorizationHeaderValue(
new RtspAuthUserInfo(username, password), Uri.EMPTY, RtspRequest.METHOD_DESCRIBE))
.isEqualTo(expectedAuthorizationHeaderValue);
}
@Test
public void getAuthorizationHeaderValue_withDigestAuthenticationMechanism_getsCorrectHeaderValue()
throws Exception {
RtspAuthenticationInfo authenticator =
new RtspAuthenticationInfo(
RtspAuthenticationInfo.DIGEST,
/* realm= */ "LIVE555 Streaming Media",
/* nonce= */ "0cdfe9719e7373b7d5bb2913e2115f3f",
/* opaque= */ "5ccc069c403ebaf9f0171e9517f40e41");
assertThat(
authenticator.getAuthorizationHeaderValue(
new RtspAuthUserInfo("username", "password"),
Uri.parse("rtsp://localhost:554/imax_cd_2k_264_6ch.mkv"),
RtspRequest.METHOD_DESCRIBE))
.isEqualTo(
"Digest username=\"username\", realm=\"LIVE555 Streaming Media\","
+ " nonce=\"0cdfe9719e7373b7d5bb2913e2115f3f\","
+ " uri=\"rtsp://localhost:554/imax_cd_2k_264_6ch.mkv\","
+ " response=\"ba9433847439387776f7fb905db3fcae\","
+ " opaque=\"5ccc069c403ebaf9f0171e9517f40e41\"");
}
}
......@@ -19,9 +19,13 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
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");
}
@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(
"Accept", "application/sdp",
"CSeq", "3",
"Content-Length", "707",
"Transport", "RTP/AVP;unicast;client_port=65458-65459");
"RTP/AVP;unicast;client_port=65456-65457", "RTP/AVP;unicast;client_port=65458-65459")
.inOrder();
}
}
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.exoplayer2.source.rtsp;
import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runMainLooperUntil;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.source.MediaPeriod;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
import com.google.common.collect.ImmutableList;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link RtspMediaPeriod}. */
@RunWith(AndroidJUnit4.class)
public class RtspMediaPeriodTest {
private static final RtspClient PLACEHOLDER_RTSP_CLIENT =
new RtspClient(
new RtspClient.SessionInfoListener() {
@Override
public void onSessionTimelineUpdated(
RtspSessionTiming timing, ImmutableList<RtspMediaTrack> tracks) {}
@Override
public void onSessionTimelineRequestFailed(String message, @Nullable Throwable cause) {}
},
/* userAgent= */ null,
Uri.EMPTY);
@Test
public void prepare_startsLoading() throws Exception {
RtspMediaPeriod rtspMediaPeriod =
new RtspMediaPeriod(
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
ImmutableList.of(
new RtspMediaTrack(
new MediaDescription.Builder(
/* mediaType= */ MediaDescription.MEDIA_TYPE_VIDEO,
/* port= */ 0,
/* transportProtocol= */ MediaDescription.RTP_AVP_PROFILE,
/* payloadType= */ 96)
.setConnection("IN IP4 0.0.0.0")
.setBitrate(500_000)
.addAttribute(SessionDescription.ATTR_RTPMAP, "96 H264/90000")
.addAttribute(
SessionDescription.ATTR_FMTP,
"96 packetization-mode=1;profile-level-id=64001F;sprop-parameter-sets=Z2QAH6zZQPARabIAAAMACAAAAwGcHjBjLA==,aOvjyyLA")
.addAttribute(SessionDescription.ATTR_CONTROL, "track1")
.build(),
Uri.parse("rtsp://localhost/test"))),
PLACEHOLDER_RTSP_CLIENT,
new UdpDataSourceRtpDataChannelFactory());
AtomicBoolean prepareCallbackCalled = new AtomicBoolean(false);
rtspMediaPeriod.prepare(
new MediaPeriod.Callback() {
@Override
public void onPrepared(MediaPeriod mediaPeriod) {
prepareCallbackCalled.set(true);
}
@Override
public void onContinueLoadingRequested(MediaPeriod source) {
source.continueLoading(/* positionUs= */ 0);
}
},
/* positionUs= */ 0);
runMainLooperUntil(prepareCallbackCalled::get);
rtspMediaPeriod.release();
}
@Test
public void getBufferedPositionUs_withNoRtspMediaTracks_returnsEndOfSource() {
RtspMediaPeriod rtspMediaPeriod =
new RtspMediaPeriod(
new DefaultAllocator(/* trimOnReset= */ true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
ImmutableList.of(),
PLACEHOLDER_RTSP_CLIENT,
new UdpDataSourceRtpDataChannelFactory());
assertThat(rtspMediaPeriod.getBufferedPositionUs()).isEqualTo(C.TIME_END_OF_SOURCE);
}
}
......@@ -176,6 +176,23 @@ public class RtspMediaTrackTest {
@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() {
MediaDescription mediaDescription =
new MediaDescription.Builder(MEDIA_TYPE_AUDIO, 0, RTP_AVP_PROFILE, 97)
......@@ -224,24 +241,6 @@ public class RtspMediaTrackTest {
@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() {
MediaDescription mediaDescription =
new MediaDescription.Builder(MEDIA_TYPE_VIDEO, 0, RTP_AVP_PROFILE, 96)
......
......@@ -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(
......
......@@ -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;
......@@ -150,6 +151,35 @@ public class SessionDescriptionTest {
}
@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(
IllegalStateException.class,
......
......@@ -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;
......@@ -29,11 +31,29 @@ import org.junit.runner.RunWith;
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))
......
/*
* 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() {}
}
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.google.android.exoplayer2.ui;
import static com.google.android.exoplayer2.testutil.truth.SpannedSubject.assertThat;
import static com.google.common.truth.Truth.assertThat;
import android.graphics.Color;
import android.text.Layout;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.UnderlineSpan;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
import com.google.android.exoplayer2.text.span.RubySpan;
import com.google.android.exoplayer2.text.span.TextAnnotation;
import com.google.android.exoplayer2.text.span.TextEmphasisSpan;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link SubtitleView}. */
@RunWith(AndroidJUnit4.class)
public class SubtitleViewUtilsTest {
private static final Cue CUE = buildCue();
@Test
public void testRemoveAllEmbeddedStyling() {
Cue.Builder cueBuilder = CUE.buildUpon();
SubtitleViewUtils.removeAllEmbeddedStyling(cueBuilder);
Cue strippedCue = cueBuilder.build();
Spanned originalText = (Spanned) CUE.text;
Spanned strippedText = (Spanned) strippedCue.text;
// Assert all non styling properties and spans are kept
assertThat(strippedCue.textAlignment).isEqualTo(CUE.textAlignment);
assertThat(strippedCue.multiRowAlignment).isEqualTo(CUE.multiRowAlignment);
assertThat(strippedCue.line).isEqualTo(CUE.line);
assertThat(strippedCue.lineType).isEqualTo(CUE.lineType);
assertThat(strippedCue.position).isEqualTo(CUE.position);
assertThat(strippedCue.positionAnchor).isEqualTo(CUE.positionAnchor);
assertThat(strippedCue.textSize).isEqualTo(Cue.DIMEN_UNSET);
assertThat(strippedCue.textSizeType).isEqualTo(Cue.TYPE_UNSET);
assertThat(strippedCue.size).isEqualTo(CUE.size);
assertThat(strippedCue.verticalType).isEqualTo(CUE.verticalType);
assertThat(strippedCue.shearDegrees).isEqualTo(CUE.shearDegrees);
TextEmphasisSpan expectedTextEmphasisSpan =
originalText.getSpans(0, originalText.length(), TextEmphasisSpan.class)[0];
assertThat(strippedText)
.hasTextEmphasisSpanBetween(
originalText.getSpanStart(expectedTextEmphasisSpan),
originalText.getSpanEnd(expectedTextEmphasisSpan));
RubySpan expectedRubySpan = originalText.getSpans(0, originalText.length(), RubySpan.class)[0];
assertThat(strippedText)
.hasRubySpanBetween(
originalText.getSpanStart(expectedRubySpan), originalText.getSpanEnd(expectedRubySpan));
HorizontalTextInVerticalContextSpan expectedHorizontalTextInVerticalContextSpan =
originalText
.getSpans(0, originalText.length(), HorizontalTextInVerticalContextSpan.class)[0];
assertThat(strippedText)
.hasHorizontalTextInVerticalContextSpanBetween(
originalText.getSpanStart(expectedHorizontalTextInVerticalContextSpan),
originalText.getSpanEnd(expectedHorizontalTextInVerticalContextSpan));
// Assert all styling properties and spans are removed
assertThat(strippedCue.windowColorSet).isFalse();
assertThat(strippedText).hasNoUnderlineSpanBetween(0, strippedText.length());
assertThat(strippedText).hasNoRelativeSizeSpanBetween(0, strippedText.length());
assertThat(strippedText).hasNoAbsoluteSizeSpanBetween(0, strippedText.length());
}
@Test
public void testRemoveEmbeddedFontSizes() {
Cue.Builder cueBuilder = CUE.buildUpon();
SubtitleViewUtils.removeEmbeddedFontSizes(cueBuilder);
Cue strippedCue = cueBuilder.build();
Spanned originalText = (Spanned) CUE.text;
Spanned strippedText = (Spanned) strippedCue.text;
// Assert all non text-size properties and spans are kept
assertThat(strippedCue.textAlignment).isEqualTo(CUE.textAlignment);
assertThat(strippedCue.multiRowAlignment).isEqualTo(CUE.multiRowAlignment);
assertThat(strippedCue.line).isEqualTo(CUE.line);
assertThat(strippedCue.lineType).isEqualTo(CUE.lineType);
assertThat(strippedCue.position).isEqualTo(CUE.position);
assertThat(strippedCue.positionAnchor).isEqualTo(CUE.positionAnchor);
assertThat(strippedCue.size).isEqualTo(CUE.size);
assertThat(strippedCue.windowColor).isEqualTo(CUE.windowColor);
assertThat(strippedCue.windowColorSet).isEqualTo(CUE.windowColorSet);
assertThat(strippedCue.verticalType).isEqualTo(CUE.verticalType);
assertThat(strippedCue.shearDegrees).isEqualTo(CUE.shearDegrees);
TextEmphasisSpan expectedTextEmphasisSpan =
originalText.getSpans(0, originalText.length(), TextEmphasisSpan.class)[0];
assertThat(strippedText)
.hasTextEmphasisSpanBetween(
originalText.getSpanStart(expectedTextEmphasisSpan),
originalText.getSpanEnd(expectedTextEmphasisSpan));
RubySpan expectedRubySpan = originalText.getSpans(0, originalText.length(), RubySpan.class)[0];
assertThat(strippedText)
.hasRubySpanBetween(
originalText.getSpanStart(expectedRubySpan), originalText.getSpanEnd(expectedRubySpan));
HorizontalTextInVerticalContextSpan expectedHorizontalTextInVerticalContextSpan =
originalText
.getSpans(0, originalText.length(), HorizontalTextInVerticalContextSpan.class)[0];
assertThat(strippedText)
.hasHorizontalTextInVerticalContextSpanBetween(
originalText.getSpanStart(expectedHorizontalTextInVerticalContextSpan),
originalText.getSpanEnd(expectedHorizontalTextInVerticalContextSpan));
UnderlineSpan expectedUnderlineSpan =
originalText.getSpans(0, originalText.length(), UnderlineSpan.class)[0];
assertThat(strippedText)
.hasUnderlineSpanBetween(
originalText.getSpanStart(expectedUnderlineSpan),
originalText.getSpanEnd(expectedUnderlineSpan));
// Assert the text-size properties and spans are removed
assertThat(strippedCue.textSize).isEqualTo(Cue.DIMEN_UNSET);
assertThat(strippedCue.textSizeType).isEqualTo(Cue.TYPE_UNSET);
assertThat(strippedText).hasNoRelativeSizeSpanBetween(0, strippedText.length());
assertThat(strippedText).hasNoAbsoluteSizeSpanBetween(0, strippedText.length());
}
private static Cue buildCue() {
SpannableString spanned =
new SpannableString("TextEmphasis おはよ Ruby ございます 123 Underline RelativeSize AbsoluteSize");
spanned.setSpan(
new TextEmphasisSpan(
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_BEFORE),
"Text emphasis ".length(),
"Text emphasis おはよ".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spanned.setSpan(
new RubySpan("おはよ", TextAnnotation.POSITION_BEFORE),
"TextEmphasis おはよ Ruby ".length(),
"TextEmphasis おはよ Ruby ございます".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spanned.setSpan(
new HorizontalTextInVerticalContextSpan(),
"TextEmphasis おはよ Ruby ございます ".length(),
"TextEmphasis おはよ Ruby ございます 123".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spanned.setSpan(
new UnderlineSpan(),
"TextEmphasis おはよ Ruby ございます 123 ".length(),
"TextEmphasis おはよ Ruby ございます 123 Underline".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spanned.setSpan(
new RelativeSizeSpan(1f),
"TextEmphasis おはよ Ruby ございます 123 Underline ".length(),
"TextEmphasis おはよ Ruby ございます 123 Underline RelativeSize".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spanned.setSpan(
new AbsoluteSizeSpan(10),
"TextEmphasis おはよ Ruby ございます 123 Underline RelativeSize ".length(),
"TextEmphasis おはよ Ruby ございます 123 Underline RelativeSize AbsoluteSize".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return new Cue.Builder()
.setText(spanned)
.setTextAlignment(Layout.Alignment.ALIGN_CENTER)
.setMultiRowAlignment(Layout.Alignment.ALIGN_NORMAL)
.setLine(5, Cue.LINE_TYPE_NUMBER)
.setLineAnchor(Cue.ANCHOR_TYPE_END)
.setPosition(0.4f)
.setPositionAnchor(Cue.ANCHOR_TYPE_MIDDLE)
.setTextSize(0.2f, Cue.TEXT_SIZE_TYPE_FRACTIONAL)
.setSize(0.8f)
.setWindowColor(Color.CYAN)
.setVerticalType(Cue.VERTICAL_TYPE_RL)
.setShearDegrees(-15f)
.build();
}
}
This diff could not be displayed because it is too large.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment