Commit 7d3f54a3 by Oliver Woodman Committed by GitHub

Merge pull request #7162 from google/dev-v2-r2.11.4

r2.11.4
parents 49910fe7 76374d78
Showing with 1139 additions and 404 deletions
# Release notes # # Release notes #
### 2.11.4 (2020-04-08) ###
* Add `SimpleExoPlayer.setWakeMode` to allow automatic `WifiLock` and `WakeLock`
handling ([#6914](https://github.com/google/ExoPlayer/issues/6914)). To use
this feature, you must add the
[WAKE_LOCK](https://developer.android.com/reference/android/Manifest.permission.html#WAKE_LOCK)
permission to your application's manifest file.
* Text:
* Catch and log exceptions in `TextRenderer` rather than re-throwing. This
allows playback to continue even if subtitle decoding fails
([#6885](https://github.com/google/ExoPlayer/issues/6885)).
* Allow missing hours and milliseconds in SubRip (.srt) timecodes
([#7122](https://github.com/google/ExoPlayer/issues/7122)).
* Audio:
* Enable playback speed adjustment and silence skipping for floating point PCM
audio, via resampling to 16-bit integer PCM. To output the original floating
point audio without adjustment, pass `enableFloatOutput=true` to the
`DefaultAudioSink` constructor
([#7134](https://github.com/google/ExoPlayer/issues/7134)).
* Workaround issue that could cause slower than realtime playback of AAC on
Android 10 ([#6671](https://github.com/google/ExoPlayer/issues/6671).
* Fix case where another app spuriously holding transient audio focus could
prevent ExoPlayer from acquiring audio focus for an indefinite period of
time ([#7182](https://github.com/google/ExoPlayer/issues/7182).
* Fix case where the player volume could be permanently ducked if audio focus
was released whilst ducking.
* Fix playback of WAV files with trailing non-media bytes
([#7129](https://github.com/google/ExoPlayer/issues/7129)).
* Fix playback of ADTS files with mid-stream ID3 metadata.
* DRM:
* Fix stuck ad playbacks with DRM protected content
([#7188](https://github.com/google/ExoPlayer/issues/7188)).
* Fix playback of Widevine protected content that only provides V1 PSSH atoms
on API levels 21 and 22.
* Fix playback of PlayReady content on Fire TV Stick (Gen 2).
* DASH:
* Update the manifest URI to avoid repeated HTTP redirects
([#6907](https://github.com/google/ExoPlayer/issues/6907)).
* Parse period `AssetIdentifier` elements.
* HLS: Recognize IMSC subtitles
([#7185](https://github.com/google/ExoPlayer/issues/7185)).
* UI: Add an option to set whether to use the orientation sensor for rotation
in spherical playbacks
([#6761](https://github.com/google/ExoPlayer/issues/6761)).
* Analytics: Fix `PlaybackStatsListener` behavior when not keeping history
([#7160](https://github.com/google/ExoPlayer/issues/7160)).
* FFmpeg extension: Add support for `x86_64` architecture.
* Opus extension: Fix parsing of negative gain values
([#7046](https://github.com/google/ExoPlayer/issues/7046)).
* Cast extension: Upgrade `play-services-cast-framework` dependency to 18.1.0.
This fixes an issue where `RemoteServiceException` was thrown due to
`Context.startForegroundService()` not calling `Service.startForeground()`
([#7191](https://github.com/google/ExoPlayer/issues/7191)).
### 2.11.3 (2020-02-19) ### ### 2.11.3 (2020-02-19) ###
* SmoothStreaming: Fix regression that broke playback in 2.11.2 * SmoothStreaming: Fix regression that broke playback in 2.11.2
......
...@@ -13,8 +13,8 @@ ...@@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.11.3' releaseVersion = '2.11.4'
releaseVersionCode = 2011003 releaseVersionCode = 2011004
minSdkVersion = 16 minSdkVersion = 16
appTargetSdkVersion = 29 appTargetSdkVersion = 29
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved
......
...@@ -57,8 +57,8 @@ dependencies { ...@@ -57,8 +57,8 @@ dependencies {
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
implementation project(modulePrefix + 'extension-cast') implementation project(modulePrefix + 'extension-cast')
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.recyclerview:recyclerview:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.google.android.material:material:1.0.0' implementation 'com.google.android.material:material:1.1.0'
} }
apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-sdk/> <uses-sdk/>
......
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <action android:name="android.intent.action.MAIN"/>
......
...@@ -64,7 +64,7 @@ android { ...@@ -64,7 +64,7 @@ android {
dependencies { dependencies {
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'com.google.android.material:material:1.0.0' implementation 'com.google.android.material:material:1.1.0'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-dash') implementation project(modulePrefix + 'library-dash')
implementation project(modulePrefix + 'library-hls') implementation project(modulePrefix + 'library-hls')
......
...@@ -39,6 +39,7 @@ import com.google.android.exoplayer2.PlaybackPreparer; ...@@ -39,6 +39,7 @@ import com.google.android.exoplayer2.PlaybackPreparer;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.demo.Sample.UriSample; import com.google.android.exoplayer2.demo.Sample.UriSample;
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager;
import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager;
...@@ -380,6 +381,7 @@ public class PlayerActivity extends AppCompatActivity ...@@ -380,6 +381,7 @@ public class PlayerActivity extends AppCompatActivity
.setTrackSelector(trackSelector) .setTrackSelector(trackSelector)
.build(); .build();
player.addListener(new PlayerEventListener()); player.addListener(new PlayerEventListener());
player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true);
player.setPlayWhenReady(startAutoPlay); player.setPlayWhenReady(startAutoPlay);
player.addAnalyticsListener(new EventLogger(trackSelector)); player.addAnalyticsListener(new EventLogger(trackSelector));
playerView.setPlayer(player); playerView.setPlayer(player);
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.demo; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2.demo;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.AssetManager; import android.content.res.AssetManager;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
...@@ -34,6 +35,7 @@ import android.widget.ExpandableListView.OnChildClickListener; ...@@ -34,6 +35,7 @@ import android.widget.ExpandableListView.OnChildClickListener;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.ParserException;
...@@ -63,6 +65,7 @@ public class SampleChooserActivity extends AppCompatActivity ...@@ -63,6 +65,7 @@ public class SampleChooserActivity extends AppCompatActivity
private static final String TAG = "SampleChooserActivity"; private static final String TAG = "SampleChooserActivity";
private String[] uris;
private boolean useExtensionRenderers; private boolean useExtensionRenderers;
private DownloadTracker downloadTracker; private DownloadTracker downloadTracker;
private SampleAdapter sampleAdapter; private SampleAdapter sampleAdapter;
...@@ -81,7 +84,6 @@ public class SampleChooserActivity extends AppCompatActivity ...@@ -81,7 +84,6 @@ public class SampleChooserActivity extends AppCompatActivity
Intent intent = getIntent(); Intent intent = getIntent();
String dataUri = intent.getDataString(); String dataUri = intent.getDataString();
String[] uris;
if (dataUri != null) { if (dataUri != null) {
uris = new String[] {dataUri}; uris = new String[] {dataUri};
} else { } else {
...@@ -105,8 +107,7 @@ public class SampleChooserActivity extends AppCompatActivity ...@@ -105,8 +107,7 @@ public class SampleChooserActivity extends AppCompatActivity
DemoApplication application = (DemoApplication) getApplication(); DemoApplication application = (DemoApplication) getApplication();
useExtensionRenderers = application.useExtensionRenderers(); useExtensionRenderers = application.useExtensionRenderers();
downloadTracker = application.getDownloadTracker(); downloadTracker = application.getDownloadTracker();
SampleListLoader loaderTask = new SampleListLoader(); loadSample();
loaderTask.execute(uris);
// Start the download service if it should be running but it's not currently. // Start the download service if it should be running but it's not currently.
// Starting the service in the foreground causes notification flicker if there is no scheduled // Starting the service in the foreground causes notification flicker if there is no scheduled
...@@ -157,6 +158,37 @@ public class SampleChooserActivity extends AppCompatActivity ...@@ -157,6 +158,37 @@ public class SampleChooserActivity extends AppCompatActivity
sampleAdapter.notifyDataSetChanged(); sampleAdapter.notifyDataSetChanged();
} }
@Override
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (grantResults.length == 0) {
// Empty results are triggered if a permission is requested while another request was already
// pending and can be safely ignored in this case.
return;
}
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
loadSample();
} else {
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
.show();
finish();
}
}
private void loadSample() {
Assertions.checkNotNull(uris);
for (int i = 0; i < uris.length; i++) {
Uri uri = Uri.parse(uris[i]);
if (Util.maybeRequestReadExternalStoragePermission(this, uri)) {
return;
}
}
SampleListLoader loaderTask = new SampleListLoader();
loaderTask.execute(uris);
}
private void onSampleGroups(final List<SampleGroup> groups, boolean sawError) { private void onSampleGroups(final List<SampleGroup> groups, boolean sawError) {
if (sawError) { if (sawError) {
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
......
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
#endif // CPU_FEATURES_COMPILED_ANY_ARM_NEON #endif // CPU_FEATURES_COMPILED_ANY_ARM_NEON
#include <jni.h> #include <jni.h>
#include <cstdint>
#include <cstring> #include <cstring>
#include <mutex> // NOLINT #include <mutex> // NOLINT
#include <new> #include <new>
...@@ -121,18 +122,22 @@ const char* GetJniErrorMessage(JniStatusCode error_code) { ...@@ -121,18 +122,22 @@ const char* GetJniErrorMessage(JniStatusCode error_code) {
} }
} }
// Manages Libgav1FrameBuffer and reference information. // Manages frame buffer and reference information.
class JniFrameBuffer { class JniFrameBuffer {
public: public:
explicit JniFrameBuffer(int id) : id_(id), reference_count_(0) { explicit JniFrameBuffer(int id) : id_(id), reference_count_(0) {}
gav1_frame_buffer_.private_data = &id_;
}
~JniFrameBuffer() { ~JniFrameBuffer() {
for (int plane_index = kPlaneY; plane_index < kMaxPlanes; plane_index++) { for (int plane_index = kPlaneY; plane_index < kMaxPlanes; plane_index++) {
delete[] gav1_frame_buffer_.data[plane_index]; delete[] raw_buffer_[plane_index];
} }
} }
// Not copyable or movable.
JniFrameBuffer(const JniFrameBuffer&) = delete;
JniFrameBuffer(JniFrameBuffer&&) = delete;
JniFrameBuffer& operator=(const JniFrameBuffer&) = delete;
JniFrameBuffer& operator=(JniFrameBuffer&&) = delete;
void SetFrameData(const libgav1::DecoderBuffer& decoder_buffer) { void SetFrameData(const libgav1::DecoderBuffer& decoder_buffer) {
for (int plane_index = kPlaneY; plane_index < decoder_buffer.NumPlanes(); for (int plane_index = kPlaneY; plane_index < decoder_buffer.NumPlanes();
plane_index++) { plane_index++) {
...@@ -160,9 +165,8 @@ class JniFrameBuffer { ...@@ -160,9 +165,8 @@ class JniFrameBuffer {
void RemoveReference() { reference_count_--; } void RemoveReference() { reference_count_--; }
bool InUse() const { return reference_count_ != 0; } bool InUse() const { return reference_count_ != 0; }
const Libgav1FrameBuffer& GetGav1FrameBuffer() const { uint8_t* RawBuffer(int plane_index) const { return raw_buffer_[plane_index]; }
return gav1_frame_buffer_; void* BufferPrivateData() const { return const_cast<int*>(&id_); }
}
// Attempts to reallocate data planes if the existing ones don't have enough // Attempts to reallocate data planes if the existing ones don't have enough
// capacity. Returns true if the allocation was successful or wasn't needed, // capacity. Returns true if the allocation was successful or wasn't needed,
...@@ -172,15 +176,14 @@ class JniFrameBuffer { ...@@ -172,15 +176,14 @@ class JniFrameBuffer {
for (int plane_index = kPlaneY; plane_index < kMaxPlanes; plane_index++) { for (int plane_index = kPlaneY; plane_index < kMaxPlanes; plane_index++) {
const int min_size = const int min_size =
(plane_index == kPlaneY) ? y_plane_min_size : uv_plane_min_size; (plane_index == kPlaneY) ? y_plane_min_size : uv_plane_min_size;
if (gav1_frame_buffer_.size[plane_index] >= min_size) continue; if (raw_buffer_size_[plane_index] >= min_size) continue;
delete[] gav1_frame_buffer_.data[plane_index]; delete[] raw_buffer_[plane_index];
gav1_frame_buffer_.data[plane_index] = raw_buffer_[plane_index] = new (std::nothrow) uint8_t[min_size];
new (std::nothrow) uint8_t[min_size]; if (!raw_buffer_[plane_index]) {
if (!gav1_frame_buffer_.data[plane_index]) { raw_buffer_size_[plane_index] = 0;
gav1_frame_buffer_.size[plane_index] = 0;
return false; return false;
} }
gav1_frame_buffer_.size[plane_index] = min_size; raw_buffer_size_[plane_index] = min_size;
} }
return true; return true;
} }
...@@ -190,9 +193,12 @@ class JniFrameBuffer { ...@@ -190,9 +193,12 @@ class JniFrameBuffer {
uint8_t* plane_[kMaxPlanes]; uint8_t* plane_[kMaxPlanes];
int displayed_width_[kMaxPlanes]; int displayed_width_[kMaxPlanes];
int displayed_height_[kMaxPlanes]; int displayed_height_[kMaxPlanes];
int id_; const int id_;
int reference_count_; int reference_count_;
Libgav1FrameBuffer gav1_frame_buffer_ = {}; // Pointers to the raw buffers allocated for the data planes.
uint8_t* raw_buffer_[kMaxPlanes] = {};
// Sizes of the raw buffers in bytes.
size_t raw_buffer_size_[kMaxPlanes] = {};
}; };
// Manages frame buffers used by libgav1 decoder and ExoPlayer. // Manages frame buffers used by libgav1 decoder and ExoPlayer.
...@@ -210,7 +216,7 @@ class JniBufferManager { ...@@ -210,7 +216,7 @@ class JniBufferManager {
} }
JniStatusCode GetBuffer(size_t y_plane_min_size, size_t uv_plane_min_size, JniStatusCode GetBuffer(size_t y_plane_min_size, size_t uv_plane_min_size,
Libgav1FrameBuffer* frame_buffer) { JniFrameBuffer** jni_buffer) {
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
JniFrameBuffer* output_buffer; JniFrameBuffer* output_buffer;
...@@ -230,7 +236,7 @@ class JniBufferManager { ...@@ -230,7 +236,7 @@ class JniBufferManager {
} }
output_buffer->AddReference(); output_buffer->AddReference();
*frame_buffer = output_buffer->GetGav1FrameBuffer(); *jni_buffer = output_buffer;
return kJniStatusOk; return kJniStatusOk;
} }
...@@ -316,29 +322,46 @@ struct JniContext { ...@@ -316,29 +322,46 @@ struct JniContext {
JniStatusCode jni_status_code = kJniStatusOk; JniStatusCode jni_status_code = kJniStatusOk;
}; };
int Libgav1GetFrameBuffer(void* private_data, size_t y_plane_min_size, Libgav1StatusCode Libgav1GetFrameBuffer(void* callback_private_data,
size_t uv_plane_min_size, int bitdepth,
Libgav1FrameBuffer* frame_buffer) { libgav1::ImageFormat image_format,
JniContext* const context = reinterpret_cast<JniContext*>(private_data); int width, int height, int left_border,
int right_border, int top_border,
int bottom_border, int stride_alignment,
libgav1::FrameBuffer* frame_buffer) {
libgav1::FrameBufferInfo info;
Libgav1StatusCode status = libgav1::ComputeFrameBufferInfo(
bitdepth, image_format, width, height, left_border, right_border,
top_border, bottom_border, stride_alignment, &info);
if (status != kLibgav1StatusOk) return status;
JniContext* const context = static_cast<JniContext*>(callback_private_data);
JniFrameBuffer* jni_buffer;
context->jni_status_code = context->buffer_manager.GetBuffer( context->jni_status_code = context->buffer_manager.GetBuffer(
y_plane_min_size, uv_plane_min_size, frame_buffer); info.y_buffer_size, info.uv_buffer_size, &jni_buffer);
if (context->jni_status_code != kJniStatusOk) { if (context->jni_status_code != kJniStatusOk) {
LOGE("%s", GetJniErrorMessage(context->jni_status_code)); LOGE("%s", GetJniErrorMessage(context->jni_status_code));
return -1; return kLibgav1StatusOutOfMemory;
} }
return 0;
uint8_t* const y_buffer = jni_buffer->RawBuffer(0);
uint8_t* const u_buffer =
(info.uv_buffer_size != 0) ? jni_buffer->RawBuffer(1) : nullptr;
uint8_t* const v_buffer =
(info.uv_buffer_size != 0) ? jni_buffer->RawBuffer(2) : nullptr;
return libgav1::SetFrameBuffer(&info, y_buffer, u_buffer, v_buffer,
jni_buffer->BufferPrivateData(), frame_buffer);
} }
int Libgav1ReleaseFrameBuffer(void* private_data, void Libgav1ReleaseFrameBuffer(void* callback_private_data,
Libgav1FrameBuffer* frame_buffer) { void* buffer_private_data) {
JniContext* const context = reinterpret_cast<JniContext*>(private_data); JniContext* const context = static_cast<JniContext*>(callback_private_data);
const int buffer_id = *reinterpret_cast<int*>(frame_buffer->private_data); const int buffer_id = *static_cast<const int*>(buffer_private_data);
context->jni_status_code = context->buffer_manager.ReleaseBuffer(buffer_id); context->jni_status_code = context->buffer_manager.ReleaseBuffer(buffer_id);
if (context->jni_status_code != kJniStatusOk) { if (context->jni_status_code != kJniStatusOk) {
LOGE("%s", GetJniErrorMessage(context->jni_status_code)); LOGE("%s", GetJniErrorMessage(context->jni_status_code));
return -1;
} }
return 0;
} }
constexpr int AlignTo16(int value) { return (value + 15) & (~15); } constexpr int AlignTo16(int value) { return (value + 15) & (~15); }
...@@ -508,8 +531,8 @@ DECODER_FUNC(jlong, gav1Init, jint threads) { ...@@ -508,8 +531,8 @@ DECODER_FUNC(jlong, gav1Init, jint threads) {
libgav1::DecoderSettings settings; libgav1::DecoderSettings settings;
settings.threads = threads; settings.threads = threads;
settings.get = Libgav1GetFrameBuffer; settings.get_frame_buffer = Libgav1GetFrameBuffer;
settings.release = Libgav1ReleaseFrameBuffer; settings.release_frame_buffer = Libgav1ReleaseFrameBuffer;
settings.callback_private_data = context; settings.callback_private_data = context;
context->libgav1_status_code = context->decoder.Init(&settings); context->libgav1_status_code = context->decoder.Init(&settings);
...@@ -544,7 +567,8 @@ DECODER_FUNC(jint, gav1Decode, jlong jContext, jobject encodedData, ...@@ -544,7 +567,8 @@ DECODER_FUNC(jint, gav1Decode, jlong jContext, jobject encodedData,
const uint8_t* const buffer = reinterpret_cast<const uint8_t*>( const uint8_t* const buffer = reinterpret_cast<const uint8_t*>(
env->GetDirectBufferAddress(encodedData)); env->GetDirectBufferAddress(encodedData));
context->libgav1_status_code = context->libgav1_status_code =
context->decoder.EnqueueFrame(buffer, length, /*user_private_data=*/0); context->decoder.EnqueueFrame(buffer, length, /*user_private_data=*/0,
/*buffer_private_data=*/nullptr);
if (context->libgav1_status_code != kLibgav1StatusOk) { if (context->libgav1_status_code != kLibgav1StatusOk) {
return kStatusError; return kStatusError;
} }
...@@ -619,7 +643,7 @@ DECODER_FUNC(jint, gav1GetFrame, jlong jContext, jobject jOutputBuffer, ...@@ -619,7 +643,7 @@ DECODER_FUNC(jint, gav1GetFrame, jlong jContext, jobject jOutputBuffer,
} }
const int buffer_id = const int buffer_id =
*reinterpret_cast<int*>(decoder_buffer->buffer_private_data); *static_cast<const int*>(decoder_buffer->buffer_private_data);
context->buffer_manager.AddBufferReference(buffer_id); context->buffer_manager.AddBufferReference(buffer_id);
JniFrameBuffer* const jni_buffer = JniFrameBuffer* const jni_buffer =
context->buffer_manager.GetBuffer(buffer_id); context->buffer_manager.GetBuffer(buffer_id);
......
...@@ -31,7 +31,7 @@ android { ...@@ -31,7 +31,7 @@ android {
} }
dependencies { dependencies {
api 'com.google.android.gms:play-services-cast-framework:17.0.0' api 'com.google.android.gms:play-services-cast-framework:18.1.0'
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'library-ui')
......
...@@ -35,22 +35,22 @@ FFMPEG_EXT_PATH="$(pwd)/extensions/ffmpeg/src/main/jni" ...@@ -35,22 +35,22 @@ FFMPEG_EXT_PATH="$(pwd)/extensions/ffmpeg/src/main/jni"
NDK_PATH="<path to Android NDK>" NDK_PATH="<path to Android NDK>"
``` ```
* Set up host platform ("darwin-x86_64" for Mac OS X): * Set the host platform (use "darwin-x86_64" for Mac OS X):
``` ```
HOST_PLATFORM="linux-x86_64" HOST_PLATFORM="linux-x86_64"
``` ```
* Configure the formats supported by adapting the following variable if needed * Configure the decoders to include. See the [Supported formats][] page for
and by setting it. See the [Supported formats][] page for more details of the details of the available decoders, and which formats they support.
formats.
``` ```
ENABLED_DECODERS=(vorbis opus flac) ENABLED_DECODERS=(vorbis opus flac)
``` ```
* Fetch and build FFmpeg. For example, executing script `build_ffmpeg.sh` will * Fetch and build FFmpeg. Executing `build_ffmpeg.sh` will fetch and build
fetch and build FFmpeg release 4.2 for armeabi-v7a, arm64-v8a and x86: FFmpeg 4.2 for `armeabi-v7a`, `arm64-v8a`, `x86` and `x86_64`. The script can
be edited if you need to build for different architectures.
``` ```
cd "${FFMPEG_EXT_PATH}" && \ cd "${FFMPEG_EXT_PATH}" && \
...@@ -63,7 +63,7 @@ cd "${FFMPEG_EXT_PATH}" && \ ...@@ -63,7 +63,7 @@ cd "${FFMPEG_EXT_PATH}" && \
``` ```
cd "${FFMPEG_EXT_PATH}" && \ cd "${FFMPEG_EXT_PATH}" && \
${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86" -j4 ${NDK_PATH}/ndk-build APP_ABI="armeabi-v7a arm64-v8a x86 x86_64" -j4
``` ```
## Build instructions (Windows) ## ## Build instructions (Windows) ##
......
...@@ -33,7 +33,7 @@ public final class FfmpegLibrary { ...@@ -33,7 +33,7 @@ public final class FfmpegLibrary {
private static final String TAG = "FfmpegLibrary"; private static final String TAG = "FfmpegLibrary";
private static final LibraryLoader LOADER = private static final LibraryLoader LOADER =
new LibraryLoader("avutil", "avresample", "swresample", "avcodec", "ffmpeg"); new LibraryLoader("avutil", "swresample", "avcodec", "ffmpeg");
private FfmpegLibrary() {} private FfmpegLibrary() {}
......
...@@ -22,11 +22,6 @@ LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so ...@@ -22,11 +22,6 @@ LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
include $(PREBUILT_SHARED_LIBRARY) include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS) include $(CLEAR_VARS)
LOCAL_MODULE := libavresample
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := libswresample LOCAL_MODULE := libswresample
LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so
include $(PREBUILT_SHARED_LIBRARY) include $(PREBUILT_SHARED_LIBRARY)
...@@ -40,6 +35,6 @@ include $(CLEAR_VARS) ...@@ -40,6 +35,6 @@ include $(CLEAR_VARS)
LOCAL_MODULE := ffmpeg LOCAL_MODULE := ffmpeg
LOCAL_SRC_FILES := ffmpeg_jni.cc LOCAL_SRC_FILES := ffmpeg_jni.cc
LOCAL_C_INCLUDES := ffmpeg LOCAL_C_INCLUDES := ffmpeg
LOCAL_SHARED_LIBRARIES := libavcodec libavresample libswresample libavutil LOCAL_SHARED_LIBRARIES := libavcodec libswresample libavutil
LOCAL_LDLIBS := -Lffmpeg/android-libs/$(TARGET_ARCH_ABI) -llog LOCAL_LDLIBS := -Lffmpeg/android-libs/$(TARGET_ARCH_ABI) -llog
include $(BUILD_SHARED_LIBRARY) include $(BUILD_SHARED_LIBRARY)
...@@ -32,8 +32,9 @@ COMMON_OPTIONS=" ...@@ -32,8 +32,9 @@ COMMON_OPTIONS="
--disable-postproc --disable-postproc
--disable-avfilter --disable-avfilter
--disable-symver --disable-symver
--enable-avresample --disable-avresample
--enable-swresample --enable-swresample
--extra-ldexeflags=-pie
" "
TOOLCHAIN_PREFIX="${NDK_PATH}/toolchains/llvm/prebuilt/${HOST_PLATFORM}/bin" TOOLCHAIN_PREFIX="${NDK_PATH}/toolchains/llvm/prebuilt/${HOST_PLATFORM}/bin"
for decoder in "${ENABLED_DECODERS[@]}" for decoder in "${ENABLED_DECODERS[@]}"
...@@ -53,7 +54,6 @@ git checkout release/4.2 ...@@ -53,7 +54,6 @@ git checkout release/4.2
--strip="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-strip" \ --strip="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-strip" \
--extra-cflags="-march=armv7-a -mfloat-abi=softfp" \ --extra-cflags="-march=armv7-a -mfloat-abi=softfp" \
--extra-ldflags="-Wl,--fix-cortex-a8" \ --extra-ldflags="-Wl,--fix-cortex-a8" \
--extra-ldexeflags=-pie \
${COMMON_OPTIONS} ${COMMON_OPTIONS}
make -j4 make -j4
make install-libs make install-libs
...@@ -65,7 +65,6 @@ make clean ...@@ -65,7 +65,6 @@ make clean
--cross-prefix="${TOOLCHAIN_PREFIX}/aarch64-linux-android21-" \ --cross-prefix="${TOOLCHAIN_PREFIX}/aarch64-linux-android21-" \
--nm="${TOOLCHAIN_PREFIX}/aarch64-linux-android-nm" \ --nm="${TOOLCHAIN_PREFIX}/aarch64-linux-android-nm" \
--strip="${TOOLCHAIN_PREFIX}/aarch64-linux-android-strip" \ --strip="${TOOLCHAIN_PREFIX}/aarch64-linux-android-strip" \
--extra-ldexeflags=-pie \
${COMMON_OPTIONS} ${COMMON_OPTIONS}
make -j4 make -j4
make install-libs make install-libs
...@@ -77,7 +76,18 @@ make clean ...@@ -77,7 +76,18 @@ make clean
--cross-prefix="${TOOLCHAIN_PREFIX}/i686-linux-android16-" \ --cross-prefix="${TOOLCHAIN_PREFIX}/i686-linux-android16-" \
--nm="${TOOLCHAIN_PREFIX}/i686-linux-android-nm" \ --nm="${TOOLCHAIN_PREFIX}/i686-linux-android-nm" \
--strip="${TOOLCHAIN_PREFIX}/i686-linux-android-strip" \ --strip="${TOOLCHAIN_PREFIX}/i686-linux-android-strip" \
--extra-ldexeflags=-pie \ --disable-asm \
${COMMON_OPTIONS}
make -j4
make install-libs
make clean
./configure \
--libdir=android-libs/x86_64 \
--arch=x86_64 \
--cpu=x86_64 \
--cross-prefix="${TOOLCHAIN_PREFIX}/x86_64-linux-android21-" \
--nm="${TOOLCHAIN_PREFIX}/x86_64-linux-android-nm" \
--strip="${TOOLCHAIN_PREFIX}/x86_64-linux-android-strip" \
--disable-asm \ --disable-asm \
${COMMON_OPTIONS} ${COMMON_OPTIONS}
make -j4 make -j4
......
...@@ -26,10 +26,10 @@ extern "C" { ...@@ -26,10 +26,10 @@ extern "C" {
#include <stdint.h> #include <stdint.h>
#endif #endif
#include <libavcodec/avcodec.h> #include <libavcodec/avcodec.h>
#include <libavresample/avresample.h>
#include <libavutil/channel_layout.h> #include <libavutil/channel_layout.h>
#include <libavutil/error.h> #include <libavutil/error.h>
#include <libavutil/opt.h> #include <libavutil/opt.h>
#include <libswresample/swresample.h>
} }
#define LOG_TAG "ffmpeg_jni" #define LOG_TAG "ffmpeg_jni"
...@@ -289,11 +289,11 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, ...@@ -289,11 +289,11 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
int sampleCount = frame->nb_samples; int sampleCount = frame->nb_samples;
int dataSize = av_samples_get_buffer_size(NULL, channelCount, sampleCount, int dataSize = av_samples_get_buffer_size(NULL, channelCount, sampleCount,
sampleFormat, 1); sampleFormat, 1);
AVAudioResampleContext *resampleContext; SwrContext *resampleContext;
if (context->opaque) { if (context->opaque) {
resampleContext = (AVAudioResampleContext *) context->opaque; resampleContext = (SwrContext *)context->opaque;
} else { } else {
resampleContext = avresample_alloc_context(); resampleContext = swr_alloc();
av_opt_set_int(resampleContext, "in_channel_layout", channelLayout, 0); av_opt_set_int(resampleContext, "in_channel_layout", channelLayout, 0);
av_opt_set_int(resampleContext, "out_channel_layout", channelLayout, 0); av_opt_set_int(resampleContext, "out_channel_layout", channelLayout, 0);
av_opt_set_int(resampleContext, "in_sample_rate", sampleRate, 0); av_opt_set_int(resampleContext, "in_sample_rate", sampleRate, 0);
...@@ -302,9 +302,9 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, ...@@ -302,9 +302,9 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
// The output format is always the requested format. // The output format is always the requested format.
av_opt_set_int(resampleContext, "out_sample_fmt", av_opt_set_int(resampleContext, "out_sample_fmt",
context->request_sample_fmt, 0); context->request_sample_fmt, 0);
result = avresample_open(resampleContext); result = swr_init(resampleContext);
if (result < 0) { if (result < 0) {
logError("avresample_open", result); logError("swr_init", result);
av_frame_free(&frame); av_frame_free(&frame);
return -1; return -1;
} }
...@@ -312,7 +312,7 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, ...@@ -312,7 +312,7 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
} }
int inSampleSize = av_get_bytes_per_sample(sampleFormat); int inSampleSize = av_get_bytes_per_sample(sampleFormat);
int outSampleSize = av_get_bytes_per_sample(context->request_sample_fmt); int outSampleSize = av_get_bytes_per_sample(context->request_sample_fmt);
int outSamples = avresample_get_out_samples(resampleContext, sampleCount); int outSamples = swr_get_out_samples(resampleContext, sampleCount);
int bufferOutSize = outSampleSize * channelCount * outSamples; int bufferOutSize = outSampleSize * channelCount * outSamples;
if (outSize + bufferOutSize > outputSize) { if (outSize + bufferOutSize > outputSize) {
LOGE("Output buffer size (%d) too small for output data (%d).", LOGE("Output buffer size (%d) too small for output data (%d).",
...@@ -320,15 +320,14 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, ...@@ -320,15 +320,14 @@ int decodePacket(AVCodecContext *context, AVPacket *packet,
av_frame_free(&frame); av_frame_free(&frame);
return -1; return -1;
} }
result = avresample_convert(resampleContext, &outputBuffer, bufferOutSize, result = swr_convert(resampleContext, &outputBuffer, bufferOutSize,
outSamples, frame->data, frame->linesize[0], (const uint8_t **)frame->data, frame->nb_samples);
sampleCount);
av_frame_free(&frame); av_frame_free(&frame);
if (result < 0) { if (result < 0) {
logError("avresample_convert", result); logError("swr_convert", result);
return result; return result;
} }
int available = avresample_available(resampleContext); int available = swr_get_out_samples(resampleContext, 0);
if (available != 0) { if (available != 0) {
LOGE("Expected no samples remaining after resampling, but found %d.", LOGE("Expected no samples remaining after resampling, but found %d.",
available); available);
...@@ -351,9 +350,9 @@ void releaseContext(AVCodecContext *context) { ...@@ -351,9 +350,9 @@ void releaseContext(AVCodecContext *context) {
if (!context) { if (!context) {
return; return;
} }
AVAudioResampleContext *resampleContext; SwrContext *swrContext;
if ((resampleContext = (AVAudioResampleContext *) context->opaque)) { if ((swrContext = (SwrContext *)context->opaque)) {
avresample_free(&resampleContext); swr_free(&swrContext);
context->opaque = NULL; context->opaque = NULL;
} }
avcodec_free_context(&context); avcodec_free_context(&context);
......
...@@ -223,7 +223,7 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { ...@@ -223,7 +223,7 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
responseByteStream = responseBody.byteStream(); responseByteStream = responseBody.byteStream();
} catch (IOException e) { } catch (IOException e) {
throw new HttpDataSourceException( throw new HttpDataSourceException(
"Unable to connect to " + dataSpec.uri, e, dataSpec, HttpDataSourceException.TYPE_OPEN); "Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN);
} }
int responseCode = response.code(); int responseCode = response.code();
......
...@@ -37,7 +37,9 @@ import org.junit.runner.RunWith; ...@@ -37,7 +37,9 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class OpusPlaybackTest { public class OpusPlaybackTest {
private static final String BEAR_OPUS_URI = "asset:///bear-opus.webm"; private static final String BEAR_OPUS_URI = "asset:///bear-opus.mka";
private static final String BEAR_OPUS_NEGATIVE_GAIN_URI =
"asset:///bear-opus-negative-gain.mka";
@Before @Before
public void setUp() { public void setUp() {
...@@ -51,6 +53,11 @@ public class OpusPlaybackTest { ...@@ -51,6 +53,11 @@ public class OpusPlaybackTest {
playUri(BEAR_OPUS_URI); playUri(BEAR_OPUS_URI);
} }
@Test
public void basicPlaybackNegativeGain() throws Exception {
playUri(BEAR_OPUS_NEGATIVE_GAIN_URI);
}
private void playUri(String uri) throws Exception { private void playUri(String uri) throws Exception {
TestPlaybackRunnable testPlaybackRunnable = TestPlaybackRunnable testPlaybackRunnable =
new TestPlaybackRunnable(Uri.parse(uri), ApplicationProvider.getApplicationContext()); new TestPlaybackRunnable(Uri.parse(uri), ApplicationProvider.getApplicationContext());
......
...@@ -90,8 +90,8 @@ import java.util.List; ...@@ -90,8 +90,8 @@ import java.util.List;
if (channelCount > 8) { if (channelCount > 8) {
throw new OpusDecoderException("Invalid channel count: " + channelCount); throw new OpusDecoderException("Invalid channel count: " + channelCount);
} }
int preskip = readLittleEndian16(headerBytes, 10); int preskip = readUnsignedLittleEndian16(headerBytes, 10);
int gain = readLittleEndian16(headerBytes, 16); int gain = readSignedLittleEndian16(headerBytes, 16);
byte[] streamMap = new byte[8]; byte[] streamMap = new byte[8];
int numStreams; int numStreams;
...@@ -228,12 +228,16 @@ import java.util.List; ...@@ -228,12 +228,16 @@ import java.util.List;
return (int) (ns * SAMPLE_RATE / 1000000000); return (int) (ns * SAMPLE_RATE / 1000000000);
} }
private static int readLittleEndian16(byte[] input, int offset) { private static int readUnsignedLittleEndian16(byte[] input, int offset) {
int value = input[offset] & 0xFF; int value = input[offset] & 0xFF;
value |= (input[offset + 1] & 0xFF) << 8; value |= (input[offset + 1] & 0xFF) << 8;
return value; return value;
} }
private static int readSignedLittleEndian16(byte[] input, int offset) {
return (short) readUnsignedLittleEndian16(input, offset);
}
private native long opusInit(int sampleRate, int channelCount, int numStreams, int numCoupled, private native long opusInit(int sampleRate, int channelCount, int numStreams, int numCoupled,
int gain, byte[] streamMap); int gain, byte[] streamMap);
private native int opusDecode(long decoder, long timeUs, ByteBuffer inputBuffer, int inputSize, private native int opusDecode(long decoder, long timeUs, ByteBuffer inputBuffer, int inputSize,
......
...@@ -34,7 +34,7 @@ android { ...@@ -34,7 +34,7 @@ android {
dependencies { dependencies {
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.work:work-runtime:2.2.0' implementation 'androidx.work:work-runtime:2.3.4'
} }
ext { ext {
......
...@@ -29,8 +29,7 @@ class CombinedJavadocPlugin implements Plugin<Project> { ...@@ -29,8 +29,7 @@ class CombinedJavadocPlugin implements Plugin<Project> {
classpath = project.files([]) classpath = project.files([])
destinationDir = project.file("$project.buildDir/docs/javadoc") destinationDir = project.file("$project.buildDir/docs/javadoc")
options { options {
links "https://docs.oracle.com/javase/7/docs/api/", links "https://developer.android.com/reference"
"https://developer.android.com/reference"
encoding = "UTF-8" encoding = "UTF-8"
} }
exclude "**/BuildConfig.java" exclude "**/BuildConfig.java"
......
...@@ -26,9 +26,7 @@ android.libraryVariants.all { variant -> ...@@ -26,9 +26,7 @@ android.libraryVariants.all { variant ->
title = "ExoPlayer ${javadocTitle}" title = "ExoPlayer ${javadocTitle}"
source = allSourceDirs source = allSourceDirs
options { options {
links "http://docs.oracle.com/javase/7/docs/api/" links "https://developer.android.com/reference"
linksOffline "https://developer.android.com/reference",
"${android.sdkDirectory}/docs/reference"
encoding = "UTF-8" encoding = "UTF-8"
} }
exclude "**/BuildConfig.java" exclude "**/BuildConfig.java"
......
...@@ -15,18 +15,11 @@ ext.fixJavadoc = { ...@@ -15,18 +15,11 @@ ext.fixJavadoc = {
def javadocPath = "${project.buildDir}/docs/javadoc" def javadocPath = "${project.buildDir}/docs/javadoc"
// Fix external Android links to target the top frame. // Fix external Android links to target the top frame.
def androidRoot = "https://developer.android.com/reference/" def androidRoot = "https://developer.android.com/reference/"
def androidLink = "<a href=\"(${androidRoot}.*?)\\?is-external=true\"" def androidLink = "<a href=\"(${androidRoot}.*?)\\?is-external=true(.*)\""
def androidFixed = "<a href=\"\\1\" target=\"_top\"" def androidFixed = "<a href=\"\\1\\2\" target=\"_top\""
ant.replaceregexp(match:androidLink, replace:androidFixed, flags:'g') { ant.replaceregexp(match:androidLink, replace:androidFixed, flags:'g') {
fileset(dir: "${javadocPath}", includes: "**/*.html") fileset(dir: "${javadocPath}", includes: "**/*.html")
} }
// Fix external Oracle links to use frames and target the top frame.
def oracleRoot = "https://docs.oracle.com/javase/7/docs/api/"
def oracleLink = "<a href=\"(${oracleRoot})(.*?)\\?is-external=true\""
def oracleFixed = "<a href=\"\\1index.html\\?\\2\" target=\"_top\""
ant.replaceregexp(match:oracleLink, replace:oracleFixed, flags:'g') {
fileset(dir: "${javadocPath}", includes: "**/*.html")
}
// Add favicon to each page // Add favicon to each page
def headTag = "<head>" def headTag = "<head>"
def headTagWithFavicon = "<head>" + def headTagWithFavicon = "<head>" +
......
...@@ -1020,6 +1020,37 @@ public final class C { ...@@ -1020,6 +1020,37 @@ public final class C {
public static final int NETWORK_TYPE_OTHER = 8; public static final int NETWORK_TYPE_OTHER = 8;
/** /**
* Mode specifying whether the player should hold a WakeLock and a WifiLock. One of {@link
* #WAKE_MODE_NONE}, {@link #WAKE_MODE_LOCAL} and {@link #WAKE_MODE_NETWORK}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({WAKE_MODE_NONE, WAKE_MODE_LOCAL, WAKE_MODE_NETWORK})
public @interface WakeMode {}
/**
* A wake mode that will not cause the player to hold any locks.
*
* <p>This is suitable for applications that do not play media with the screen off.
*/
public static final int WAKE_MODE_NONE = 0;
/**
* A wake mode that will cause the player to hold a {@link android.os.PowerManager.WakeLock}
* during playback.
*
* <p>This is suitable for applications that play media with the screen off and do not load media
* over wifi.
*/
public static final int WAKE_MODE_LOCAL = 1;
/**
* A wake mode that will cause the player to hold a {@link android.os.PowerManager.WakeLock} and a
* {@link android.net.wifi.WifiManager.WifiLock} during playback.
*
* <p>This is suitable for applications that play media with the screen off and may load media
* over wifi.
*/
public static final int WAKE_MODE_NETWORK = 2;
/**
* Track role flags. Possible flag values are {@link #ROLE_FLAG_MAIN}, {@link * Track role flags. Possible flag values are {@link #ROLE_FLAG_MAIN}, {@link
* #ROLE_FLAG_ALTERNATE}, {@link #ROLE_FLAG_SUPPLEMENTARY}, {@link #ROLE_FLAG_COMMENTARY}, {@link * #ROLE_FLAG_ALTERNATE}, {@link #ROLE_FLAG_SUPPLEMENTARY}, {@link #ROLE_FLAG_COMMENTARY}, {@link
* #ROLE_FLAG_DUB}, {@link #ROLE_FLAG_EMERGENCY}, {@link #ROLE_FLAG_CAPTION}, {@link * #ROLE_FLAG_DUB}, {@link #ROLE_FLAG_EMERGENCY}, {@link #ROLE_FLAG_CAPTION}, {@link
......
...@@ -386,7 +386,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -386,7 +386,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
playbackInfo = playbackInfo.copyWithPlaybackError(e); playbackInfo = playbackInfo.copyWithPlaybackError(e);
maybeNotifyPlaybackInfoChanged(); maybeNotifyPlaybackInfoChanged();
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "Source error.", e); Log.e(TAG, "Source error", e);
stopInternal( stopInternal(
/* forceResetRenderers= */ false, /* forceResetRenderers= */ false,
/* resetPositionAndState= */ false, /* resetPositionAndState= */ false,
...@@ -394,7 +394,7 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -394,7 +394,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
playbackInfo = playbackInfo.copyWithPlaybackError(ExoPlaybackException.createForSource(e)); playbackInfo = playbackInfo.copyWithPlaybackError(ExoPlaybackException.createForSource(e));
maybeNotifyPlaybackInfoChanged(); maybeNotifyPlaybackInfoChanged();
} catch (RuntimeException | OutOfMemoryError e) { } catch (RuntimeException | OutOfMemoryError e) {
Log.e(TAG, "Internal runtime error.", e); Log.e(TAG, "Internal runtime error", e);
ExoPlaybackException error = ExoPlaybackException error =
e instanceof OutOfMemoryError e instanceof OutOfMemoryError
? ExoPlaybackException.createForOutOfMemoryError((OutOfMemoryError) e) ? ExoPlaybackException.createForOutOfMemoryError((OutOfMemoryError) e)
......
...@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { ...@@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */ /** The version of the library expressed as a string, for example "1.2.3". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "2.11.3"; public static final String VERSION = "2.11.4";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.3"; public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.4";
/** /**
* The version of the library expressed as an integer, for example 1002003. * The version of the library expressed as an integer, for example 1002003.
...@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { ...@@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
* integer version 123045006 (123-045-006). * integer version 123045006 (123-045-006).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 2011003; public static final int VERSION_INT = 2011004;
/** /**
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
......
...@@ -325,6 +325,7 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -325,6 +325,7 @@ public class SimpleExoPlayer extends BasePlayer
private final AudioBecomingNoisyManager audioBecomingNoisyManager; private final AudioBecomingNoisyManager audioBecomingNoisyManager;
private final AudioFocusManager audioFocusManager; private final AudioFocusManager audioFocusManager;
private final WakeLockManager wakeLockManager; private final WakeLockManager wakeLockManager;
private final WifiLockManager wifiLockManager;
@Nullable private Format videoFormat; @Nullable private Format videoFormat;
@Nullable private Format audioFormat; @Nullable private Format audioFormat;
...@@ -445,8 +446,8 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -445,8 +446,8 @@ public class SimpleExoPlayer extends BasePlayer
player = player =
new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper);
analyticsCollector.setPlayer(player); analyticsCollector.setPlayer(player);
addListener(analyticsCollector); player.addListener(analyticsCollector);
addListener(componentListener); player.addListener(componentListener);
videoDebugListeners.add(analyticsCollector); videoDebugListeners.add(analyticsCollector);
videoListeners.add(analyticsCollector); videoListeners.add(analyticsCollector);
audioDebugListeners.add(analyticsCollector); audioDebugListeners.add(analyticsCollector);
...@@ -460,6 +461,7 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -460,6 +461,7 @@ public class SimpleExoPlayer extends BasePlayer
new AudioBecomingNoisyManager(context, eventHandler, componentListener); new AudioBecomingNoisyManager(context, eventHandler, componentListener);
audioFocusManager = new AudioFocusManager(context, eventHandler, componentListener); audioFocusManager = new AudioFocusManager(context, eventHandler, componentListener);
wakeLockManager = new WakeLockManager(context); wakeLockManager = new WakeLockManager(context);
wifiLockManager = new WifiLockManager(context);
} }
@Override @Override
...@@ -684,11 +686,11 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -684,11 +686,11 @@ public class SimpleExoPlayer extends BasePlayer
} }
} }
audioFocusManager.setAudioAttributes(handleAudioFocus ? audioAttributes : null);
boolean playWhenReady = getPlayWhenReady();
@AudioFocusManager.PlayerCommand @AudioFocusManager.PlayerCommand
int playerCommand = int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState());
audioFocusManager.setAudioAttributes( updatePlayWhenReady(playWhenReady, playerCommand);
handleAudioFocus ? audioAttributes : null, getPlayWhenReady(), getPlaybackState());
updatePlayWhenReady(getPlayWhenReady(), playerCommand);
} }
@Override @Override
...@@ -1187,9 +1189,10 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1187,9 +1189,10 @@ public class SimpleExoPlayer extends BasePlayer
} }
this.mediaSource = mediaSource; this.mediaSource = mediaSource;
mediaSource.addEventListener(eventHandler, analyticsCollector); mediaSource.addEventListener(eventHandler, analyticsCollector);
boolean playWhenReady = getPlayWhenReady();
@AudioFocusManager.PlayerCommand @AudioFocusManager.PlayerCommand
int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady()); int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, Player.STATE_BUFFERING);
updatePlayWhenReady(getPlayWhenReady(), playerCommand); updatePlayWhenReady(playWhenReady, playerCommand);
player.prepare(mediaSource, resetPosition, resetState); player.prepare(mediaSource, resetPosition, resetState);
} }
...@@ -1197,7 +1200,7 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1197,7 +1200,7 @@ public class SimpleExoPlayer extends BasePlayer
public void setPlayWhenReady(boolean playWhenReady) { public void setPlayWhenReady(boolean playWhenReady) {
verifyApplicationThread(); verifyApplicationThread();
@AudioFocusManager.PlayerCommand @AudioFocusManager.PlayerCommand
int playerCommand = audioFocusManager.handleSetPlayWhenReady(playWhenReady, getPlaybackState()); int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState());
updatePlayWhenReady(playWhenReady, playerCommand); updatePlayWhenReady(playWhenReady, playerCommand);
} }
...@@ -1276,6 +1279,7 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1276,6 +1279,7 @@ public class SimpleExoPlayer extends BasePlayer
@Override @Override
public void stop(boolean reset) { public void stop(boolean reset) {
verifyApplicationThread(); verifyApplicationThread();
audioFocusManager.updateAudioFocus(getPlayWhenReady(), Player.STATE_IDLE);
player.stop(reset); player.stop(reset);
if (mediaSource != null) { if (mediaSource != null) {
mediaSource.removeEventListener(analyticsCollector); mediaSource.removeEventListener(analyticsCollector);
...@@ -1284,7 +1288,6 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1284,7 +1288,6 @@ public class SimpleExoPlayer extends BasePlayer
mediaSource = null; mediaSource = null;
} }
} }
audioFocusManager.handleStop();
currentCues = Collections.emptyList(); currentCues = Collections.emptyList();
} }
...@@ -1292,8 +1295,9 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1292,8 +1295,9 @@ public class SimpleExoPlayer extends BasePlayer
public void release() { public void release() {
verifyApplicationThread(); verifyApplicationThread();
audioBecomingNoisyManager.setEnabled(false); audioBecomingNoisyManager.setEnabled(false);
audioFocusManager.handleStop();
wakeLockManager.setStayAwake(false); wakeLockManager.setStayAwake(false);
wifiLockManager.setStayAwake(false);
audioFocusManager.release();
player.release(); player.release();
removeSurfaceCallbacks(); removeSurfaceCallbacks();
if (surface != null) { if (surface != null) {
...@@ -1432,9 +1436,45 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1432,9 +1436,45 @@ public class SimpleExoPlayer extends BasePlayer
* *
* @param handleWakeLock Whether the player should use a {@link android.os.PowerManager.WakeLock} * @param handleWakeLock Whether the player should use a {@link android.os.PowerManager.WakeLock}
* to ensure the device stays awake for playback, even when the screen is off. * to ensure the device stays awake for playback, even when the screen is off.
* @deprecated Use {@link #setWakeMode(int)} instead.
*/ */
@Deprecated
public void setHandleWakeLock(boolean handleWakeLock) { public void setHandleWakeLock(boolean handleWakeLock) {
wakeLockManager.setEnabled(handleWakeLock); setWakeMode(handleWakeLock ? C.WAKE_MODE_LOCAL : C.WAKE_MODE_NONE);
}
/**
* Sets how the player should keep the device awake for playback when the screen is off.
*
* <p>Enabling this feature requires the {@link android.Manifest.permission#WAKE_LOCK} permission.
* It should be used together with a foreground {@link android.app.Service} for use cases where
* playback occurs and the screen is off (e.g. background audio playback). It is not useful when
* the screen will be kept on during playback (e.g. foreground video playback).
*
* <p>When enabled, the locks ({@link android.os.PowerManager.WakeLock} / {@link
* android.net.wifi.WifiManager.WifiLock}) will be held whenever the player is in the {@link
* #STATE_READY} or {@link #STATE_BUFFERING} states with {@code playWhenReady = true}. The locks
* held depends on the specified {@link C.WakeMode}.
*
* @param wakeMode The {@link C.WakeMode} option to keep the device awake during playback.
*/
public void setWakeMode(@C.WakeMode int wakeMode) {
switch (wakeMode) {
case C.WAKE_MODE_NONE:
wakeLockManager.setEnabled(false);
wifiLockManager.setEnabled(false);
break;
case C.WAKE_MODE_LOCAL:
wakeLockManager.setEnabled(true);
wifiLockManager.setEnabled(false);
break;
case C.WAKE_MODE_NETWORK:
wakeLockManager.setEnabled(true);
wifiLockManager.setEnabled(true);
break;
default:
break;
}
} }
// Internal methods. // Internal methods.
...@@ -1537,6 +1577,24 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1537,6 +1577,24 @@ public class SimpleExoPlayer extends BasePlayer
} }
} }
private void updateWakeAndWifiLock() {
@State int playbackState = getPlaybackState();
switch (playbackState) {
case Player.STATE_READY:
case Player.STATE_BUFFERING:
wakeLockManager.setStayAwake(getPlayWhenReady());
wifiLockManager.setStayAwake(getPlayWhenReady());
break;
case Player.STATE_ENDED:
case Player.STATE_IDLE:
wakeLockManager.setStayAwake(false);
wifiLockManager.setStayAwake(false);
break;
default:
throw new IllegalStateException();
}
}
private final class ComponentListener private final class ComponentListener
implements VideoRendererEventListener, implements VideoRendererEventListener,
AudioRendererEventListener, AudioRendererEventListener,
...@@ -1781,16 +1839,7 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1781,16 +1839,7 @@ public class SimpleExoPlayer extends BasePlayer
@Override @Override
public void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) { public void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) {
switch (playbackState) { updateWakeAndWifiLock();
case Player.STATE_READY:
case Player.STATE_BUFFERING:
wakeLockManager.setStayAwake(playWhenReady);
break;
case Player.STATE_ENDED:
case Player.STATE_IDLE:
wakeLockManager.setStayAwake(false);
break;
}
} }
} }
} }
...@@ -39,7 +39,8 @@ import com.google.android.exoplayer2.util.Log; ...@@ -39,7 +39,8 @@ import com.google.android.exoplayer2.util.Log;
private boolean stayAwake; private boolean stayAwake;
public WakeLockManager(Context context) { public WakeLockManager(Context context) {
powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); powerManager =
(PowerManager) context.getApplicationContext().getSystemService(Context.POWER_SERVICE);
} }
/** /**
...@@ -48,18 +49,19 @@ import com.google.android.exoplayer2.util.Log; ...@@ -48,18 +49,19 @@ import com.google.android.exoplayer2.util.Log;
* <p>By default, wake lock handling is not enabled. Enabling this will acquire the wake lock if * <p>By default, wake lock handling is not enabled. Enabling this will acquire the wake lock if
* necessary. Disabling this will release the wake lock if it is held. * necessary. Disabling this will release the wake lock if it is held.
* *
* @param enabled True if the player should handle a {@link WakeLock}, false otherwise. Please * <p>Enabling {@link WakeLock} requires the {@link android.Manifest.permission#WAKE_LOCK}.
* note that enabling this requires the {@link android.Manifest.permission#WAKE_LOCK} *
* permission. * @param enabled True if the player should handle a {@link WakeLock}, false otherwise.
*/ */
public void setEnabled(boolean enabled) { public void setEnabled(boolean enabled) {
if (enabled) { if (enabled) {
if (wakeLock == null) { if (wakeLock == null) {
if (powerManager == null) { if (powerManager == null) {
Log.w(TAG, "PowerManager was null, therefore the WakeLock was not created."); Log.w(TAG, "PowerManager is null, therefore not creating the WakeLock.");
return; return;
} }
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG); wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);
wakeLock.setReferenceCounted(false);
} }
} }
...@@ -86,17 +88,14 @@ import com.google.android.exoplayer2.util.Log; ...@@ -86,17 +88,14 @@ import com.google.android.exoplayer2.util.Log;
// reasonable timeout that would not affect the user. // reasonable timeout that would not affect the user.
@SuppressLint("WakelockTimeout") @SuppressLint("WakelockTimeout")
private void updateWakeLock() { private void updateWakeLock() {
// Needed for the library nullness check. If enabled is true, the wakelock will not be null. if (wakeLock == null) {
if (wakeLock != null) { return;
if (enabled) { }
if (stayAwake && !wakeLock.isHeld()) {
wakeLock.acquire(); if (enabled && stayAwake) {
} else if (!stayAwake && wakeLock.isHeld()) { wakeLock.acquire();
wakeLock.release(); } else {
} wakeLock.release();
} else if (wakeLock.isHeld()) {
wakeLock.release();
}
} }
} }
} }
/*
* Copyright (C) 2020 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;
import android.content.Context;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.WifiLock;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.util.Log;
/**
* Handles a {@link WifiLock}
*
* <p>The handling of wifi locks requires the {@link android.Manifest.permission#WAKE_LOCK}
* permission.
*/
/* package */ final class WifiLockManager {
private static final String TAG = "WifiLockManager";
private static final String WIFI_LOCK_TAG = "ExoPlayer:WifiLockManager";
@Nullable private final WifiManager wifiManager;
@Nullable private WifiLock wifiLock;
private boolean enabled;
private boolean stayAwake;
public WifiLockManager(Context context) {
wifiManager =
(WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
}
/**
* Sets whether to enable the usage of a {@link WifiLock}.
*
* <p>By default, wifi lock handling is not enabled. Enabling will acquire the wifi lock if
* necessary. Disabling will release the wifi lock if held.
*
* <p>Enabling {@link WifiLock} requires the {@link android.Manifest.permission#WAKE_LOCK}.
*
* @param enabled True if the player should handle a {@link WifiLock}.
*/
public void setEnabled(boolean enabled) {
if (enabled && wifiLock == null) {
if (wifiManager == null) {
Log.w(TAG, "WifiManager is null, therefore not creating the WifiLock.");
return;
}
wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, WIFI_LOCK_TAG);
wifiLock.setReferenceCounted(false);
}
this.enabled = enabled;
updateWifiLock();
}
/**
* Sets whether to acquire or release the {@link WifiLock}.
*
* <p>The wifi lock will not be acquired unless handling has been enabled through {@link
* #setEnabled(boolean)}.
*
* @param stayAwake True if the player should acquire the {@link WifiLock}. False if it should
* release.
*/
public void setStayAwake(boolean stayAwake) {
this.stayAwake = stayAwake;
updateWifiLock();
}
private void updateWifiLock() {
if (wifiLock == null) {
return;
}
if (enabled && stayAwake) {
wifiLock.acquire();
} else {
wifiLock.release();
}
}
}
...@@ -50,7 +50,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ...@@ -50,7 +50,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
* <p>For accurate measurements, the listener should be added to the player before loading media, * <p>For accurate measurements, the listener should be added to the player before loading media,
* i.e., {@link Player#getPlaybackState()} should be {@link Player#STATE_IDLE}. * i.e., {@link Player#getPlaybackState()} should be {@link Player#STATE_IDLE}.
* *
* <p>Playback stats are gathered separately for all playback session, i.e. each window in the * <p>Playback stats are gathered separately for each playback session, i.e. each window in the
* {@link Timeline} and each single ad. * {@link Timeline} and each single ad.
*/ */
public final class PlaybackStatsListener public final class PlaybackStatsListener
...@@ -931,6 +931,9 @@ public final class PlaybackStatsListener ...@@ -931,6 +931,9 @@ public final class PlaybackStatsListener
} }
private void maybeUpdateMediaTimeHistory(long realtimeMs, long mediaTimeMs) { private void maybeUpdateMediaTimeHistory(long realtimeMs, long mediaTimeMs) {
if (!keepHistory) {
return;
}
if (currentPlaybackState != PlaybackStats.PLAYBACK_STATE_PLAYING) { if (currentPlaybackState != PlaybackStats.PLAYBACK_STATE_PLAYING) {
if (mediaTimeMs == C.TIME_UNSET) { if (mediaTimeMs == C.TIME_UNSET) {
return; return;
......
...@@ -238,7 +238,7 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -238,7 +238,7 @@ public final class DefaultAudioSink implements AudioSink {
@Nullable private final AudioCapabilities audioCapabilities; @Nullable private final AudioCapabilities audioCapabilities;
private final AudioProcessorChain audioProcessorChain; private final AudioProcessorChain audioProcessorChain;
private final boolean enableConvertHighResIntPcmToFloat; private final boolean enableFloatOutput;
private final ChannelMappingAudioProcessor channelMappingAudioProcessor; private final ChannelMappingAudioProcessor channelMappingAudioProcessor;
private final TrimmingAudioProcessor trimmingAudioProcessor; private final TrimmingAudioProcessor trimmingAudioProcessor;
private final AudioProcessor[] toIntPcmAvailableAudioProcessors; private final AudioProcessor[] toIntPcmAvailableAudioProcessors;
...@@ -299,7 +299,7 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -299,7 +299,7 @@ public final class DefaultAudioSink implements AudioSink {
*/ */
public DefaultAudioSink( public DefaultAudioSink(
@Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors) { @Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors) {
this(audioCapabilities, audioProcessors, /* enableConvertHighResIntPcmToFloat= */ false); this(audioCapabilities, audioProcessors, /* enableFloatOutput= */ false);
} }
/** /**
...@@ -309,19 +309,16 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -309,19 +309,16 @@ public final class DefaultAudioSink implements AudioSink {
* default capabilities (no encoded audio passthrough support) should be assumed. * default capabilities (no encoded audio passthrough support) should be assumed.
* @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before
* output. May be empty. * output. May be empty.
* @param enableConvertHighResIntPcmToFloat Whether to enable conversion of high resolution * @param enableFloatOutput Whether to enable 32-bit float output. Where possible, 32-bit float
* integer PCM to 32-bit float for output, if possible. Functionality that uses 16-bit integer * output will be used if the input is 32-bit float, and also if the input is high resolution
* audio processing (for example, speed and pitch adjustment) will not be available when float * (24-bit or 32-bit) integer PCM. Audio processing (for example, speed adjustment) will not
* output is in use. * be available when float output is in use.
*/ */
public DefaultAudioSink( public DefaultAudioSink(
@Nullable AudioCapabilities audioCapabilities, @Nullable AudioCapabilities audioCapabilities,
AudioProcessor[] audioProcessors, AudioProcessor[] audioProcessors,
boolean enableConvertHighResIntPcmToFloat) { boolean enableFloatOutput) {
this( this(audioCapabilities, new DefaultAudioProcessorChain(audioProcessors), enableFloatOutput);
audioCapabilities,
new DefaultAudioProcessorChain(audioProcessors),
enableConvertHighResIntPcmToFloat);
} }
/** /**
...@@ -332,18 +329,18 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -332,18 +329,18 @@ public final class DefaultAudioSink implements AudioSink {
* default capabilities (no encoded audio passthrough support) should be assumed. * default capabilities (no encoded audio passthrough support) should be assumed.
* @param audioProcessorChain An {@link AudioProcessorChain} which is used to apply playback * @param audioProcessorChain An {@link AudioProcessorChain} which is used to apply playback
* parameters adjustments. The instance passed in must not be reused in other sinks. * parameters adjustments. The instance passed in must not be reused in other sinks.
* @param enableConvertHighResIntPcmToFloat Whether to enable conversion of high resolution * @param enableFloatOutput Whether to enable 32-bit float output. Where possible, 32-bit float
* integer PCM to 32-bit float for output, if possible. Functionality that uses 16-bit integer * output will be used if the input is 32-bit float, and also if the input is high resolution
* audio processing (for example, speed and pitch adjustment) will not be available when float * (24-bit or 32-bit) integer PCM. Audio processing (for example, speed adjustment) will not
* output is in use. * be available when float output is in use.
*/ */
public DefaultAudioSink( public DefaultAudioSink(
@Nullable AudioCapabilities audioCapabilities, @Nullable AudioCapabilities audioCapabilities,
AudioProcessorChain audioProcessorChain, AudioProcessorChain audioProcessorChain,
boolean enableConvertHighResIntPcmToFloat) { boolean enableFloatOutput) {
this.audioCapabilities = audioCapabilities; this.audioCapabilities = audioCapabilities;
this.audioProcessorChain = Assertions.checkNotNull(audioProcessorChain); this.audioProcessorChain = Assertions.checkNotNull(audioProcessorChain);
this.enableConvertHighResIntPcmToFloat = enableConvertHighResIntPcmToFloat; this.enableFloatOutput = enableFloatOutput;
releasingConditionVariable = new ConditionVariable(true); releasingConditionVariable = new ConditionVariable(true);
audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener()); audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener());
channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
...@@ -422,37 +419,34 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -422,37 +419,34 @@ public final class DefaultAudioSink implements AudioSink {
} }
boolean isInputPcm = Util.isEncodingLinearPcm(inputEncoding); boolean isInputPcm = Util.isEncodingLinearPcm(inputEncoding);
boolean processingEnabled = isInputPcm && inputEncoding != C.ENCODING_PCM_FLOAT; boolean processingEnabled = isInputPcm;
int sampleRate = inputSampleRate; int sampleRate = inputSampleRate;
int channelCount = inputChannelCount; int channelCount = inputChannelCount;
@C.Encoding int encoding = inputEncoding; @C.Encoding int encoding = inputEncoding;
boolean shouldConvertHighResIntPcmToFloat = boolean useFloatOutput =
enableConvertHighResIntPcmToFloat enableFloatOutput
&& supportsOutput(inputChannelCount, C.ENCODING_PCM_FLOAT) && supportsOutput(inputChannelCount, C.ENCODING_PCM_FLOAT)
&& Util.isEncodingHighResolutionIntegerPcm(inputEncoding); && Util.isEncodingHighResolutionPcm(inputEncoding);
AudioProcessor[] availableAudioProcessors = AudioProcessor[] availableAudioProcessors =
shouldConvertHighResIntPcmToFloat useFloatOutput ? toFloatPcmAvailableAudioProcessors : toIntPcmAvailableAudioProcessors;
? toFloatPcmAvailableAudioProcessors
: toIntPcmAvailableAudioProcessors;
if (processingEnabled) { if (processingEnabled) {
trimmingAudioProcessor.setTrimFrameCount(trimStartFrames, trimEndFrames); trimmingAudioProcessor.setTrimFrameCount(trimStartFrames, trimEndFrames);
channelMappingAudioProcessor.setChannelMap(outputChannels); channelMappingAudioProcessor.setChannelMap(outputChannels);
AudioProcessor.AudioFormat inputAudioFormat = AudioProcessor.AudioFormat outputFormat =
new AudioProcessor.AudioFormat(sampleRate, channelCount, encoding); new AudioProcessor.AudioFormat(sampleRate, channelCount, encoding);
AudioProcessor.AudioFormat outputAudioFormat = inputAudioFormat;
for (AudioProcessor audioProcessor : availableAudioProcessors) { for (AudioProcessor audioProcessor : availableAudioProcessors) {
try { try {
outputAudioFormat = audioProcessor.configure(inputAudioFormat); AudioProcessor.AudioFormat nextFormat = audioProcessor.configure(outputFormat);
if (audioProcessor.isActive()) {
outputFormat = nextFormat;
}
} catch (UnhandledAudioFormatException e) { } catch (UnhandledAudioFormatException e) {
throw new ConfigurationException(e); throw new ConfigurationException(e);
} }
if (audioProcessor.isActive()) {
inputAudioFormat = outputAudioFormat;
}
} }
sampleRate = outputAudioFormat.sampleRate; sampleRate = outputFormat.sampleRate;
channelCount = outputAudioFormat.channelCount; channelCount = outputFormat.channelCount;
encoding = outputAudioFormat.encoding; encoding = outputFormat.encoding;
} }
int outputChannelConfig = getChannelConfig(channelCount, isInputPcm); int outputChannelConfig = getChannelConfig(channelCount, isInputPcm);
...@@ -464,7 +458,7 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -464,7 +458,7 @@ public final class DefaultAudioSink implements AudioSink {
isInputPcm ? Util.getPcmFrameSize(inputEncoding, inputChannelCount) : C.LENGTH_UNSET; isInputPcm ? Util.getPcmFrameSize(inputEncoding, inputChannelCount) : C.LENGTH_UNSET;
int outputPcmFrameSize = int outputPcmFrameSize =
isInputPcm ? Util.getPcmFrameSize(encoding, channelCount) : C.LENGTH_UNSET; isInputPcm ? Util.getPcmFrameSize(encoding, channelCount) : C.LENGTH_UNSET;
boolean canApplyPlaybackParameters = processingEnabled && !shouldConvertHighResIntPcmToFloat; boolean canApplyPlaybackParameters = processingEnabled && !useFloatOutput;
Configuration pendingConfiguration = Configuration pendingConfiguration =
new Configuration( new Configuration(
isInputPcm, isInputPcm,
......
...@@ -16,13 +16,19 @@ ...@@ -16,13 +16,19 @@
package com.google.android.exoplayer2.audio; package com.google.android.exoplayer2.audio;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
/** /**
* An {@link AudioProcessor} that converts 24-bit and 32-bit integer PCM audio to 32-bit float PCM * An {@link AudioProcessor} that converts high resolution PCM audio to 32-bit float. The following
* audio. * encodings are supported as input:
*
* <ul>
* <li>{@link C#ENCODING_PCM_24BIT}
* <li>{@link C#ENCODING_PCM_32BIT}
* <li>{@link C#ENCODING_PCM_FLOAT} ({@link #isActive()} will return {@code false})
* </ul>
*/ */
/* package */ final class FloatResamplingAudioProcessor extends BaseAudioProcessor { /* package */ final class FloatResamplingAudioProcessor extends BaseAudioProcessor {
...@@ -32,10 +38,11 @@ import java.nio.ByteBuffer; ...@@ -32,10 +38,11 @@ import java.nio.ByteBuffer;
@Override @Override
public AudioFormat onConfigure(AudioFormat inputAudioFormat) public AudioFormat onConfigure(AudioFormat inputAudioFormat)
throws UnhandledAudioFormatException { throws UnhandledAudioFormatException {
if (!Util.isEncodingHighResolutionIntegerPcm(inputAudioFormat.encoding)) { @C.PcmEncoding int encoding = inputAudioFormat.encoding;
if (!Util.isEncodingHighResolutionPcm(encoding)) {
throw new UnhandledAudioFormatException(inputAudioFormat); throw new UnhandledAudioFormatException(inputAudioFormat);
} }
return Util.isEncodingHighResolutionIntegerPcm(inputAudioFormat.encoding) return encoding != C.ENCODING_PCM_FLOAT
? new AudioFormat( ? new AudioFormat(
inputAudioFormat.sampleRate, inputAudioFormat.channelCount, C.ENCODING_PCM_FLOAT) inputAudioFormat.sampleRate, inputAudioFormat.channelCount, C.ENCODING_PCM_FLOAT)
: AudioFormat.NOT_SET; : AudioFormat.NOT_SET;
...@@ -43,31 +50,42 @@ import java.nio.ByteBuffer; ...@@ -43,31 +50,42 @@ import java.nio.ByteBuffer;
@Override @Override
public void queueInput(ByteBuffer inputBuffer) { public void queueInput(ByteBuffer inputBuffer) {
Assertions.checkState(Util.isEncodingHighResolutionIntegerPcm(inputAudioFormat.encoding));
boolean isInput32Bit = inputAudioFormat.encoding == C.ENCODING_PCM_32BIT;
int position = inputBuffer.position(); int position = inputBuffer.position();
int limit = inputBuffer.limit(); int limit = inputBuffer.limit();
int size = limit - position; int size = limit - position;
int resampledSize = isInput32Bit ? size : (size / 3) * 4; ByteBuffer buffer;
ByteBuffer buffer = replaceOutputBuffer(resampledSize); switch (inputAudioFormat.encoding) {
if (isInput32Bit) { case C.ENCODING_PCM_24BIT:
for (int i = position; i < limit; i += 4) { buffer = replaceOutputBuffer((size / 3) * 4);
int pcm32BitInteger = for (int i = position; i < limit; i += 3) {
(inputBuffer.get(i) & 0xFF) int pcm32BitInteger =
| ((inputBuffer.get(i + 1) & 0xFF) << 8) ((inputBuffer.get(i) & 0xFF) << 8)
| ((inputBuffer.get(i + 2) & 0xFF) << 16) | ((inputBuffer.get(i + 1) & 0xFF) << 16)
| ((inputBuffer.get(i + 3) & 0xFF) << 24); | ((inputBuffer.get(i + 2) & 0xFF) << 24);
writePcm32BitFloat(pcm32BitInteger, buffer); writePcm32BitFloat(pcm32BitInteger, buffer);
} }
} else { // Input is 24-bit PCM. break;
for (int i = position; i < limit; i += 3) { case C.ENCODING_PCM_32BIT:
int pcm32BitInteger = buffer = replaceOutputBuffer(size);
((inputBuffer.get(i) & 0xFF) << 8) for (int i = position; i < limit; i += 4) {
| ((inputBuffer.get(i + 1) & 0xFF) << 16) int pcm32BitInteger =
| ((inputBuffer.get(i + 2) & 0xFF) << 24); (inputBuffer.get(i) & 0xFF)
writePcm32BitFloat(pcm32BitInteger, buffer); | ((inputBuffer.get(i + 1) & 0xFF) << 8)
} | ((inputBuffer.get(i + 2) & 0xFF) << 16)
| ((inputBuffer.get(i + 3) & 0xFF) << 24);
writePcm32BitFloat(pcm32BitInteger, buffer);
}
break;
case C.ENCODING_PCM_8BIT:
case C.ENCODING_PCM_16BIT:
case C.ENCODING_PCM_16BIT_BIG_ENDIAN:
case C.ENCODING_PCM_FLOAT:
case C.ENCODING_INVALID:
case Format.NO_VALUE:
default:
// Never happens.
throw new IllegalStateException();
} }
inputBuffer.position(inputBuffer.limit()); inputBuffer.position(inputBuffer.limit());
......
...@@ -20,8 +20,17 @@ import com.google.android.exoplayer2.Format; ...@@ -20,8 +20,17 @@ import com.google.android.exoplayer2.Format;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
/** /**
* An {@link AudioProcessor} that converts 8-bit, 24-bit and 32-bit integer PCM audio to 16-bit * An {@link AudioProcessor} that converts different PCM audio encodings to 16-bit integer PCM. The
* integer PCM audio. * following encodings are supported as input:
*
* <ul>
* <li>{@link C#ENCODING_PCM_8BIT}
* <li>{@link C#ENCODING_PCM_16BIT} ({@link #isActive()} will return {@code false})
* <li>{@link C#ENCODING_PCM_16BIT_BIG_ENDIAN}
* <li>{@link C#ENCODING_PCM_24BIT}
* <li>{@link C#ENCODING_PCM_32BIT}
* <li>{@link C#ENCODING_PCM_FLOAT}
* </ul>
*/ */
/* package */ final class ResamplingAudioProcessor extends BaseAudioProcessor { /* package */ final class ResamplingAudioProcessor extends BaseAudioProcessor {
...@@ -33,7 +42,8 @@ import java.nio.ByteBuffer; ...@@ -33,7 +42,8 @@ import java.nio.ByteBuffer;
&& encoding != C.ENCODING_PCM_16BIT && encoding != C.ENCODING_PCM_16BIT
&& encoding != C.ENCODING_PCM_16BIT_BIG_ENDIAN && encoding != C.ENCODING_PCM_16BIT_BIG_ENDIAN
&& encoding != C.ENCODING_PCM_24BIT && encoding != C.ENCODING_PCM_24BIT
&& encoding != C.ENCODING_PCM_32BIT) { && encoding != C.ENCODING_PCM_32BIT
&& encoding != C.ENCODING_PCM_FLOAT) {
throw new UnhandledAudioFormatException(inputAudioFormat); throw new UnhandledAudioFormatException(inputAudioFormat);
} }
return encoding != C.ENCODING_PCM_16BIT return encoding != C.ENCODING_PCM_16BIT
...@@ -60,10 +70,10 @@ import java.nio.ByteBuffer; ...@@ -60,10 +70,10 @@ import java.nio.ByteBuffer;
resampledSize = (size / 3) * 2; resampledSize = (size / 3) * 2;
break; break;
case C.ENCODING_PCM_32BIT: case C.ENCODING_PCM_32BIT:
case C.ENCODING_PCM_FLOAT:
resampledSize = size / 2; resampledSize = size / 2;
break; break;
case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_16BIT:
case C.ENCODING_PCM_FLOAT:
case C.ENCODING_INVALID: case C.ENCODING_INVALID:
case Format.NO_VALUE: case Format.NO_VALUE:
default: default:
...@@ -101,8 +111,16 @@ import java.nio.ByteBuffer; ...@@ -101,8 +111,16 @@ import java.nio.ByteBuffer;
buffer.put(inputBuffer.get(i + 3)); buffer.put(inputBuffer.get(i + 3));
} }
break; break;
case C.ENCODING_PCM_16BIT:
case C.ENCODING_PCM_FLOAT: case C.ENCODING_PCM_FLOAT:
// 32 bit floating point -> 16 bit resampling. Floating point values are in the range
// [-1.0, 1.0], so need to be scaled by Short.MAX_VALUE.
for (int i = position; i < limit; i += 4) {
short value = (short) (inputBuffer.getFloat(i) * Short.MAX_VALUE);
buffer.put((byte) (value & 0xFF));
buffer.put((byte) ((value >> 8) & 0xFF));
}
break;
case C.ENCODING_PCM_16BIT:
case C.ENCODING_INVALID: case C.ENCODING_INVALID:
case Format.NO_VALUE: case Format.NO_VALUE:
default: default:
......
...@@ -155,15 +155,16 @@ import java.nio.ByteBuffer; ...@@ -155,15 +155,16 @@ import java.nio.ByteBuffer;
@Override @Override
protected void onFlush() { protected void onFlush() {
if (reconfigurationPending) { if (reconfigurationPending) {
// This is the initial flush after reconfiguration. Prepare to trim bytes from the start/end.
reconfigurationPending = false; reconfigurationPending = false;
endBuffer = new byte[trimEndFrames * inputAudioFormat.bytesPerFrame]; endBuffer = new byte[trimEndFrames * inputAudioFormat.bytesPerFrame];
pendingTrimStartBytes = trimStartFrames * inputAudioFormat.bytesPerFrame; pendingTrimStartBytes = trimStartFrames * inputAudioFormat.bytesPerFrame;
} else { } else {
// Audio processors are flushed after initial configuration, so we leave the pending trim // This is a flush during playback (after the initial flush). We assume this was caused by a
// start byte count unmodified if the processor was just configured. Otherwise we (possibly // seek to a non-zero position and clear pending start bytes. This assumption may be wrong (we
// incorrectly) assume that this is a seek to a non-zero position. We should instead check the // may be seeking to zero), but playing data that should have been trimmed shouldn't be
// timestamp of the first input buffer queued after flushing to decide whether to trim (see // noticeable after a seek. Ideally we would check the timestamp of the first input buffer
// also [Internal: b/77292509]). // queued after flushing to decide whether to trim (see also [Internal: b/77292509]).
pendingTrimStartBytes = 0; pendingTrimStartBytes = 0;
} }
endBufferSize = 0; endBufferSize = 0;
......
...@@ -61,6 +61,7 @@ public final class WavUtil { ...@@ -61,6 +61,7 @@ public final class WavUtil {
return TYPE_PCM; return TYPE_PCM;
case C.ENCODING_PCM_FLOAT: case C.ENCODING_PCM_FLOAT:
return TYPE_FLOAT; return TYPE_FLOAT;
case C.ENCODING_PCM_16BIT_BIG_ENDIAN: // Not TYPE_PCM, because TYPE_PCM is little endian.
case C.ENCODING_INVALID: case C.ENCODING_INVALID:
case Format.NO_VALUE: case Format.NO_VALUE:
default: default:
......
...@@ -63,6 +63,14 @@ public class DecoderInputBuffer extends Buffer { ...@@ -63,6 +63,14 @@ public class DecoderInputBuffer extends Buffer {
/** The buffer's data, or {@code null} if no data has been set. */ /** The buffer's data, or {@code null} if no data has been set. */
@Nullable public ByteBuffer data; @Nullable public ByteBuffer data;
// TODO: Remove this temporary signaling once end-of-stream propagation for clips using content
// protection is fixed. See [Internal: b/153326944] for details.
/**
* Whether the last attempt to read a sample into this buffer failed due to not yet having the DRM
* keys associated with the next sample.
*/
public boolean waitingForKeys;
/** /**
* The time at which the sample should be presented. * The time at which the sample should be presented.
*/ */
...@@ -137,6 +145,7 @@ public class DecoderInputBuffer extends Buffer { ...@@ -137,6 +145,7 @@ public class DecoderInputBuffer extends Buffer {
} }
// Instantiate a new buffer if possible. // Instantiate a new buffer if possible.
ByteBuffer newData = createReplacementByteBuffer(requiredCapacity); ByteBuffer newData = createReplacementByteBuffer(requiredCapacity);
newData.order(data.order());
// Copy data up to the current position from the old buffer to the new one. // Copy data up to the current position from the old buffer to the new one.
if (position > 0) { if (position > 0) {
data.flip(); data.flip();
...@@ -182,6 +191,7 @@ public class DecoderInputBuffer extends Buffer { ...@@ -182,6 +191,7 @@ public class DecoderInputBuffer extends Buffer {
if (supplementalData != null) { if (supplementalData != null) {
supplementalData.clear(); supplementalData.clear();
} }
waitingForKeys = false;
} }
private ByteBuffer createReplacementByteBuffer(int requiredCapacity) { private ByteBuffer createReplacementByteBuffer(int requiredCapacity) {
......
...@@ -149,6 +149,7 @@ public abstract class SimpleDecoder< ...@@ -149,6 +149,7 @@ public abstract class SimpleDecoder<
while (!queuedOutputBuffers.isEmpty()) { while (!queuedOutputBuffers.isEmpty()) {
queuedOutputBuffers.removeFirst().release(); queuedOutputBuffers.removeFirst().release();
} }
exception = null;
} }
} }
...@@ -225,6 +226,7 @@ public abstract class SimpleDecoder< ...@@ -225,6 +226,7 @@ public abstract class SimpleDecoder<
if (inputBuffer.isDecodeOnly()) { if (inputBuffer.isDecodeOnly()) {
outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
} }
@Nullable E exception;
try { try {
exception = decode(inputBuffer, outputBuffer, resetDecoder); exception = decode(inputBuffer, outputBuffer, resetDecoder);
} catch (RuntimeException e) { } catch (RuntimeException e) {
...@@ -238,8 +240,9 @@ public abstract class SimpleDecoder< ...@@ -238,8 +240,9 @@ public abstract class SimpleDecoder<
exception = createUnexpectedDecodeException(e); exception = createUnexpectedDecodeException(e);
} }
if (exception != null) { if (exception != null) {
// Memory barrier to ensure that the decoder exception is visible from the playback thread. synchronized (lock) {
synchronized (lock) {} this.exception = exception;
}
return false; return false;
} }
} }
......
...@@ -341,14 +341,20 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto ...@@ -341,14 +341,20 @@ public final class FrameworkMediaDrm implements ExoMediaDrm<FrameworkMediaCrypto
C.PLAYREADY_UUID, addLaUrlAttributeIfMissing(schemeSpecificData)); C.PLAYREADY_UUID, addLaUrlAttributeIfMissing(schemeSpecificData));
} }
// Prior to L the Widevine CDM required data to be extracted from the PSSH atom. Some Amazon // Prior to API level 21, the Widevine CDM required scheme specific data to be extracted from
// devices also required data to be extracted from the PSSH atom for PlayReady. // the PSSH atom. We also extract the data on API levels 21 and 22 because these API levels
if ((Util.SDK_INT < 21 && C.WIDEVINE_UUID.equals(uuid)) // don't handle V1 PSSH atoms, but do handle scheme specific data regardless of whether it's
// extracted from a V0 or a V1 PSSH atom. Hence extracting the data allows us to support content
// that only provides V1 PSSH atoms. API levels 23 and above understand V0 and V1 PSSH atoms,
// and so we do not extract the data.
// Some Amazon devices also require data to be extracted from the PSSH atom for PlayReady.
if ((Util.SDK_INT < 23 && C.WIDEVINE_UUID.equals(uuid))
|| (C.PLAYREADY_UUID.equals(uuid) || (C.PLAYREADY_UUID.equals(uuid)
&& "Amazon".equals(Util.MANUFACTURER) && "Amazon".equals(Util.MANUFACTURER)
&& ("AFTB".equals(Util.MODEL) // Fire TV Gen 1 && ("AFTB".equals(Util.MODEL) // Fire TV Gen 1
|| "AFTS".equals(Util.MODEL) // Fire TV Gen 2 || "AFTS".equals(Util.MODEL) // Fire TV Gen 2
|| "AFTM".equals(Util.MODEL)))) { // Fire TV Stick Gen 1 || "AFTM".equals(Util.MODEL) // Fire TV Stick Gen 1
|| "AFTT".equals(Util.MODEL)))) { // Fire TV Stick Gen 2
byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(initData, uuid); byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(initData, uuid);
if (psshData != null) { if (psshData != null) {
// Extraction succeeded, so return the extracted data. // Extraction succeeded, so return the extracted data.
......
...@@ -433,10 +433,15 @@ import java.util.List; ...@@ -433,10 +433,15 @@ import java.util.List;
long editDuration = long editDuration =
Util.scaleLargeTimestamp( Util.scaleLargeTimestamp(
track.editListDurations[i], track.timescale, track.movieTimescale); track.editListDurations[i], track.timescale, track.movieTimescale);
startIndices[i] = Util.binarySearchCeil(timestamps, editMediaTime, true, true); startIndices[i] =
Util.binarySearchFloor(
timestamps, editMediaTime, /* inclusive= */ true, /* stayInBounds= */ true);
endIndices[i] = endIndices[i] =
Util.binarySearchCeil( Util.binarySearchCeil(
timestamps, editMediaTime + editDuration, omitClippedSample, false); timestamps,
editMediaTime + editDuration,
/* inclusive= */ omitClippedSample,
/* stayInBounds= */ false);
while (startIndices[i] < endIndices[i] while (startIndices[i] < endIndices[i]
&& (flags[startIndices[i]] & C.BUFFER_FLAG_KEY_FRAME) == 0) { && (flags[startIndices[i]] & C.BUFFER_FLAG_KEY_FRAME) == 0) {
// Applying the edit correctly would require prerolling from the previous sync sample. In // Applying the edit correctly would require prerolling from the previous sync sample. In
...@@ -474,7 +479,7 @@ import java.util.List; ...@@ -474,7 +479,7 @@ import java.util.List;
long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale); long ptsUs = Util.scaleLargeTimestamp(pts, C.MICROS_PER_SECOND, track.movieTimescale);
long timeInSegmentUs = long timeInSegmentUs =
Util.scaleLargeTimestamp( Util.scaleLargeTimestamp(
timestamps[j] - editMediaTime, C.MICROS_PER_SECOND, track.timescale); Math.max(0, timestamps[j] - editMediaTime), C.MICROS_PER_SECOND, track.timescale);
editedTimestamps[sampleIndex] = ptsUs + timeInSegmentUs; editedTimestamps[sampleIndex] = ptsUs + timeInSegmentUs;
if (copyMetadata && editedSizes[sampleIndex] > editedMaximumSize) { if (copyMetadata && editedSizes[sampleIndex] > editedMaximumSize) {
editedMaximumSize = sizes[j]; editedMaximumSize = sizes[j];
......
...@@ -345,42 +345,44 @@ public final class AdtsReader implements ElementaryStreamReader { ...@@ -345,42 +345,44 @@ public final class AdtsReader implements ElementaryStreamReader {
} }
/** /**
* Returns whether the given syncPositionCandidate is a real SYNC word. * Checks whether a candidate SYNC word position is likely to be the position of a real SYNC word.
* * The caller must check that the first byte of the SYNC word is 0xFF before calling this method.
* <p>SYNC word pattern can occur within AAC data, so we perform a few checks to make sure this is * This method performs the following checks:
* really a SYNC word. This includes:
* *
* <ul> * <ul>
* <li>Checking if MPEG version of this frame matches the first detected version. * <li>The MPEG version of this frame must match the previously detected version.
* <li>Checking if the sample rate index of this frame matches the first detected sample rate * <li>The sample rate index of this frame must match the previously detected sample rate index.
* index. * <li>The frame size must be at least 7 bytes
* <li>Checking if the bytes immediately after the current package also match a SYNC-word. * <li>The bytes following the frame must be either another SYNC word with the same MPEG
* version, or the start of an ID3 header.
* </ul> * </ul>
* *
* If the buffer runs out of data for any check, optimistically skip that check, because * With the exception of the first check, if there is insufficient data in the buffer then checks
* AdtsReader consumes each buffer as a whole. We will still run a header validity check later. * are optimistically skipped and {@code true} is returned.
*
* @param pesBuffer The buffer containing at data to check.
* @param syncPositionCandidate The candidate SYNC word position. May be -1 if the first byte of
* the candidate was the last byte of the previously consumed buffer.
* @return True if all checks were passed or skipped, indicating the position is likely to be the
* position of a real SYNC word. False otherwise.
*/ */
private boolean checkSyncPositionValid(ParsableByteArray pesBuffer, int syncPositionCandidate) { private boolean checkSyncPositionValid(ParsableByteArray pesBuffer, int syncPositionCandidate) {
// The SYNC word contains 2 bytes, and the first byte may be in the previously consumed buffer.
// Hence the second byte of the SYNC word may be byte 0 of this buffer, and
// syncPositionCandidate (which indicates position of the first byte of the SYNC word) may be
// -1.
// Since the first byte of the SYNC word is always FF, which does not contain any informational
// bits, we set the byte position to be the second byte in the SYNC word to ensure it's always
// within this buffer.
pesBuffer.setPosition(syncPositionCandidate + 1); pesBuffer.setPosition(syncPositionCandidate + 1);
if (!tryRead(pesBuffer, adtsScratch.data, 1)) { if (!tryRead(pesBuffer, adtsScratch.data, 1)) {
return false; return false;
} }
// The MPEG version of this frame must match the previously detected version.
adtsScratch.setPosition(4); adtsScratch.setPosition(4);
int currentFrameVersion = adtsScratch.readBits(1); int currentFrameVersion = adtsScratch.readBits(1);
if (firstFrameVersion != VERSION_UNSET && currentFrameVersion != firstFrameVersion) { if (firstFrameVersion != VERSION_UNSET && currentFrameVersion != firstFrameVersion) {
return false; return false;
} }
// The sample rate index of this frame must match the previously detected sample rate index.
if (firstFrameSampleRateIndex != C.INDEX_UNSET) { if (firstFrameSampleRateIndex != C.INDEX_UNSET) {
if (!tryRead(pesBuffer, adtsScratch.data, 1)) { if (!tryRead(pesBuffer, adtsScratch.data, 1)) {
// Insufficient data for further checks.
return true; return true;
} }
adtsScratch.setPosition(2); adtsScratch.setPosition(2);
...@@ -391,24 +393,50 @@ public final class AdtsReader implements ElementaryStreamReader { ...@@ -391,24 +393,50 @@ public final class AdtsReader implements ElementaryStreamReader {
pesBuffer.setPosition(syncPositionCandidate + 2); pesBuffer.setPosition(syncPositionCandidate + 2);
} }
// Optionally check the byte after this frame matches SYNC word. // The frame size must be at least 7 bytes.
if (!tryRead(pesBuffer, adtsScratch.data, 4)) { if (!tryRead(pesBuffer, adtsScratch.data, 4)) {
// Insufficient data for further checks.
return true; return true;
} }
adtsScratch.setPosition(14); adtsScratch.setPosition(14);
int frameSize = adtsScratch.readBits(13); int frameSize = adtsScratch.readBits(13);
if (frameSize <= 6) { if (frameSize < 7) {
// Not a frame.
return false; return false;
} }
// The bytes following the frame must be either another SYNC word with the same MPEG version, or
// the start of an ID3 header.
byte[] data = pesBuffer.data;
int dataLimit = pesBuffer.limit();
int nextSyncPosition = syncPositionCandidate + frameSize; int nextSyncPosition = syncPositionCandidate + frameSize;
if (nextSyncPosition + 1 >= pesBuffer.limit()) { if (nextSyncPosition >= dataLimit) {
// Insufficient data for further checks.
return true; return true;
} }
return (isAdtsSyncBytes(pesBuffer.data[nextSyncPosition], pesBuffer.data[nextSyncPosition + 1]) if (data[nextSyncPosition] == (byte) 0xFF) {
&& (firstFrameVersion == VERSION_UNSET if (nextSyncPosition + 1 == dataLimit) {
|| ((pesBuffer.data[nextSyncPosition + 1] & 0x8) >> 3) == currentFrameVersion)); // Insufficient data for further checks.
return true;
}
return isAdtsSyncBytes((byte) 0xFF, data[nextSyncPosition + 1])
&& ((data[nextSyncPosition + 1] & 0x8) >> 3) == currentFrameVersion;
} else {
if (data[nextSyncPosition] != 'I') {
return false;
}
if (nextSyncPosition + 1 == dataLimit) {
// Insufficient data for further checks.
return true;
}
if (data[nextSyncPosition + 1] != 'D') {
return false;
}
if (nextSyncPosition + 2 == dataLimit) {
// Insufficient data for further checks.
return true;
}
return data[nextSyncPosition + 2] == '3';
}
} }
private boolean isAdtsSyncBytes(byte firstByte, byte secondByte) { private boolean isAdtsSyncBytes(byte firstByte, byte secondByte) {
......
...@@ -259,14 +259,14 @@ public final class WavExtractor implements Extractor { ...@@ -259,14 +259,14 @@ public final class WavExtractor implements Extractor {
public boolean sampleData(ExtractorInput input, long bytesLeft) public boolean sampleData(ExtractorInput input, long bytesLeft)
throws IOException, InterruptedException { throws IOException, InterruptedException {
// Write sample data until we've reached the target sample size, or the end of the data. // Write sample data until we've reached the target sample size, or the end of the data.
boolean endOfSampleData = bytesLeft == 0; while (bytesLeft > 0 && pendingOutputBytes < targetSampleSizeBytes) {
while (!endOfSampleData && pendingOutputBytes < targetSampleSizeBytes) {
int bytesToRead = (int) Math.min(targetSampleSizeBytes - pendingOutputBytes, bytesLeft); int bytesToRead = (int) Math.min(targetSampleSizeBytes - pendingOutputBytes, bytesLeft);
int bytesAppended = trackOutput.sampleData(input, bytesToRead, true); int bytesAppended = trackOutput.sampleData(input, bytesToRead, true);
if (bytesAppended == RESULT_END_OF_INPUT) { if (bytesAppended == RESULT_END_OF_INPUT) {
endOfSampleData = true; bytesLeft = 0;
} else { } else {
pendingOutputBytes += bytesAppended; pendingOutputBytes += bytesAppended;
bytesLeft -= bytesAppended;
} }
} }
...@@ -288,7 +288,7 @@ public final class WavExtractor implements Extractor { ...@@ -288,7 +288,7 @@ public final class WavExtractor implements Extractor {
pendingOutputBytes = offset; pendingOutputBytes = offset;
} }
return endOfSampleData; return bytesLeft <= 0;
} }
} }
......
...@@ -344,6 +344,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -344,6 +344,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
private boolean codecNeedsReconfigureWorkaround; private boolean codecNeedsReconfigureWorkaround;
private boolean codecNeedsDiscardToSpsWorkaround; private boolean codecNeedsDiscardToSpsWorkaround;
private boolean codecNeedsFlushWorkaround; private boolean codecNeedsFlushWorkaround;
private boolean codecNeedsSosFlushWorkaround;
private boolean codecNeedsEosFlushWorkaround; private boolean codecNeedsEosFlushWorkaround;
private boolean codecNeedsEosOutputExceptionWorkaround; private boolean codecNeedsEosOutputExceptionWorkaround;
private boolean codecNeedsMonoChannelCountWorkaround; private boolean codecNeedsMonoChannelCountWorkaround;
...@@ -364,6 +365,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -364,6 +365,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
@DrainAction private int codecDrainAction; @DrainAction private int codecDrainAction;
private boolean codecReceivedBuffers; private boolean codecReceivedBuffers;
private boolean codecReceivedEos; private boolean codecReceivedEos;
private boolean codecHasOutputMediaFormat;
private long largestQueuedPresentationTimeUs; private long largestQueuedPresentationTimeUs;
private long lastBufferInStreamPresentationTimeUs; private long lastBufferInStreamPresentationTimeUs;
private boolean inputStreamEnded; private boolean inputStreamEnded;
...@@ -652,6 +654,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -652,6 +654,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
availableCodecInfos = null; availableCodecInfos = null;
codecInfo = null; codecInfo = null;
codecFormat = null; codecFormat = null;
codecHasOutputMediaFormat = false;
resetInputBuffer(); resetInputBuffer();
resetOutputBuffer(); resetOutputBuffer();
resetCodecBuffers(); resetCodecBuffers();
...@@ -765,6 +768,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -765,6 +768,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
} }
if (codecDrainAction == DRAIN_ACTION_REINITIALIZE if (codecDrainAction == DRAIN_ACTION_REINITIALIZE
|| codecNeedsFlushWorkaround || codecNeedsFlushWorkaround
|| (codecNeedsSosFlushWorkaround && !codecHasOutputMediaFormat)
|| (codecNeedsEosFlushWorkaround && codecReceivedEos)) { || (codecNeedsEosFlushWorkaround && codecReceivedEos)) {
releaseCodec(); releaseCodec();
return true; return true;
...@@ -944,6 +948,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -944,6 +948,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
codecNeedsReconfigureWorkaround = codecNeedsReconfigureWorkaround(codecName); codecNeedsReconfigureWorkaround = codecNeedsReconfigureWorkaround(codecName);
codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, codecFormat); codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, codecFormat);
codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName); codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName);
codecNeedsSosFlushWorkaround = codecNeedsSosFlushWorkaround(codecName);
codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName); codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName);
codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName); codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName);
codecNeedsMonoChannelCountWorkaround = codecNeedsMonoChannelCountWorkaround =
...@@ -1610,6 +1615,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -1610,6 +1615,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
/** Processes a new output {@link MediaFormat}. */ /** Processes a new output {@link MediaFormat}. */
private void processOutputFormat() throws ExoPlaybackException { private void processOutputFormat() throws ExoPlaybackException {
codecHasOutputMediaFormat = true;
MediaFormat mediaFormat = codec.getOutputFormat(); MediaFormat mediaFormat = codec.getOutputFormat();
if (codecAdaptationWorkaroundMode != ADAPTATION_WORKAROUND_MODE_NEVER if (codecAdaptationWorkaroundMode != ADAPTATION_WORKAROUND_MODE_NEVER
&& mediaFormat.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT && mediaFormat.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT
...@@ -1989,4 +1995,20 @@ public abstract class MediaCodecRenderer extends BaseRenderer { ...@@ -1989,4 +1995,20 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
&& "OMX.MTK.AUDIO.DECODER.MP3".equals(name); && "OMX.MTK.AUDIO.DECODER.MP3".equals(name);
} }
/**
* Returns whether the decoder is known to behave incorrectly if flushed prior to having output a
* {@link MediaFormat}.
*
* <p>If true is returned, the renderer will work around the issue by instantiating a new decoder
* when this case occurs.
*
* <p>See [Internal: b/141097367].
*
* @param name The name of the decoder.
* @return True if the decoder is known to behave incorrectly if flushed prior to having output a
* {@link MediaFormat}. False otherwise.
*/
private static boolean codecNeedsSosFlushWorkaround(String name) {
return Util.SDK_INT == 29 && "c2.android.aac.decoder".equals(name);
}
} }
...@@ -25,6 +25,7 @@ import android.util.Pair; ...@@ -25,6 +25,7 @@ import android.util.Pair;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import androidx.annotation.CheckResult; import androidx.annotation.CheckResult;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
...@@ -289,9 +290,16 @@ public final class MediaCodecUtil { ...@@ -289,9 +290,16 @@ public final class MediaCodecUtil {
// Note: MediaCodecList is sorted by the framework such that the best decoders come first. // Note: MediaCodecList is sorted by the framework such that the best decoders come first.
for (int i = 0; i < numberOfCodecs; i++) { for (int i = 0; i < numberOfCodecs; i++) {
android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i); android.media.MediaCodecInfo codecInfo = mediaCodecList.getCodecInfoAt(i);
if (isAlias(codecInfo)) {
// Skip aliases of other codecs, since they will also be listed under their canonical
// names.
continue;
}
String name = codecInfo.getName(); String name = codecInfo.getName();
@Nullable if (!isCodecUsableDecoder(codecInfo, name, secureDecodersExplicit, mimeType)) {
String codecMimeType = getCodecMimeType(codecInfo, name, secureDecodersExplicit, mimeType); continue;
}
@Nullable String codecMimeType = getCodecMimeType(codecInfo, name, mimeType);
if (codecMimeType == null) { if (codecMimeType == null) {
continue; continue;
} }
...@@ -373,7 +381,6 @@ public final class MediaCodecUtil { ...@@ -373,7 +381,6 @@ public final class MediaCodecUtil {
* *
* @param info The codec information. * @param info The codec information.
* @param name The name of the codec * @param name The name of the codec
* @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present.
* @param mimeType The MIME type. * @param mimeType The MIME type.
* @return The codec's supported MIME type for media of type {@code mimeType}, or {@code null} if * @return The codec's supported MIME type for media of type {@code mimeType}, or {@code null} if
* the codec can't be used. If non-null, the returned type will be equal to {@code mimeType} * the codec can't be used. If non-null, the returned type will be equal to {@code mimeType}
...@@ -383,12 +390,7 @@ public final class MediaCodecUtil { ...@@ -383,12 +390,7 @@ public final class MediaCodecUtil {
private static String getCodecMimeType( private static String getCodecMimeType(
android.media.MediaCodecInfo info, android.media.MediaCodecInfo info,
String name, String name,
boolean secureDecodersExplicit,
String mimeType) { String mimeType) {
if (!isCodecUsableDecoder(info, name, secureDecodersExplicit, mimeType)) {
return null;
}
String[] supportedTypes = info.getSupportedTypes(); String[] supportedTypes = info.getSupportedTypes();
for (String supportedType : supportedTypes) { for (String supportedType : supportedTypes) {
if (supportedType.equalsIgnoreCase(mimeType)) { if (supportedType.equalsIgnoreCase(mimeType)) {
...@@ -591,6 +593,15 @@ public final class MediaCodecUtil { ...@@ -591,6 +593,15 @@ public final class MediaCodecUtil {
} }
} }
private static boolean isAlias(android.media.MediaCodecInfo info) {
return Util.SDK_INT >= 29 && isAliasV29(info);
}
@RequiresApi(29)
private static boolean isAliasV29(android.media.MediaCodecInfo info) {
return info.isAlias();
}
/** /**
* The result of {@link android.media.MediaCodecInfo#isHardwareAccelerated()} for API levels 29+, * The result of {@link android.media.MediaCodecInfo#isHardwareAccelerated()} for API levels 29+,
* or a best-effort approximation for lower levels. * or a best-effort approximation for lower levels.
......
...@@ -245,8 +245,8 @@ public final class DownloadHelper { ...@@ -245,8 +245,8 @@ public final class DownloadHelper {
* @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest.
* @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are
* selected. * selected.
* @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which
* {@code renderersFactory}. * tracks can be selected.
* @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
* downloading. * downloading.
* @return A {@link DownloadHelper} for DASH streams. * @return A {@link DownloadHelper} for DASH streams.
...@@ -315,8 +315,8 @@ public final class DownloadHelper { ...@@ -315,8 +315,8 @@ public final class DownloadHelper {
* @param dataSourceFactory A {@link DataSource.Factory} used to load the playlist. * @param dataSourceFactory A {@link DataSource.Factory} used to load the playlist.
* @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are
* selected. * selected.
* @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which
* {@code renderersFactory}. * tracks can be selected.
* @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
* downloading. * downloading.
* @return A {@link DownloadHelper} for HLS streams. * @return A {@link DownloadHelper} for HLS streams.
...@@ -385,8 +385,8 @@ public final class DownloadHelper { ...@@ -385,8 +385,8 @@ public final class DownloadHelper {
* @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest.
* @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are
* selected. * selected.
* @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which
* {@code renderersFactory}. * tracks can be selected.
* @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for
* downloading. * downloading.
* @return A {@link DownloadHelper} for SmoothStreaming streams. * @return A {@link DownloadHelper} for SmoothStreaming streams.
...@@ -414,27 +414,27 @@ public final class DownloadHelper { ...@@ -414,27 +414,27 @@ public final class DownloadHelper {
/** /**
* Equivalent to {@link #createMediaSource(DownloadRequest, Factory, DrmSessionManager) * Equivalent to {@link #createMediaSource(DownloadRequest, Factory, DrmSessionManager)
* createMediaSource(downloadRequest, dataSourceFactory, * createMediaSource(downloadRequest, dataSourceFactory, null)}.
* DrmSessionManager.getDummyDrmSessionManager())}.
*/ */
public static MediaSource createMediaSource( public static MediaSource createMediaSource(
DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory) { DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory) {
return createMediaSource( return createMediaSource(downloadRequest, dataSourceFactory, /* drmSessionManager= */ null);
downloadRequest, dataSourceFactory, DrmSessionManager.getDummyDrmSessionManager());
} }
/** /**
* Utility method to create a MediaSource which only contains the tracks defined in {@code * Utility method to create a {@link MediaSource} that only exposes the tracks defined in {@code
* downloadRequest}. * downloadRequest}.
* *
* @param downloadRequest A {@link DownloadRequest}. * @param downloadRequest A {@link DownloadRequest}.
* @param dataSourceFactory A factory for {@link DataSource}s to read the media. * @param dataSourceFactory A factory for {@link DataSource}s to read the media.
* @return A MediaSource which only contains the tracks defined in {@code downloadRequest}. * @param drmSessionManager An optional {@link DrmSessionManager} to be passed to the {@link
* MediaSource}.
* @return A {@link MediaSource} that only exposes the tracks defined in {@code downloadRequest}.
*/ */
public static MediaSource createMediaSource( public static MediaSource createMediaSource(
DownloadRequest downloadRequest, DownloadRequest downloadRequest,
DataSource.Factory dataSourceFactory, DataSource.Factory dataSourceFactory,
DrmSessionManager<?> drmSessionManager) { @Nullable DrmSessionManager<?> drmSessionManager) {
@Nullable Constructor<? extends MediaSourceFactory> constructor; @Nullable Constructor<? extends MediaSourceFactory> constructor;
switch (downloadRequest.type) { switch (downloadRequest.type) {
case DownloadRequest.TYPE_DASH: case DownloadRequest.TYPE_DASH:
......
...@@ -595,7 +595,7 @@ public abstract class DownloadService extends Service { ...@@ -595,7 +595,7 @@ public abstract class DownloadService extends Service {
} }
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
lastStartId = startId; lastStartId = startId;
taskRemoved = false; taskRemoved = false;
@Nullable String intentAction = null; @Nullable String intentAction = null;
...@@ -617,7 +617,9 @@ public abstract class DownloadService extends Service { ...@@ -617,7 +617,9 @@ public abstract class DownloadService extends Service {
// Do nothing. // Do nothing.
break; break;
case ACTION_ADD_DOWNLOAD: case ACTION_ADD_DOWNLOAD:
@Nullable DownloadRequest downloadRequest = intent.getParcelableExtra(KEY_DOWNLOAD_REQUEST); @Nullable
DownloadRequest downloadRequest =
Assertions.checkNotNull(intent).getParcelableExtra(KEY_DOWNLOAD_REQUEST);
if (downloadRequest == null) { if (downloadRequest == null) {
Log.e(TAG, "Ignored ADD_DOWNLOAD: Missing " + KEY_DOWNLOAD_REQUEST + " extra"); Log.e(TAG, "Ignored ADD_DOWNLOAD: Missing " + KEY_DOWNLOAD_REQUEST + " extra");
} else { } else {
...@@ -642,7 +644,7 @@ public abstract class DownloadService extends Service { ...@@ -642,7 +644,7 @@ public abstract class DownloadService extends Service {
downloadManager.pauseDownloads(); downloadManager.pauseDownloads();
break; break;
case ACTION_SET_STOP_REASON: case ACTION_SET_STOP_REASON:
if (!intent.hasExtra(KEY_STOP_REASON)) { if (!Assertions.checkNotNull(intent).hasExtra(KEY_STOP_REASON)) {
Log.e(TAG, "Ignored SET_STOP_REASON: Missing " + KEY_STOP_REASON + " extra"); Log.e(TAG, "Ignored SET_STOP_REASON: Missing " + KEY_STOP_REASON + " extra");
} else { } else {
int stopReason = intent.getIntExtra(KEY_STOP_REASON, /* defaultValue= */ 0); int stopReason = intent.getIntExtra(KEY_STOP_REASON, /* defaultValue= */ 0);
...@@ -650,7 +652,9 @@ public abstract class DownloadService extends Service { ...@@ -650,7 +652,9 @@ public abstract class DownloadService extends Service {
} }
break; break;
case ACTION_SET_REQUIREMENTS: case ACTION_SET_REQUIREMENTS:
@Nullable Requirements requirements = intent.getParcelableExtra(KEY_REQUIREMENTS); @Nullable
Requirements requirements =
Assertions.checkNotNull(intent).getParcelableExtra(KEY_REQUIREMENTS);
if (requirements == null) { if (requirements == null) {
Log.e(TAG, "Ignored SET_REQUIREMENTS: Missing " + KEY_REQUIREMENTS + " extra"); Log.e(TAG, "Ignored SET_REQUIREMENTS: Missing " + KEY_REQUIREMENTS + " extra");
} else { } else {
......
...@@ -324,7 +324,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb ...@@ -324,7 +324,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb
if (endUs != C.TIME_END_OF_SOURCE if (endUs != C.TIME_END_OF_SOURCE
&& ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs) && ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs)
|| (result == C.RESULT_NOTHING_READ || (result == C.RESULT_NOTHING_READ
&& getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) { && getBufferedPositionUs() == C.TIME_END_OF_SOURCE
&& !buffer.waitingForKeys))) {
buffer.clear(); buffer.clear();
buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
sentEos = true; sentEos = true;
......
...@@ -539,7 +539,7 @@ public class SampleQueue implements TrackOutput { ...@@ -539,7 +539,7 @@ public class SampleQueue implements TrackOutput {
boolean loadingFinished, boolean loadingFinished,
long decodeOnlyUntilUs, long decodeOnlyUntilUs,
SampleExtrasHolder extrasHolder) { SampleExtrasHolder extrasHolder) {
buffer.waitingForKeys = false;
// This is a temporary fix for https://github.com/google/ExoPlayer/issues/6155. // This is a temporary fix for https://github.com/google/ExoPlayer/issues/6155.
// TODO: Remove it and replace it with a fix that discards samples when writing to the queue. // TODO: Remove it and replace it with a fix that discards samples when writing to the queue.
boolean hasNextSample; boolean hasNextSample;
...@@ -573,6 +573,7 @@ public class SampleQueue implements TrackOutput { ...@@ -573,6 +573,7 @@ public class SampleQueue implements TrackOutput {
} }
if (!mayReadSample(relativeReadIndex)) { if (!mayReadSample(relativeReadIndex)) {
buffer.waitingForKeys = true;
return C.RESULT_NOTHING_READ; return C.RESULT_NOTHING_READ;
} }
......
...@@ -23,11 +23,11 @@ import androidx.annotation.IntDef; ...@@ -23,11 +23,11 @@ import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.BaseRenderer;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
...@@ -45,6 +45,8 @@ import java.util.List; ...@@ -45,6 +45,8 @@ import java.util.List;
*/ */
public final class TextRenderer extends BaseRenderer implements Callback { public final class TextRenderer extends BaseRenderer implements Callback {
private static final String TAG = "TextRenderer";
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@IntDef({ @IntDef({
...@@ -143,19 +145,13 @@ public final class TextRenderer extends BaseRenderer implements Callback { ...@@ -143,19 +145,13 @@ public final class TextRenderer extends BaseRenderer implements Callback {
@Override @Override
protected void onPositionReset(long positionUs, boolean joining) { protected void onPositionReset(long positionUs, boolean joining) {
clearOutput();
inputStreamEnded = false; inputStreamEnded = false;
outputStreamEnded = false; outputStreamEnded = false;
if (decoderReplacementState != REPLACEMENT_STATE_NONE) { resetOutputAndDecoder();
replaceDecoder();
} else {
releaseBuffers();
decoder.flush();
}
} }
@Override @Override
public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { public void render(long positionUs, long elapsedRealtimeUs) {
if (outputStreamEnded) { if (outputStreamEnded) {
return; return;
} }
...@@ -165,7 +161,8 @@ public final class TextRenderer extends BaseRenderer implements Callback { ...@@ -165,7 +161,8 @@ public final class TextRenderer extends BaseRenderer implements Callback {
try { try {
nextSubtitle = decoder.dequeueOutputBuffer(); nextSubtitle = decoder.dequeueOutputBuffer();
} catch (SubtitleDecoderException e) { } catch (SubtitleDecoderException e) {
throw createRendererException(e, streamFormat); handleDecoderError(e);
return;
} }
} }
...@@ -247,7 +244,8 @@ public final class TextRenderer extends BaseRenderer implements Callback { ...@@ -247,7 +244,8 @@ public final class TextRenderer extends BaseRenderer implements Callback {
} }
} }
} catch (SubtitleDecoderException e) { } catch (SubtitleDecoderException e) {
throw createRendererException(e, streamFormat); handleDecoderError(e);
return;
} }
} }
...@@ -329,4 +327,24 @@ public final class TextRenderer extends BaseRenderer implements Callback { ...@@ -329,4 +327,24 @@ public final class TextRenderer extends BaseRenderer implements Callback {
output.onCues(cues); output.onCues(cues);
} }
/**
* Called when {@link #decoder} throws an exception, so it can be logged and playback can
* continue.
*
* <p>Logs {@code e} and resets state to allow decoding the next sample.
*/
private void handleDecoderError(SubtitleDecoderException e) {
Log.e(TAG, "Subtitle decoding failed. streamFormat=" + streamFormat, e);
resetOutputAndDecoder();
}
private void resetOutputAndDecoder() {
clearOutput();
if (decoderReplacementState != REPLACEMENT_STATE_NONE) {
replaceDecoder();
} else {
releaseBuffers();
decoder.flush();
}
}
} }
...@@ -41,10 +41,12 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { ...@@ -41,10 +41,12 @@ public final class SubripDecoder extends SimpleSubtitleDecoder {
private static final String TAG = "SubripDecoder"; private static final String TAG = "SubripDecoder";
private static final String SUBRIP_TIMECODE = "(?:(\\d+):)?(\\d+):(\\d+),(\\d+)"; // Some SRT files don't include hours or milliseconds in the timecode, so we use optional groups.
private static final String SUBRIP_TIMECODE = "(?:(\\d+):)?(\\d+):(\\d+)(?:,(\\d+))?";
private static final Pattern SUBRIP_TIMING_LINE = private static final Pattern SUBRIP_TIMING_LINE =
Pattern.compile("\\s*(" + SUBRIP_TIMECODE + ")\\s*-->\\s*(" + SUBRIP_TIMECODE + ")\\s*"); Pattern.compile("\\s*(" + SUBRIP_TIMECODE + ")\\s*-->\\s*(" + SUBRIP_TIMECODE + ")\\s*");
// NOTE: Android Studio's suggestion to simplify '\\}' is incorrect [internal: b/144480183].
private static final Pattern SUBRIP_TAG_PATTERN = Pattern.compile("\\{\\\\.*?\\}"); private static final Pattern SUBRIP_TAG_PATTERN = Pattern.compile("\\{\\\\.*?\\}");
private static final String SUBRIP_ALIGNMENT_TAG = "\\{\\\\an[1-9]\\}"; private static final String SUBRIP_ALIGNMENT_TAG = "\\{\\\\an[1-9]\\}";
...@@ -229,10 +231,14 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { ...@@ -229,10 +231,14 @@ public final class SubripDecoder extends SimpleSubtitleDecoder {
} }
private static long parseTimecode(Matcher matcher, int groupOffset) { private static long parseTimecode(Matcher matcher, int groupOffset) {
long timestampMs = Long.parseLong(matcher.group(groupOffset + 1)) * 60 * 60 * 1000; @Nullable String hours = matcher.group(groupOffset + 1);
long timestampMs = hours != null ? Long.parseLong(hours) * 60 * 60 * 1000 : 0;
timestampMs += Long.parseLong(matcher.group(groupOffset + 2)) * 60 * 1000; timestampMs += Long.parseLong(matcher.group(groupOffset + 2)) * 60 * 1000;
timestampMs += Long.parseLong(matcher.group(groupOffset + 3)) * 1000; timestampMs += Long.parseLong(matcher.group(groupOffset + 3)) * 1000;
timestampMs += Long.parseLong(matcher.group(groupOffset + 4)); @Nullable String millis = matcher.group(groupOffset + 4);
if (millis != null) {
timestampMs += Long.parseLong(millis);
}
return timestampMs * 1000; return timestampMs * 1000;
} }
......
...@@ -279,8 +279,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou ...@@ -279,8 +279,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
try { try {
connection = makeConnection(dataSpec); connection = makeConnection(dataSpec);
} catch (IOException e) { } catch (IOException e) {
throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, throw new HttpDataSourceException(
dataSpec, HttpDataSourceException.TYPE_OPEN); "Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN);
} }
String responseMessage; String responseMessage;
...@@ -289,8 +289,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou ...@@ -289,8 +289,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
responseMessage = connection.getResponseMessage(); responseMessage = connection.getResponseMessage();
} catch (IOException e) { } catch (IOException e) {
closeConnectionQuietly(); closeConnectionQuietly();
throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, throw new HttpDataSourceException(
dataSpec, HttpDataSourceException.TYPE_OPEN); "Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN);
} }
// Check for a valid response code. // Check for a valid response code.
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.util; package com.google.android.exoplayer2.util;
import android.os.SystemClock; import android.os.SystemClock;
import android.text.TextUtils;
import android.view.Surface; import android.view.Surface;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
...@@ -198,7 +199,7 @@ public class EventLogger implements AnalyticsListener { ...@@ -198,7 +199,7 @@ public class EventLogger implements AnalyticsListener {
logd(eventTime, "tracks", "[]"); logd(eventTime, "tracks", "[]");
return; return;
} }
logd("tracks [" + getEventTimeString(eventTime) + ", "); logd("tracks [" + getEventTimeString(eventTime));
// Log tracks associated to renderers. // Log tracks associated to renderers.
int rendererCount = mappedTrackInfo.getRendererCount(); int rendererCount = mappedTrackInfo.getRendererCount();
for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) {
...@@ -282,7 +283,7 @@ public class EventLogger implements AnalyticsListener { ...@@ -282,7 +283,7 @@ public class EventLogger implements AnalyticsListener {
@Override @Override
public void onMetadata(EventTime eventTime, Metadata metadata) { public void onMetadata(EventTime eventTime, Metadata metadata) {
logd("metadata [" + getEventTimeString(eventTime) + ", "); logd("metadata [" + getEventTimeString(eventTime));
printMetadata(metadata, " "); printMetadata(metadata, " ");
logd("]"); logd("]");
} }
...@@ -469,27 +470,26 @@ public class EventLogger implements AnalyticsListener { ...@@ -469,27 +470,26 @@ public class EventLogger implements AnalyticsListener {
} }
/** /**
* Logs an error message and exception. * Logs an error message.
* *
* @param msg The message to log. * @param msg The message to log.
* @param tr The exception to log.
*/ */
protected void loge(String msg, @Nullable Throwable tr) { protected void loge(String msg) {
Log.e(tag, msg, tr); Log.e(tag, msg);
} }
// Internal methods // Internal methods
private void logd(EventTime eventTime, String eventName) { private void logd(EventTime eventTime, String eventName) {
logd(getEventString(eventTime, eventName)); logd(getEventString(eventTime, eventName, /* eventDescription= */ null, /* throwable= */ null));
} }
private void logd(EventTime eventTime, String eventName, String eventDescription) { private void logd(EventTime eventTime, String eventName, String eventDescription) {
logd(getEventString(eventTime, eventName, eventDescription)); logd(getEventString(eventTime, eventName, eventDescription, /* throwable= */ null));
} }
private void loge(EventTime eventTime, String eventName, @Nullable Throwable throwable) { private void loge(EventTime eventTime, String eventName, @Nullable Throwable throwable) {
loge(getEventString(eventTime, eventName), throwable); loge(getEventString(eventTime, eventName, /* eventDescription= */ null, throwable));
} }
private void loge( private void loge(
...@@ -497,7 +497,7 @@ public class EventLogger implements AnalyticsListener { ...@@ -497,7 +497,7 @@ public class EventLogger implements AnalyticsListener {
String eventName, String eventName,
String eventDescription, String eventDescription,
@Nullable Throwable throwable) { @Nullable Throwable throwable) {
loge(getEventString(eventTime, eventName, eventDescription), throwable); loge(getEventString(eventTime, eventName, eventDescription, throwable));
} }
private void printInternalError(EventTime eventTime, String type, Exception e) { private void printInternalError(EventTime eventTime, String type, Exception e) {
...@@ -510,12 +510,21 @@ public class EventLogger implements AnalyticsListener { ...@@ -510,12 +510,21 @@ public class EventLogger implements AnalyticsListener {
} }
} }
private String getEventString(EventTime eventTime, String eventName) { private String getEventString(
return eventName + " [" + getEventTimeString(eventTime) + "]"; EventTime eventTime,
} String eventName,
@Nullable String eventDescription,
private String getEventString(EventTime eventTime, String eventName, String eventDescription) { @Nullable Throwable throwable) {
return eventName + " [" + getEventTimeString(eventTime) + ", " + eventDescription + "]"; String eventString = eventName + " [" + getEventTimeString(eventTime);
if (eventDescription != null) {
eventString += ", " + eventDescription;
}
@Nullable String throwableString = Log.getThrowableString(throwable);
if (!TextUtils.isEmpty(throwableString)) {
eventString += "\n " + throwableString.replace("\n", "\n ") + '\n';
}
eventString += "]";
return eventString;
} }
private String getEventTimeString(EventTime eventTime) { private String getEventTimeString(EventTime eventTime) {
......
...@@ -21,6 +21,7 @@ import androidx.annotation.Nullable; ...@@ -21,6 +21,7 @@ import androidx.annotation.Nullable;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.net.UnknownHostException;
/** Wrapper around {@link android.util.Log} which allows to set the log level. */ /** Wrapper around {@link android.util.Log} which allows to set the log level. */
public final class Log { public final class Log {
...@@ -69,7 +70,8 @@ public final class Log { ...@@ -69,7 +70,8 @@ public final class Log {
} }
/** /**
* Sets whether stack traces of {@link Throwable}s will be logged to logcat. * Sets whether stack traces of {@link Throwable}s will be logged to logcat. Stack trace logging
* is enabled by default.
* *
* @param logStackTraces Whether stack traces will be logged. * @param logStackTraces Whether stack traces will be logged.
*/ */
...@@ -86,11 +88,7 @@ public final class Log { ...@@ -86,11 +88,7 @@ public final class Log {
/** @see android.util.Log#d(String, String, Throwable) */ /** @see android.util.Log#d(String, String, Throwable) */
public static void d(String tag, String message, @Nullable Throwable throwable) { public static void d(String tag, String message, @Nullable Throwable throwable) {
if (!logStackTraces) { d(tag, appendThrowableString(message, throwable));
d(tag, appendThrowableMessage(message, throwable));
} else if (logLevel == LOG_LEVEL_ALL) {
android.util.Log.d(tag, message, throwable);
}
} }
/** @see android.util.Log#i(String, String) */ /** @see android.util.Log#i(String, String) */
...@@ -102,11 +100,7 @@ public final class Log { ...@@ -102,11 +100,7 @@ public final class Log {
/** @see android.util.Log#i(String, String, Throwable) */ /** @see android.util.Log#i(String, String, Throwable) */
public static void i(String tag, String message, @Nullable Throwable throwable) { public static void i(String tag, String message, @Nullable Throwable throwable) {
if (!logStackTraces) { i(tag, appendThrowableString(message, throwable));
i(tag, appendThrowableMessage(message, throwable));
} else if (logLevel <= LOG_LEVEL_INFO) {
android.util.Log.i(tag, message, throwable);
}
} }
/** @see android.util.Log#w(String, String) */ /** @see android.util.Log#w(String, String) */
...@@ -118,11 +112,7 @@ public final class Log { ...@@ -118,11 +112,7 @@ public final class Log {
/** @see android.util.Log#w(String, String, Throwable) */ /** @see android.util.Log#w(String, String, Throwable) */
public static void w(String tag, String message, @Nullable Throwable throwable) { public static void w(String tag, String message, @Nullable Throwable throwable) {
if (!logStackTraces) { w(tag, appendThrowableString(message, throwable));
w(tag, appendThrowableMessage(message, throwable));
} else if (logLevel <= LOG_LEVEL_WARNING) {
android.util.Log.w(tag, message, throwable);
}
} }
/** @see android.util.Log#e(String, String) */ /** @see android.util.Log#e(String, String) */
...@@ -134,18 +124,54 @@ public final class Log { ...@@ -134,18 +124,54 @@ public final class Log {
/** @see android.util.Log#e(String, String, Throwable) */ /** @see android.util.Log#e(String, String, Throwable) */
public static void e(String tag, String message, @Nullable Throwable throwable) { public static void e(String tag, String message, @Nullable Throwable throwable) {
if (!logStackTraces) { e(tag, appendThrowableString(message, throwable));
e(tag, appendThrowableMessage(message, throwable));
} else if (logLevel <= LOG_LEVEL_ERROR) {
android.util.Log.e(tag, message, throwable);
}
} }
private static String appendThrowableMessage(String message, @Nullable Throwable throwable) { /**
* Returns a string representation of a {@link Throwable} suitable for logging, taking into
* account whether {@link #setLogStackTraces(boolean)} stack trace logging} is enabled.
*
* <p>Stack trace logging may be unconditionally suppressed for some expected failure modes (e.g.,
* {@link Throwable Throwables} that are expected if the device doesn't have network connectivity)
* to avoid log spam.
*
* @param throwable The {@link Throwable}.
* @return The string representation of the {@link Throwable}.
*/
@Nullable
public static String getThrowableString(@Nullable Throwable throwable) {
if (throwable == null) { if (throwable == null) {
return message; return null;
} else if (isCausedByUnknownHostException(throwable)) {
// UnknownHostException implies the device doesn't have network connectivity.
// UnknownHostException.getMessage() may return a string that's more verbose than desired for
// logging an expected failure mode. Conversely, android.util.Log.getStackTraceString has
// special handling to return the empty string, which can result in logging that doesn't
// indicate the failure mode at all. Hence we special case this exception to always return a
// concise but useful message.
return "UnknownHostException (no network)";
} else if (!logStackTraces) {
return throwable.getMessage();
} else {
return android.util.Log.getStackTraceString(throwable).trim().replace("\t", " ");
}
}
private static String appendThrowableString(String message, @Nullable Throwable throwable) {
@Nullable String throwableString = getThrowableString(throwable);
if (!TextUtils.isEmpty(throwableString)) {
message += "\n " + throwableString.replace("\n", "\n ") + '\n';
}
return message;
}
private static boolean isCausedByUnknownHostException(@Nullable Throwable throwable) {
while (throwable != null) {
if (throwable instanceof UnknownHostException) {
return true;
}
throwable = throwable.getCause();
} }
String throwableMessage = throwable.getMessage(); return false;
return TextUtils.isEmpty(throwableMessage) ? message : message + " - " + throwableMessage;
} }
} }
...@@ -122,22 +122,22 @@ public final class MimeTypes { ...@@ -122,22 +122,22 @@ public final class MimeTypes {
customMimeTypes.add(customMimeType); customMimeTypes.add(customMimeType);
} }
/** Returns whether the given string is an audio mime type. */ /** Returns whether the given string is an audio MIME type. */
public static boolean isAudio(@Nullable String mimeType) { public static boolean isAudio(@Nullable String mimeType) {
return BASE_TYPE_AUDIO.equals(getTopLevelType(mimeType)); return BASE_TYPE_AUDIO.equals(getTopLevelType(mimeType));
} }
/** Returns whether the given string is a video mime type. */ /** Returns whether the given string is a video MIME type. */
public static boolean isVideo(@Nullable String mimeType) { public static boolean isVideo(@Nullable String mimeType) {
return BASE_TYPE_VIDEO.equals(getTopLevelType(mimeType)); return BASE_TYPE_VIDEO.equals(getTopLevelType(mimeType));
} }
/** Returns whether the given string is a text mime type. */ /** Returns whether the given string is a text MIME type. */
public static boolean isText(@Nullable String mimeType) { public static boolean isText(@Nullable String mimeType) {
return BASE_TYPE_TEXT.equals(getTopLevelType(mimeType)); return BASE_TYPE_TEXT.equals(getTopLevelType(mimeType));
} }
/** Returns whether the given string is an application mime type. */ /** Returns whether the given string is an application MIME type. */
public static boolean isApplication(@Nullable String mimeType) { public static boolean isApplication(@Nullable String mimeType) {
return BASE_TYPE_APPLICATION.equals(getTopLevelType(mimeType)); return BASE_TYPE_APPLICATION.equals(getTopLevelType(mimeType));
} }
...@@ -173,13 +173,14 @@ public final class MimeTypes { ...@@ -173,13 +173,14 @@ public final class MimeTypes {
* @param codecs The codecs attribute. * @param codecs The codecs attribute.
* @return The derived video mimeType, or null if it could not be derived. * @return The derived video mimeType, or null if it could not be derived.
*/ */
public static @Nullable String getVideoMediaMimeType(@Nullable String codecs) { @Nullable
public static String getVideoMediaMimeType(@Nullable String codecs) {
if (codecs == null) { if (codecs == null) {
return null; return null;
} }
String[] codecList = Util.splitCodecs(codecs); String[] codecList = Util.splitCodecs(codecs);
for (String codec : codecList) { for (String codec : codecList) {
String mimeType = getMediaMimeType(codec); @Nullable String mimeType = getMediaMimeType(codec);
if (mimeType != null && isVideo(mimeType)) { if (mimeType != null && isVideo(mimeType)) {
return mimeType; return mimeType;
} }
...@@ -193,13 +194,14 @@ public final class MimeTypes { ...@@ -193,13 +194,14 @@ public final class MimeTypes {
* @param codecs The codecs attribute. * @param codecs The codecs attribute.
* @return The derived audio mimeType, or null if it could not be derived. * @return The derived audio mimeType, or null if it could not be derived.
*/ */
public static @Nullable String getAudioMediaMimeType(@Nullable String codecs) { @Nullable
public static String getAudioMediaMimeType(@Nullable String codecs) {
if (codecs == null) { if (codecs == null) {
return null; return null;
} }
String[] codecList = Util.splitCodecs(codecs); String[] codecList = Util.splitCodecs(codecs);
for (String codec : codecList) { for (String codec : codecList) {
String mimeType = getMediaMimeType(codec); @Nullable String mimeType = getMediaMimeType(codec);
if (mimeType != null && isAudio(mimeType)) { if (mimeType != null && isAudio(mimeType)) {
return mimeType; return mimeType;
} }
...@@ -213,7 +215,8 @@ public final class MimeTypes { ...@@ -213,7 +215,8 @@ public final class MimeTypes {
* @param codec The codec identifier to derive. * @param codec The codec identifier to derive.
* @return The mimeType, or null if it could not be derived. * @return The mimeType, or null if it could not be derived.
*/ */
public static @Nullable String getMediaMimeType(@Nullable String codec) { @Nullable
public static String getMediaMimeType(@Nullable String codec) {
if (codec == null) { if (codec == null) {
return null; return null;
} }
...@@ -234,7 +237,7 @@ public final class MimeTypes { ...@@ -234,7 +237,7 @@ public final class MimeTypes {
} else if (codec.startsWith("vp8") || codec.startsWith("vp08")) { } else if (codec.startsWith("vp8") || codec.startsWith("vp08")) {
return MimeTypes.VIDEO_VP8; return MimeTypes.VIDEO_VP8;
} else if (codec.startsWith("mp4a")) { } else if (codec.startsWith("mp4a")) {
String mimeType = null; @Nullable String mimeType = null;
if (codec.startsWith("mp4a.")) { if (codec.startsWith("mp4a.")) {
String objectTypeString = codec.substring(5); // remove the 'mp4a.' prefix String objectTypeString = codec.substring(5); // remove the 'mp4a.' prefix
if (objectTypeString.length() >= 2) { if (objectTypeString.length() >= 2) {
...@@ -243,7 +246,7 @@ public final class MimeTypes { ...@@ -243,7 +246,7 @@ public final class MimeTypes {
int objectTypeInt = Integer.parseInt(objectTypeHexString, 16); int objectTypeInt = Integer.parseInt(objectTypeHexString, 16);
mimeType = getMimeTypeFromMp4ObjectType(objectTypeInt); mimeType = getMimeTypeFromMp4ObjectType(objectTypeInt);
} catch (NumberFormatException ignored) { } catch (NumberFormatException ignored) {
// ignored // Ignored.
} }
} }
} }
...@@ -266,6 +269,10 @@ public final class MimeTypes { ...@@ -266,6 +269,10 @@ public final class MimeTypes {
return MimeTypes.AUDIO_VORBIS; return MimeTypes.AUDIO_VORBIS;
} else if (codec.startsWith("flac")) { } else if (codec.startsWith("flac")) {
return MimeTypes.AUDIO_FLAC; return MimeTypes.AUDIO_FLAC;
} else if (codec.startsWith("stpp")) {
return MimeTypes.APPLICATION_TTML;
} else if (codec.startsWith("wvtt")) {
return MimeTypes.TEXT_VTT;
} else { } else {
return getCustomMimeTypeForCodec(codec); return getCustomMimeTypeForCodec(codec);
} }
...@@ -405,7 +412,8 @@ public final class MimeTypes { ...@@ -405,7 +412,8 @@ public final class MimeTypes {
* Returns the top-level type of {@code mimeType}, or null if {@code mimeType} is null or does not * Returns the top-level type of {@code mimeType}, or null if {@code mimeType} is null or does not
* contain a forward slash character ({@code '/'}). * contain a forward slash character ({@code '/'}).
*/ */
private static @Nullable String getTopLevelType(@Nullable String mimeType) { @Nullable
private static String getTopLevelType(@Nullable String mimeType) {
if (mimeType == null) { if (mimeType == null) {
return null; return null;
} }
...@@ -416,7 +424,8 @@ public final class MimeTypes { ...@@ -416,7 +424,8 @@ public final class MimeTypes {
return mimeType.substring(0, indexOfSlash); return mimeType.substring(0, indexOfSlash);
} }
private static @Nullable String getCustomMimeTypeForCodec(String codec) { @Nullable
private static String getCustomMimeTypeForCodec(String codec) {
int customMimeTypeCount = customMimeTypes.size(); int customMimeTypeCount = customMimeTypes.size();
for (int i = 0; i < customMimeTypeCount; i++) { for (int i = 0; i < customMimeTypeCount; i++) {
CustomMimeType customMimeType = customMimeTypes.get(i); CustomMimeType customMimeType = customMimeTypes.get(i);
......
...@@ -1360,13 +1360,15 @@ public final class Util { ...@@ -1360,13 +1360,15 @@ public final class Util {
} }
/** /**
* Returns whether {@code encoding} is high resolution (&gt; 16-bit) integer PCM. * Returns whether {@code encoding} is high resolution (&gt; 16-bit) PCM.
* *
* @param encoding The encoding of the audio data. * @param encoding The encoding of the audio data.
* @return Whether the encoding is high resolution integer PCM. * @return Whether the encoding is high resolution PCM.
*/ */
public static boolean isEncodingHighResolutionIntegerPcm(@C.PcmEncoding int encoding) { public static boolean isEncodingHighResolutionPcm(@C.PcmEncoding int encoding) {
return encoding == C.ENCODING_PCM_24BIT || encoding == C.ENCODING_PCM_32BIT; return encoding == C.ENCODING_PCM_24BIT
|| encoding == C.ENCODING_PCM_32BIT
|| encoding == C.ENCODING_PCM_FLOAT;
} }
/** /**
......
...@@ -1000,6 +1000,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -1000,6 +1000,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
processOutputFormat(getCodec(), format.width, format.height); processOutputFormat(getCodec(), format.width, format.height);
} }
maybeNotifyVideoSizeChanged(); maybeNotifyVideoSizeChanged();
decoderCounters.renderedOutputBufferCount++;
maybeNotifyRenderedFirstFrame(); maybeNotifyRenderedFirstFrame();
onProcessedOutputBuffer(presentationTimeUs); onProcessedOutputBuffer(presentationTimeUs);
} }
......
...@@ -8,5 +8,5 @@ This is the second subtitle. ...@@ -8,5 +8,5 @@ This is the second subtitle.
Second subtitle with second line. Second subtitle with second line.
3 3
00:00:04,567 --> 00:00:08,901 02:00:04,567 --> 02:00:08,901
This is the third subtitle. This is the third subtitle.
...@@ -9,5 +9,5 @@ This is the second subtitle. ...@@ -9,5 +9,5 @@ This is the second subtitle.
Second subtitle with second line. Second subtitle with second line.
3 3
00:00:04,567 --> 00:00:08,901 02:00:04,567 --> 02:00:08,901
This is the third subtitle. This is the third subtitle.
...@@ -7,5 +7,5 @@ This is the second subtitle. ...@@ -7,5 +7,5 @@ This is the second subtitle.
Second subtitle with second line. Second subtitle with second line.
3 3
00:00:04,567 --> 00:00:08,901 02:00:04,567 --> 02:00:08,901
This is the third subtitle. This is the third subtitle.
...@@ -7,13 +7,13 @@ This is the second subtitle. ...@@ -7,13 +7,13 @@ This is the second subtitle.
Second subtitle with second line. Second subtitle with second line.
3 3
00:00:04,567 --> 00:00:08,901 02:00:04,567 --> 02:00:08,901
This is the third subtitle. This is the third subtitle.
4 4
--> 00:00:10,901 --> 02:00:10,901
This is the fourth subtitle. This is the fourth subtitle.
5 5
00:00:12,901 --> 02:00:12,901 -->
This is the fifth subtitle. This is the fifth subtitle.
...@@ -8,5 +8,5 @@ This is the second subtitle. ...@@ -8,5 +8,5 @@ This is the second subtitle.
Second subtitle with second line. Second subtitle with second line.
3 3
00:00:04,567 --> 00:00:08,901 02:00:04,567 --> 02:00:08,901
This is the third subtitle. This is the third subtitle.
1
00:00,000 --> 00:01,234
This is the first subtitle.
2
00:00:02 --> 00:00:03
This is the second subtitle.
Second subtitle with second line.
3
02:00:04,567 --> 02:00:08,901
This is the third subtitle.
...@@ -8,5 +8,5 @@ This is the second subtitle. ...@@ -8,5 +8,5 @@ This is the second subtitle.
Second subtitle with second line. Second subtitle with second line.
3 3
00:00:04,567 --> 00:00:08,901 02:00:04,567 --> 02:00:08,901
This is the third subtitle. This is the third subtitle.
/*
* Copyright 2020 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.analytics;
import static com.google.common.truth.Truth.assertThat;
import androidx.annotation.Nullable;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link PlaybackStatsListener}. */
@RunWith(AndroidJUnit4.class)
public final class PlaybackStatsListenerTest {
private static final AnalyticsListener.EventTime TEST_EVENT_TIME =
new AnalyticsListener.EventTime(
/* realtimeMs= */ 500,
Timeline.EMPTY,
/* windowIndex= */ 0,
/* mediaPeriodId= */ null,
/* eventPlaybackPositionMs= */ 0,
/* currentPlaybackPositionMs= */ 0,
/* totalBufferedDurationMs= */ 0);
@Test
public void playback_withKeepHistory_updatesStats() {
PlaybackStatsListener playbackStatsListener =
new PlaybackStatsListener(/* keepHistory= */ true, /* callback= */ null);
playbackStatsListener.onPlayerStateChanged(
TEST_EVENT_TIME, /* playWhenReady= */ true, Player.STATE_BUFFERING);
playbackStatsListener.onPlayerStateChanged(
TEST_EVENT_TIME, /* playWhenReady= */ true, Player.STATE_READY);
playbackStatsListener.onPlayerStateChanged(
TEST_EVENT_TIME, /* playWhenReady= */ true, Player.STATE_ENDED);
@Nullable PlaybackStats playbackStats = playbackStatsListener.getPlaybackStats();
assertThat(playbackStats).isNotNull();
assertThat(playbackStats.endedCount).isEqualTo(1);
}
@Test
public void playback_withoutKeepHistory_updatesStats() {
PlaybackStatsListener playbackStatsListener =
new PlaybackStatsListener(/* keepHistory= */ false, /* callback= */ null);
playbackStatsListener.onPlayerStateChanged(
TEST_EVENT_TIME, /* playWhenReady= */ true, Player.STATE_BUFFERING);
playbackStatsListener.onPlayerStateChanged(
TEST_EVENT_TIME, /* playWhenReady= */ true, Player.STATE_READY);
playbackStatsListener.onPlayerStateChanged(
TEST_EVENT_TIME, /* playWhenReady= */ true, Player.STATE_ENDED);
@Nullable PlaybackStats playbackStats = playbackStatsListener.getPlaybackStats();
assertThat(playbackStats).isNotNull();
assertThat(playbackStats.endedCount).isEqualTo(1);
}
}
...@@ -30,6 +30,11 @@ public final class AdtsExtractorTest { ...@@ -30,6 +30,11 @@ public final class AdtsExtractorTest {
} }
@Test @Test
public void testSample_with_id3() throws Exception {
ExtractorAsserts.assertBehavior(AdtsExtractor::new, "ts/sample_with_id3.adts");
}
@Test
public void testSample_withSeeking() throws Exception { public void testSample_withSeeking() throws Exception {
ExtractorAsserts.assertBehavior( ExtractorAsserts.assertBehavior(
() -> new AdtsExtractor(/* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING), () -> new AdtsExtractor(/* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING),
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.extractor.wav; package com.google.android.exoplayer2.extractor.wav;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import org.junit.Test; import org.junit.Test;
...@@ -30,7 +31,7 @@ public final class WavExtractorTest { ...@@ -30,7 +31,7 @@ public final class WavExtractorTest {
} }
@Test @Test
public void testSampleImaAdpcm() throws Exception { public void sample_imaAdpcm() throws Exception {
ExtractorAsserts.assertBehavior(WavExtractor::new, "wav/sample_ima_adpcm.wav"); ExtractorAsserts.assertBehavior(WavExtractor::new, "wav/sample_ima_adpcm.wav");
} }
} }
...@@ -372,8 +372,10 @@ public final class SampleQueueTest { ...@@ -372,8 +372,10 @@ public final class SampleQueueTest {
assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED); assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED);
assertReadNothing(/* formatRequired= */ false); assertReadNothing(/* formatRequired= */ false);
assertThat(inputBuffer.waitingForKeys).isTrue();
when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS);
assertReadEncryptedSample(/* sampleIndex= */ 0); assertReadEncryptedSample(/* sampleIndex= */ 0);
assertThat(inputBuffer.waitingForKeys).isFalse();
} }
@Test @Test
......
...@@ -39,6 +39,7 @@ public final class SubripDecoderTest { ...@@ -39,6 +39,7 @@ public final class SubripDecoderTest {
private static final String TYPICAL_NEGATIVE_TIMESTAMPS = "subrip/typical_negative_timestamps"; private static final String TYPICAL_NEGATIVE_TIMESTAMPS = "subrip/typical_negative_timestamps";
private static final String TYPICAL_UNEXPECTED_END = "subrip/typical_unexpected_end"; private static final String TYPICAL_UNEXPECTED_END = "subrip/typical_unexpected_end";
private static final String TYPICAL_WITH_TAGS = "subrip/typical_with_tags"; private static final String TYPICAL_WITH_TAGS = "subrip/typical_with_tags";
private static final String TYPICAL_NO_HOURS_AND_MILLIS = "subrip/typical_no_hours_and_millis";
@Test @Test
public void testDecodeEmpty() throws IOException { public void testDecodeEmpty() throws IOException {
...@@ -151,9 +152,14 @@ public final class SubripDecoderTest { ...@@ -151,9 +152,14 @@ public final class SubripDecoderTest {
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_WITH_TAGS); TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_WITH_TAGS);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false); Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertTypicalCue1(subtitle, 0); assertThat(subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString())
assertTypicalCue2(subtitle, 2); .isEqualTo("This is the first subtitle.");
assertTypicalCue3(subtitle, 4);
assertThat(subtitle.getCues(subtitle.getEventTime(2)).get(0).text.toString())
.isEqualTo("This is the second subtitle.\nSecond subtitle with second line.");
assertThat(subtitle.getCues(subtitle.getEventTime(4)).get(0).text.toString())
.isEqualTo("This is the third subtitle.");
assertThat(subtitle.getCues(subtitle.getEventTime(6)).get(0).text.toString()) assertThat(subtitle.getCues(subtitle.getEventTime(6)).get(0).text.toString())
.isEqualTo("This { \\an2} is not a valid tag due to the space after the opening bracket."); .isEqualTo("This { \\an2} is not a valid tag due to the space after the opening bracket.");
...@@ -172,6 +178,21 @@ public final class SubripDecoderTest { ...@@ -172,6 +178,21 @@ public final class SubripDecoderTest {
assertAlignmentCue(subtitle, 26, Cue.ANCHOR_TYPE_START, Cue.ANCHOR_TYPE_END); // {/an9} assertAlignmentCue(subtitle, 26, Cue.ANCHOR_TYPE_START, Cue.ANCHOR_TYPE_END); // {/an9}
} }
@Test
public void decodeTypicalNoHoursAndMillis() throws IOException {
SubripDecoder decoder = new SubripDecoder();
byte[] bytes =
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(), TYPICAL_NO_HOURS_AND_MILLIS);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertThat(subtitle.getEventTimeCount()).isEqualTo(6);
assertTypicalCue1(subtitle, 0);
assertThat(subtitle.getEventTime(2)).isEqualTo(2_000_000);
assertThat(subtitle.getEventTime(3)).isEqualTo(3_000_000);
assertTypicalCue3(subtitle, 4);
}
private static void assertTypicalCue1(Subtitle subtitle, int eventIndex) { private static void assertTypicalCue1(Subtitle subtitle, int eventIndex) {
assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0); assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0);
assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString())
...@@ -187,10 +208,12 @@ public final class SubripDecoderTest { ...@@ -187,10 +208,12 @@ public final class SubripDecoderTest {
} }
private static void assertTypicalCue3(Subtitle subtitle, int eventIndex) { private static void assertTypicalCue3(Subtitle subtitle, int eventIndex) {
assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(4567000); long expectedStartTimeUs = (((2L * 60L * 60L) + 4L) * 1000L + 567L) * 1000L;
assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(expectedStartTimeUs);
assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString())
.isEqualTo("This is the third subtitle."); .isEqualTo("This is the third subtitle.");
assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(8901000); long expectedEndTimeUs = (((2L * 60L * 60L) + 8L) * 1000L + 901L) * 1000L;
assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(expectedEndTimeUs);
} }
private static void assertAlignmentCue( private static void assertAlignmentCue(
......
...@@ -73,6 +73,10 @@ public final class MimeTypesTest { ...@@ -73,6 +73,10 @@ public final class MimeTypesTest {
assertThat(MimeTypes.getMediaMimeType("mp4a.AA")).isEqualTo(MimeTypes.AUDIO_DTS_HD); assertThat(MimeTypes.getMediaMimeType("mp4a.AA")).isEqualTo(MimeTypes.AUDIO_DTS_HD);
assertThat(MimeTypes.getMediaMimeType("mp4a.AB")).isEqualTo(MimeTypes.AUDIO_DTS_HD); assertThat(MimeTypes.getMediaMimeType("mp4a.AB")).isEqualTo(MimeTypes.AUDIO_DTS_HD);
assertThat(MimeTypes.getMediaMimeType("mp4a.AD")).isEqualTo(MimeTypes.AUDIO_OPUS); assertThat(MimeTypes.getMediaMimeType("mp4a.AD")).isEqualTo(MimeTypes.AUDIO_OPUS);
assertThat(MimeTypes.getMediaMimeType("wvtt")).isEqualTo(MimeTypes.TEXT_VTT);
assertThat(MimeTypes.getMediaMimeType("stpp.")).isEqualTo(MimeTypes.APPLICATION_TTML);
assertThat(MimeTypes.getMediaMimeType("stpp.ttml.im1t")).isEqualTo(MimeTypes.APPLICATION_TTML);
} }
@Test @Test
......
...@@ -807,15 +807,18 @@ public final class DashMediaSource extends BaseMediaSource { ...@@ -807,15 +807,18 @@ public final class DashMediaSource extends BaseMediaSource {
manifestLoadPending &= manifest.dynamic; manifestLoadPending &= manifest.dynamic;
manifestLoadStartTimestampMs = elapsedRealtimeMs - loadDurationMs; manifestLoadStartTimestampMs = elapsedRealtimeMs - loadDurationMs;
manifestLoadEndTimestampMs = elapsedRealtimeMs; manifestLoadEndTimestampMs = elapsedRealtimeMs;
if (manifest.location != null) {
synchronized (manifestUriLock) { synchronized (manifestUriLock) {
// This condition checks that replaceManifestUri wasn't called between the start and end of // Checks whether replaceManifestUri(Uri) was called to manually replace the URI between the
// this load. If it was, we ignore the manifest location and prefer the manual replacement. // start and end of this load. If it was then isSameUriInstance evaluates to false, and we
@SuppressWarnings("ReferenceEquality") // prefer the manual replacement to one derived from the previous request.
boolean isSameUriInstance = loadable.dataSpec.uri == manifestUri; @SuppressWarnings("ReferenceEquality")
if (isSameUriInstance) { boolean isSameUriInstance = loadable.dataSpec.uri == manifestUri;
manifestUri = manifest.location; if (isSameUriInstance) {
} // Replace the manifest URI with one specified by a manifest Location element (if present),
// or with the final (possibly redirected) URI. This follows the recommendation in
// DASH-IF-IOP 4.3, section 3.2.15.3. See: https://dashif.org/docs/DASH-IF-IOP-v4.3.pdf.
manifestUri = manifest.location != null ? manifest.location : loadable.getUri();
} }
} }
......
...@@ -222,10 +222,11 @@ public class DashManifestParser extends DefaultHandler ...@@ -222,10 +222,11 @@ public class DashManifestParser extends DefaultHandler
protected Pair<Period, Long> parsePeriod(XmlPullParser xpp, String baseUrl, long defaultStartMs) protected Pair<Period, Long> parsePeriod(XmlPullParser xpp, String baseUrl, long defaultStartMs)
throws XmlPullParserException, IOException { throws XmlPullParserException, IOException {
String id = xpp.getAttributeValue(null, "id"); @Nullable String id = xpp.getAttributeValue(null, "id");
long startMs = parseDuration(xpp, "start", defaultStartMs); long startMs = parseDuration(xpp, "start", defaultStartMs);
long durationMs = parseDuration(xpp, "duration", C.TIME_UNSET); long durationMs = parseDuration(xpp, "duration", C.TIME_UNSET);
SegmentBase segmentBase = null; @Nullable SegmentBase segmentBase = null;
@Nullable Descriptor assetIdentifier = null;
List<AdaptationSet> adaptationSets = new ArrayList<>(); List<AdaptationSet> adaptationSets = new ArrayList<>();
List<EventStream> eventStreams = new ArrayList<>(); List<EventStream> eventStreams = new ArrayList<>();
boolean seenFirstBaseUrl = false; boolean seenFirstBaseUrl = false;
...@@ -246,17 +247,24 @@ public class DashManifestParser extends DefaultHandler ...@@ -246,17 +247,24 @@ public class DashManifestParser extends DefaultHandler
segmentBase = parseSegmentList(xpp, null, durationMs); segmentBase = parseSegmentList(xpp, null, durationMs);
} else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) {
segmentBase = parseSegmentTemplate(xpp, null, Collections.emptyList(), durationMs); segmentBase = parseSegmentTemplate(xpp, null, Collections.emptyList(), durationMs);
} else if (XmlPullParserUtil.isStartTag(xpp, "AssetIdentifier")) {
assetIdentifier = parseDescriptor(xpp, "AssetIdentifier");
} else { } else {
maybeSkipTag(xpp); maybeSkipTag(xpp);
} }
} while (!XmlPullParserUtil.isEndTag(xpp, "Period")); } while (!XmlPullParserUtil.isEndTag(xpp, "Period"));
return Pair.create(buildPeriod(id, startMs, adaptationSets, eventStreams), durationMs); return Pair.create(
buildPeriod(id, startMs, adaptationSets, eventStreams, assetIdentifier), durationMs);
} }
protected Period buildPeriod(String id, long startMs, List<AdaptationSet> adaptationSets, protected Period buildPeriod(
List<EventStream> eventStreams) { @Nullable String id,
return new Period(id, startMs, adaptationSets, eventStreams); long startMs,
List<AdaptationSet> adaptationSets,
List<EventStream> eventStreams,
@Nullable Descriptor assetIdentifier) {
return new Period(id, startMs, adaptationSets, eventStreams, assetIdentifier);
} }
// AdaptationSet parsing. // AdaptationSet parsing.
......
...@@ -45,13 +45,16 @@ public class Period { ...@@ -45,13 +45,16 @@ public class Period {
*/ */
public final List<EventStream> eventStreams; public final List<EventStream> eventStreams;
/** The asset identifier for this period, if one exists */
@Nullable public final Descriptor assetIdentifier;
/** /**
* @param id The period identifier. May be null. * @param id The period identifier. May be null.
* @param startMs The start time of the period in milliseconds. * @param startMs The start time of the period in milliseconds.
* @param adaptationSets The adaptation sets belonging to the period. * @param adaptationSets The adaptation sets belonging to the period.
*/ */
public Period(@Nullable String id, long startMs, List<AdaptationSet> adaptationSets) { public Period(@Nullable String id, long startMs, List<AdaptationSet> adaptationSets) {
this(id, startMs, adaptationSets, Collections.emptyList()); this(id, startMs, adaptationSets, Collections.emptyList(), /* assetIdentifier= */ null);
} }
/** /**
...@@ -62,10 +65,27 @@ public class Period { ...@@ -62,10 +65,27 @@ public class Period {
*/ */
public Period(@Nullable String id, long startMs, List<AdaptationSet> adaptationSets, public Period(@Nullable String id, long startMs, List<AdaptationSet> adaptationSets,
List<EventStream> eventStreams) { List<EventStream> eventStreams) {
this(id, startMs, adaptationSets, eventStreams, /* assetIdentifier= */ null);
}
/**
* @param id The period identifier. May be null.
* @param startMs The start time of the period in milliseconds.
* @param adaptationSets The adaptation sets belonging to the period.
* @param eventStreams The {@link EventStream}s belonging to the period.
* @param assetIdentifier The asset identifier for this period
*/
public Period(
@Nullable String id,
long startMs,
List<AdaptationSet> adaptationSets,
List<EventStream> eventStreams,
@Nullable Descriptor assetIdentifier) {
this.id = id; this.id = id;
this.startMs = startMs; this.startMs = startMs;
this.adaptationSets = Collections.unmodifiableList(adaptationSets); this.adaptationSets = Collections.unmodifiableList(adaptationSets);
this.eventStreams = Collections.unmodifiableList(eventStreams); this.eventStreams = Collections.unmodifiableList(eventStreams);
this.assetIdentifier = assetIdentifier;
} }
/** /**
......
...@@ -49,7 +49,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -49,7 +49,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
public final class WebvttExtractor implements Extractor { public final class WebvttExtractor implements Extractor {
private static final Pattern LOCAL_TIMESTAMP = Pattern.compile("LOCAL:([^,]+)"); private static final Pattern LOCAL_TIMESTAMP = Pattern.compile("LOCAL:([^,]+)");
private static final Pattern MEDIA_TIMESTAMP = Pattern.compile("MPEGTS:(\\d+)"); private static final Pattern MEDIA_TIMESTAMP = Pattern.compile("MPEGTS:(-?\\d+)");
private static final int HEADER_MIN_LENGTH = 6 /* "WEBVTT" */; private static final int HEADER_MIN_LENGTH = 6 /* "WEBVTT" */;
private static final int HEADER_MAX_LENGTH = 3 /* optional Byte Order Mark */ + HEADER_MIN_LENGTH; private static final int HEADER_MAX_LENGTH = 3 /* optional Byte Order Mark */ + HEADER_MIN_LENGTH;
......
...@@ -480,17 +480,27 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -480,17 +480,27 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
} }
break; break;
case TYPE_SUBTITLES: case TYPE_SUBTITLES:
codecs = null;
sampleMimeType = null;
variant = getVariantWithSubtitleGroup(variants, groupId);
if (variant != null) {
codecs = Util.getCodecsOfType(variant.format.codecs, C.TRACK_TYPE_TEXT);
sampleMimeType = MimeTypes.getMediaMimeType(codecs);
}
if (sampleMimeType == null) {
sampleMimeType = MimeTypes.TEXT_VTT;
}
format = format =
Format.createTextContainerFormat( Format.createTextContainerFormat(
/* id= */ formatId, /* id= */ formatId,
/* label= */ name, /* label= */ name,
/* containerMimeType= */ MimeTypes.APPLICATION_M3U8, /* containerMimeType= */ MimeTypes.APPLICATION_M3U8,
/* sampleMimeType= */ MimeTypes.TEXT_VTT, sampleMimeType,
/* codecs= */ null, codecs,
/* bitrate= */ Format.NO_VALUE, /* bitrate= */ Format.NO_VALUE,
selectionFlags, selectionFlags,
roleFlags, roleFlags,
language) language)
.copyWithMetadata(metadata); .copyWithMetadata(metadata);
subtitles.add(new Rendition(uri, format, groupId, name)); subtitles.add(new Rendition(uri, format, groupId, name));
break; break;
...@@ -569,6 +579,17 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -569,6 +579,17 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
return null; return null;
} }
@Nullable
private static Variant getVariantWithSubtitleGroup(ArrayList<Variant> variants, String groupId) {
for (int i = 0; i < variants.size(); i++) {
Variant variant = variants.get(i);
if (groupId.equals(variant.subtitleGroupId)) {
return variant;
}
}
return null;
}
private static HlsMediaPlaylist parseMediaPlaylist( private static HlsMediaPlaylist parseMediaPlaylist(
HlsMasterPlaylist masterPlaylist, LineIterator iterator, String baseUri) throws IOException { HlsMasterPlaylist masterPlaylist, LineIterator iterator, String baseUri) throws IOException {
@HlsMediaPlaylist.PlaylistType int playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN; @HlsMediaPlaylist.PlaylistType int playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN;
......
...@@ -194,6 +194,19 @@ public class HlsMasterPlaylistParserTest { ...@@ -194,6 +194,19 @@ public class HlsMasterPlaylistParserTest {
+ "#EXT-X-MEDIA:TYPE=SUBTITLES," + "#EXT-X-MEDIA:TYPE=SUBTITLES,"
+ "GROUP-ID=\"sub1\",NAME=\"English\",URI=\"s1/en/prog_index.m3u8\"\n"; + "GROUP-ID=\"sub1\",NAME=\"English\",URI=\"s1/en/prog_index.m3u8\"\n";
private static final String PLAYLIST_WITH_TTML_SUBTITLE =
" #EXTM3U\n"
+ "\n"
+ "#EXT-X-VERSION:6\n"
+ "\n"
+ "#EXT-X-INDEPENDENT-SEGMENTS\n"
+ "\n"
+ "#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"stpp.ttml.im1t,mp4a.40.2,avc1.66.30\",RESOLUTION=304x128,AUDIO=\"aud1\",SUBTITLES=\"sub1\"\n"
+ "http://example.com/low.m3u8\n"
+ "\n"
+ "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud1\",NAME=\"English\",URI=\"a1/index.m3u8\"\n"
+ "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"sub1\",NAME=\"English\",AUTOSELECT=YES,DEFAULT=YES,URI=\"s1/en/prog_index.m3u8\"\n";
@Test @Test
public void testParseMasterPlaylist() throws IOException { public void testParseMasterPlaylist() throws IOException {
HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE);
...@@ -380,6 +393,18 @@ public class HlsMasterPlaylistParserTest { ...@@ -380,6 +393,18 @@ public class HlsMasterPlaylistParserTest {
.isEqualTo(createExtXMediaMetadata(/* groupId= */ "aud3", /* name= */ "English")); .isEqualTo(createExtXMediaMetadata(/* groupId= */ "aud3", /* name= */ "English"));
} }
@Test
public void parseMasterPlaylist_withTtmlSubtitle() throws IOException {
HlsMasterPlaylist playlistWithTtmlSubtitle =
parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_TTML_SUBTITLE);
HlsMasterPlaylist.Variant variant = playlistWithTtmlSubtitle.variants.get(0);
Format firstTextFormat = playlistWithTtmlSubtitle.subtitles.get(0).format;
assertThat(firstTextFormat.id).isEqualTo("sub1:English");
assertThat(firstTextFormat.containerMimeType).isEqualTo(MimeTypes.APPLICATION_M3U8);
assertThat(firstTextFormat.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_TTML);
assertThat(variant.format.codecs).isEqualTo("stpp.ttml.im1t,mp4a.40.2,avc1.66.30");
}
private static Metadata createExtXStreamInfMetadata(HlsTrackMetadataEntry.VariantInfo... infos) { private static Metadata createExtXStreamInfMetadata(HlsTrackMetadataEntry.VariantInfo... infos) {
return new Metadata( return new Metadata(
new HlsTrackMetadataEntry(/* groupId= */ null, /* name= */ null, Arrays.asList(infos))); new HlsTrackMetadataEntry(/* groupId= */ null, /* name= */ null, Arrays.asList(infos)));
......
...@@ -140,7 +140,7 @@ public class PlayerNotificationManager { ...@@ -140,7 +140,7 @@ public class PlayerNotificationManager {
* *
* @param player The {@link Player} for which a notification is being built. * @param player The {@link Player} for which a notification is being built.
*/ */
String getCurrentContentTitle(Player player); CharSequence getCurrentContentTitle(Player player);
/** /**
* Creates a content intent for the current media item. * Creates a content intent for the current media item.
...@@ -160,7 +160,7 @@ public class PlayerNotificationManager { ...@@ -160,7 +160,7 @@ public class PlayerNotificationManager {
* @param player The {@link Player} for which a notification is being built. * @param player The {@link Player} for which a notification is being built.
*/ */
@Nullable @Nullable
String getCurrentContentText(Player player); CharSequence getCurrentContentText(Player player);
/** /**
* Gets the content sub text for the current media item. * Gets the content sub text for the current media item.
...@@ -170,7 +170,7 @@ public class PlayerNotificationManager { ...@@ -170,7 +170,7 @@ public class PlayerNotificationManager {
* @param player The {@link Player} for which a notification is being built. * @param player The {@link Player} for which a notification is being built.
*/ */
@Nullable @Nullable
default String getCurrentSubText(Player player) { default CharSequence getCurrentSubText(Player player) {
return null; return null;
} }
......
...@@ -143,6 +143,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -143,6 +143,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
* <li>Corresponding method: None * <li>Corresponding method: None
* <li>Default: {@code surface_view} * <li>Default: {@code surface_view}
* </ul> * </ul>
* <li><b>{@code use_sensor_rotation}</b> - Whether to use the orientation sensor for rotation
* during spherical playbacks (if available).
* <ul>
* <li>Corresponding method: {@link #setUseSensorRotation(boolean)}
* <li>Default: {@code true}
* </ul>
* <li><b>{@code shutter_background_color}</b> - The background color of the {@code exo_shutter} * <li><b>{@code shutter_background_color}</b> - The background color of the {@code exo_shutter}
* view. * view.
* <ul> * <ul>
...@@ -308,6 +314,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider ...@@ -308,6 +314,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
@Nullable private Drawable defaultArtwork; @Nullable private Drawable defaultArtwork;
private @ShowBuffering int showBuffering; private @ShowBuffering int showBuffering;
private boolean keepContentOnPlayerReset; private boolean keepContentOnPlayerReset;
private boolean useSensorRotation;
@Nullable private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider; @Nullable private ErrorMessageProvider<? super ExoPlaybackException> errorMessageProvider;
@Nullable private CharSequence customErrorMessage; @Nullable private CharSequence customErrorMessage;
private int controllerShowTimeoutMs; private int controllerShowTimeoutMs;
...@@ -367,6 +374,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider ...@@ -367,6 +374,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
boolean controllerAutoShow = true; boolean controllerAutoShow = true;
boolean controllerHideDuringAds = true; boolean controllerHideDuringAds = true;
int showBuffering = SHOW_BUFFERING_NEVER; int showBuffering = SHOW_BUFFERING_NEVER;
useSensorRotation = true;
if (attrs != null) { if (attrs != null) {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.PlayerView, 0, 0); TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.PlayerView, 0, 0);
try { try {
...@@ -390,6 +398,8 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider ...@@ -390,6 +398,8 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
R.styleable.PlayerView_keep_content_on_player_reset, keepContentOnPlayerReset); R.styleable.PlayerView_keep_content_on_player_reset, keepContentOnPlayerReset);
controllerHideDuringAds = controllerHideDuringAds =
a.getBoolean(R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds); a.getBoolean(R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds);
useSensorRotation =
a.getBoolean(R.styleable.PlayerView_use_sensor_rotation, useSensorRotation);
} finally { } finally {
a.recycle(); a.recycle();
} }
...@@ -422,6 +432,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider ...@@ -422,6 +432,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
case SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW: case SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW:
SphericalGLSurfaceView sphericalGLSurfaceView = new SphericalGLSurfaceView(context); SphericalGLSurfaceView sphericalGLSurfaceView = new SphericalGLSurfaceView(context);
sphericalGLSurfaceView.setSingleTapListener(componentListener); sphericalGLSurfaceView.setSingleTapListener(componentListener);
sphericalGLSurfaceView.setUseSensorRotation(useSensorRotation);
surfaceView = sphericalGLSurfaceView; surfaceView = sphericalGLSurfaceView;
break; break;
case SURFACE_TYPE_VIDEO_DECODER_GL_SURFACE_VIEW: case SURFACE_TYPE_VIDEO_DECODER_GL_SURFACE_VIEW:
...@@ -747,6 +758,22 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider ...@@ -747,6 +758,22 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider
} }
/** /**
* Sets whether to use the orientation sensor for rotation during spherical playbacks (if
* available)
*
* @param useSensorRotation Whether to use the orientation sensor for rotation during spherical
* playbacks.
*/
public void setUseSensorRotation(boolean useSensorRotation) {
if (this.useSensorRotation != useSensorRotation) {
this.useSensorRotation = useSensorRotation;
if (surfaceView instanceof SphericalGLSurfaceView) {
((SphericalGLSurfaceView) surfaceView).setUseSensorRotation(useSensorRotation);
}
}
}
/**
* Sets whether a buffering spinner is displayed when the player is in the buffering state. The * Sets whether a buffering spinner is displayed when the player is in the buffering state. The
* buffering spinner is not displayed by default. * buffering spinner is not displayed by default.
* *
......
...@@ -72,6 +72,9 @@ public final class SphericalGLSurfaceView extends GLSurfaceView { ...@@ -72,6 +72,9 @@ public final class SphericalGLSurfaceView extends GLSurfaceView {
@Nullable private SurfaceTexture surfaceTexture; @Nullable private SurfaceTexture surfaceTexture;
@Nullable private Surface surface; @Nullable private Surface surface;
@Nullable private Player.VideoComponent videoComponent; @Nullable private Player.VideoComponent videoComponent;
private boolean useSensorRotation;
private boolean isStarted;
private boolean isOrientationListenerRegistered;
public SphericalGLSurfaceView(Context context) { public SphericalGLSurfaceView(Context context) {
this(context, null); this(context, null);
...@@ -104,6 +107,7 @@ public final class SphericalGLSurfaceView extends GLSurfaceView { ...@@ -104,6 +107,7 @@ public final class SphericalGLSurfaceView extends GLSurfaceView {
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = Assertions.checkNotNull(windowManager).getDefaultDisplay(); Display display = Assertions.checkNotNull(windowManager).getDefaultDisplay();
orientationListener = new OrientationListener(display, touchTracker, renderer); orientationListener = new OrientationListener(display, touchTracker, renderer);
useSensorRotation = true;
setEGLContextClientVersion(2); setEGLContextClientVersion(2);
setRenderer(renderer); setRenderer(renderer);
...@@ -145,20 +149,23 @@ public final class SphericalGLSurfaceView extends GLSurfaceView { ...@@ -145,20 +149,23 @@ public final class SphericalGLSurfaceView extends GLSurfaceView {
touchTracker.setSingleTapListener(listener); touchTracker.setSingleTapListener(listener);
} }
/** Sets whether to use the orientation sensor for rotation (if available). */
public void setUseSensorRotation(boolean useSensorRotation) {
this.useSensorRotation = useSensorRotation;
updateOrientationListenerRegistration();
}
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
if (orientationSensor != null) { isStarted = true;
sensorManager.registerListener( updateOrientationListenerRegistration();
orientationListener, orientationSensor, SensorManager.SENSOR_DELAY_FASTEST);
}
} }
@Override @Override
public void onPause() { public void onPause() {
if (orientationSensor != null) { isStarted = false;
sensorManager.unregisterListener(orientationListener); updateOrientationListenerRegistration();
}
super.onPause(); super.onPause();
} }
...@@ -181,6 +188,20 @@ public final class SphericalGLSurfaceView extends GLSurfaceView { ...@@ -181,6 +188,20 @@ public final class SphericalGLSurfaceView extends GLSurfaceView {
}); });
} }
private void updateOrientationListenerRegistration() {
boolean enabled = useSensorRotation && isStarted;
if (orientationSensor == null || enabled == isOrientationListenerRegistered) {
return;
}
if (enabled) {
sensorManager.registerListener(
orientationListener, orientationSensor, SensorManager.SENSOR_DELAY_FASTEST);
} else {
sensorManager.unregisterListener(orientationListener);
}
isOrientationListenerRegistered = enabled;
}
// Called on GL thread. // Called on GL thread.
private void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture) { private void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture) {
mainHandler.post( mainHandler.post(
......
...@@ -77,8 +77,8 @@ ...@@ -77,8 +77,8 @@
<enum name="always" value="2"/> <enum name="always" value="2"/>
</attr> </attr>
<attr name="keep_content_on_player_reset" format="boolean"/> <attr name="keep_content_on_player_reset" format="boolean"/>
<attr name="use_sensor_rotation" format="boolean"/>
<attr name="player_layout_id" format="reference"/> <attr name="player_layout_id" format="reference"/>
<attr name="surface_type"/> <attr name="surface_type"/>
<!-- AspectRatioFrameLayout attributes --> <!-- AspectRatioFrameLayout attributes -->
<attr name="resize_mode"/> <attr name="resize_mode"/>
......
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