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