Commit 4f365cef by Dustin Committed by GitHub

Merge branch 'google:dev-v2' into dev-v2

parents bf1a1565 91511039
Showing with 2205 additions and 479 deletions
......@@ -4,7 +4,7 @@
* Core library:
* Support preferred video role flags in track selection
((#9402)[https://github.com/google/ExoPlayer/issues/9402]).
([#9402](https://github.com/google/ExoPlayer/issues/9402)).
* Prefer audio content preferences (for example, "default" audio track or
track matching system Locale language) over technical track selection
constraints (for example, preferred MIME type, or maximum channel
......@@ -13,24 +13,34 @@
can always be made distinguishable by setting an `id` in the
`TrackGroup` constructor. This fixes a crash when resuming playback
after backgrounding the app with an active track override
((#9718)[https://github.com/google/ExoPlayer/issues/9718]).
([#9718](https://github.com/google/ExoPlayer/issues/9718)).
* Sleep and retry when creating a `MediaCodec` instance fails. This works
around an issue that occurs on some devices when switching a surface
from a secure codec to another codec
((#8696)[https://github.com/google/ExoPlayer/issues/8696]).
([#8696](https://github.com/google/ExoPlayer/issues/8696)).
* Add `MediaCodecAdapter.getMetrics()` to allow users obtain metrics data
from `MediaCodec`.
([#9766](https://github.com/google/ExoPlayer/issues/9766)).
* Amend logic in `AdaptiveTrackSelection` to allow a quality increase
under sufficient network bandwidth even if playback is very close to the
live edge ((#9784)[https://github.com/google/ExoPlayer/issues/9784]).
live edge ([#9784](https://github.com/google/ExoPlayer/issues/9784)).
* Fix Maven dependency resolution
((#8353)[https://github.com/google/ExoPlayer/issues/8353]).
([#8353](https://github.com/google/ExoPlayer/issues/8353)).
* Fix decoder fallback logic for Dolby Atmos (E-AC3-JOC) and Dolby Vision
to use a compatible base decoder (E-AC3 or H264/H265) if needed.
* Disable automatic speed adjustment for live streams that neither have
low-latency features nor a user request setting the speed
((#9329)[https://github.com/google/ExoPlayer/issues/9329]).
([#9329](https://github.com/google/ExoPlayer/issues/9329)).
* Update video track selection logic to take preferred MIME types and role
flags into account when selecting multiple video tracks for adaptation
([#9519](https://github.com/google/ExoPlayer/issues/9519)).
* Update video and audio track selection logic to only choose formats for
adaptive selections that have the same level of decoder and hardware
support ([#9565](https://github.com/google/ExoPlayer/issues/9565)).
* Update video track selection logic to prefer more efficient codecs if
multiple codecs are supported by primary, hardware-accelerated decoders
([#4835](https://github.com/google/ExoPlayer/issues/4835)).
* Rename `DecoderCounters#inputBufferCount` to `queuedInputBufferCount`.
* Android 12 compatibility:
* Upgrade the Cast extension to depend on
`com.google.android.gms:play-services-cast-framework:20.1.0`. Earlier
......@@ -43,28 +53,40 @@
constructors.
* Change `AudioCapabilities` APIs to require passing explicitly
`AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES` instead of `null`.
* Allow customization of the `AudioTrack` buffer size calculation by
injecting an `AudioTrackBufferSizeProvider` to `DefaultAudioSink`.
([#8891](https://github.com/google/ExoPlayer/issues/8891)).
* Extractors:
* Fix inconsistency with spec in H.265 SPS nal units parsing
((#9719)[https://github.com/google/ExoPlayer/issues/9719]).
([#9719](https://github.com/google/ExoPlayer/issues/9719)).
* Parse Vorbis Comments (including `METADATA_BLOCK_PICTURE`) in Ogg Opus
and Vorbis files.
* Text:
* Add a `MediaItem.SubtitleConfiguration#id` field which is propagated to
the `Format#id` field of the subtitle track created from the
configuration
((#9673)[https://github.com/google/ExoPlayer/issues/9673]).
* Rename `DecoderCounters#inputBufferCount` to `queuedInputBufferCount`.
([#9673](https://github.com/google/ExoPlayer/issues/9673)).
* Add basic support for WebVTT subtitles in Matroska containers
([#9886](https://github.com/google/ExoPlayer/issues/9886)).
* DRM:
* Remove `playbackLooper` from `DrmSessionManager.(pre)acquireSession`.
When a `DrmSessionManager` is used by an app in a custom `MediaSource`,
the `playbackLooper` needs to be passed to `DrmSessionManager.setPlayer`
instead.
* IMA:
* Ad playback / IMA:
* Add a method to `AdPlaybackState` to allow resetting an ad group so that
it can be played again
([#9615](https://github.com/google/ExoPlayer/issues/9615)).
* Enforce playback speed of 1.0 during ad playback
([#9018](https://github.com/google/ExoPlayer/issues/9018)).
* DASH:
* Support the `forced-subtitle` track role
([#9727](https://github.com/google/ExoPlayer/issues/9727)).
* Stop interpreting the `main` track role as `C.SELECTION_FLAG_DEFAULT`.
* Fix bug when base URLs have been assigned the same service location and
priority in manifests that do not declare the dvb namespace. This
prevents the exclusion logic to exclude base URL when they actually
should be used as a fallback base URL.
* HLS:
* Use chunkless preparation by default to improve start up time. If your
renditions contain muxed closed-caption tracks that are *not* declared
......@@ -81,23 +103,36 @@
* Fix the color of the numbers in `StyledPlayerView` rewind and
fastforward buttons when using certain themes
([#9765](https://github.com/google/ExoPlayer/issues/9765)).
* Correctly translate playback speed strings
([#9811](https://github.com/google/ExoPlayer/issues/9811)).
* Transformer:
* Increase required min API version to 21.
* `TransformationException` is now used to describe errors that occur
during a transformation.
* Add `TransformationRequest` for specifying the transformation options.
* Allow multiple listeners to be registered.
* Fix Transformer being stuck when the codec output is partially read.
* Fix potential NPE in `Transformer.getProgress` when releasing the muxer
throws.
* Add a demo app for applying transformations.
* MediaSession extension:
* Remove deprecated call to `onStop(/* reset= */ true)` and provide an
opt-out flag for apps that don't want to clear the playlist on stop.
* RTSP:
* Provide a client API to override the `SocketFactory` used for any server
connection ([#9606](https://github.com/google/ExoPlayer/pull/9606)).
* Prefers DIGEST authentication method over BASIC if both are present.
* Prefers DIGEST authentication method over BASIC if both are present
([#9800](https://github.com/google/ExoPlayer/issues/9800)).
* Handle when RTSP track timing is not available
([#9775](https://github.com/google/ExoPlayer/issues/9775)).
* Ignores invalid RTP-Info header values
([#9619](https://github.com/google/ExoPlayer/issues/9619)).
* Cast extension
* Fix bug that prevented `CastPlayer` from calling `onIsPlayingChanged`
correctly.
correctly ([#9792](https://github.com/google/ExoPlayer/issues/9792)).
* Support audio metadata including artwork with
`DefaultMediaItemConverter`
([#9663](https://github.com/google/ExoPlayer/issues/9663)).
* Remove deprecated symbols:
* Remove `MediaSourceFactory#setDrmSessionManager`,
`MediaSourceFactory#setDrmHttpDataSourceFactory`, and
......@@ -114,6 +149,8 @@
`MediaItem.LiveConfiguration.Builder#setTargetOffsetMs` to override the
manifest, or `DashMediaSource#setFallbackTargetLiveOffsetMs` to provide
a fallback value.
* Remove `(Simple)ExoPlayer.setThrowsWhenUsingWrongThread`. Opting out of
the thread enforcement is no longer possible.
### 2.16.1 (2021-11-18)
......
......@@ -37,6 +37,7 @@ project.ext {
androidxAnnotationVersion = '1.3.0'
androidxAppCompatVersion = '1.3.1'
androidxCollectionVersion = '1.1.0'
androidxConstraintLayoutVersion = '2.0.4'
androidxCoreVersion = '1.7.0'
androidxFuturesVersion = '1.1.0'
androidxMediaVersion = '1.4.3'
......
......@@ -223,10 +223,12 @@ import java.util.ArrayList;
if (currentPlayer != localPlayer || tracksInfo == lastSeenTrackGroupInfo) {
return;
}
if (!tracksInfo.isTypeSupportedOrEmpty(C.TRACK_TYPE_VIDEO)) {
if (!tracksInfo.isTypeSupportedOrEmpty(
C.TRACK_TYPE_VIDEO, /* allowExceedsCapabilities= */ true)) {
listener.onUnsupportedTrack(C.TRACK_TYPE_VIDEO);
}
if (!tracksInfo.isTypeSupportedOrEmpty(C.TRACK_TYPE_AUDIO)) {
if (!tracksInfo.isTypeSupportedOrEmpty(
C.TRACK_TYPE_AUDIO, /* allowExceedsCapabilities= */ true)) {
listener.onUnsupportedTrack(C.TRACK_TYPE_AUDIO);
}
lastSeenTrackGroupInfo = tracksInfo;
......
......@@ -15,19 +15,19 @@
#extension GL_OES_EGL_image_external : require
precision mediump float;
// External texture containing video decoder output.
uniform samplerExternalOES tex_sampler_0;
uniform samplerExternalOES uTexSampler0;
// Texture containing the overlap bitmap.
uniform sampler2D tex_sampler_1;
uniform sampler2D uTexSampler1;
// Horizontal scaling factor for the overlap bitmap.
uniform float scaleX;
uniform float uScaleX;
// Vertical scaling factory for the overlap bitmap.
uniform float scaleY;
varying vec2 v_texcoord;
uniform float uScaleY;
varying vec2 vTexCoords;
void main() {
vec4 videoColor = texture2D(tex_sampler_0, v_texcoord);
vec4 overlayColor = texture2D(tex_sampler_1,
vec2(v_texcoord.x * scaleX,
v_texcoord.y * scaleY));
vec4 videoColor = texture2D(uTexSampler0, vTexCoords);
vec4 overlayColor = texture2D(uTexSampler1,
vec2(vTexCoords.x * uScaleX,
vTexCoords.y * uScaleY));
// Blend the video decoder output and the overlay bitmap.
gl_FragColor = videoColor * (1.0 - overlayColor.a)
+ overlayColor * overlayColor.a;
......
......@@ -11,11 +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 vec4 a_position;
attribute vec4 a_texcoord;
uniform mat4 tex_transform;
varying vec2 v_texcoord;
attribute vec4 aFramePosition;
attribute vec4 aTexCoords;
uniform mat4 uTexTransform;
varying vec2 vTexCoords;
void main() {
gl_Position = a_position;
v_texcoord = (tex_transform * a_texcoord).xy;
gl_Position = aFramePosition;
vTexCoords = (uTexTransform * aTexCoords).xy;
}
......@@ -86,9 +86,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
throw new IllegalStateException(e);
}
program.setBufferAttribute(
"a_position", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT);
"aFramePosition", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT);
program.setBufferAttribute(
"a_texcoord", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT);
"aTexCoords", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT);
GLES20.glGenTextures(1, textures, 0);
GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
......@@ -118,11 +118,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// Run the shader program.
GlUtil.Program program = checkNotNull(this.program);
program.setSamplerTexIdUniform("tex_sampler_0", frameTexture, /* unit= */ 0);
program.setSamplerTexIdUniform("tex_sampler_1", textures[0], /* unit= */ 1);
program.setFloatUniform("scaleX", bitmapScaleX);
program.setFloatUniform("scaleY", bitmapScaleY);
program.setFloatsUniform("tex_transform", transformMatrix);
program.setSamplerTexIdUniform("uTexSampler0", frameTexture, /* unit= */ 0);
program.setSamplerTexIdUniform("uTexSampler1", textures[0], /* unit= */ 1);
program.setFloatUniform("uScaleX", bitmapScaleX);
program.setFloatUniform("uScaleY", bitmapScaleY);
program.setFloatsUniform("uTexTransform", transformMatrix);
program.bindAttributesAndUniforms();
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
......
......@@ -441,10 +441,12 @@ public class PlayerActivity extends AppCompatActivity
if (tracksInfo == lastSeenTracksInfo) {
return;
}
if (!tracksInfo.isTypeSupportedOrEmpty(C.TRACK_TYPE_VIDEO)) {
if (!tracksInfo.isTypeSupportedOrEmpty(
C.TRACK_TYPE_VIDEO, /* allowExceedsCapabilities= */ true)) {
showToast(R.string.error_unsupported_video);
}
if (!tracksInfo.isTypeSupportedOrEmpty(C.TRACK_TYPE_AUDIO)) {
if (!tracksInfo.isTypeSupportedOrEmpty(
C.TRACK_TYPE_AUDIO, /* allowExceedsCapabilities= */ true)) {
showToast(R.string.error_unsupported_audio);
}
lastSeenTracksInfo = tracksInfo;
......
......@@ -22,12 +22,14 @@
<uses-sdk/>
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/application_name"
android:exported="true">
<activity android:name=".MainActivity">
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/application_name"
android:exported="true">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
......
# Transformer demo
This app demonstrates how to use the [Transformer][] API to modify videos, for
example by removing audio or video.
See the [demos README](../README.md) for instructions on how to build and run
this demo.
[Transformer]: https://exoplayer.dev/transforming-media.html
/*
* 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.
*/
apply from: '../../constants.gradle'
apply plugin: 'com.android.application'
android {
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
minSdkVersion 21
targetSdkVersion project.ext.appTargetSdkVersion
multiDexEnabled true
}
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt')
signingConfig signingConfigs.debug
}
}
lintOptions {
// This demo app isn't indexed and doesn't have translations.
disable 'GoogleAppIndexingWarning','MissingTranslation'
}
}
dependencies {
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.constraintlayout:constraintlayout:' + androidxConstraintLayoutVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation 'com.google.android.material:material:' + androidxMaterialVersion
implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-transformer')
implementation project(modulePrefix + 'library-ui')
}
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.google.android.exoplayer2.transformerdemo">
<uses-sdk />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat"
android:taskAffinity=""
tools:targetApi="29">
<activity android:name=".ConfigurationActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
android:launchMode="singleTop"
android:label="@string/app_name"
android:exported="true"
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="com.google.android.exoplayer2.transformerdemo.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="http"/>
<data android:scheme="https"/>
<data android:scheme="content"/>
<data android:scheme="asset"/>
<data android:scheme="file"/>
</intent-filter>
</activity>
<activity android:name=".TransformerActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
android:launchMode="singleTop"
android:label="@string/app_name"
android:exported="true"
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar"/>
</application>
</manifest>
/*
* 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.
*/
@NonNullApi
package com.google.android.exoplayer2.transformerdemo;
import com.google.android.exoplayer2.util.NonNullApi;
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ConfigurationActivity">
<TextView
android:id="@+id/configuration_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:text="@string/configuration"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/choose_file_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:text="@string/choose_file_title"
app:layout_constraintTop_toBottomOf="@+id/configuration_text_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/chosen_file_text_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:paddingLeft="24dp"
android:paddingRight="24dp"
android:textSize="12sp"
android:gravity="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/choose_file_button" />
<TableLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:stretchColumns="1"
android:layout_marginTop="32dp"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:measureWithLargestChild="true"
android:paddingLeft="24dp"
android:paddingRight="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/chosen_file_text_view" >
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/remove_audio" />
<CheckBox
android:id="@+id/remove_audio_checkbox"
android:layout_gravity="right"/>
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/remove_video"/>
<CheckBox
android:id="@+id/remove_video_checkbox"
android:layout_gravity="right" />
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/flatten_for_slow_motion"/>
<CheckBox
android:id="@+id/flatten_for_slow_motion_checkbox"
android:layout_gravity="right" />
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:id="@+id/audio_mime_text_view"
android:text="@string/audio_mime"/>
<Spinner
android:id="@+id/audio_mime_spinner"
android:layout_gravity="right|center_vertical"
android:gravity="right" />
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:id="@+id/video_mime_text_view"
android:text="@string/video_mime"/>
<Spinner
android:id="@+id/video_mime_spinner"
android:layout_gravity="right|center_vertical"
android:gravity="right" />
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:id="@+id/resolution_height_text_view"
android:text="@string/resolution_height"/>
<Spinner
android:id="@+id/resolution_height_spinner"
android:layout_gravity="right|center_vertical"
android:gravity="right" />
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:id="@+id/translate"
android:text="@string/translate"/>
<Spinner
android:id="@+id/translate_spinner"
android:layout_gravity="right|center_vertical"
android:gravity="right" />
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:id="@+id/scale"
android:text="@string/scale"/>
<Spinner
android:id="@+id/scale_spinner"
android:layout_gravity="right|center_vertical"
android:gravity="right" />
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:id="@+id/rotate"
android:text="@string/rotate"/>
<Spinner
android:id="@+id/rotate_spinner"
android:layout_gravity="right|center_vertical"
android:gravity="right" />
</TableRow>
</TableLayout>
<Button
android:id="@+id/transform_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="28dp"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:text="@string/transform"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:gravity="left|center_vertical"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:textIsSelectable="false" />
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:keepScreenOn="true"
android:orientation="vertical">
<com.google.android.material.card.MaterialCardView
android:layout_margin="8dp"
android:layout_height="wrap_content"
android:layout_width="match_parent"
app:cardCornerRadius="4dp"
app:cardElevation="2dp"
android:gravity="center_vertical" >
<TextView
android:id="@+id/information_text_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="8dp"
android:paddingBottom="8dp" />
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_margin="16dp"
app:cardCornerRadius="4dp"
app:cardElevation="2dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.exoplayer2.ui.StyledPlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@+id/debug_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:textSize="10sp"
tools:ignore="SmallSp"/>
<LinearLayout
android:id="@+id/progress_view_group"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:orientation="vertical">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp"
android:paddingBottom="16dp"
android:layout_gravity="center" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/debug_preview" />
<com.google.android.exoplayer2.ui.AspectRatioFrameLayout
android:id="@+id/debug_aspect_ratio_frame_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/debug_preview_not_available" />
</com.google.android.exoplayer2.ui.AspectRatioFrameLayout>
</LinearLayout>
</FrameLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name" translatable="false">Transformer Demo</string>
<string name="configuration" translatable="false">Configuration</string>
<string name="choose_file_title" translatable="false">Choose File</string>
<string name="remove_audio" translatable="false">Remove audio</string>
<string name="remove_video" translatable="false">Remove video</string>
<string name="flatten_for_slow_motion" translatable="false">Flatten for slow motion</string>
<string name="audio_mime" translatable="false">Output audio MIME type</string>
<string name="video_mime" translatable="false">Output video MIME type</string>
<string name="resolution_height" translatable="false">Output video resolution</string>
<string name="translate" translatable="false">Translate video</string>
<string name="scale" translatable="false">Scale video</string>
<string name="rotate" translatable="false">Rotate video (degrees)</string>
<string name="transform" translatable="false">Transform</string>
<string name="debug_preview" translatable="false">Debug preview:</string>
<string name="debug_preview_not_available" translatable="false">No debug preview available</string>
<string name="transformation_started" translatable="false">Transformation started</string>
<string name="transformation_timer" translatable="false">Transformation started %d seconds ago.</string>
<string name="transformation_completed" translatable="false">Transformation completed in %d seconds.</string>
<string name="transformation_error" translatable="false">Transformation error</string>
</resources>
......@@ -128,7 +128,7 @@ containing the same content at different bitrates.
An Android API for playing audio.
For more information, see the
[Javadoc](https://developer.android.com/reference/android/media/AudioTrack).
[Javadoc]({{ site.android_sdk }}/android/media/AudioTrack).
###### CDM
......@@ -137,7 +137,7 @@ decrypting [DRM](#drm) protected content. CDMs are accessed via Android’s
[`MediaDrm`](#mediadrm) API.
For more information, see the
[Javadoc](https://developer.android.com/reference/android/media/MediaDrm).
[Javadoc]({{ site.android_sdk }}/android/media/MediaDrm).
###### IMA
......@@ -153,14 +153,14 @@ An Android API for accessing media [codecs](#codec) (i.e. encoder and decoder
components) in the platform.
For more information, see the
[Javadoc](https://developer.android.com/reference/android/media/MediaCodec).
[Javadoc]({{ site.android_sdk }}/android/media/MediaCodec).
###### MediaDrm
An Android API for accessing [CDMs](#cdm) in the platform.
For more information, see the
[Javadoc](https://developer.android.com/reference/android/media/MediaDrm).
[Javadoc]({{ site.android_sdk }}/android/media/MediaDrm).
###### Audio offload
......@@ -181,7 +181,7 @@ For more information, see the
###### Surface
See the [Javadoc](https://developer.android.com/reference/android/view/Surface)
See the [Javadoc]({{ site.android_sdk }}/android/view/Surface)
and the
[Android graphics documentation](https://source.android.com/devices/graphics/arch-sh).
......@@ -212,14 +212,14 @@ transfers. In [adaptive streaming](#adaptive-streaming), bandwidth estimates can
be used to select between different bitrate [tracks](#track) during playback.
For more information, see the component
[Javadoc](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/upstream/BandwidthMeter.html).
[Javadoc]({{ site.exo_sdk }}/upstream/BandwidthMeter.html).
###### DataSource
Component for requesting data (e.g. over HTTP, from a local file, etc).
For more information, see the component
[Javadoc](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/upstream/DataSource.html).
[Javadoc]({{ site.exo_sdk }}/upstream/DataSource.html).
###### Extractor
......@@ -228,7 +228,7 @@ Component that parses a media [container](#container) format, outputting
belonging to each track suitable for consumption by a decoder.
For more information, see the component
[Javadoc](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/extractor/Extractor.html).
[Javadoc]({{ site.exo_sdk }}/extractor/Extractor.html).
###### LoadControl
......@@ -236,7 +236,7 @@ Component that decides when to start and stop loading, and when to start
playback.
For more information, see the component
[Javadoc](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/LoadControl.html).
[Javadoc]({{ site.exo_sdk }}/LoadControl.html).
###### MediaSource
......@@ -245,7 +245,7 @@ Provides high-level information about the structure of media (as a
(corresponding to periods of the `Timeline`) for playback.
For more information, see the component
[Javadoc](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/source/MediaSource.html).
[Javadoc]({{ site.exo_sdk }}/source/MediaSource.html).
###### MediaPeriod
......@@ -257,7 +257,7 @@ media are loaded and when loading starts and stops are made by the
respectively.
For more information, see the component
[Javadoc](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/source/MediaPeriod.html).
[Javadoc]({{ site.exo_sdk }}/source/MediaPeriod.html).
###### Renderer
......@@ -266,7 +266,7 @@ and [`AudioTrack`](#audiotrack) are the standard Android platform components to
which video and audio data are rendered.
For more information, see the component
[Javadoc](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/Renderer.html).
[Javadoc]({{ site.exo_sdk }}/Renderer.html).
###### Timeline
......@@ -275,7 +275,7 @@ through to complex compositions of media such as playlists and streams with
inserted ads.
For more information, see the component
[Javadoc](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/Timeline.html).
[Javadoc]({{ site.exo_sdk }}/Timeline.html).
###### TrackGroup
......@@ -284,7 +284,7 @@ content, normally at different bitrates for
[adaptive streaming](#adaptive-streaming).
For more information, see the component
[Javadoc](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/source/TrackGroup.html).
[Javadoc]({{ site.exo_sdk }}/source/TrackGroup.html).
###### TrackSelection
......@@ -295,7 +295,7 @@ responsible for selecting the appropriate track whenever a new media chunk
starts being loaded.
For more information, see the component
[Javadoc](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/trackselection/TrackSelection.html).
[Javadoc]({{ site.exo_sdk }}/trackselection/TrackSelection.html).
###### TrackSelector
......@@ -305,4 +305,4 @@ player’s [`Renderers`](#renderer), a `TrackSelector` will generate a
[`TrackSelection`](#trackselection) for each `Renderer`.
For more information, see the component
[Javadoc](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/trackselection/TrackSelector.html).
[Javadoc]({{ site.exo_sdk }}/trackselection/TrackSelector.html).
......@@ -119,11 +119,7 @@ which the player must be accessed can be queried using
If you see `IllegalStateException` being thrown with the message "Player is
accessed on the wrong thread", then some code in your app is accessing an
`ExoPlayer` instance on the wrong thread (the exception's stack trace shows you
where). You can temporarily opt out from these exceptions being thrown by
calling `ExoPlayer.setThrowsWhenUsingWrongThread(false)`, in which case the
issue will be logged as a warning instead. Using this opt out is not safe and
may result in unexpected or obscure errors. It will be removed in ExoPlayer
2.16.
where).
{:.info}
For more information about ExoPlayer's threading model, see the
......
......@@ -12,7 +12,7 @@ events is easy:
// Add a listener to receive events from the player.
player.addListener(listener);
~~~
{: .language-java}
{: .language-java }
`Player.Listener` has empty default methods, so you only need to implement
the methods you're interested in. See the [Javadoc][] for a full description of
......@@ -195,7 +195,7 @@ additional logging with a single line.
```
player.addAnalyticsListener(new EventLogger(trackSelector));
```
{: .language-java}
{: .language-java }
Passing the `trackSelector` enables additional logging, but is optional and so
`null` can be passed instead. See the [debug logging page][] for more details.
......@@ -220,7 +220,7 @@ player
// Do something at the specified playback position.
})
.setLooper(Looper.getMainLooper())
.setPosition(/* windowIndex= */ 0, /* positionMs= */ 120_000)
.setPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 120_000)
.setPayload(customPayloadData)
.setDeleteAfterDelivery(false)
.send();
......
......@@ -51,9 +51,9 @@ methods, as listed below and shown in the following figure.
`Timeline`. The current `Timeline.Window` can be retrieved from the `Timeline`
using `Player.getCurrentWindowIndex` and `Timeline.getWindow`. Within the
`Window`:
* `Window.liveConfiguration` contains the target live offset and and live
offset adjustment parameters. These values are based on information in the
media and any app-provided overrides set in `MediaItem.liveConfiguration`.
* `Window.liveConfiguration` contains the target live offset and live offset
adjustment parameters. These values are based on information in the media
and any app-provided overrides set in `MediaItem.liveConfiguration`.
* `Window.windowStartTimeMs` is the time since the Unix Epoch at which the
live window starts.
* `Window.getCurrentUnixTimeMs` is the time since the Unix Epoch of the
......
......@@ -153,4 +153,4 @@ the player also needs to have its `DefaultMediaSourceFactory`
[configured accordingly]({{ site.baseurl }}/ad-insertion.html#declarative-ad-support).
[playlist API]: {{ site.baseurl }}/playlists.html
[`MediaItem.Builder` Javadoc]: {{ site.baseurl }}/doc/reference/com/google/android/exoplayer2/MediaItem.Builder.html
[`MediaItem.Builder` Javadoc]: {{ site.exo_sdk }}/MediaItem.Builder.html
......@@ -44,7 +44,7 @@ ExoPlayer player = new ExoPlayer.Builder(context)
{: .language-java}
The
[`DefaultMediaSourceFactory` JavaDoc]({{ site.baseurl }}/doc/reference/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.html)
[`DefaultMediaSourceFactory` JavaDoc]({{ site.exo_sdk }}/source/DefaultMediaSourceFactory.html)
describes the available options in more detail.
It's also possible to inject a custom `MediaSource.Factory` implementation, for
......@@ -79,4 +79,4 @@ exoPlayer.play();
[HLS]: {{ site.baseurl }}/hls.html
[RTSP]: {{ site.baseurl }}/rtsp.html
[regular media files]: {{ site.baseurl }}/progressive.html
[`ExoPlayer`]: {{ site.baseurl }}/doc/reference/com/google/android/exoplayer2/ExoPlayer.html
[`ExoPlayer`]: {{ site.exo_sdk }}/ExoPlayer.html
......@@ -85,6 +85,6 @@ for (int i = 0; i < trackGroups.length; i++) {
{: .language-java}
[`MediaMetadata`]: {{ site.exo_sdk }}/MediaMetadata.html
[`Metadata.Entry`]: {{ site.exo_sdk}}/metadata/Metadata.Entry.html
[`Metadata.Entry`]: {{ site.exo_sdk }}/metadata/Metadata.Entry.html
[`MetadataRetriever`]: {{ site.exo_sdk }}/MetadataRetriever.html
[`MotionPhotoMetadata`]: {{ site.exo_sdk }}/metadata/mp4/MotionPhotoMetadata.html
......@@ -164,7 +164,7 @@ from HTTPS to HTTP and so is a cross-protocol redirect. ExoPlayer will not
follow this redirect in its default configuration, meaning playback will fail.
If you need to, you can configure ExoPlayer to follow cross-protocol redirects
when instantiating `DefaultHttpDataSourceFactory` instances used in your
when instantiating [`DefaultHttpDataSource.Factory`][] instances used in your
application. Learn about selecting and configuring the network stack
[here]({{ site.base_url }}/customization.html#configuring-the-network-stack).
......@@ -326,7 +326,7 @@ is the official way to play YouTube videos on Android.
[`setFragmentedMp4ExtractorFlags`]: {{ site.exo_sdk }}/extractor/DefaultExtractorsFactory#setFragmentedMp4ExtractorFlags(int)
[Wikipedia]: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
[wget]: https://www.gnu.org/software/wget/manual/wget.html
[`DefaultHttpDataSourceFactory`]: {{ site.exo_sdk }}/upstream/DefaultHttpDataSourceFactory.html
[`DefaultHttpDataSource.Factory`]: {{ site.exo_sdk }}/upstream/DefaultHttpDataSource.Factory.html
[ExoPlayer module]: {{ site.base_url }}/hello-world.html#add-exoplayer-modules
[issue tracker]: https://github.com/google/ExoPlayer/issues
[`isCurrentWindowLive`]: {{ site.exo_sdk }}/Player.html#isCurrentWindowLive()
......
......@@ -112,20 +112,20 @@ gets from the libgav1 decoder:
* GL rendering using GL shader for color space conversion
* If you are using `ExoPlayer` with `PlayerView` or
`StyledPlayerView`, enable this option by setting `surface_type` of view
to be `video_decoder_gl_surface_view`.
* If you are using `ExoPlayer` with `PlayerView` or `StyledPlayerView`,
enable this option by setting `surface_type` of view to be
`video_decoder_gl_surface_view`.
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a
message of type `Renderer.MSG_SET_VIDEO_OUTPUT` with an instance of
`VideoDecoderOutputBufferRenderer` as its object.
`VideoDecoderGLSurfaceView` is the concrete
`VideoDecoderOutputBufferRenderer` implementation used by
`PlayerView` and `StyledPlayerView`.
`VideoDecoderOutputBufferRenderer` implementation used by `PlayerView`
and `StyledPlayerView`.
* Native rendering using `ANativeWindow`
* If you are using `ExoPlayer` with `PlayerView` or
`StyledPlayerView`, this option is enabled by default.
* If you are using `ExoPlayer` with `PlayerView` or `StyledPlayerView`,
this option is enabled by default.
* Otherwise, enable this option by sending `Libgav1VideoRenderer` a
message of type `Renderer.MSG_SET_VIDEO_OUTPUT` with an instance of
`SurfaceView` as its object.
......
......@@ -14,7 +14,7 @@
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
dependencies {
api 'com.google.android.gms:play-services-cast-framework:20.1.0'
api 'com.google.android.gms:play-services-cast-framework:21.0.1'
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation project(modulePrefix + 'library-common')
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
......
......@@ -20,9 +20,11 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaMetadata;
import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.common.images.WebImage;
import java.util.HashMap;
import java.util.Iterator;
import java.util.UUID;
......@@ -45,10 +47,43 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
@Override
public MediaItem toMediaItem(MediaQueueItem mediaQueueItem) {
// `item` came from `toMediaQueueItem()` so the custom JSON data must be set.
MediaInfo mediaInfo = mediaQueueItem.getMedia();
@Nullable MediaInfo mediaInfo = mediaQueueItem.getMedia();
Assertions.checkNotNull(mediaInfo);
return getMediaItem(Assertions.checkNotNull(mediaInfo.getCustomData()));
com.google.android.exoplayer2.MediaMetadata.Builder metadataBuilder =
new com.google.android.exoplayer2.MediaMetadata.Builder();
@Nullable MediaMetadata metadata = mediaInfo.getMetadata();
if (metadata != null) {
if (metadata.containsKey(MediaMetadata.KEY_TITLE)) {
metadataBuilder.setTitle(metadata.getString(MediaMetadata.KEY_TITLE));
}
if (metadata.containsKey(MediaMetadata.KEY_SUBTITLE)) {
metadataBuilder.setSubtitle(metadata.getString(MediaMetadata.KEY_SUBTITLE));
}
if (metadata.containsKey(MediaMetadata.KEY_ARTIST)) {
metadataBuilder.setArtist(metadata.getString(MediaMetadata.KEY_ARTIST));
}
if (metadata.containsKey(MediaMetadata.KEY_ALBUM_ARTIST)) {
metadataBuilder.setAlbumArtist(metadata.getString(MediaMetadata.KEY_ALBUM_ARTIST));
}
if (metadata.containsKey(MediaMetadata.KEY_ALBUM_TITLE)) {
metadataBuilder.setArtist(metadata.getString(MediaMetadata.KEY_ALBUM_TITLE));
}
if (!metadata.getImages().isEmpty()) {
metadataBuilder.setArtworkUri(metadata.getImages().get(0).getUrl());
}
if (metadata.containsKey(MediaMetadata.KEY_COMPOSER)) {
metadataBuilder.setComposer(metadata.getString(MediaMetadata.KEY_COMPOSER));
}
if (metadata.containsKey(MediaMetadata.KEY_DISC_NUMBER)) {
metadataBuilder.setDiscNumber(metadata.getInt(MediaMetadata.KEY_DISC_NUMBER));
}
if (metadata.containsKey(MediaMetadata.KEY_TRACK_NUMBER)) {
metadataBuilder.setTrackNumber(metadata.getInt(MediaMetadata.KEY_TRACK_NUMBER));
}
}
// `mediaQueueItem` came from `toMediaQueueItem()` so the custom JSON data must be set.
return getMediaItem(
Assertions.checkNotNull(mediaInfo.getCustomData()), metadataBuilder.build());
}
@Override
......@@ -57,10 +92,41 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
if (mediaItem.localConfiguration.mimeType == null) {
throw new IllegalArgumentException("The item must specify its mimeType");
}
MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE);
MediaMetadata metadata =
new MediaMetadata(
MimeTypes.isAudio(mediaItem.localConfiguration.mimeType)
? MediaMetadata.MEDIA_TYPE_MUSIC_TRACK
: MediaMetadata.MEDIA_TYPE_MOVIE);
if (mediaItem.mediaMetadata.title != null) {
metadata.putString(MediaMetadata.KEY_TITLE, mediaItem.mediaMetadata.title.toString());
}
if (mediaItem.mediaMetadata.subtitle != null) {
metadata.putString(MediaMetadata.KEY_SUBTITLE, mediaItem.mediaMetadata.subtitle.toString());
}
if (mediaItem.mediaMetadata.artist != null) {
metadata.putString(MediaMetadata.KEY_ARTIST, mediaItem.mediaMetadata.artist.toString());
}
if (mediaItem.mediaMetadata.albumArtist != null) {
metadata.putString(
MediaMetadata.KEY_ALBUM_ARTIST, mediaItem.mediaMetadata.albumArtist.toString());
}
if (mediaItem.mediaMetadata.albumTitle != null) {
metadata.putString(
MediaMetadata.KEY_ALBUM_TITLE, mediaItem.mediaMetadata.albumTitle.toString());
}
if (mediaItem.mediaMetadata.artworkUri != null) {
metadata.addImage(new WebImage(mediaItem.mediaMetadata.artworkUri));
}
if (mediaItem.mediaMetadata.composer != null) {
metadata.putString(MediaMetadata.KEY_COMPOSER, mediaItem.mediaMetadata.composer.toString());
}
if (mediaItem.mediaMetadata.discNumber != null) {
metadata.putInt(MediaMetadata.KEY_DISC_NUMBER, mediaItem.mediaMetadata.discNumber);
}
if (mediaItem.mediaMetadata.trackNumber != null) {
metadata.putInt(MediaMetadata.KEY_TRACK_NUMBER, mediaItem.mediaMetadata.trackNumber);
}
MediaInfo mediaInfo =
new MediaInfo.Builder(mediaItem.localConfiguration.uri.toString())
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
......@@ -73,19 +139,15 @@ public final class DefaultMediaItemConverter implements MediaItemConverter {
// Deserialization.
private static MediaItem getMediaItem(JSONObject customData) {
private static MediaItem getMediaItem(
JSONObject customData, com.google.android.exoplayer2.MediaMetadata mediaMetadata) {
try {
JSONObject mediaItemJson = customData.getJSONObject(KEY_MEDIA_ITEM);
MediaItem.Builder builder = new MediaItem.Builder();
builder.setUri(Uri.parse(mediaItemJson.getString(KEY_URI)));
builder.setMediaId(mediaItemJson.getString(KEY_MEDIA_ID));
if (mediaItemJson.has(KEY_TITLE)) {
com.google.android.exoplayer2.MediaMetadata mediaMetadata =
new com.google.android.exoplayer2.MediaMetadata.Builder()
.setTitle(mediaItemJson.getString(KEY_TITLE))
.build();
builder.setMediaMetadata(mediaMetadata);
}
MediaItem.Builder builder =
new MediaItem.Builder()
.setUri(Uri.parse(mediaItemJson.getString(KEY_URI)))
.setMediaId(mediaItemJson.getString(KEY_MEDIA_ID))
.setMediaMetadata(mediaMetadata);
if (mediaItemJson.has(KEY_MIME_TYPE)) {
builder.setMimeType(mediaItemJson.getString(KEY_MIME_TYPE));
}
......
......@@ -20,7 +20,7 @@ android {
}
dependencies {
api "com.google.android.gms:play-services-cronet:17.0.1"
api "com.google.android.gms:play-services-cronet:18.0.1"
implementation project(modulePrefix + 'library-common')
implementation project(modulePrefix + 'library-datasource')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
......
......@@ -25,7 +25,7 @@ android {
}
dependencies {
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.25.1'
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.26.0'
implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
......
......@@ -71,14 +71,15 @@ import java.util.Set;
* #setPlayer(Player)}. If the ads loader is no longer required, it must be released by calling
* {@link #release()}.
*
* <p>See https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility for
* information on compatible ad tag formats. Pass the ad tag URI when setting media item playback
* properties (if using the media item API) or as a {@link DataSpec} when constructing the {@link
* AdsMediaSource} (if using media sources directly). For the latter case, please note that this
* implementation delegates loading of the data spec to the IMA SDK, so range and headers
* specifications will be ignored in ad tag URIs. Literal ads responses can be encoded as data
* scheme data specs, for example, by constructing the data spec using a URI generated via {@link
* Util#getDataUriForString(String, String)}.
* <p>See <a
* href="https://developers.google.com/interactive-media-ads/docs/sdks/android/compatibility">IMA's
* Support and compatibility page</a> for information on compatible ad tag formats. Pass the ad tag
* URI when setting media item playback properties (if using the media item API) or as a {@link
* DataSpec} when constructing the {@link AdsMediaSource} (if using media sources directly). For the
* latter case, please note that this implementation delegates loading of the data spec to the IMA
* SDK, so range and headers specifications will be ignored in ad tag URIs. Literal ads responses
* can be encoded as data scheme data specs, for example, by constructing the data spec using a URI
* generated via {@link Util#getDataUriForString(String, String)}.
*
* <p>The IMA SDK can report obstructions to the ad view for accurate viewability measurement. This
* means that any overlay views that obstruct the ad overlay but are essential for playback need to
......
......@@ -924,8 +924,7 @@ public class SessionPlayerConnectorTest {
assertThat(onPlaylistChangedLatch.getCount()).isEqualTo(1);
}
// TODO(b/168860979): De-flake and re-enable.
@Ignore
@Ignore("Internal ref: b/168860979")
@Test
@LargeTest
public void replacePlaylistItem_calledOnlyOnce_notifiesPlaylistChangeOnlyOnce() throws Exception {
......
......@@ -125,20 +125,20 @@ gets from the libvpx decoder:
* GL rendering using GL shader for color space conversion
* If you are using `ExoPlayer` with `PlayerView` or
`StyledPlayerView`, enable this option by setting `surface_type` of view
to be `video_decoder_gl_surface_view`.
* If you are using `ExoPlayer` with `PlayerView` or `StyledPlayerView`,
enable this option by setting `surface_type` of view to be
`video_decoder_gl_surface_view`.
* Otherwise, enable this option by sending `LibvpxVideoRenderer` a message
of type `Renderer.MSG_SET_VIDEO_OUTPUT` with an instance of
`VideoDecoderOutputBufferRenderer` as its object.
`VideoDecoderGLSurfaceView` is the concrete
`VideoDecoderOutputBufferRenderer` implementation used by
`PlayerView` and `StyledPlayerView`.
`VideoDecoderOutputBufferRenderer` implementation used by `PlayerView`
and `StyledPlayerView`.
* Native rendering using `ANativeWindow`
* If you are using `ExoPlayer` with `PlayerView` or
`StyledPlayerView`, this option is enabled by default.
* If you are using `ExoPlayer` with `PlayerView` or `StyledPlayerView`,
this option is enabled by default.
* Otherwise, enable this option by sending `LibvpxVideoRenderer` a message
of type `Renderer.MSG_SET_VIDEO_OUTPUT` with an instance of
`SurfaceView` as its object.
......
......@@ -28,7 +28,7 @@ public final class VpxLibrary {
}
private static final LibraryLoader LOADER = new LibraryLoader("vpx", "vpxV2JNI");
@C.CryptoType private static int cryptoType = C.CRYPTO_TYPE_UNSUPPORTED;
private static @C.CryptoType int cryptoType = C.CRYPTO_TYPE_UNSUPPORTED;
private VpxLibrary() {}
......
......@@ -100,7 +100,7 @@ public final class HeartRating extends Rating {
private static HeartRating fromBundle(Bundle bundle) {
checkArgument(
bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_DEFAULT)
bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_UNSET)
== TYPE);
boolean isRated = bundle.getBoolean(keyForField(FIELD_RATED), /* defaultValue= */ false);
return isRated
......
......@@ -96,7 +96,7 @@ public final class PercentageRating extends Rating {
private static PercentageRating fromBundle(Bundle bundle) {
checkArgument(
bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_DEFAULT)
bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_UNSET)
== TYPE);
float percent = bundle.getFloat(keyForField(FIELD_PERCENT), /* defaultValue= */ RATING_UNSET);
return percent == RATING_UNSET ? new PercentageRating() : new PercentageRating(percent);
......
......@@ -41,7 +41,7 @@ public abstract class Rating implements Bundleable {
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
RATING_TYPE_DEFAULT,
RATING_TYPE_UNSET,
RATING_TYPE_HEART,
RATING_TYPE_PERCENTAGE,
RATING_TYPE_STAR,
......@@ -49,7 +49,7 @@ public abstract class Rating implements Bundleable {
})
/* package */ @interface RatingType {}
/* package */ static final int RATING_TYPE_DEFAULT = -1;
/* package */ static final int RATING_TYPE_UNSET = -1;
/* package */ static final int RATING_TYPE_HEART = 0;
/* package */ static final int RATING_TYPE_PERCENTAGE = 1;
/* package */ static final int RATING_TYPE_STAR = 2;
......@@ -68,7 +68,7 @@ public abstract class Rating implements Bundleable {
private static Rating fromBundle(Bundle bundle) {
@RatingType
int ratingType =
bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_DEFAULT);
bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_UNSET);
switch (ratingType) {
case RATING_TYPE_HEART:
return HeartRating.CREATOR.fromBundle(bundle);
......@@ -78,8 +78,9 @@ public abstract class Rating implements Bundleable {
return StarRating.CREATOR.fromBundle(bundle);
case RATING_TYPE_THUMB:
return ThumbRating.CREATOR.fromBundle(bundle);
case RATING_TYPE_UNSET:
default:
throw new IllegalArgumentException("Encountered unknown rating type: " + ratingType);
throw new IllegalArgumentException("Unknown RatingType: " + ratingType);
}
}
......
......@@ -125,7 +125,7 @@ public final class StarRating extends Rating {
private static StarRating fromBundle(Bundle bundle) {
checkArgument(
bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_DEFAULT)
bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_UNSET)
== TYPE);
int maxStars =
bundle.getInt(keyForField(FIELD_MAX_STARS), /* defaultValue= */ MAX_STARS_DEFAULT);
......
......@@ -97,7 +97,7 @@ public final class ThumbRating extends Rating {
private static ThumbRating fromBundle(Bundle bundle) {
checkArgument(
bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_DEFAULT)
bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_UNSET)
== TYPE);
boolean rated = bundle.getBoolean(keyForField(FIELD_RATED), /* defaultValue= */ false);
return rated
......
......@@ -25,7 +25,6 @@ import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Booleans;
......@@ -35,11 +34,12 @@ import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.List;
/** Immutable information ({@link TrackGroupInfo}) about tracks. */
/** Information about groups of tracks. */
public final class TracksInfo implements Bundleable {
/**
* Information about tracks in a {@link TrackGroup}: their {@link C.TrackType}, if their format is
* supported by the player and if they are selected for playback.
* Information about a single group of tracks, including the underlying {@link TrackGroup}, the
* {@link C.TrackType type} of tracks it contains, and the level to which each track is supported
* by the player.
*/
public static final class TrackGroupInfo implements Bundleable {
private final TrackGroup trackGroup;
......@@ -74,7 +74,7 @@ public final class TracksInfo implements Bundleable {
}
/**
* Returns the level of support for a track in a {@link TrackGroup}.
* Returns the level of support for a specified track.
*
* @param trackIndex The index of the track in the {@link TrackGroup}.
* @return The {@link C.FormatSupport} of the track.
......@@ -85,24 +85,58 @@ public final class TracksInfo implements Bundleable {
}
/**
* Returns if a track in a {@link TrackGroup} is supported for playback.
* Returns whether a specified track is supported for playback, without exceeding the advertised
* capabilities of the device. Equivalent to {@code isTrackSupported(trackIndex, false)}.
*
* @param trackIndex The index of the track in the {@link TrackGroup}.
* @return True if the track's format can be played, false otherwise.
*/
public boolean isTrackSupported(int trackIndex) {
return trackSupport[trackIndex] == C.FORMAT_HANDLED;
return isTrackSupported(trackIndex, /* allowExceedsCapabilities= */ false);
}
/** Returns if at least one track in a {@link TrackGroup} is selected for playback. */
/**
* Returns whether a specified track is supported for playback.
*
* @param trackIndex The index of the track in the {@link TrackGroup}.
* @param allowExceedsCapabilities Whether to consider the track as supported if it has a
* supported {@link Format#sampleMimeType MIME type}, but otherwise exceeds the advertised
* capabilities of the device. For example, a video track for which there's a corresponding
* decoder whose maximum advertised resolution is exceeded by the resolution of the track.
* Such tracks may be playable in some cases.
* @return True if the track's format can be played, false otherwise.
*/
public boolean isTrackSupported(int trackIndex, boolean allowExceedsCapabilities) {
return trackSupport[trackIndex] == C.FORMAT_HANDLED
|| (allowExceedsCapabilities
&& trackSupport[trackIndex] == C.FORMAT_EXCEEDS_CAPABILITIES);
}
/** Returns whether at least one track in the group is selected for playback. */
public boolean isSelected() {
return Booleans.contains(trackSelected, true);
}
/** Returns if at least one track in a {@link TrackGroup} is supported. */
/**
* Returns whether at least one track in the group is supported for playback, without exceeding
* the advertised capabilities of the device. Equivalent to {@code isSupported(false)}.
*/
public boolean isSupported() {
return isSupported(/* allowExceedsCapabilities= */ false);
}
/**
* Returns whether at least one track in the group is supported for playback.
*
* @param allowExceedsCapabilities Whether to consider a track as supported if it has a
* supported {@link Format#sampleMimeType MIME type}, but otherwise exceeds the advertised
* capabilities of the device. For example, a video track for which there's a corresponding
* decoder whose maximum advertised resolution is exceeded by the resolution of the track.
* Such tracks may be playable in some cases.
*/
public boolean isSupported(boolean allowExceedsCapabilities) {
for (int i = 0; i < trackSupport.length; i++) {
if (isTrackSupported(i)) {
if (isTrackSupported(i, allowExceedsCapabilities)) {
return true;
}
}
......@@ -110,27 +144,24 @@ public final class TracksInfo implements Bundleable {
}
/**
* Returns if a track in a {@link TrackGroup} is selected for playback.
* Returns whether a specified track is selected for playback.
*
* <p>Multiple tracks of a track group may be selected. This is common in adaptive streaming,
* where multiple tracks of different quality are selected and the player switches between them
* depending on the network and the {@link TrackSelectionParameters}.
* <p>Note that multiple tracks in the group may be selected. This is common in adaptive
* streaming, where tracks of different qualities are selected and the player switches between
* them during playback (e.g., based on the available network bandwidth).
*
* <p>While this class doesn't provide which selected track is currently playing, some player
* implementations have ways of getting such information. For example ExoPlayer provides this
* information in {@code ExoTrackSelection.getSelectedFormat}.
* <p>This class doesn't provide a way to determine which of the selected tracks is currently
* playing, however some player implementations have ways of getting such information. For
* example, ExoPlayer provides this information via {@code ExoTrackSelection.getSelectedFormat}.
*
* @param trackIndex The index of the track in the {@link TrackGroup}.
* @return true if the track is selected, false otherwise.
* @return True if the track is selected, false otherwise.
*/
public boolean isTrackSelected(int trackIndex) {
return trackSelected[trackIndex];
}
/**
* Returns the {@link C.TrackType} of the tracks in the {@link TrackGroup}. Tracks in a group
* are all of the same type.
*/
/** Returns the {@link C.TrackType} of the group. */
public @C.TrackType int getTrackType() {
return trackType;
}
......@@ -212,28 +243,49 @@ public final class TracksInfo implements Bundleable {
private final ImmutableList<TrackGroupInfo> trackGroupInfos;
/** An empty {@code TrackInfo} containing no {@link TrackGroupInfo}. */
/** An {@code TrackInfo} that contains no tracks. */
public static final TracksInfo EMPTY = new TracksInfo(ImmutableList.of());
/** Constructs {@code TracksInfo} from the provided {@link TrackGroupInfo}. */
/**
* Constructs an instance.
*
* @param trackGroupInfos The {@link TrackGroupInfo TrackGroupInfos} describing the groups of
* tracks.
*/
public TracksInfo(List<TrackGroupInfo> trackGroupInfos) {
this.trackGroupInfos = ImmutableList.copyOf(trackGroupInfos);
}
/** Returns the {@link TrackGroupInfo TrackGroupInfos}, describing each {@link TrackGroup}. */
/** Returns the {@link TrackGroupInfo TrackGroupInfos} describing the groups of tracks. */
public ImmutableList<TrackGroupInfo> getTrackGroupInfos() {
return trackGroupInfos;
}
/**
* Returns true if at least one track of type {@code trackType} is {@link
* TrackGroupInfo#isTrackSupported(int) supported}, or there are no tracks of this type.
* TrackGroupInfo#isTrackSupported(int) supported} or if there are no tracks of this type.
*/
public boolean isTypeSupportedOrEmpty(@C.TrackType int trackType) {
return isTypeSupportedOrEmpty(trackType, /* allowExceedsCapabilities= */ false);
}
/**
* Returns true if at least one track of type {@code trackType} is {@link
* TrackGroupInfo#isTrackSupported(int, boolean) supported} or if there are no tracks of this
* type.
*
* @param allowExceedsCapabilities Whether to consider the track as supported if it has a
* supported {@link Format#sampleMimeType MIME type}, but otherwise exceeds the advertised
* capabilities of the device. For example, a video track for which there's a corresponding
* decoder whose maximum advertised resolution is exceeded by the resolution of the track.
* Such tracks may be playable in some cases.
*/
public boolean isTypeSupportedOrEmpty(
@C.TrackType int trackType, boolean allowExceedsCapabilities) {
boolean supported = true;
for (int i = 0; i < trackGroupInfos.size(); i++) {
if (trackGroupInfos.get(i).trackType == trackType) {
if (trackGroupInfos.get(i).isSupported()) {
if (trackGroupInfos.get(i).isSupported(allowExceedsCapabilities)) {
return true;
} else {
supported = false;
......
......@@ -220,6 +220,17 @@ public final class MediaFormatUtil {
case C.ENCODING_PCM_FLOAT:
mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_FLOAT;
break;
case C.ENCODING_PCM_24BIT:
mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_24BIT_PACKED;
break;
case C.ENCODING_PCM_32BIT:
mediaFormatPcmEncoding = AudioFormat.ENCODING_PCM_32BIT;
break;
case C.ENCODING_INVALID:
mediaFormatPcmEncoding = AudioFormat.ENCODING_INVALID;
break;
case Format.NO_VALUE:
case C.ENCODING_PCM_16BIT_BIG_ENDIAN:
default:
// No matching value. Do nothing.
return;
......
......@@ -2405,6 +2405,8 @@ public final class Util {
return "camera motion";
case C.TRACK_TYPE_NONE:
return "none";
case C.TRACK_TYPE_UNKNOWN:
return "unknown";
default:
return trackType >= C.TRACK_TYPE_CUSTOM_BASE ? "custom (" + trackType + ")" : "?";
}
......@@ -2537,6 +2539,20 @@ public final class Util {
.build();
}
/**
* Returns the sum of all summands of the given array.
*
* @param summands The summands to calculate the sum from.
* @return The sum of all summands.
*/
public static long sum(long... summands) {
long sum = 0;
for (long summand : summands) {
sum += summand;
}
return sum;
}
@Nullable
private static String getSystemProperty(String name) {
try {
......
......@@ -146,10 +146,10 @@ public class MediaFormatUtilTest {
@Test
public void createMediaFormatFromFormat_withPcmEncoding_setsCustomPcmEncodingEntry() {
Format format = new Format.Builder().setPcmEncoding(C.ENCODING_PCM_32BIT).build();
Format format = new Format.Builder().setPcmEncoding(C.ENCODING_PCM_16BIT_BIG_ENDIAN).build();
MediaFormat mediaFormat = MediaFormatUtil.createMediaFormatFromFormat(format);
assertThat(mediaFormat.getInteger(MediaFormatUtil.KEY_PCM_ENCODING_EXTENDED))
.isEqualTo(C.ENCODING_PCM_32BIT);
.isEqualTo(C.ENCODING_PCM_16BIT_BIG_ENDIAN);
assertThat(mediaFormat.containsKey(MediaFormat.KEY_PCM_ENCODING)).isFalse();
}
}
......@@ -99,10 +99,9 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
Assertions.checkState(state == STATE_DISABLED);
this.configuration = configuration;
state = STATE_ENABLED;
lastResetPositionUs = positionUs;
onEnabled(joining, mayRenderStartOfStream);
replaceStream(formats, stream, startPositionUs, offsetUs);
onPositionReset(positionUs, joining);
resetPosition(positionUs, joining);
}
@Override
......@@ -159,10 +158,14 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities {
@Override
public final void resetPosition(long positionUs) throws ExoPlaybackException {
resetPosition(positionUs, /* joining= */ false);
}
private void resetPosition(long positionUs, boolean joining) throws ExoPlaybackException {
streamIsFinal = false;
lastResetPositionUs = positionUs;
readingPositionUs = positionUs;
onPositionReset(positionUs, false);
onPositionReset(positionUs, joining);
}
@Override
......
......@@ -1457,19 +1457,6 @@ public interface ExoPlayer extends Player {
void setPriorityTaskManager(@Nullable PriorityTaskManager priorityTaskManager);
/**
* Sets whether the player should throw an {@link IllegalStateException} when methods are called
* from a thread other than the one associated with {@link #getApplicationLooper()}.
*
* <p>The default is {@code true} and this method will be removed in the future.
*
* @param throwsWhenUsingWrongThread Whether to throw when methods are called from a wrong thread.
* @deprecated Disabling the enforcement can result in hard-to-detect bugs. Do not use this method
* except to ease the transition while wrong thread access problems are fixed.
*/
@Deprecated
void setThrowsWhenUsingWrongThread(boolean throwsWhenUsingWrongThread);
/**
* Sets whether audio offload scheduling is enabled. If enabled, ExoPlayer's main loop will run as
* rarely as possible when playing an audio stream using audio offload.
*
......
......@@ -230,7 +230,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
BandwidthMeter bandwidthMeter,
@Player.RepeatMode int repeatMode,
boolean shuffleModeEnabled,
@Nullable AnalyticsCollector analyticsCollector,
AnalyticsCollector analyticsCollector,
SeekParameters seekParameters,
LivePlaybackSpeedControl livePlaybackSpeedControl,
long releaseTimeoutMs,
......@@ -1226,7 +1226,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
/* forceBufferingState= */ playbackInfo.playbackState == Player.STATE_ENDED);
seekPositionAdjusted |= periodPositionUs != newPeriodPositionUs;
periodPositionUs = newPeriodPositionUs;
updateLivePlaybackSpeedControl(
updatePlaybackSpeedSettingsForNewPeriod(
/* newTimeline= */ playbackInfo.timeline,
/* newPeriodId= */ periodId,
/* oldTimeline= */ playbackInfo.timeline,
......@@ -1866,7 +1866,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
newPositionUs = seekToPeriodPosition(newPeriodId, newPositionUs, forceBufferingState);
}
} finally {
updateLivePlaybackSpeedControl(
updatePlaybackSpeedSettingsForNewPeriod(
/* newTimeline= */ timeline,
newPeriodId,
/* oldTimeline= */ playbackInfo.timeline,
......@@ -1906,16 +1906,19 @@ import java.util.concurrent.atomic.AtomicBoolean;
}
}
private void updateLivePlaybackSpeedControl(
private void updatePlaybackSpeedSettingsForNewPeriod(
Timeline newTimeline,
MediaPeriodId newPeriodId,
Timeline oldTimeline,
MediaPeriodId oldPeriodId,
long positionForTargetOffsetOverrideUs) {
if (newTimeline.isEmpty() || !shouldUseLivePlaybackSpeedControl(newTimeline, newPeriodId)) {
// Live playback speed control is unused for the current period, reset speed if adjusted.
if (mediaClock.getPlaybackParameters().speed != playbackInfo.playbackParameters.speed) {
mediaClock.setPlaybackParameters(playbackInfo.playbackParameters);
if (!shouldUseLivePlaybackSpeedControl(newTimeline, newPeriodId)) {
// Live playback speed control is unused for the current period, reset speed to user-defined
// playback parameters or 1.0 for ad playback.
PlaybackParameters targetPlaybackParameters =
newPeriodId.isAd() ? PlaybackParameters.DEFAULT : playbackInfo.playbackParameters;
if (!mediaClock.getPlaybackParameters().equals(targetPlaybackParameters)) {
mediaClock.setPlaybackParameters(targetPlaybackParameters);
}
return;
}
......@@ -2046,10 +2049,18 @@ import java.util.concurrent.atomic.AtomicBoolean;
return;
}
MediaPeriodHolder oldReadingPeriodHolder = readingPeriodHolder;
TrackSelectorResult oldTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult();
readingPeriodHolder = queue.advanceReadingPeriod();
TrackSelectorResult newTrackSelectorResult = readingPeriodHolder.getTrackSelectorResult();
updatePlaybackSpeedSettingsForNewPeriod(
/* newTimeline= */ playbackInfo.timeline,
/* newPeriodId= */ readingPeriodHolder.info.id,
/* oldTimeline= */ playbackInfo.timeline,
/* oldPeriodId= */ oldReadingPeriodHolder.info.id,
/* positionForTargetOffsetOverrideUs= */ C.TIME_UNSET);
if (readingPeriodHolder.prepared
&& readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET) {
// The new period starts with a discontinuity, so the renderers will play out all data, then
......@@ -2134,7 +2145,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
// If we advance more than one period at a time, notify listeners after each update.
maybeNotifyPlaybackInfoChanged();
}
MediaPeriodHolder oldPlayingPeriodHolder = queue.getPlayingPeriod();
MediaPeriodHolder newPlayingPeriodHolder = queue.advancePlayingPeriod();
playbackInfo =
handlePositionDiscontinuity(
......@@ -2144,12 +2154,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
/* discontinuityStartPositionUs= */ newPlayingPeriodHolder.info.startPositionUs,
/* reportDiscontinuity= */ true,
Player.DISCONTINUITY_REASON_AUTO_TRANSITION);
updateLivePlaybackSpeedControl(
/* newTimeline= */ playbackInfo.timeline,
/* newPeriodId= */ newPlayingPeriodHolder.info.id,
/* oldTimeline= */ playbackInfo.timeline,
/* oldPeriodId= */ oldPlayingPeriodHolder.info.id,
/* positionForTargetOffsetOverrideUs= */ C.TIME_UNSET);
resetPendingPauseAtEndOfPeriod();
updatePlaybackPositions();
advancedPlayingPeriod = true;
......
......@@ -66,7 +66,7 @@ import com.google.common.collect.ImmutableList;
private final Timeline.Period period;
private final Timeline.Window window;
@Nullable private final AnalyticsCollector analyticsCollector;
private final AnalyticsCollector analyticsCollector;
private final Handler analyticsCollectorHandler;
private long nextWindowSequenceNumber;
......@@ -82,13 +82,12 @@ import com.google.common.collect.ImmutableList;
/**
* Creates a new media period queue.
*
* @param analyticsCollector An optional {@link AnalyticsCollector} to be informed of queue
* changes.
* @param analyticsCollector An {@link AnalyticsCollector} to be informed of queue changes.
* @param analyticsCollectorHandler The {@link Handler} to call {@link AnalyticsCollector} methods
* on.
*/
public MediaPeriodQueue(
@Nullable AnalyticsCollector analyticsCollector, Handler analyticsCollectorHandler) {
AnalyticsCollector analyticsCollector, Handler analyticsCollectorHandler) {
this.analyticsCollector = analyticsCollector;
this.analyticsCollectorHandler = analyticsCollectorHandler;
period = new Timeline.Period();
......@@ -451,17 +450,15 @@ import com.google.common.collect.ImmutableList;
// Internal methods.
private void notifyQueueUpdate() {
if (analyticsCollector != null) {
ImmutableList.Builder<MediaPeriodId> builder = ImmutableList.builder();
@Nullable MediaPeriodHolder period = playing;
while (period != null) {
builder.add(period.info.id);
period = period.getNext();
}
@Nullable MediaPeriodId readingPeriodId = reading == null ? null : reading.info.id;
analyticsCollectorHandler.post(
() -> analyticsCollector.updateMediaPeriodQueueInfo(builder.build(), readingPeriodId));
ImmutableList.Builder<MediaPeriodId> builder = ImmutableList.builder();
@Nullable MediaPeriodHolder period = playing;
while (period != null) {
builder.add(period.info.id);
period = period.getNext();
}
@Nullable MediaPeriodId readingPeriodId = reading == null ? null : reading.info.id;
analyticsCollectorHandler.post(
() -> analyticsCollector.updateMediaPeriodQueueInfo(builder.build(), readingPeriodId));
}
/**
......
......@@ -91,15 +91,15 @@ import java.util.Set;
*
* @param listener The {@link MediaSourceListInfoRefreshListener} to be informed of timeline
* changes.
* @param analyticsCollector An optional {@link AnalyticsCollector} to be registered for media
* source events.
* @param analyticsCollector An {@link AnalyticsCollector} to be registered for media source
* events.
* @param analyticsCollectorHandler The {@link Handler} to call {@link AnalyticsCollector} methods
* on.
* @param playerId The {@link PlayerId} of the player using this list.
*/
public MediaSourceList(
MediaSourceListInfoRefreshListener listener,
@Nullable AnalyticsCollector analyticsCollector,
AnalyticsCollector analyticsCollector,
Handler analyticsCollectorHandler,
PlayerId playerId) {
this.playerId = playerId;
......@@ -112,10 +112,8 @@ import java.util.Set;
drmEventDispatcher = new DrmSessionEventListener.EventDispatcher();
childSources = new HashMap<>();
enabledMediaSourceHolders = new HashSet<>();
if (analyticsCollector != null) {
mediaSourceEventDispatcher.addEventListener(analyticsCollectorHandler, analyticsCollector);
drmEventDispatcher.addEventListener(analyticsCollectorHandler, analyticsCollector);
}
mediaSourceEventDispatcher.addEventListener(analyticsCollectorHandler, analyticsCollector);
drmEventDispatcher.addEventListener(analyticsCollectorHandler, analyticsCollector);
}
/**
......
......@@ -1541,9 +1541,7 @@ public class SimpleExoPlayer extends BasePlayer
streamVolumeManager.setMuted(muted);
}
@Deprecated
@Override
public void setThrowsWhenUsingWrongThread(boolean throwsWhenUsingWrongThread) {
/* package */ void setThrowsWhenUsingWrongThread(boolean throwsWhenUsingWrongThread) {
this.throwsWhenUsingWrongThread = throwsWhenUsingWrongThread;
}
......
......@@ -177,7 +177,8 @@ public final class DefaultPlaybackSessionManager implements PlaybackSessionManag
Iterator<SessionDescriptor> iterator = sessions.values().iterator();
while (iterator.hasNext()) {
SessionDescriptor session = iterator.next();
if (!session.tryResolvingToNewTimeline(previousTimeline, currentTimeline)) {
if (!session.tryResolvingToNewTimeline(previousTimeline, currentTimeline)
|| session.isFinishedAtEventTime(eventTime)) {
iterator.remove();
if (session.isCreated) {
if (session.sessionId.equals(currentSessionId)) {
......
......@@ -40,6 +40,7 @@ import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.C.ContentType;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.Format;
......@@ -65,7 +66,6 @@ import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.UdpDataSource;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.NetworkTypeObserver;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoSize;
......@@ -74,6 +74,7 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.UUID;
import org.checkerframework.checker.nullness.compatqual.NullableType;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
......@@ -112,7 +113,10 @@ public final class MediaMetricsListener
private final long startTimeMs;
private final Timeline.Window window;
private final Timeline.Period period;
private final HashMap<String, Long> bandwidthTimeMs;
private final HashMap<String, Long> bandwidthBytes;
@Nullable private String activeSessionId;
@Nullable private PlaybackMetrics.Builder metricsBuilder;
@Player.DiscontinuityReason private int discontinuityReason;
private int currentPlaybackState;
......@@ -129,8 +133,6 @@ public final class MediaMetricsListener
private boolean hasFatalError;
private int droppedFrames;
private int playedFrames;
private long bandwidthTimeMs;
private long bandwidthBytes;
private int audioUnderruns;
/**
......@@ -144,6 +146,8 @@ public final class MediaMetricsListener
this.playbackSession = playbackSession;
window = new Timeline.Window();
period = new Timeline.Period();
bandwidthBytes = new HashMap<>();
bandwidthTimeMs = new HashMap<>();
startTimeMs = SystemClock.elapsedRealtime();
currentPlaybackState = PlaybackStateEvent.STATE_NOT_STARTED;
currentNetworkType = NetworkEvent.NETWORK_TYPE_UNKNOWN;
......@@ -168,6 +172,7 @@ public final class MediaMetricsListener
return;
}
finishCurrentSession();
activeSessionId = sessionId;
metricsBuilder =
new PlaybackMetrics.Builder()
.setPlayerName(ExoPlayerLibraryInfo.TAG)
......@@ -182,11 +187,14 @@ public final class MediaMetricsListener
@Override
public void onSessionFinished(
EventTime eventTime, String sessionId, boolean automaticTransitionToNextPlayback) {
if (eventTime.mediaPeriodId != null && eventTime.mediaPeriodId.isAd()) {
// Ignore ad sessions.
return;
if ((eventTime.mediaPeriodId != null && eventTime.mediaPeriodId.isAd())
|| !sessionId.equals(activeSessionId)) {
// Ignore ad sessions and other sessions that are finished before becoming active.
} else {
finishCurrentSession();
}
finishCurrentSession();
bandwidthTimeMs.remove(sessionId);
bandwidthBytes.remove(sessionId);
}
// AnalyticsListener implementation.
......@@ -213,8 +221,17 @@ public final class MediaMetricsListener
@Override
public void onBandwidthEstimate(
EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {
bandwidthTimeMs += totalLoadTimeMs;
bandwidthBytes += totalBytesLoaded;
if (eventTime.mediaPeriodId != null) {
String sessionId =
sessionManager.getSessionForMediaPeriodId(
eventTime.timeline, checkNotNull(eventTime.mediaPeriodId));
@Nullable Long prevBandwidthBytes = bandwidthBytes.get(sessionId);
@Nullable Long prevBandwidthTimeMs = bandwidthTimeMs.get(sessionId);
bandwidthBytes.put(
sessionId, (prevBandwidthBytes == null ? 0 : prevBandwidthBytes) + totalBytesLoaded);
bandwidthTimeMs.put(
sessionId, (prevBandwidthTimeMs == null ? 0 : prevBandwidthTimeMs) + totalLoadTimeMs);
}
}
@Override
......@@ -578,16 +595,25 @@ public final class MediaMetricsListener
metricsBuilder.setAudioUnderrunCount(audioUnderruns);
metricsBuilder.setVideoFramesDropped(droppedFrames);
metricsBuilder.setVideoFramesPlayed(playedFrames);
metricsBuilder.setNetworkTransferDurationMillis(bandwidthTimeMs);
@Nullable Long networkTimeMs = bandwidthTimeMs.get(activeSessionId);
metricsBuilder.setNetworkTransferDurationMillis(networkTimeMs == null ? 0 : networkTimeMs);
// TODO(b/181121847): Report localBytesRead. This requires additional callbacks or plumbing.
metricsBuilder.setNetworkBytesRead(bandwidthBytes);
@Nullable Long networkBytes = bandwidthBytes.get(activeSessionId);
metricsBuilder.setNetworkBytesRead(networkBytes == null ? 0 : networkBytes);
// TODO(b/181121847): Detect stream sources mixed and local depending on localBytesRead.
metricsBuilder.setStreamSource(
bandwidthBytes > 0
networkBytes != null && networkBytes > 0
? PlaybackMetrics.STREAM_SOURCE_NETWORK
: PlaybackMetrics.STREAM_SOURCE_UNKNOWN);
playbackSession.reportPlaybackMetrics(metricsBuilder.build());
metricsBuilder = null;
activeSessionId = null;
audioUnderruns = 0;
droppedFrames = 0;
playedFrames = 0;
currentVideoFormat = null;
currentAudioFormat = null;
currentTextFormat = null;
}
private static int getTrackChangeReason(@C.SelectionReason int trackSelectionReason) {
......@@ -636,19 +662,23 @@ public final class MediaMetricsListener
}
private static int getStreamType(MediaItem mediaItem) {
if (mediaItem.localConfiguration == null || mediaItem.localConfiguration.mimeType == null) {
if (mediaItem.localConfiguration == null) {
return PlaybackMetrics.STREAM_TYPE_UNKNOWN;
}
String mimeType = mediaItem.localConfiguration.mimeType;
switch (mimeType) {
case MimeTypes.APPLICATION_M3U8:
@ContentType
int contentType =
Util.inferContentTypeForUriAndMimeType(
mediaItem.localConfiguration.uri, mediaItem.localConfiguration.mimeType);
switch (contentType) {
case C.TYPE_HLS:
return PlaybackMetrics.STREAM_TYPE_HLS;
case MimeTypes.APPLICATION_MPD:
case C.TYPE_DASH:
return PlaybackMetrics.STREAM_TYPE_DASH;
case MimeTypes.APPLICATION_SS:
case C.TYPE_SS:
return PlaybackMetrics.STREAM_TYPE_SS;
case C.TYPE_RTSP:
default:
return PlaybackMetrics.STREAM_TYPE_PROGRESSIVE;
return PlaybackMetrics.STREAM_TYPE_OTHER;
}
}
......
......@@ -404,6 +404,13 @@ public interface AudioSink {
*/
void setAudioAttributes(AudioAttributes audioAttributes);
/**
* Returns the audio attributes used for audio playback, or {@code null} if the sink does not use
* audio attributes.
*/
@Nullable
AudioAttributes getAudioAttributes();
/** Sets the audio session id. */
void setAudioSessionId(int audioSessionId);
......
......@@ -120,6 +120,12 @@ public class ForwardingAudioSink implements AudioSink {
}
@Override
@Nullable
public AudioAttributes getAudioAttributes() {
return sink.getAudioAttributes();
}
@Override
public void setAudioSessionId(int audioSessionId) {
sink.setAudioSessionId(audioSessionId);
}
......
......@@ -565,6 +565,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
/**
* Returns whether the renderer needs to re-initialize the codec, possibly as a result of a change
* in device capabilities.
*/
protected boolean shouldReinitCodec() {
return false;
}
/**
* Returns whether the codec needs the renderer to propagate the end-of-stream signal directly,
* rather than by using an end-of-stream buffer queued to the codec.
*/
......@@ -1118,7 +1126,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
decoderCounters.decoderInitCount++;
long elapsed = codecInitializedTimestamp - codecInitializingTimestamp;
onCodecInitialized(codecName, codecInitializedTimestamp, elapsed);
onCodecInitialized(codecName, configuration, codecInitializedTimestamp, elapsed);
}
private boolean shouldContinueRendering(long renderStartTimeMs) {
......@@ -1158,6 +1166,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
if (codec == null || codecDrainState == DRAIN_STATE_WAIT_END_OF_STREAM || inputStreamEnded) {
return false;
}
if (codecDrainState == DRAIN_STATE_NONE && shouldReinitCodec()) {
drainAndReinitializeCodec();
}
if (inputIndex < 0) {
inputIndex = codec.dequeueInputBufferIndex();
......@@ -1352,12 +1363,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
* <p>The default implementation is a no-op.
*
* @param name The name of the codec that was initialized.
* @param configuration The {@link MediaCodecAdapter.Configuration} used to configure the codec.
* @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization
* finished.
* @param initializationDurationMs The time taken to initialize the codec in milliseconds.
*/
protected void onCodecInitialized(
String name, long initializedTimestampMs, long initializationDurationMs) {
String name,
MediaCodecAdapter.Configuration configuration,
long initializedTimestampMs,
long initializationDurationMs) {
// Do nothing.
}
......
......@@ -142,7 +142,7 @@ public final class MediaCodecUtil {
return decoderInfos.isEmpty() ? null : decoderInfos.get(0);
}
/*
/**
* Returns all {@link MediaCodecInfo}s for the given mime type, in the order given by {@link
* MediaCodecList}.
*
......
......@@ -365,15 +365,14 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
};
mediaSources[i + 1] =
new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)
.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
.createMediaSource(
MediaItem.fromUri(subtitleConfigurations.get(i).uri.toString()));
} else {
SingleSampleMediaSource.Factory singleSampleSourceFactory =
new SingleSampleMediaSource.Factory(dataSourceFactory)
.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy);
mediaSources[i + 1] =
singleSampleSourceFactory.createMediaSource(
subtitleConfigurations.get(i), /* durationUs= */ C.TIME_UNSET);
new SingleSampleMediaSource.Factory(dataSourceFactory)
.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
.createMediaSource(subtitleConfigurations.get(i), /* durationUs= */ C.TIME_UNSET);
}
}
......
......@@ -66,7 +66,7 @@ import java.util.Arrays;
/** Clears all sample data. */
public void reset() {
clearAllocationNodes(firstAllocationNode);
firstAllocationNode = new AllocationNode(0, allocationLength);
firstAllocationNode.reset(/* startPosition= */ 0, allocationLength);
readAllocationNode = firstAllocationNode;
writeAllocationNode = firstAllocationNode;
totalBytesWritten = 0;
......@@ -462,9 +462,9 @@ import java.util.Arrays;
private static final class AllocationNode implements Allocator.AllocationNode {
/** The absolute position of the start of the data (inclusive). */
public final long startPosition;
public long startPosition;
/** The absolute position of the end of the data (exclusive). */
public final long endPosition;
public long endPosition;
/**
* The {@link Allocation}, or {@code null} if the node is not {@link #initialize initialized}.
*/
......@@ -481,6 +481,17 @@ import java.util.Arrays;
* initialized.
*/
public AllocationNode(long startPosition, int allocationLength) {
reset(startPosition, allocationLength);
}
/**
* Sets the {@link #startPosition} and the {@link Allocation} length.
*
* <p>Must only be called for uninitialized instances, where {@link #allocation} is {@code
* null}.
*/
public void reset(long startPosition, int allocationLength) {
Assertions.checkState(allocation == null);
this.startPosition = startPosition;
this.endPosition = startPosition + allocationLength;
}
......
......@@ -991,7 +991,6 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
public ServerSideAdInsertionTimeline(
Timeline contentTimeline, ImmutableMap<Object, AdPlaybackState> adPlaybackStates) {
super(contentTimeline);
checkState(contentTimeline.getPeriodCount() == 1);
checkState(contentTimeline.getWindowCount() == 1);
Period period = new Period();
for (int i = 0; i < contentTimeline.getPeriodCount(); i++) {
......@@ -1005,25 +1004,23 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
super.getWindow(windowIndex, window, defaultPositionProjectionUs);
Object firstPeriodUid =
checkNotNull(getPeriod(/* periodIndex= */ 0, new Period(), /* setIds= */ true).uid);
AdPlaybackState adPlaybackState = checkNotNull(adPlaybackStates.get(firstPeriodUid));
checkNotNull(getPeriod(window.firstPeriodIndex, new Period(), /* setIds= */ true).uid);
AdPlaybackState firstAdPlaybackState = checkNotNull(adPlaybackStates.get(firstPeriodUid));
long positionInPeriodUs =
getMediaPeriodPositionUsForContent(
window.positionInFirstPeriodUs,
/* nextAdGroupIndex= */ C.INDEX_UNSET,
adPlaybackState);
firstAdPlaybackState);
if (window.durationUs == C.TIME_UNSET) {
if (adPlaybackState.contentDurationUs != C.TIME_UNSET) {
window.durationUs = adPlaybackState.contentDurationUs - positionInPeriodUs;
if (firstAdPlaybackState.contentDurationUs != C.TIME_UNSET) {
window.durationUs = firstAdPlaybackState.contentDurationUs - positionInPeriodUs;
}
} else {
long actualWindowEndPositionInPeriodUs = window.positionInFirstPeriodUs + window.durationUs;
long windowEndPositionInPeriodUs =
getMediaPeriodPositionUsForContent(
actualWindowEndPositionInPeriodUs,
/* nextAdGroupIndex= */ C.INDEX_UNSET,
adPlaybackState);
window.durationUs = windowEndPositionInPeriodUs - positionInPeriodUs;
Period lastPeriod = getPeriod(/* periodIndex= */ window.lastPeriodIndex, new Period());
window.durationUs =
lastPeriod.durationUs == C.TIME_UNSET
? C.TIME_UNSET
: lastPeriod.positionInWindowUs + lastPeriod.durationUs;
}
window.positionInFirstPeriodUs = positionInPeriodUs;
return window;
......@@ -1041,11 +1038,26 @@ public final class ServerSideAdInsertionMediaSource extends BaseMediaSource
getMediaPeriodPositionUsForContent(
durationUs, /* nextAdGroupIndex= */ C.INDEX_UNSET, adPlaybackState);
}
long positionInWindowUs =
-getMediaPeriodPositionUsForContent(
-period.getPositionInWindowUs(),
/* nextAdGroupIndex= */ C.INDEX_UNSET,
adPlaybackState);
long positionInWindowUs = 0;
Period innerPeriod = new Period();
for (int i = 0; i < periodIndex + 1; i++) {
timeline.getPeriod(/* periodIndex= */ i, innerPeriod, /* setIds= */ true);
AdPlaybackState innerAdPlaybackState = checkNotNull(adPlaybackStates.get(innerPeriod.uid));
if (i == 0) {
positionInWindowUs =
-getMediaPeriodPositionUsForContent(
-innerPeriod.getPositionInWindowUs(),
/* nextAdGroupIndex= */ C.INDEX_UNSET,
innerAdPlaybackState);
}
if (i != periodIndex) {
positionInWindowUs +=
getMediaPeriodPositionUsForContent(
innerPeriod.durationUs,
/* nextAdGroupIndex= */ C.INDEX_UNSET,
innerAdPlaybackState);
}
}
period.set(
period.id,
period.uid,
......
......@@ -15,6 +15,7 @@
*/
package com.google.android.exoplayer2.source.ads;
import static com.google.android.exoplayer2.util.Util.sum;
import static java.lang.Math.max;
import androidx.annotation.CheckResult;
......@@ -33,23 +34,25 @@ public final class ServerSideAdInsertionUtil {
/**
* Adds a new server-side inserted ad group to an {@link AdPlaybackState}.
*
* <p>If the first ad with a non-zero duration is not the first ad in the group, all ads before
* that ad are marked as skipped.
*
* @param adPlaybackState The existing {@link AdPlaybackState}.
* @param fromPositionUs The position in the underlying server-side inserted ads stream at which
* the ad group starts, in microseconds.
* @param toPositionUs The position in the underlying server-side inserted ads stream at which the
* ad group ends, in microseconds.
* @param contentResumeOffsetUs The timestamp offset which should be added to the content stream
* when resuming playback after the ad group. An offset of 0 collapses the ad group to a
* single insertion point, an offset of {@code toPositionUs-fromPositionUs} keeps the original
* stream timestamps after the ad group.
* @param adDurationsUs The durations of the ads to be added to the group, in microseconds.
* @return The updated {@link AdPlaybackState}.
*/
@CheckResult
public static AdPlaybackState addAdGroupToAdPlaybackState(
AdPlaybackState adPlaybackState,
long fromPositionUs,
long toPositionUs,
long contentResumeOffsetUs) {
long contentResumeOffsetUs,
long... adDurationsUs) {
long adGroupInsertionPositionUs =
getMediaPeriodPositionUsForContent(
fromPositionUs, /* nextAdGroupIndex= */ C.INDEX_UNSET, adPlaybackState);
......@@ -59,39 +62,21 @@ public final class ServerSideAdInsertionUtil {
&& adPlaybackState.getAdGroup(insertionIndex).timeUs <= adGroupInsertionPositionUs) {
insertionIndex++;
}
long adDurationUs = toPositionUs - fromPositionUs;
adPlaybackState =
adPlaybackState
.withNewAdGroup(insertionIndex, adGroupInsertionPositionUs)
.withIsServerSideInserted(insertionIndex, /* isServerSideInserted= */ true)
.withAdCount(insertionIndex, /* adCount= */ 1)
.withAdDurationsUs(insertionIndex, adDurationUs)
.withAdCount(insertionIndex, /* adCount= */ adDurationsUs.length)
.withAdDurationsUs(insertionIndex, adDurationsUs)
.withContentResumeOffsetUs(insertionIndex, contentResumeOffsetUs);
return correctFollowingAdGroupTimes(
adPlaybackState, insertionIndex, adDurationUs, contentResumeOffsetUs);
}
/**
* Returns the duration of the underlying server-side inserted ads stream for the current {@link
* Timeline.Period} in the {@link Player}.
*
* @param player The {@link Player}.
* @param adPlaybackState The {@link AdPlaybackState} defining the ad groups.
* @return The duration of the underlying server-side inserted ads stream, in microseconds, or
* {@link C#TIME_UNSET} if it can't be determined.
*/
public static long getStreamDurationUs(Player player, AdPlaybackState adPlaybackState) {
Timeline timeline = player.getCurrentTimeline();
if (timeline.isEmpty()) {
return C.TIME_UNSET;
// Mark all ads as skipped that are before the first ad with a non-zero duration.
int adIndex = 0;
while (adIndex < adDurationsUs.length && adDurationsUs[adIndex] == 0) {
adPlaybackState =
adPlaybackState.withSkippedAd(insertionIndex, /* adIndexInAdGroup= */ adIndex++);
}
Timeline.Period period =
timeline.getPeriod(player.getCurrentPeriodIndex(), new Timeline.Period());
if (period.durationUs == C.TIME_UNSET) {
return C.TIME_UNSET;
}
return getStreamPositionUsForContent(
period.durationUs, /* nextAdGroupIndex= */ C.INDEX_UNSET, adPlaybackState);
return correctFollowingAdGroupTimes(
adPlaybackState, insertionIndex, sum(adDurationsUs), contentResumeOffsetUs);
}
/**
......
......@@ -490,7 +490,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
releaseCodec();
}
eventDispatcher.enabled(decoderCounters);
frameReleaseHelper.onEnabled();
mayRenderFirstFrameAfterEnableIfNotStarted = mayRenderStartOfStream;
renderedFirstFrameAfterEnable = false;
}
......@@ -558,7 +557,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
clearReportedVideoSize();
clearRenderedFirstFrame();
haveReportedFirstFrameRenderedForCurrentSurface = false;
frameReleaseHelper.onDisabled();
tunnelingOnFrameRenderedListener = null;
try {
super.onDisabled();
......@@ -770,7 +768,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@Override
protected void onCodecInitialized(
String name, long initializedTimestampMs, long initializationDurationMs) {
String name,
MediaCodecAdapter.Configuration configuration,
long initializedTimestampMs,
long initializationDurationMs) {
eventDispatcher.decoderInitialized(name, initializedTimestampMs, initializationDurationMs);
codecNeedsSetOutputSurfaceWorkaround = codecNeedsSetOutputSurfaceWorkaround(name);
codecHandlesHdr10PlusOutOfBandMetadata =
......
......@@ -149,18 +149,14 @@ public final class VideoFrameReleaseHelper {
updateSurfacePlaybackFrameRate(/* forceUpdate= */ true);
}
/** Called when the renderer is enabled. */
public void onEnabled() {
if (displayHelper != null) {
checkNotNull(vsyncSampler).addObserver();
displayHelper.register(this::updateDefaultDisplayRefreshRateParams);
}
}
/** Called when the renderer is started. */
public void onStarted() {
started = true;
resetAdjustment();
if (displayHelper != null) {
checkNotNull(vsyncSampler).addObserver();
displayHelper.register(this::updateDefaultDisplayRefreshRateParams);
}
updateSurfacePlaybackFrameRate(/* forceUpdate= */ false);
}
......@@ -227,15 +223,11 @@ public final class VideoFrameReleaseHelper {
/** Called when the renderer is stopped. */
public void onStopped() {
started = false;
clearSurfaceFrameRate();
}
/** Called when the renderer is disabled. */
public void onDisabled() {
if (displayHelper != null) {
displayHelper.unregister();
checkNotNull(vsyncSampler).removeObserver();
}
clearSurfaceFrameRate();
}
// Frame release time adjustment.
......
......@@ -9325,6 +9325,85 @@ public final class ExoPlayerTest {
}
@Test
public void setPlaybackSpeed_withAdPlayback_onlyAppliesToContent() throws Exception {
// Create renderer with media clock to listen to playback parameter changes.
ArrayList<PlaybackParameters> playbackParameters = new ArrayList<>();
FakeMediaClockRenderer audioRenderer =
new FakeMediaClockRenderer(C.TRACK_TYPE_AUDIO) {
private long positionUs;
@Override
protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs) {
this.positionUs = offsetUs;
}
@Override
public long getPositionUs() {
// Continuously increase position to let playback progress.
positionUs += 10_000;
return positionUs;
}
@Override
public void setPlaybackParameters(PlaybackParameters parameters) {
playbackParameters.add(parameters);
}
@Override
public PlaybackParameters getPlaybackParameters() {
return playbackParameters.isEmpty()
? PlaybackParameters.DEFAULT
: Iterables.getLast(playbackParameters);
}
};
ExoPlayer player = new TestExoPlayerBuilder(context).setRenderers(audioRenderer).build();
AdPlaybackState adPlaybackState =
FakeTimeline.createAdPlaybackState(
/* adsPerAdGroup= */ 1,
/* adGroupTimesUs...= */ 0,
7 * C.MICROS_PER_SECOND,
C.TIME_END_OF_SOURCE);
TimelineWindowDefinition adTimelineDefinition =
new TimelineWindowDefinition(
/* periodCount= */ 1,
/* id= */ 0,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* isLive= */ false,
/* isPlaceholder= */ false,
/* durationUs= */ 10 * C.MICROS_PER_SECOND,
/* defaultPositionUs= */ 0,
/* windowOffsetInFirstPeriodUs= */ 0,
adPlaybackState);
player.setMediaSource(
new FakeMediaSource(
new FakeTimeline(adTimelineDefinition), ExoPlayerTestRunner.AUDIO_FORMAT));
Player.Listener mockListener = mock(Player.Listener.class);
player.addListener(mockListener);
player.setPlaybackSpeed(5f);
player.prepare();
player.play();
runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
// Assert that the renderer received the playback speed updates at each ad/content boundary.
assertThat(playbackParameters)
.containsExactly(
/* preroll ad */ new PlaybackParameters(1f),
/* content after preroll */ new PlaybackParameters(5f),
/* midroll ad */ new PlaybackParameters(1f),
/* content after midroll */ new PlaybackParameters(5f),
/* postroll ad */ new PlaybackParameters(1f),
/* content after postroll */ new PlaybackParameters(5f))
.inOrder();
// Assert that user-set speed was reported, but none of the ad overrides.
verify(mockListener).onPlaybackParametersChanged(any());
verify(mockListener).onPlaybackParametersChanged(new PlaybackParameters(5.0f));
}
@Test
public void targetLiveOffsetInMedia_withSetPlaybackParameters_usesPlaybackParameterSpeed()
throws Exception {
long windowStartUnixTimeMs = 987_654_321_000L;
......
......@@ -22,7 +22,9 @@ import static org.robolectric.Shadows.shadowOf;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
import com.google.android.exoplayer2.analytics.PlayerId;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller;
......@@ -37,6 +39,7 @@ import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
import com.google.android.exoplayer2.upstream.Allocator;
import com.google.android.exoplayer2.util.Clock;
import com.google.common.collect.ImmutableList;
import org.junit.Before;
import org.junit.Test;
......@@ -74,12 +77,16 @@ public final class MediaPeriodQueueTest {
@Before
public void setUp() {
AnalyticsCollector analyticsCollector = new AnalyticsCollector(Clock.DEFAULT);
analyticsCollector.setPlayer(
new ExoPlayer.Builder(ApplicationProvider.getApplicationContext()).build(),
Looper.getMainLooper());
mediaPeriodQueue =
new MediaPeriodQueue(/* analyticsCollector= */ null, new Handler(Looper.getMainLooper()));
new MediaPeriodQueue(analyticsCollector, new Handler(Looper.getMainLooper()));
mediaSourceList =
new MediaSourceList(
mock(MediaSourceList.MediaSourceListInfoRefreshListener.class),
/* analyticsCollector= */ null,
analyticsCollector,
new Handler(Looper.getMainLooper()),
PlayerId.UNSET);
rendererCapabilities = new RendererCapabilities[0];
......
......@@ -25,12 +25,16 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.os.Looper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
import com.google.android.exoplayer2.analytics.PlayerId;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ShuffleOrder;
import com.google.android.exoplayer2.testutil.FakeMediaSource;
import com.google.android.exoplayer2.testutil.FakeShuffleOrder;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.Collections;
......@@ -51,10 +55,14 @@ public class MediaSourceListTest {
@Before
public void setUp() {
AnalyticsCollector analyticsCollector = new AnalyticsCollector(Clock.DEFAULT);
analyticsCollector.setPlayer(
new ExoPlayer.Builder(ApplicationProvider.getApplicationContext()).build(),
Looper.getMainLooper());
mediaSourceList =
new MediaSourceList(
mock(MediaSourceList.MediaSourceListInfoRefreshListener.class),
/* analyticsCollector= */ null,
analyticsCollector,
Util.createHandlerForCurrentOrMainLooper(),
PlayerId.UNSET);
}
......
......@@ -744,6 +744,126 @@ public final class DefaultPlaybackSessionManagerTest {
}
@Test
public void timelineUpdate_toNewMediaWithWindowIndexOnly_finishesOtherSessions() {
Timeline firstTimeline =
new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1000),
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 2000),
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 3000));
EventTime eventTimeFirstTimelineWithPeriodId =
createEventTime(
firstTimeline,
/* windowIndex= */ 0,
new MediaPeriodId(
firstTimeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));
EventTime eventTimeFirstTimelineWindowOnly1 =
createEventTime(firstTimeline, /* windowIndex= */ 1, /* mediaPeriodId= */ null);
EventTime eventTimeFirstTimelineWindowOnly2 =
createEventTime(firstTimeline, /* windowIndex= */ 2, /* mediaPeriodId= */ null);
Timeline secondTimeline =
new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 2000),
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 4000));
EventTime eventTimeSecondTimeline =
createEventTime(secondTimeline, /* windowIndex= */ 0, /* mediaPeriodId= */ null);
sessionManager.updateSessionsWithTimelineChange(eventTimeFirstTimelineWithPeriodId);
sessionManager.updateSessions(eventTimeFirstTimelineWindowOnly1);
sessionManager.updateSessions(eventTimeFirstTimelineWindowOnly2);
sessionManager.updateSessionsWithTimelineChange(eventTimeSecondTimeline);
InOrder inOrder = inOrder(mockListener);
ArgumentCaptor<String> firstId = ArgumentCaptor.forClass(String.class);
inOrder
.verify(mockListener)
.onSessionCreated(eq(eventTimeFirstTimelineWithPeriodId), firstId.capture());
inOrder
.verify(mockListener)
.onSessionActive(eventTimeFirstTimelineWithPeriodId, firstId.getValue());
ArgumentCaptor<String> secondId = ArgumentCaptor.forClass(String.class);
inOrder
.verify(mockListener)
.onSessionCreated(eq(eventTimeFirstTimelineWindowOnly1), secondId.capture());
ArgumentCaptor<String> thirdId = ArgumentCaptor.forClass(String.class);
inOrder
.verify(mockListener)
.onSessionCreated(eq(eventTimeFirstTimelineWindowOnly2), thirdId.capture());
// The sessions may finish at the same time, so the order of these two callbacks is undefined.
ArgumentCaptor<String> finishedSessions = ArgumentCaptor.forClass(String.class);
inOrder
.verify(mockListener, times(2))
.onSessionFinished(
eq(eventTimeSecondTimeline),
finishedSessions.capture(),
/* automaticTransitionToNextPlayback= */ eq(false));
assertThat(finishedSessions.getAllValues())
.containsExactly(firstId.getValue(), thirdId.getValue());
inOrder.verify(mockListener).onSessionActive(eventTimeSecondTimeline, secondId.getValue());
inOrder.verifyNoMoreInteractions();
}
@Test
public void timelineUpdate_toNewMediaWithMediaPeriodId_finishesOtherSessions() {
Timeline firstTimeline =
new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1000),
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 2000),
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 3000));
EventTime eventTimeFirstTimeline1 =
createEventTime(
firstTimeline,
/* windowIndex= */ 0,
new MediaPeriodId(
firstTimeline.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0));
EventTime eventTimeFirstTimeline2 =
createEventTime(
firstTimeline,
/* windowIndex= */ 1,
new MediaPeriodId(
firstTimeline.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1));
EventTime eventTimeFirstTimeline3 =
createEventTime(
firstTimeline,
/* windowIndex= */ 2,
new MediaPeriodId(
firstTimeline.getUidOfPeriod(/* periodIndex= */ 2), /* windowSequenceNumber= */ 2));
Timeline secondTimeline =
new FakeTimeline(
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 2000),
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 1000),
new TimelineWindowDefinition(/* periodCount= */ 1, /* id= */ 3000));
EventTime eventTimeSecondTimeline =
createEventTime(
secondTimeline,
/* windowIndex= */ 0,
new MediaPeriodId(
secondTimeline.getUidOfPeriod(/* periodIndex= */ 0),
/* windowSequenceNumber= */ 1));
sessionManager.updateSessionsWithTimelineChange(eventTimeFirstTimeline1);
sessionManager.updateSessions(eventTimeFirstTimeline2);
sessionManager.updateSessions(eventTimeFirstTimeline3);
sessionManager.updateSessionsWithTimelineChange(eventTimeSecondTimeline);
InOrder inOrder = inOrder(mockListener);
ArgumentCaptor<String> firstId = ArgumentCaptor.forClass(String.class);
inOrder.verify(mockListener).onSessionCreated(eq(eventTimeFirstTimeline1), firstId.capture());
inOrder.verify(mockListener).onSessionActive(eventTimeFirstTimeline1, firstId.getValue());
ArgumentCaptor<String> secondId = ArgumentCaptor.forClass(String.class);
inOrder.verify(mockListener).onSessionCreated(eq(eventTimeFirstTimeline2), secondId.capture());
ArgumentCaptor<String> thirdId = ArgumentCaptor.forClass(String.class);
inOrder.verify(mockListener).onSessionCreated(eq(eventTimeFirstTimeline3), thirdId.capture());
inOrder
.verify(mockListener)
.onSessionFinished(
eventTimeSecondTimeline,
firstId.getValue(),
/* automaticTransitionToNextPlayback= */ false);
inOrder.verify(mockListener).onSessionActive(eventTimeSecondTimeline, secondId.getValue());
inOrder.verifyNoMoreInteractions();
}
@Test
public void positionDiscontinuity_withinWindow_doesNotFinishSession() {
Timeline timeline =
new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ 100));
......
......@@ -48,7 +48,9 @@ public final class MkvPlaybackTest {
"sample_with_ssa_subtitles.mkv",
"sample_with_null_terminated_ssa_subtitles.mkv",
"sample_with_srt.mkv",
"sample_with_null_terminated_srt.mkv");
"sample_with_null_terminated_srt.mkv",
"sample_with_vtt_subtitles.mkv",
"sample_with_null_terminated_vtt_subtitles.mkv");
}
@ParameterizedRobolectricTestRunner.Parameter public String inputFile;
......
......@@ -182,20 +182,20 @@ public final class ServerSideAdInsertionMediaSourceTest {
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ 0,
/* toPositionUs= */ 200_000,
/* contentResumeOffsetUs= */ 0);
/* contentResumeOffsetUs= */ 0,
/* adDurationsUs...= */ 200_000);
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ 400_000,
/* toPositionUs= */ 700_000,
/* contentResumeOffsetUs= */ 1_000_000);
/* contentResumeOffsetUs= */ 1_000_000,
/* adDurationsUs...= */ 300_000);
AdPlaybackState firstAdPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ 900_000,
/* toPositionUs= */ 1_000_000,
/* contentResumeOffsetUs= */ 0);
/* contentResumeOffsetUs= */ 0,
/* adDurationsUs...= */ 100_000);
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
mediaSourceRef.set(
......@@ -252,8 +252,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
addAdGroupToAdPlaybackState(
new AdPlaybackState(/* adsId= */ new Object()),
/* fromPositionUs= */ 900_000,
/* toPositionUs= */ 1_000_000,
/* contentResumeOffsetUs= */ 0);
/* contentResumeOffsetUs= */ 0,
/* adDurationsUs...= */ 100_000);
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
mediaSourceRef.set(
new ServerSideAdInsertionMediaSource(
......@@ -280,8 +280,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
addAdGroupToAdPlaybackState(
firstAdPlaybackState,
/* fromPositionUs= */ 0,
/* toPositionUs= */ 500_000,
/* contentResumeOffsetUs= */ 0);
/* contentResumeOffsetUs= */ 0,
/* adDurationsUs...= */ 500_000);
mediaSourceRef
.get()
.setAdPlaybackStates(ImmutableMap.of(periodUid.get(), secondAdPlaybackState));
......@@ -323,8 +323,8 @@ public final class ServerSideAdInsertionMediaSourceTest {
addAdGroupToAdPlaybackState(
new AdPlaybackState(/* adsId= */ new Object()),
/* fromPositionUs= */ 0,
/* toPositionUs= */ 500_000,
/* contentResumeOffsetUs= */ 0);
/* contentResumeOffsetUs= */ 0,
/* adDurationsUs...= */ 500_000);
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
mediaSourceRef.set(
new ServerSideAdInsertionMediaSource(
......@@ -391,20 +391,20 @@ public final class ServerSideAdInsertionMediaSourceTest {
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ 0,
/* toPositionUs= */ 100_000,
/* contentResumeOffsetUs= */ 0);
/* contentResumeOffsetUs= */ 0,
/* adDurationsUs...= */ 100_000);
adPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ 600_000,
/* toPositionUs= */ 700_000,
/* contentResumeOffsetUs= */ 1_000_000);
/* contentResumeOffsetUs= */ 1_000_000,
/* adDurationsUs...= */ 100_000);
AdPlaybackState firstAdPlaybackState =
addAdGroupToAdPlaybackState(
adPlaybackState,
/* fromPositionUs= */ 900_000,
/* toPositionUs= */ 1_000_000,
/* contentResumeOffsetUs= */ 0);
/* contentResumeOffsetUs= */ 0,
/* adDurationsUs...= */ 100_000);
AtomicReference<ServerSideAdInsertionMediaSource> mediaSourceRef = new AtomicReference<>();
mediaSourceRef.set(
......@@ -427,7 +427,7 @@ public final class ServerSideAdInsertionMediaSourceTest {
player.setMediaSource(mediaSourceRef.get());
player.prepare();
// Play to the first content part, then seek past the midroll.
playUntilPosition(player, /* windowIndex= */ 0, /* positionMs= */ 100);
playUntilPosition(player, /* mediaItemIndex= */ 0, /* positionMs= */ 100);
player.seekTo(/* positionMs= */ 1_600);
runUntilPendingCommandsAreFullyHandled(player);
long positionAfterSeekMs = player.getCurrentPosition();
......
......@@ -22,6 +22,7 @@ import static com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil
import static com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil.getStreamPositionUsForAd;
import static com.google.android.exoplayer2.source.ads.ServerSideAdInsertionUtil.getStreamPositionUsForContent;
import static com.google.common.truth.Truth.assertThat;
import static java.util.Arrays.stream;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
......@@ -46,8 +47,8 @@ public final class ServerSideAdInsertionUtilTest {
addAdGroupToAdPlaybackState(
state,
/* fromPositionUs= */ 4300,
/* toPositionUs= */ 4500,
/* contentResumeOffsetUs= */ 400);
/* contentResumeOffsetUs= */ 400,
/* adDurationsUs...= */ 200);
assertThat(state)
.isEqualTo(
......@@ -64,8 +65,8 @@ public final class ServerSideAdInsertionUtilTest {
addAdGroupToAdPlaybackState(
state,
/* fromPositionUs= */ 2100,
/* toPositionUs= */ 2400,
/* contentResumeOffsetUs= */ 0);
/* contentResumeOffsetUs= */ 0,
/* adDurationsUs...= */ 300);
assertThat(state)
.isEqualTo(
......@@ -86,8 +87,8 @@ public final class ServerSideAdInsertionUtilTest {
addAdGroupToAdPlaybackState(
state,
/* fromPositionUs= */ 0,
/* toPositionUs= */ 100,
/* contentResumeOffsetUs= */ 50);
/* contentResumeOffsetUs= */ 50,
/* adDurationsUs...= */ 100);
assertThat(state)
.isEqualTo(
......@@ -112,8 +113,8 @@ public final class ServerSideAdInsertionUtilTest {
addAdGroupToAdPlaybackState(
state,
/* fromPositionUs= */ 5000,
/* toPositionUs= */ 6000,
/* contentResumeOffsetUs= */ 0);
/* contentResumeOffsetUs= */ 0,
/* adDurationsUs...= */ 1000);
assertThat(state)
.isEqualTo(
......@@ -144,6 +145,33 @@ public final class ServerSideAdInsertionUtilTest {
}
@Test
public void addAdGroupToAdPlaybackState_emptyLeadingAds_markedAsSkipped() {
AdPlaybackState state = new AdPlaybackState(ADS_ID);
state =
addAdGroupToAdPlaybackState(
state,
/* fromPositionUs= */ 0,
/* contentResumeOffsetUs= */ 50_000,
/* adDurationsUs...= */ 0,
0,
10_000,
40_000,
0);
AdPlaybackState.AdGroup adGroup = state.getAdGroup(/* adGroupIndex= */ 0);
assertThat(adGroup.durationsUs[0]).isEqualTo(0);
assertThat(adGroup.states[0]).isEqualTo(AdPlaybackState.AD_STATE_SKIPPED);
assertThat(adGroup.durationsUs[1]).isEqualTo(0);
assertThat(adGroup.states[1]).isEqualTo(AdPlaybackState.AD_STATE_SKIPPED);
assertThat(adGroup.durationsUs[2]).isEqualTo(10_000);
assertThat(adGroup.states[2]).isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);
assertThat(adGroup.durationsUs[4]).isEqualTo(0);
assertThat(adGroup.states[4]).isEqualTo(AdPlaybackState.AD_STATE_UNAVAILABLE);
assertThat(stream(adGroup.durationsUs).sum()).isEqualTo(50_000);
}
@Test
public void getStreamPositionUsForAd_returnsCorrectPositions() {
// stream: 0-- ad1 --200-- content --2100-- ad2 --2300-- content --4300-- ad3 --4500-- content
// content timeline: 0 - [ad1] - 100-2000 -[ad2] - 2000-4000 - [ad3] - 4400-end
......
......@@ -66,7 +66,9 @@ public final class BaseUrlExclusionList {
public void exclude(BaseUrl baseUrlToExclude, long exclusionDurationMs) {
long excludeUntilMs = SystemClock.elapsedRealtime() + exclusionDurationMs;
addExclusion(baseUrlToExclude.serviceLocation, excludeUntilMs, excludedServiceLocations);
addExclusion(baseUrlToExclude.priority, excludeUntilMs, excludedPriorities);
if (baseUrlToExclude.priority != BaseUrl.PRIORITY_UNSET) {
addExclusion(baseUrlToExclude.priority, excludeUntilMs, excludedPriorities);
}
}
/**
......
......@@ -217,7 +217,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
periodDurationUs,
representation,
selectedBaseUrl != null ? selectedBaseUrl : representation.baseUrls.get(0),
BundledChunkExtractor.FACTORY.createProgressiveMediaExtractor(
chunkExtractorFactory.createProgressiveMediaExtractor(
trackType,
representation.format,
enableEventMessageTrack,
......
......@@ -21,10 +21,12 @@ import com.google.common.base.Objects;
/** A base URL, as defined by ISO 23009-1, 2nd edition, 5.6. and ETSI TS 103 285 V1.2.1, 10.8.2.1 */
public final class BaseUrl {
/** The default priority. */
public static final int DEFAULT_PRIORITY = 1;
/** The default weight. */
public static final int DEFAULT_WEIGHT = 1;
/** The default priority. */
public static final int DEFAULT_DVB_PRIORITY = 1;
/** Constant representing an unset priority in a manifest that does not declare a DVB profile. */
public static final int PRIORITY_UNSET = Integer.MIN_VALUE;
/** The URL. */
public final String url;
......@@ -36,11 +38,11 @@ public final class BaseUrl {
public final int weight;
/**
* Creates an instance with {@link #DEFAULT_PRIORITY default priority}, {@link #DEFAULT_WEIGHT
* Creates an instance with {@link #PRIORITY_UNSET an unset priority}, {@link #DEFAULT_WEIGHT
* default weight} and using the URL as the service location.
*/
public BaseUrl(String url) {
this(url, /* serviceLocation= */ url, DEFAULT_PRIORITY, DEFAULT_WEIGHT);
this(url, /* serviceLocation= */ url, PRIORITY_UNSET, DEFAULT_WEIGHT);
}
/** Creates an instance. */
......
......@@ -213,7 +213,7 @@ public abstract class Representation {
new RangedUri(null, initializationStart, initializationEnd - initializationStart + 1);
SingleSegmentBase segmentBase =
new SingleSegmentBase(rangedUri, 1, 0, indexStart, indexEnd - indexStart + 1);
List<BaseUrl> baseUrls = ImmutableList.of(new BaseUrl(uri));
ImmutableList<BaseUrl> baseUrls = ImmutableList.of(new BaseUrl(uri));
return new SingleSegmentRepresentation(
revisionId,
format,
......
......@@ -15,6 +15,8 @@
*/
package com.google.android.exoplayer2.source.dash;
import static com.google.android.exoplayer2.source.dash.manifest.BaseUrl.DEFAULT_DVB_PRIORITY;
import static com.google.android.exoplayer2.source.dash.manifest.BaseUrl.DEFAULT_WEIGHT;
import static com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy.DEFAULT_LOCATION_EXCLUSION_MS;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
......@@ -174,6 +176,32 @@ public class BaseUrlExclusionListTest {
}
@Test
public void selectBaseUrl_priorityUnset_isNotExcluded() {
BaseUrlExclusionList baseUrlExclusionList = new BaseUrlExclusionList();
ImmutableList<BaseUrl> baseUrls =
ImmutableList.of(
new BaseUrl(
/* url= */ "a-1",
/* serviceLocation= */ "a",
BaseUrl.PRIORITY_UNSET,
/* weight= */ 1),
new BaseUrl(
/* url= */ "a-2",
/* serviceLocation= */ "a",
BaseUrl.PRIORITY_UNSET,
/* weight= */ 1),
new BaseUrl(
/* url= */ "b",
/* serviceLocation= */ "b",
BaseUrl.PRIORITY_UNSET,
/* weight= */ 1));
baseUrlExclusionList.exclude(baseUrls.get(0), 10_000);
assertThat(baseUrlExclusionList.selectBaseUrl(baseUrls).serviceLocation).isEqualTo("b");
}
@Test
public void selectBaseUrl_emptyBaseUrlList_selectionIsNull() {
BaseUrlExclusionList baseUrlExclusionList = new BaseUrlExclusionList();
......@@ -183,7 +211,8 @@ public class BaseUrlExclusionListTest {
@Test
public void reset_dropsAllExclusions() {
BaseUrlExclusionList baseUrlExclusionList = new BaseUrlExclusionList();
List<BaseUrl> baseUrls = ImmutableList.of(new BaseUrl("a"));
ImmutableList<BaseUrl> baseUrls =
ImmutableList.of(new BaseUrl("a", "a", DEFAULT_DVB_PRIORITY, DEFAULT_WEIGHT));
baseUrlExclusionList.exclude(baseUrls.get(0), 5000);
baseUrlExclusionList.reset();
......
......@@ -61,6 +61,10 @@ public class DashManifestParserTest {
"media/mpd/sample_mpd_availabilityTimeOffset_baseUrl";
private static final String SAMPLE_MPD_MULTIPLE_BASE_URLS =
"media/mpd/sample_mpd_multiple_baseUrls";
private static final String SAMPLE_MPD_RELATIVE_BASE_URLS_DVB_PROFILE_NOT_DECLARED =
"media/mpd/sample_mpd_relative_baseUrls_dvb_profile_not_declared";
private static final String SAMPLE_MPD_RELATIVE_BASE_URLS_DVB_PROFILE_DECLARED =
"media/mpd/sample_mpd_relative_baseUrls_dvb_profile_declared";
private static final String SAMPLE_MPD_AVAILABILITY_TIME_OFFSET_SEGMENT_TEMPLATE =
"media/mpd/sample_mpd_availabilityTimeOffset_segmentTemplate";
private static final String SAMPLE_MPD_AVAILABILITY_TIME_OFFSET_SEGMENT_LIST =
......@@ -749,6 +753,41 @@ public class DashManifestParserTest {
}
@Test
public void baseUrl_relativeBaseUrlsNoDvbNamespace_hasDifferentPrioritiesAndServiceLocation()
throws IOException {
DashManifestParser parser = new DashManifestParser();
DashManifest manifest =
parser.parse(
Uri.parse("https://example.com/test.mpd"),
TestUtil.getInputStream(
ApplicationProvider.getApplicationContext(),
SAMPLE_MPD_RELATIVE_BASE_URLS_DVB_PROFILE_NOT_DECLARED));
ImmutableList<BaseUrl> baseUrls =
manifest.getPeriod(0).adaptationSets.get(0).representations.get(0).baseUrls;
assertThat(baseUrls.get(0).priority).isEqualTo(BaseUrl.PRIORITY_UNSET);
assertThat(baseUrls.get(1).priority).isEqualTo(BaseUrl.PRIORITY_UNSET);
assertThat(baseUrls.get(0).serviceLocation).isNotEqualTo(baseUrls.get(1).serviceLocation);
}
@Test
public void baseUrl_relativeBaseUrlsWithDvbNamespace_inheritsPrioritiesAndServiceLocation()
throws IOException {
DashManifestParser parser = new DashManifestParser();
DashManifest manifest =
parser.parse(
Uri.parse("https://example.com/test.mpd"),
TestUtil.getInputStream(
ApplicationProvider.getApplicationContext(),
SAMPLE_MPD_RELATIVE_BASE_URLS_DVB_PROFILE_DECLARED));
ImmutableList<BaseUrl> baseUrls =
manifest.getPeriod(0).adaptationSets.get(0).representations.get(0).baseUrls;
assertThat(baseUrls.get(0).priority).isEqualTo(baseUrls.get(1).priority);
assertThat(baseUrls.get(0).serviceLocation).isEqualTo(baseUrls.get(1).serviceLocation);
}
@Test
public void serviceDescriptionElement_allValuesSet() throws IOException {
DashManifestParser parser = new DashManifestParser();
......
......@@ -109,8 +109,7 @@ public class DownloadManagerDashTest {
testThread.release();
}
// Disabled due to flakiness.
@Ignore
@Ignore("Disabled due to flakiness")
@Test
public void saveAndLoadActionFile() throws Throwable {
// Configure fakeDataSet to block until interrupted when TEST_MPD is read.
......
......@@ -157,7 +157,7 @@ public class DownloadServiceDashTest {
testThread.release();
}
@Ignore // b/78877092
@Ignore("Internal ref: b/78877092")
@Test
public void multipleDownloadRequest() throws Throwable {
downloadKeys(fakeStreamKey1);
......@@ -168,7 +168,7 @@ public class DownloadServiceDashTest {
assertCachedData(cache, fakeDataSet);
}
@Ignore // b/78877092
@Ignore("Internal ref: b/78877092")
@Test
public void removeAction() throws Throwable {
downloadKeys(fakeStreamKey1, fakeStreamKey2);
......@@ -182,7 +182,7 @@ public class DownloadServiceDashTest {
assertCacheEmpty(cache);
}
@Ignore // b/78877092
@Ignore("Internal ref: b/78877092")
@Test
public void removeBeforeDownloadComplete() throws Throwable {
pauseDownloadCondition = new ConditionVariable();
......
......@@ -24,10 +24,9 @@ import com.google.android.exoplayer2.metadata.flac.PictureFrame;
import com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
......@@ -168,9 +167,12 @@ public final class FlacMetadataReader {
metadataHolder.flacStreamMetadata =
flacStreamMetadata.copyWithVorbisComments(vorbisComments);
} else if (type == FlacConstants.METADATA_TYPE_PICTURE) {
PictureFrame pictureFrame = readPictureMetadataBlock(input, length);
ParsableByteArray pictureBlock = new ParsableByteArray(length);
input.readFully(pictureBlock.getData(), 0, length);
pictureBlock.skipBytes(FlacConstants.METADATA_BLOCK_HEADER_SIZE);
PictureFrame pictureFrame = PictureFrame.fromPictureBlock(pictureBlock);
metadataHolder.flacStreamMetadata =
flacStreamMetadata.copyWithPictureFrames(Collections.singletonList(pictureFrame));
flacStreamMetadata.copyWithPictureFrames(ImmutableList.of(pictureFrame));
} else {
input.skipFully(length);
}
......@@ -268,28 +270,5 @@ public final class FlacMetadataReader {
return Arrays.asList(commentHeader.comments);
}
private static PictureFrame readPictureMetadataBlock(ExtractorInput input, int length)
throws IOException {
ParsableByteArray scratch = new ParsableByteArray(length);
input.readFully(scratch.getData(), 0, length);
scratch.skipBytes(FlacConstants.METADATA_BLOCK_HEADER_SIZE);
int pictureType = scratch.readInt();
int mimeTypeLength = scratch.readInt();
String mimeType = scratch.readString(mimeTypeLength, Charsets.US_ASCII);
int descriptionLength = scratch.readInt();
String description = scratch.readString(descriptionLength);
int width = scratch.readInt();
int height = scratch.readInt();
int depth = scratch.readInt();
int colors = scratch.readInt();
int pictureDataLength = scratch.readInt();
byte[] pictureData = new byte[pictureDataLength];
scratch.readBytes(pictureData, 0, pictureDataLength);
return new PictureFrame(
pictureType, mimeType, description, width, height, depth, colors, pictureData);
}
private FlacMetadataReader() {}
}
......@@ -15,13 +15,13 @@
*/
package com.google.android.exoplayer2.extractor;
import static com.google.android.exoplayer2.extractor.VorbisUtil.parseVorbisComments;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.flac.PictureFrame;
import com.google.android.exoplayer2.metadata.flac.VorbisComment;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.Util;
......@@ -60,8 +60,6 @@ public final class FlacStreamMetadata {
/** Indicates that a value is not in the corresponding lookup table. */
public static final int NOT_IN_LOOKUP_TABLE = -1;
/** Separator between the field name of a Vorbis comment and the corresponding value. */
private static final String SEPARATOR = "=";
/** Minimum number of samples per block. */
public final int minBlockSizeSamples;
......@@ -149,7 +147,7 @@ public final class FlacStreamMetadata {
bitsPerSample,
totalSamples,
/* seekTable= */ null,
buildMetadata(vorbisComments, pictureFrames));
concatenateVorbisMetadata(vorbisComments, pictureFrames));
}
private FlacStreamMetadata(
......@@ -274,8 +272,7 @@ public final class FlacStreamMetadata {
public FlacStreamMetadata copyWithVorbisComments(List<String> vorbisComments) {
@Nullable
Metadata appendedMetadata =
getMetadataCopyWithAppendedEntriesFrom(
buildMetadata(vorbisComments, Collections.emptyList()));
getMetadataCopyWithAppendedEntriesFrom(parseVorbisComments(vorbisComments));
return new FlacStreamMetadata(
minBlockSizeSamples,
maxBlockSizeSamples,
......@@ -292,9 +289,7 @@ public final class FlacStreamMetadata {
/** Returns a copy of {@code this} with the given picture frames added to the metadata. */
public FlacStreamMetadata copyWithPictureFrames(List<PictureFrame> pictureFrames) {
@Nullable
Metadata appendedMetadata =
getMetadataCopyWithAppendedEntriesFrom(
buildMetadata(Collections.emptyList(), pictureFrames));
Metadata appendedMetadata = getMetadataCopyWithAppendedEntriesFrom(new Metadata(pictureFrames));
return new FlacStreamMetadata(
minBlockSizeSamples,
maxBlockSizeSamples,
......@@ -308,6 +303,20 @@ public final class FlacStreamMetadata {
appendedMetadata);
}
/**
* Returns a new {@link Metadata} instance created from {@code vorbisComments} and {@code
* pictureFrames}.
*/
@Nullable
private static Metadata concatenateVorbisMetadata(
List<String> vorbisComments, List<PictureFrame> pictureFrames) {
@Nullable Metadata parsedVorbisComments = parseVorbisComments(vorbisComments);
if (parsedVorbisComments == null && pictureFrames.isEmpty()) {
return null;
}
return new Metadata(pictureFrames).copyWithAppendedEntriesFrom(parsedVorbisComments);
}
private static int getSampleRateLookupKey(int sampleRate) {
switch (sampleRate) {
case 88200:
......@@ -353,27 +362,4 @@ public final class FlacStreamMetadata {
return NOT_IN_LOOKUP_TABLE;
}
}
@Nullable
private static Metadata buildMetadata(
List<String> vorbisComments, List<PictureFrame> pictureFrames) {
if (vorbisComments.isEmpty() && pictureFrames.isEmpty()) {
return null;
}
ArrayList<Metadata.Entry> metadataEntries = new ArrayList<>();
for (int i = 0; i < vorbisComments.size(); i++) {
String vorbisComment = vorbisComments.get(i);
String[] keyAndValue = Util.splitAtFirst(vorbisComment, SEPARATOR);
if (keyAndValue.length != 2) {
Log.w(TAG, "Failed to parse Vorbis comment: " + vorbisComment);
} else {
VorbisComment entry = new VorbisComment(keyAndValue[0], keyAndValue[1]);
metadataEntries.add(entry);
}
}
metadataEntries.addAll(pictureFrames);
return metadataEntries.isEmpty() ? null : new Metadata(metadataEntries);
}
}
......@@ -15,11 +15,20 @@
*/
package com.google.android.exoplayer2.extractor;
import android.util.Base64;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.Metadata.Entry;
import com.google.android.exoplayer2.metadata.flac.PictureFrame;
import com.google.android.exoplayer2.metadata.vorbis.VorbisComment;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/** Utility methods for parsing Vorbis streams. */
public final class VorbisUtil {
......@@ -249,6 +258,45 @@ public final class VorbisUtil {
}
/**
* Builds a {@link Metadata} instance from a list of Vorbis Comments.
*
* <p>METADATA_BLOCK_PICTURE comments will be transformed into {@link PictureFrame} entries. All
* others will be transformed into {@link VorbisComment} entries.
*
* @param vorbisComments The raw input of comments, as a key-value pair KEY=VAL.
* @return The fully parsed Metadata instance. Null if no vorbis comments could be parsed.
*/
@Nullable
public static Metadata parseVorbisComments(List<String> vorbisComments) {
List<Entry> metadataEntries = new ArrayList<>();
for (int i = 0; i < vorbisComments.size(); i++) {
String vorbisComment = vorbisComments.get(i);
String[] keyAndValue = Util.splitAtFirst(vorbisComment, "=");
if (keyAndValue.length != 2) {
Log.w(TAG, "Failed to parse Vorbis comment: " + vorbisComment);
continue;
}
if (keyAndValue[0].equals("METADATA_BLOCK_PICTURE")) {
// This tag is a special cover art tag, outlined by
// https://wiki.xiph.org/index.php/VorbisComment#Cover_art.
// Decode it from Base64 and transform it into a PictureFrame.
try {
byte[] decoded = Base64.decode(keyAndValue[1], Base64.DEFAULT);
metadataEntries.add(PictureFrame.fromPictureBlock(new ParsableByteArray(decoded)));
} catch (RuntimeException e) {
Log.w(TAG, "Failed to parse vorbis picture", e);
}
} else {
VorbisComment entry = new VorbisComment(keyAndValue[0], keyAndValue[1]);
metadataEntries.add(entry);
}
}
return metadataEntries.isEmpty() ? null : new Metadata(metadataEntries);
}
/**
* Verifies whether the next bytes in {@code header} are a Vorbis header of the given {@code
* headerType}.
*
......
......@@ -138,6 +138,7 @@ public class MatroskaExtractor implements Extractor {
private static final String CODEC_ID_PCM_FLOAT = "A_PCM/FLOAT/IEEE";
private static final String CODEC_ID_SUBRIP = "S_TEXT/UTF8";
private static final String CODEC_ID_ASS = "S_TEXT/ASS";
private static final String CODEC_ID_VTT = "S_TEXT/WEBVTT";
private static final String CODEC_ID_VOBSUB = "S_VOBSUB";
private static final String CODEC_ID_PGS = "S_HDMV/PGS";
private static final String CODEC_ID_DVBSUB = "S_DVBSUB";
......@@ -323,6 +324,32 @@ public class MatroskaExtractor implements Extractor {
/** The format of an SSA timecode. */
private static final String SSA_TIMECODE_FORMAT = "%01d:%02d:%02d:%02d";
/**
* A template for the prefix that must be added to each VTT sample.
*
* <p>The display time of each subtitle is passed as {@code timeUs} to {@link
* TrackOutput#sampleMetadata}. The start and end timecodes in this template are relative to
* {@code timeUs}. Hence the start timecode is always zero. The 12 byte end timecode starting at
* {@link #VTT_PREFIX_END_TIMECODE_OFFSET} is set to a placeholder value, and must be replaced
* with the duration of the subtitle.
*
* <p>Equivalent to the UTF-8 string: "WEBVTT\n\n00:00:00.000 --> 00:00:00.000\n".
*/
private static final byte[] VTT_PREFIX =
new byte[] {
87, 69, 66, 86, 84, 84, 10, 10, 48, 48, 58, 48, 48, 58, 48, 48, 46, 48, 48, 48, 32, 45, 45,
62, 32, 48, 48, 58, 48, 48, 58, 48, 48, 46, 48, 48, 48, 10
};
/** The byte offset of the end timecode in {@link #VTT_PREFIX}. */
private static final int VTT_PREFIX_END_TIMECODE_OFFSET = 25;
/**
* The value by which to divide a time in microseconds to convert it to the unit of the last value
* in a VTT timecode (milliseconds).
*/
private static final long VTT_TIMECODE_LAST_VALUE_SCALING_FACTOR = 1000;
/** The format of a VTT timecode. */
private static final String VTT_TIMECODE_FORMAT = "%02d:%02d:%02d.%03d";
/** The length in bytes of a WAVEFORMATEX structure. */
private static final int WAVE_FORMAT_SIZE = 18;
/** Format tag indicating a WAVEFORMATEXTENSIBLE structure. */
......@@ -1342,7 +1369,9 @@ public class MatroskaExtractor implements Extractor {
track.trueHdSampleRechunker.sampleMetadata(
track.output, timeUs, flags, size, offset, track.cryptoData);
} else {
if (CODEC_ID_SUBRIP.equals(track.codecId) || CODEC_ID_ASS.equals(track.codecId)) {
if (CODEC_ID_SUBRIP.equals(track.codecId)
|| CODEC_ID_ASS.equals(track.codecId)
|| CODEC_ID_VTT.equals(track.codecId)) {
if (blockSampleCount > 1) {
Log.w(TAG, "Skipping subtitle sample in laced block.");
} else if (blockDurationUs == C.TIME_UNSET) {
......@@ -1415,6 +1444,9 @@ public class MatroskaExtractor implements Extractor {
} else if (CODEC_ID_ASS.equals(track.codecId)) {
writeSubtitleSampleData(input, SSA_PREFIX, size);
return finishWriteSampleData();
} else if (CODEC_ID_VTT.equals(track.codecId)) {
writeSubtitleSampleData(input, VTT_PREFIX, size);
return finishWriteSampleData();
}
TrackOutput output = track.output;
......@@ -1641,7 +1673,8 @@ public class MatroskaExtractor implements Extractor {
* <p>See documentation on {@link #SSA_DIALOGUE_FORMAT} and {@link #SUBRIP_PREFIX} for why we use
* the duration as the end timecode.
*
* @param codecId The subtitle codec; must be {@link #CODEC_ID_SUBRIP} or {@link #CODEC_ID_ASS}.
* @param codecId The subtitle codec; must be {@link #CODEC_ID_SUBRIP}, {@link #CODEC_ID_ASS} or
* {@link #CODEC_ID_VTT}.
* @param durationUs The duration of the sample, in microseconds.
* @param subtitleData The subtitle sample in which to overwrite the end timecode (output
* parameter).
......@@ -1662,6 +1695,12 @@ public class MatroskaExtractor implements Extractor {
durationUs, SSA_TIMECODE_FORMAT, SSA_TIMECODE_LAST_VALUE_SCALING_FACTOR);
endTimecodeOffset = SSA_PREFIX_END_TIMECODE_OFFSET;
break;
case CODEC_ID_VTT:
endTimecode =
formatSubtitleTimecode(
durationUs, VTT_TIMECODE_FORMAT, VTT_TIMECODE_LAST_VALUE_SCALING_FACTOR);
endTimecodeOffset = VTT_PREFIX_END_TIMECODE_OFFSET;
break;
default:
throw new IllegalArgumentException();
}
......@@ -1830,6 +1869,7 @@ public class MatroskaExtractor implements Extractor {
case CODEC_ID_PCM_FLOAT:
case CODEC_ID_SUBRIP:
case CODEC_ID_ASS:
case CODEC_ID_VTT:
case CODEC_ID_VOBSUB:
case CODEC_ID_PGS:
case CODEC_ID_DVBSUB:
......@@ -2157,6 +2197,9 @@ public class MatroskaExtractor implements Extractor {
mimeType = MimeTypes.TEXT_SSA;
initializationData = ImmutableList.of(SSA_DIALOGUE_FORMAT, getCodecPrivate(codecId));
break;
case CODEC_ID_VTT:
mimeType = MimeTypes.TEXT_VTT;
break;
case CODEC_ID_VOBSUB:
mimeType = MimeTypes.APPLICATION_VOBSUB;
initializationData = ImmutableList.of(getCodecPrivate(codecId));
......@@ -2245,6 +2288,7 @@ public class MatroskaExtractor implements Extractor {
.setColorInfo(colorInfo);
} else if (MimeTypes.APPLICATION_SUBRIP.equals(mimeType)
|| MimeTypes.TEXT_SSA.equals(mimeType)
|| MimeTypes.TEXT_VTT.equals(mimeType)
|| MimeTypes.APPLICATION_VOBSUB.equals(mimeType)
|| MimeTypes.APPLICATION_PGS.equals(mimeType)
|| MimeTypes.APPLICATION_DVBSUBS.equals(mimeType)) {
......
......@@ -15,12 +15,18 @@
*/
package com.google.android.exoplayer2.extractor.ogg;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.audio.OpusUtil;
import com.google.android.exoplayer2.extractor.VorbisUtil;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.common.collect.ImmutableList;
import java.util.Arrays;
import java.util.List;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
......@@ -28,26 +34,13 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
/** {@link StreamReader} to extract Opus data out of Ogg byte stream. */
/* package */ final class OpusReader extends StreamReader {
private static final int OPUS_CODE = 0x4f707573;
private static final byte[] OPUS_SIGNATURE = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'};
private boolean headerRead;
private static final byte[] OPUS_ID_HEADER_SIGNATURE = {'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'};
private static final byte[] OPUS_COMMENT_HEADER_SIGNATURE = {
'O', 'p', 'u', 's', 'T', 'a', 'g', 's'
};
public static boolean verifyBitstreamType(ParsableByteArray data) {
if (data.bytesLeft() < OPUS_SIGNATURE.length) {
return false;
}
byte[] header = new byte[OPUS_SIGNATURE.length];
data.readBytes(header, 0, OPUS_SIGNATURE.length);
return Arrays.equals(header, OPUS_SIGNATURE);
}
@Override
protected void reset(boolean headerData) {
super.reset(headerData);
if (headerData) {
headerRead = false;
}
return peekPacketStartsWith(data, OPUS_ID_HEADER_SIGNATURE);
}
@Override
......@@ -57,11 +50,16 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
@Override
@EnsuresNonNullIf(expression = "#3.format", result = false)
protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData) {
if (!headerRead) {
protected boolean readHeaders(ParsableByteArray packet, long position, SetupData setupData)
throws ParserException {
if (peekPacketStartsWith(packet, OPUS_ID_HEADER_SIGNATURE)) {
byte[] headerBytes = Arrays.copyOf(packet.getData(), packet.limit());
int channelCount = OpusUtil.getChannelCount(headerBytes);
List<byte[]> initializationData = OpusUtil.buildInitializationData(headerBytes);
// The ID header must come at the start of the file:
// https://datatracker.ietf.org/doc/html/rfc7845#section-3
checkState(setupData.format == null);
setupData.format =
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_OPUS)
......@@ -69,13 +67,33 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
.setSampleRate(OpusUtil.SAMPLE_RATE)
.setInitializationData(initializationData)
.build();
headerRead = true;
return true;
} else if (peekPacketStartsWith(packet, OPUS_COMMENT_HEADER_SIGNATURE)) {
// The comment header must come immediately after the ID header, so the format will already
// be populated: https://datatracker.ietf.org/doc/html/rfc7845#section-3
checkStateNotNull(setupData.format);
packet.skipBytes(OPUS_COMMENT_HEADER_SIGNATURE.length);
VorbisUtil.CommentHeader commentHeader =
VorbisUtil.readVorbisCommentHeader(
packet, /* hasMetadataHeader= */ false, /* hasFramingBit= */ false);
@Nullable
Metadata vorbisMetadata =
VorbisUtil.parseVorbisComments(ImmutableList.copyOf(commentHeader.comments));
if (vorbisMetadata == null) {
return true;
}
setupData.format =
setupData
.format
.buildUpon()
.setMetadata(vorbisMetadata.copyWithAppendedEntriesFrom(setupData.format.metadata))
.build();
return true;
} else {
checkNotNull(setupData.format); // Has been set when the header was read.
boolean headerPacket = packet.readInt() == OPUS_CODE;
packet.setPosition(0);
return headerPacket;
// The ID header must come at the start of the file, so the format must already be populated:
// https://datatracker.ietf.org/doc/html/rfc7845#section-3
checkStateNotNull(setupData.format);
return false;
}
}
......@@ -114,4 +132,22 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
}
return (long) frames * length;
}
/**
* Returns true if the given {@link ParsableByteArray} starts with {@code expectedPrefix}. Does
* not change the {@link ParsableByteArray#getPosition() position} of {@code packet}.
*
* @param packet The packet data.
* @return True if the packet starts with {@code expectedPrefix}, false if not.
*/
private static boolean peekPacketStartsWith(ParsableByteArray packet, byte[] expectedPrefix) {
if (packet.bytesLeft() < expectedPrefix.length) {
return false;
}
int startPosition = packet.getPosition();
byte[] header = new byte[expectedPrefix.length];
packet.readBytes(header, 0, expectedPrefix.length);
packet.setPosition(startPosition);
return Arrays.equals(header, expectedPrefix);
}
}
......@@ -24,8 +24,10 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.extractor.VorbisUtil;
import com.google.android.exoplayer2.extractor.VorbisUtil.Mode;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
......@@ -111,6 +113,10 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
codecInitializationData.add(idHeader.data);
codecInitializationData.add(vorbisSetup.setupHeaderData);
@Nullable
Metadata metadata =
VorbisUtil.parseVorbisComments(ImmutableList.copyOf(vorbisSetup.commentHeader.comments));
setupData.format =
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_VORBIS)
......@@ -119,6 +125,7 @@ import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
.setChannelCount(idHeader.channels)
.setSampleRate(idHeader.sampleRate)
.setInitializationData(codecInitializationData)
.setMetadata(metadata)
.build();
return true;
}
......
......@@ -22,9 +22,11 @@ import android.os.Parcelable;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.MediaMetadata;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.ParsableByteArray;
import com.google.common.base.Charsets;
import java.util.Arrays;
/** A picture parsed from a FLAC file. */
/** A picture parsed from a Vorbis Comment or a FLAC picture block. */
public final class PictureFrame implements Metadata.Entry {
/** The type of the picture. */
......@@ -134,6 +136,35 @@ public final class PictureFrame implements Metadata.Entry {
return 0;
}
/**
* Parses a {@code METADATA_BLOCK_PICTURE} into a {@code PictureFrame} instance.
*
* <p>{@code pictureBlock} may be read directly from a <a
* href="https://xiph.org/flac/format.html#metadata_block_picture">FLAC file</a>, or decoded from
* the base64 content of a <a
* href="https://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE">Vorbis Comment</a>.
*
* @param pictureBlock The data of the {@code METADATA_BLOCK_PICTURE}, not including any headers.
* @return A {@code PictureFrame} parsed from {@code pictureBlock}.
*/
public static PictureFrame fromPictureBlock(ParsableByteArray pictureBlock) {
int pictureType = pictureBlock.readInt();
int mimeTypeLength = pictureBlock.readInt();
String mimeType = pictureBlock.readString(mimeTypeLength, Charsets.US_ASCII);
int descriptionLength = pictureBlock.readInt();
String description = pictureBlock.readString(descriptionLength);
int width = pictureBlock.readInt();
int height = pictureBlock.readInt();
int depth = pictureBlock.readInt();
int colors = pictureBlock.readInt();
int pictureDataLength = pictureBlock.readInt();
byte[] pictureData = new byte[pictureDataLength];
pictureBlock.readBytes(pictureData, 0, pictureDataLength);
return new PictureFrame(
pictureType, mimeType, description, width, height, depth, colors, pictureData);
}
public static final Parcelable.Creator<PictureFrame> CREATOR =
new Parcelable.Creator<PictureFrame>() {
......
......@@ -23,8 +23,9 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.MediaMetadata;
import com.google.android.exoplayer2.metadata.Metadata;
/** A vorbis comment. */
public final class VorbisComment implements Metadata.Entry {
/** @deprecated Use {@link com.google.android.exoplayer2.metadata.vorbis.VorbisComment} instead. */
@Deprecated
public class VorbisComment implements Metadata.Entry {
/** The key. */
public final String key;
......@@ -41,7 +42,7 @@ public final class VorbisComment implements Metadata.Entry {
this.value = value;
}
/* package */ VorbisComment(Parcel in) {
protected VorbisComment(Parcel in) {
this.key = castNonNull(in.readString());
this.value = castNonNull(in.readString());
}
......
/*
* Copyright (C) 2019 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.metadata.vorbis;
import android.os.Parcel;
/** A vorbis comment, extracted from a FLAC or Ogg file. */
@SuppressWarnings("deprecation") // Extending deprecated type for backwards compatibility.
public final class VorbisComment extends com.google.android.exoplayer2.metadata.flac.VorbisComment {
/**
* @param key The key.
* @param value The value.
*/
public VorbisComment(String key, String value) {
super(key, value);
}
/* package */ VorbisComment(Parcel in) {
super(in);
}
public static final Creator<VorbisComment> CREATOR =
new Creator<VorbisComment>() {
@Override
public VorbisComment createFromParcel(Parcel in) {
return new VorbisComment(in);
}
@Override
public VorbisComment[] newArray(int size) {
return new VorbisComment[size];
}
};
}
/*
* Copyright (C) 2019 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.
*/
@NonNullApi
package com.google.android.exoplayer2.metadata.vorbis;
import com.google.android.exoplayer2.util.NonNullApi;
......@@ -25,7 +25,7 @@ import com.google.android.exoplayer2.extractor.FlacMetadataReader.FlacStreamMeta
import com.google.android.exoplayer2.extractor.flac.FlacConstants;
import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.flac.PictureFrame;
import com.google.android.exoplayer2.metadata.flac.VorbisComment;
import com.google.android.exoplayer2.metadata.vorbis.VorbisComment;
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.util.ParsableByteArray;
......
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