diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e23bf592a5..8820273a73 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,59 @@ # 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) ### * SmoothStreaming: Fix regression that broke playback in 2.11.2 diff --git a/constants.gradle b/constants.gradle index e29e5cb7c8..c79130cacb 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.11.3' - releaseVersionCode = 2011003 + releaseVersion = '2.11.4' + releaseVersionCode = 2011004 minSdkVersion = 16 appTargetSdkVersion = 29 targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle index f9228e4b79..c929f09c87 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -57,8 +57,8 @@ dependencies { implementation project(modulePrefix + 'library-ui') implementation project(modulePrefix + 'extension-cast') implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion - implementation 'androidx.recyclerview:recyclerview:1.0.0' - implementation 'com.google.android.material:material:1.0.0' + implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation 'com.google.android.material:material:1.1.0' } apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml index dbfdd833f6..d92d9e2303 100644 --- a/demos/cast/src/main/AndroidManifest.xml +++ b/demos/cast/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ + diff --git a/demos/gl/src/main/AndroidManifest.xml b/demos/gl/src/main/AndroidManifest.xml index de47da531e..4c95d1ec2f 100644 --- a/demos/gl/src/main/AndroidManifest.xml +++ b/demos/gl/src/main/AndroidManifest.xml @@ -28,6 +28,7 @@ diff --git a/demos/main/build.gradle b/demos/main/build.gradle index ab47b6de81..b7a8666fe3 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -64,7 +64,7 @@ android { dependencies { implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion 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-dash') implementation project(modulePrefix + 'library-hls') diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index c100c853e7..5a577449fa 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -39,6 +39,7 @@ import com.google.android.exoplayer2.PlaybackPreparer; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RenderersFactory; 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.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -380,6 +381,7 @@ public class PlayerActivity extends AppCompatActivity .setTrackSelector(trackSelector) .build(); player.addListener(new PlayerEventListener()); + player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true); player.setPlayWhenReady(startAutoPlay); player.addAnalyticsListener(new EventLogger(trackSelector)); playerView.setPlayer(player); diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index cdce29aa5e..f18d43094a 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.demo; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.net.Uri; import android.os.AsyncTask; @@ -34,6 +35,7 @@ import android.widget.ExpandableListView.OnChildClickListener; import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import com.google.android.exoplayer2.ParserException; @@ -63,6 +65,7 @@ public class SampleChooserActivity extends AppCompatActivity private static final String TAG = "SampleChooserActivity"; + private String[] uris; private boolean useExtensionRenderers; private DownloadTracker downloadTracker; private SampleAdapter sampleAdapter; @@ -81,7 +84,6 @@ public class SampleChooserActivity extends AppCompatActivity Intent intent = getIntent(); String dataUri = intent.getDataString(); - String[] uris; if (dataUri != null) { uris = new String[] {dataUri}; } else { @@ -105,8 +107,7 @@ public class SampleChooserActivity extends AppCompatActivity DemoApplication application = (DemoApplication) getApplication(); useExtensionRenderers = application.useExtensionRenderers(); downloadTracker = application.getDownloadTracker(); - SampleListLoader loaderTask = new SampleListLoader(); - loaderTask.execute(uris); + loadSample(); // 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 @@ -157,6 +158,37 @@ public class SampleChooserActivity extends AppCompatActivity 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 groups, boolean sawError) { if (sawError) { Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) diff --git a/extensions/av1/src/main/jni/gav1_jni.cc b/extensions/av1/src/main/jni/gav1_jni.cc index 9ac3ea5cd2..e0cef86d22 100644 --- a/extensions/av1/src/main/jni/gav1_jni.cc +++ b/extensions/av1/src/main/jni/gav1_jni.cc @@ -27,6 +27,7 @@ #endif // CPU_FEATURES_COMPILED_ANY_ARM_NEON #include +#include #include #include // NOLINT #include @@ -121,18 +122,22 @@ const char* GetJniErrorMessage(JniStatusCode error_code) { } } -// Manages Libgav1FrameBuffer and reference information. +// Manages frame buffer and reference information. class JniFrameBuffer { public: - explicit JniFrameBuffer(int id) : id_(id), reference_count_(0) { - gav1_frame_buffer_.private_data = &id_; - } + explicit JniFrameBuffer(int id) : id_(id), reference_count_(0) {} ~JniFrameBuffer() { 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) { for (int plane_index = kPlaneY; plane_index < decoder_buffer.NumPlanes(); plane_index++) { @@ -160,9 +165,8 @@ class JniFrameBuffer { void RemoveReference() { reference_count_--; } bool InUse() const { return reference_count_ != 0; } - const Libgav1FrameBuffer& GetGav1FrameBuffer() const { - return gav1_frame_buffer_; - } + uint8_t* RawBuffer(int plane_index) const { return raw_buffer_[plane_index]; } + void* BufferPrivateData() const { return const_cast(&id_); } // 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, @@ -172,15 +176,14 @@ class JniFrameBuffer { for (int plane_index = kPlaneY; plane_index < kMaxPlanes; plane_index++) { const int min_size = (plane_index == kPlaneY) ? y_plane_min_size : uv_plane_min_size; - if (gav1_frame_buffer_.size[plane_index] >= min_size) continue; - delete[] gav1_frame_buffer_.data[plane_index]; - gav1_frame_buffer_.data[plane_index] = - new (std::nothrow) uint8_t[min_size]; - if (!gav1_frame_buffer_.data[plane_index]) { - gav1_frame_buffer_.size[plane_index] = 0; + if (raw_buffer_size_[plane_index] >= min_size) continue; + delete[] raw_buffer_[plane_index]; + raw_buffer_[plane_index] = new (std::nothrow) uint8_t[min_size]; + if (!raw_buffer_[plane_index]) { + raw_buffer_size_[plane_index] = 0; return false; } - gav1_frame_buffer_.size[plane_index] = min_size; + raw_buffer_size_[plane_index] = min_size; } return true; } @@ -190,9 +193,12 @@ class JniFrameBuffer { uint8_t* plane_[kMaxPlanes]; int displayed_width_[kMaxPlanes]; int displayed_height_[kMaxPlanes]; - int id_; + const int id_; 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. @@ -210,7 +216,7 @@ class JniBufferManager { } JniStatusCode GetBuffer(size_t y_plane_min_size, size_t uv_plane_min_size, - Libgav1FrameBuffer* frame_buffer) { + JniFrameBuffer** jni_buffer) { std::lock_guard lock(mutex_); JniFrameBuffer* output_buffer; @@ -230,7 +236,7 @@ class JniBufferManager { } output_buffer->AddReference(); - *frame_buffer = output_buffer->GetGav1FrameBuffer(); + *jni_buffer = output_buffer; return kJniStatusOk; } @@ -316,29 +322,46 @@ struct JniContext { JniStatusCode jni_status_code = kJniStatusOk; }; -int Libgav1GetFrameBuffer(void* private_data, size_t y_plane_min_size, - size_t uv_plane_min_size, - Libgav1FrameBuffer* frame_buffer) { - JniContext* const context = reinterpret_cast(private_data); +Libgav1StatusCode Libgav1GetFrameBuffer(void* callback_private_data, + int bitdepth, + libgav1::ImageFormat image_format, + 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(callback_private_data); + JniFrameBuffer* jni_buffer; 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) { 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, - Libgav1FrameBuffer* frame_buffer) { - JniContext* const context = reinterpret_cast(private_data); - const int buffer_id = *reinterpret_cast(frame_buffer->private_data); +void Libgav1ReleaseFrameBuffer(void* callback_private_data, + void* buffer_private_data) { + JniContext* const context = static_cast(callback_private_data); + const int buffer_id = *static_cast(buffer_private_data); context->jni_status_code = context->buffer_manager.ReleaseBuffer(buffer_id); if (context->jni_status_code != kJniStatusOk) { LOGE("%s", GetJniErrorMessage(context->jni_status_code)); - return -1; } - return 0; } constexpr int AlignTo16(int value) { return (value + 15) & (~15); } @@ -508,8 +531,8 @@ DECODER_FUNC(jlong, gav1Init, jint threads) { libgav1::DecoderSettings settings; settings.threads = threads; - settings.get = Libgav1GetFrameBuffer; - settings.release = Libgav1ReleaseFrameBuffer; + settings.get_frame_buffer = Libgav1GetFrameBuffer; + settings.release_frame_buffer = Libgav1ReleaseFrameBuffer; settings.callback_private_data = context; context->libgav1_status_code = context->decoder.Init(&settings); @@ -544,7 +567,8 @@ DECODER_FUNC(jint, gav1Decode, jlong jContext, jobject encodedData, const uint8_t* const buffer = reinterpret_cast( env->GetDirectBufferAddress(encodedData)); 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) { return kStatusError; } @@ -619,7 +643,7 @@ DECODER_FUNC(jint, gav1GetFrame, jlong jContext, jobject jOutputBuffer, } const int buffer_id = - *reinterpret_cast(decoder_buffer->buffer_private_data); + *static_cast(decoder_buffer->buffer_private_data); context->buffer_manager.AddBufferReference(buffer_id); JniFrameBuffer* const jni_buffer = context->buffer_manager.GetBuffer(buffer_id); diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index 0d7d96db4c..3486fbb08f 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -31,7 +31,7 @@ android { } 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 project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index 1b2db8f0f4..f6e3944572 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -35,22 +35,22 @@ FFMPEG_EXT_PATH="$(pwd)/extensions/ffmpeg/src/main/jni" NDK_PATH="" ``` -* 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" ``` -* Configure the formats supported by adapting the following variable if needed - and by setting it. See the [Supported formats][] page for more details of the - formats. +* Configure the decoders to include. See the [Supported formats][] page for + details of the available decoders, and which formats they support. ``` ENABLED_DECODERS=(vorbis opus flac) ``` -* Fetch and build FFmpeg. For example, executing script `build_ffmpeg.sh` will - fetch and build FFmpeg release 4.2 for armeabi-v7a, arm64-v8a and x86: +* Fetch and build FFmpeg. Executing `build_ffmpeg.sh` will fetch and build + 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}" && \ @@ -63,7 +63,7 @@ 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) ## diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java index 4639851263..dc72e12e65 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java @@ -33,7 +33,7 @@ public final class FfmpegLibrary { private static final String TAG = "FfmpegLibrary"; private static final LibraryLoader LOADER = - new LibraryLoader("avutil", "avresample", "swresample", "avcodec", "ffmpeg"); + new LibraryLoader("avutil", "swresample", "avcodec", "ffmpeg"); private FfmpegLibrary() {} diff --git a/extensions/ffmpeg/src/main/jni/Android.mk b/extensions/ffmpeg/src/main/jni/Android.mk index 22a4edcdae..bcaf12cd11 100644 --- a/extensions/ffmpeg/src/main/jni/Android.mk +++ b/extensions/ffmpeg/src/main/jni/Android.mk @@ -21,11 +21,6 @@ LOCAL_MODULE := libavcodec LOCAL_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so include $(PREBUILT_SHARED_LIBRARY) -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_SRC_FILES := ffmpeg/android-libs/$(TARGET_ARCH_ABI)/$(LOCAL_MODULE).so @@ -40,6 +35,6 @@ include $(CLEAR_VARS) LOCAL_MODULE := ffmpeg LOCAL_SRC_FILES := ffmpeg_jni.cc 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 include $(BUILD_SHARED_LIBRARY) diff --git a/extensions/ffmpeg/src/main/jni/build_ffmpeg.sh b/extensions/ffmpeg/src/main/jni/build_ffmpeg.sh index a76fa0e589..833ea189b2 100755 --- a/extensions/ffmpeg/src/main/jni/build_ffmpeg.sh +++ b/extensions/ffmpeg/src/main/jni/build_ffmpeg.sh @@ -32,8 +32,9 @@ COMMON_OPTIONS=" --disable-postproc --disable-avfilter --disable-symver - --enable-avresample + --disable-avresample --enable-swresample + --extra-ldexeflags=-pie " TOOLCHAIN_PREFIX="${NDK_PATH}/toolchains/llvm/prebuilt/${HOST_PLATFORM}/bin" for decoder in "${ENABLED_DECODERS[@]}" @@ -53,7 +54,6 @@ git checkout release/4.2 --strip="${TOOLCHAIN_PREFIX}/arm-linux-androideabi-strip" \ --extra-cflags="-march=armv7-a -mfloat-abi=softfp" \ --extra-ldflags="-Wl,--fix-cortex-a8" \ - --extra-ldexeflags=-pie \ ${COMMON_OPTIONS} make -j4 make install-libs @@ -65,7 +65,6 @@ make clean --cross-prefix="${TOOLCHAIN_PREFIX}/aarch64-linux-android21-" \ --nm="${TOOLCHAIN_PREFIX}/aarch64-linux-android-nm" \ --strip="${TOOLCHAIN_PREFIX}/aarch64-linux-android-strip" \ - --extra-ldexeflags=-pie \ ${COMMON_OPTIONS} make -j4 make install-libs @@ -77,7 +76,18 @@ make clean --cross-prefix="${TOOLCHAIN_PREFIX}/i686-linux-android16-" \ --nm="${TOOLCHAIN_PREFIX}/i686-linux-android-nm" \ --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 \ ${COMMON_OPTIONS} make -j4 diff --git a/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc index dcd4560e4a..400039af89 100644 --- a/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc +++ b/extensions/ffmpeg/src/main/jni/ffmpeg_jni.cc @@ -26,10 +26,10 @@ extern "C" { #include #endif #include -#include #include #include #include +#include } #define LOG_TAG "ffmpeg_jni" @@ -289,11 +289,11 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, int sampleCount = frame->nb_samples; int dataSize = av_samples_get_buffer_size(NULL, channelCount, sampleCount, sampleFormat, 1); - AVAudioResampleContext *resampleContext; + SwrContext *resampleContext; if (context->opaque) { - resampleContext = (AVAudioResampleContext *) context->opaque; + resampleContext = (SwrContext *)context->opaque; } else { - resampleContext = avresample_alloc_context(); + resampleContext = swr_alloc(); 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, "in_sample_rate", sampleRate, 0); @@ -302,9 +302,9 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, // The output format is always the requested format. av_opt_set_int(resampleContext, "out_sample_fmt", context->request_sample_fmt, 0); - result = avresample_open(resampleContext); + result = swr_init(resampleContext); if (result < 0) { - logError("avresample_open", result); + logError("swr_init", result); av_frame_free(&frame); return -1; } @@ -312,7 +312,7 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, } int inSampleSize = av_get_bytes_per_sample(sampleFormat); 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; if (outSize + bufferOutSize > outputSize) { LOGE("Output buffer size (%d) too small for output data (%d).", @@ -320,15 +320,14 @@ int decodePacket(AVCodecContext *context, AVPacket *packet, av_frame_free(&frame); return -1; } - result = avresample_convert(resampleContext, &outputBuffer, bufferOutSize, - outSamples, frame->data, frame->linesize[0], - sampleCount); + result = swr_convert(resampleContext, &outputBuffer, bufferOutSize, + (const uint8_t **)frame->data, frame->nb_samples); av_frame_free(&frame); if (result < 0) { - logError("avresample_convert", result); + logError("swr_convert", result); return result; } - int available = avresample_available(resampleContext); + int available = swr_get_out_samples(resampleContext, 0); if (available != 0) { LOGE("Expected no samples remaining after resampling, but found %d.", available); @@ -351,9 +350,9 @@ void releaseContext(AVCodecContext *context) { if (!context) { return; } - AVAudioResampleContext *resampleContext; - if ((resampleContext = (AVAudioResampleContext *) context->opaque)) { - avresample_free(&resampleContext); + SwrContext *swrContext; + if ((swrContext = (SwrContext *)context->opaque)) { + swr_free(&swrContext); context->opaque = NULL; } avcodec_free_context(&context); diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index 3053961f49..fe2bdd672b 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -223,7 +223,7 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { responseByteStream = responseBody.byteStream(); } catch (IOException e) { 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(); diff --git a/extensions/opus/src/androidTest/assets/bear-opus-negative-gain.mka b/extensions/opus/src/androidTest/assets/bear-opus-negative-gain.mka new file mode 100644 index 0000000000..3ee21adfd3 Binary files /dev/null and b/extensions/opus/src/androidTest/assets/bear-opus-negative-gain.mka differ diff --git a/extensions/opus/src/androidTest/assets/bear-opus.webm b/extensions/opus/src/androidTest/assets/bear-opus.mka similarity index 100% rename from extensions/opus/src/androidTest/assets/bear-opus.webm rename to extensions/opus/src/androidTest/assets/bear-opus.mka diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java index b3d5b525d5..582ca997df 100644 --- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java +++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java @@ -37,7 +37,9 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) 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 public void setUp() { @@ -51,6 +53,11 @@ public class OpusPlaybackTest { playUri(BEAR_OPUS_URI); } + @Test + public void basicPlaybackNegativeGain() throws Exception { + playUri(BEAR_OPUS_NEGATIVE_GAIN_URI); + } + private void playUri(String uri) throws Exception { TestPlaybackRunnable testPlaybackRunnable = new TestPlaybackRunnable(Uri.parse(uri), ApplicationProvider.getApplicationContext()); diff --git a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java index f0e993e3b9..5b880cbb71 100644 --- a/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java +++ b/extensions/opus/src/main/java/com/google/android/exoplayer2/ext/opus/OpusDecoder.java @@ -90,8 +90,8 @@ import java.util.List; if (channelCount > 8) { throw new OpusDecoderException("Invalid channel count: " + channelCount); } - int preskip = readLittleEndian16(headerBytes, 10); - int gain = readLittleEndian16(headerBytes, 16); + int preskip = readUnsignedLittleEndian16(headerBytes, 10); + int gain = readSignedLittleEndian16(headerBytes, 16); byte[] streamMap = new byte[8]; int numStreams; @@ -228,12 +228,16 @@ import java.util.List; 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; value |= (input[offset + 1] & 0xFF) << 8; 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, int gain, byte[] streamMap); private native int opusDecode(long decoder, long timeUs, ByteBuffer inputBuffer, int inputSize, diff --git a/extensions/workmanager/build.gradle b/extensions/workmanager/build.gradle index 6a7aa10722..9671279269 100644 --- a/extensions/workmanager/build.gradle +++ b/extensions/workmanager/build.gradle @@ -34,7 +34,7 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.work:work-runtime:2.2.0' + implementation 'androidx.work:work-runtime:2.3.4' } ext { diff --git a/javadoc_combined.gradle b/javadoc_combined.gradle index d2fa241a81..3b482910ae 100644 --- a/javadoc_combined.gradle +++ b/javadoc_combined.gradle @@ -29,8 +29,7 @@ class CombinedJavadocPlugin implements Plugin { classpath = project.files([]) destinationDir = project.file("$project.buildDir/docs/javadoc") options { - links "https://docs.oracle.com/javase/7/docs/api/", - "https://developer.android.com/reference" + links "https://developer.android.com/reference" encoding = "UTF-8" } exclude "**/BuildConfig.java" diff --git a/javadoc_library.gradle b/javadoc_library.gradle index 74fcc3dd6c..dd508a1781 100644 --- a/javadoc_library.gradle +++ b/javadoc_library.gradle @@ -26,9 +26,7 @@ android.libraryVariants.all { variant -> title = "ExoPlayer ${javadocTitle}" source = allSourceDirs options { - links "http://docs.oracle.com/javase/7/docs/api/" - linksOffline "https://developer.android.com/reference", - "${android.sdkDirectory}/docs/reference" + links "https://developer.android.com/reference" encoding = "UTF-8" } exclude "**/BuildConfig.java" diff --git a/javadoc_util.gradle b/javadoc_util.gradle index d5b1f56720..b9962d33a9 100644 --- a/javadoc_util.gradle +++ b/javadoc_util.gradle @@ -15,18 +15,11 @@ ext.fixJavadoc = { def javadocPath = "${project.buildDir}/docs/javadoc" // Fix external Android links to target the top frame. def androidRoot = "https://developer.android.com/reference/" - def androidLink = "Call {@link #updateAudioFocus(boolean, int)} to update the audio focus based on these + * attributes. + * * @param audioAttributes The audio attributes or {@code null} if audio focus should not be * managed automatically. - * @param playWhenReady The current state of {@link ExoPlayer#getPlayWhenReady()}. - * @param playerState The current player state; {@link ExoPlayer#getPlaybackState()}. - * @return A {@link PlayerCommand} to execute on the player. */ - @PlayerCommand - public int setAudioAttributes( - @Nullable AudioAttributes audioAttributes, boolean playWhenReady, int playerState) { + public void setAudioAttributes(@Nullable AudioAttributes audioAttributes) { if (!Util.areEqual(this.audioAttributes, audioAttributes)) { this.audioAttributes = audioAttributes; focusGain = convertAudioAttributesToFocusGain(audioAttributes); - Assertions.checkArgument( focusGain == C.AUDIOFOCUS_GAIN || focusGain == C.AUDIOFOCUS_NONE, "Automatic handling of audio focus is only available for USAGE_MEDIA and USAGE_GAME."); - if (playWhenReady - && (playerState == Player.STATE_BUFFERING || playerState == Player.STATE_READY)) { - return requestAudioFocus(); - } } - - return playerState == Player.STATE_IDLE - ? handleIdle(playWhenReady) - : handlePrepare(playWhenReady); } /** - * Called by a player as part of {@link ExoPlayer#prepare(MediaSource, boolean, boolean)}. + * Called by the player to abandon or request audio focus based on the desired player state. * - * @param playWhenReady The current state of {@link ExoPlayer#getPlayWhenReady()}. + * @param playWhenReady The desired value of playWhenReady. + * @param playbackState The desired playback state. * @return A {@link PlayerCommand} to execute on the player. */ @PlayerCommand - public int handlePrepare(boolean playWhenReady) { + public int updateAudioFocus(boolean playWhenReady, @Player.State int playbackState) { + if (shouldAbandonAudioFocus(playbackState)) { + abandonAudioFocus(); + return playWhenReady ? PLAYER_COMMAND_PLAY_WHEN_READY : PLAYER_COMMAND_DO_NOT_PLAY; + } return playWhenReady ? requestAudioFocus() : PLAYER_COMMAND_DO_NOT_PLAY; } /** - * Called by the player as part of {@link ExoPlayer#setPlayWhenReady(boolean)}. - * - * @param playWhenReady The desired value of playWhenReady. - * @param playerState The current state of the player. - * @return A {@link PlayerCommand} to execute on the player. + * Called when the manager is no longer required. Audio focus will be released without making any + * calls to the {@link PlayerControl}. */ - @PlayerCommand - public int handleSetPlayWhenReady(boolean playWhenReady, int playerState) { - if (!playWhenReady) { - abandonAudioFocus(); - return PLAYER_COMMAND_DO_NOT_PLAY; - } - - return playerState == Player.STATE_IDLE ? handleIdle(playWhenReady) : requestAudioFocus(); - } - - /** Called by the player as part of {@link ExoPlayer#stop(boolean)}. */ - public void handleStop() { - abandonAudioFocus(/* forceAbandon= */ true); + public void release() { + playerControl = null; + abandonAudioFocus(); } // Internal methods. @@ -201,62 +178,35 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return focusListener; } - @PlayerCommand - private int handleIdle(boolean playWhenReady) { - return playWhenReady ? PLAYER_COMMAND_PLAY_WHEN_READY : PLAYER_COMMAND_DO_NOT_PLAY; + private boolean shouldAbandonAudioFocus(@Player.State int playbackState) { + return playbackState == Player.STATE_IDLE || focusGain != C.AUDIOFOCUS_GAIN; } @PlayerCommand private int requestAudioFocus() { - int focusRequestResult; - - if (focusGain == C.AUDIOFOCUS_NONE) { - if (audioFocusState != AUDIO_FOCUS_STATE_NO_FOCUS) { - abandonAudioFocus(/* forceAbandon= */ true); - } + if (audioFocusState == AUDIO_FOCUS_STATE_HAVE_FOCUS) { return PLAYER_COMMAND_PLAY_WHEN_READY; } - - if (audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) { - if (Util.SDK_INT >= 26) { - focusRequestResult = requestAudioFocusV26(); - } else { - focusRequestResult = requestAudioFocusDefault(); - } - audioFocusState = - focusRequestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED - ? AUDIO_FOCUS_STATE_HAVE_FOCUS - : AUDIO_FOCUS_STATE_NO_FOCUS; - } - - if (audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) { + int requestResult = Util.SDK_INT >= 26 ? requestAudioFocusV26() : requestAudioFocusDefault(); + if (requestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + setAudioFocusState(AUDIO_FOCUS_STATE_HAVE_FOCUS); + return PLAYER_COMMAND_PLAY_WHEN_READY; + } else { + setAudioFocusState(AUDIO_FOCUS_STATE_NO_FOCUS); return PLAYER_COMMAND_DO_NOT_PLAY; } - - return audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT - ? PLAYER_COMMAND_WAIT_FOR_CALLBACK - : PLAYER_COMMAND_PLAY_WHEN_READY; } private void abandonAudioFocus() { - abandonAudioFocus(/* forceAbandon= */ false); - } - - private void abandonAudioFocus(boolean forceAbandon) { - if (focusGain == C.AUDIOFOCUS_NONE && audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) { + if (audioFocusState == AUDIO_FOCUS_STATE_NO_FOCUS) { return; } - - if (focusGain != C.AUDIOFOCUS_GAIN - || audioFocusState == AUDIO_FOCUS_STATE_LOST_FOCUS - || forceAbandon) { - if (Util.SDK_INT >= 26) { - abandonAudioFocusV26(); - } else { - abandonAudioFocusDefault(); - } - audioFocusState = AUDIO_FOCUS_STATE_NO_FOCUS; + if (Util.SDK_INT >= 26) { + abandonAudioFocusV26(); + } else { + abandonAudioFocusDefault(); } + setAudioFocusState(AUDIO_FOCUS_STATE_NO_FOCUS); } private int requestAudioFocusDefault() { @@ -312,7 +262,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; */ @C.AudioFocusGain private static int convertAudioAttributesToFocusGain(@Nullable AudioAttributes audioAttributes) { - if (audioAttributes == null) { // Don't handle audio focus. It may be either video only contents or developers // want to have more finer grained control. (e.g. adding audio focus listener) @@ -382,63 +331,55 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } } - private void handleAudioFocusChange(int focusChange) { - // Convert the platform focus change to internal state. - switch (focusChange) { - case AudioManager.AUDIOFOCUS_LOSS: - audioFocusState = AUDIO_FOCUS_STATE_LOST_FOCUS; - break; - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: - audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT; - break; - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: - if (willPauseWhenDucked()) { - audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT; - } else { - audioFocusState = AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK; - } - break; - case AudioManager.AUDIOFOCUS_GAIN: - audioFocusState = AUDIO_FOCUS_STATE_HAVE_FOCUS; - break; - default: - Log.w(TAG, "Unknown focus change type: " + focusChange); - // Early return. - return; - } - - // Handle the internal state (change). - switch (audioFocusState) { - case AUDIO_FOCUS_STATE_NO_FOCUS: - // Focus was not requested; nothing to do. - break; - case AUDIO_FOCUS_STATE_LOST_FOCUS: - playerControl.executePlayerCommand(PLAYER_COMMAND_DO_NOT_PLAY); - abandonAudioFocus(/* forceAbandon= */ true); - break; - case AUDIO_FOCUS_STATE_LOSS_TRANSIENT: - playerControl.executePlayerCommand(PLAYER_COMMAND_WAIT_FOR_CALLBACK); - break; - case AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK: - // Volume will be adjusted by the code below. - break; - case AUDIO_FOCUS_STATE_HAVE_FOCUS: - playerControl.executePlayerCommand(PLAYER_COMMAND_PLAY_WHEN_READY); - break; - default: - throw new IllegalStateException("Unknown audio focus state: " + audioFocusState); + private void setAudioFocusState(@AudioFocusState int audioFocusState) { + if (this.audioFocusState == audioFocusState) { + return; } + this.audioFocusState = audioFocusState; float volumeMultiplier = (audioFocusState == AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK) ? AudioFocusManager.VOLUME_MULTIPLIER_DUCK : AudioFocusManager.VOLUME_MULTIPLIER_DEFAULT; - if (AudioFocusManager.this.volumeMultiplier != volumeMultiplier) { - AudioFocusManager.this.volumeMultiplier = volumeMultiplier; + if (this.volumeMultiplier == volumeMultiplier) { + return; + } + this.volumeMultiplier = volumeMultiplier; + if (playerControl != null) { playerControl.setVolumeMultiplier(volumeMultiplier); } } + private void handlePlatformAudioFocusChange(int focusChange) { + switch (focusChange) { + case AudioManager.AUDIOFOCUS_GAIN: + setAudioFocusState(AUDIO_FOCUS_STATE_HAVE_FOCUS); + executePlayerCommand(PLAYER_COMMAND_PLAY_WHEN_READY); + return; + case AudioManager.AUDIOFOCUS_LOSS: + executePlayerCommand(PLAYER_COMMAND_DO_NOT_PLAY); + abandonAudioFocus(); + return; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: + if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || willPauseWhenDucked()) { + executePlayerCommand(PLAYER_COMMAND_WAIT_FOR_CALLBACK); + setAudioFocusState(AUDIO_FOCUS_STATE_LOSS_TRANSIENT); + } else { + setAudioFocusState(AUDIO_FOCUS_STATE_LOSS_TRANSIENT_DUCK); + } + return; + default: + Log.w(TAG, "Unknown focus change type: " + focusChange); + } + } + + private void executePlayerCommand(@PlayerCommand int playerCommand) { + if (playerControl != null) { + playerControl.executePlayerCommand(playerCommand); + } + } + // Internal audio focus listener. private class AudioFocusListener implements AudioManager.OnAudioFocusChangeListener { @@ -450,7 +391,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public void onAudioFocusChange(int focusChange) { - eventHandler.post(() -> handleAudioFocusChange(focusChange)); + eventHandler.post(() -> handlePlatformAudioFocusChange(focusChange)); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index e926e90d22..43cedf985b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -1019,6 +1019,37 @@ public final class C { /** Network type for other connections which are not Wifi or cellular (e.g. VPN, Bluetooth). */ 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. + * + *

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. + * + *

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. + * + *

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 * #ROLE_FLAG_ALTERNATE}, {@link #ROLE_FLAG_SUPPLEMENTARY}, {@link #ROLE_FLAG_COMMENTARY}, {@link diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 240c6436c4..6fd23a93c5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -386,7 +386,7 @@ import java.util.concurrent.atomic.AtomicBoolean; playbackInfo = playbackInfo.copyWithPlaybackError(e); maybeNotifyPlaybackInfoChanged(); } catch (IOException e) { - Log.e(TAG, "Source error.", e); + Log.e(TAG, "Source error", e); stopInternal( /* forceResetRenderers= */ false, /* resetPositionAndState= */ false, @@ -394,7 +394,7 @@ import java.util.concurrent.atomic.AtomicBoolean; playbackInfo = playbackInfo.copyWithPlaybackError(ExoPlaybackException.createForSource(e)); maybeNotifyPlaybackInfoChanged(); } catch (RuntimeException | OutOfMemoryError e) { - Log.e(TAG, "Internal runtime error.", e); + Log.e(TAG, "Internal runtime error", e); ExoPlaybackException error = e instanceof OutOfMemoryError ? ExoPlaybackException.createForOutOfMemoryError((OutOfMemoryError) e) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 51d077270a..06743732e7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { /** The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.11.3"; + public static final String VERSION = "2.11.4"; /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.3"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.11.4"; /** * The version of the library expressed as an integer, for example 1002003. @@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2011003; + public static final int VERSION_INT = 2011004; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 52b686000a..839c65b124 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -325,6 +325,7 @@ public class SimpleExoPlayer extends BasePlayer private final AudioBecomingNoisyManager audioBecomingNoisyManager; private final AudioFocusManager audioFocusManager; private final WakeLockManager wakeLockManager; + private final WifiLockManager wifiLockManager; @Nullable private Format videoFormat; @Nullable private Format audioFormat; @@ -445,8 +446,8 @@ public class SimpleExoPlayer extends BasePlayer player = new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); analyticsCollector.setPlayer(player); - addListener(analyticsCollector); - addListener(componentListener); + player.addListener(analyticsCollector); + player.addListener(componentListener); videoDebugListeners.add(analyticsCollector); videoListeners.add(analyticsCollector); audioDebugListeners.add(analyticsCollector); @@ -460,6 +461,7 @@ public class SimpleExoPlayer extends BasePlayer new AudioBecomingNoisyManager(context, eventHandler, componentListener); audioFocusManager = new AudioFocusManager(context, eventHandler, componentListener); wakeLockManager = new WakeLockManager(context); + wifiLockManager = new WifiLockManager(context); } @Override @@ -684,11 +686,11 @@ public class SimpleExoPlayer extends BasePlayer } } + audioFocusManager.setAudioAttributes(handleAudioFocus ? audioAttributes : null); + boolean playWhenReady = getPlayWhenReady(); @AudioFocusManager.PlayerCommand - int playerCommand = - audioFocusManager.setAudioAttributes( - handleAudioFocus ? audioAttributes : null, getPlayWhenReady(), getPlaybackState()); - updatePlayWhenReady(getPlayWhenReady(), playerCommand); + int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState()); + updatePlayWhenReady(playWhenReady, playerCommand); } @Override @@ -1187,9 +1189,10 @@ public class SimpleExoPlayer extends BasePlayer } this.mediaSource = mediaSource; mediaSource.addEventListener(eventHandler, analyticsCollector); + boolean playWhenReady = getPlayWhenReady(); @AudioFocusManager.PlayerCommand - int playerCommand = audioFocusManager.handlePrepare(getPlayWhenReady()); - updatePlayWhenReady(getPlayWhenReady(), playerCommand); + int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, Player.STATE_BUFFERING); + updatePlayWhenReady(playWhenReady, playerCommand); player.prepare(mediaSource, resetPosition, resetState); } @@ -1197,7 +1200,7 @@ public class SimpleExoPlayer extends BasePlayer public void setPlayWhenReady(boolean playWhenReady) { verifyApplicationThread(); @AudioFocusManager.PlayerCommand - int playerCommand = audioFocusManager.handleSetPlayWhenReady(playWhenReady, getPlaybackState()); + int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState()); updatePlayWhenReady(playWhenReady, playerCommand); } @@ -1276,6 +1279,7 @@ public class SimpleExoPlayer extends BasePlayer @Override public void stop(boolean reset) { verifyApplicationThread(); + audioFocusManager.updateAudioFocus(getPlayWhenReady(), Player.STATE_IDLE); player.stop(reset); if (mediaSource != null) { mediaSource.removeEventListener(analyticsCollector); @@ -1284,7 +1288,6 @@ public class SimpleExoPlayer extends BasePlayer mediaSource = null; } } - audioFocusManager.handleStop(); currentCues = Collections.emptyList(); } @@ -1292,8 +1295,9 @@ public class SimpleExoPlayer extends BasePlayer public void release() { verifyApplicationThread(); audioBecomingNoisyManager.setEnabled(false); - audioFocusManager.handleStop(); wakeLockManager.setStayAwake(false); + wifiLockManager.setStayAwake(false); + audioFocusManager.release(); player.release(); removeSurfaceCallbacks(); if (surface != null) { @@ -1432,9 +1436,45 @@ public class SimpleExoPlayer extends BasePlayer * * @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. + * @deprecated Use {@link #setWakeMode(int)} instead. */ + @Deprecated 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. + * + *

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). + * + *

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. @@ -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 implements VideoRendererEventListener, AudioRendererEventListener, @@ -1781,16 +1839,7 @@ public class SimpleExoPlayer extends BasePlayer @Override public void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) { - switch (playbackState) { - case Player.STATE_READY: - case Player.STATE_BUFFERING: - wakeLockManager.setStayAwake(playWhenReady); - break; - case Player.STATE_ENDED: - case Player.STATE_IDLE: - wakeLockManager.setStayAwake(false); - break; - } + updateWakeAndWifiLock(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/WakeLockManager.java b/library/core/src/main/java/com/google/android/exoplayer2/WakeLockManager.java index f498eea6f4..6de302d62d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/WakeLockManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/WakeLockManager.java @@ -39,7 +39,8 @@ import com.google.android.exoplayer2.util.Log; private boolean stayAwake; 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; *

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. * - * @param enabled True if the player should handle a {@link WakeLock}, false otherwise. Please - * note that enabling this requires the {@link android.Manifest.permission#WAKE_LOCK} - * permission. + *

Enabling {@link WakeLock} requires the {@link android.Manifest.permission#WAKE_LOCK}. + * + * @param enabled True if the player should handle a {@link WakeLock}, false otherwise. */ public void setEnabled(boolean enabled) { if (enabled) { if (wakeLock == 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; } wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG); + wakeLock.setReferenceCounted(false); } } @@ -86,17 +88,14 @@ import com.google.android.exoplayer2.util.Log; // reasonable timeout that would not affect the user. @SuppressLint("WakelockTimeout") private void updateWakeLock() { - // Needed for the library nullness check. If enabled is true, the wakelock will not be null. - if (wakeLock != null) { - if (enabled) { - if (stayAwake && !wakeLock.isHeld()) { - wakeLock.acquire(); - } else if (!stayAwake && wakeLock.isHeld()) { - wakeLock.release(); - } - } else if (wakeLock.isHeld()) { - wakeLock.release(); - } + if (wakeLock == null) { + return; + } + + if (enabled && stayAwake) { + wakeLock.acquire(); + } else { + wakeLock.release(); } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/WifiLockManager.java b/library/core/src/main/java/com/google/android/exoplayer2/WifiLockManager.java new file mode 100644 index 0000000000..d3700a646a --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/WifiLockManager.java @@ -0,0 +1,94 @@ +/* + * 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} + * + *

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}. + * + *

By default, wifi lock handling is not enabled. Enabling will acquire the wifi lock if + * necessary. Disabling will release the wifi lock if held. + * + *

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}. + * + *

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(); + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java index 3430bfc1dd..a9fd9d8641 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/PlaybackStatsListener.java @@ -50,7 +50,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; *

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}. * - *

Playback stats are gathered separately for all playback session, i.e. each window in the + *

Playback stats are gathered separately for each playback session, i.e. each window in the * {@link Timeline} and each single ad. */ public final class PlaybackStatsListener @@ -931,6 +931,9 @@ public final class PlaybackStatsListener } private void maybeUpdateMediaTimeHistory(long realtimeMs, long mediaTimeMs) { + if (!keepHistory) { + return; + } if (currentPlaybackState != PlaybackStats.PLAYBACK_STATE_PLAYING) { if (mediaTimeMs == C.TIME_UNSET) { return; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 27abf486fa..ba31c118e7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -238,7 +238,7 @@ public final class DefaultAudioSink implements AudioSink { @Nullable private final AudioCapabilities audioCapabilities; private final AudioProcessorChain audioProcessorChain; - private final boolean enableConvertHighResIntPcmToFloat; + private final boolean enableFloatOutput; private final ChannelMappingAudioProcessor channelMappingAudioProcessor; private final TrimmingAudioProcessor trimmingAudioProcessor; private final AudioProcessor[] toIntPcmAvailableAudioProcessors; @@ -299,7 +299,7 @@ public final class DefaultAudioSink implements AudioSink { */ public DefaultAudioSink( @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 { * default capabilities (no encoded audio passthrough support) should be assumed. * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio before * output. May be empty. - * @param enableConvertHighResIntPcmToFloat Whether to enable conversion of high resolution - * integer PCM to 32-bit float for output, if possible. Functionality that uses 16-bit integer - * audio processing (for example, speed and pitch adjustment) will not be available when float - * output is in use. + * @param enableFloatOutput Whether to enable 32-bit float output. Where possible, 32-bit float + * output will be used if the input is 32-bit float, and also if the input is high resolution + * (24-bit or 32-bit) integer PCM. Audio processing (for example, speed adjustment) will not + * be available when float output is in use. */ public DefaultAudioSink( @Nullable AudioCapabilities audioCapabilities, AudioProcessor[] audioProcessors, - boolean enableConvertHighResIntPcmToFloat) { - this( - audioCapabilities, - new DefaultAudioProcessorChain(audioProcessors), - enableConvertHighResIntPcmToFloat); + boolean enableFloatOutput) { + this(audioCapabilities, new DefaultAudioProcessorChain(audioProcessors), enableFloatOutput); } /** @@ -332,18 +329,18 @@ public final class DefaultAudioSink implements AudioSink { * default capabilities (no encoded audio passthrough support) should be assumed. * @param audioProcessorChain An {@link AudioProcessorChain} which is used to apply playback * parameters adjustments. The instance passed in must not be reused in other sinks. - * @param enableConvertHighResIntPcmToFloat Whether to enable conversion of high resolution - * integer PCM to 32-bit float for output, if possible. Functionality that uses 16-bit integer - * audio processing (for example, speed and pitch adjustment) will not be available when float - * output is in use. + * @param enableFloatOutput Whether to enable 32-bit float output. Where possible, 32-bit float + * output will be used if the input is 32-bit float, and also if the input is high resolution + * (24-bit or 32-bit) integer PCM. Audio processing (for example, speed adjustment) will not + * be available when float output is in use. */ public DefaultAudioSink( @Nullable AudioCapabilities audioCapabilities, AudioProcessorChain audioProcessorChain, - boolean enableConvertHighResIntPcmToFloat) { + boolean enableFloatOutput) { this.audioCapabilities = audioCapabilities; this.audioProcessorChain = Assertions.checkNotNull(audioProcessorChain); - this.enableConvertHighResIntPcmToFloat = enableConvertHighResIntPcmToFloat; + this.enableFloatOutput = enableFloatOutput; releasingConditionVariable = new ConditionVariable(true); audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener()); channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); @@ -422,37 +419,34 @@ public final class DefaultAudioSink implements AudioSink { } boolean isInputPcm = Util.isEncodingLinearPcm(inputEncoding); - boolean processingEnabled = isInputPcm && inputEncoding != C.ENCODING_PCM_FLOAT; + boolean processingEnabled = isInputPcm; int sampleRate = inputSampleRate; int channelCount = inputChannelCount; @C.Encoding int encoding = inputEncoding; - boolean shouldConvertHighResIntPcmToFloat = - enableConvertHighResIntPcmToFloat + boolean useFloatOutput = + enableFloatOutput && supportsOutput(inputChannelCount, C.ENCODING_PCM_FLOAT) - && Util.isEncodingHighResolutionIntegerPcm(inputEncoding); + && Util.isEncodingHighResolutionPcm(inputEncoding); AudioProcessor[] availableAudioProcessors = - shouldConvertHighResIntPcmToFloat - ? toFloatPcmAvailableAudioProcessors - : toIntPcmAvailableAudioProcessors; + useFloatOutput ? toFloatPcmAvailableAudioProcessors : toIntPcmAvailableAudioProcessors; if (processingEnabled) { trimmingAudioProcessor.setTrimFrameCount(trimStartFrames, trimEndFrames); channelMappingAudioProcessor.setChannelMap(outputChannels); - AudioProcessor.AudioFormat inputAudioFormat = + AudioProcessor.AudioFormat outputFormat = new AudioProcessor.AudioFormat(sampleRate, channelCount, encoding); - AudioProcessor.AudioFormat outputAudioFormat = inputAudioFormat; for (AudioProcessor audioProcessor : availableAudioProcessors) { try { - outputAudioFormat = audioProcessor.configure(inputAudioFormat); + AudioProcessor.AudioFormat nextFormat = audioProcessor.configure(outputFormat); + if (audioProcessor.isActive()) { + outputFormat = nextFormat; + } } catch (UnhandledAudioFormatException e) { throw new ConfigurationException(e); } - if (audioProcessor.isActive()) { - inputAudioFormat = outputAudioFormat; - } } - sampleRate = outputAudioFormat.sampleRate; - channelCount = outputAudioFormat.channelCount; - encoding = outputAudioFormat.encoding; + sampleRate = outputFormat.sampleRate; + channelCount = outputFormat.channelCount; + encoding = outputFormat.encoding; } int outputChannelConfig = getChannelConfig(channelCount, isInputPcm); @@ -464,7 +458,7 @@ public final class DefaultAudioSink implements AudioSink { isInputPcm ? Util.getPcmFrameSize(inputEncoding, inputChannelCount) : C.LENGTH_UNSET; int outputPcmFrameSize = isInputPcm ? Util.getPcmFrameSize(encoding, channelCount) : C.LENGTH_UNSET; - boolean canApplyPlaybackParameters = processingEnabled && !shouldConvertHighResIntPcmToFloat; + boolean canApplyPlaybackParameters = processingEnabled && !useFloatOutput; Configuration pendingConfiguration = new Configuration( isInputPcm, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java index a75e675e6e..ca6b4f3f13 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/FloatResamplingAudioProcessor.java @@ -16,13 +16,19 @@ package com.google.android.exoplayer2.audio; 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 java.nio.ByteBuffer; /** - * An {@link AudioProcessor} that converts 24-bit and 32-bit integer PCM audio to 32-bit float PCM - * audio. + * An {@link AudioProcessor} that converts high resolution PCM audio to 32-bit float. The following + * encodings are supported as input: + * + *

    + *
  • {@link C#ENCODING_PCM_24BIT} + *
  • {@link C#ENCODING_PCM_32BIT} + *
  • {@link C#ENCODING_PCM_FLOAT} ({@link #isActive()} will return {@code false}) + *
*/ /* package */ final class FloatResamplingAudioProcessor extends BaseAudioProcessor { @@ -32,10 +38,11 @@ import java.nio.ByteBuffer; @Override public AudioFormat onConfigure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException { - if (!Util.isEncodingHighResolutionIntegerPcm(inputAudioFormat.encoding)) { + @C.PcmEncoding int encoding = inputAudioFormat.encoding; + if (!Util.isEncodingHighResolutionPcm(encoding)) { throw new UnhandledAudioFormatException(inputAudioFormat); } - return Util.isEncodingHighResolutionIntegerPcm(inputAudioFormat.encoding) + return encoding != C.ENCODING_PCM_FLOAT ? new AudioFormat( inputAudioFormat.sampleRate, inputAudioFormat.channelCount, C.ENCODING_PCM_FLOAT) : AudioFormat.NOT_SET; @@ -43,31 +50,42 @@ import java.nio.ByteBuffer; @Override public void queueInput(ByteBuffer inputBuffer) { - Assertions.checkState(Util.isEncodingHighResolutionIntegerPcm(inputAudioFormat.encoding)); - boolean isInput32Bit = inputAudioFormat.encoding == C.ENCODING_PCM_32BIT; int position = inputBuffer.position(); int limit = inputBuffer.limit(); int size = limit - position; - int resampledSize = isInput32Bit ? size : (size / 3) * 4; - ByteBuffer buffer = replaceOutputBuffer(resampledSize); - if (isInput32Bit) { - for (int i = position; i < limit; i += 4) { - int pcm32BitInteger = - (inputBuffer.get(i) & 0xFF) - | ((inputBuffer.get(i + 1) & 0xFF) << 8) - | ((inputBuffer.get(i + 2) & 0xFF) << 16) - | ((inputBuffer.get(i + 3) & 0xFF) << 24); - writePcm32BitFloat(pcm32BitInteger, buffer); - } - } else { // Input is 24-bit PCM. - for (int i = position; i < limit; i += 3) { - int pcm32BitInteger = - ((inputBuffer.get(i) & 0xFF) << 8) - | ((inputBuffer.get(i + 1) & 0xFF) << 16) - | ((inputBuffer.get(i + 2) & 0xFF) << 24); - writePcm32BitFloat(pcm32BitInteger, buffer); - } + ByteBuffer buffer; + switch (inputAudioFormat.encoding) { + case C.ENCODING_PCM_24BIT: + buffer = replaceOutputBuffer((size / 3) * 4); + for (int i = position; i < limit; i += 3) { + int pcm32BitInteger = + ((inputBuffer.get(i) & 0xFF) << 8) + | ((inputBuffer.get(i + 1) & 0xFF) << 16) + | ((inputBuffer.get(i + 2) & 0xFF) << 24); + writePcm32BitFloat(pcm32BitInteger, buffer); + } + break; + case C.ENCODING_PCM_32BIT: + buffer = replaceOutputBuffer(size); + for (int i = position; i < limit; i += 4) { + int pcm32BitInteger = + (inputBuffer.get(i) & 0xFF) + | ((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()); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java index 7175b93614..883f5bcb92 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/ResamplingAudioProcessor.java @@ -20,8 +20,17 @@ import com.google.android.exoplayer2.Format; import java.nio.ByteBuffer; /** - * An {@link AudioProcessor} that converts 8-bit, 24-bit and 32-bit integer PCM audio to 16-bit - * integer PCM audio. + * An {@link AudioProcessor} that converts different PCM audio encodings to 16-bit integer PCM. The + * following encodings are supported as input: + * + *
    + *
  • {@link C#ENCODING_PCM_8BIT} + *
  • {@link C#ENCODING_PCM_16BIT} ({@link #isActive()} will return {@code false}) + *
  • {@link C#ENCODING_PCM_16BIT_BIG_ENDIAN} + *
  • {@link C#ENCODING_PCM_24BIT} + *
  • {@link C#ENCODING_PCM_32BIT} + *
  • {@link C#ENCODING_PCM_FLOAT} + *
*/ /* package */ final class ResamplingAudioProcessor extends BaseAudioProcessor { @@ -33,7 +42,8 @@ import java.nio.ByteBuffer; && encoding != C.ENCODING_PCM_16BIT && encoding != C.ENCODING_PCM_16BIT_BIG_ENDIAN && encoding != C.ENCODING_PCM_24BIT - && encoding != C.ENCODING_PCM_32BIT) { + && encoding != C.ENCODING_PCM_32BIT + && encoding != C.ENCODING_PCM_FLOAT) { throw new UnhandledAudioFormatException(inputAudioFormat); } return encoding != C.ENCODING_PCM_16BIT @@ -60,10 +70,10 @@ import java.nio.ByteBuffer; resampledSize = (size / 3) * 2; break; case C.ENCODING_PCM_32BIT: + case C.ENCODING_PCM_FLOAT: resampledSize = size / 2; break; case C.ENCODING_PCM_16BIT: - case C.ENCODING_PCM_FLOAT: case C.ENCODING_INVALID: case Format.NO_VALUE: default: @@ -101,8 +111,16 @@ import java.nio.ByteBuffer; buffer.put(inputBuffer.get(i + 3)); } break; - case C.ENCODING_PCM_16BIT: 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 Format.NO_VALUE: default: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java index 9437e4ac26..8d84325d93 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/TrimmingAudioProcessor.java @@ -155,15 +155,16 @@ import java.nio.ByteBuffer; @Override protected void onFlush() { if (reconfigurationPending) { + // This is the initial flush after reconfiguration. Prepare to trim bytes from the start/end. reconfigurationPending = false; endBuffer = new byte[trimEndFrames * inputAudioFormat.bytesPerFrame]; pendingTrimStartBytes = trimStartFrames * inputAudioFormat.bytesPerFrame; } else { - // Audio processors are flushed after initial configuration, so we leave the pending trim - // start byte count unmodified if the processor was just configured. Otherwise we (possibly - // incorrectly) assume that this is a seek to a non-zero position. We should instead check the - // timestamp of the first input buffer queued after flushing to decide whether to trim (see - // also [Internal: b/77292509]). + // This is a flush during playback (after the initial flush). We assume this was caused by a + // seek to a non-zero position and clear pending start bytes. This assumption may be wrong (we + // may be seeking to zero), but playing data that should have been trimmed shouldn't be + // noticeable after a seek. Ideally we would check the timestamp of the first input buffer + // queued after flushing to decide whether to trim (see also [Internal: b/77292509]). pendingTrimStartBytes = 0; } endBufferSize = 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java index dff81021de..208989124a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/WavUtil.java @@ -61,6 +61,7 @@ public final class WavUtil { return TYPE_PCM; case C.ENCODING_PCM_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 Format.NO_VALUE: default: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java index 302ae0609f..bd5df4c8b1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderInputBuffer.java @@ -63,6 +63,14 @@ public class DecoderInputBuffer extends Buffer { /** The buffer's data, or {@code null} if no data has been set. */ @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. */ @@ -137,6 +145,7 @@ public class DecoderInputBuffer extends Buffer { } // Instantiate a new buffer if possible. ByteBuffer newData = createReplacementByteBuffer(requiredCapacity); + newData.order(data.order()); // Copy data up to the current position from the old buffer to the new one. if (position > 0) { data.flip(); @@ -182,6 +191,7 @@ public class DecoderInputBuffer extends Buffer { if (supplementalData != null) { supplementalData.clear(); } + waitingForKeys = false; } private ByteBuffer createReplacementByteBuffer(int requiredCapacity) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java index 03aabecb0e..4eef1ea32d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/SimpleDecoder.java @@ -149,6 +149,7 @@ public abstract class SimpleDecoder< while (!queuedOutputBuffers.isEmpty()) { queuedOutputBuffers.removeFirst().release(); } + exception = null; } } @@ -225,6 +226,7 @@ public abstract class SimpleDecoder< if (inputBuffer.isDecodeOnly()) { outputBuffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); } + @Nullable E exception; try { exception = decode(inputBuffer, outputBuffer, resetDecoder); } catch (RuntimeException e) { @@ -238,8 +240,9 @@ public abstract class SimpleDecoder< exception = createUnexpectedDecodeException(e); } 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; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index 8ac92b093c..56d1aeea4b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -341,14 +341,20 @@ public final class FrameworkMediaDrm implements ExoMediaDrm editedMaximumSize) { editedMaximumSize = sizes[j]; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java index 589b543170..bde575f39a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/AdtsReader.java @@ -345,42 +345,44 @@ public final class AdtsReader implements ElementaryStreamReader { } /** - * Returns whether the given syncPositionCandidate is a real SYNC word. - * - *

SYNC word pattern can occur within AAC data, so we perform a few checks to make sure this is - * really a SYNC word. This includes: + * 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. + * This method performs the following checks: * *

    - *
  • Checking if MPEG version of this frame matches the first detected version. - *
  • Checking if the sample rate index of this frame matches the first detected sample rate - * index. - *
  • Checking if the bytes immediately after the current package also match a SYNC-word. + *
  • The MPEG version of this frame must match the previously detected version. + *
  • The sample rate index of this frame must match the previously detected sample rate index. + *
  • The frame size must be at least 7 bytes + *
  • The bytes following the frame must be either another SYNC word with the same MPEG + * version, or the start of an ID3 header. *
* - * If the buffer runs out of data for any check, optimistically skip that check, because - * AdtsReader consumes each buffer as a whole. We will still run a header validity check later. + * With the exception of the first check, if there is insufficient data in the buffer then checks + * 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) { - // 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); if (!tryRead(pesBuffer, adtsScratch.data, 1)) { return false; } + // The MPEG version of this frame must match the previously detected version. adtsScratch.setPosition(4); int currentFrameVersion = adtsScratch.readBits(1); if (firstFrameVersion != VERSION_UNSET && currentFrameVersion != firstFrameVersion) { return false; } + // The sample rate index of this frame must match the previously detected sample rate index. if (firstFrameSampleRateIndex != C.INDEX_UNSET) { if (!tryRead(pesBuffer, adtsScratch.data, 1)) { + // Insufficient data for further checks. return true; } adtsScratch.setPosition(2); @@ -391,24 +393,50 @@ public final class AdtsReader implements ElementaryStreamReader { 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)) { + // Insufficient data for further checks. return true; } adtsScratch.setPosition(14); int frameSize = adtsScratch.readBits(13); - if (frameSize <= 6) { - // Not a frame. + if (frameSize < 7) { 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; - if (nextSyncPosition + 1 >= pesBuffer.limit()) { + if (nextSyncPosition >= dataLimit) { + // Insufficient data for further checks. return true; } - return (isAdtsSyncBytes(pesBuffer.data[nextSyncPosition], pesBuffer.data[nextSyncPosition + 1]) - && (firstFrameVersion == VERSION_UNSET - || ((pesBuffer.data[nextSyncPosition + 1] & 0x8) >> 3) == currentFrameVersion)); + if (data[nextSyncPosition] == (byte) 0xFF) { + if (nextSyncPosition + 1 == dataLimit) { + // 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) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java index 45a8c24e67..dd27294246 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/wav/WavExtractor.java @@ -259,14 +259,14 @@ public final class WavExtractor implements Extractor { public boolean sampleData(ExtractorInput input, long bytesLeft) throws IOException, InterruptedException { // Write sample data until we've reached the target sample size, or the end of the data. - boolean endOfSampleData = bytesLeft == 0; - while (!endOfSampleData && pendingOutputBytes < targetSampleSizeBytes) { + while (bytesLeft > 0 && pendingOutputBytes < targetSampleSizeBytes) { int bytesToRead = (int) Math.min(targetSampleSizeBytes - pendingOutputBytes, bytesLeft); int bytesAppended = trackOutput.sampleData(input, bytesToRead, true); if (bytesAppended == RESULT_END_OF_INPUT) { - endOfSampleData = true; + bytesLeft = 0; } else { pendingOutputBytes += bytesAppended; + bytesLeft -= bytesAppended; } } @@ -288,7 +288,7 @@ public final class WavExtractor implements Extractor { pendingOutputBytes = offset; } - return endOfSampleData; + return bytesLeft <= 0; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 4830b78760..e1026ed196 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -344,6 +344,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private boolean codecNeedsReconfigureWorkaround; private boolean codecNeedsDiscardToSpsWorkaround; private boolean codecNeedsFlushWorkaround; + private boolean codecNeedsSosFlushWorkaround; private boolean codecNeedsEosFlushWorkaround; private boolean codecNeedsEosOutputExceptionWorkaround; private boolean codecNeedsMonoChannelCountWorkaround; @@ -364,6 +365,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @DrainAction private int codecDrainAction; private boolean codecReceivedBuffers; private boolean codecReceivedEos; + private boolean codecHasOutputMediaFormat; private long largestQueuedPresentationTimeUs; private long lastBufferInStreamPresentationTimeUs; private boolean inputStreamEnded; @@ -652,6 +654,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { availableCodecInfos = null; codecInfo = null; codecFormat = null; + codecHasOutputMediaFormat = false; resetInputBuffer(); resetOutputBuffer(); resetCodecBuffers(); @@ -765,6 +768,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } if (codecDrainAction == DRAIN_ACTION_REINITIALIZE || codecNeedsFlushWorkaround + || (codecNeedsSosFlushWorkaround && !codecHasOutputMediaFormat) || (codecNeedsEosFlushWorkaround && codecReceivedEos)) { releaseCodec(); return true; @@ -944,6 +948,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecNeedsReconfigureWorkaround = codecNeedsReconfigureWorkaround(codecName); codecNeedsDiscardToSpsWorkaround = codecNeedsDiscardToSpsWorkaround(codecName, codecFormat); codecNeedsFlushWorkaround = codecNeedsFlushWorkaround(codecName); + codecNeedsSosFlushWorkaround = codecNeedsSosFlushWorkaround(codecName); codecNeedsEosFlushWorkaround = codecNeedsEosFlushWorkaround(codecName); codecNeedsEosOutputExceptionWorkaround = codecNeedsEosOutputExceptionWorkaround(codecName); codecNeedsMonoChannelCountWorkaround = @@ -1610,6 +1615,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { /** Processes a new output {@link MediaFormat}. */ private void processOutputFormat() throws ExoPlaybackException { + codecHasOutputMediaFormat = true; MediaFormat mediaFormat = codec.getOutputFormat(); if (codecAdaptationWorkaroundMode != ADAPTATION_WORKAROUND_MODE_NEVER && mediaFormat.getInteger(MediaFormat.KEY_WIDTH) == ADAPTATION_WORKAROUND_SLICE_WIDTH_HEIGHT @@ -1989,4 +1995,20 @@ public abstract class MediaCodecRenderer extends BaseRenderer { && "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}. + * + *

If true is returned, the renderer will work around the issue by instantiating a new decoder + * when this case occurs. + * + *

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); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index bb6c52d253..2b8dcc0a55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -25,6 +25,7 @@ import android.util.Pair; import android.util.SparseIntArray; import androidx.annotation.CheckResult; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Log; @@ -289,9 +290,16 @@ public final class MediaCodecUtil { // Note: MediaCodecList is sorted by the framework such that the best decoders come first. for (int i = 0; i < numberOfCodecs; 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(); - @Nullable - String codecMimeType = getCodecMimeType(codecInfo, name, secureDecodersExplicit, mimeType); + if (!isCodecUsableDecoder(codecInfo, name, secureDecodersExplicit, mimeType)) { + continue; + } + @Nullable String codecMimeType = getCodecMimeType(codecInfo, name, mimeType); if (codecMimeType == null) { continue; } @@ -373,7 +381,6 @@ public final class MediaCodecUtil { * * @param info The codec information. * @param name The name of the codec - * @param secureDecodersExplicit Whether secure decoders were explicitly listed, if present. * @param mimeType The MIME type. * @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} @@ -383,12 +390,7 @@ public final class MediaCodecUtil { private static String getCodecMimeType( android.media.MediaCodecInfo info, String name, - boolean secureDecodersExplicit, String mimeType) { - if (!isCodecUsableDecoder(info, name, secureDecodersExplicit, mimeType)) { - return null; - } - String[] supportedTypes = info.getSupportedTypes(); for (String supportedType : supportedTypes) { if (supportedType.equalsIgnoreCase(mimeType)) { @@ -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+, * or a best-effort approximation for lower levels. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index d176b1905c..6707c1e496 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -245,8 +245,8 @@ public final class DownloadHelper { * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are * selected. - * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by - * {@code renderersFactory}. + * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which + * tracks can be selected. * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for * downloading. * @return A {@link DownloadHelper} for DASH streams. @@ -315,8 +315,8 @@ public final class DownloadHelper { * @param dataSourceFactory A {@link DataSource.Factory} used to load the playlist. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are * selected. - * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by - * {@code renderersFactory}. + * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which + * tracks can be selected. * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for * downloading. * @return A {@link DownloadHelper} for HLS streams. @@ -385,8 +385,8 @@ public final class DownloadHelper { * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are * selected. - * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by - * {@code renderersFactory}. + * @param drmSessionManager An optional {@link DrmSessionManager}. Used to help determine which + * tracks can be selected. * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for * downloading. * @return A {@link DownloadHelper} for SmoothStreaming streams. @@ -414,27 +414,27 @@ public final class DownloadHelper { /** * Equivalent to {@link #createMediaSource(DownloadRequest, Factory, DrmSessionManager) - * createMediaSource(downloadRequest, dataSourceFactory, - * DrmSessionManager.getDummyDrmSessionManager())}. + * createMediaSource(downloadRequest, dataSourceFactory, null)}. */ public static MediaSource createMediaSource( DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory) { - return createMediaSource( - downloadRequest, dataSourceFactory, DrmSessionManager.getDummyDrmSessionManager()); + return createMediaSource(downloadRequest, dataSourceFactory, /* drmSessionManager= */ null); } /** - * 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}. * * @param downloadRequest A {@link DownloadRequest}. * @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( DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory, - DrmSessionManager drmSessionManager) { + @Nullable DrmSessionManager drmSessionManager) { @Nullable Constructor constructor; switch (downloadRequest.type) { case DownloadRequest.TYPE_DASH: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index b1ab5ac7c6..819478b80e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -595,7 +595,7 @@ public abstract class DownloadService extends Service { } @Override - public int onStartCommand(Intent intent, int flags, int startId) { + public int onStartCommand(@Nullable Intent intent, int flags, int startId) { lastStartId = startId; taskRemoved = false; @Nullable String intentAction = null; @@ -617,7 +617,9 @@ public abstract class DownloadService extends Service { // Do nothing. break; 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) { Log.e(TAG, "Ignored ADD_DOWNLOAD: Missing " + KEY_DOWNLOAD_REQUEST + " extra"); } else { @@ -642,7 +644,7 @@ public abstract class DownloadService extends Service { downloadManager.pauseDownloads(); break; 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"); } else { int stopReason = intent.getIntExtra(KEY_STOP_REASON, /* defaultValue= */ 0); @@ -650,7 +652,9 @@ public abstract class DownloadService extends Service { } break; case ACTION_SET_REQUIREMENTS: - @Nullable Requirements requirements = intent.getParcelableExtra(KEY_REQUIREMENTS); + @Nullable + Requirements requirements = + Assertions.checkNotNull(intent).getParcelableExtra(KEY_REQUIREMENTS); if (requirements == null) { Log.e(TAG, "Ignored SET_REQUIREMENTS: Missing " + KEY_REQUIREMENTS + " extra"); } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java index 8aafb9a0e5..4385a41ff3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaPeriod.java @@ -324,7 +324,8 @@ public final class ClippingMediaPeriod implements MediaPeriod, MediaPeriod.Callb if (endUs != C.TIME_END_OF_SOURCE && ((result == C.RESULT_BUFFER_READ && buffer.timeUs >= endUs) || (result == C.RESULT_NOTHING_READ - && getBufferedPositionUs() == C.TIME_END_OF_SOURCE))) { + && getBufferedPositionUs() == C.TIME_END_OF_SOURCE + && !buffer.waitingForKeys))) { buffer.clear(); buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); sentEos = true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index d4e447bc61..b5cfe6ed72 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -539,7 +539,7 @@ public class SampleQueue implements TrackOutput { boolean loadingFinished, long decodeOnlyUntilUs, SampleExtrasHolder extrasHolder) { - + buffer.waitingForKeys = false; // 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. boolean hasNextSample; @@ -573,6 +573,7 @@ public class SampleQueue implements TrackOutput { } if (!mayReadSample(relativeReadIndex)) { + buffer.waitingForKeys = true; return C.RESULT_NOTHING_READ; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index 058b1c4526..46c26db122 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -23,11 +23,11 @@ import androidx.annotation.IntDef; import androidx.annotation.Nullable; import com.google.android.exoplayer2.BaseRenderer; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.RendererCapabilities; 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.Util; import java.lang.annotation.Documented; @@ -45,6 +45,8 @@ import java.util.List; */ public final class TextRenderer extends BaseRenderer implements Callback { + private static final String TAG = "TextRenderer"; + @Documented @Retention(RetentionPolicy.SOURCE) @IntDef({ @@ -143,19 +145,13 @@ public final class TextRenderer extends BaseRenderer implements Callback { @Override protected void onPositionReset(long positionUs, boolean joining) { - clearOutput(); inputStreamEnded = false; outputStreamEnded = false; - if (decoderReplacementState != REPLACEMENT_STATE_NONE) { - replaceDecoder(); - } else { - releaseBuffers(); - decoder.flush(); - } + resetOutputAndDecoder(); } @Override - public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + public void render(long positionUs, long elapsedRealtimeUs) { if (outputStreamEnded) { return; } @@ -165,7 +161,8 @@ public final class TextRenderer extends BaseRenderer implements Callback { try { nextSubtitle = decoder.dequeueOutputBuffer(); } catch (SubtitleDecoderException e) { - throw createRendererException(e, streamFormat); + handleDecoderError(e); + return; } } @@ -247,7 +244,8 @@ public final class TextRenderer extends BaseRenderer implements Callback { } } } catch (SubtitleDecoderException e) { - throw createRendererException(e, streamFormat); + handleDecoderError(e); + return; } } @@ -329,4 +327,24 @@ public final class TextRenderer extends BaseRenderer implements Callback { output.onCues(cues); } + /** + * Called when {@link #decoder} throws an exception, so it can be logged and playback can + * continue. + * + *

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(); + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java index 0c402ac018..cef7e3f53f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/subrip/SubripDecoder.java @@ -41,10 +41,12 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { 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 = 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 String SUBRIP_ALIGNMENT_TAG = "\\{\\\\an[1-9]\\}"; @@ -229,10 +231,14 @@ public final class SubripDecoder extends SimpleSubtitleDecoder { } 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 + 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; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index ae115ab58c..ec11ad2348 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -279,8 +279,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou try { connection = makeConnection(dataSpec); } catch (IOException e) { - throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, - dataSpec, HttpDataSourceException.TYPE_OPEN); + throw new HttpDataSourceException( + "Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN); } String responseMessage; @@ -289,8 +289,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou responseMessage = connection.getResponseMessage(); } catch (IOException e) { closeConnectionQuietly(); - throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, - dataSpec, HttpDataSourceException.TYPE_OPEN); + throw new HttpDataSourceException( + "Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN); } // Check for a valid response code. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java index 0a303c1df7..9d145caee5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.util; import android.os.SystemClock; +import android.text.TextUtils; import android.view.Surface; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -198,7 +199,7 @@ public class EventLogger implements AnalyticsListener { logd(eventTime, "tracks", "[]"); return; } - logd("tracks [" + getEventTimeString(eventTime) + ", "); + logd("tracks [" + getEventTimeString(eventTime)); // Log tracks associated to renderers. int rendererCount = mappedTrackInfo.getRendererCount(); for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { @@ -282,7 +283,7 @@ public class EventLogger implements AnalyticsListener { @Override public void onMetadata(EventTime eventTime, Metadata metadata) { - logd("metadata [" + getEventTimeString(eventTime) + ", "); + logd("metadata [" + getEventTimeString(eventTime)); printMetadata(metadata, " "); logd("]"); } @@ -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 tr The exception to log. */ - protected void loge(String msg, @Nullable Throwable tr) { - Log.e(tag, msg, tr); + protected void loge(String msg) { + Log.e(tag, msg); } // Internal methods 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) { - logd(getEventString(eventTime, eventName, eventDescription)); + logd(getEventString(eventTime, eventName, eventDescription, /* throwable= */ null)); } 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( @@ -497,7 +497,7 @@ public class EventLogger implements AnalyticsListener { String eventName, String eventDescription, @Nullable Throwable throwable) { - loge(getEventString(eventTime, eventName, eventDescription), throwable); + loge(getEventString(eventTime, eventName, eventDescription, throwable)); } private void printInternalError(EventTime eventTime, String type, Exception e) { @@ -510,12 +510,21 @@ public class EventLogger implements AnalyticsListener { } } - private String getEventString(EventTime eventTime, String eventName) { - return eventName + " [" + getEventTimeString(eventTime) + "]"; - } - - private String getEventString(EventTime eventTime, String eventName, String eventDescription) { - return eventName + " [" + getEventTimeString(eventTime) + ", " + eventDescription + "]"; + private String getEventString( + EventTime eventTime, + String eventName, + @Nullable String eventDescription, + @Nullable Throwable throwable) { + 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) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Log.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Log.java index a29460b84c..e5e6f88d4d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Log.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Log.java @@ -21,6 +21,7 @@ import androidx.annotation.Nullable; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.net.UnknownHostException; /** Wrapper around {@link android.util.Log} which allows to set the log level. */ 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. */ @@ -86,11 +88,7 @@ public final class Log { /** @see android.util.Log#d(String, String, Throwable) */ public static void d(String tag, String message, @Nullable Throwable throwable) { - if (!logStackTraces) { - d(tag, appendThrowableMessage(message, throwable)); - } else if (logLevel == LOG_LEVEL_ALL) { - android.util.Log.d(tag, message, throwable); - } + d(tag, appendThrowableString(message, throwable)); } /** @see android.util.Log#i(String, String) */ @@ -102,11 +100,7 @@ public final class Log { /** @see android.util.Log#i(String, String, Throwable) */ public static void i(String tag, String message, @Nullable Throwable throwable) { - if (!logStackTraces) { - i(tag, appendThrowableMessage(message, throwable)); - } else if (logLevel <= LOG_LEVEL_INFO) { - android.util.Log.i(tag, message, throwable); - } + i(tag, appendThrowableString(message, throwable)); } /** @see android.util.Log#w(String, String) */ @@ -118,11 +112,7 @@ public final class Log { /** @see android.util.Log#w(String, String, Throwable) */ public static void w(String tag, String message, @Nullable Throwable throwable) { - if (!logStackTraces) { - w(tag, appendThrowableMessage(message, throwable)); - } else if (logLevel <= LOG_LEVEL_WARNING) { - android.util.Log.w(tag, message, throwable); - } + w(tag, appendThrowableString(message, throwable)); } /** @see android.util.Log#e(String, String) */ @@ -134,18 +124,54 @@ public final class Log { /** @see android.util.Log#e(String, String, Throwable) */ public static void e(String tag, String message, @Nullable Throwable throwable) { - if (!logStackTraces) { - e(tag, appendThrowableMessage(message, throwable)); - } else if (logLevel <= LOG_LEVEL_ERROR) { - android.util.Log.e(tag, message, throwable); + e(tag, appendThrowableString(message, throwable)); + } + + /** + * Returns a string representation of a {@link Throwable} suitable for logging, taking into + * account whether {@link #setLogStackTraces(boolean)} stack trace logging} is enabled. + * + *

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) { + 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 appendThrowableMessage(String message, @Nullable Throwable throwable) { - if (throwable == null) { - return message; + 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'; } - String throwableMessage = throwable.getMessage(); - return TextUtils.isEmpty(throwableMessage) ? message : message + " - " + throwableMessage; + return message; + } + + private static boolean isCausedByUnknownHostException(@Nullable Throwable throwable) { + while (throwable != null) { + if (throwable instanceof UnknownHostException) { + return true; + } + throwable = throwable.getCause(); + } + return false; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java index e61ab83777..c3ca9257d6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/MimeTypes.java @@ -122,22 +122,22 @@ public final class MimeTypes { 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) { 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) { 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) { 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) { return BASE_TYPE_APPLICATION.equals(getTopLevelType(mimeType)); } @@ -173,13 +173,14 @@ public final class MimeTypes { * @param codecs The codecs attribute. * @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) { return null; } String[] codecList = Util.splitCodecs(codecs); for (String codec : codecList) { - String mimeType = getMediaMimeType(codec); + @Nullable String mimeType = getMediaMimeType(codec); if (mimeType != null && isVideo(mimeType)) { return mimeType; } @@ -193,13 +194,14 @@ public final class MimeTypes { * @param codecs The codecs attribute. * @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) { return null; } String[] codecList = Util.splitCodecs(codecs); for (String codec : codecList) { - String mimeType = getMediaMimeType(codec); + @Nullable String mimeType = getMediaMimeType(codec); if (mimeType != null && isAudio(mimeType)) { return mimeType; } @@ -213,7 +215,8 @@ public final class MimeTypes { * @param codec The codec identifier to derive. * @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) { return null; } @@ -234,7 +237,7 @@ public final class MimeTypes { } else if (codec.startsWith("vp8") || codec.startsWith("vp08")) { return MimeTypes.VIDEO_VP8; } else if (codec.startsWith("mp4a")) { - String mimeType = null; + @Nullable String mimeType = null; if (codec.startsWith("mp4a.")) { String objectTypeString = codec.substring(5); // remove the 'mp4a.' prefix if (objectTypeString.length() >= 2) { @@ -243,7 +246,7 @@ public final class MimeTypes { int objectTypeInt = Integer.parseInt(objectTypeHexString, 16); mimeType = getMimeTypeFromMp4ObjectType(objectTypeInt); } catch (NumberFormatException ignored) { - // ignored + // Ignored. } } } @@ -266,6 +269,10 @@ public final class MimeTypes { return MimeTypes.AUDIO_VORBIS; } else if (codec.startsWith("flac")) { return MimeTypes.AUDIO_FLAC; + } else if (codec.startsWith("stpp")) { + return MimeTypes.APPLICATION_TTML; + } else if (codec.startsWith("wvtt")) { + return MimeTypes.TEXT_VTT; } else { return getCustomMimeTypeForCodec(codec); } @@ -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 * 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) { return null; } @@ -416,7 +424,8 @@ public final class MimeTypes { return mimeType.substring(0, indexOfSlash); } - private static @Nullable String getCustomMimeTypeForCodec(String codec) { + @Nullable + private static String getCustomMimeTypeForCodec(String codec) { int customMimeTypeCount = customMimeTypes.size(); for (int i = 0; i < customMimeTypeCount; i++) { CustomMimeType customMimeType = customMimeTypes.get(i); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 5518eeaf36..ea43ee7bb3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -1360,13 +1360,15 @@ public final class Util { } /** - * Returns whether {@code encoding} is high resolution (> 16-bit) integer PCM. + * Returns whether {@code encoding} is high resolution (> 16-bit) PCM. * * @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) { - return encoding == C.ENCODING_PCM_24BIT || encoding == C.ENCODING_PCM_32BIT; + public static boolean isEncodingHighResolutionPcm(@C.PcmEncoding int encoding) { + return encoding == C.ENCODING_PCM_24BIT + || encoding == C.ENCODING_PCM_32BIT + || encoding == C.ENCODING_PCM_FLOAT; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 4e72a1b3d7..25dc09be81 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -1000,6 +1000,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { processOutputFormat(getCodec(), format.width, format.height); } maybeNotifyVideoSizeChanged(); + decoderCounters.renderedOutputBufferCount++; maybeNotifyRenderedFirstFrame(); onProcessedOutputBuffer(presentationTimeUs); } diff --git a/library/core/src/test/assets/subrip/typical b/library/core/src/test/assets/subrip/typical index 1331f75651..cc9a3da871 100644 --- a/library/core/src/test/assets/subrip/typical +++ b/library/core/src/test/assets/subrip/typical @@ -8,5 +8,5 @@ This is the second subtitle. Second subtitle with second line. 3 -00:00:04,567 --> 00:00:08,901 +02:00:04,567 --> 02:00:08,901 This is the third subtitle. diff --git a/library/core/src/test/assets/subrip/typical_extra_blank_line b/library/core/src/test/assets/subrip/typical_extra_blank_line index f5882a1d68..392cb7e91c 100644 --- a/library/core/src/test/assets/subrip/typical_extra_blank_line +++ b/library/core/src/test/assets/subrip/typical_extra_blank_line @@ -9,5 +9,5 @@ This is the second subtitle. Second subtitle with second line. 3 -00:00:04,567 --> 00:00:08,901 +02:00:04,567 --> 02:00:08,901 This is the third subtitle. diff --git a/library/core/src/test/assets/subrip/typical_missing_sequence b/library/core/src/test/assets/subrip/typical_missing_sequence index 56d49ac63c..e75711d7a8 100644 --- a/library/core/src/test/assets/subrip/typical_missing_sequence +++ b/library/core/src/test/assets/subrip/typical_missing_sequence @@ -7,5 +7,5 @@ This is the second subtitle. Second subtitle with second line. 3 -00:00:04,567 --> 00:00:08,901 +02:00:04,567 --> 02:00:08,901 This is the third subtitle. diff --git a/library/core/src/test/assets/subrip/typical_missing_timecode b/library/core/src/test/assets/subrip/typical_missing_timecode index cd25ffca3b..bce61a77f9 100644 --- a/library/core/src/test/assets/subrip/typical_missing_timecode +++ b/library/core/src/test/assets/subrip/typical_missing_timecode @@ -7,13 +7,13 @@ This is the second subtitle. Second subtitle with second line. 3 -00:00:04,567 --> 00:00:08,901 +02:00:04,567 --> 02:00:08,901 This is the third subtitle. 4 - --> 00:00:10,901 + --> 02:00:10,901 This is the fourth subtitle. 5 -00:00:12,901 --> +02:00:12,901 --> This is the fifth subtitle. diff --git a/library/core/src/test/assets/subrip/typical_negative_timestamps b/library/core/src/test/assets/subrip/typical_negative_timestamps index 2a47c0993b..1df7bf68e3 100644 --- a/library/core/src/test/assets/subrip/typical_negative_timestamps +++ b/library/core/src/test/assets/subrip/typical_negative_timestamps @@ -8,5 +8,5 @@ This is the second subtitle. Second subtitle with second line. 3 -00:00:04,567 --> 00:00:08,901 +02:00:04,567 --> 02:00:08,901 This is the third subtitle. diff --git a/library/core/src/test/assets/subrip/typical_no_hours_and_millis b/library/core/src/test/assets/subrip/typical_no_hours_and_millis new file mode 100644 index 0000000000..5340fc72c2 --- /dev/null +++ b/library/core/src/test/assets/subrip/typical_no_hours_and_millis @@ -0,0 +1,12 @@ +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. diff --git a/library/core/src/test/assets/subrip/typical_with_byte_order_mark b/library/core/src/test/assets/subrip/typical_with_byte_order_mark index 4f5b32f4d7..050e1c02a6 100644 --- a/library/core/src/test/assets/subrip/typical_with_byte_order_mark +++ b/library/core/src/test/assets/subrip/typical_with_byte_order_mark @@ -8,5 +8,5 @@ This is the second subtitle. Second subtitle with second line. 3 -00:00:04,567 --> 00:00:08,901 +02:00:04,567 --> 02:00:08,901 This is the third subtitle. diff --git a/library/core/src/test/assets/ts/sample_with_id3.adts b/library/core/src/test/assets/ts/sample_with_id3.adts new file mode 100644 index 0000000000..690fe90bd0 Binary files /dev/null and b/library/core/src/test/assets/ts/sample_with_id3.adts differ diff --git a/library/core/src/test/assets/ts/sample_with_id3.adts.0.dump b/library/core/src/test/assets/ts/sample_with_id3.adts.0.dump new file mode 100644 index 0000000000..b9cde05d69 --- /dev/null +++ b/library/core/src/test/assets/ts/sample_with_id3.adts.0.dump @@ -0,0 +1,641 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 0: + format: + bitrate = -1 + id = 0 + containerMimeType = null + sampleMimeType = audio/mp4a-latm + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + metadata = null + initializationData: + data = length 2, hash 5F7 + total output bytes = 30797 + sample count = 144 + sample 0: + time = 0 + flags = 1 + data = length 23, hash 47DE9131 + sample 1: + time = 23219 + flags = 1 + data = length 6, hash 31CF3A46 + sample 2: + time = 46438 + flags = 1 + data = length 6, hash 31CF3A46 + sample 3: + time = 69657 + flags = 1 + data = length 6, hash 31CF3A46 + sample 4: + time = 92876 + flags = 1 + data = length 6, hash 31EC5206 + sample 5: + time = 116095 + flags = 1 + data = length 171, hash 4F6478F6 + sample 6: + time = 139314 + flags = 1 + data = length 202, hash AF4068A3 + sample 7: + time = 162533 + flags = 1 + data = length 210, hash E4C10618 + sample 8: + time = 185752 + flags = 1 + data = length 217, hash 9ECCD0D9 + sample 9: + time = 208971 + flags = 1 + data = length 212, hash 6BAC2CD9 + sample 10: + time = 232190 + flags = 1 + data = length 223, hash 188B6010 + sample 11: + time = 255409 + flags = 1 + data = length 222, hash C1A04D0C + sample 12: + time = 278628 + flags = 1 + data = length 220, hash D65F9768 + sample 13: + time = 301847 + flags = 1 + data = length 227, hash B96C9E14 + sample 14: + time = 325066 + flags = 1 + data = length 229, hash 9FB09972 + sample 15: + time = 348285 + flags = 1 + data = length 220, hash 2271F053 + sample 16: + time = 371504 + flags = 1 + data = length 226, hash 5EDD2F4F + sample 17: + time = 394723 + flags = 1 + data = length 239, hash 957510E0 + sample 18: + time = 417942 + flags = 1 + data = length 224, hash 718A8F47 + sample 19: + time = 441161 + flags = 1 + data = length 225, hash 5E11E293 + sample 20: + time = 464380 + flags = 1 + data = length 227, hash FCE50D27 + sample 21: + time = 487599 + flags = 1 + data = length 212, hash 77908C40 + sample 22: + time = 510818 + flags = 1 + data = length 227, hash 34C4EB32 + sample 23: + time = 534037 + flags = 1 + data = length 231, hash 95488307 + sample 24: + time = 557256 + flags = 1 + data = length 226, hash 97F12D6F + sample 25: + time = 580475 + flags = 1 + data = length 236, hash 91A9D9A2 + sample 26: + time = 603694 + flags = 1 + data = length 227, hash 27A608F9 + sample 27: + time = 626913 + flags = 1 + data = length 229, hash 57DAAE4 + sample 28: + time = 650132 + flags = 1 + data = length 235, hash ED30AC34 + sample 29: + time = 673351 + flags = 1 + data = length 227, hash BD3D6280 + sample 30: + time = 696570 + flags = 1 + data = length 233, hash 694B1087 + sample 31: + time = 719789 + flags = 1 + data = length 232, hash 1EDFE047 + sample 32: + time = 743008 + flags = 1 + data = length 228, hash E2A831F4 + sample 33: + time = 766227 + flags = 1 + data = length 231, hash 757E6012 + sample 34: + time = 789446 + flags = 1 + data = length 223, hash 4003D791 + sample 35: + time = 812665 + flags = 1 + data = length 232, hash 3CF9A07C + sample 36: + time = 835884 + flags = 1 + data = length 228, hash 25AC3FF7 + sample 37: + time = 859103 + flags = 1 + data = length 220, hash 2C1824CE + sample 38: + time = 882322 + flags = 1 + data = length 229, hash 46FDD8FB + sample 39: + time = 905541 + flags = 1 + data = length 237, hash F6988018 + sample 40: + time = 928760 + flags = 1 + data = length 242, hash 60436B6B + sample 41: + time = 951979 + flags = 1 + data = length 275, hash 90EDFA8E + sample 42: + time = 975198 + flags = 1 + data = length 242, hash 5C86EFCB + sample 43: + time = 998417 + flags = 1 + data = length 233, hash E0A51B82 + sample 44: + time = 1021636 + flags = 1 + data = length 235, hash 590DF14F + sample 45: + time = 1044855 + flags = 1 + data = length 238, hash 69AF4E6E + sample 46: + time = 1068074 + flags = 1 + data = length 235, hash E745AE8D + sample 47: + time = 1091293 + flags = 1 + data = length 223, hash 295F2A13 + sample 48: + time = 1114512 + flags = 1 + data = length 228, hash E2F47B21 + sample 49: + time = 1137731 + flags = 1 + data = length 229, hash 262C3CFE + sample 50: + time = 1160950 + flags = 1 + data = length 232, hash 4B5BF5E8 + sample 51: + time = 1184169 + flags = 1 + data = length 233, hash F3D80836 + sample 52: + time = 1207388 + flags = 1 + data = length 237, hash 32E0A11E + sample 53: + time = 1230607 + flags = 1 + data = length 228, hash E1B89F13 + sample 54: + time = 1253826 + flags = 1 + data = length 237, hash 8BDD9E38 + sample 55: + time = 1277045 + flags = 1 + data = length 235, hash 3C84161F + sample 56: + time = 1300264 + flags = 1 + data = length 227, hash A47E1789 + sample 57: + time = 1323483 + flags = 1 + data = length 228, hash 869FDFD3 + sample 58: + time = 1346702 + flags = 1 + data = length 233, hash 272ECE2 + sample 59: + time = 1369921 + flags = 1 + data = length 227, hash DB6B9618 + sample 60: + time = 1393140 + flags = 1 + data = length 212, hash 63214325 + sample 61: + time = 1416359 + flags = 1 + data = length 221, hash 9BA588A1 + sample 62: + time = 1439578 + flags = 1 + data = length 225, hash 21EFD50C + sample 63: + time = 1462797 + flags = 1 + data = length 231, hash F3AD0BF + sample 64: + time = 1486016 + flags = 1 + data = length 224, hash 822C9210 + sample 65: + time = 1509235 + flags = 1 + data = length 195, hash D4EF53EE + sample 66: + time = 1532454 + flags = 1 + data = length 195, hash A816647A + sample 67: + time = 1555673 + flags = 1 + data = length 184, hash 9A2B7E6 + sample 68: + time = 1578892 + flags = 1 + data = length 210, hash 956E3600 + sample 69: + time = 1602111 + flags = 1 + data = length 234, hash 35CFDA0A + sample 70: + time = 1625330 + flags = 1 + data = length 239, hash 9E15AC1E + sample 71: + time = 1648549 + flags = 1 + data = length 228, hash F3B70641 + sample 72: + time = 1671768 + flags = 1 + data = length 237, hash 124E3194 + sample 73: + time = 1694987 + flags = 1 + data = length 231, hash 950CD7C8 + sample 74: + time = 1718206 + flags = 1 + data = length 236, hash A12E49AF + sample 75: + time = 1741425 + flags = 1 + data = length 242, hash 43BC9C24 + sample 76: + time = 1764644 + flags = 1 + data = length 241, hash DCF0B17 + sample 77: + time = 1787863 + flags = 1 + data = length 251, hash C0B99968 + sample 78: + time = 1811082 + flags = 1 + data = length 245, hash 9B38ED1C + sample 79: + time = 1834301 + flags = 1 + data = length 238, hash 1BA69079 + sample 80: + time = 1857520 + flags = 1 + data = length 233, hash 44C8C6BF + sample 81: + time = 1880739 + flags = 1 + data = length 231, hash EABBEE02 + sample 82: + time = 1903958 + flags = 1 + data = length 226, hash D09C44FB + sample 83: + time = 1927177 + flags = 1 + data = length 235, hash BE6A6608 + sample 84: + time = 1950396 + flags = 1 + data = length 235, hash 2735F454 + sample 85: + time = 1973615 + flags = 1 + data = length 238, hash B160DFE7 + sample 86: + time = 1996834 + flags = 1 + data = length 232, hash 1B217D2E + sample 87: + time = 2020053 + flags = 1 + data = length 251, hash D1C14CEA + sample 88: + time = 2043272 + flags = 1 + data = length 256, hash 97C87F08 + sample 89: + time = 2066491 + flags = 1 + data = length 237, hash 6645DB3 + sample 90: + time = 2089710 + flags = 1 + data = length 235, hash 727A1C82 + sample 91: + time = 2112929 + flags = 1 + data = length 234, hash 5015F8B5 + sample 92: + time = 2136148 + flags = 1 + data = length 241, hash 9102144B + sample 93: + time = 2159367 + flags = 1 + data = length 224, hash 64E0D807 + sample 94: + time = 2182586 + flags = 1 + data = length 228, hash 1922B852 + sample 95: + time = 2205805 + flags = 1 + data = length 224, hash 953502D8 + sample 96: + time = 2229024 + flags = 1 + data = length 214, hash 92B87FE7 + sample 97: + time = 2252243 + flags = 1 + data = length 213, hash BB0C8D86 + sample 98: + time = 2275462 + flags = 1 + data = length 206, hash 9AD21017 + sample 99: + time = 2298681 + flags = 1 + data = length 209, hash C479FE94 + sample 100: + time = 2321900 + flags = 1 + data = length 220, hash 3033DCE1 + sample 101: + time = 2345119 + flags = 1 + data = length 217, hash 7D589C94 + sample 102: + time = 2368338 + flags = 1 + data = length 216, hash AAF6C183 + sample 103: + time = 2391557 + flags = 1 + data = length 206, hash 1EE1207F + sample 104: + time = 2414776 + flags = 1 + data = length 204, hash 4BEB1210 + sample 105: + time = 2437995 + flags = 1 + data = length 213, hash 21A841C9 + sample 106: + time = 2461214 + flags = 1 + data = length 207, hash B80B0424 + sample 107: + time = 2484433 + flags = 1 + data = length 212, hash 4785A1C3 + sample 108: + time = 2507652 + flags = 1 + data = length 205, hash 59BF7229 + sample 109: + time = 2530871 + flags = 1 + data = length 208, hash FA313DDE + sample 110: + time = 2554090 + flags = 1 + data = length 211, hash 190D85FD + sample 111: + time = 2577309 + flags = 1 + data = length 211, hash BA050052 + sample 112: + time = 2600528 + flags = 1 + data = length 211, hash F3080F10 + sample 113: + time = 2623747 + flags = 1 + data = length 210, hash F41B7BE7 + sample 114: + time = 2646966 + flags = 1 + data = length 207, hash 2176C97E + sample 115: + time = 2670185 + flags = 1 + data = length 220, hash 32087455 + sample 116: + time = 2693404 + flags = 1 + data = length 213, hash 4E5649A8 + sample 117: + time = 2716623 + flags = 1 + data = length 213, hash 5F12FDCF + sample 118: + time = 2739842 + flags = 1 + data = length 204, hash 1E895C2A + sample 119: + time = 2763061 + flags = 1 + data = length 219, hash 45382270 + sample 120: + time = 2786280 + flags = 1 + data = length 205, hash D66C6A1D + sample 121: + time = 2809499 + flags = 1 + data = length 204, hash 467AD01F + sample 122: + time = 2832718 + flags = 1 + data = length 211, hash F0435574 + sample 123: + time = 2855937 + flags = 1 + data = length 206, hash 8C96B75F + sample 124: + time = 2879156 + flags = 1 + data = length 200, hash 82553248 + sample 125: + time = 2902375 + flags = 1 + data = length 180, hash 1E51E6CE + sample 126: + time = 2925594 + flags = 1 + data = length 196, hash 33151DC4 + sample 127: + time = 2948813 + flags = 1 + data = length 197, hash 1E62A7D6 + sample 128: + time = 2972032 + flags = 1 + data = length 206, hash 6A6C4CC9 + sample 129: + time = 2995251 + flags = 1 + data = length 209, hash A72FABAA + sample 130: + time = 3018470 + flags = 1 + data = length 217, hash BA33B985 + sample 131: + time = 3041689 + flags = 1 + data = length 235, hash 9919CFD9 + sample 132: + time = 3064908 + flags = 1 + data = length 236, hash A22C7267 + sample 133: + time = 3088127 + flags = 1 + data = length 213, hash 3D57C901 + sample 134: + time = 3111346 + flags = 1 + data = length 205, hash 47F68FDE + sample 135: + time = 3134565 + flags = 1 + data = length 210, hash 9A756E9C + sample 136: + time = 3157784 + flags = 1 + data = length 210, hash BD45C31F + sample 137: + time = 3181003 + flags = 1 + data = length 207, hash 8774FF7B + sample 138: + time = 3204222 + flags = 1 + data = length 149, hash 4678C0E5 + sample 139: + time = 3227441 + flags = 1 + data = length 161, hash E991035D + sample 140: + time = 3250660 + flags = 1 + data = length 197, hash C3013689 + sample 141: + time = 3273879 + flags = 1 + data = length 208, hash E6C0237 + sample 142: + time = 3297098 + flags = 1 + data = length 232, hash A330F188 + sample 143: + time = 3320317 + flags = 1 + data = length 174, hash 2B69C34E +track 1: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = application/id3 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + metadata = null + initializationData: + total output bytes = 141 + sample count = 2 + sample 0: + time = 0 + flags = 1 + data = length 55, hash A7EB51A0 + sample 1: + time = 23219 + flags = 1 + data = length 86, hash 3FA72D40 +tracksEnded = true diff --git a/library/core/src/test/assets/ts/sample_with_id3.adts.unknown_length.dump b/library/core/src/test/assets/ts/sample_with_id3.adts.unknown_length.dump new file mode 100644 index 0000000000..b9cde05d69 --- /dev/null +++ b/library/core/src/test/assets/ts/sample_with_id3.adts.unknown_length.dump @@ -0,0 +1,641 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 2 +track 0: + format: + bitrate = -1 + id = 0 + containerMimeType = null + sampleMimeType = audio/mp4a-latm + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = 1 + sampleRate = 44100 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + metadata = null + initializationData: + data = length 2, hash 5F7 + total output bytes = 30797 + sample count = 144 + sample 0: + time = 0 + flags = 1 + data = length 23, hash 47DE9131 + sample 1: + time = 23219 + flags = 1 + data = length 6, hash 31CF3A46 + sample 2: + time = 46438 + flags = 1 + data = length 6, hash 31CF3A46 + sample 3: + time = 69657 + flags = 1 + data = length 6, hash 31CF3A46 + sample 4: + time = 92876 + flags = 1 + data = length 6, hash 31EC5206 + sample 5: + time = 116095 + flags = 1 + data = length 171, hash 4F6478F6 + sample 6: + time = 139314 + flags = 1 + data = length 202, hash AF4068A3 + sample 7: + time = 162533 + flags = 1 + data = length 210, hash E4C10618 + sample 8: + time = 185752 + flags = 1 + data = length 217, hash 9ECCD0D9 + sample 9: + time = 208971 + flags = 1 + data = length 212, hash 6BAC2CD9 + sample 10: + time = 232190 + flags = 1 + data = length 223, hash 188B6010 + sample 11: + time = 255409 + flags = 1 + data = length 222, hash C1A04D0C + sample 12: + time = 278628 + flags = 1 + data = length 220, hash D65F9768 + sample 13: + time = 301847 + flags = 1 + data = length 227, hash B96C9E14 + sample 14: + time = 325066 + flags = 1 + data = length 229, hash 9FB09972 + sample 15: + time = 348285 + flags = 1 + data = length 220, hash 2271F053 + sample 16: + time = 371504 + flags = 1 + data = length 226, hash 5EDD2F4F + sample 17: + time = 394723 + flags = 1 + data = length 239, hash 957510E0 + sample 18: + time = 417942 + flags = 1 + data = length 224, hash 718A8F47 + sample 19: + time = 441161 + flags = 1 + data = length 225, hash 5E11E293 + sample 20: + time = 464380 + flags = 1 + data = length 227, hash FCE50D27 + sample 21: + time = 487599 + flags = 1 + data = length 212, hash 77908C40 + sample 22: + time = 510818 + flags = 1 + data = length 227, hash 34C4EB32 + sample 23: + time = 534037 + flags = 1 + data = length 231, hash 95488307 + sample 24: + time = 557256 + flags = 1 + data = length 226, hash 97F12D6F + sample 25: + time = 580475 + flags = 1 + data = length 236, hash 91A9D9A2 + sample 26: + time = 603694 + flags = 1 + data = length 227, hash 27A608F9 + sample 27: + time = 626913 + flags = 1 + data = length 229, hash 57DAAE4 + sample 28: + time = 650132 + flags = 1 + data = length 235, hash ED30AC34 + sample 29: + time = 673351 + flags = 1 + data = length 227, hash BD3D6280 + sample 30: + time = 696570 + flags = 1 + data = length 233, hash 694B1087 + sample 31: + time = 719789 + flags = 1 + data = length 232, hash 1EDFE047 + sample 32: + time = 743008 + flags = 1 + data = length 228, hash E2A831F4 + sample 33: + time = 766227 + flags = 1 + data = length 231, hash 757E6012 + sample 34: + time = 789446 + flags = 1 + data = length 223, hash 4003D791 + sample 35: + time = 812665 + flags = 1 + data = length 232, hash 3CF9A07C + sample 36: + time = 835884 + flags = 1 + data = length 228, hash 25AC3FF7 + sample 37: + time = 859103 + flags = 1 + data = length 220, hash 2C1824CE + sample 38: + time = 882322 + flags = 1 + data = length 229, hash 46FDD8FB + sample 39: + time = 905541 + flags = 1 + data = length 237, hash F6988018 + sample 40: + time = 928760 + flags = 1 + data = length 242, hash 60436B6B + sample 41: + time = 951979 + flags = 1 + data = length 275, hash 90EDFA8E + sample 42: + time = 975198 + flags = 1 + data = length 242, hash 5C86EFCB + sample 43: + time = 998417 + flags = 1 + data = length 233, hash E0A51B82 + sample 44: + time = 1021636 + flags = 1 + data = length 235, hash 590DF14F + sample 45: + time = 1044855 + flags = 1 + data = length 238, hash 69AF4E6E + sample 46: + time = 1068074 + flags = 1 + data = length 235, hash E745AE8D + sample 47: + time = 1091293 + flags = 1 + data = length 223, hash 295F2A13 + sample 48: + time = 1114512 + flags = 1 + data = length 228, hash E2F47B21 + sample 49: + time = 1137731 + flags = 1 + data = length 229, hash 262C3CFE + sample 50: + time = 1160950 + flags = 1 + data = length 232, hash 4B5BF5E8 + sample 51: + time = 1184169 + flags = 1 + data = length 233, hash F3D80836 + sample 52: + time = 1207388 + flags = 1 + data = length 237, hash 32E0A11E + sample 53: + time = 1230607 + flags = 1 + data = length 228, hash E1B89F13 + sample 54: + time = 1253826 + flags = 1 + data = length 237, hash 8BDD9E38 + sample 55: + time = 1277045 + flags = 1 + data = length 235, hash 3C84161F + sample 56: + time = 1300264 + flags = 1 + data = length 227, hash A47E1789 + sample 57: + time = 1323483 + flags = 1 + data = length 228, hash 869FDFD3 + sample 58: + time = 1346702 + flags = 1 + data = length 233, hash 272ECE2 + sample 59: + time = 1369921 + flags = 1 + data = length 227, hash DB6B9618 + sample 60: + time = 1393140 + flags = 1 + data = length 212, hash 63214325 + sample 61: + time = 1416359 + flags = 1 + data = length 221, hash 9BA588A1 + sample 62: + time = 1439578 + flags = 1 + data = length 225, hash 21EFD50C + sample 63: + time = 1462797 + flags = 1 + data = length 231, hash F3AD0BF + sample 64: + time = 1486016 + flags = 1 + data = length 224, hash 822C9210 + sample 65: + time = 1509235 + flags = 1 + data = length 195, hash D4EF53EE + sample 66: + time = 1532454 + flags = 1 + data = length 195, hash A816647A + sample 67: + time = 1555673 + flags = 1 + data = length 184, hash 9A2B7E6 + sample 68: + time = 1578892 + flags = 1 + data = length 210, hash 956E3600 + sample 69: + time = 1602111 + flags = 1 + data = length 234, hash 35CFDA0A + sample 70: + time = 1625330 + flags = 1 + data = length 239, hash 9E15AC1E + sample 71: + time = 1648549 + flags = 1 + data = length 228, hash F3B70641 + sample 72: + time = 1671768 + flags = 1 + data = length 237, hash 124E3194 + sample 73: + time = 1694987 + flags = 1 + data = length 231, hash 950CD7C8 + sample 74: + time = 1718206 + flags = 1 + data = length 236, hash A12E49AF + sample 75: + time = 1741425 + flags = 1 + data = length 242, hash 43BC9C24 + sample 76: + time = 1764644 + flags = 1 + data = length 241, hash DCF0B17 + sample 77: + time = 1787863 + flags = 1 + data = length 251, hash C0B99968 + sample 78: + time = 1811082 + flags = 1 + data = length 245, hash 9B38ED1C + sample 79: + time = 1834301 + flags = 1 + data = length 238, hash 1BA69079 + sample 80: + time = 1857520 + flags = 1 + data = length 233, hash 44C8C6BF + sample 81: + time = 1880739 + flags = 1 + data = length 231, hash EABBEE02 + sample 82: + time = 1903958 + flags = 1 + data = length 226, hash D09C44FB + sample 83: + time = 1927177 + flags = 1 + data = length 235, hash BE6A6608 + sample 84: + time = 1950396 + flags = 1 + data = length 235, hash 2735F454 + sample 85: + time = 1973615 + flags = 1 + data = length 238, hash B160DFE7 + sample 86: + time = 1996834 + flags = 1 + data = length 232, hash 1B217D2E + sample 87: + time = 2020053 + flags = 1 + data = length 251, hash D1C14CEA + sample 88: + time = 2043272 + flags = 1 + data = length 256, hash 97C87F08 + sample 89: + time = 2066491 + flags = 1 + data = length 237, hash 6645DB3 + sample 90: + time = 2089710 + flags = 1 + data = length 235, hash 727A1C82 + sample 91: + time = 2112929 + flags = 1 + data = length 234, hash 5015F8B5 + sample 92: + time = 2136148 + flags = 1 + data = length 241, hash 9102144B + sample 93: + time = 2159367 + flags = 1 + data = length 224, hash 64E0D807 + sample 94: + time = 2182586 + flags = 1 + data = length 228, hash 1922B852 + sample 95: + time = 2205805 + flags = 1 + data = length 224, hash 953502D8 + sample 96: + time = 2229024 + flags = 1 + data = length 214, hash 92B87FE7 + sample 97: + time = 2252243 + flags = 1 + data = length 213, hash BB0C8D86 + sample 98: + time = 2275462 + flags = 1 + data = length 206, hash 9AD21017 + sample 99: + time = 2298681 + flags = 1 + data = length 209, hash C479FE94 + sample 100: + time = 2321900 + flags = 1 + data = length 220, hash 3033DCE1 + sample 101: + time = 2345119 + flags = 1 + data = length 217, hash 7D589C94 + sample 102: + time = 2368338 + flags = 1 + data = length 216, hash AAF6C183 + sample 103: + time = 2391557 + flags = 1 + data = length 206, hash 1EE1207F + sample 104: + time = 2414776 + flags = 1 + data = length 204, hash 4BEB1210 + sample 105: + time = 2437995 + flags = 1 + data = length 213, hash 21A841C9 + sample 106: + time = 2461214 + flags = 1 + data = length 207, hash B80B0424 + sample 107: + time = 2484433 + flags = 1 + data = length 212, hash 4785A1C3 + sample 108: + time = 2507652 + flags = 1 + data = length 205, hash 59BF7229 + sample 109: + time = 2530871 + flags = 1 + data = length 208, hash FA313DDE + sample 110: + time = 2554090 + flags = 1 + data = length 211, hash 190D85FD + sample 111: + time = 2577309 + flags = 1 + data = length 211, hash BA050052 + sample 112: + time = 2600528 + flags = 1 + data = length 211, hash F3080F10 + sample 113: + time = 2623747 + flags = 1 + data = length 210, hash F41B7BE7 + sample 114: + time = 2646966 + flags = 1 + data = length 207, hash 2176C97E + sample 115: + time = 2670185 + flags = 1 + data = length 220, hash 32087455 + sample 116: + time = 2693404 + flags = 1 + data = length 213, hash 4E5649A8 + sample 117: + time = 2716623 + flags = 1 + data = length 213, hash 5F12FDCF + sample 118: + time = 2739842 + flags = 1 + data = length 204, hash 1E895C2A + sample 119: + time = 2763061 + flags = 1 + data = length 219, hash 45382270 + sample 120: + time = 2786280 + flags = 1 + data = length 205, hash D66C6A1D + sample 121: + time = 2809499 + flags = 1 + data = length 204, hash 467AD01F + sample 122: + time = 2832718 + flags = 1 + data = length 211, hash F0435574 + sample 123: + time = 2855937 + flags = 1 + data = length 206, hash 8C96B75F + sample 124: + time = 2879156 + flags = 1 + data = length 200, hash 82553248 + sample 125: + time = 2902375 + flags = 1 + data = length 180, hash 1E51E6CE + sample 126: + time = 2925594 + flags = 1 + data = length 196, hash 33151DC4 + sample 127: + time = 2948813 + flags = 1 + data = length 197, hash 1E62A7D6 + sample 128: + time = 2972032 + flags = 1 + data = length 206, hash 6A6C4CC9 + sample 129: + time = 2995251 + flags = 1 + data = length 209, hash A72FABAA + sample 130: + time = 3018470 + flags = 1 + data = length 217, hash BA33B985 + sample 131: + time = 3041689 + flags = 1 + data = length 235, hash 9919CFD9 + sample 132: + time = 3064908 + flags = 1 + data = length 236, hash A22C7267 + sample 133: + time = 3088127 + flags = 1 + data = length 213, hash 3D57C901 + sample 134: + time = 3111346 + flags = 1 + data = length 205, hash 47F68FDE + sample 135: + time = 3134565 + flags = 1 + data = length 210, hash 9A756E9C + sample 136: + time = 3157784 + flags = 1 + data = length 210, hash BD45C31F + sample 137: + time = 3181003 + flags = 1 + data = length 207, hash 8774FF7B + sample 138: + time = 3204222 + flags = 1 + data = length 149, hash 4678C0E5 + sample 139: + time = 3227441 + flags = 1 + data = length 161, hash E991035D + sample 140: + time = 3250660 + flags = 1 + data = length 197, hash C3013689 + sample 141: + time = 3273879 + flags = 1 + data = length 208, hash E6C0237 + sample 142: + time = 3297098 + flags = 1 + data = length 232, hash A330F188 + sample 143: + time = 3320317 + flags = 1 + data = length 174, hash 2B69C34E +track 1: + format: + bitrate = -1 + id = 1 + containerMimeType = null + sampleMimeType = application/id3 + maxInputSize = -1 + width = -1 + height = -1 + frameRate = -1.0 + rotationDegrees = 0 + pixelWidthHeightRatio = 1.0 + channelCount = -1 + sampleRate = -1 + pcmEncoding = -1 + encoderDelay = 0 + encoderPadding = 0 + subsampleOffsetUs = 9223372036854775807 + selectionFlags = 0 + language = null + drmInitData = - + metadata = null + initializationData: + total output bytes = 141 + sample count = 2 + sample 0: + time = 0 + flags = 1 + data = length 55, hash A7EB51A0 + sample 1: + time = 23219 + flags = 1 + data = length 86, hash 3FA72D40 +tracksEnded = true diff --git a/library/core/src/test/assets/wav/sample_with_trailing_bytes.wav b/library/core/src/test/assets/wav/sample_with_trailing_bytes.wav new file mode 100644 index 0000000000..0b06efcf09 Binary files /dev/null and b/library/core/src/test/assets/wav/sample_with_trailing_bytes.wav differ diff --git a/library/core/src/test/java/com/google/android/exoplayer2/AudioFocusManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/AudioFocusManagerTest.java index 9a44d6def6..b0f258c0c2 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/AudioFocusManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/AudioFocusManagerTest.java @@ -65,13 +65,11 @@ public class AudioFocusManagerTest { @Test public void setAudioAttributes_withNullUsage_doesNotManageAudioFocus() { // Ensure that NULL audio attributes -> don't manage audio focus - assertThat( - audioFocusManager.setAudioAttributes( - /* audioAttributes= */ null, /* playWhenReady= */ false, Player.STATE_IDLE)) + audioFocusManager.setAudioAttributes(/* audioAttributes= */ null); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_IDLE)) .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); - assertThat( - audioFocusManager.setAudioAttributes( - /* audioAttributes= */ null, /* playWhenReady= */ true, Player.STATE_READY)) + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); ShadowAudioManager.AudioFocusRequest request = Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); @@ -85,18 +83,17 @@ public class AudioFocusManagerTest { AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_READY)) + audioFocusManager.setAudioAttributes(media); + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); ShadowAudioManager.AudioFocusRequest request = Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); assertThat(request.durationHint).isEqualTo(AudioManager.AUDIOFOCUS_GAIN); // Ensure that setting null audio attributes with audio focus releases audio focus. - assertThat( - audioFocusManager.setAudioAttributes( - /* audioAttributes= */ null, /* playWhenReady= */ true, Player.STATE_READY)) + audioFocusManager.setAudioAttributes(/* audioAttributes= */ null); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); AudioManager.OnAudioFocusChangeListener lastRequest = Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener(); @@ -110,18 +107,16 @@ public class AudioFocusManagerTest { AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_READY)) + audioFocusManager.setAudioAttributes(media); + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); ShadowAudioManager.AudioFocusRequest request = Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); assertThat(getAudioFocusGainFromRequest(request)).isEqualTo(AudioManager.AUDIOFOCUS_GAIN); // Ensure that setting null audio attributes with audio focus releases audio focus. - assertThat( - audioFocusManager.setAudioAttributes( - /* audioAttributes= */ null, /* playWhenReady= */ true, Player.STATE_READY)) + audioFocusManager.setAudioAttributes(/* audioAttributes= */ null); + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); AudioFocusRequest lastRequest = Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest(); @@ -130,10 +125,10 @@ public class AudioFocusManagerTest { @Test public void setAudioAttributes_withUsageAlarm_throwsIllegalArgumentException() { - // Ensure that audio attributes that map to AUDIOFOCUS_GAIN_TRANSIENT* throw + // Ensure that audio attributes that map to AUDIOFOCUS_GAIN_TRANSIENT* throw. AudioAttributes alarm = new AudioAttributes.Builder().setUsage(C.USAGE_ALARM).build(); try { - audioFocusManager.setAudioAttributes(alarm, /* playWhenReady= */ false, Player.STATE_IDLE); + audioFocusManager.setAudioAttributes(alarm); fail(); } catch (IllegalArgumentException e) { // Expected @@ -147,9 +142,9 @@ public class AudioFocusManagerTest { Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_READY)) + audioFocusManager.setAudioAttributes(media); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); ShadowAudioManager.AudioFocusRequest request = Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); @@ -163,9 +158,9 @@ public class AudioFocusManagerTest { Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_ENDED)) + audioFocusManager.setAudioAttributes(media); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_ENDED)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); ShadowAudioManager.AudioFocusRequest request = Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); @@ -173,42 +168,221 @@ public class AudioFocusManagerTest { } @Test - public void handlePrepare_afterSetAudioAttributes_setsPlayerCommandPlayWhenReady() { + public void updateAudioFocus_idleToBuffering_setsPlayerCommandPlayWhenReady() { // Ensure that when playWhenReady is true while the player is IDLE, audio focus is only - // requested after calling handlePrepare. + // requested after calling prepare (= changing the state to BUFFERING). AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); - Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(media); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_IDLE)) + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_IDLE)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); assertThat(Shadows.shadowOf(audioManager).getLastAudioFocusRequest()).isNull(); - assertThat(audioFocusManager.handlePrepare(/* playWhenReady= */ true)) + assertThat( + audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_BUFFERING)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + ShadowAudioManager.AudioFocusRequest request = + Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); + assertThat(getAudioFocusGainFromRequest(request)).isEqualTo(AudioManager.AUDIOFOCUS_GAIN); + } + + @Test + public void updateAudioFocus_pausedToPlaying_setsPlayerCommandPlayWhenReady() { + AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); + Shadows.shadowOf(audioManager) + .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(media); + + // Audio focus should not be requested yet, because playWhenReady=false. + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); + assertThat(Shadows.shadowOf(audioManager).getLastAudioFocusRequest()).isNull(); + + // Audio focus should be requested now that playWhenReady=true. + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + ShadowAudioManager.AudioFocusRequest request = + Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); + assertThat(getAudioFocusGainFromRequest(request)).isEqualTo(AudioManager.AUDIOFOCUS_GAIN); + } + + // See https://github.com/google/ExoPlayer/issues/7182 for context. + @Test + public void updateAudioFocus_pausedToPlaying_withTransientLoss_setsPlayerCommandPlayWhenReady() { + AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); + Shadows.shadowOf(audioManager) + .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(media); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + + // Simulate transient focus loss. + audioFocusManager.getFocusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT); + + // Focus should be re-requested, rather than staying in a state of transient focus loss. + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); } @Test - public void handleSetPlayWhenReady_afterSetAudioAttributes_setsPlayerCommandPlayWhenReady() { - // Ensure that audio focus is not requested until playWhenReady is true. + public void updateAudioFocus_pausedToPlaying_withTransientDuck_setsPlayerCommandPlayWhenReady() { AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); - Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(media); - assertThat(audioFocusManager.handlePrepare(/* playWhenReady= */ false)) - .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); - assertThat(Shadows.shadowOf(audioManager).getLastAudioFocusRequest()).isNull(); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ false, Player.STATE_READY)) - .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); - assertThat(Shadows.shadowOf(audioManager).getLastAudioFocusRequest()).isNull(); - assertThat( - audioFocusManager.handleSetPlayWhenReady(/* playWhenReady= */ true, Player.STATE_READY)) + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + + // Simulate transient ducking. + audioFocusManager + .getFocusListener() + .onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK); + assertThat(testPlayerControl.lastVolumeMultiplier).isLessThan(1.0f); + + // Focus should be re-requested, rather than staying in a state of transient ducking. This + // should restore the volume to 1.0. + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + assertThat(testPlayerControl.lastVolumeMultiplier).isEqualTo(1.0f); + } + + @Test + public void updateAudioFocus_abandonFocusWhenDucked_restoresFullVolume() { + AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); + Shadows.shadowOf(audioManager) + .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(media); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + + // Simulate transient ducking. + audioFocusManager + .getFocusListener() + .onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK); + assertThat(testPlayerControl.lastVolumeMultiplier).isLessThan(1.0f); + + // Configure the manager to no longer handle audio focus. + audioFocusManager.setAudioAttributes(null); + + // Focus should be abandoned, which should restore the volume to 1.0. + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + assertThat(testPlayerControl.lastVolumeMultiplier).isEqualTo(1.0f); + } + + @Test + @Config(maxSdk = 25) + public void updateAudioFocus_readyToIdle_abandonsAudioFocus() { + // Ensure that stopping the player (=changing state to idle) abandons audio focus. + AudioAttributes media = + new AudioAttributes.Builder() + .setUsage(C.USAGE_MEDIA) + .setContentType(C.CONTENT_TYPE_SPEECH) + .build(); + Shadows.shadowOf(audioManager) + .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(media); + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull(); + + ShadowAudioManager.AudioFocusRequest request = + Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_IDLE)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()) + .isEqualTo(request.listener); + } + + @Test + @Config(minSdk = 26, maxSdk = TARGET_SDK) + public void updateAudioFocus_readyToIdle_abandonsAudioFocus_v26() { + // Ensure that stopping the player (=changing state to idle) abandons audio focus. + AudioAttributes media = + new AudioAttributes.Builder() + .setUsage(C.USAGE_MEDIA) + .setContentType(C.CONTENT_TYPE_SPEECH) + .build(); + Shadows.shadowOf(audioManager) + .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(media); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull(); + + ShadowAudioManager.AudioFocusRequest request = + Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_IDLE)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()) + .isEqualTo(request.audioFocusRequest); + } + + @Test + @Config(maxSdk = 25) + public void updateAudioFocus_readyToIdle_withoutHandlingAudioFocus_isNoOp() { + // Ensure that changing state to idle is a no-op if audio focus isn't handled. + Shadows.shadowOf(audioManager) + .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(null); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); + assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull(); + ShadowAudioManager.AudioFocusRequest request = + Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); + assertThat(request).isNull(); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_IDLE)) + .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); + assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull(); + } + + @Test + @Config(minSdk = 26, maxSdk = TARGET_SDK) + public void updateAudioFocus_readyToIdle_withoutHandlingAudioFocus_isNoOp_v26() { + // Ensure that changing state to idle is a no-op if audio focus isn't handled. + Shadows.shadowOf(audioManager) + .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(null); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); + assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull(); + ShadowAudioManager.AudioFocusRequest request = + Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); + assertThat(request).isNull(); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ false, Player.STATE_IDLE)) + .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); + assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull(); + } + + @Test + public void release_doesNotCallPlayerControlToRestoreVolume() { + AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); + Shadows.shadowOf(audioManager) + .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + audioFocusManager.setAudioAttributes(media); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) + .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); + + // Simulate transient ducking. + audioFocusManager + .getFocusListener() + .onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK); + assertThat(testPlayerControl.lastVolumeMultiplier).isLessThan(1.0f); + + audioFocusManager.release(); + + // PlaybackController.setVolumeMultiplier should not have been called to restore the volume. + assertThat(testPlayerControl.lastVolumeMultiplier).isLessThan(1.0f); } @Test @@ -217,17 +391,17 @@ public class AudioFocusManagerTest { // AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK, and returns to the default value after focus is // regained. AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); - Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_READY)) + audioFocusManager.setAudioAttributes(media); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); audioFocusManager .getFocusListener() .onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK); + assertThat(testPlayerControl.lastVolumeMultiplier).isLessThan(1.0f); assertThat(testPlayerControl.lastPlayerCommand).isEqualTo(NO_COMMAND_RECEIVED); audioFocusManager.getFocusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_GAIN); @@ -243,12 +417,11 @@ public class AudioFocusManagerTest { .setUsage(C.USAGE_MEDIA) .setContentType(C.CONTENT_TYPE_SPEECH) .build(); - Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_READY)) + audioFocusManager.setAudioAttributes(media); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); audioFocusManager @@ -261,16 +434,15 @@ public class AudioFocusManagerTest { } @Test - public void onAudioFocusChange_withTransientLost_sendsCommandWaitForCallback() { + public void onAudioFocusChange_withTransientLoss_sendsCommandWaitForCallback() { // Ensure that the player is commanded to pause when audio focus is lost with // AUDIOFOCUS_LOSS_TRANSIENT. AudioAttributes media = new AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).build(); - Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_READY)) + audioFocusManager.setAudioAttributes(media); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); audioFocusManager.getFocusListener().onAudioFocusChange(AudioManager.AUDIOFOCUS_LOSS_TRANSIENT); @@ -280,7 +452,7 @@ public class AudioFocusManagerTest { @Test @Config(maxSdk = 25) - public void onAudioFocusChange_withAudioFocusLost_sendsDoNotPlayAndAbandondsFocus() { + public void onAudioFocusChange_withFocusLoss_sendsDoNotPlayAndAbandonsFocus() { // Ensure that AUDIOFOCUS_LOSS causes AudioFocusManager to pause playback and abandon audio // focus. AudioAttributes media = @@ -288,12 +460,11 @@ public class AudioFocusManagerTest { .setUsage(C.USAGE_MEDIA) .setContentType(C.CONTENT_TYPE_SPEECH) .build(); - Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_READY)) + audioFocusManager.setAudioAttributes(media); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull(); @@ -307,7 +478,7 @@ public class AudioFocusManagerTest { @Test @Config(minSdk = 26, maxSdk = TARGET_SDK) - public void onAudioFocusChange_withAudioFocusLost_sendsDoNotPlayAndAbandondsFocus_v26() { + public void onAudioFocusChange_withFocusLoss_sendsDoNotPlayAndAbandonsFocus_v26() { // Ensure that AUDIOFOCUS_LOSS causes AudioFocusManager to pause playback and abandon audio // focus. AudioAttributes media = @@ -315,12 +486,11 @@ public class AudioFocusManagerTest { .setUsage(C.USAGE_MEDIA) .setContentType(C.CONTENT_TYPE_SPEECH) .build(); - Shadows.shadowOf(audioManager) .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_READY)) + audioFocusManager.setAudioAttributes(media); + + assertThat(audioFocusManager.updateAudioFocus(/* playWhenReady= */ true, Player.STATE_READY)) .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull(); @@ -330,120 +500,6 @@ public class AudioFocusManagerTest { .isEqualTo(Shadows.shadowOf(audioManager).getLastAudioFocusRequest().audioFocusRequest); } - @Test - @Config(maxSdk = 25) - public void handleStop_withAudioFocus_abandonsAudioFocus() { - // Ensure that handleStop causes AudioFocusManager to abandon audio focus. - AudioAttributes media = - new AudioAttributes.Builder() - .setUsage(C.USAGE_MEDIA) - .setContentType(C.CONTENT_TYPE_SPEECH) - .build(); - - Shadows.shadowOf(audioManager) - .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_READY)) - .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); - assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull(); - - ShadowAudioManager.AudioFocusRequest request = - Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); - audioFocusManager.handleStop(); - assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()) - .isEqualTo(request.listener); - } - - @Test - @Config(minSdk = 26, maxSdk = TARGET_SDK) - public void handleStop_withAudioFocus_abandonsAudioFocus_v26() { - // Ensure that handleStop causes AudioFocusManager to abandon audio focus. - AudioAttributes media = - new AudioAttributes.Builder() - .setUsage(C.USAGE_MEDIA) - .setContentType(C.CONTENT_TYPE_SPEECH) - .build(); - - Shadows.shadowOf(audioManager) - .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ true, Player.STATE_READY)) - .isEqualTo(PLAYER_COMMAND_PLAY_WHEN_READY); - assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull(); - - ShadowAudioManager.AudioFocusRequest request = - Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); - audioFocusManager.handleStop(); - assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()) - .isEqualTo(request.audioFocusRequest); - } - - @Test - @Config(maxSdk = 25) - public void handleStop_withoutAudioFocus_stillAbandonsFocus() { - // Ensure that handleStop causes AudioFocusManager to call through to abandon audio focus - // even if focus wasn't requested. - AudioAttributes media = - new AudioAttributes.Builder() - .setUsage(C.USAGE_MEDIA) - .setContentType(C.CONTENT_TYPE_SPEECH) - .build(); - - Shadows.shadowOf(audioManager) - .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - media, /* playWhenReady= */ false, Player.STATE_READY)) - .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); - assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull(); - ShadowAudioManager.AudioFocusRequest request = - Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); - assertThat(request).isNull(); - - audioFocusManager.handleStop(); - assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNotNull(); - } - - @Test - @Config(maxSdk = 25) - public void handleStop_withoutHandlingAudioFocus_isNoOp() { - // Ensure that handleStop is a no-op if audio focus isn't handled. - Shadows.shadowOf(audioManager) - .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - /* audioAttributes= */ null, /* playWhenReady= */ false, Player.STATE_READY)) - .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); - assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull(); - ShadowAudioManager.AudioFocusRequest request = - Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); - assertThat(request).isNull(); - - audioFocusManager.handleStop(); - assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusListener()).isNull(); - } - - @Test - @Config(minSdk = 26, maxSdk = TARGET_SDK) - public void handleStop_withoutHandlingAudioFocus_isNoOp_v26() { - // Ensure that handleStop is a no-op if audio focus isn't handled. - Shadows.shadowOf(audioManager) - .setNextFocusRequestResponse(AudioManager.AUDIOFOCUS_REQUEST_GRANTED); - assertThat( - audioFocusManager.setAudioAttributes( - /* audioAttributes= */ null, /* playWhenReady= */ false, Player.STATE_READY)) - .isEqualTo(PLAYER_COMMAND_DO_NOT_PLAY); - assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull(); - ShadowAudioManager.AudioFocusRequest request = - Shadows.shadowOf(audioManager).getLastAudioFocusRequest(); - assertThat(request).isNull(); - - audioFocusManager.handleStop(); - assertThat(Shadows.shadowOf(audioManager).getLastAbandonedAudioFocusRequest()).isNull(); - } - private int getAudioFocusGainFromRequest(ShadowAudioManager.AudioFocusRequest audioFocusRequest) { return Util.SDK_INT >= 26 ? audioFocusRequest.audioFocusRequest.getFocusGain() diff --git a/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java new file mode 100644 index 0000000000..10122d36ec --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/analytics/PlaybackStatsListenerTest.java @@ -0,0 +1,74 @@ +/* + * 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); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java index 56776aea09..3ef454d716 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java @@ -29,6 +29,11 @@ public final class AdtsExtractorTest { ExtractorAsserts.assertBehavior(AdtsExtractor::new, "ts/sample.adts"); } + @Test + public void testSample_with_id3() throws Exception { + ExtractorAsserts.assertBehavior(AdtsExtractor::new, "ts/sample_with_id3.adts"); + } + @Test public void testSample_withSeeking() throws Exception { ExtractorAsserts.assertBehavior( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java index 7f9549ea75..0f4220c7f3 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/wav/WavExtractorTest.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.extractor.wav; +import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import org.junit.Test; @@ -30,7 +31,7 @@ public final class WavExtractorTest { } @Test - public void testSampleImaAdpcm() throws Exception { + public void sample_imaAdpcm() throws Exception { ExtractorAsserts.assertBehavior(WavExtractor::new, "wav/sample_ima_adpcm.wav"); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java index 427e81d29f..a34488d2e7 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/SampleQueueTest.java @@ -372,8 +372,10 @@ public final class SampleQueueTest { assertReadFormat(/* formatRequired= */ false, FORMAT_ENCRYPTED); assertReadNothing(/* formatRequired= */ false); + assertThat(inputBuffer.waitingForKeys).isTrue(); when(mockDrmSession.getState()).thenReturn(DrmSession.STATE_OPENED_WITH_KEYS); assertReadEncryptedSample(/* sampleIndex= */ 0); + assertThat(inputBuffer.waitingForKeys).isFalse(); } @Test diff --git a/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java b/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java index 9f66f65a56..dec7a25523 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/text/subrip/SubripDecoderTest.java @@ -39,6 +39,7 @@ public final class SubripDecoderTest { 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_WITH_TAGS = "subrip/typical_with_tags"; + private static final String TYPICAL_NO_HOURS_AND_MILLIS = "subrip/typical_no_hours_and_millis"; @Test public void testDecodeEmpty() throws IOException { @@ -151,9 +152,14 @@ public final class SubripDecoderTest { TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_WITH_TAGS); Subtitle subtitle = decoder.decode(bytes, bytes.length, false); - assertTypicalCue1(subtitle, 0); - assertTypicalCue2(subtitle, 2); - assertTypicalCue3(subtitle, 4); + assertThat(subtitle.getCues(subtitle.getEventTime(0)).get(0).text.toString()) + .isEqualTo("This is the first subtitle."); + + 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()) .isEqualTo("This { \\an2} is not a valid tag due to the space after the opening bracket."); @@ -172,6 +178,21 @@ public final class SubripDecoderTest { 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) { assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0); assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) @@ -187,10 +208,12 @@ public final class SubripDecoderTest { } 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()) .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( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java b/library/core/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java index 288ad918b2..e397383490 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/util/MimeTypesTest.java @@ -73,6 +73,10 @@ public final class MimeTypesTest { assertThat(MimeTypes.getMediaMimeType("mp4a.AA")).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("wvtt")).isEqualTo(MimeTypes.TEXT_VTT); + assertThat(MimeTypes.getMediaMimeType("stpp.")).isEqualTo(MimeTypes.APPLICATION_TTML); + assertThat(MimeTypes.getMediaMimeType("stpp.ttml.im1t")).isEqualTo(MimeTypes.APPLICATION_TTML); } @Test diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index dfcd62b8b1..dcd4b15cae 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -807,15 +807,18 @@ public final class DashMediaSource extends BaseMediaSource { manifestLoadPending &= manifest.dynamic; manifestLoadStartTimestampMs = elapsedRealtimeMs - loadDurationMs; manifestLoadEndTimestampMs = elapsedRealtimeMs; - if (manifest.location != null) { - synchronized (manifestUriLock) { - // This condition checks that replaceManifestUri wasn't called between the start and end of - // this load. If it was, we ignore the manifest location and prefer the manual replacement. - @SuppressWarnings("ReferenceEquality") - boolean isSameUriInstance = loadable.dataSpec.uri == manifestUri; - if (isSameUriInstance) { - manifestUri = manifest.location; - } + + synchronized (manifestUriLock) { + // Checks whether replaceManifestUri(Uri) was called to manually replace the URI between the + // start and end of this load. If it was then isSameUriInstance evaluates to false, and we + // prefer the manual replacement to one derived from the previous request. + @SuppressWarnings("ReferenceEquality") + boolean isSameUriInstance = loadable.dataSpec.uri == manifestUri; + 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(); } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index b107be4794..95129d68c4 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -222,10 +222,11 @@ public class DashManifestParser extends DefaultHandler protected Pair parsePeriod(XmlPullParser xpp, String baseUrl, long defaultStartMs) throws XmlPullParserException, IOException { - String id = xpp.getAttributeValue(null, "id"); + @Nullable String id = xpp.getAttributeValue(null, "id"); long startMs = parseDuration(xpp, "start", defaultStartMs); long durationMs = parseDuration(xpp, "duration", C.TIME_UNSET); - SegmentBase segmentBase = null; + @Nullable SegmentBase segmentBase = null; + @Nullable Descriptor assetIdentifier = null; List adaptationSets = new ArrayList<>(); List eventStreams = new ArrayList<>(); boolean seenFirstBaseUrl = false; @@ -246,17 +247,24 @@ public class DashManifestParser extends DefaultHandler segmentBase = parseSegmentList(xpp, null, durationMs); } else if (XmlPullParserUtil.isStartTag(xpp, "SegmentTemplate")) { segmentBase = parseSegmentTemplate(xpp, null, Collections.emptyList(), durationMs); + } else if (XmlPullParserUtil.isStartTag(xpp, "AssetIdentifier")) { + assetIdentifier = parseDescriptor(xpp, "AssetIdentifier"); } else { maybeSkipTag(xpp); } } 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 adaptationSets, - List eventStreams) { - return new Period(id, startMs, adaptationSets, eventStreams); + protected Period buildPeriod( + @Nullable String id, + long startMs, + List adaptationSets, + List eventStreams, + @Nullable Descriptor assetIdentifier) { + return new Period(id, startMs, adaptationSets, eventStreams, assetIdentifier); } // AdaptationSet parsing. diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java index 18614ca4b0..b472aed50c 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/Period.java @@ -45,13 +45,16 @@ public class Period { */ public final List eventStreams; + /** The asset identifier for this period, if one exists */ + @Nullable public final Descriptor assetIdentifier; + /** * @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. */ public Period(@Nullable String id, long startMs, List adaptationSets) { - this(id, startMs, adaptationSets, Collections.emptyList()); + this(id, startMs, adaptationSets, Collections.emptyList(), /* assetIdentifier= */ null); } /** @@ -62,10 +65,27 @@ public class Period { */ public Period(@Nullable String id, long startMs, List adaptationSets, List 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 adaptationSets, + List eventStreams, + @Nullable Descriptor assetIdentifier) { this.id = id; this.startMs = startMs; this.adaptationSets = Collections.unmodifiableList(adaptationSets); this.eventStreams = Collections.unmodifiableList(eventStreams); + this.assetIdentifier = assetIdentifier; } /** diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java index a62e135b77..285ec1b6a1 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/WebvttExtractor.java @@ -49,7 +49,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; public final class WebvttExtractor implements Extractor { 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_MAX_LENGTH = 3 /* optional Byte Order Mark */ + HEADER_MIN_LENGTH; diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 993ce8e5c1..6bd447f746 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -480,17 +480,27 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser 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( HlsMasterPlaylist masterPlaylist, LineIterator iterator, String baseUri) throws IOException { @HlsMediaPlaylist.PlaylistType int playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN; diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index 0aa78d9f02..0d0ef83313 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -194,6 +194,19 @@ public class HlsMasterPlaylistParserTest { + "#EXT-X-MEDIA:TYPE=SUBTITLES," + "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 public void testParseMasterPlaylist() throws IOException { HlsMasterPlaylist masterPlaylist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_SIMPLE); @@ -380,6 +393,18 @@ public class HlsMasterPlaylistParserTest { .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) { return new Metadata( new HlsTrackMetadataEntry(/* groupId= */ null, /* name= */ null, Arrays.asList(infos))); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index 047041dd60..cf8fd2e3ee 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -140,7 +140,7 @@ public class PlayerNotificationManager { * * @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. @@ -160,7 +160,7 @@ public class PlayerNotificationManager { * @param player The {@link Player} for which a notification is being built. */ @Nullable - String getCurrentContentText(Player player); + CharSequence getCurrentContentText(Player player); /** * Gets the content sub text for the current media item. @@ -170,7 +170,7 @@ public class PlayerNotificationManager { * @param player The {@link Player} for which a notification is being built. */ @Nullable - default String getCurrentSubText(Player player) { + default CharSequence getCurrentSubText(Player player) { return null; } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index 03168643cf..2eae9c1dde 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -143,6 +143,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; *

  • Corresponding method: None *
  • Default: {@code surface_view} * + *
  • {@code use_sensor_rotation} - Whether to use the orientation sensor for rotation + * during spherical playbacks (if available). + *
      + *
    • Corresponding method: {@link #setUseSensorRotation(boolean)} + *
    • Default: {@code true} + *
    *
  • {@code shutter_background_color} - The background color of the {@code exo_shutter} * view. *
      @@ -308,6 +314,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider @Nullable private Drawable defaultArtwork; private @ShowBuffering int showBuffering; private boolean keepContentOnPlayerReset; + private boolean useSensorRotation; @Nullable private ErrorMessageProvider errorMessageProvider; @Nullable private CharSequence customErrorMessage; private int controllerShowTimeoutMs; @@ -367,6 +374,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider boolean controllerAutoShow = true; boolean controllerHideDuringAds = true; int showBuffering = SHOW_BUFFERING_NEVER; + useSensorRotation = true; if (attrs != null) { TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.PlayerView, 0, 0); try { @@ -390,6 +398,8 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider R.styleable.PlayerView_keep_content_on_player_reset, keepContentOnPlayerReset); controllerHideDuringAds = a.getBoolean(R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds); + useSensorRotation = + a.getBoolean(R.styleable.PlayerView_use_sensor_rotation, useSensorRotation); } finally { a.recycle(); } @@ -422,6 +432,7 @@ public class PlayerView extends FrameLayout implements AdsLoader.AdViewProvider case SURFACE_TYPE_SPHERICAL_GL_SURFACE_VIEW: SphericalGLSurfaceView sphericalGLSurfaceView = new SphericalGLSurfaceView(context); sphericalGLSurfaceView.setSingleTapListener(componentListener); + sphericalGLSurfaceView.setUseSensorRotation(useSensorRotation); surfaceView = sphericalGLSurfaceView; break; case SURFACE_TYPE_VIDEO_DECODER_GL_SURFACE_VIEW: @@ -746,6 +757,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 * buffering spinner is not displayed by default. diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalGLSurfaceView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalGLSurfaceView.java index c01fccf54b..1c96f41df5 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalGLSurfaceView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalGLSurfaceView.java @@ -72,6 +72,9 @@ public final class SphericalGLSurfaceView extends GLSurfaceView { @Nullable private SurfaceTexture surfaceTexture; @Nullable private Surface surface; @Nullable private Player.VideoComponent videoComponent; + private boolean useSensorRotation; + private boolean isStarted; + private boolean isOrientationListenerRegistered; public SphericalGLSurfaceView(Context context) { this(context, null); @@ -104,6 +107,7 @@ public final class SphericalGLSurfaceView extends GLSurfaceView { WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Display display = Assertions.checkNotNull(windowManager).getDefaultDisplay(); orientationListener = new OrientationListener(display, touchTracker, renderer); + useSensorRotation = true; setEGLContextClientVersion(2); setRenderer(renderer); @@ -145,20 +149,23 @@ public final class SphericalGLSurfaceView extends GLSurfaceView { touchTracker.setSingleTapListener(listener); } + /** Sets whether to use the orientation sensor for rotation (if available). */ + public void setUseSensorRotation(boolean useSensorRotation) { + this.useSensorRotation = useSensorRotation; + updateOrientationListenerRegistration(); + } + @Override public void onResume() { super.onResume(); - if (orientationSensor != null) { - sensorManager.registerListener( - orientationListener, orientationSensor, SensorManager.SENSOR_DELAY_FASTEST); - } + isStarted = true; + updateOrientationListenerRegistration(); } @Override public void onPause() { - if (orientationSensor != null) { - sensorManager.unregisterListener(orientationListener); - } + isStarted = false; + updateOrientationListenerRegistration(); super.onPause(); } @@ -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. private void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture) { mainHandler.post( diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index 535bf320fb..e0a6b7faf4 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -77,8 +77,8 @@ + -