From 8792c20be53f2d69024104eca824f5ad1e6af486 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 9 Jan 2019 10:24:32 +0000 Subject: [PATCH 001/110] Update README and dev guide with Java 8 config for Kotlin. Setting the target conpatibility only seems to work for Java. Added the equivalent Kotlin config options to the docs. Issue:#5276 PiperOrigin-RevId: 228482496 --- README.md | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 37967dd527..03f16bd655 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ repository and depend on the modules locally. ### From JCenter ### +#### 1. Add repositories #### + The easiest way to get started using ExoPlayer is to add it as a gradle dependency. You need to make sure you have the Google and JCenter repositories included in the `build.gradle` file in the root of your project: @@ -38,6 +40,8 @@ repositories { } ``` +#### 2. Add ExoPlayer module dependencies #### + Next add a dependency in the `build.gradle` file of your app module. The following will add a dependency to the full library: @@ -45,15 +49,7 @@ following will add a dependency to the full library: implementation 'com.google.android.exoplayer:exoplayer:2.X.X' ``` -where `2.X.X` is your preferred version. If not enabled already, you also need -to turn on Java 8 support in all `build.gradle` files depending on ExoPlayer, by -adding the following to the `android` section: - -```gradle -compileOptions { - targetCompatibility JavaVersion.VERSION_1_8 -} -``` +where `2.X.X` is your preferred version. As an alternative to the full library, you can depend on only the library modules that you actually need. For example the following will add dependencies @@ -87,6 +83,32 @@ JCenter can be found on [Bintray][]. [extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/ [Bintray]: https://bintray.com/google/exoplayer +#### 3. Turn on Java 8 support #### + +If not enabled already, you also need to turn on Java 8 support in all +`build.gradle` files depending on ExoPlayer, by adding the following to the +`android` section: + +```gradle +compileOptions { + targetCompatibility JavaVersion.VERSION_1_8 +} +``` + +Note that if you want to use Java 8 features in your own code, the following +additional options need to be set: + +```gradle +// For Java compilers: +compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 +} +// For Kotlin compilers: +kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 +} +``` + ### Locally ### Cloning the repository and depending on the modules locally is required when From 637b52ae0e7d5515f0e581da73df600a9cb56e51 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 9 Jan 2019 17:23:30 +0000 Subject: [PATCH 002/110] Add missing call to timeline.getWindow. The window object is used without being filled with data. This used to work well for most cases as the same live stream is sending regular updates and the first update is almost never used if it's not the first item in a playlist. It causes problems when the first timeline update of a live stream is actually used for playback (e.g. when the live stream is lazily prepared in a playlist and played first). PiperOrigin-RevId: 228530232 --- .../android/exoplayer2/source/ConcatenatingMediaSource.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index db52a87ef8..7845694c45 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -761,6 +761,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource Date: Thu, 10 Jan 2019 10:33:11 +0000 Subject: [PATCH 003/110] Add DownloadIndex and DefaultDownloadIndex DownloadIndex will be used to store and query DownloadStates. PiperOrigin-RevId: 228673766 --- .../offline/DefaultDownloadIndex.java | 528 ++++++++++++++++++ .../exoplayer2/offline/DownloadAction.java | 2 +- .../exoplayer2/offline/DownloadIndex.java | 52 ++ .../exoplayer2/offline/DownloadState.java | 1 - .../offline/DownloadStateCursor.java | 127 +++++ .../offline/DefaultDownloadIndexTest.java | 491 ++++++++++++++++ 6 files changed, 1199 insertions(+), 2 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadStateCursor.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java new file mode 100644 index 0000000000..8881a038f9 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java @@ -0,0 +1,528 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.offline; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.database.sqlite.SQLiteOpenHelper; +import android.net.Uri; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A {@link DownloadIndex} which uses SQLite to persist {@link DownloadState}s. + * + *

Database access may take a long time, do not call methods of this class from + * the application main thread. + */ +public final class DefaultDownloadIndex implements DownloadIndex { + + /** Provides {@link SQLiteDatabase} instances. */ + public interface DatabaseProvider { + /** Closes any open database object. */ + void close(); + + /** + * Creates and/or opens a database that will be used for reading and writing. + * + *

Once opened successfully, the database is cached, so you can call this method every time + * you need to write to the database. (Make sure to call {@link #close} when you no longer need + * the database.) Errors such as bad permissions or a full disk may cause this method to fail, + * but future attempts may succeed if the problem is fixed. + * + * @throws SQLiteException If the database cannot be opened for writing. + * @return A read/write database object valid until {@link #close} is called. + */ + SQLiteDatabase getWritableDatabase(); + + /** + * Creates and/or opens a database. This will be the same object returned by {@link + * #getWritableDatabase} unless some problem, such as a full disk, requires the database to be + * opened read-only. In that case, a read-only database object will be returned. If the problem + * is fixed, a future call to {@link #getWritableDatabase} may succeed, in which case the + * read-only database object will be closed and the read/write object will be returned in the + * future. + * + *

Once opened successfully, the database should be cached. When the database is no longer + * needed, {@link #close} will be called. + * + * @throws SQLiteException If the database cannot be opened. + * @return A database object valid until {@link #getWritableDatabase} or {@link #close} is + * called. + */ + SQLiteDatabase getReadableDatabase(); + } + + private static final String DATABASE_NAME = "exoplayer_internal.db"; + + private final DatabaseProvider databaseProvider; + @Nullable private DownloadStateTable downloadStateTable; + + /** + * Creates a DefaultDownloadIndex which stores the {@link DownloadState}s on a SQLite database. + * + * @param context A Context. + */ + public DefaultDownloadIndex(Context context) { + this(new DefaultDatabaseProvider(context)); + } + + /** + * Creates a DefaultDownloadIndex which stores the {@link DownloadState}s on a SQLite database + * provided by {@code databaseProvider}. + * + * @param databaseProvider A DatabaseProvider which provides the database which will be used to + * store DownloadStatus table. + */ + public DefaultDownloadIndex(DatabaseProvider databaseProvider) { + this.databaseProvider = databaseProvider; + } + + @Override + public void release() { + databaseProvider.close(); + } + + @Override + @Nullable + public DownloadState getDownloadState(String id) { + return getDownloadStateTable().get(id); + } + + @Override + public DownloadStateCursor getDownloadStates(@DownloadState.State int... states) { + return getDownloadStateTable().get(states); + } + + @Override + public void putDownloadState(DownloadState downloadState) { + getDownloadStateTable().replace(downloadState); + } + + @Override + public void removeDownloadState(String id) { + getDownloadStateTable().delete(id); + } + + private DownloadStateTable getDownloadStateTable() { + if (downloadStateTable == null) { + downloadStateTable = new DownloadStateTable(databaseProvider); + } + return downloadStateTable; + } + + @VisibleForTesting + /* package */ static boolean doesTableExist(DatabaseProvider databaseProvider, String tableName) { + SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase(); + long count = + DatabaseUtils.queryNumEntries( + readableDatabase, "sqlite_master", "tbl_name = ?", new String[] {tableName}); + return count > 0; + } + + private static final class DownloadStateCursorImpl implements DownloadStateCursor { + + private final Cursor cursor; + + private DownloadStateCursorImpl(Cursor cursor) { + this.cursor = cursor; + } + + @Override + public DownloadState getDownloadState() { + return DownloadStateTable.getDownloadState(cursor); + } + + @Override + public int getCount() { + return cursor.getCount(); + } + + @Override + public int getPosition() { + return cursor.getPosition(); + } + + @Override + public boolean moveToPosition(int position) { + return cursor.moveToPosition(position); + } + + @Override + public void close() { + cursor.close(); + } + + @Override + public boolean isClosed() { + return cursor.isClosed(); + } + } + + @VisibleForTesting + /* package */ static final class DownloadStateTable { + @VisibleForTesting /* package */ static final String TABLE_NAME = "ExoPlayerDownloadStates"; + @VisibleForTesting /* package */ static final int TABLE_VERSION = 1; + + private static final String COLUMN_ID = "id"; + private static final String COLUMN_TYPE = "title"; + private static final String COLUMN_URI = "subtitle"; + private static final String COLUMN_CACHE_KEY = "cache_key"; + private static final String COLUMN_STATE = "state"; + private static final String COLUMN_DOWNLOAD_PERCENTAGE = "download_percentage"; + private static final String COLUMN_DOWNLOADED_BYTES = "downloaded_bytes"; + private static final String COLUMN_TOTAL_BYTES = "total_bytes"; + private static final String COLUMN_FAILURE_REASON = "failure_reason"; + private static final String COLUMN_STOP_FLAGS = "stop_flags"; + private static final String COLUMN_START_TIME_MS = "start_time_ms"; + private static final String COLUMN_UPDATE_TIME_MS = "update_time_ms"; + private static final String COLUMN_STREAM_KEYS = "stream_keys"; + private static final String COLUMN_CUSTOM_METADATA = "custom_metadata"; + + private static final int COLUMN_INDEX_ID = 0; + private static final int COLUMN_INDEX_TYPE = 1; + private static final int COLUMN_INDEX_URI = 2; + private static final int COLUMN_INDEX_CACHE_KEY = 3; + private static final int COLUMN_INDEX_STATE = 4; + private static final int COLUMN_INDEX_DOWNLOAD_PERCENTAGE = 5; + private static final int COLUMN_INDEX_DOWNLOADED_BYTES = 6; + private static final int COLUMN_INDEX_TOTAL_BYTES = 7; + private static final int COLUMN_INDEX_FAILURE_REASON = 8; + private static final int COLUMN_INDEX_STOP_FLAGS = 9; + private static final int COLUMN_INDEX_START_TIME_MS = 10; + private static final int COLUMN_INDEX_UPDATE_TIME_MS = 11; + private static final int COLUMN_INDEX_STREAM_KEYS = 12; + private static final int COLUMN_INDEX_CUSTOM_METADATA = 13; + + private static final String COLUMN_SELECTION_ID = COLUMN_ID + " = ?"; + + private static final String[] COLUMNS = + new String[] { + COLUMN_ID, + COLUMN_TYPE, + COLUMN_URI, + COLUMN_CACHE_KEY, + COLUMN_STATE, + COLUMN_DOWNLOAD_PERCENTAGE, + COLUMN_DOWNLOADED_BYTES, + COLUMN_TOTAL_BYTES, + COLUMN_FAILURE_REASON, + COLUMN_STOP_FLAGS, + COLUMN_START_TIME_MS, + COLUMN_UPDATE_TIME_MS, + COLUMN_STREAM_KEYS, + COLUMN_CUSTOM_METADATA + }; + + private static final String SQL_DROP_TABLE = "DROP TABLE IF EXISTS " + TABLE_NAME; + private static final String SQL_CREATE_TABLE = + "CREATE TABLE IF NOT EXISTS " + + TABLE_NAME + + " (" + + COLUMN_ID + + " TEXT PRIMARY KEY NOT NULL," + + COLUMN_TYPE + + " TEXT NOT NULL," + + COLUMN_URI + + " TEXT NOT NULL," + + COLUMN_CACHE_KEY + + " TEXT," + + COLUMN_STATE + + " INTEGER NOT NULL," + + COLUMN_DOWNLOAD_PERCENTAGE + + " REAL NOT NULL," + + COLUMN_DOWNLOADED_BYTES + + " INTEGER NOT NULL," + + COLUMN_TOTAL_BYTES + + " INTEGER NOT NULL," + + COLUMN_FAILURE_REASON + + " INTEGER NOT NULL," + + COLUMN_STOP_FLAGS + + " INTEGER NOT NULL," + + COLUMN_START_TIME_MS + + " INTEGER NOT NULL," + + COLUMN_UPDATE_TIME_MS + + " INTEGER NOT NULL," + + COLUMN_STREAM_KEYS + + " TEXT NOT NULL," + + COLUMN_CUSTOM_METADATA + + " BLOB NOT NULL)"; + + private final DatabaseProvider databaseProvider; + + public DownloadStateTable(DatabaseProvider databaseProvider) { + this.databaseProvider = databaseProvider; + VersionTable versionTable = new VersionTable(databaseProvider); + int version = versionTable.getVersion(VersionTable.FEATURE_OFFLINE); + if (!doesTableExist(databaseProvider, TABLE_NAME) + || version == 0 + || version > TABLE_VERSION) { + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.beginTransaction(); + try { + writableDatabase.execSQL(SQL_DROP_TABLE); + writableDatabase.execSQL(SQL_CREATE_TABLE); + versionTable.setVersion(VersionTable.FEATURE_OFFLINE, TABLE_VERSION); + writableDatabase.setTransactionSuccessful(); + } finally { + writableDatabase.endTransaction(); + } + } else if (version < TABLE_VERSION) { + // There is no previous version currently. + throw new IllegalStateException(); + } + } + + public void replace(DownloadState downloadState) { + ContentValues values = new ContentValues(); + values.put(COLUMN_ID, downloadState.id); + values.put(COLUMN_TYPE, downloadState.type); + values.put(COLUMN_URI, downloadState.uri.toString()); + values.put(COLUMN_CACHE_KEY, downloadState.cacheKey); + values.put(COLUMN_STATE, downloadState.state); + values.put(COLUMN_DOWNLOAD_PERCENTAGE, downloadState.downloadPercentage); + values.put(COLUMN_DOWNLOADED_BYTES, downloadState.downloadedBytes); + values.put(COLUMN_TOTAL_BYTES, downloadState.totalBytes); + values.put(COLUMN_FAILURE_REASON, downloadState.failureReason); + values.put(COLUMN_STOP_FLAGS, downloadState.stopFlags); + values.put(COLUMN_START_TIME_MS, downloadState.startTimeMs); + values.put(COLUMN_UPDATE_TIME_MS, downloadState.updateTimeMs); + values.put(COLUMN_STREAM_KEYS, encodeStreamKeys(downloadState.streamKeys)); + values.put(COLUMN_CUSTOM_METADATA, downloadState.customMetadata); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values); + } + + @Nullable + public DownloadState get(String id) { + String[] selectionArgs = {id}; + try (Cursor cursor = query(COLUMN_SELECTION_ID, selectionArgs)) { + if (cursor.getCount() == 0) { + return null; + } + cursor.moveToNext(); + DownloadState downloadState = getDownloadState(cursor); + Assertions.checkState(id.equals(downloadState.id)); + return downloadState; + } + } + + public DownloadStateCursor get(@DownloadState.State int... states) { + String selection = null; + if (states.length > 0) { + StringBuilder selectionBuilder = new StringBuilder(); + selectionBuilder.append(COLUMN_STATE).append(" IN ("); + for (int i = 0; i < states.length; i++) { + if (i > 0) { + selectionBuilder.append(','); + } + selectionBuilder.append(states[i]); + } + selectionBuilder.append(')'); + selection = selectionBuilder.toString(); + } + Cursor cursor = query(selection, /* selectionArgs= */ null); + return new DownloadStateCursorImpl(cursor); + } + + public void delete(String id) { + String[] selectionArgs = {id}; + databaseProvider.getWritableDatabase().delete(TABLE_NAME, COLUMN_SELECTION_ID, selectionArgs); + } + + private Cursor query(@Nullable String selection, @Nullable String[] selectionArgs) { + String sortOrder = COLUMN_START_TIME_MS + " ASC"; + return databaseProvider + .getReadableDatabase() + .query( + TABLE_NAME, + COLUMNS, + selection, + selectionArgs, + /* groupBy= */ null, + /* having= */ null, + sortOrder); + } + + private static DownloadState getDownloadState(Cursor cursor) { + return new DownloadState( + cursor.getString(COLUMN_INDEX_ID), + cursor.getString(COLUMN_INDEX_TYPE), + Uri.parse(cursor.getString(COLUMN_INDEX_URI)), + cursor.getString(COLUMN_INDEX_CACHE_KEY), + cursor.getInt(COLUMN_INDEX_STATE), + cursor.getFloat(COLUMN_INDEX_DOWNLOAD_PERCENTAGE), + cursor.getLong(COLUMN_INDEX_DOWNLOADED_BYTES), + cursor.getLong(COLUMN_INDEX_TOTAL_BYTES), + cursor.getInt(COLUMN_INDEX_FAILURE_REASON), + cursor.getInt(COLUMN_INDEX_STOP_FLAGS), + cursor.getLong(COLUMN_INDEX_START_TIME_MS), + cursor.getLong(COLUMN_INDEX_UPDATE_TIME_MS), + decodeStreamKeys(cursor.getString(COLUMN_INDEX_STREAM_KEYS)), + cursor.getBlob(COLUMN_INDEX_CUSTOM_METADATA)); + } + + private static String encodeStreamKeys(StreamKey[] streamKeys) { + StringBuilder stringBuilder = new StringBuilder(); + for (StreamKey streamKey : streamKeys) { + stringBuilder + .append(streamKey.periodIndex) + .append('.') + .append(streamKey.groupIndex) + .append('.') + .append(streamKey.trackIndex) + .append(','); + } + if (stringBuilder.length() > 0) { + stringBuilder.setLength(stringBuilder.length() - 1); + } + return stringBuilder.toString(); + } + + private static StreamKey[] decodeStreamKeys(String encodedStreamKeys) { + if (encodedStreamKeys.isEmpty()) { + return new StreamKey[0]; + } + String[] streamKeysStrings = Util.split(encodedStreamKeys, ","); + int streamKeysCount = streamKeysStrings.length; + StreamKey[] streamKeys = new StreamKey[streamKeysCount]; + for (int i = 0; i < streamKeysCount; i++) { + String[] indices = Util.split(streamKeysStrings[i], "\\."); + Assertions.checkState(indices.length == 3); + streamKeys[i] = + new StreamKey( + Integer.parseInt(indices[0]), + Integer.parseInt(indices[1]), + Integer.parseInt(indices[2])); + } + return streamKeys; + } + } + + @VisibleForTesting + /* package */ static final class VersionTable { + private static final String TABLE_NAME = "ExoPlayerVersions"; + + private static final String COLUMN_FEATURE = "feature"; + private static final String COLUMN_VERSION = "version"; + + private static final String SQL_CREATE_TABLE = + "CREATE TABLE IF NOT EXISTS " + + TABLE_NAME + + " (" + + COLUMN_FEATURE + + " INTEGER PRIMARY KEY NOT NULL," + + COLUMN_VERSION + + " INTEGER NOT NULL)"; + + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({FEATURE_OFFLINE, FEATURE_CACHE}) + private @interface Feature {} + + public static final int FEATURE_OFFLINE = 0; + public static final int FEATURE_CACHE = 1; + + private final DatabaseProvider databaseProvider; + + public VersionTable(DatabaseProvider databaseProvider) { + this.databaseProvider = databaseProvider; + if (!doesTableExist(databaseProvider, TABLE_NAME)) { + databaseProvider.getWritableDatabase().execSQL(SQL_CREATE_TABLE); + } + } + + public void setVersion(@Feature int feature, int version) { + ContentValues values = new ContentValues(); + values.put(COLUMN_FEATURE, feature); + values.put(COLUMN_VERSION, version); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values); + } + + public int getVersion(@Feature int feature) { + String selection = COLUMN_FEATURE + " = ?"; + String[] selectionArgs = {Integer.toString(feature)}; + try (Cursor cursor = + databaseProvider + .getReadableDatabase() + .query( + TABLE_NAME, + new String[] {COLUMN_VERSION}, + selection, + selectionArgs, + /* groupBy= */ null, + /* having= */ null, + /* orderBy= */ null)) { + if (cursor.getCount() == 0) { + return 0; + } + cursor.moveToNext(); + return cursor.getInt(/* COLUMN_VERSION index */ 0); + } + } + } + + private static final class DefaultDatabaseProvider extends SQLiteOpenHelper + implements DatabaseProvider { + public DefaultDatabaseProvider(Context context) { + super(context, DATABASE_NAME, /* factory= */ null, /* version= */ 1); + } + + @Override + public void onCreate(SQLiteDatabase db) { + // Table creation is done in DownloadStateTable constructor. + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // Upgrade is handled in DownloadStateTable constructor. + } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // TODO: Wipe the database. + super.onDowngrade(db, oldVersion, newVersion); + } + + // DatabaseProvider implementation. + + @Override + public synchronized void close() { + super.close(); + } + + @Override + public SQLiteDatabase getWritableDatabase() { + return super.getWritableDatabase(); + } + + @Override + public SQLiteDatabase getReadableDatabase() { + return super.getReadableDatabase(); + } + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java index d809ab4754..40ea094b5e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadAction.java @@ -156,7 +156,7 @@ public final class DownloadAction { ArrayList mutableKeys = new ArrayList<>(keys); Collections.sort(mutableKeys); this.keys = Collections.unmodifiableList(mutableKeys); - this.data = data != null ? data : Util.EMPTY_BYTE_ARRAY; + this.data = data != null ? Arrays.copyOf(data, data.length) : Util.EMPTY_BYTE_ARRAY; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java new file mode 100644 index 0000000000..71726b64a9 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.offline; + +import android.support.annotation.Nullable; + +/** Persists {@link DownloadState}s. */ +interface DownloadIndex { + /** Releases the used resources. */ + void release(); + + /** + * Returns the {@link DownloadState} with the given {@code id}, or null. + * + * @param id ID of a {@link DownloadState}. + * @return The {@link DownloadState} with the given {@code id}, or null if a download state with + * this id doesn't exist. + */ + @Nullable + DownloadState getDownloadState(String id); + + /** + * Returns a {@link DownloadStateCursor} to {@link DownloadState}s with the given {@code states}. + * + * @param states Returns only the {@link DownloadState}s with this states. If empty, returns all. + * @return A cursor to {@link DownloadState}s with the given {@code states}. + */ + DownloadStateCursor getDownloadStates(@DownloadState.State int... states); + + /** + * Adds or replaces a {@link DownloadState}. + * + * @param downloadState The {@link DownloadState} to be added. + */ + void putDownloadState(DownloadState downloadState); + + /** Removes the {@link DownloadState} with the given {@code id}. */ + void removeDownloadState(String id); +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java index eed32720a3..7bbd078822 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.google.android.exoplayer2.offline; import android.net.Uri; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadStateCursor.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadStateCursor.java new file mode 100644 index 0000000000..680976c77b --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadStateCursor.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.offline; + +/** Provides random read-write access to the result set returned by a database query. */ +interface DownloadStateCursor { + + /** Returns the DownloadState at the current position. */ + DownloadState getDownloadState(); + + /** Returns the numbers of DownloadStates in the cursor. */ + int getCount(); + + /** + * Returns the current position of the cursor in the DownloadState set. The value is zero-based. + * When the DownloadState set is first returned the cursor will be at positon -1, which is before + * the first DownloadState. After the last DownloadState is returned another call to next() will + * leave the cursor past the last entry, at a position of count(). + * + * @return the current cursor position. + */ + int getPosition(); + + /** + * Move the cursor to an absolute position. The valid range of values is -1 <= position <= + * count. + * + *

This method will return true if the request destination was reachable, otherwise, it returns + * false. + * + * @param position the zero-based position to move to. + * @return whether the requested move fully succeeded. + */ + boolean moveToPosition(int position); + + /** + * Move the cursor to the first DownloadState. + * + *

This method will return false if the cursor is empty. + * + * @return whether the move succeeded. + */ + default boolean moveToFirst() { + return moveToPosition(0); + } + + /** + * Move the cursor to the last DownloadState. + * + *

This method will return false if the cursor is empty. + * + * @return whether the move succeeded. + */ + default boolean moveToLast() { + return moveToPosition(getCount() - 1); + } + + /** + * Move the cursor to the next DownloadState. + * + *

This method will return false if the cursor is already past the last entry in the result + * set. + * + * @return whether the move succeeded. + */ + default boolean moveToNext() { + return moveToPosition(getPosition() + 1); + } + + /** + * Move the cursor to the previous DownloadState. + * + *

This method will return false if the cursor is already before the first entry in the result + * set. + * + * @return whether the move succeeded. + */ + default boolean moveToPrevious() { + return moveToPosition(getPosition() - 1); + } + + /** Returns whether the cursor is pointing to the first DownloadState. */ + default boolean isFirst() { + return getPosition() == 0 && getCount() != 0; + } + + /** Returns whether the cursor is pointing to the last DownloadState. */ + default boolean isLast() { + int count = getCount(); + return getPosition() == (count - 1) && count != 0; + } + + /** Returns whether the cursor is pointing to the position before the first DownloadState. */ + default boolean isBeforeFirst() { + if (getCount() == 0) { + return true; + } + return getPosition() == -1; + } + + /** Returns whether the cursor is pointing to the position after the last DownloadState. */ + default boolean isAfterLast() { + if (getCount() == 0) { + return true; + } + return getPosition() == getCount(); + } + + /** Closes the Cursor, releasing all of its resources and making it completely invalid. */ + void close(); + + /** Returns whether the cursor is closed */ + boolean isClosed(); +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java new file mode 100644 index 0000000000..fe32415ffa --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java @@ -0,0 +1,491 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.offline; + +import static com.google.android.exoplayer2.offline.DefaultDownloadIndex.VersionTable.FEATURE_CACHE; +import static com.google.android.exoplayer2.offline.DefaultDownloadIndex.VersionTable.FEATURE_OFFLINE; +import static com.google.common.truth.Truth.assertThat; + +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.net.Uri; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import java.util.Arrays; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +/** Unit tests for {@link DefaultDownloadIndex}. */ +@RunWith(RobolectricTestRunner.class) +public class DefaultDownloadIndexTest { + + private DefaultDownloadIndex downloadIndex; + + @Before + public void setUp() { + downloadIndex = new DefaultDownloadIndex(RuntimeEnvironment.application); + } + + @After + public void tearDown() { + downloadIndex.release(); + } + + @Test + public void getDownloadState_nonExistingId_returnsNull() { + assertThat(downloadIndex.getDownloadState("non existing id")).isNull(); + } + + @Test + public void addAndGetDownloadState_nonExistingId_returnsTheSameDownloadState() { + String id = "id"; + DownloadState downloadState = new DownloadStateBuilder(id).build(); + + downloadIndex.putDownloadState(downloadState); + DownloadState readDownloadState = downloadIndex.getDownloadState(id); + + assertEqual(readDownloadState, downloadState); + } + + @Test + public void addAndGetDownloadState_existingId_returnsUpdatedDownloadState() { + String id = "id"; + DownloadStateBuilder downloadStateBuilder = new DownloadStateBuilder(id); + downloadIndex.putDownloadState(downloadStateBuilder.build()); + + DownloadState downloadState = + downloadStateBuilder + .setType("different type") + .setUri("different uri") + .setCacheKey("different cacheKey") + .setState(DownloadState.STATE_FAILED) + .setDownloadPercentage(50) + .setDownloadedBytes(200) + .setTotalBytes(400) + .setFailureReason(DownloadState.FAILURE_REASON_UNKNOWN) + .setStopFlags(DownloadState.STOP_FLAG_STOPPED) + .setStartTimeMs(10) + .setUpdateTimeMs(20) + .setStreamKeys( + new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* trackIndex= */ 2), + new StreamKey(/* periodIndex= */ 3, /* groupIndex= */ 4, /* trackIndex= */ 5)) + .setCustomMetadata(new byte[] {0, 1, 2, 3}) + .build(); + downloadIndex.putDownloadState(downloadState); + DownloadState readDownloadState = downloadIndex.getDownloadState(id); + + assertThat(readDownloadState).isNotNull(); + assertEqual(readDownloadState, downloadState); + } + + @Test + public void releaseAndRecreateDownloadIndex_returnsTheSameDownloadState() { + String id = "id"; + DownloadState downloadState = new DownloadStateBuilder(id).build(); + + downloadIndex.putDownloadState(downloadState); + downloadIndex.release(); + downloadIndex = new DefaultDownloadIndex(RuntimeEnvironment.application); + DownloadState readDownloadState = downloadIndex.getDownloadState(id); + + assertThat(readDownloadState).isNotNull(); + assertEqual(readDownloadState, downloadState); + } + + @Test + public void customDatabaseProvider_getDownloadStateReturnsNull() { + String id = "id"; + DownloadState downloadState = new DownloadStateBuilder(id).build(); + + downloadIndex.putDownloadState(downloadState); + downloadIndex.release(); + DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl(); + downloadIndex = new DefaultDownloadIndex(databaseProvider); + DownloadState readDownloadState = downloadIndex.getDownloadState(id); + + assertThat(readDownloadState).isNull(); + databaseProvider.close(); + } + + @Test + public void removeDownloadState_nonExistingId_doesNotFail() { + downloadIndex.removeDownloadState("non existing id"); + } + + @Test + public void removeDownloadState_existingId_getDownloadStateReturnsNull() { + String id = "id"; + DownloadState downloadState = new DownloadStateBuilder(id).build(); + downloadIndex.putDownloadState(downloadState); + + downloadIndex.removeDownloadState(id); + DownloadState readDownloadState = downloadIndex.getDownloadState(id); + + assertThat(readDownloadState).isNull(); + } + + @Test + public void getDownloadStates_emptyDownloadIndex_returnsEmptyArray() { + assertThat(downloadIndex.getDownloadStates().getCount()).isEqualTo(0); + } + + @Test + public void getDownloadStates_noState_returnsAllDownloadStatusSortedByStartTime() { + DownloadState downloadState1 = new DownloadStateBuilder("id1").setStartTimeMs(1).build(); + downloadIndex.putDownloadState(downloadState1); + DownloadState downloadState2 = new DownloadStateBuilder("id2").setStartTimeMs(0).build(); + downloadIndex.putDownloadState(downloadState2); + + DownloadStateCursor cursor = downloadIndex.getDownloadStates(); + + assertThat(cursor.getCount()).isEqualTo(2); + cursor.moveToNext(); + assertEqual(cursor.getDownloadState(), downloadState2); + cursor.moveToNext(); + assertEqual(cursor.getDownloadState(), downloadState1); + } + + @Test + public void getDownloadStates_withStates_returnsAllDownloadStatusWithTheSameStates() { + DownloadState downloadState1 = + new DownloadStateBuilder("id1") + .setStartTimeMs(0) + .setState(DownloadState.STATE_REMOVED) + .build(); + downloadIndex.putDownloadState(downloadState1); + DownloadState downloadState2 = + new DownloadStateBuilder("id2") + .setStartTimeMs(1) + .setState(DownloadState.STATE_STOPPED) + .build(); + downloadIndex.putDownloadState(downloadState2); + DownloadState downloadState3 = + new DownloadStateBuilder("id3") + .setStartTimeMs(2) + .setState(DownloadState.STATE_COMPLETED) + .build(); + downloadIndex.putDownloadState(downloadState3); + + DownloadStateCursor cursor = + downloadIndex.getDownloadStates(DownloadState.STATE_REMOVED, DownloadState.STATE_COMPLETED); + + assertThat(cursor.getCount()).isEqualTo(2); + cursor.moveToNext(); + assertEqual(cursor.getDownloadState(), downloadState1); + cursor.moveToNext(); + assertEqual(cursor.getDownloadState(), downloadState3); + } + + @Test + public void doesTableExist_nonExistingTable_returnsFalse() { + DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl(); + + assertThat(DefaultDownloadIndex.doesTableExist(databaseProvider, "NonExistingTable")).isFalse(); + + databaseProvider.close(); + } + + @Test + public void doesTableExist_existingTable_returnsTrue() { + DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl(); + String tableName = "ExistingTable"; + databaseProvider.getWritableDatabase().execSQL("CREATE TABLE " + tableName + "(dummy)"); + + assertThat(DefaultDownloadIndex.doesTableExist(databaseProvider, tableName)).isTrue(); + + databaseProvider.close(); + } + + @Test + public void getVersion_nonExistingTable_returnsZero() { + DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl(); + DefaultDownloadIndex.VersionTable versionTable = + new DefaultDownloadIndex.VersionTable(databaseProvider); + + int version = versionTable.getVersion(FEATURE_OFFLINE); + + assertThat(version).isEqualTo(0); + databaseProvider.close(); + } + + @Test + public void getVersion_returnsSetVersion() { + DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl(); + DefaultDownloadIndex.VersionTable versionTable = + new DefaultDownloadIndex.VersionTable(databaseProvider); + + versionTable.setVersion(FEATURE_OFFLINE, 1); + assertThat(versionTable.getVersion(FEATURE_OFFLINE)).isEqualTo(1); + + versionTable.setVersion(FEATURE_OFFLINE, 10); + assertThat(versionTable.getVersion(FEATURE_OFFLINE)).isEqualTo(10); + + versionTable.setVersion(FEATURE_CACHE, 5); + assertThat(versionTable.getVersion(FEATURE_CACHE)).isEqualTo(5); + assertThat(versionTable.getVersion(FEATURE_OFFLINE)).isEqualTo(10); + + databaseProvider.close(); + } + + @Test + public void downloadStateTableConstructor_noTable_createsTable() { + DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl(); + assertThat( + DefaultDownloadIndex.doesTableExist( + databaseProvider, DefaultDownloadIndex.DownloadStateTable.TABLE_NAME)) + .isFalse(); + + new DefaultDownloadIndex.DownloadStateTable(databaseProvider); + + assertThat( + DefaultDownloadIndex.doesTableExist( + databaseProvider, DefaultDownloadIndex.DownloadStateTable.TABLE_NAME)) + .isTrue(); + + databaseProvider.close(); + } + + @Test + public void downloadStateTableConstructor_versionZero_versionSet() { + DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl(); + + new DefaultDownloadIndex.DownloadStateTable(databaseProvider); + + DefaultDownloadIndex.VersionTable versionTable = + new DefaultDownloadIndex.VersionTable(databaseProvider); + assertThat(versionTable.getVersion(FEATURE_OFFLINE)) + .isEqualTo(DefaultDownloadIndex.DownloadStateTable.TABLE_VERSION); + databaseProvider.close(); + } + + @Test + public void downloadStateTableConstructor_greaterVersion_tableRecreated() { + DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl(); + databaseProvider + .getWritableDatabase() + .execSQL("CREATE TABLE " + DefaultDownloadIndex.DownloadStateTable.TABLE_NAME + "(dummy)"); + DefaultDownloadIndex.VersionTable versionTable = + new DefaultDownloadIndex.VersionTable(databaseProvider); + versionTable.setVersion(FEATURE_OFFLINE, Integer.MAX_VALUE); + + DefaultDownloadIndex.DownloadStateTable downloadStateTable = + new DefaultDownloadIndex.DownloadStateTable(databaseProvider); + String id = "id"; + DownloadState downloadState = new DownloadStateBuilder(id).build(); + downloadStateTable.replace(downloadState); + DownloadState readDownloadState = downloadStateTable.get(id); + assertEqual(readDownloadState, downloadState); + + assertThat(versionTable.getVersion(FEATURE_OFFLINE)) + .isEqualTo(DefaultDownloadIndex.DownloadStateTable.TABLE_VERSION); + databaseProvider.close(); + } + + private static void assertEqual(DownloadState downloadState, DownloadState expected) { + assertThat(areEqual(downloadState, expected)).isTrue(); + } + + private static boolean areEqual(DownloadState downloadState, DownloadState that) { + if (downloadState.state != that.state) { + return false; + } + if (Float.compare(that.downloadPercentage, downloadState.downloadPercentage) != 0) { + return false; + } + if (downloadState.downloadedBytes != that.downloadedBytes) { + return false; + } + if (downloadState.totalBytes != that.totalBytes) { + return false; + } + if (downloadState.startTimeMs != that.startTimeMs) { + return false; + } + if (downloadState.updateTimeMs != that.updateTimeMs) { + return false; + } + if (downloadState.failureReason != that.failureReason) { + return false; + } + if (downloadState.stopFlags != that.stopFlags) { + return false; + } + if (!downloadState.id.equals(that.id)) { + return false; + } + if (!downloadState.type.equals(that.type)) { + return false; + } + if (!downloadState.uri.equals(that.uri)) { + return false; + } + if (downloadState.cacheKey != null + ? !downloadState.cacheKey.equals(that.cacheKey) + : that.cacheKey != null) { + return false; + } + if (!Arrays.equals(downloadState.streamKeys, that.streamKeys)) { + return false; + } + return Arrays.equals(downloadState.customMetadata, that.customMetadata); + } + + private static class DownloadStateBuilder { + private String id; + private String type; + private String uri; + @Nullable private String cacheKey; + private int state; + private float downloadPercentage; + private long downloadedBytes; + private long totalBytes; + private int failureReason; + private int stopFlags; + private long startTimeMs; + private long updateTimeMs; + private StreamKey[] streamKeys; + private byte[] customMetadata; + + private DownloadStateBuilder(String id) { + this.id = id; + this.type = "type"; + this.uri = "uri"; + this.cacheKey = null; + this.state = DownloadState.STATE_QUEUED; + this.downloadPercentage = (float) C.PERCENTAGE_UNSET; + this.downloadedBytes = (long) 0; + this.totalBytes = (long) C.LENGTH_UNSET; + this.failureReason = DownloadState.FAILURE_REASON_NONE; + this.stopFlags = 0; + this.startTimeMs = (long) 0; + this.updateTimeMs = (long) 0; + this.streamKeys = new StreamKey[0]; + this.customMetadata = new byte[0]; + } + + public DownloadStateBuilder setId(String id) { + this.id = id; + return this; + } + + public DownloadStateBuilder setType(String type) { + this.type = type; + return this; + } + + public DownloadStateBuilder setUri(String uri) { + this.uri = uri; + return this; + } + + public DownloadStateBuilder setCacheKey(@Nullable String cacheKey) { + this.cacheKey = cacheKey; + return this; + } + + public DownloadStateBuilder setState(int state) { + this.state = state; + return this; + } + + public DownloadStateBuilder setDownloadPercentage(float downloadPercentage) { + this.downloadPercentage = downloadPercentage; + return this; + } + + public DownloadStateBuilder setDownloadedBytes(long downloadedBytes) { + this.downloadedBytes = downloadedBytes; + return this; + } + + public DownloadStateBuilder setTotalBytes(long totalBytes) { + this.totalBytes = totalBytes; + return this; + } + + public DownloadStateBuilder setFailureReason(int failureReason) { + this.failureReason = failureReason; + return this; + } + + public DownloadStateBuilder setStopFlags(int stopFlags) { + this.stopFlags = stopFlags; + return this; + } + + public DownloadStateBuilder setStartTimeMs(long startTimeMs) { + this.startTimeMs = startTimeMs; + return this; + } + + public DownloadStateBuilder setUpdateTimeMs(long updateTimeMs) { + this.updateTimeMs = updateTimeMs; + return this; + } + + public DownloadStateBuilder setStreamKeys(StreamKey... streamKeys) { + this.streamKeys = streamKeys; + return this; + } + + public DownloadStateBuilder setCustomMetadata(byte[] customMetadata) { + this.customMetadata = customMetadata; + return this; + } + + public DownloadState build() { + return new DownloadState( + id, + type, + Uri.parse(uri), + cacheKey, + state, + downloadPercentage, + downloadedBytes, + totalBytes, + failureReason, + stopFlags, + startTimeMs, + updateTimeMs, + streamKeys, + customMetadata); + } + } + + private static final class DatabaseProviderImpl extends SQLiteOpenHelper + implements DefaultDownloadIndex.DatabaseProvider { + private static final int DATABASE_VERSION = 1; + private static final String DATABASE_NAME = "TestExoPlayerDownloadIndex.db"; + + public DatabaseProviderImpl() { + super(RuntimeEnvironment.application, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + // Do nothing. + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // Do nothing. + } + } +} From 71d4f39400f20743e01c98f6e1f9a06a0200cfaa Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 10 Jan 2019 14:46:40 +0000 Subject: [PATCH 004/110] Call Listener methods on the thread started RequirementsWatcher PiperOrigin-RevId: 228701917 --- .../scheduler/RequirementsWatcher.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java index 021c10439a..fded95614c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java @@ -68,6 +68,7 @@ public final class RequirementsWatcher { private int notMetRequirements; private CapabilityValidatedCallback networkCallback; + private Handler handler; /** * @param context Any context. @@ -87,6 +88,7 @@ public final class RequirementsWatcher { */ public void start() { Assertions.checkNotNull(Looper.myLooper()); + handler = new Handler(); notMetRequirements = requirements.getNotMetRequirements(context); @@ -111,7 +113,7 @@ public final class RequirementsWatcher { } } receiver = new DeviceStatusChangeReceiver(); - context.registerReceiver(receiver, filter, null, new Handler()); + context.registerReceiver(receiver, filter, null, handler); logd(this + " started"); } @@ -195,16 +197,22 @@ public final class RequirementsWatcher { private final class CapabilityValidatedCallback extends ConnectivityManager.NetworkCallback { @Override public void onAvailable(Network network) { - super.onAvailable(network); - logd(RequirementsWatcher.this + " NetworkCallback.onAvailable"); - checkRequirements(); + onNetworkCallback(); } @Override public void onLost(Network network) { - super.onLost(network); - logd(RequirementsWatcher.this + " NetworkCallback.onLost"); - checkRequirements(); + onNetworkCallback(); + } + + private void onNetworkCallback() { + handler.post( + () -> { + if (networkCallback != null) { + logd(RequirementsWatcher.this + " NetworkCallback"); + checkRequirements(); + } + }); } } } From 2d30d66746242018a3aff0f239048e93c34ecc0f Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 11 Jan 2019 18:39:27 +0000 Subject: [PATCH 005/110] Fix release of DRM sessions There were some edge cases in which we'd forget to release DRM sessions. For example if we read a format and acquired a pendingDrmSession (in onInputFormatChanged), then immediately read another format and overwrote pendingDrmSession, we'd forget to release the one that's been overwritten. This change hopefully makes release much clearer. We keep a list of all drm sessions we're currently holding. Whenever we update either drmSession or pendingDrmSession, we release any other sessions that are in the list. PiperOrigin-RevId: 228905465 --- .../ext/vp9/LibvpxVideoRenderer.java | 82 +++++++------ .../audio/AudioRendererEventListener.java | 1 + .../audio/MediaCodecAudioRenderer.java | 1 - .../audio/SimpleDecoderAudioRenderer.java | 81 +++++++------ .../mediacodec/MediaCodecRenderer.java | 111 +++++++++--------- .../video/MediaCodecVideoRenderer.java | 1 - .../video/VideoRendererEventListener.java | 1 + 7 files changed, 148 insertions(+), 130 deletions(-) diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index e3081cd2d2..e61030a2e1 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -127,8 +127,8 @@ public class LibvpxVideoRenderer extends BaseRenderer { private VpxDecoder decoder; private VpxInputBuffer inputBuffer; private VpxOutputBuffer outputBuffer; - private DrmSession drmSession; - private DrmSession pendingDrmSession; + @Nullable private DrmSession decoderDrmSession; + @Nullable private DrmSession sourceDrmSession; private @ReinitializationState int decoderReinitializationState; private boolean decoderReceivedBuffers; @@ -364,24 +364,10 @@ public class LibvpxVideoRenderer extends BaseRenderer { clearReportedVideoSize(); clearRenderedFirstFrame(); try { + setSourceDrmSession(null); releaseDecoder(); } finally { - try { - if (drmSession != null) { - drmSessionManager.releaseSession(drmSession); - } - } finally { - try { - if (pendingDrmSession != null && pendingDrmSession != drmSession) { - drmSessionManager.releaseSession(pendingDrmSession); - } - } finally { - drmSession = null; - pendingDrmSession = null; - decoderCounters.ensureUpdated(); - eventDispatcher.disabled(decoderCounters); - } - } + eventDispatcher.disabled(decoderCounters); } } @@ -433,18 +419,35 @@ public class LibvpxVideoRenderer extends BaseRenderer { /** Releases the decoder. */ @CallSuper protected void releaseDecoder() { - if (decoder == null) { - return; - } - inputBuffer = null; outputBuffer = null; - decoder.release(); - decoder = null; - decoderCounters.decoderReleaseCount++; decoderReinitializationState = REINITIALIZATION_STATE_NONE; decoderReceivedBuffers = false; buffersInCodecCount = 0; + if (decoder != null) { + decoder.release(); + decoder = null; + decoderCounters.decoderReleaseCount++; + } + setDecoderDrmSession(null); + } + + private void setSourceDrmSession(@Nullable DrmSession session) { + DrmSession previous = sourceDrmSession; + sourceDrmSession = session; + releaseDrmSessionIfUnused(previous); + } + + private void setDecoderDrmSession(@Nullable DrmSession session) { + DrmSession previous = decoderDrmSession; + decoderDrmSession = session; + releaseDrmSessionIfUnused(previous); + } + + private void releaseDrmSessionIfUnused(@Nullable DrmSession session) { + if (session != null && session != decoderDrmSession && session != sourceDrmSession) { + drmSessionManager.releaseSession(session); + } } /** @@ -467,16 +470,20 @@ public class LibvpxVideoRenderer extends BaseRenderer { throw ExoPlaybackException.createForRenderer( new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); } - pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(), format.drmInitData); - if (pendingDrmSession == drmSession) { - drmSessionManager.releaseSession(pendingDrmSession); + DrmSession session = + drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); + if (session == decoderDrmSession || session == sourceDrmSession) { + // We already had this session. The manager must be reference counting, so release it once + // to get the count attributed to this renderer back down to 1. + drmSessionManager.releaseSession(session); } + setSourceDrmSession(session); } else { - pendingDrmSession = null; + setSourceDrmSession(null); } } - if (pendingDrmSession != drmSession) { + if (sourceDrmSession != decoderDrmSession) { if (decoderReceivedBuffers) { // Signal end of stream and wait for any final output buffers before re-initialization. decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; @@ -704,12 +711,13 @@ public class LibvpxVideoRenderer extends BaseRenderer { return; } - drmSession = pendingDrmSession; + setDecoderDrmSession(sourceDrmSession); + ExoMediaCrypto mediaCrypto = null; - if (drmSession != null) { - mediaCrypto = drmSession.getMediaCrypto(); + if (decoderDrmSession != null) { + mediaCrypto = decoderDrmSession.getMediaCrypto(); if (mediaCrypto == null) { - DrmSessionException drmError = drmSession.getError(); + DrmSessionException drmError = decoderDrmSession.getError(); if (drmError != null) { // Continue for now. We may be able to avoid failure if the session recovers, or if a new // input format causes the session to be replaced before it's used. @@ -922,12 +930,12 @@ public class LibvpxVideoRenderer extends BaseRenderer { } private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { - if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { + if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { return false; } - @DrmSession.State int drmSessionState = drmSession.getState(); + @DrmSession.State int drmSessionState = decoderDrmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); + throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex()); } return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java index eff7bc8de2..48fbea75b4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java @@ -147,6 +147,7 @@ public interface AudioRendererEventListener { * Invokes {@link AudioRendererEventListener#onAudioDisabled(DecoderCounters)}. */ public void disabled(final DecoderCounters counters) { + counters.ensureUpdated(); if (listener != null) { handler.post( () -> { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 49c391c4cc..7fc6c16db8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -548,7 +548,6 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media try { super.onDisabled(); } finally { - decoderCounters.ensureUpdated(); eventDispatcher.disabled(decoderCounters); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java index bfd7bbc165..f2e8a23811 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java @@ -106,8 +106,8 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements ? extends AudioDecoderException> decoder; private DecoderInputBuffer inputBuffer; private SimpleOutputBuffer outputBuffer; - private DrmSession drmSession; - private DrmSession pendingDrmSession; + @Nullable private DrmSession decoderDrmSession; + @Nullable private DrmSession sourceDrmSession; @ReinitializationState private int decoderReinitializationState; private boolean decoderReceivedBuffers; @@ -462,12 +462,12 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { - if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { + if (decoderDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { return false; } - @DrmSession.State int drmSessionState = drmSession.getState(); + @DrmSession.State int drmSessionState = decoderDrmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); + throw ExoPlaybackException.createForRenderer(decoderDrmSession.getError(), getIndex()); } return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } @@ -568,25 +568,11 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements audioTrackNeedsConfigure = true; waitingForKeys = false; try { + setSourceDrmSession(null); releaseDecoder(); audioSink.reset(); } finally { - try { - if (drmSession != null) { - drmSessionManager.releaseSession(drmSession); - } - } finally { - try { - if (pendingDrmSession != null && pendingDrmSession != drmSession) { - drmSessionManager.releaseSession(pendingDrmSession); - } - } finally { - drmSession = null; - pendingDrmSession = null; - decoderCounters.ensureUpdated(); - eventDispatcher.disabled(decoderCounters); - } - } + eventDispatcher.disabled(decoderCounters); } } @@ -615,12 +601,13 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements return; } - drmSession = pendingDrmSession; + setDecoderDrmSession(sourceDrmSession); + ExoMediaCrypto mediaCrypto = null; - if (drmSession != null) { - mediaCrypto = drmSession.getMediaCrypto(); + if (decoderDrmSession != null) { + mediaCrypto = decoderDrmSession.getMediaCrypto(); if (mediaCrypto == null) { - DrmSessionException drmError = drmSession.getError(); + DrmSessionException drmError = decoderDrmSession.getError(); if (drmError != null) { // Continue for now. We may be able to avoid failure if the session recovers, or if a new // input format causes the session to be replaced before it's used. @@ -646,17 +633,34 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements } private void releaseDecoder() { - if (decoder == null) { - return; - } - inputBuffer = null; outputBuffer = null; - decoder.release(); - decoder = null; - decoderCounters.decoderReleaseCount++; decoderReinitializationState = REINITIALIZATION_STATE_NONE; decoderReceivedBuffers = false; + if (decoder != null) { + decoder.release(); + decoder = null; + decoderCounters.decoderReleaseCount++; + } + setDecoderDrmSession(null); + } + + private void setSourceDrmSession(@Nullable DrmSession session) { + DrmSession previous = sourceDrmSession; + sourceDrmSession = session; + releaseDrmSessionIfUnused(previous); + } + + private void setDecoderDrmSession(@Nullable DrmSession session) { + DrmSession previous = decoderDrmSession; + decoderDrmSession = session; + releaseDrmSessionIfUnused(previous); + } + + private void releaseDrmSessionIfUnused(@Nullable DrmSession session) { + if (session != null && session != decoderDrmSession && session != sourceDrmSession) { + drmSessionManager.releaseSession(session); + } } private void onInputFormatChanged(Format newFormat) throws ExoPlaybackException { @@ -671,13 +675,16 @@ public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements throw ExoPlaybackException.createForRenderer( new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); } - pendingDrmSession = drmSessionManager.acquireSession(Looper.myLooper(), - inputFormat.drmInitData); - if (pendingDrmSession == drmSession) { - drmSessionManager.releaseSession(pendingDrmSession); + DrmSession session = + drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); + if (session == decoderDrmSession || session == sourceDrmSession) { + // We already had this session. The manager must be reference counting, so release it once + // to get the count attributed to this renderer back down to 1. + drmSessionManager.releaseSession(session); } + setSourceDrmSession(session); } else { - pendingDrmSession = null; + setSourceDrmSession(null); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index b578467933..a538bca4b2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -287,13 +287,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private final DecoderInputBuffer flagsOnlyBuffer; private final FormatHolder formatHolder; private final TimedValueQueue formatQueue; - private final List decodeOnlyPresentationTimestamps; + private final ArrayList decodeOnlyPresentationTimestamps; private final MediaCodec.BufferInfo outputBufferInfo; @Nullable private Format inputFormat; private Format outputFormat; - private DrmSession drmSession; - private DrmSession pendingDrmSession; + @Nullable private DrmSession codecDrmSession; + @Nullable private DrmSession sourceDrmSession; private long renderTimeLimitMs; private float rendererOperatingRate; @Nullable private MediaCodec codec; @@ -457,14 +457,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return; } - drmSession = pendingDrmSession; + setCodecDrmSession(sourceDrmSession); + String mimeType = inputFormat.sampleMimeType; MediaCrypto wrappedMediaCrypto = null; boolean drmSessionRequiresSecureDecoder = false; - if (drmSession != null) { - FrameworkMediaCrypto mediaCrypto = drmSession.getMediaCrypto(); + if (codecDrmSession != null) { + FrameworkMediaCrypto mediaCrypto = codecDrmSession.getMediaCrypto(); if (mediaCrypto == null) { - DrmSessionException drmError = drmSession.getError(); + DrmSessionException drmError = codecDrmSession.getError(); if (drmError != null) { // Continue for now. We may be able to avoid failure if the session recovers, or if a new // input format causes the session to be replaced before it's used. @@ -477,9 +478,9 @@ public abstract class MediaCodecRenderer extends BaseRenderer { drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType); } if (deviceNeedsDrmKeysToConfigureCodecWorkaround()) { - @DrmSession.State int drmSessionState = drmSession.getState(); + @DrmSession.State int drmSessionState = codecDrmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); + throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex()); } else if (drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS) { // Wait for keys. return; @@ -552,7 +553,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Override protected void onDisabled() { inputFormat = null; - if (drmSession != null || pendingDrmSession != null) { + if (codecDrmSession != null || sourceDrmSession != null) { // TODO: Do something better with this case. onReset(); } else { @@ -565,51 +566,32 @@ public abstract class MediaCodecRenderer extends BaseRenderer { try { releaseCodec(); } finally { - try { - if (drmSession != null) { - drmSessionManager.releaseSession(drmSession); - } - } finally { - try { - if (pendingDrmSession != null && pendingDrmSession != drmSession) { - drmSessionManager.releaseSession(pendingDrmSession); - } - } finally { - drmSession = null; - pendingDrmSession = null; - } - } + setSourceDrmSession(null); } } protected void releaseCodec() { availableCodecInfos = null; - if (codec != null) { - codecInfo = null; - codecFormat = null; - resetInputBuffer(); - resetOutputBuffer(); - resetCodecBuffers(); - waitingForKeys = false; - codecHotswapDeadlineMs = C.TIME_UNSET; - decodeOnlyPresentationTimestamps.clear(); - decoderCounters.decoderReleaseCount++; - try { - codec.stop(); - } finally { + codecInfo = null; + codecFormat = null; + resetInputBuffer(); + resetOutputBuffer(); + resetCodecBuffers(); + waitingForKeys = false; + codecHotswapDeadlineMs = C.TIME_UNSET; + decodeOnlyPresentationTimestamps.clear(); + try { + if (codec != null) { + decoderCounters.decoderReleaseCount++; try { - codec.release(); + codec.stop(); } finally { - codec = null; - if (drmSession != null && pendingDrmSession != drmSession) { - try { - drmSessionManager.releaseSession(drmSession); - } finally { - drmSession = null; - } - } + codec.release(); } } + } finally { + codec = null; + setCodecDrmSession(null); } } @@ -928,6 +910,24 @@ public abstract class MediaCodecRenderer extends BaseRenderer { outputBuffer = null; } + private void setSourceDrmSession(@Nullable DrmSession session) { + DrmSession previous = sourceDrmSession; + sourceDrmSession = session; + releaseDrmSessionIfUnused(previous); + } + + private void setCodecDrmSession(@Nullable DrmSession session) { + DrmSession previous = codecDrmSession; + codecDrmSession = session; + releaseDrmSessionIfUnused(previous); + } + + private void releaseDrmSessionIfUnused(@Nullable DrmSession session) { + if (session != null && session != codecDrmSession && session != sourceDrmSession) { + drmSessionManager.releaseSession(session); + } + } + /** * @return Whether it may be possible to feed more input data. * @throws ExoPlaybackException If an error occurs feeding the input buffer. @@ -1082,12 +1082,12 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException { - if (drmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { + if (codecDrmSession == null || (!bufferEncrypted && playClearSamplesWithoutKeys)) { return false; } - @DrmSession.State int drmSessionState = drmSession.getState(); + @DrmSession.State int drmSessionState = codecDrmSession.getState(); if (drmSessionState == DrmSession.STATE_ERROR) { - throw ExoPlaybackException.createForRenderer(drmSession.getError(), getIndex()); + throw ExoPlaybackException.createForRenderer(codecDrmSession.getError(), getIndex()); } return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } @@ -1126,13 +1126,16 @@ public abstract class MediaCodecRenderer extends BaseRenderer { throw ExoPlaybackException.createForRenderer( new IllegalStateException("Media requires a DrmSessionManager"), getIndex()); } - pendingDrmSession = + DrmSession session = drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); - if (pendingDrmSession == drmSession) { - drmSessionManager.releaseSession(pendingDrmSession); + if (session == codecDrmSession || session == sourceDrmSession) { + // We already had this session. The manager must be reference counting, so release it once + // to get the count attributed to this renderer back down to 1. + drmSessionManager.releaseSession(session); } + setSourceDrmSession(session); } else { - pendingDrmSession = null; + setSourceDrmSession(null); } } @@ -1143,7 +1146,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { // We have an existing codec that we may need to reconfigure or re-initialize. If the existing // codec instance is being kept then its operating rate may need to be updated. - if (pendingDrmSession != drmSession) { + if (sourceDrmSession != codecDrmSession) { drainAndReinitializeCodec(); } else { switch (canKeepCodec(codec, codecInfo, codecFormat, newFormat)) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 7c4287710d..b92dd44eb2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -375,7 +375,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { try { super.onDisabled(); } finally { - decoderCounters.ensureUpdated(); eventDispatcher.disabled(decoderCounters); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java index 7d78ba03c7..f2f451b3d0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java @@ -179,6 +179,7 @@ public interface VideoRendererEventListener { /** Invokes {@link VideoRendererEventListener#onVideoDisabled(DecoderCounters)}. */ public void disabled(DecoderCounters counters) { + counters.ensureUpdated(); if (listener != null) { handler.post( () -> { From 1f03093dc52c7e5ddd22ad9675d4037ea9a0aa92 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 11 Jan 2019 19:02:00 +0000 Subject: [PATCH 006/110] DRM: Pass sessionId rather than MediaCrypto ExoMediaCrypto.requiresSecureDecoderComponent() is removed, and FrameworkMediaCrypto.forceAllowInsecureDecoderComponents is made public to allow determining whether a secure decoder is required to be implemented in MediaCodecRenderer. PiperOrigin-RevId: 228909771 --- .../exoplayer2/drm/ExoMediaCrypto.java | 13 +---- .../android/exoplayer2/drm/ExoMediaDrm.java | 6 +-- .../exoplayer2/drm/FrameworkMediaCrypto.java | 53 +++++++------------ .../exoplayer2/drm/FrameworkMediaDrm.java | 3 +- .../mediacodec/MediaCodecRenderer.java | 50 +++++++++++------ 5 files changed, 58 insertions(+), 67 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaCrypto.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaCrypto.java index d5a4f6add5..feba7eaaf4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaCrypto.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaCrypto.java @@ -15,14 +15,5 @@ */ package com.google.android.exoplayer2.drm; -/** - * An opaque {@link android.media.MediaCrypto} equivalent. - */ -public interface ExoMediaCrypto { - - /** - * @see android.media.MediaCrypto#requiresSecureDecoderComponent(String) - */ - boolean requiresSecureDecoderComponent(String mimeType); - -} +/** An opaque {@link android.media.MediaCrypto} equivalent. */ +public interface ExoMediaCrypto {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java index 24c3ddbbd0..aca56139de 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/ExoMediaDrm.java @@ -265,11 +265,9 @@ public interface ExoMediaDrm { /** * @see android.media.MediaCrypto#MediaCrypto(UUID, byte[]) - * - * @param initData Opaque initialization data specific to the crypto scheme. + * @param sessionId The DRM session ID. * @return An object extends {@link ExoMediaCrypto}, using opaque crypto scheme specific data. * @throws MediaCryptoException If the instance can't be created. */ - T createMediaCrypto(byte[] initData) throws MediaCryptoException; - + T createMediaCrypto(byte[] sessionId) throws MediaCryptoException; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java index 4e58ed6a31..156138ab9b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java @@ -17,48 +17,35 @@ package com.google.android.exoplayer2.drm; import android.annotation.TargetApi; import android.media.MediaCrypto; -import com.google.android.exoplayer2.util.Assertions; +import java.util.UUID; /** - * An {@link ExoMediaCrypto} implementation that wraps the framework {@link MediaCrypto}. + * An {@link ExoMediaCrypto} implementation that contains the necessary information to build or + * update a framework {@link MediaCrypto}. */ @TargetApi(16) public final class FrameworkMediaCrypto implements ExoMediaCrypto { - private final MediaCrypto mediaCrypto; - private final boolean forceAllowInsecureDecoderComponents; + /** The DRM scheme UUID. */ + public final UUID uuid; + /** The DRM session id. */ + public final byte[] sessionId; + /** + * Whether to allow use of insecure decoder components even if the underlying platform says + * otherwise. + */ + public final boolean forceAllowInsecureDecoderComponents; /** - * @param mediaCrypto The {@link MediaCrypto} to wrap. + * @param uuid The DRM scheme UUID. + * @param sessionId The DRM session id. + * @param forceAllowInsecureDecoderComponents Whether to allow use of insecure decoder components + * even if the underlying platform says otherwise. */ - public FrameworkMediaCrypto(MediaCrypto mediaCrypto) { - this(mediaCrypto, false); - } - - /** - * @param mediaCrypto The {@link MediaCrypto} to wrap. - * @param forceAllowInsecureDecoderComponents Whether to force - * {@link #requiresSecureDecoderComponent(String)} to return {@code false}, rather than - * {@link MediaCrypto#requiresSecureDecoderComponent(String)} of the wrapped - * {@link MediaCrypto}. - */ - public FrameworkMediaCrypto(MediaCrypto mediaCrypto, - boolean forceAllowInsecureDecoderComponents) { - this.mediaCrypto = Assertions.checkNotNull(mediaCrypto); + public FrameworkMediaCrypto( + UUID uuid, byte[] sessionId, boolean forceAllowInsecureDecoderComponents) { + this.uuid = uuid; + this.sessionId = sessionId; this.forceAllowInsecureDecoderComponents = forceAllowInsecureDecoderComponents; } - - /** - * Returns the wrapped {@link MediaCrypto}. - */ - public MediaCrypto getWrappedMediaCrypto() { - return mediaCrypto; - } - - @Override - public boolean requiresSecureDecoderComponent(String mimeType) { - return !forceAllowInsecureDecoderComponents - && mediaCrypto.requiresSecureDecoderComponent(mimeType); - } - } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java index fda85a759c..b139288f98 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaDrm.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.drm; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.media.DeniedByServerException; -import android.media.MediaCrypto; import android.media.MediaCryptoException; import android.media.MediaDrm; import android.media.MediaDrmException; @@ -210,7 +209,7 @@ public final class FrameworkMediaDrm implements ExoMediaDrm schemeDatas) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index a538bca4b2..4736811841 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -20,6 +20,7 @@ import android.media.MediaCodec; import android.media.MediaCodec.CodecException; import android.media.MediaCodec.CryptoException; import android.media.MediaCrypto; +import android.media.MediaCryptoException; import android.media.MediaFormat; import android.os.Bundle; import android.os.Looper; @@ -294,6 +295,8 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private Format outputFormat; @Nullable private DrmSession codecDrmSession; @Nullable private DrmSession sourceDrmSession; + @Nullable private MediaCrypto mediaCrypto; + private boolean drmSessionRequiresSecureDecoder; private long renderTimeLimitMs; private float rendererOperatingRate; @Nullable private MediaCodec codec; @@ -460,22 +463,28 @@ public abstract class MediaCodecRenderer extends BaseRenderer { setCodecDrmSession(sourceDrmSession); String mimeType = inputFormat.sampleMimeType; - MediaCrypto wrappedMediaCrypto = null; - boolean drmSessionRequiresSecureDecoder = false; if (codecDrmSession != null) { - FrameworkMediaCrypto mediaCrypto = codecDrmSession.getMediaCrypto(); if (mediaCrypto == null) { - DrmSessionException drmError = codecDrmSession.getError(); - if (drmError != null) { - // Continue for now. We may be able to avoid failure if the session recovers, or if a new - // input format causes the session to be replaced before it's used. + FrameworkMediaCrypto sessionMediaCrypto = codecDrmSession.getMediaCrypto(); + if (sessionMediaCrypto == null) { + DrmSessionException drmError = codecDrmSession.getError(); + if (drmError != null) { + // Continue for now. We may be able to avoid failure if the session recovers, or if a + // new input format causes the session to be replaced before it's used. + } else { + // The drm session isn't open yet. + return; + } } else { - // The drm session isn't open yet. - return; + try { + mediaCrypto = new MediaCrypto(sessionMediaCrypto.uuid, sessionMediaCrypto.sessionId); + } catch (MediaCryptoException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } + drmSessionRequiresSecureDecoder = + !sessionMediaCrypto.forceAllowInsecureDecoderComponents + && mediaCrypto.requiresSecureDecoderComponent(mimeType); } - } else { - wrappedMediaCrypto = mediaCrypto.getWrappedMediaCrypto(); - drmSessionRequiresSecureDecoder = mediaCrypto.requiresSecureDecoderComponent(mimeType); } if (deviceNeedsDrmKeysToConfigureCodecWorkaround()) { @DrmSession.State int drmSessionState = codecDrmSession.getState(); @@ -489,7 +498,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } try { - maybeInitCodecWithFallback(wrappedMediaCrypto, drmSessionRequiresSecureDecoder); + maybeInitCodecWithFallback(mediaCrypto, drmSessionRequiresSecureDecoder); } catch (DecoderInitializationException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } @@ -553,7 +562,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Override protected void onDisabled() { inputFormat = null; - if (codecDrmSession != null || sourceDrmSession != null) { + if (sourceDrmSession != null || codecDrmSession != null) { // TODO: Do something better with this case. onReset(); } else { @@ -591,7 +600,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } } finally { codec = null; - setCodecDrmSession(null); + try { + if (mediaCrypto != null) { + mediaCrypto.release(); + } + } finally { + mediaCrypto = null; + setCodecDrmSession(null); + } } } @@ -923,7 +939,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } private void releaseDrmSessionIfUnused(@Nullable DrmSession session) { - if (session != null && session != codecDrmSession && session != sourceDrmSession) { + if (session != null && session != sourceDrmSession && session != codecDrmSession) { drmSessionManager.releaseSession(session); } } @@ -1128,7 +1144,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } DrmSession session = drmSessionManager.acquireSession(Looper.myLooper(), newFormat.drmInitData); - if (session == codecDrmSession || session == sourceDrmSession) { + if (session == sourceDrmSession || session == codecDrmSession) { // We already had this session. The manager must be reference counting, so release it once // to get the count attributed to this renderer back down to 1. drmSessionManager.releaseSession(session); From f44fc542bb6cd22fb430ec3ca269d36a1ffc819e Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 14 Jan 2019 10:05:17 +0000 Subject: [PATCH 007/110] =?UTF-8?q?=EF=BF=BCBlacklist=20OMX.SEC.mp3.dec=20?= =?UTF-8?q?for=20more=20devices?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue: #4519 PiperOrigin-RevId: 229145790 --- .../google/android/exoplayer2/mediacodec/MediaCodecUtil.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 4d971d461e..9ae50179c3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -326,7 +326,9 @@ public final class MediaCodecUtil { || Util.MODEL.startsWith("SM-G350") || Util.MODEL.startsWith("SM-G386") || Util.MODEL.startsWith("SM-T231") - || Util.MODEL.startsWith("SM-T530"))) { + || Util.MODEL.startsWith("SM-T530") + || Util.MODEL.startsWith("SCH-I535") + || Util.MODEL.startsWith("SPH-L710"))) { return false; } if ("OMX.brcm.audio.mp3.decoder".equals(name) From 86637facdd16f95a4ba2b444234dea722696daa5 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 14 Jan 2019 14:02:46 +0000 Subject: [PATCH 008/110] Don't forget isSeekable in ExtractorMediaSource. We currently forget whether a source is seekable at re-preparation. This was implemented intentionally this way under the assumption that we really can't seek until we have loaded the seek map again. However, seek operations are only allowed after a media period is prepared. So there is no harm in remembering whether a source is seekable. This problem currently prevents reusing ClippingMediaSources with ExtractorMediaSource and a non-zero start clip position. Issue: #5351 PiperOrigin-RevId: 229169441 --- RELEASENOTES.md | 3 +++ .../google/android/exoplayer2/source/ExtractorMediaSource.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1634b8de74..307b0534a1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -41,6 +41,9 @@ * Add `Handler` parameter to `ConcatenatingMediaSource` methods which take a callback `Runnable`. * Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`. +* Fix issue with reusing a `ClippingMediaSource` with an inner + `ExtractorMediaSource` and a non-zero start position + ([#5351](https://github.com/google/ExoPlayer/issues/5351)). ### 2.9.3 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 86f00ff099..91273626c9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -366,7 +366,7 @@ public final class ExtractorMediaSource extends BaseMediaSource @Override public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { transferListener = mediaTransferListener; - notifySourceInfoRefreshed(timelineDurationUs, /* isSeekable= */ false); + notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable); } @Override From 1b62277a0b2bf72d68abd3afb6510647a7ff6643 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 14 Jan 2019 15:09:13 +0000 Subject: [PATCH 009/110] Disable cache fragmentation except for progressive DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION is added to indicate to the cache when fragmentation is allowed. This flag is set for progressive requests only. To avoid breaking changes, CacheDataSink defaults to ignoring the flag (and enabling fragmentation) for now. Respecting the flag can be enabled manually. DownloaderConstructorHelper enables respecting of the flag. Issue: #4253 PiperOrigin-RevId: 229176835 --- RELEASENOTES.md | 2 + .../offline/DownloaderConstructorHelper.java | 12 +-- .../offline/ProgressiveDownloader.java | 6 +- .../source/ExtractorMediaPeriod.java | 4 +- .../android/exoplayer2/upstream/DataSpec.java | 20 +++-- .../upstream/cache/CacheDataSink.java | 81 +++++++++++-------- .../upstream/cache/CacheDataSinkFactory.java | 47 ++++++++--- .../upstream/cache/CacheDataSource.java | 2 +- .../cache/CacheDataSourceFactory.java | 2 +- .../upstream/cache/CacheDataSourceTest.java | 15 ++-- .../upstream/cache/CacheUtilTest.java | 6 +- 11 files changed, 129 insertions(+), 68 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 307b0534a1..fb251646fb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -44,6 +44,8 @@ * Fix issue with reusing a `ClippingMediaSource` with an inner `ExtractorMediaSource` and a non-zero start position ([#5351](https://github.com/google/ExoPlayer/issues/5351)). +* Downloading/Caching: Improve cache performance + ([#4253](https://github.com/google/ExoPlayer/issues/4253)). ### 2.9.3 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java index 100e1a03fe..59a11934b1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java @@ -109,16 +109,18 @@ public final class DownloaderConstructorHelper { cacheReadDataSourceFactory != null ? cacheReadDataSourceFactory : new FileDataSourceFactory(); - DataSink.Factory writeDataSinkFactory = - cacheWriteDataSinkFactory != null - ? cacheWriteDataSinkFactory - : new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_MAX_CACHE_FILE_SIZE); + if (cacheWriteDataSinkFactory == null) { + CacheDataSinkFactory factory = + new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE); + factory.experimental_setRespectCacheFragmentationFlag(true); + cacheWriteDataSinkFactory = factory; + } onlineCacheDataSourceFactory = new CacheDataSourceFactory( cache, upstreamFactory, readDataSourceFactory, - writeDataSinkFactory, + cacheWriteDataSinkFactory, CacheDataSource.FLAG_BLOCK_ON_CACHE, /* eventListener= */ null, cacheKeyFactory); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java index 41f0944b75..25b4e07bcd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloader.java @@ -53,7 +53,11 @@ public final class ProgressiveDownloader implements Downloader { Uri uri, @Nullable String customCacheKey, DownloaderConstructorHelper constructorHelper) { this.dataSpec = new DataSpec( - uri, /* absoluteStreamPosition= */ 0, C.LENGTH_UNSET, customCacheKey, /* flags= */ 0); + uri, + /* absoluteStreamPosition= */ 0, + C.LENGTH_UNSET, + customCacheKey, + /* flags= */ DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION); this.cache = constructorHelper.getCache(); this.dataSource = constructorHelper.createCacheDataSource(); this.cacheKeyFactory = constructorHelper.getCacheKeyFactory(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 21cb4aa226..40fac19178 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -988,7 +988,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; position, C.LENGTH_UNSET, customCacheKey, - DataSpec.FLAG_ALLOW_ICY_METADATA | DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN); + DataSpec.FLAG_ALLOW_ICY_METADATA + | DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN + | DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION); } private void setLoadPosition(long position, long timeUs) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index 75af3e96df..2996de4527 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -32,14 +32,19 @@ public final class DataSpec { /** * The flags that apply to any request for data. Possible flag values are {@link - * #FLAG_ALLOW_GZIP}, {@link #FLAG_ALLOW_ICY_METADATA} and {@link - * #FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN}. + * #FLAG_ALLOW_GZIP}, {@link #FLAG_ALLOW_ICY_METADATA}, {@link #FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN} + * and {@link #FLAG_ALLOW_CACHE_FRAGMENTATION}. */ @Documented @Retention(RetentionPolicy.SOURCE) @IntDef( flag = true, - value = {FLAG_ALLOW_GZIP, FLAG_ALLOW_ICY_METADATA, FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN}) + value = { + FLAG_ALLOW_GZIP, + FLAG_ALLOW_ICY_METADATA, + FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN, + FLAG_ALLOW_CACHE_FRAGMENTATION + }) public @interface Flags {} /** * Allows an underlying network stack to request that the server use gzip compression. @@ -53,12 +58,17 @@ public final class DataSpec { * DataSource#read(byte[], int, int)} will be the decompressed data. */ public static final int FLAG_ALLOW_GZIP = 1; - /** Allows an underlying network stack to request that the stream contain ICY metadata. */ public static final int FLAG_ALLOW_ICY_METADATA = 1 << 1; // 2 - /** Prevents caching if the length cannot be resolved when the {@link DataSource} is opened. */ public static final int FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN = 1 << 2; // 4 + /** + * Allows fragmentation of this request into multiple cache files, meaning a cache eviction policy + * will be able to evict individual fragments of the data. Depending on the cache implementation, + * setting this flag may also enable more concurrent access to the data (e.g. reading one fragment + * whilst writing another). + */ + public static final int FLAG_ALLOW_CACHE_FRAGMENTATION = 1 << 4; // 8 /** * The set of HTTP methods that are supported by ExoPlayer {@link HttpDataSource}s. One of {@link diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index 1da2c3f636..d527805120 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -37,20 +37,22 @@ import java.io.OutputStream; */ public final class CacheDataSink implements DataSink { - /** Default {@code maxCacheFileSize} recommended for caching use cases. */ - public static final long DEFAULT_MAX_CACHE_FILE_SIZE = 5 * 1024 * 1024; + /** Default {@code fragmentSize} recommended for caching use cases. */ + public static final long DEFAULT_FRAGMENT_SIZE = 5 * 1024 * 1024; /** Default buffer size in bytes. */ public static final int DEFAULT_BUFFER_SIZE = 20 * 1024; - private static final long MIN_RECOMMENDED_MAX_CACHE_FILE_SIZE = 2 * 1024 * 1024; + private static final long MIN_RECOMMENDED_FRAGMENT_SIZE = 2 * 1024 * 1024; private static final String TAG = "CacheDataSink"; private final Cache cache; - private final long maxCacheFileSize; + private final long fragmentSize; private final int bufferSize; private boolean syncFileDescriptor; + private boolean respectCacheFragmentationFlag; private DataSpec dataSpec; + private long dataSpecFragmentSize; private File file; private OutputStream outputStream; private FileOutputStream underlyingFileOutputStream; @@ -73,42 +75,39 @@ public final class CacheDataSink implements DataSink { * Constructs an instance using {@link #DEFAULT_BUFFER_SIZE}. * * @param cache The cache into which data should be written. - * @param maxCacheFileSize The maximum size of a cache file, in bytes. If a request results in - * data being written whose size exceeds this value, then the data will be fragmented into - * multiple cache files. If set to {@link C#LENGTH_UNSET} then no fragmentation will occur. - * Using a small value allows for finer-grained cache eviction policies, at the cost of - * increased overhead both on the cache implementation and the file system. Values under - * {@code (2 * 1024 * 1024)} are not recommended. + * @param fragmentSize For requests that should be fragmented into multiple cache files, this is + * the maximum size of a cache file in bytes. If set to {@link C#LENGTH_UNSET} then no + * fragmentation will occur. Using a small value allows for finer-grained cache eviction + * policies, at the cost of increased overhead both on the cache implementation and the file + * system. Values under {@code (2 * 1024 * 1024)} are not recommended. */ - public CacheDataSink(Cache cache, long maxCacheFileSize) { - this(cache, maxCacheFileSize, DEFAULT_BUFFER_SIZE); + public CacheDataSink(Cache cache, long fragmentSize) { + this(cache, fragmentSize, DEFAULT_BUFFER_SIZE); } /** * @param cache The cache into which data should be written. - * @param maxCacheFileSize The maximum size of a cache file, in bytes. If a request results in - * data being written whose size exceeds this value, then the data will be fragmented into - * multiple cache files. If set to {@link C#LENGTH_UNSET} then no fragmentation will occur. - * Using a small value allows for finer-grained cache eviction policies, at the cost of - * increased overhead both on the cache implementation and the file system. Values under - * {@code (2 * 1024 * 1024)} are not recommended. + * @param fragmentSize For requests that should be fragmented into multiple cache files, this is + * the maximum size of a cache file in bytes. If set to {@link C#LENGTH_UNSET} then no + * fragmentation will occur. Using a small value allows for finer-grained cache eviction + * policies, at the cost of increased overhead both on the cache implementation and the file + * system. Values under {@code (2 * 1024 * 1024)} are not recommended. * @param bufferSize The buffer size in bytes for writing to a cache file. A zero or negative * value disables buffering. */ - public CacheDataSink(Cache cache, long maxCacheFileSize, int bufferSize) { + public CacheDataSink(Cache cache, long fragmentSize, int bufferSize) { Assertions.checkState( - maxCacheFileSize > 0 || maxCacheFileSize == C.LENGTH_UNSET, - "maxCacheFileSize must be positive or C.LENGTH_UNSET."); - if (maxCacheFileSize != C.LENGTH_UNSET - && maxCacheFileSize < MIN_RECOMMENDED_MAX_CACHE_FILE_SIZE) { + fragmentSize > 0 || fragmentSize == C.LENGTH_UNSET, + "fragmentSize must be positive or C.LENGTH_UNSET."); + if (fragmentSize != C.LENGTH_UNSET && fragmentSize < MIN_RECOMMENDED_FRAGMENT_SIZE) { Log.w( TAG, - "maxCacheFileSize is below the minimum recommended value of " - + MIN_RECOMMENDED_MAX_CACHE_FILE_SIZE + "fragmentSize is below the minimum recommended value of " + + MIN_RECOMMENDED_FRAGMENT_SIZE + ". This may cause poor cache performance."); } this.cache = Assertions.checkNotNull(cache); - this.maxCacheFileSize = maxCacheFileSize == C.LENGTH_UNSET ? Long.MAX_VALUE : maxCacheFileSize; + this.fragmentSize = fragmentSize == C.LENGTH_UNSET ? Long.MAX_VALUE : fragmentSize; this.bufferSize = bufferSize; syncFileDescriptor = true; } @@ -116,8 +115,7 @@ public final class CacheDataSink implements DataSink { /** * Sets whether file descriptors are synced when closing output streams. * - *

This method is experimental, and will be renamed or removed in a future release. It should - * only be called before the renderer is used. + *

This method is experimental, and will be renamed or removed in a future release. * * @param syncFileDescriptor Whether file descriptors are synced when closing output streams. */ @@ -125,6 +123,20 @@ public final class CacheDataSink implements DataSink { this.syncFileDescriptor = syncFileDescriptor; } + /** + * Sets whether this instance respects the {@link DataSpec#FLAG_ALLOW_CACHE_FRAGMENTATION} flag. + * If set to {@code false} requests will always be fragmented. If set to {@code true} requests + * will be fragmented only if the flag is set. + * + *

This method is experimental, and will be renamed or removed in a future release. + * + * @param respectCacheFragmentationFlag Whether to respect the {@link + * DataSpec#FLAG_ALLOW_CACHE_FRAGMENTATION} flag. + */ + public void experimental_setRespectCacheFragmentationFlag(boolean respectCacheFragmentationFlag) { + this.respectCacheFragmentationFlag = respectCacheFragmentationFlag; + } + @Override public void open(DataSpec dataSpec) throws CacheDataSinkException { if (dataSpec.length == C.LENGTH_UNSET @@ -133,6 +145,11 @@ public final class CacheDataSink implements DataSink { return; } this.dataSpec = dataSpec; + this.dataSpecFragmentSize = + !respectCacheFragmentationFlag + || dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION) + ? fragmentSize + : Long.MAX_VALUE; dataSpecBytesWritten = 0; try { openNextOutputStream(); @@ -149,12 +166,12 @@ public final class CacheDataSink implements DataSink { try { int bytesWritten = 0; while (bytesWritten < length) { - if (outputStreamBytesWritten == maxCacheFileSize) { + if (outputStreamBytesWritten == dataSpecFragmentSize) { closeCurrentOutputStream(); openNextOutputStream(); } - int bytesToWrite = (int) Math.min(length - bytesWritten, - maxCacheFileSize - outputStreamBytesWritten); + int bytesToWrite = + (int) Math.min(length - bytesWritten, dataSpecFragmentSize - outputStreamBytesWritten); outputStream.write(buffer, offset + bytesWritten, bytesToWrite); bytesWritten += bytesToWrite; outputStreamBytesWritten += bytesToWrite; @@ -181,7 +198,7 @@ public final class CacheDataSink implements DataSink { long length = dataSpec.length == C.LENGTH_UNSET ? C.LENGTH_UNSET - : Math.min(dataSpec.length - dataSpecBytesWritten, maxCacheFileSize); + : Math.min(dataSpec.length - dataSpecBytesWritten, dataSpecFragmentSize); file = cache.startFile( dataSpec.key, dataSpec.absoluteStreamPosition + dataSpecBytesWritten, length); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java index 6dcb14f5fb..9540597c2e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java @@ -23,27 +23,50 @@ import com.google.android.exoplayer2.upstream.DataSink; public final class CacheDataSinkFactory implements DataSink.Factory { private final Cache cache; - private final long maxCacheFileSize; + private final long fragmentSize; private final int bufferSize; - /** - * @see CacheDataSink#CacheDataSink(Cache, long) - */ - public CacheDataSinkFactory(Cache cache, long maxCacheFileSize) { - this(cache, maxCacheFileSize, CacheDataSink.DEFAULT_BUFFER_SIZE); + private boolean syncFileDescriptor; + private boolean respectCacheFragmentationFlag; + + /** @see CacheDataSink#CacheDataSink(Cache, long) */ + public CacheDataSinkFactory(Cache cache, long fragmentSize) { + this(cache, fragmentSize, CacheDataSink.DEFAULT_BUFFER_SIZE); + } + + /** @see CacheDataSink#CacheDataSink(Cache, long, int) */ + public CacheDataSinkFactory(Cache cache, long fragmentSize, int bufferSize) { + this.cache = cache; + this.fragmentSize = fragmentSize; + this.bufferSize = bufferSize; } /** - * @see CacheDataSink#CacheDataSink(Cache, long, int) + * See {@link CacheDataSink#experimental_setSyncFileDescriptor(boolean)}. + * + *

This method is experimental, and will be renamed or removed in a future release. */ - public CacheDataSinkFactory(Cache cache, long maxCacheFileSize, int bufferSize) { - this.cache = cache; - this.maxCacheFileSize = maxCacheFileSize; - this.bufferSize = bufferSize; + public CacheDataSinkFactory experimental_setSyncFileDescriptor(boolean syncFileDescriptor) { + this.syncFileDescriptor = syncFileDescriptor; + return this; + } + + /** + * See {@link CacheDataSink#experimental_setRespectCacheFragmentationFlag(boolean)}. + * + *

This method is experimental, and will be renamed or removed in a future release. + */ + public CacheDataSinkFactory experimental_setRespectCacheFragmentationFlag( + boolean respectCacheFragmentationFlag) { + this.respectCacheFragmentationFlag = respectCacheFragmentationFlag; + return this; } @Override public DataSink createDataSink() { - return new CacheDataSink(cache, maxCacheFileSize, bufferSize); + CacheDataSink dataSink = new CacheDataSink(cache, fragmentSize, bufferSize); + dataSink.experimental_setSyncFileDescriptor(syncFileDescriptor); + dataSink.experimental_setRespectCacheFragmentationFlag(respectCacheFragmentationFlag); + return dataSink; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java index 579f5d05e9..909bd40023 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSource.java @@ -172,7 +172,7 @@ public final class CacheDataSource implements DataSource { cache, upstream, new FileDataSource(), - new CacheDataSink(cache, CacheDataSink.DEFAULT_MAX_CACHE_FILE_SIZE), + new CacheDataSink(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE), flags, /* eventListener= */ null); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java index e25c3d7a4a..9675aa1762 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceFactory.java @@ -50,7 +50,7 @@ public final class CacheDataSourceFactory implements DataSource.Factory { cache, upstreamFactory, new FileDataSourceFactory(), - new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_MAX_CACHE_FILE_SIZE), + new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE), flags, /* eventListener= */ null); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index 666fa87e9e..f8c417499f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -47,7 +47,7 @@ import org.robolectric.RuntimeEnvironment; public final class CacheDataSourceTest { private static final byte[] TEST_DATA = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; - private static final int MAX_CACHE_FILE_SIZE = 3; + private static final int CACHE_FRAGMENT_SIZE = 3; private static final String DATASPEC_KEY = "dataSpecKey"; private Uri testDataUri; @@ -81,13 +81,13 @@ public final class CacheDataSourceTest { } @Test - public void testMaxCacheFileSize() throws Exception { + public void testFragmentSize() throws Exception { CacheDataSource cacheDataSource = createCacheDataSource(false, false); assertReadDataContentLength(cacheDataSource, boundedDataSpec, false, false); for (String key : cache.getKeys()) { for (CacheSpan cacheSpan : cache.getCachedSpans(key)) { - assertThat(cacheSpan.length <= MAX_CACHE_FILE_SIZE).isTrue(); - assertThat(cacheSpan.file.length() <= MAX_CACHE_FILE_SIZE).isTrue(); + assertThat(cacheSpan.length <= CACHE_FRAGMENT_SIZE).isTrue(); + assertThat(cacheSpan.file.length() <= CACHE_FRAGMENT_SIZE).isTrue(); } } } @@ -548,14 +548,14 @@ public final class CacheDataSourceTest { setReadException, unknownLength, CacheDataSource.FLAG_BLOCK_ON_CACHE, - new CacheDataSink(cache, MAX_CACHE_FILE_SIZE), + new CacheDataSink(cache, CACHE_FRAGMENT_SIZE), cacheKeyFactory); } private CacheDataSource createCacheDataSource( boolean setReadException, boolean unknownLength, @CacheDataSource.Flags int flags) { return createCacheDataSource( - setReadException, unknownLength, flags, new CacheDataSink(cache, MAX_CACHE_FILE_SIZE)); + setReadException, unknownLength, flags, new CacheDataSink(cache, CACHE_FRAGMENT_SIZE)); } private CacheDataSource createCacheDataSource( @@ -602,6 +602,7 @@ public final class CacheDataSourceTest { } private DataSpec buildDataSpec(long position, long length, @Nullable String key) { - return new DataSpec(testDataUri, position, length, key); + return new DataSpec( + testDataUri, position, length, key, DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java index d1bf734a98..228567a4bc 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheUtilTest.java @@ -336,17 +336,17 @@ public final class CacheUtilTest { FakeDataSource dataSource = new FakeDataSource(fakeDataSet); Uri uri = Uri.parse("test_data"); - DataSpec dataSpec = new DataSpec(uri); + DataSpec dataSpec = new DataSpec(uri, DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION); CacheUtil.cache( dataSpec, cache, /* cacheKeyFactory= */ null, - // Set maxCacheFileSize to 10 to make sure there are multiple spans. + // Set fragmentSize to 10 to make sure there are multiple spans. new CacheDataSource( cache, dataSource, new FileDataSource(), - new CacheDataSink(cache, /* maxCacheFileSize= */ 10), + new CacheDataSink(cache, /* fragmentSize= */ 10), /* flags= */ 0, /* eventListener= */ null), new byte[CacheUtil.DEFAULT_BUFFER_SIZE_BYTES], From 0bfbcea632c14c56e6c14d8a3d1760b6e251a26b Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 14 Jan 2019 22:17:50 +0000 Subject: [PATCH 010/110] Reset requiresSecureDecoder boolean on codec release PiperOrigin-RevId: 229253065 --- .../mediacodec/MediaCodecRenderer.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 4736811841..77d3b31ab7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -296,7 +296,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Nullable private DrmSession codecDrmSession; @Nullable private DrmSession sourceDrmSession; @Nullable private MediaCrypto mediaCrypto; - private boolean drmSessionRequiresSecureDecoder; + private boolean mediaCryptoRequiresSecureDecoder; private long renderTimeLimitMs; private float rendererOperatingRate; @Nullable private MediaCodec codec; @@ -481,7 +481,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } catch (MediaCryptoException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } - drmSessionRequiresSecureDecoder = + mediaCryptoRequiresSecureDecoder = !sessionMediaCrypto.forceAllowInsecureDecoderComponents && mediaCrypto.requiresSecureDecoderComponent(mimeType); } @@ -498,7 +498,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } try { - maybeInitCodecWithFallback(mediaCrypto, drmSessionRequiresSecureDecoder); + maybeInitCodecWithFallback(mediaCrypto, mediaCryptoRequiresSecureDecoder); } catch (DecoderInitializationException e) { throw ExoPlaybackException.createForRenderer(e, getIndex()); } @@ -606,6 +606,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } } finally { mediaCrypto = null; + mediaCryptoRequiresSecureDecoder = false; setCodecDrmSession(null); } } @@ -727,18 +728,18 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } private void maybeInitCodecWithFallback( - MediaCrypto crypto, boolean drmSessionRequiresSecureDecoder) + MediaCrypto crypto, boolean mediaCryptoRequiresSecureDecoder) throws DecoderInitializationException { if (availableCodecInfos == null) { try { availableCodecInfos = - new ArrayDeque<>(getAvailableCodecInfos(drmSessionRequiresSecureDecoder)); + new ArrayDeque<>(getAvailableCodecInfos(mediaCryptoRequiresSecureDecoder)); preferredDecoderInitializationException = null; } catch (DecoderQueryException e) { throw new DecoderInitializationException( inputFormat, e, - drmSessionRequiresSecureDecoder, + mediaCryptoRequiresSecureDecoder, DecoderInitializationException.DECODER_QUERY_ERROR); } } @@ -747,7 +748,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { throw new DecoderInitializationException( inputFormat, /* cause= */ null, - drmSessionRequiresSecureDecoder, + mediaCryptoRequiresSecureDecoder, DecoderInitializationException.NO_SUITABLE_DECODER_ERROR); } @@ -766,7 +767,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { availableCodecInfos.removeFirst(); DecoderInitializationException exception = new DecoderInitializationException( - inputFormat, e, drmSessionRequiresSecureDecoder, codecInfo.name); + inputFormat, e, mediaCryptoRequiresSecureDecoder, codecInfo.name); if (preferredDecoderInitializationException == null) { preferredDecoderInitializationException = exception; } else { @@ -782,11 +783,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer { availableCodecInfos = null; } - private List getAvailableCodecInfos(boolean drmSessionRequiresSecureDecoder) + private List getAvailableCodecInfos(boolean mediaCryptoRequiresSecureDecoder) throws DecoderQueryException { List codecInfos = - getDecoderInfos(mediaCodecSelector, inputFormat, drmSessionRequiresSecureDecoder); - if (codecInfos.isEmpty() && drmSessionRequiresSecureDecoder) { + getDecoderInfos(mediaCodecSelector, inputFormat, mediaCryptoRequiresSecureDecoder); + if (codecInfos.isEmpty() && mediaCryptoRequiresSecureDecoder) { // The drm session indicates that a secure decoder is required, but the device does not // have one. Assuming that supportsFormat indicated support for the media being played, we // know that it does not require a secure output path. Most CDM implementations allow From 9ab08bbe5df406e3812acbf9c1980fc52f0262e0 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 14 Jan 2019 23:02:03 +0000 Subject: [PATCH 011/110] Fix DRM protected SmoothStreaming with subtitles Issue: #5378 PiperOrigin-RevId: 229261658 --- RELEASENOTES.md | 2 + .../smoothstreaming/DefaultSsChunkSource.java | 11 +++-- .../source/smoothstreaming/SsChunkSource.java | 3 -- .../source/smoothstreaming/SsMediaPeriod.java | 41 +----------------- .../smoothstreaming/manifest/SsManifest.java | 5 ++- .../manifest/SsManifestParser.java | 43 ++++++++++++++++++- .../src/test/assets/sample_ismc_1 | 2 +- .../src/test/assets/sample_ismc_2 | 2 +- .../source/smoothstreaming/SsTestUtils.java | 10 +---- 9 files changed, 57 insertions(+), 62 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fb251646fb..b93fa95c40 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -27,6 +27,8 @@ * Rename TaskState to DownloadState. * Add new states to DownloadState. * Replace DownloadState.action with DownloadAction fields. +* SmoothStreaming: Fix support for subtitles in DRM protected streams + ([#5378](https://github.com/google/ExoPlayer/issues/5378)). * Add support for SHOUTcast ICY metadata ([#3735](https://github.com/google/ExoPlayer/issues/3735)). * IMA extension: diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java index d50375d4c9..45521726c0 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultSsChunkSource.java @@ -61,14 +61,13 @@ public class DefaultSsChunkSource implements SsChunkSource { SsManifest manifest, int elementIndex, TrackSelection trackSelection, - TrackEncryptionBox[] trackEncryptionBoxes, @Nullable TransferListener transferListener) { DataSource dataSource = dataSourceFactory.createDataSource(); if (transferListener != null) { dataSource.addTransferListener(transferListener); } - return new DefaultSsChunkSource(manifestLoaderErrorThrower, manifest, elementIndex, - trackSelection, dataSource, trackEncryptionBoxes); + return new DefaultSsChunkSource( + manifestLoaderErrorThrower, manifest, elementIndex, trackSelection, dataSource); } } @@ -90,15 +89,13 @@ public class DefaultSsChunkSource implements SsChunkSource { * @param streamElementIndex The index of the stream element in the manifest. * @param trackSelection The track selection. * @param dataSource A {@link DataSource} suitable for loading the media data. - * @param trackEncryptionBoxes Track encryption boxes for the stream. */ public DefaultSsChunkSource( LoaderErrorThrower manifestLoaderErrorThrower, SsManifest manifest, int streamElementIndex, TrackSelection trackSelection, - DataSource dataSource, - TrackEncryptionBox[] trackEncryptionBoxes) { + DataSource dataSource) { this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.manifest = manifest; this.streamElementIndex = streamElementIndex; @@ -110,6 +107,8 @@ public class DefaultSsChunkSource implements SsChunkSource { for (int i = 0; i < extractorWrappers.length; i++) { int manifestTrackIndex = trackSelection.getIndexInTrackGroup(i); Format format = streamElement.formats[manifestTrackIndex]; + TrackEncryptionBox[] trackEncryptionBoxes = + format.drmInitData != null ? manifest.protectionElement.trackEncryptionBoxes : null; int nalUnitLengthFieldLength = streamElement.type == C.TRACK_TYPE_VIDEO ? 4 : 0; Track track = new Track(manifestTrackIndex, streamElement.type, streamElement.timescale, C.TIME_UNSET, manifest.durationUs, format, Track.TRANSFORMATION_NONE, diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java index f333a6f92c..4940f1592f 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsChunkSource.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.source.smoothstreaming; import android.support.annotation.Nullable; -import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; import com.google.android.exoplayer2.source.chunk.ChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -38,7 +37,6 @@ public interface SsChunkSource extends ChunkSource { * @param manifest The initial manifest. * @param streamElementIndex The index of the corresponding stream element in the manifest. * @param trackSelection The track selection. - * @param trackEncryptionBoxes Track encryption boxes for the stream. * @param transferListener The transfer listener which should be informed of any data transfers. * May be null if no listener is available. * @return The created {@link SsChunkSource}. @@ -48,7 +46,6 @@ public interface SsChunkSource extends ChunkSource { SsManifest manifest, int streamElementIndex, TrackSelection trackSelection, - TrackEncryptionBox[] trackEncryptionBoxes, @Nullable TransferListener transferListener); } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index fc22c45c5a..8798ea09b2 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -16,10 +16,8 @@ package com.google.android.exoplayer2.source.smoothstreaming; import android.support.annotation.Nullable; -import android.util.Base64; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SeekParameters; -import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; @@ -30,7 +28,6 @@ import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.chunk.ChunkSampleStream; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.ProtectionElement; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; @@ -44,8 +41,6 @@ import java.util.List; /* package */ final class SsMediaPeriod implements MediaPeriod, SequenceableLoader.Callback> { - private static final int INITIALIZATION_VECTOR_SIZE = 8; - private final SsChunkSource.Factory chunkSourceFactory; private final @Nullable TransferListener transferListener; private final LoaderErrorThrower manifestLoaderErrorThrower; @@ -53,7 +48,6 @@ import java.util.List; private final EventDispatcher eventDispatcher; private final Allocator allocator; private final TrackGroupArray trackGroups; - private final TrackEncryptionBox[] trackEncryptionBoxes; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private @Nullable Callback callback; @@ -71,6 +65,7 @@ import java.util.List; EventDispatcher eventDispatcher, LoaderErrorThrower manifestLoaderErrorThrower, Allocator allocator) { + this.manifest = manifest; this.chunkSourceFactory = chunkSourceFactory; this.transferListener = transferListener; this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; @@ -78,18 +73,7 @@ import java.util.List; this.eventDispatcher = eventDispatcher; this.allocator = allocator; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; - trackGroups = buildTrackGroups(manifest); - ProtectionElement protectionElement = manifest.protectionElement; - if (protectionElement != null) { - byte[] keyId = getProtectionElementKeyId(protectionElement.data); - // We assume pattern encryption does not apply. - trackEncryptionBoxes = new TrackEncryptionBox[] { - new TrackEncryptionBox(true, null, INITIALIZATION_VECTOR_SIZE, keyId, 0, 0, null)}; - } else { - trackEncryptionBoxes = null; - } - this.manifest = manifest; sampleStreams = newSampleStreamArray(0); compositeSequenceableLoader = compositeSequenceableLoaderFactory.createCompositeSequenceableLoader(sampleStreams); @@ -241,7 +225,6 @@ import java.util.List; manifest, streamElementIndex, selection, - trackEncryptionBoxes, transferListener); return new ChunkSampleStream<>( manifest.streamElements[streamElementIndex].type, @@ -267,26 +250,4 @@ import java.util.List; private static ChunkSampleStream[] newSampleStreamArray(int length) { return new ChunkSampleStream[length]; } - - private static byte[] getProtectionElementKeyId(byte[] initData) { - StringBuilder initDataStringBuilder = new StringBuilder(); - for (int i = 0; i < initData.length; i += 2) { - initDataStringBuilder.append((char) initData[i]); - } - String initDataString = initDataStringBuilder.toString(); - String keyIdString = initDataString.substring( - initDataString.indexOf("") + 5, initDataString.indexOf("")); - byte[] keyId = Base64.decode(keyIdString, Base64.DEFAULT); - swap(keyId, 0, 3); - swap(keyId, 1, 2); - swap(keyId, 4, 5); - swap(keyId, 6, 7); - return keyId; - } - - private static void swap(byte[] data, int firstPosition, int secondPosition) { - byte temp = data[firstPosition]; - data[firstPosition] = data[secondPosition]; - data[secondPosition] = temp; - } } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java index 2c508f0fde..cfb772a86b 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source.smoothstreaming.manifest; import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; import com.google.android.exoplayer2.offline.FilterableManifest; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.util.Assertions; @@ -41,10 +42,12 @@ public class SsManifest implements FilterableManifest { public final UUID uuid; public final byte[] data; + public final TrackEncryptionBox[] trackEncryptionBoxes; - public ProtectionElement(UUID uuid, byte[] data) { + public ProtectionElement(UUID uuid, byte[] data, TrackEncryptionBox[] trackEncryptionBoxes) { this.uuid = uuid; this.data = data; + this.trackEncryptionBoxes = trackEncryptionBoxes; } } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java index 3d5ade403a..4c1c6ee0cc 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/manifest/SsManifestParser.java @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil; +import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.ProtectionElement; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; import com.google.android.exoplayer2.upstream.ParsingLoadable; @@ -397,9 +398,10 @@ public class SsManifestParser implements ParsingLoadable.Parser { public static final String TAG = "Protection"; public static final String TAG_PROTECTION_HEADER = "ProtectionHeader"; - public static final String KEY_SYSTEM_ID = "SystemID"; + private static final int INITIALIZATION_VECTOR_SIZE = 8; + private boolean inProtectionHeader; private UUID uuid; private byte[] initData; @@ -439,7 +441,44 @@ public class SsManifestParser implements ParsingLoadable.Parser { @Override public Object build() { - return new ProtectionElement(uuid, PsshAtomUtil.buildPsshAtom(uuid, initData)); + return new ProtectionElement( + uuid, PsshAtomUtil.buildPsshAtom(uuid, initData), buildTrackEncryptionBoxes(initData)); + } + + private static TrackEncryptionBox[] buildTrackEncryptionBoxes(byte[] initData) { + return new TrackEncryptionBox[] { + new TrackEncryptionBox( + /* isEncrypted= */ true, + /* schemeType= */ null, + INITIALIZATION_VECTOR_SIZE, + getProtectionElementKeyId(initData), + /* defaultEncryptedBlocks= */ 0, + /* defaultClearBlocks= */ 0, + /* defaultInitializationVector= */ null) + }; + } + + private static byte[] getProtectionElementKeyId(byte[] initData) { + StringBuilder initDataStringBuilder = new StringBuilder(); + for (int i = 0; i < initData.length; i += 2) { + initDataStringBuilder.append((char) initData[i]); + } + String initDataString = initDataStringBuilder.toString(); + String keyIdString = + initDataString.substring( + initDataString.indexOf("") + 5, initDataString.indexOf("")); + byte[] keyId = Base64.decode(keyIdString, Base64.DEFAULT); + swap(keyId, 0, 3); + swap(keyId, 1, 2); + swap(keyId, 4, 5); + swap(keyId, 6, 7); + return keyId; + } + + private static void swap(byte[] data, int firstPosition, int secondPosition) { + byte temp = data[firstPosition]; + data[firstPosition] = data[secondPosition]; + data[secondPosition] = temp; } private static String stripCurlyBraces(String uuidString) { diff --git a/library/smoothstreaming/src/test/assets/sample_ismc_1 b/library/smoothstreaming/src/test/assets/sample_ismc_1 index 25a37d65b4..1d279d0a67 100644 --- a/library/smoothstreaming/src/test/assets/sample_ismc_1 +++ b/library/smoothstreaming/src/test/assets/sample_ismc_1 @@ -3,7 +3,7 @@ Duration="2300000000" TimeScale="10000000"> - + fgMAAAEAAQB0AzwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AQgBhAFUATQBPAEcAYwBzAGgAVQBDAEQAZAB3ADMANABZAGMAawBmAFoAQQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBnADcATgBhAFIARABJAEkATwA5ADAAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAcwA6AC8ALwBUAC0ATwBOAEwASQBOAEUALgBEAFUATQBNAFkALQBTAEUAUgBWAEUAUgAvAEEAcgB0AGUAbQBpAHMATABpAGMAZQBuAHMAZQBTAGUAcgB2AGUAcgAvAFAAbABhAHkAUgBlAGEAZAB5AE0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAEEAXwBVAFIATAA+ADwAQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwAQwBJAEQAPgAxADcANQA4ADIANgA8AC8AQwBJAEQAPgA8AEkASQBTAF8ARABSAE0AXwBWAEUAUgBTAEkATwBOAD4ANwAuADEALgAxADUANgA1AC4ANAA8AC8ASQBJAFMAXwBEAFIATQBfAFYARQBSAFMASQBPAE4APgA8AC8AQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A diff --git a/library/smoothstreaming/src/test/assets/sample_ismc_2 b/library/smoothstreaming/src/test/assets/sample_ismc_2 index 5875a18183..7f2a53036f 100644 --- a/library/smoothstreaming/src/test/assets/sample_ismc_2 +++ b/library/smoothstreaming/src/test/assets/sample_ismc_2 @@ -3,7 +3,7 @@ Duration="2300000000" TimeScale="10000000"> - + fgMAAAEAAQB0AzwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AQgBhAFUATQBPAEcAYwBzAGgAVQBDAEQAZAB3ADMANABZAGMAawBmAFoAQQA9AD0APAAvAEsASQBEAD4APABDAEgARQBDAEsAUwBVAE0APgBnADcATgBhAFIARABJAEkATwA5ADAAPQA8AC8AQwBIAEUAQwBLAFMAVQBNAD4APABMAEEAXwBVAFIATAA+AGgAdAB0AHAAcwA6AC8ALwBUAC0ATwBOAEwASQBOAEUALgBEAFUATQBNAFkALQBTAEUAUgBWAEUAUgAvAEEAcgB0AGUAbQBpAHMATABpAGMAZQBuAHMAZQBTAGUAcgB2AGUAcgAvAFAAbABhAHkAUgBlAGEAZAB5AE0AYQBuAGEAZwBlAHIALgBhAHMAbQB4ADwALwBMAEEAXwBVAFIATAA+ADwAQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwAQwBJAEQAPgAxADcANQA4ADIANgA8AC8AQwBJAEQAPgA8AEkASQBTAF8ARABSAE0AXwBWAEUAUgBTAEkATwBOAD4ANwAuADEALgAxADUANgA1AC4ANAA8AC8ASQBJAFMAXwBEAFIATQBfAFYARQBSAFMASQBPAE4APgA8AC8AQwBVAFMAVABPAE0AQQBUAFQAUgBJAEIAVQBUAEUAUwA+ADwALwBEAEEAVABBAD4APAAvAFcAUgBNAEgARQBBAEQARQBSAD4A diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsTestUtils.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsTestUtils.java index 4a2b23edc4..1e770756bc 100644 --- a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsTestUtils.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsTestUtils.java @@ -15,13 +15,12 @@ */ package com.google.android.exoplayer2.source.smoothstreaming; -import android.util.Base64; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.mp4.TrackEncryptionBox; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.ProtectionElement; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest.StreamElement; -import java.nio.charset.StandardCharsets; import java.util.Collections; /** Util methods for SmoothStreaming tests. */ @@ -41,12 +40,7 @@ public class SsTestUtils { private static final int TEST_MAX_HEIGHT = 768; private static final String TEST_LANGUAGE = "eng"; private static final ProtectionElement TEST_PROTECTION_ELEMENT = - new ProtectionElement( - C.WIDEVINE_UUID, - ("" - + Base64.encodeToString(new byte[] {0, 1, 2, 3, 4, 5, 6, 7}, Base64.DEFAULT) - + "") - .getBytes(StandardCharsets.UTF_16LE)); + new ProtectionElement(C.WIDEVINE_UUID, new byte[0], new TrackEncryptionBox[0]); private SsTestUtils() {} From 1900e941449db022bbbb9c7ddadd8375bd786feb Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 15 Jan 2019 09:32:38 +0000 Subject: [PATCH 012/110] Do not select a track in AdaptiveTrackSelection constructor. This is not necessary as the track selection needs to be updated with updateSelectedTrack anyway. It's also error-prone as the selection code calls into a protected method of a not fully initialized class. PiperOrigin-RevId: 229331669 --- .../AdaptiveTrackSelection.java | 12 ++- .../AdaptiveTrackSelectionTest.java | 90 ++++++++++++------- 2 files changed, 64 insertions(+), 38 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java index b39a5d19f0..dd2d0001f8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelection.java @@ -391,7 +391,7 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { this.minTimeBetweenBufferReevaluationMs = minTimeBetweenBufferReevaluationMs; this.clock = clock; playbackSpeed = 1f; - reason = C.SELECTION_REASON_INITIAL; + reason = C.SELECTION_REASON_UNKNOWN; lastBufferEvaluationMs = C.TIME_UNSET; trackBitrateEstimator = TrackBitrateEstimator.DEFAULT; formats = new Format[length]; @@ -403,9 +403,6 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { formats[i] = format; formatBitrates[i] = formats[i].bitrate; } - @SuppressWarnings("nullness:method.invocation.invalid") - int selectedIndex = determineIdealSelectedIndex(Long.MIN_VALUE, formatBitrates); - this.selectedIndex = selectedIndex; } /** @@ -453,6 +450,13 @@ public class AdaptiveTrackSelection extends BaseTrackSelection { // Update the estimated track bitrates. trackBitrateEstimator.getBitrates(formats, queue, mediaChunkIterators, trackBitrates); + // Make initial selection + if (reason == C.SELECTION_REASON_UNKNOWN) { + reason = C.SELECTION_REASON_INITIAL; + selectedIndex = determineIdealSelectedIndex(nowMs, trackBitrates); + return; + } + // Stash the current selection, then make a new one. int currentSelectedIndex = selectedIndex; selectedIndex = determineIdealSelectedIndex(nowMs, trackBitrates); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java index c3836e63f6..bf6b935161 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/AdaptiveTrackSelectionTest.java @@ -244,7 +244,14 @@ public final class AdaptiveTrackSelectionTest { // But TrackBitrateEstimator returns 1500 for 3rd track so it should switch up. TrackBitrateEstimator estimator = mock(TrackBitrateEstimator.class); - when(estimator.getBitrates(any(), any(), any(), any())).thenReturn(new int[] {500, 1000, 1500}); + when(estimator.getBitrates(any(), any(), any(), any())) + .then( + (invocation) -> { + int[] returnValue = new int[] {500, 1000, 1500}; + int[] inputArray = (int[]) invocation.getArguments()[3]; + System.arraycopy(returnValue, 0, inputArray, 0, returnValue.length); + return returnValue; + }); adaptiveTrackSelection = adaptiveTrackSelection(trackGroup); adaptiveTrackSelection.experimental_setTrackBitrateEstimator(estimator); @@ -385,49 +392,64 @@ public final class AdaptiveTrackSelectionTest { private AdaptiveTrackSelection adaptiveTrackSelectionWithMinDurationForQualityIncreaseMs( TrackGroup trackGroup, long minDurationForQualityIncreaseMs) { - return new AdaptiveTrackSelection( - trackGroup, - selectedAllTracksInGroup(trackGroup), - mockBandwidthMeter, - minDurationForQualityIncreaseMs, - AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, - AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, - /* bandwidthFraction= */ 1.0f, - AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, - AdaptiveTrackSelection.DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, - fakeClock); + return prepareTrackSelection( + new AdaptiveTrackSelection( + trackGroup, + selectedAllTracksInGroup(trackGroup), + mockBandwidthMeter, + minDurationForQualityIncreaseMs, + AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, + /* bandwidthFraction= */ 1.0f, + AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + AdaptiveTrackSelection.DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + fakeClock)); } private AdaptiveTrackSelection adaptiveTrackSelectionWithMaxDurationForQualityDecreaseMs( TrackGroup trackGroup, long maxDurationForQualityDecreaseMs) { - return new AdaptiveTrackSelection( - trackGroup, - selectedAllTracksInGroup(trackGroup), - mockBandwidthMeter, - AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, - maxDurationForQualityDecreaseMs, - AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, - /* bandwidthFraction= */ 1.0f, - AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, - AdaptiveTrackSelection.DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, - fakeClock); + return prepareTrackSelection( + new AdaptiveTrackSelection( + trackGroup, + selectedAllTracksInGroup(trackGroup), + mockBandwidthMeter, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, + maxDurationForQualityDecreaseMs, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_TO_RETAIN_AFTER_DISCARD_MS, + /* bandwidthFraction= */ 1.0f, + AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + AdaptiveTrackSelection.DEFAULT_MIN_TIME_BETWEEN_BUFFER_REEVALUTATION_MS, + fakeClock)); } private AdaptiveTrackSelection adaptiveTrackSelectionWithMinTimeBetweenBufferReevaluationMs( TrackGroup trackGroup, long durationToRetainAfterDiscardMs, long minTimeBetweenBufferReevaluationMs) { - return new AdaptiveTrackSelection( - trackGroup, - selectedAllTracksInGroup(trackGroup), - mockBandwidthMeter, - AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, - AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, - durationToRetainAfterDiscardMs, - /* bandwidthFraction= */ 1.0f, - AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, - minTimeBetweenBufferReevaluationMs, - fakeClock); + return prepareTrackSelection( + new AdaptiveTrackSelection( + trackGroup, + selectedAllTracksInGroup(trackGroup), + mockBandwidthMeter, + AdaptiveTrackSelection.DEFAULT_MIN_DURATION_FOR_QUALITY_INCREASE_MS, + AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS, + durationToRetainAfterDiscardMs, + /* bandwidthFraction= */ 1.0f, + AdaptiveTrackSelection.DEFAULT_BUFFERED_FRACTION_TO_LIVE_EDGE_FOR_QUALITY_INCREASE, + minTimeBetweenBufferReevaluationMs, + fakeClock)); + } + + private AdaptiveTrackSelection prepareTrackSelection( + AdaptiveTrackSelection adaptiveTrackSelection) { + adaptiveTrackSelection.enable(); + adaptiveTrackSelection.updateSelectedTrack( + /* playbackPositionUs= */ 0, + /* bufferedDurationUs= */ 0, + /* availableDurationUs= */ C.TIME_UNSET, + /* queue= */ Collections.emptyList(), + /* mediaChunkIterators= */ THREE_EMPTY_MEDIA_CHUNK_ITERATORS); + return adaptiveTrackSelection; } private int[] selectedAllTracksInGroup(TrackGroup trackGroup) { From fcda01eb5c383751557f8ea817202aa98a12647e Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 15 Jan 2019 11:07:05 +0000 Subject: [PATCH 013/110] Remove messages on release of ConcatenatingMediaSource. That was previously handled by the player. But since we switched to Handler messages instead of player messages, we should do that manually. PiperOrigin-RevId: 229341747 --- .../exoplayer2/source/ConcatenatingMediaSource.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index 7845694c45..e6d409c659 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -473,10 +473,13 @@ public class ConcatenatingMediaSource extends CompositeMediaSource> addMessage = From b97b35e2e0f1c1bf50c0c04de892592d3b2fe942 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 15 Jan 2019 14:18:29 +0000 Subject: [PATCH 014/110] Add buffer flag for last sample to improve buffered position calculation. The buffered position is currently based on the mimimum queued timestamp of all AV tracks. If the tracks have unequal lengths, one track continues loading without bounds as the "buffered position" will always stay at the shorter track's duration. This change adds an optional buffer flag to mark the last sample of the stream. This is set in the Mp4Extractor only so far. ExtractorMediaSource uses this flag to ignore AV streams in the buffered duration calculation if they already finished loading. Issue:#3670 PiperOrigin-RevId: 229359899 --- RELEASENOTES.md | 2 ++ .../java/com/google/android/exoplayer2/C.java | 7 +++-- .../extractor/mp4/TrackSampleTable.java | 3 +++ .../source/ExtractorMediaPeriod.java | 7 ++--- .../source/SampleMetadataQueue.java | 26 ++++++++++++++----- .../exoplayer2/source/SampleQueue.java | 9 +++++++ .../src/test/assets/mp4/sample.mp4.0.dump | 4 +-- .../src/test/assets/mp4/sample.mp4.1.dump | 4 +-- .../src/test/assets/mp4/sample.mp4.2.dump | 4 +-- .../src/test/assets/mp4/sample.mp4.3.dump | 4 +-- 10 files changed, 51 insertions(+), 19 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b93fa95c40..dd3aab3e12 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -48,6 +48,8 @@ ([#5351](https://github.com/google/ExoPlayer/issues/5351)). * Downloading/Caching: Improve cache performance ([#4253](https://github.com/google/ExoPlayer/issues/4253)). +* Fix issue where uneven track durations in MP4 streams can cause OOM problems + ([#3670](https://github.com/google/ExoPlayer/issues/3670)). ### 2.9.3 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 77d39fe866..8810b51000 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -460,8 +460,8 @@ public final class C { /** * Flags which can apply to a buffer containing a media sample. Possible flag values are {@link - * #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_ENCRYPTED} and - * {@link #BUFFER_FLAG_DECODE_ONLY}. + * #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_LAST_SAMPLE}, + * {@link #BUFFER_FLAG_ENCRYPTED} and {@link #BUFFER_FLAG_DECODE_ONLY}. */ @Documented @Retention(RetentionPolicy.SOURCE) @@ -470,6 +470,7 @@ public final class C { value = { BUFFER_FLAG_KEY_FRAME, BUFFER_FLAG_END_OF_STREAM, + BUFFER_FLAG_LAST_SAMPLE, BUFFER_FLAG_ENCRYPTED, BUFFER_FLAG_DECODE_ONLY }) @@ -482,6 +483,8 @@ public final class C { * Flag for empty buffers that signal that the end of the stream was reached. */ public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM; + /** Indicates that a buffer is known to contain the last media sample of the stream. */ + public static final int BUFFER_FLAG_LAST_SAMPLE = 1 << 29; // 0x20000000 /** Indicates that a buffer is (at least partially) encrypted. */ public static final int BUFFER_FLAG_ENCRYPTED = 1 << 30; // 0x40000000 /** Indicates that a buffer should be decoded but not rendered. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java index 56851fc1e0..59ea386335 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/mp4/TrackSampleTable.java @@ -64,6 +64,9 @@ import com.google.android.exoplayer2.util.Util; this.flags = flags; this.durationUs = durationUs; sampleCount = offsets.length; + if (flags.length > 0) { + flags[flags.length - 1] |= C.BUFFER_FLAG_LAST_SAMPLE; + } } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java index 40fac19178..e842d4f253 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java @@ -356,18 +356,19 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } else if (isPendingReset()) { return pendingResetPositionUs; } - long largestQueuedTimestampUs; + long largestQueuedTimestampUs = C.TIME_UNSET; if (haveAudioVideoTracks) { // Ignore non-AV tracks, which may be sparse or poorly interleaved. largestQueuedTimestampUs = Long.MAX_VALUE; int trackCount = sampleQueues.length; for (int i = 0; i < trackCount; i++) { - if (trackIsAudioVideoFlags[i]) { + if (trackIsAudioVideoFlags[i] && !sampleQueues[i].isLastSampleQueued()) { largestQueuedTimestampUs = Math.min(largestQueuedTimestampUs, sampleQueues[i].getLargestQueuedTimestampUs()); } } - } else { + } + if (largestQueuedTimestampUs == C.TIME_UNSET) { largestQueuedTimestampUs = getLargestQueuedTimestampUs(); } return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java index e5b950cf2e..ab5c5e57d9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleMetadataQueue.java @@ -57,6 +57,7 @@ import com.google.android.exoplayer2.util.Util; private long largestDiscardedTimestampUs; private long largestQueuedTimestampUs; + private boolean isLastSampleQueued; private boolean upstreamKeyframeRequired; private boolean upstreamFormatRequired; private Format upstreamFormat; @@ -93,6 +94,7 @@ import com.google.android.exoplayer2.util.Util; upstreamKeyframeRequired = true; largestDiscardedTimestampUs = Long.MIN_VALUE; largestQueuedTimestampUs = Long.MIN_VALUE; + isLastSampleQueued = false; if (resetUpstreamFormat) { upstreamFormat = null; upstreamFormatRequired = true; @@ -118,6 +120,7 @@ import com.google.android.exoplayer2.util.Util; Assertions.checkArgument(0 <= discardCount && discardCount <= (length - readPosition)); length -= discardCount; largestQueuedTimestampUs = Math.max(largestDiscardedTimestampUs, getLargestTimestamp(length)); + isLastSampleQueued = discardCount == 0 && isLastSampleQueued; if (length == 0) { return 0; } else { @@ -186,6 +189,19 @@ import com.google.android.exoplayer2.util.Util; return largestQueuedTimestampUs; } + /** + * Returns whether the last sample of the stream has knowingly been queued. A return value of + * {@code false} means that the last sample had not been queued or that it's unknown whether the + * last sample has been queued. + * + *

Samples that were discarded by calling {@link #discardUpstreamSamples(int)} are not + * considered as having been queued. Samples that were dequeued from the front of the queue are + * considered as having been queued. + */ + public synchronized boolean isLastSampleQueued() { + return isLastSampleQueued; + } + /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */ public synchronized long getFirstTimestampUs() { return length == 0 ? Long.MIN_VALUE : timesUs[relativeFirstIndex]; @@ -224,7 +240,7 @@ import com.google.android.exoplayer2.util.Util; boolean formatRequired, boolean loadingFinished, Format downstreamFormat, SampleExtrasHolder extrasHolder) { if (!hasNextSample()) { - if (loadingFinished) { + if (loadingFinished || isLastSampleQueued) { buffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM); return C.RESULT_BUFFER_READ; } else if (upstreamFormat != null @@ -388,7 +404,9 @@ import com.google.android.exoplayer2.util.Util; upstreamKeyframeRequired = false; } Assertions.checkState(!upstreamFormatRequired); - commitSampleTimestamp(timeUs); + + isLastSampleQueued = (sampleFlags & C.BUFFER_FLAG_LAST_SAMPLE) != 0; + largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs); int relativeEndIndex = getRelativeIndex(length); timesUs[relativeEndIndex] = timeUs; @@ -439,10 +457,6 @@ import com.google.android.exoplayer2.util.Util; } } - public synchronized void commitSampleTimestamp(long timeUs) { - largestQueuedTimestampUs = Math.max(largestQueuedTimestampUs, timeUs); - } - /** * Attempts to discard samples from the end of the queue to allow samples starting from the * specified timestamp to be spliced in. Samples will not be discarded prior to the read position. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java index ecc720c656..0886e79d21 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SampleQueue.java @@ -224,6 +224,15 @@ public class SampleQueue implements TrackOutput { return metadataQueue.getLargestQueuedTimestampUs(); } + /** + * Returns whether the last sample of the stream has knowingly been queued. A return value of + * {@code false} means that the last sample had not been queued or that it's unknown whether the + * last sample has been queued. + */ + public boolean isLastSampleQueued() { + return metadataQueue.isLastSampleQueued(); + } + /** Returns the timestamp of the first sample, or {@link Long#MIN_VALUE} if the queue is empty. */ public long getFirstTimestampUs() { return metadataQueue.getFirstTimestampUs(); diff --git a/library/core/src/test/assets/mp4/sample.mp4.0.dump b/library/core/src/test/assets/mp4/sample.mp4.0.dump index efc804d48b..b05d8250ab 100644 --- a/library/core/src/test/assets/mp4/sample.mp4.0.dump +++ b/library/core/src/test/assets/mp4/sample.mp4.0.dump @@ -147,7 +147,7 @@ track 0: data = length 530, hash C98BC6A8 sample 29: time = 934266 - flags = 0 + flags = 536870912 data = length 568, hash 4FE5C8EA track 1: format: @@ -352,6 +352,6 @@ track 1: data = length 229, hash FFF98DF0 sample 44: time = 1065678 - flags = 1 + flags = 536870913 data = length 6, hash 31B22286 tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample.mp4.1.dump b/library/core/src/test/assets/mp4/sample.mp4.1.dump index 10104b5e81..84d86f8ccf 100644 --- a/library/core/src/test/assets/mp4/sample.mp4.1.dump +++ b/library/core/src/test/assets/mp4/sample.mp4.1.dump @@ -147,7 +147,7 @@ track 0: data = length 530, hash C98BC6A8 sample 29: time = 934266 - flags = 0 + flags = 536870912 data = length 568, hash 4FE5C8EA track 1: format: @@ -304,6 +304,6 @@ track 1: data = length 229, hash FFF98DF0 sample 32: time = 1065678 - flags = 1 + flags = 536870913 data = length 6, hash 31B22286 tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample.mp4.2.dump b/library/core/src/test/assets/mp4/sample.mp4.2.dump index 8af96be673..9bbe8caa01 100644 --- a/library/core/src/test/assets/mp4/sample.mp4.2.dump +++ b/library/core/src/test/assets/mp4/sample.mp4.2.dump @@ -147,7 +147,7 @@ track 0: data = length 530, hash C98BC6A8 sample 29: time = 934266 - flags = 0 + flags = 536870912 data = length 568, hash 4FE5C8EA track 1: format: @@ -244,6 +244,6 @@ track 1: data = length 229, hash FFF98DF0 sample 17: time = 1065678 - flags = 1 + flags = 536870913 data = length 6, hash 31B22286 tracksEnded = true diff --git a/library/core/src/test/assets/mp4/sample.mp4.3.dump b/library/core/src/test/assets/mp4/sample.mp4.3.dump index f1259661ed..f210f277b3 100644 --- a/library/core/src/test/assets/mp4/sample.mp4.3.dump +++ b/library/core/src/test/assets/mp4/sample.mp4.3.dump @@ -147,7 +147,7 @@ track 0: data = length 530, hash C98BC6A8 sample 29: time = 934266 - flags = 0 + flags = 536870912 data = length 568, hash 4FE5C8EA track 1: format: @@ -184,6 +184,6 @@ track 1: data = length 229, hash FFF98DF0 sample 2: time = 1065678 - flags = 1 + flags = 536870913 data = length 6, hash 31B22286 tracksEnded = true From 36883e6277fed2fbec5a83afddc8c93e1838b884 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 15 Jan 2019 15:02:38 +0000 Subject: [PATCH 015/110] Bump version for 2.9.4 release PiperOrigin-RevId: 229364563 --- RELEASENOTES.md | 26 ++++++++++--------- constants.gradle | 4 +-- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 ++--- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 4d03b06687..071f6e05ff 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -17,7 +17,8 @@ * Add `setStreamKeys` method to factories of DASH, SmoothStreaming and HLS media sources to simplify filtering by downloaded streams. * Caching: - * Improve performance of `SimpleCache`. + * Improve performance of `SimpleCache` + ([#4253](https://github.com/google/ExoPlayer/issues/4253)). * Cache data with unknown length by default. The previous flag to opt in to this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been replaced with an opt out flag @@ -27,29 +28,30 @@ * Rename TaskState to DownloadState. * Add new states to DownloadState. * Replace DownloadState.action with DownloadAction fields. -* SmoothStreaming: Fix support for subtitles in DRM protected streams - ([#5378](https://github.com/google/ExoPlayer/issues/5378)). * Add support for SHOUTcast ICY metadata ([#3735](https://github.com/google/ExoPlayer/issues/3735)). * CEA-608: Improved conformance to the specification ([#3860](https://github.com/google/ExoPlayer/issues/3860)). -* IMA extension: - * Clear ads loader listeners on release - ([#4114](https://github.com/google/ExoPlayer/issues/4114)). - * Require setting the `Player` on `AdsLoader` instances before playback. +* IMA extension: Require setting the `Player` on `AdsLoader` instances before + playback. +* Add `Handler` parameter to `ConcatenatingMediaSource` methods which take a + callback `Runnable`. +* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`. + +### 2.9.4 ### + +* IMA extension: Clear ads loader listeners on release + ([#4114](https://github.com/google/ExoPlayer/issues/4114)). +* SmoothStreaming: Fix support for subtitles in DRM protected streams + ([#5378](https://github.com/google/ExoPlayer/issues/5378)). * FFmpeg extension: Treat invalid data errors as non-fatal to match the behavior of MediaCodec ([#5293](https://github.com/google/ExoPlayer/issues/5293)). * Fix issue where sending callbacks for playlist changes may cause problems because of parallel player access ([#5240](https://github.com/google/ExoPlayer/issues/5240)). -* Add `Handler` parameter to `ConcatenatingMediaSource` methods which take a - callback `Runnable`. -* Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`. * Fix issue with reusing a `ClippingMediaSource` with an inner `ExtractorMediaSource` and a non-zero start position ([#5351](https://github.com/google/ExoPlayer/issues/5351)). -* Downloading/Caching: Improve cache performance - ([#4253](https://github.com/google/ExoPlayer/issues/4253)). * Fix issue where uneven track durations in MP4 streams can cause OOM problems ([#3670](https://github.com/google/ExoPlayer/issues/3670)). diff --git a/constants.gradle b/constants.gradle index ac801d2d3b..716ddbadba 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.9.3' - releaseVersionCode = 2009003 + releaseVersion = '2.9.4' + releaseVersionCode = 2009004 // Important: ExoPlayer specifies a minSdkVersion of 14 because various // components provided by the library may be of use on older devices. // However, please note that the core media playback functionality provided diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 792f6cf651..36723c5d73 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { /** The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.9.3"; + public static final String VERSION = "2.9.4"; /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.3"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.4"; /** * The version of the library expressed as an integer, for example 1002003. @@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2009003; + public static final int VERSION_INT = 2009004; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From c6092bbb43a605d8fe9517fd90d0e3bec1806650 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 15 Jan 2019 15:07:47 +0000 Subject: [PATCH 016/110] Fix typo. PiperOrigin-RevId: 229365333 --- .../exoplayer2/source/dash/DefaultDashChunkSource.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 33890a6767..a02b2f1ee7 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -457,10 +457,10 @@ public class DefaultDashChunkSource implements DashChunkSource { } private ArrayList getRepresentations() { - List manifestAdapationSets = manifest.getPeriod(periodIndex).adaptationSets; + List manifestAdaptationSets = manifest.getPeriod(periodIndex).adaptationSets; ArrayList representations = new ArrayList<>(); for (int adaptationSetIndex : adaptationSetIndices) { - representations.addAll(manifestAdapationSets.get(adaptationSetIndex).representations); + representations.addAll(manifestAdaptationSets.get(adaptationSetIndex).representations); } return representations; } From ca7675ceae7fde67cfca292ed7c1745a050573f7 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 15 Jan 2019 15:08:12 +0000 Subject: [PATCH 017/110] Fix bug where missing switch adaptation set causes multiple identical track groups. When the extra adaptation set of a switch group isn't defined in the manifest, we currently assume it's the first adaptation group. This either leads to wrong grouping or duplicate track groups. Such a case may easily happen if the manifest is filtered such that only one of the switch adaptation sets will be present in the manifest. PiperOrigin-RevId: 229365379 --- .../source/dash/DashMediaPeriod.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index d0a12b9688..cba7a9941b 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -46,6 +46,7 @@ import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.LoaderErrorThrower; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -452,13 +453,22 @@ import java.util.List; if (adaptationSetSwitchingProperty == null) { groupedAdaptationSetIndices[groupCount++] = new int[] {i}; } else { - String[] extraAdaptationSetIds = adaptationSetSwitchingProperty.value.split(","); + String[] extraAdaptationSetIds = Util.split(adaptationSetSwitchingProperty.value, ","); int[] adaptationSetIndices = new int[1 + extraAdaptationSetIds.length]; adaptationSetIndices[0] = i; + int outputIndex = 1; for (int j = 0; j < extraAdaptationSetIds.length; j++) { - int extraIndex = idToIndexMap.get(Integer.parseInt(extraAdaptationSetIds[j])); - adaptationSetUsedFlags[extraIndex] = true; - adaptationSetIndices[1 + j] = extraIndex; + int extraIndex = + idToIndexMap.get( + Integer.parseInt(extraAdaptationSetIds[j]), /* valueIfKeyNotFound= */ -1); + if (extraIndex != -1) { + adaptationSetUsedFlags[extraIndex] = true; + adaptationSetIndices[outputIndex] = extraIndex; + outputIndex++; + } + } + if (outputIndex < adaptationSetIndices.length) { + adaptationSetIndices = Arrays.copyOf(adaptationSetIndices, outputIndex); } groupedAdaptationSetIndices[groupCount++] = adaptationSetIndices; } From 8adc16a6232a523e46a86010b758d4c35ea645d9 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 16 Jan 2019 11:05:44 +0000 Subject: [PATCH 018/110] allow developers to set the subText of the notifcation Issue: #5344 PiperOrigin-RevId: 229527963 --- RELEASENOTES.md | 2 ++ .../exoplayer2/ui/PlayerNotificationManager.java | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 071f6e05ff..1150edcf8c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -54,6 +54,8 @@ ([#5351](https://github.com/google/ExoPlayer/issues/5351)). * Fix issue where uneven track durations in MP4 streams can cause OOM problems ([#3670](https://github.com/google/ExoPlayer/issues/3670)). +* Add the sub text to the MediaDescriptionAdapter of the + PlayerNotificationManager. ### 2.9.3 ### diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index 30c19d5f82..4d6b83ccae 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -126,6 +126,18 @@ public class PlayerNotificationManager { @Nullable String getCurrentContentText(Player player); + /** + * Gets the content sub text for the current media item. + * + *

See {@link NotificationCompat.Builder#setSubText(CharSequence)}. + * + * @param player The {@link Player} for which a notification is being built. + */ + @Nullable + default String getCurrentSubText(Player player) { + return null; + } + /** * Gets the large icon for the current media item. * @@ -946,6 +958,7 @@ public class PlayerNotificationManager { // Set media specific notification properties from MediaDescriptionAdapter. builder.setContentTitle(mediaDescriptionAdapter.getCurrentContentTitle(player)); builder.setContentText(mediaDescriptionAdapter.getCurrentContentText(player)); + builder.setSubText(mediaDescriptionAdapter.getCurrentSubText(player)); if (largeIcon == null) { largeIcon = mediaDescriptionAdapter.getCurrentLargeIcon( From ec77f737eece3c07f001261249b539fcf341251b Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 16 Jan 2019 14:03:07 +0000 Subject: [PATCH 019/110] Make DownloadManager watch requirements directly PiperOrigin-RevId: 229544734 --- .../exoplayer2/demo/DemoApplication.java | 4 +- .../exoplayer2/demo/DownloadTracker.java | 9 + .../exoplayer2/offline/DownloadManager.java | 98 +++++++- .../exoplayer2/offline/DownloadService.java | 221 +++++++----------- .../exoplayer2/scheduler/Requirements.java | 24 +- .../scheduler/RequirementsWatcher.java | 17 +- .../offline/DownloadManagerTest.java | 8 +- .../dash/offline/DownloadManagerDashTest.java | 5 +- .../dash/offline/DownloadServiceDashTest.java | 5 +- .../testutil/TestDownloadManagerListener.java | 7 + 10 files changed, 249 insertions(+), 149 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java index 9b72df8d98..560a9be58a 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java @@ -101,10 +101,12 @@ public class DemoApplication extends Application { new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory()); downloadManager = new DownloadManager( + this, new File(getDownloadDirectory(), DOWNLOAD_ACTION_FILE), new DefaultDownloaderFactory(downloaderConstructorHelper), MAX_SIMULTANEOUS_DOWNLOADS, - DownloadManager.DEFAULT_MIN_RETRY_COUNT); + DownloadManager.DEFAULT_MIN_RETRY_COUNT, + DownloadManager.DEFAULT_REQUIREMENTS); downloadTracker = new DownloadTracker( /* context= */ this, diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java index 08ecdf3be7..559bbcef0f 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java @@ -41,6 +41,7 @@ import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadState; import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper; import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.dash.offline.DashDownloadHelper; import com.google.android.exoplayer2.source.hls.offline.HlsDownloadHelper; @@ -159,6 +160,14 @@ public class DownloadTracker implements DownloadManager.Listener { // Do nothing. } + @Override + public void onRequirementsStateChanged( + DownloadManager downloadManager, + Requirements requirements, + @Requirements.RequirementFlags int notMetRequirements) { + // Do nothing. + } + // Internal methods private void loadTrackedActions() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java index c8c02d4980..8932140a34 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java @@ -28,12 +28,15 @@ import static com.google.android.exoplayer2.offline.DownloadState.STATE_STOPPED; import static com.google.android.exoplayer2.offline.DownloadState.STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY; import static com.google.android.exoplayer2.offline.DownloadState.STOP_FLAG_STOPPED; +import android.content.Context; import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.scheduler.Requirements; +import com.google.android.exoplayer2.scheduler.RequirementsWatcher; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import java.io.File; @@ -74,18 +77,35 @@ public final class DownloadManager { * @param downloadManager The reporting instance. */ void onIdle(DownloadManager downloadManager); + + /** + * Called when the download requirements state changed. + * + * @param downloadManager The reporting instance. + * @param requirements Requirements needed to be met to start downloads. + * @param notMetRequirements {@link Requirements.RequirementFlags RequirementFlags} that are not + * met, or 0. + */ + void onRequirementsStateChanged( + DownloadManager downloadManager, + Requirements requirements, + @Requirements.RequirementFlags int notMetRequirements); } /** The default maximum number of simultaneous downloads. */ public static final int DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS = 1; /** The default minimum number of times a download must be retried before failing. */ public static final int DEFAULT_MIN_RETRY_COUNT = 5; + /** The default requirement is that the device has network connectivity. */ + public static final Requirements DEFAULT_REQUIREMENTS = + new Requirements(Requirements.NETWORK_TYPE_ANY, false, false); private static final String TAG = "DownloadManager"; private static final boolean DEBUG = false; private final int maxActiveDownloads; private final int minRetryCount; + private final Context context; private final ActionFile actionFile; private final DownloaderFactory downloaderFactory; private final ArrayList downloads; @@ -99,31 +119,43 @@ public final class DownloadManager { private boolean initialized; private boolean released; @DownloadState.StopFlags private int stickyStopFlags; + private RequirementsWatcher requirementsWatcher; /** * Constructs a {@link DownloadManager}. * + * @param context Any context. * @param actionFile The file in which active actions are saved. * @param downloaderFactory A factory for creating {@link Downloader}s. */ - public DownloadManager(File actionFile, DownloaderFactory downloaderFactory) { + public DownloadManager(Context context, File actionFile, DownloaderFactory downloaderFactory) { this( - actionFile, downloaderFactory, DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS, DEFAULT_MIN_RETRY_COUNT); + context, + actionFile, + downloaderFactory, + DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS, + DEFAULT_MIN_RETRY_COUNT, + DEFAULT_REQUIREMENTS); } /** * Constructs a {@link DownloadManager}. * + * @param context Any context. * @param actionFile The file in which active actions are saved. * @param downloaderFactory A factory for creating {@link Downloader}s. * @param maxSimultaneousDownloads The maximum number of simultaneous downloads. * @param minRetryCount The minimum number of times a download must be retried before failing. + * @param requirements The requirements needed to be met to start downloads. */ public DownloadManager( + Context context, File actionFile, DownloaderFactory downloaderFactory, int maxSimultaneousDownloads, - int minRetryCount) { + int minRetryCount, + Requirements requirements) { + this.context = context.getApplicationContext(); this.actionFile = new ActionFile(actionFile); this.downloaderFactory = downloaderFactory; this.maxActiveDownloads = maxSimultaneousDownloads; @@ -146,10 +178,30 @@ public final class DownloadManager { listeners = new CopyOnWriteArraySet<>(); actionQueue = new ArrayDeque<>(); + watchRequirements(requirements); loadActions(); logd("Created"); } + /** + * Sets the requirements needed to be met to start downloads. + * + * @param requirements Need to be met to start downloads. + */ + public void setRequirements(Requirements requirements) { + Assertions.checkState(!released); + if (requirements.equals(requirementsWatcher.getRequirements())) { + return; + } + requirementsWatcher.stop(); + notifyListenersRequirementsStateChange(watchRequirements(requirements)); + } + + /** Returns the requirements needed to be met to start downloads. */ + public Requirements getRequirements() { + return requirementsWatcher.getRequirements(); + } + /** * Adds a {@link Listener}. * @@ -278,6 +330,9 @@ public final class DownloadManager { } setStopFlags(STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY); released = true; + if (requirementsWatcher != null) { + requirementsWatcher.stop(); + } final ConditionVariable fileIOFinishedCondition = new ConditionVariable(); fileIOHandler.post(fileIOFinishedCondition::open); fileIOFinishedCondition.block(); @@ -346,6 +401,15 @@ public final class DownloadManager { } } + private void notifyListenersRequirementsStateChange( + @Requirements.RequirementFlags int notMetRequirements) { + logdFlags("Not met requirements are changed", notMetRequirements); + for (Listener listener : listeners) { + listener.onRequirementsStateChanged( + DownloadManager.this, requirementsWatcher.getRequirements(), notMetRequirements); + } + } + private void loadActions() { fileIOHandler.post( () -> { @@ -420,6 +484,18 @@ public final class DownloadManager { } } + @Requirements.RequirementFlags + private int watchRequirements(Requirements requirements) { + requirementsWatcher = new RequirementsWatcher(context, new RequirementListener(), requirements); + @Requirements.RequirementFlags int notMetRequirements = requirementsWatcher.start(); + if (notMetRequirements == 0) { + startDownloads(); + } else { + stopDownloads(); + } + return notMetRequirements; + } + private static final class Download { private final String id; @@ -693,4 +769,20 @@ public final class DownloadManager { return Math.min((errorCount - 1) * 1000, 5000); } } + + private class RequirementListener implements RequirementsWatcher.Listener { + @Override + public void requirementsMet(RequirementsWatcher requirementsWatcher) { + startDownloads(); + notifyListenersRequirementsStateChange(0); + } + + @Override + public void requirementsNotMet( + RequirementsWatcher requirementsWatcher, + @Requirements.RequirementFlags int notMetRequirements) { + stopDownloads(); + notifyListenersRequirementsStateChange(notMetRequirements); + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index 3031a032db..d424ed5ef0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -25,8 +25,8 @@ import android.os.Looper; import android.support.annotation.Nullable; import android.support.annotation.StringRes; import com.google.android.exoplayer2.scheduler.Requirements; -import com.google.android.exoplayer2.scheduler.RequirementsWatcher; import com.google.android.exoplayer2.scheduler.Scheduler; +import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.NotificationUtil; import com.google.android.exoplayer2.util.Util; @@ -43,10 +43,6 @@ public abstract class DownloadService extends Service { /** Starts a download service, adding a new {@link DownloadAction} to be executed. */ public static final String ACTION_ADD = "com.google.android.exoplayer.downloadService.action.ADD"; - /** Reloads the download requirements. */ - public static final String ACTION_RELOAD_REQUIREMENTS = - "com.google.android.exoplayer.downloadService.action.RELOAD_REQUIREMENTS"; - /** Like {@link #ACTION_INIT}, but with {@link #KEY_FOREGROUND} implicitly set to true. */ private static final String ACTION_RESTART = "com.google.android.exoplayer.downloadService.action.RESTART"; @@ -70,20 +66,16 @@ public abstract class DownloadService extends Service { private static final String TAG = "DownloadService"; private static final boolean DEBUG = false; - // Keep the requirements helper for each DownloadService as long as there are downloads (and the - // process is running). This allows downloads to resume when there's no scheduler. It may also - // allow downloads the resume more quickly than when relying on the scheduler alone. - private static final HashMap, RequirementsHelper> - requirementsHelpers = new HashMap<>(); - private static final Requirements DEFAULT_REQUIREMENTS = - new Requirements(Requirements.NETWORK_TYPE_ANY, false, false); + // Keep DownloadManagerListeners for each DownloadService as long as there are downloads (and the + // process is running). This allows DownloadService to restart when there's no scheduler. + private static final HashMap, DownloadManagerHelper> + downloadManagerListeners = new HashMap<>(); private final @Nullable ForegroundNotificationUpdater foregroundNotificationUpdater; private final @Nullable String channelId; private final @StringRes int channelName; private DownloadManager downloadManager; - private DownloadManagerListener downloadManagerListener; private int lastStartId; private boolean startedInForeground; private boolean taskRemoved; @@ -227,9 +219,16 @@ public abstract class DownloadService extends Service { NotificationUtil.createNotificationChannel( this, channelId, channelName, NotificationUtil.IMPORTANCE_LOW); } - downloadManager = getDownloadManager(); - downloadManagerListener = new DownloadManagerListener(); - downloadManager.addListener(downloadManagerListener); + Class clazz = getClass(); + DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(clazz); + if (downloadManagerHelper == null) { + downloadManagerHelper = + new DownloadManagerHelper( + getApplicationContext(), getDownloadManager(), getScheduler(), clazz); + downloadManagerListeners.put(clazz, downloadManagerHelper); + } + downloadManager = downloadManagerHelper.downloadManager; + downloadManagerHelper.attachService(this); } @Override @@ -264,22 +263,11 @@ public abstract class DownloadService extends Service { } } break; - case ACTION_RELOAD_REQUIREMENTS: - stopWatchingRequirements(); - break; default: Log.e(TAG, "Ignoring unrecognized action: " + intentAction); break; } - Requirements requirements = getRequirements(); - if (requirements.checkRequirements(this)) { - downloadManager.startDownloads(); - } else { - downloadManager.stopDownloads(); - } - maybeStartWatchingRequirements(requirements); - if (downloadManager.isIdle()) { stop(); } @@ -295,11 +283,12 @@ public abstract class DownloadService extends Service { @Override public void onDestroy() { logd("onDestroy"); + DownloadManagerHelper downloadManagerHelper = downloadManagerListeners.get(getClass()); + boolean unschedule = downloadManager.getDownloadCount() <= 0; + downloadManagerHelper.detachService(this, unschedule); if (foregroundNotificationUpdater != null) { foregroundNotificationUpdater.stopPeriodicUpdates(); } - downloadManager.removeListener(downloadManagerListener); - maybeStopWatchingRequirements(); } /** DownloadService isn't designed to be bound. */ @@ -311,9 +300,7 @@ public abstract class DownloadService extends Service { /** * Returns a {@link DownloadManager} to be used to downloaded content. Called only once in the - * life cycle of the service. The service will call {@link DownloadManager#startDownloads()} and - * {@link DownloadManager#stopDownloads} as necessary when requirements returned by {@link - * #getRequirements()} are met or stop being met. + * life cycle of the process. */ protected abstract DownloadManager getDownloadManager(); @@ -324,14 +311,6 @@ public abstract class DownloadService extends Service { */ protected abstract @Nullable Scheduler getScheduler(); - /** - * Returns requirements for downloads to take place. By default the only requirement is that the - * device has network connectivity. - */ - protected Requirements getRequirements() { - return DEFAULT_REQUIREMENTS; - } - /** * Should be overridden in the subclass if the service will be run in the foreground. * @@ -363,32 +342,16 @@ public abstract class DownloadService extends Service { // Do nothing. } - private void maybeStartWatchingRequirements(Requirements requirements) { - if (downloadManager.getDownloadCount() == 0) { - return; - } - Class clazz = getClass(); - RequirementsHelper requirementsHelper = requirementsHelpers.get(clazz); - if (requirementsHelper == null) { - requirementsHelper = new RequirementsHelper(this, requirements, getScheduler(), clazz); - requirementsHelpers.put(clazz, requirementsHelper); - requirementsHelper.start(); - logd("started watching requirements"); - } - } - - private void maybeStopWatchingRequirements() { - if (downloadManager.getDownloadCount() > 0) { - return; - } - stopWatchingRequirements(); - } - - private void stopWatchingRequirements() { - RequirementsHelper requirementsHelper = requirementsHelpers.remove(getClass()); - if (requirementsHelper != null) { - requirementsHelper.stop(); - logd("stopped watching requirements"); + private void notifyDownloadStateChange(DownloadState downloadState) { + onDownloadStateChanged(downloadState); + if (foregroundNotificationUpdater != null) { + if (downloadState.state == DownloadState.STATE_DOWNLOADING + || downloadState.state == DownloadState.STATE_REMOVING + || downloadState.state == DownloadState.STATE_RESTARTING) { + foregroundNotificationUpdater.startPeriodicUpdates(); + } else { + foregroundNotificationUpdater.update(); + } } } @@ -420,33 +383,6 @@ public abstract class DownloadService extends Service { return new Intent(context, clazz).setAction(action); } - private final class DownloadManagerListener implements DownloadManager.Listener { - @Override - public void onInitialized(DownloadManager downloadManager) { - maybeStartWatchingRequirements(getRequirements()); - } - - @Override - public void onDownloadStateChanged( - DownloadManager downloadManager, DownloadState downloadState) { - DownloadService.this.onDownloadStateChanged(downloadState); - if (foregroundNotificationUpdater != null) { - if (downloadState.state == DownloadState.STATE_DOWNLOADING - || downloadState.state == DownloadState.STATE_REMOVING - || downloadState.state == DownloadState.STATE_RESTARTING) { - foregroundNotificationUpdater.startPeriodicUpdates(); - } else { - foregroundNotificationUpdater.update(); - } - } - } - - @Override - public final void onIdle(DownloadManager downloadManager) { - stop(); - } - } - private final class ForegroundNotificationUpdater implements Runnable { private final int notificationId; @@ -494,58 +430,87 @@ public abstract class DownloadService extends Service { } } - private static final class RequirementsHelper implements RequirementsWatcher.Listener { + private static final class DownloadManagerHelper implements DownloadManager.Listener { private final Context context; - private final Requirements requirements; - private final @Nullable Scheduler scheduler; + private final DownloadManager downloadManager; + @Nullable private final Scheduler scheduler; private final Class serviceClass; - private final RequirementsWatcher requirementsWatcher; + @Nullable private DownloadService downloadService; - private RequirementsHelper( + private DownloadManagerHelper( Context context, - Requirements requirements, + DownloadManager downloadManager, @Nullable Scheduler scheduler, Class serviceClass) { this.context = context; - this.requirements = requirements; + this.downloadManager = downloadManager; this.scheduler = scheduler; this.serviceClass = serviceClass; - requirementsWatcher = new RequirementsWatcher(context, this, requirements); - } - - public void start() { - requirementsWatcher.start(); - } - - public void stop() { - requirementsWatcher.stop(); + downloadManager.addListener(this); if (scheduler != null) { + Requirements requirements = downloadManager.getRequirements(); + setSchedulerEnabled(/* enabled= */ !requirements.checkRequirements(context), requirements); + } + } + + public void attachService(DownloadService downloadService) { + Assertions.checkState(this.downloadService == null); + this.downloadService = downloadService; + } + + public void detachService(DownloadService downloadService, boolean unschedule) { + Assertions.checkState(this.downloadService == downloadService); + this.downloadService = null; + if (unschedule) { scheduler.cancel(); } } @Override - public void requirementsMet(RequirementsWatcher requirementsWatcher) { - try { - notifyService(); - } catch (Exception e) { - /* If we can't notify the service, don't stop the scheduler. */ - return; - } - if (scheduler != null) { - scheduler.cancel(); + public void onInitialized(DownloadManager downloadManager) { + // Do nothing. + } + + @Override + public void onDownloadStateChanged( + DownloadManager downloadManager, DownloadState downloadState) { + if (downloadService != null) { + downloadService.notifyDownloadStateChange(downloadState); } } @Override - public void requirementsNotMet(RequirementsWatcher requirementsWatcher) { - try { - notifyService(); - } catch (Exception e) { - /* Do nothing. The service isn't running anyway. */ + public final void onIdle(DownloadManager downloadManager) { + if (downloadService != null) { + downloadService.stop(); + } + } + + @Override + public void onRequirementsStateChanged( + DownloadManager downloadManager, + Requirements requirements, + @Requirements.RequirementFlags int notMetRequirements) { + boolean requirementsMet = notMetRequirements == 0; + if (downloadService == null && requirementsMet) { + try { + Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_INIT); + context.startService(intent); + } catch (IllegalStateException e) { + /* startService fails if the app is in the background then don't stop the scheduler. */ + return; + } } if (scheduler != null) { + setSchedulerEnabled(/* enabled= */ !requirementsMet, requirements); + } + } + + private void setSchedulerEnabled(boolean enabled, Requirements requirements) { + if (!enabled) { + scheduler.cancel(); + } else { String servicePackage = context.getPackageName(); boolean success = scheduler.schedule(requirements, servicePackage, ACTION_RESTART); if (!success) { @@ -553,15 +518,5 @@ public abstract class DownloadService extends Service { } } } - - private void notifyService() throws Exception { - Intent intent = getIntent(context, serviceClass, DownloadService.ACTION_INIT); - try { - context.startService(intent); - } catch (IllegalStateException e) { - /* startService will fail if the app is in the background and the service isn't running. */ - throw new Exception(e); - } - } } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java index 4019d1ae70..77630a4543 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java @@ -155,16 +155,14 @@ public final class Requirements { * @return Whether the requirements are met. */ public boolean checkRequirements(Context context) { - return checkNetworkRequirements(context) - && checkChargingRequirement(context) - && checkIdleRequirement(context); + return getNotMetRequirements(context) == 0; } /** - * Returns the requirement flags that are not met, or 0. + * Returns {@link RequirementFlags} that are not met, or 0. * * @param context Any context. - * @return The requirement flags that are not met, or 0. + * @return RequirementFlags that are not met, or 0. */ @RequirementFlags public int getNotMetRequirements(Context context) { @@ -285,4 +283,20 @@ public final class Requirements { + (isIdleRequired() ? ",idle" : "") + '}'; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + return requirements == ((Requirements) o).requirements; + } + + @Override + public int hashCode() { + return requirements; + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java index fded95614c..686f19d161 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java @@ -55,8 +55,12 @@ public final class RequirementsWatcher { * requirements are not met. * * @param requirementsWatcher Calling instance. + * @param notMetRequirements {@link Requirements.RequirementFlags RequirementFlags} that are not + * met, or 0. */ - void requirementsNotMet(RequirementsWatcher requirementsWatcher); + void requirementsNotMet( + RequirementsWatcher requirementsWatcher, + @Requirements.RequirementFlags int notMetRequirements); } private static final String TAG = "RequirementsWatcher"; @@ -66,7 +70,7 @@ public final class RequirementsWatcher { private final Requirements requirements; private DeviceStatusChangeReceiver receiver; - private int notMetRequirements; + @Requirements.RequirementFlags private int notMetRequirements; private CapabilityValidatedCallback networkCallback; private Handler handler; @@ -85,8 +89,11 @@ public final class RequirementsWatcher { /** * Starts watching for changes. Must be called from a thread that has an associated {@link * Looper}. Listener methods are called on the caller thread. + * + * @return Initial {@link Requirements.RequirementFlags RequirementFlags} that are not met, or 0. */ - public void start() { + @Requirements.RequirementFlags + public int start() { Assertions.checkNotNull(Looper.myLooper()); handler = new Handler(); @@ -115,6 +122,7 @@ public final class RequirementsWatcher { receiver = new DeviceStatusChangeReceiver(); context.registerReceiver(receiver, filter, null, handler); logd(this + " started"); + return notMetRequirements; } /** Stops watching for changes. */ @@ -162,6 +170,7 @@ public final class RequirementsWatcher { } private void checkRequirements() { + @Requirements.RequirementFlags int notMetRequirements = requirements.getNotMetRequirements(context); if (this.notMetRequirements == notMetRequirements) { logd("notMetRequirements hasn't changed: " + notMetRequirements); @@ -173,7 +182,7 @@ public final class RequirementsWatcher { listener.requirementsMet(this); } else { logd("stop job"); - listener.requirementsNotMet(this); + listener.requirementsNotMet(this, notMetRequirements); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java index 5902ac894a..283d343e63 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadManagerTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.fail; import android.net.Uri; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.offline.DownloadState.State; +import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.testutil.TestDownloadManagerListener; @@ -426,7 +427,12 @@ public class DownloadManagerTest { () -> { downloadManager = new DownloadManager( - actionFile, downloaderFactory, maxActiveDownloadTasks, MIN_RETRY_COUNT); + RuntimeEnvironment.application, + actionFile, + downloaderFactory, + maxActiveDownloadTasks, + MIN_RETRY_COUNT, + new Requirements(0)); downloadManagerListener = new TestDownloadManagerListener(downloadManager, dummyMainThread); downloadManager.startDownloads(); diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java index ccccb20ccb..28040ec538 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java @@ -30,6 +30,7 @@ import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeDataSet; import com.google.android.exoplayer2.testutil.FakeDataSource; @@ -241,11 +242,13 @@ public class DownloadManagerDashTest { Factory fakeDataSourceFactory = new FakeDataSource.Factory().setFakeDataSet(fakeDataSet); downloadManager = new DownloadManager( + RuntimeEnvironment.application, actionFile, new DefaultDownloaderFactory( new DownloaderConstructorHelper(cache, fakeDataSourceFactory)), /* maxSimultaneousDownloads= */ 1, - /* minRetryCount= */ 3); + /* minRetryCount= */ 3, + new Requirements(0)); downloadManagerListener = new TestDownloadManagerListener(downloadManager, dummyMainThread); diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java index adae0d7b04..bf32b65ba7 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadServiceDashTest.java @@ -30,6 +30,7 @@ import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.offline.StreamKey; +import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.scheduler.Scheduler; import com.google.android.exoplayer2.testutil.DummyMainThread; import com.google.android.exoplayer2.testutil.FakeDataSet; @@ -117,11 +118,13 @@ public class DownloadServiceDashTest { actionFile.delete(); final DownloadManager dashDownloadManager = new DownloadManager( + RuntimeEnvironment.application, actionFile, new DefaultDownloaderFactory( new DownloaderConstructorHelper(cache, fakeDataSourceFactory)), /* maxSimultaneousDownloads= */ 1, - /* minRetryCount= */ 3); + /* minRetryCount= */ 3, + new Requirements(0)); downloadManagerListener = new TestDownloadManagerListener(dashDownloadManager, dummyMainThread); dashDownloadManager.startDownloads(); diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java index 8216b881f3..2109cceda8 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/TestDownloadManagerListener.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import android.os.ConditionVariable; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadState; +import com.google.android.exoplayer2.scheduler.Requirements; import java.util.HashMap; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.CountDownLatch; @@ -82,6 +83,12 @@ public final class TestDownloadManagerListener implements DownloadManager.Listen } } + @Override + public void onRequirementsStateChanged( + DownloadManager downloadManager, Requirements requirements, int notMetRequirements) { + // Do nothing. + } + /** * Blocks until all remove and download tasks are complete and throws an exception if there was an * error. From 16a185de1d42b049608c128bc203ea968234864a Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 16 Jan 2019 20:18:42 +0000 Subject: [PATCH 020/110] make PlayerNotificationListener better suited for foreground services Issue: #5301 Issue: #4988 Issue: #4813 Issue: #5344 Issue: #5117 PiperOrigin-RevId: 229603354 --- RELEASENOTES.md | 2 + .../ui/PlayerNotificationManager.java | 183 +++++++++++------- 2 files changed, 111 insertions(+), 74 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1150edcf8c..96d1fb1ff3 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -37,6 +37,8 @@ * Add `Handler` parameter to `ConcatenatingMediaSource` methods which take a callback `Runnable`. * Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`. +* Change signature of `PlayerNotificationManager.NotificationListener` to better + fit service requirements. Remove ability to set a custom stop action. ### 2.9.4 ### diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index 4d6b83ccae..103534d8ca 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -58,7 +58,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * player state. * *

The notification is cancelled when {@code null} is passed to {@link #setPlayer(Player)} or - * when an intent with action {@link #ACTION_STOP} is received. + * when the notification is dismissed by the user. * *

If the player is released it must be removed from the manager by calling {@code * setPlayer(null)} which will cancel the notification. @@ -72,11 +72,17 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * are displayed. *

    *
  • Corresponding setter: {@link #setUseNavigationActions(boolean)} + *
  • Default: {@code true} *
- *
  • {@code stopAction} - Sets which stop action should be used. If set to null, the stop - * action is not displayed. + *
  • {@code usePlayPauseActions} - Sets whether the play and pause actions are displayed. *
      - *
    • Corresponding setter: {@link #setStopAction(String)}} + *
    • Corresponding setter: {@link #setUsePlayPauseActions(boolean)} + *
    • Default: {@code true} + *
    + *
  • {@code useStopAction} - Sets whether the stop action is displayed. + *
      + *
    • Corresponding setter: {@link #setUseStopAction(boolean)} + *
    • Default: {@code false} *
    *
  • {@code rewindIncrementMs} - Sets the rewind increment. If set to zero the rewind * action is not displayed. @@ -87,7 +93,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; *
  • {@code fastForwardIncrementMs} - Sets the fast forward increment. If set to zero the * fast forward action is not included in the notification. *
      - *
    • Corresponding setter: {@link #setFastForwardIncrementMs(long)}} + *
    • Corresponding setter: {@link #setFastForwardIncrementMs(long)} *
    • Default: {@link #DEFAULT_FAST_FORWARD_MS} (5000) *
    * @@ -195,7 +201,7 @@ public class PlayerNotificationManager { void onCustomAction(Player player, String action, Intent intent); } - /** A listener for start and cancellation of the notification. */ + /** A listener for changes to the notification. */ public interface NotificationListener { /** @@ -203,15 +209,41 @@ public class PlayerNotificationManager { * * @param notificationId The id with which the notification has been posted. * @param notification The {@link Notification}. + * @deprecated Use {@link #onNotificationPosted(int, Notification, boolean)} instead. */ - void onNotificationStarted(int notificationId, Notification notification); + @Deprecated + default void onNotificationStarted(int notificationId, Notification notification) {} /** * Called after the notification has been cancelled. * * @param notificationId The id of the notification which has been cancelled. + * @deprecated Use {@link #onNotificationCancelled(int, boolean)}. */ - void onNotificationCancelled(int notificationId); + @Deprecated + default void onNotificationCancelled(int notificationId) {} + + /** + * Called after the notification has been cancelled. + * + * @param notificationId The id of the notification which has been cancelled. + * @param dismissedByUser {@code true} if the notification is cancelled because the user + * dismissed the notification. + */ + default void onNotificationCancelled(int notificationId, boolean dismissedByUser) {} + + /** + * Called each time after the notification has been posted. + * + *

    The {@code isPlayerActive} flag indicates whether a service in which the player may run + * needs to be in the foreground. + * + * @param notificationId The id of the notification which has been posted. + * @param notification The {@link Notification}. + * @param isPlayerActive {@code true} if the player is active. + */ + default void onNotificationPosted( + int notificationId, Notification notification, boolean isPlayerActive) {} } /** Receives a {@link Bitmap}. */ @@ -235,7 +267,7 @@ public class PlayerNotificationManager { if (player != null && notificationTag == currentNotificationTag && isNotificationStarted) { - updateNotification(bitmap); + startOrUpdateNotification(bitmap); } }); } @@ -254,10 +286,15 @@ public class PlayerNotificationManager { public static final String ACTION_FAST_FORWARD = "com.google.android.exoplayer.ffwd"; /** The action which rewinds. */ public static final String ACTION_REWIND = "com.google.android.exoplayer.rewind"; - /** The action which cancels the notification and stops playback. */ + /** The action which stops playback. */ public static final String ACTION_STOP = "com.google.android.exoplayer.stop"; /** The extra key of the instance id of the player notification manager. */ public static final String EXTRA_INSTANCE_ID = "INSTANCE_ID"; + /** + * The action which is executed when the notification is dismissed. It cancels the notification + * and calls {@link NotificationListener#onNotificationCancelled(int, boolean)}. + */ + private static final String ACTION_DISMISS = "com.google.android.exoplayer.dismiss"; /** * Visibility of notification on the lock screen. One of {@link @@ -311,6 +348,7 @@ public class PlayerNotificationManager { private final NotificationBroadcastReceiver notificationBroadcastReceiver; private final Map playbackActions; private final Map customActions; + private final PendingIntent dismissPendingIntent; private final int instanceId; private final Timeline.Window window; @@ -323,8 +361,7 @@ public class PlayerNotificationManager { private @Nullable MediaSessionCompat.Token mediaSessionToken; private boolean useNavigationActions; private boolean usePlayPauseActions; - private @Nullable String stopAction; - private @Nullable PendingIntent stopPendingIntent; + private boolean useStopAction; private long fastForwardMs; private long rewindMs; private int badgeIconType; @@ -519,7 +556,6 @@ public class PlayerNotificationManager { priority = NotificationCompat.PRIORITY_LOW; fastForwardMs = DEFAULT_FAST_FORWARD_MS; rewindMs = DEFAULT_REWIND_MS; - stopAction = ACTION_STOP; badgeIconType = NotificationCompat.BADGE_ICON_SMALL; visibility = NotificationCompat.VISIBILITY_PUBLIC; @@ -535,7 +571,8 @@ public class PlayerNotificationManager { for (String action : customActions.keySet()) { intentFilter.addAction(action); } - stopPendingIntent = Assertions.checkNotNull(playbackActions.get(ACTION_STOP)).actionIntent; + dismissPendingIntent = createBroadcastIntent(ACTION_DISMISS, context, instanceId); + intentFilter.addAction(ACTION_DISMISS); } /** @@ -562,7 +599,7 @@ public class PlayerNotificationManager { if (this.player != null) { this.player.removeListener(playerListener); if (player == null) { - stopNotification(); + stopNotification(/* dismissedByUser= */ false); } } this.player = player; @@ -570,9 +607,7 @@ public class PlayerNotificationManager { wasPlayWhenReady = player.getPlayWhenReady(); lastPlaybackState = player.getPlaybackState(); player.addListener(playerListener); - if (lastPlaybackState != Player.STATE_IDLE) { - startOrUpdateNotification(); - } + startOrUpdateNotification(); } } @@ -664,24 +699,15 @@ public class PlayerNotificationManager { } /** - * Sets the name of the action to be used as stop action to cancel the notification. If {@code - * null} is passed the stop action is not displayed. + * Sets whether the stop action should be used. * - * @param stopAction The name of the stop action which must be {@link #ACTION_STOP} or an action - * provided by the {@link CustomActionReceiver}. {@code null} to omit the stop action. + * @param useStopAction Whether to use the stop action. */ - public final void setStopAction(@Nullable String stopAction) { - if (Util.areEqual(stopAction, this.stopAction)) { + public final void setUseStopAction(boolean useStopAction) { + if (this.useStopAction == useStopAction) { return; } - this.stopAction = stopAction; - if (ACTION_STOP.equals(stopAction)) { - stopPendingIntent = Assertions.checkNotNull(playbackActions.get(ACTION_STOP)).actionIntent; - } else if (stopAction != null) { - stopPendingIntent = Assertions.checkNotNull(customActions.get(stopAction)).actionIntent; - } else { - stopPendingIntent = null; - } + this.useStopAction = useStopAction; invalidate(); } @@ -864,36 +890,50 @@ public class PlayerNotificationManager { /** Forces an update of the notification if already started. */ public void invalidate() { if (isNotificationStarted && player != null) { - updateNotification(null); + startOrUpdateNotification(); } } + @Nullable + private Notification startOrUpdateNotification() { + return player != null ? startOrUpdateNotification(/* bitmap= */ null) : null; + } + @RequiresNonNull("player") - private Notification updateNotification(@Nullable Bitmap bitmap) { + @Nullable + private Notification startOrUpdateNotification(@Nullable Bitmap bitmap) { Notification notification = createNotification(player, bitmap); + if (notification == null) { + stopNotification(/* dismissedByUser= */ false); + return null; + } notificationManager.notify(notificationId, notification); + if (!isNotificationStarted) { + isNotificationStarted = true; + context.registerReceiver(notificationBroadcastReceiver, intentFilter); + if (notificationListener != null) { + notificationListener.onNotificationStarted(notificationId, notification); + } + } + NotificationListener listener = notificationListener; + Player player = this.player; + if (listener != null && player != null) { + boolean isPlayerActive = + player.getPlayWhenReady() && player.getPlaybackState() != Player.STATE_IDLE; + listener.onNotificationPosted(notificationId, notification, isPlayerActive); + } return notification; } - private void startOrUpdateNotification() { - if (player != null) { - Notification notification = updateNotification(null); - if (!isNotificationStarted) { - isNotificationStarted = true; - context.registerReceiver(notificationBroadcastReceiver, intentFilter); - if (notificationListener != null) { - notificationListener.onNotificationStarted(notificationId, notification); - } - } - } - } - - private void stopNotification() { + private void stopNotification(boolean dismissedByUser) { if (isNotificationStarted) { - notificationManager.cancel(notificationId); + if (!dismissedByUser) { + notificationManager.cancel(notificationId); + } isNotificationStarted = false; context.unregisterReceiver(notificationBroadcastReceiver); if (notificationListener != null) { + notificationListener.onNotificationCancelled(notificationId, dismissedByUser); notificationListener.onNotificationCancelled(notificationId); } } @@ -904,9 +944,14 @@ public class PlayerNotificationManager { * * @param player The player for which state to build a notification. * @param largeIcon The large icon to be used. - * @return The {@link Notification} which has been built. + * @return The {@link Notification} which has been built, or {@code null} if no notification + * should be displayed. */ + @Nullable protected Notification createNotification(Player player, @Nullable Bitmap largeIcon) { + if (player.getPlaybackState() == Player.STATE_IDLE) { + return null; + } NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId); List actionNames = getActions(player); for (int i = 0; i < actionNames.size(); i++) { @@ -925,14 +970,13 @@ public class PlayerNotificationManager { mediaStyle.setMediaSession(mediaSessionToken); } mediaStyle.setShowActionsInCompactView(getActionIndicesForCompactView(actionNames, player)); - // Configure stop action (eg. when user dismisses the notification when !isOngoing). - boolean useStopAction = stopAction != null; - mediaStyle.setShowCancelButton(useStopAction); - if (useStopAction && stopPendingIntent != null) { - builder.setDeleteIntent(stopPendingIntent); - mediaStyle.setCancelButtonIntent(stopPendingIntent); - } + // Configure dismiss action prior to API 21 ('x' button). + mediaStyle.setShowCancelButton(true); + mediaStyle.setCancelButtonIntent(dismissPendingIntent); + // Set intent which is sent if the user selects 'clear all' + builder.setDeleteIntent(dismissPendingIntent); builder.setStyle(mediaStyle); + // Set notification properties from getters. builder .setBadgeIconType(badgeIconType) @@ -1030,8 +1074,8 @@ public class PlayerNotificationManager { if (customActionReceiver != null) { stringActions.addAll(customActionReceiver.getCustomActions(player)); } - if (ACTION_STOP.equals(stopAction)) { - stringActions.add(stopAction); + if (useStopAction) { + stringActions.add(ACTION_STOP); } return stringActions; } @@ -1176,27 +1220,20 @@ public class PlayerNotificationManager { @Override public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if ((wasPlayWhenReady != playWhenReady && playbackState != Player.STATE_IDLE) - || lastPlaybackState != playbackState) { + if (wasPlayWhenReady != playWhenReady || lastPlaybackState != playbackState) { startOrUpdateNotification(); + wasPlayWhenReady = playWhenReady; + lastPlaybackState = playbackState; } - wasPlayWhenReady = playWhenReady; - lastPlaybackState = playbackState; } @Override public void onTimelineChanged(Timeline timeline, @Nullable Object manifest, int reason) { - if (player == null || player.getPlaybackState() == Player.STATE_IDLE) { - return; - } startOrUpdateNotification(); } @Override public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) { - if (player == null || player.getPlaybackState() == Player.STATE_IDLE) { - return; - } startOrUpdateNotification(); } @@ -1207,9 +1244,6 @@ public class PlayerNotificationManager { @Override public void onRepeatModeChanged(int repeatMode) { - if (player == null || player.getPlaybackState() == Player.STATE_IDLE) { - return; - } startOrUpdateNotification(); } } @@ -1245,8 +1279,9 @@ public class PlayerNotificationManager { } else if (ACTION_NEXT.equals(action)) { next(player); } else if (ACTION_STOP.equals(action)) { - controlDispatcher.dispatchStop(player, true); - stopNotification(); + controlDispatcher.dispatchStop(player, /* reset= */ true); + } else if (ACTION_DISMISS.equals(action)) { + stopNotification(/* dismissedByUser= */ true); } else if (customActionReceiver != null && customActions.containsKey(action)) { customActionReceiver.onCustomAction(player, action, intent); } From f2139d1b7173b38b21111526b9f32bff8d6cdc8b Mon Sep 17 00:00:00 2001 From: eguven Date: Thu, 17 Jan 2019 15:36:25 +0000 Subject: [PATCH 021/110] Add DownloadIndexUtil This class includes helper methods to upgrade ActionFiles and custom download records to DownloadIndex. PiperOrigin-RevId: 229744441 --- .../exoplayer2/offline/DownloadIndexUtil.java | 144 +++++++++++++++++ .../offline/DownloadIndexUtilTest.java | 153 ++++++++++++++++++ 2 files changed, 297 insertions(+) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndexUtil.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadIndexUtilTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndexUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndexUtil.java new file mode 100644 index 0000000000..63602c7641 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndexUtil.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.offline; + +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.offline.DownloadState.State; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; + +/** {@link DownloadIndex} related utility methods. */ +public final class DownloadIndexUtil { + + /** An interface to provide custom download ids during ActionFile upgrade. */ + public interface DownloadIdProvider { + + /** + * Returns a custom download id for given action. + * + * @param downloadAction The action which is an id requested for. + * @return A custom download id for given action. + */ + String getId(DownloadAction downloadAction); + } + + private DownloadIndexUtil() {} + + /** + * Upgrades an {@link ActionFile} to {@link DownloadIndex}. + * + *

    This method shouldn't be called while {@link DownloadIndex} is used by {@link + * DownloadManager}. + * + * @param actionFile The action file to upgrade. + * @param downloadIndex Actions are converted to {@link DownloadState}s and stored in this index. + * @param downloadIdProvider A nullable custom download id provider. + * @throws IOException If there is an error during loading actions. + */ + public static void upgradeActionFile( + ActionFile actionFile, + DownloadIndex downloadIndex, + @Nullable DownloadIdProvider downloadIdProvider) + throws IOException { + if (downloadIdProvider == null) { + downloadIdProvider = downloadAction -> downloadAction.id; + } + for (DownloadAction action : actionFile.load()) { + addAction(downloadIndex, downloadIdProvider.getId(action), action); + } + } + + /** + * Converts a {@link DownloadAction} to {@link DownloadState} and stored in the given {@link + * DownloadIndex}. + * + *

    This method shouldn't be called while {@link DownloadIndex} is used by {@link + * DownloadManager}. + * + * @param downloadIndex The action is converted to {@link DownloadState} and stored in this index. + * @param id A nullable custom download id which overwrites {@link DownloadAction#id}. + * @param action The action to be stored in {@link DownloadIndex}. + */ + public static void addAction( + DownloadIndex downloadIndex, @Nullable String id, DownloadAction action) { + DownloadState downloadState = downloadIndex.getDownloadState(id != null ? id : action.id); + if (downloadState != null) { + downloadState = merge(downloadState, action); + } else { + downloadState = convert(action); + } + downloadIndex.putDownloadState(downloadState); + } + + private static DownloadState merge(DownloadState downloadState, DownloadAction action) { + Assertions.checkArgument(action.type.equals(downloadState.type)); + @State int newState; + if (action.isRemoveAction) { + newState = DownloadState.STATE_REMOVING; + } else { + if (downloadState.state == DownloadState.STATE_REMOVING + || downloadState.state == DownloadState.STATE_RESTARTING) { + newState = DownloadState.STATE_RESTARTING; + } else if (downloadState.state == DownloadState.STATE_STOPPED) { + newState = DownloadState.STATE_STOPPED; + } else { + newState = DownloadState.STATE_QUEUED; + } + } + HashSet keys = new HashSet<>(action.keys); + Collections.addAll(keys, downloadState.streamKeys); + StreamKey[] newKeys = keys.toArray(new StreamKey[0]); + return new DownloadState( + downloadState.id, + downloadState.type, + action.uri, + action.customCacheKey, + newState, + /* downloadPercentage= */ C.PERCENTAGE_UNSET, + downloadState.downloadedBytes, + /* totalBytes= */ C.LENGTH_UNSET, + downloadState.failureReason, + downloadState.stopFlags, + downloadState.startTimeMs, + downloadState.updateTimeMs, + newKeys, + action.data); + } + + private static DownloadState convert(DownloadAction action) { + long currentTimeMs = System.currentTimeMillis(); + return new DownloadState( + action.id, + action.type, + action.uri, + action.customCacheKey, + /* state= */ action.isRemoveAction + ? DownloadState.STATE_REMOVING + : DownloadState.STATE_QUEUED, + /* downloadPercentage= */ C.PERCENTAGE_UNSET, + /* downloadedBytes= */ 0, + /* totalBytes= */ C.LENGTH_UNSET, + DownloadState.FAILURE_REASON_NONE, + /* stopFlags= */ 0, + /* startTimeMs= */ currentTimeMs, + /* updateTimeMs= */ currentTimeMs, + action.keys.toArray(new StreamKey[0]), + action.data); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadIndexUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadIndexUtilTest.java new file mode 100644 index 0000000000..376c840296 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadIndexUtilTest.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.offline; + +import static com.google.android.exoplayer2.offline.DownloadAction.TYPE_DASH; +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import com.google.android.exoplayer2.util.Util; +import java.io.File; +import java.util.Arrays; +import java.util.List; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +/** Unit tests for {@link DownloadIndexUtil}. */ +@RunWith(RobolectricTestRunner.class) +public class DownloadIndexUtilTest { + + private DefaultDownloadIndex downloadIndex; + private File tempFile; + + @Before + public void setUp() throws Exception { + tempFile = Util.createTempFile(RuntimeEnvironment.application, "ExoPlayerTest"); + downloadIndex = new DefaultDownloadIndex(RuntimeEnvironment.application); + } + + @After + public void tearDown() { + downloadIndex.release(); + tempFile.delete(); + } + + @Test + public void addAction_nonExistingDownloadState_createsNewDownloadState() { + byte[] data = new byte[] {1, 2, 3, 4}; + DownloadAction action = + DownloadAction.createDownloadAction( + TYPE_DASH, + Uri.parse("https://www.test.com/download"), + asList( + new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* trackIndex= */ 2), + new StreamKey(/* periodIndex= */ 3, /* groupIndex= */ 4, /* trackIndex= */ 5)), + /* customCacheKey= */ "key123", + data); + + DownloadIndexUtil.addAction(downloadIndex, action.id, action); + + assertDownloadIndexContainsAction(action, DownloadState.STATE_QUEUED); + } + + @Test + public void addAction_existingDownloadState_createsMergedDownloadState() { + StreamKey streamKey1 = + new StreamKey(/* periodIndex= */ 3, /* groupIndex= */ 4, /* trackIndex= */ 5); + StreamKey streamKey2 = + new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* trackIndex= */ 2); + DownloadAction action1 = + DownloadAction.createDownloadAction( + TYPE_DASH, + Uri.parse("https://www.test.com/download1"), + asList(streamKey1), + /* customCacheKey= */ "key123", + new byte[] {1, 2, 3, 4}); + DownloadAction action2 = + DownloadAction.createDownloadAction( + TYPE_DASH, + Uri.parse("https://www.test.com/download2"), + asList(streamKey2), + /* customCacheKey= */ "key123", + new byte[] {5, 4, 3, 2, 1}); + DownloadIndexUtil.addAction(downloadIndex, action1.id, action1); + + DownloadIndexUtil.addAction(downloadIndex, action2.id, action2); + + DownloadState downloadState = downloadIndex.getDownloadState(action2.id); + assertThat(downloadState).isNotNull(); + assertThat(downloadState.type).isEqualTo(action2.type); + assertThat(downloadState.cacheKey).isEqualTo(action2.customCacheKey); + assertThat(downloadState.customMetadata).isEqualTo(action2.data); + assertThat(downloadState.uri).isEqualTo(action2.uri); + assertThat(downloadState.streamKeys).isEqualTo(new StreamKey[] {streamKey2, streamKey1}); + assertThat(downloadState.state).isEqualTo(DownloadState.STATE_QUEUED); + } + + @Test + public void upgradeActionFile_createsDownloadStates() throws Exception { + ActionFile actionFile = new ActionFile(tempFile); + StreamKey streamKey1 = + new StreamKey(/* periodIndex= */ 3, /* groupIndex= */ 4, /* trackIndex= */ 5); + StreamKey streamKey2 = + new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* trackIndex= */ 2); + DownloadAction action1 = + DownloadAction.createDownloadAction( + TYPE_DASH, + Uri.parse("https://www.test.com/download1"), + asList(streamKey1), + /* customCacheKey= */ "key123", + new byte[] {1, 2, 3, 4}); + DownloadAction action2 = + DownloadAction.createDownloadAction( + TYPE_DASH, + Uri.parse("https://www.test.com/download2"), + asList(streamKey2), + /* customCacheKey= */ "key234", + new byte[] {5, 4, 3, 2, 1}); + actionFile.store(action1, action2); + DownloadAction action3 = + DownloadAction.createRemoveAction( + TYPE_DASH, Uri.parse("https://www.test.com/download3"), /* customCacheKey= */ "key345"); + actionFile.store(action1, action2, action3); + + DownloadIndexUtil.upgradeActionFile(actionFile, downloadIndex, /* downloadIdProvider= */ null); + + assertDownloadIndexContainsAction(action1, DownloadState.STATE_QUEUED); + assertDownloadIndexContainsAction(action2, DownloadState.STATE_QUEUED); + assertDownloadIndexContainsAction(action3, DownloadState.STATE_REMOVING); + } + + private void assertDownloadIndexContainsAction(DownloadAction action, int state) { + DownloadState downloadState = downloadIndex.getDownloadState(action.id); + assertThat(downloadState).isNotNull(); + assertThat(downloadState.type).isEqualTo(action.type); + assertThat(downloadState.cacheKey).isEqualTo(action.customCacheKey); + assertThat(downloadState.customMetadata).isEqualTo(action.data); + assertThat(downloadState.uri).isEqualTo(action.uri); + assertThat(downloadState.streamKeys).isEqualTo(action.keys.toArray(new StreamKey[0])); + assertThat(downloadState.state).isEqualTo(state); + } + + @SuppressWarnings("unchecked") + private static List asList(StreamKey... streamKeys) { + return Arrays.asList(streamKeys); + } +} From 76baa5724caad62c3bddd312181cf6362b579c5b Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 17 Jan 2019 16:53:00 +0000 Subject: [PATCH 022/110] solve nullness check warnings with asserts PiperOrigin-RevId: 229755532 --- .../android/exoplayer2/ui/PlayerNotificationManager.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index 103534d8ca..c01aabc9f2 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -896,12 +896,14 @@ public class PlayerNotificationManager { @Nullable private Notification startOrUpdateNotification() { - return player != null ? startOrUpdateNotification(/* bitmap= */ null) : null; + Assertions.checkNotNull(this.player); + return startOrUpdateNotification(/* bitmap= */ null); } @RequiresNonNull("player") @Nullable private Notification startOrUpdateNotification(@Nullable Bitmap bitmap) { + Player player = this.player; Notification notification = createNotification(player, bitmap); if (notification == null) { stopNotification(/* dismissedByUser= */ false); @@ -916,8 +918,7 @@ public class PlayerNotificationManager { } } NotificationListener listener = notificationListener; - Player player = this.player; - if (listener != null && player != null) { + if (listener != null) { boolean isPlayerActive = player.getPlayWhenReady() && player.getPlaybackState() != Player.STATE_IDLE; listener.onNotificationPosted(notificationId, notification, isPlayerActive); From 22413b8037426ed0c40b479aa09680bca40e5df8 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 17 Jan 2019 17:00:11 +0000 Subject: [PATCH 023/110] Add start position to MediaSource.createPeriod. That's the same position set in MediaPeriod.prepare (where it may be removed in the future). Having the position at an earlier point is necessary to fix an issue with lazy preparation in ConcatenatingMediaSource where the prepare position was assumed to be known but MediaPeriod.prepare hasn't been called yet. Issue:#5350 PiperOrigin-RevId: 229756637 --- RELEASENOTES.md | 4 +++ .../exoplayer2/ext/ima/ImaAdsMediaSource.java | 4 +-- .../android/exoplayer2/MediaPeriodHolder.java | 6 ++-- .../source/ClippingMediaSource.java | 4 +-- .../source/ConcatenatingMediaSource.java | 8 +++-- .../source/DeferredMediaPeriod.java | 36 ++++++++++--------- .../source/ExtractorMediaSource.java | 2 +- .../exoplayer2/source/LoopingMediaSource.java | 7 ++-- .../exoplayer2/source/MediaSource.java | 7 ++-- .../exoplayer2/source/MergingMediaSource.java | 4 +-- .../source/SingleSampleMediaSource.java | 2 +- .../exoplayer2/source/ads/AdsMediaSource.java | 8 +++-- .../source/dash/DashMediaSource.java | 3 +- .../exoplayer2/source/hls/HlsMediaSource.java | 2 +- .../source/smoothstreaming/SsMediaSource.java | 2 +- .../exoplayer2/testutil/FakeMediaSource.java | 2 +- .../testutil/MediaSourceTestRunner.java | 19 ++++++++-- 17 files changed, 74 insertions(+), 46 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 96d1fb1ff3..51264dcfe0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -39,6 +39,10 @@ * Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`. * Change signature of `PlayerNotificationManager.NotificationListener` to better fit service requirements. Remove ability to set a custom stop action. +* Add `startPositionUs` to `MediaSource.createPeriod`. This fixes an issue where + using lazy preparation in `ConcatenatingMediaSource` with an + `ExtractorMediaSource` overrides initial seek positions + ([#5350](https://github.com/google/ExoPlayer/issues/5350)). ### 2.9.4 ### diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index 574bac5d35..bcccd6cec7 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -93,8 +93,8 @@ public final class ImaAdsMediaSource extends BaseMediaSource implements SourceIn } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { - return adsMediaSource.createPeriod(id, allocator); + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + return adsMediaSource.createPeriod(id, allocator, startPositionUs); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java index 7becac7b55..19622c6801 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java @@ -89,7 +89,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; this.info = info; sampleStreams = new SampleStream[rendererCapabilities.length]; mayRetainStreamFlags = new boolean[rendererCapabilities.length]; - mediaPeriod = createMediaPeriod(info.id, mediaSource, allocator); + mediaPeriod = createMediaPeriod(info.id, mediaSource, allocator, info.startPositionUs); } /** @@ -399,8 +399,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Returns a media period corresponding to the given {@code id}. */ private static MediaPeriod createMediaPeriod( - MediaPeriodId id, MediaSource mediaSource, Allocator allocator) { - MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator); + MediaPeriodId id, MediaSource mediaSource, Allocator allocator, long startPositionUs) { + MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator, startPositionUs); if (id.endPositionUs != C.TIME_UNSET && id.endPositionUs != C.TIME_END_OF_SOURCE) { mediaPeriod = new ClippingMediaPeriod( diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index 0ab6eeae18..d3b8226822 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -206,10 +206,10 @@ public final class ClippingMediaSource extends CompositeMediaSource { } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { ClippingMediaPeriod mediaPeriod = new ClippingMediaPeriod( - mediaSource.createPeriod(id, allocator), + mediaSource.createPeriod(id, allocator, startPositionUs), enableInitialDiscontinuity, periodStartUs, periodEndUs); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index e6d409c659..6dc7a0a327 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -438,7 +438,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource { } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { if (loopCount == Integer.MAX_VALUE) { - return childSource.createPeriod(id, allocator); + return childSource.createPeriod(id, allocator, startPositionUs); } Object childPeriodUid = LoopingTimeline.getChildPeriodUidFromConcatenatedUid(id.periodUid); MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(childPeriodUid); childMediaPeriodIdToMediaPeriodId.put(childMediaPeriodId, id); - MediaPeriod mediaPeriod = childSource.createPeriod(childMediaPeriodId, allocator); + MediaPeriod mediaPeriod = + childSource.createPeriod(childMediaPeriodId, allocator, startPositionUs); mediaPeriodToChildMediaPeriodId.put(mediaPeriod, childMediaPeriodId); return mediaPeriod; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 2d80fb7f13..1419f9a98f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -34,8 +34,8 @@ import java.io.IOException; * on the {@link SourceInfoRefreshListener}s passed to {@link * #prepareSource(SourceInfoRefreshListener, TransferListener)}. *

  • To provide {@link MediaPeriod} instances for the periods in its timeline. MediaPeriods are - * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator)}, and provide a way for - * the player to load and read the media. + * obtained by calling {@link #createPeriod(MediaPeriodId, Allocator, long)}, and provide a + * way for the player to load and read the media. * * * All methods are called on the player's internal playback thread, as described in the {@link @@ -261,9 +261,10 @@ public interface MediaSource { * * @param id The identifier of the period. * @param allocator An {@link Allocator} from which to obtain media buffer allocations. + * @param startPositionUs The expected start position, in microseconds. * @return A new {@link MediaPeriod}. */ - MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator); + MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs); /** * Releases the period. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index fdeb2b6184..1ea3404e81 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -120,13 +120,13 @@ public final class MergingMediaSource extends CompositeMediaSource { } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { MediaPeriod[] periods = new MediaPeriod[mediaSources.length]; int periodIndex = timelines[0].getIndexOfPeriod(id.periodUid); for (int i = 0; i < periods.length; i++) { MediaPeriodId childMediaPeriodId = id.copyWithPeriodUid(timelines[i].getUidOfPeriod(periodIndex)); - periods[i] = mediaSources[i].createPeriod(childMediaPeriodId, allocator); + periods[i] = mediaSources[i].createPeriod(childMediaPeriodId, allocator, startPositionUs); } return new MergingMediaPeriod(compositeSequenceableLoaderFactory, periods); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 358875eb1e..d13fa06434 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -313,7 +313,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { return new SingleSampleMediaPeriod( dataSpec, dataSourceFactory, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 663f9c64fc..4754466235 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -334,7 +334,7 @@ public final class AdsMediaSource extends CompositeMediaSource { } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { if (adPlaybackState.adGroupCount > 0 && id.isAd()) { int adGroupIndex = id.adGroupIndex; int adIndexInAdGroup = id.adIndexInAdGroup; @@ -353,7 +353,8 @@ public final class AdsMediaSource extends CompositeMediaSource { prepareChildSource(id, adMediaSource); } MediaSource mediaSource = adGroupMediaSources[adGroupIndex][adIndexInAdGroup]; - DeferredMediaPeriod deferredMediaPeriod = new DeferredMediaPeriod(mediaSource, id, allocator); + DeferredMediaPeriod deferredMediaPeriod = + new DeferredMediaPeriod(mediaSource, id, allocator, startPositionUs); deferredMediaPeriod.setPrepareErrorListener( new AdPrepareErrorListener(adUri, adGroupIndex, adIndexInAdGroup)); List mediaPeriods = deferredMediaPeriodByAdMediaSource.get(mediaSource); @@ -369,7 +370,8 @@ public final class AdsMediaSource extends CompositeMediaSource { } return deferredMediaPeriod; } else { - DeferredMediaPeriod mediaPeriod = new DeferredMediaPeriod(contentMediaSource, id, allocator); + DeferredMediaPeriod mediaPeriod = + new DeferredMediaPeriod(contentMediaSource, id, allocator, startPositionUs); mediaPeriod.createPeriod(id); return mediaPeriod; } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index b31b770b03..cfdbdac1ea 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -654,7 +654,8 @@ public final class DashMediaSource extends BaseMediaSource { } @Override - public MediaPeriod createPeriod(MediaPeriodId periodId, Allocator allocator) { + public MediaPeriod createPeriod( + MediaPeriodId periodId, Allocator allocator, long startPositionUs) { int periodIndex = (Integer) periodId.periodUid - firstPeriodId; EventDispatcher periodEventDispatcher = createEventDispatcher(periodId, manifest.getPeriod(periodIndex).startMs); diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index 8058460b9f..f2b76dddc4 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -428,7 +428,7 @@ public final class HlsMediaSource extends BaseMediaSource } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { EventDispatcher eventDispatcher = createEventDispatcher(id); return new HlsMediaPeriod( extractorFactory, diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index 39f707b09c..0f5544a993 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -552,7 +552,7 @@ public final class SsMediaSource extends BaseMediaSource } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { EventDispatcher eventDispatcher = createEventDispatcher(id); SsMediaPeriod period = new SsMediaPeriod( diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index 8165eebaea..de4be82b38 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -112,7 +112,7 @@ public class FakeMediaSource extends BaseMediaSource { } @Override - public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator) { + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { assertThat(preparedSource).isTrue(); assertThat(releasedSource).isFalse(); int periodIndex = timeline.getIndexOfPeriod(id.periodUid); diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java index 5de1ab87b6..9514768416 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaSourceTestRunner.java @@ -132,15 +132,28 @@ public class MediaSourceTestRunner { } /** - * Calls {@link MediaSource#createPeriod(MediaSource.MediaPeriodId, Allocator)} on the playback - * thread, asserting that a non-null {@link MediaPeriod} is returned. + * Calls {@link MediaSource#createPeriod(MediaSource.MediaPeriodId, Allocator, long)} with a zero + * start position on the playback thread, asserting that a non-null {@link MediaPeriod} is + * returned. * * @param periodId The id of the period to create. * @return The created {@link MediaPeriod}. */ public MediaPeriod createPeriod(final MediaPeriodId periodId) { + return createPeriod(periodId, /* startPositionUs= */ 0); + } + + /** + * Calls {@link MediaSource#createPeriod(MediaSource.MediaPeriodId, Allocator, long)} on the + * playback thread, asserting that a non-null {@link MediaPeriod} is returned. + * + * @param periodId The id of the period to create. + * @return The created {@link MediaPeriod}. + */ + public MediaPeriod createPeriod(final MediaPeriodId periodId, long startPositionUs) { final MediaPeriod[] holder = new MediaPeriod[1]; - runOnPlaybackThread(() -> holder[0] = mediaSource.createPeriod(periodId, allocator)); + runOnPlaybackThread( + () -> holder[0] = mediaSource.createPeriod(periodId, allocator, startPositionUs)); assertThat(holder[0]).isNotNull(); return holder[0]; } From ac86d3b5f6bb44b68d673c7a06e4a34bf0202f3c Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 17 Jan 2019 17:08:52 +0000 Subject: [PATCH 024/110] Add missing @Nullable to SimpleExoPlayer fields and methods. Issue:#5402 PiperOrigin-RevId: 229758525 --- .../android/exoplayer2/SimpleExoPlayer.java | 43 +++++++++---------- .../exoplayer2/ui/DebugTextViewHelper.java | 31 ++++++++++--- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index b44259f50b..e498038fde 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -94,25 +94,25 @@ public class SimpleExoPlayer extends BasePlayer private final AudioFocusManager audioFocusManager; - private Format videoFormat; - private Format audioFormat; + @Nullable private Format videoFormat; + @Nullable private Format audioFormat; - private Surface surface; + @Nullable private Surface surface; private boolean ownsSurface; private @C.VideoScalingMode int videoScalingMode; - private SurfaceHolder surfaceHolder; - private TextureView textureView; + @Nullable private SurfaceHolder surfaceHolder; + @Nullable private TextureView textureView; private int surfaceWidth; private int surfaceHeight; - private DecoderCounters videoDecoderCounters; - private DecoderCounters audioDecoderCounters; + @Nullable private DecoderCounters videoDecoderCounters; + @Nullable private DecoderCounters audioDecoderCounters; private int audioSessionId; private AudioAttributes audioAttributes; private float audioVolume; - private MediaSource mediaSource; + @Nullable private MediaSource mediaSource; private List currentCues; - private VideoFrameMetadataListener videoFrameMetadataListener; - private CameraMotionListener cameraMotionListener; + @Nullable private VideoFrameMetadataListener videoFrameMetadataListener; + @Nullable private CameraMotionListener cameraMotionListener; private boolean hasNotifiedFullWrongThreadWarning; /** @@ -558,30 +558,26 @@ public class SimpleExoPlayer extends BasePlayer setPlaybackParameters(playbackParameters); } - /** - * Returns the video format currently being played, or null if no video is being played. - */ + /** Returns the video format currently being played, or null if no video is being played. */ + @Nullable public Format getVideoFormat() { return videoFormat; } - /** - * Returns the audio format currently being played, or null if no audio is being played. - */ + /** Returns the audio format currently being played, or null if no audio is being played. */ + @Nullable public Format getAudioFormat() { return audioFormat; } - /** - * Returns {@link DecoderCounters} for video, or null if no video is being played. - */ + /** Returns {@link DecoderCounters} for video, or null if no video is being played. */ + @Nullable public DecoderCounters getVideoDecoderCounters() { return videoDecoderCounters; } - /** - * Returns {@link DecoderCounters} for audio, or null if no audio is being played. - */ + /** Returns {@link DecoderCounters} for audio, or null if no audio is being played. */ + @Nullable public DecoderCounters getAudioDecoderCounters() { return audioDecoderCounters; } @@ -1053,7 +1049,8 @@ public class SimpleExoPlayer extends BasePlayer } @Override - public @Nullable Object getCurrentManifest() { + @Nullable + public Object getCurrentManifest() { verifyApplicationThread(); return player.getCurrentManifest(); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java index 8c7c507f92..da2081db31 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DebugTextViewHelper.java @@ -137,23 +137,40 @@ public class DebugTextViewHelper implements Player.EventListener, Runnable { /** Returns a string containing video debugging information. */ protected String getVideoString() { Format format = player.getVideoFormat(); - if (format == null) { + DecoderCounters decoderCounters = player.getVideoDecoderCounters(); + if (format == null || decoderCounters == null) { return ""; } - return "\n" + format.sampleMimeType + "(id:" + format.id + " r:" + format.width + "x" - + format.height + getPixelAspectRatioString(format.pixelWidthHeightRatio) - + getDecoderCountersBufferCountString(player.getVideoDecoderCounters()) + ")"; + return "\n" + + format.sampleMimeType + + "(id:" + + format.id + + " r:" + + format.width + + "x" + + format.height + + getPixelAspectRatioString(format.pixelWidthHeightRatio) + + getDecoderCountersBufferCountString(decoderCounters) + + ")"; } /** Returns a string containing audio debugging information. */ protected String getAudioString() { Format format = player.getAudioFormat(); - if (format == null) { + DecoderCounters decoderCounters = player.getAudioDecoderCounters(); + if (format == null || decoderCounters == null) { return ""; } - return "\n" + format.sampleMimeType + "(id:" + format.id + " hz:" + format.sampleRate + " ch:" + return "\n" + + format.sampleMimeType + + "(id:" + + format.id + + " hz:" + + format.sampleRate + + " ch:" + format.channelCount - + getDecoderCountersBufferCountString(player.getAudioDecoderCounters()) + ")"; + + getDecoderCountersBufferCountString(decoderCounters) + + ")"; } private static String getDecoderCountersBufferCountString(DecoderCounters counters) { From ef24125d20b3092c270299e092bdd2f7994bc7e8 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 18 Jan 2019 00:44:17 +0000 Subject: [PATCH 025/110] Fix release notes for 2.9.4 PiperOrigin-RevId: 229841782 --- RELEASENOTES.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 51264dcfe0..061a095183 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -39,10 +39,6 @@ * Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`. * Change signature of `PlayerNotificationManager.NotificationListener` to better fit service requirements. Remove ability to set a custom stop action. -* Add `startPositionUs` to `MediaSource.createPeriod`. This fixes an issue where - using lazy preparation in `ConcatenatingMediaSource` with an - `ExtractorMediaSource` overrides initial seek positions - ([#5350](https://github.com/google/ExoPlayer/issues/5350)). ### 2.9.4 ### @@ -60,8 +56,12 @@ ([#5351](https://github.com/google/ExoPlayer/issues/5351)). * Fix issue where uneven track durations in MP4 streams can cause OOM problems ([#3670](https://github.com/google/ExoPlayer/issues/3670)). -* Add the sub text to the MediaDescriptionAdapter of the - PlayerNotificationManager. +* Add `startPositionUs` to `MediaSource.createPeriod`. This fixes an issue where + using lazy preparation in `ConcatenatingMediaSource` with an + `ExtractorMediaSource` overrides initial seek positions + ([#5350](https://github.com/google/ExoPlayer/issues/5350)). +* Add subtext to the `MediaDescriptionAdapter` of the + `PlayerNotificationManager`. ### 2.9.3 ### From 23c07e5c4d1077e67087fa80d8b27d571cf5a60b Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 18 Jan 2019 14:57:10 +0000 Subject: [PATCH 026/110] Update buffer-based ABR to not select track in constructor. This mimicks a similar change in the default AdaptiveTrackSelection. Also adds an option to cap initial format height. PiperOrigin-RevId: 229923149 --- .../BufferSizeAdaptationBuilder.java | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java index 5c8350cb1d..ee1d1c62da 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/BufferSizeAdaptationBuilder.java @@ -43,7 +43,7 @@ public final class BufferSizeAdaptationBuilder { public interface DynamicFormatFilter { /** Filter which allows all formats. */ - DynamicFormatFilter NO_FILTER = (format, trackBitrate) -> true; + DynamicFormatFilter NO_FILTER = (format, trackBitrate, isInitialSelection) -> true; /** * Called when updating the selected track to determine whether a candidate track is allowed. If @@ -52,8 +52,9 @@ public final class BufferSizeAdaptationBuilder { * @param format The {@link Format} of the candidate track. * @param trackBitrate The estimated bitrate of the track. May differ from {@link * Format#bitrate} if a more accurate estimate of the current track bitrate is available. + * @param isInitialSelection Whether this is for the initial track selection. */ - boolean isFormatAllowed(Format format, int trackBitrate); + boolean isFormatAllowed(Format format, int trackBitrate, boolean isInitialSelection); } /** @@ -344,7 +345,7 @@ public final class BufferSizeAdaptationBuilder { formatBitrates = new int[length]; maxBitrate = getFormat(/* index= */ 0).bitrate; minBitrate = getFormat(/* index= */ length - 1).bitrate; - selectionReason = C.SELECTION_REASON_INITIAL; + selectionReason = C.SELECTION_REASON_UNKNOWN; playbackSpeed = 1.0f; // We use a log-linear function to map from bitrate to buffer size: @@ -354,9 +355,6 @@ public final class BufferSizeAdaptationBuilder { (maxBufferUs - hysteresisBufferUs - minBufferUs) / Math.log(maxBitrate / minBitrate); bitrateToBufferFunctionIntercept = minBufferUs - bitrateToBufferFunctionSlope * Math.log(minBitrate); - - updateFormatBitrates(/* nowMs= */ Long.MIN_VALUE); - selectedIndex = selectIdealIndexUsingBandwidth(); } @Override @@ -393,6 +391,14 @@ public final class BufferSizeAdaptationBuilder { List queue, MediaChunkIterator[] mediaChunkIterators) { updateFormatBitrates(/* nowMs= */ clock.elapsedRealtime()); + + // Make initial selection + if (selectionReason == C.SELECTION_REASON_UNKNOWN) { + selectionReason = C.SELECTION_REASON_INITIAL; + selectedIndex = selectIdealIndexUsingBandwidth(/* isInitialSelection= */ true); + return; + } + long bufferUs = getCurrentPeriodBufferedDurationUs(playbackPositionUs, bufferedDurationUs); int oldSelectedIndex = selectedIndex; if (isInSteadyState) { @@ -428,7 +434,8 @@ public final class BufferSizeAdaptationBuilder { for (int i = 0; i < formatBitrates.length; i++) { if (formatBitrates[i] != BITRATE_BLACKLISTED) { if (getTargetBufferForBitrateUs(formatBitrates[i]) < bufferUs - && dynamicFormatFilter.isFormatAllowed(getFormat(i), formatBitrates[i])) { + && dynamicFormatFilter.isFormatAllowed( + getFormat(i), formatBitrates[i], /* isInitialSelection= */ false)) { return i; } lowestBitrateNonBlacklistedIndex = i; @@ -440,7 +447,7 @@ public final class BufferSizeAdaptationBuilder { // Startup. private void selectIndexStartUpPhase(long bufferUs) { - int startUpSelectedIndex = selectIdealIndexUsingBandwidth(); + int startUpSelectedIndex = selectIdealIndexUsingBandwidth(/* isInitialSelection= */ false); int steadyStateSelectedIndex = selectIdealIndexUsingBufferSize(bufferUs); if (steadyStateSelectedIndex <= selectedIndex) { // Switch to steady state if we have enough buffer to maintain current selection. @@ -457,14 +464,15 @@ public final class BufferSizeAdaptationBuilder { } } - private int selectIdealIndexUsingBandwidth() { + private int selectIdealIndexUsingBandwidth(boolean isInitialSelection) { long effectiveBitrate = (long) (bandwidthMeter.getBitrateEstimate() * startUpBandwidthFraction); int lowestBitrateNonBlacklistedIndex = 0; for (int i = 0; i < formatBitrates.length; i++) { if (formatBitrates[i] != BITRATE_BLACKLISTED) { if (Math.round(formatBitrates[i] * playbackSpeed) <= effectiveBitrate - && dynamicFormatFilter.isFormatAllowed(getFormat(i), formatBitrates[i])) { + && dynamicFormatFilter.isFormatAllowed( + getFormat(i), formatBitrates[i], isInitialSelection)) { return i; } lowestBitrateNonBlacklistedIndex = i; From 310925ca129449b67f79167f0e9d2875f01438ff Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 18 Jan 2019 16:21:57 +0000 Subject: [PATCH 027/110] Upgrade to GVR SDK 1.190.0 Change the dependency to the new monolithic GVR SDK target. PiperOrigin-RevId: 229931549 --- RELEASENOTES.md | 1 + extensions/gvr/build.gradle | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 061a095183..f45bd03dec 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -48,6 +48,7 @@ ([#5378](https://github.com/google/ExoPlayer/issues/5378)). * FFmpeg extension: Treat invalid data errors as non-fatal to match the behavior of MediaCodec ([#5293](https://github.com/google/ExoPlayer/issues/5293)). +* GVR extension: upgrade GVR SDK dependency to 1.190.0. * Fix issue where sending callbacks for playlist changes may cause problems because of parallel player access ([#5240](https://github.com/google/ExoPlayer/issues/5240)). diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index c845cb3423..ba6bc8afda 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -33,8 +33,7 @@ dependencies { implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') implementation 'com.android.support:support-annotations:' + supportLibraryVersion - implementation 'com.google.vr:sdk-audio:1.80.0' - implementation 'com.google.vr:sdk-controller:1.80.0' + implementation 'com.google.vr:sdk-base:1.190.0' api 'com.google.vr:sdk-base:1.80.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion } From 6a55fda66d5c51b75497bd940f138e3a82b674e0 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 18 Jan 2019 16:27:53 +0000 Subject: [PATCH 028/110] Add DRM protected content to the Cast demo APP Allows testing DRM-protected content in the receiver app. Includes the DRM-related data to the media items, but does not add support for DRM content in the local player yet. PiperOrigin-RevId: 229932329 --- .../android/exoplayer2/castdemo/DemoUtil.java | 35 ++++++++++++++++--- .../exoplayer2/castdemo/MainActivity.java | 20 +++++++---- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java index 776ed1a3bd..e45ceb7c83 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java @@ -15,10 +15,14 @@ */ package com.google.android.exoplayer2.castdemo; +import android.net.Uri; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.MimeTypes; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.UUID; /** Utility methods and constants for the Cast demo application. */ /* package */ final class DemoUtil { @@ -32,6 +36,16 @@ import java.util.List; public final String name; /** The mime type of the sample media content. */ public final String mimeType; + /** + * The {@link UUID} of the DRM scheme that protects the content, or null if the content is not + * DRM-protected. + */ + @Nullable public final UUID drmSchemeUuid; + /** + * The url from which players should obtain DRM licenses, or null if the content is not + * DRM-protected. + */ + @Nullable public final Uri licenseServerUri; /** * @param uri See {@link #uri}. @@ -39,9 +53,21 @@ import java.util.List; * @param mimeType See {@link #mimeType}. */ public Sample(String uri, String name, String mimeType) { + this(uri, name, mimeType, /* drmSchemeUuid= */ null, /* licenseServerUriString= */ null); + } + + public Sample( + String uri, + String name, + String mimeType, + @Nullable UUID drmSchemeUuid, + @Nullable String licenseServerUriString) { this.uri = uri; this.name = name; this.mimeType = mimeType; + this.drmSchemeUuid = drmSchemeUuid; + this.licenseServerUri = + licenseServerUriString != null ? Uri.parse(licenseServerUriString) : null; } @Override @@ -65,22 +91,23 @@ import java.util.List; samples.add( new Sample( "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd", - "DASH (clear,MP4,H264)", + "Clear DASH: Tears", MIME_TYPE_DASH)); samples.add( new Sample( "https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/" + "hls/TearsOfSteel.m3u8", - "Tears of Steel (HLS)", + "Clear HLS: Tears of Steel", MIME_TYPE_HLS)); samples.add( new Sample( "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3" + "/bipbop_4x3_variant.m3u8", - "HLS Basic (TS)", + "Clear HLS: Basic 4x3", MIME_TYPE_HLS)); samples.add( - new Sample("https://html5demos.com/assets/dizzy.mp4", "Dizzy (MP4)", MIME_TYPE_VIDEO_MP4)); + new Sample( + "https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4)); SAMPLES = Collections.unmodifiableList(samples); } diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 9d4c3ec87f..058adf7c9c 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -42,6 +42,7 @@ import com.google.android.gms.cast.CastMediaControlIntent; import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.gms.cast.framework.CastContext; import com.google.android.gms.dynamite.DynamiteModule; +import java.util.Collections; /** * An activity that plays video using {@link SimpleExoPlayer} and supports casting using ExoPlayer's @@ -182,13 +183,18 @@ public class MainActivity extends AppCompatActivity sampleList.setOnItemClickListener( (parent, view, position, id) -> { DemoUtil.Sample sample = DemoUtil.SAMPLES.get(position); - playerManager.addItem( - mediaItemBuilder - .clear() - .setMedia(sample.uri) - .setTitle(sample.name) - .setMimeType(sample.mimeType) - .build()); + mediaItemBuilder + .clear() + .setMedia(sample.uri) + .setTitle(sample.name) + .setMimeType(sample.mimeType); + if (sample.drmSchemeUuid != null) { + mediaItemBuilder.setDrmSchemes( + Collections.singletonList( + new MediaItem.DrmScheme( + sample.drmSchemeUuid, new MediaItem.UriBundle(sample.licenseServerUri)))); + } + playerManager.addItem(mediaItemBuilder.build()); mediaQueueListAdapter.notifyItemInserted(playerManager.getMediaQueueSize() - 1); }); return dialogList; From 14eb561e3818579ab77ff727b612f0f9b55dcecc Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 18 Jan 2019 16:39:44 +0000 Subject: [PATCH 029/110] Use MediaCrypto.setMediaDrmSession to avoid black flicker Issue: #3561 PiperOrigin-RevId: 229934093 --- RELEASENOTES.md | 2 + .../mediacodec/MediaCodecRenderer.java | 165 +++++++++++++----- .../video/MediaCodecVideoRenderer.java | 2 +- 3 files changed, 128 insertions(+), 41 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f45bd03dec..cfded4c9d6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -28,6 +28,8 @@ * Rename TaskState to DownloadState. * Add new states to DownloadState. * Replace DownloadState.action with DownloadAction fields. +* DRM: Fix black flicker when keys rotate in DRM protected content + ([#3561](https://github.com/google/ExoPlayer/issues/3561)). * Add support for SHOUTcast ICY metadata ([#3735](https://github.com/google/ExoPlayer/issues/3735)). * CEA-608: Improved conformance to the specification diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 77d3b31ab7..35f5c14f3f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -240,14 +240,21 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({DRAIN_ACTION_NONE, DRAIN_ACTION_FLUSH, DRAIN_ACTION_REINITIALIZE}) + @IntDef({ + DRAIN_ACTION_NONE, + DRAIN_ACTION_FLUSH, + DRAIN_ACTION_UPDATE_DRM_SESSION, + DRAIN_ACTION_REINITIALIZE + }) private @interface DrainAction {} /** No special action should be taken. */ private static final int DRAIN_ACTION_NONE = 0; /** The codec should be flushed. */ private static final int DRAIN_ACTION_FLUSH = 1; - /** The codec should be re-initialized. */ - private static final int DRAIN_ACTION_REINITIALIZE = 2; + /** The codec should be flushed and updated to use the pending DRM session. */ + private static final int DRAIN_ACTION_UPDATE_DRM_SESSION = 2; + /** The codec should be reinitialized. */ + private static final int DRAIN_ACTION_REINITIALIZE = 3; @Documented @Retention(RetentionPolicy.SOURCE) @@ -547,7 +554,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { inputStreamEnded = false; outputStreamEnded = false; - flushOrReinitCodec(); + flushOrReinitializeCodec(); formatQueue.clear(); } @@ -679,12 +686,15 @@ public abstract class MediaCodecRenderer extends BaseRenderer { *

    The implementation of this method calls {@link #flushOrReleaseCodec()}, and {@link * #maybeInitCodec()} if the codec needs to be re-instantiated. * + * @return Whether the codec was released and reinitialized, rather than being flushed. * @throws ExoPlaybackException If an error occurs re-instantiating the codec. */ - protected final void flushOrReinitCodec() throws ExoPlaybackException { - if (flushOrReleaseCodec()) { + protected final boolean flushOrReinitializeCodec() throws ExoPlaybackException { + boolean released = flushOrReleaseCodec(); + if (released) { maybeInitCodec(); } + return released; } /** @@ -1163,40 +1173,58 @@ public abstract class MediaCodecRenderer extends BaseRenderer { // We have an existing codec that we may need to reconfigure or re-initialize. If the existing // codec instance is being kept then its operating rate may need to be updated. - if (sourceDrmSession != codecDrmSession) { + + if ((sourceDrmSession == null && codecDrmSession != null) + || (sourceDrmSession != null && codecDrmSession == null) + || (sourceDrmSession != null && !codecInfo.secure) + || (Util.SDK_INT < 23 && sourceDrmSession != codecDrmSession)) { + // We might need to switch between the clear and protected output paths, or we're using DRM + // prior to API level 23 where the codec needs to be re-initialized to switch to the new DRM + // session. drainAndReinitializeCodec(); - } else { - switch (canKeepCodec(codec, codecInfo, codecFormat, newFormat)) { - case KEEP_CODEC_RESULT_NO: - drainAndReinitializeCodec(); - break; - case KEEP_CODEC_RESULT_YES_WITH_FLUSH: + return; + } + + switch (canKeepCodec(codec, codecInfo, codecFormat, newFormat)) { + case KEEP_CODEC_RESULT_NO: + drainAndReinitializeCodec(); + break; + case KEEP_CODEC_RESULT_YES_WITH_FLUSH: + codecFormat = newFormat; + updateCodecOperatingRate(); + if (sourceDrmSession != codecDrmSession) { + drainAndUpdateCodecDrmSession(); + } else { drainAndFlushCodec(); + } + break; + case KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION: + if (codecNeedsReconfigureWorkaround) { + drainAndReinitializeCodec(); + } else { + codecReconfigured = true; + codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; + codecNeedsAdaptationWorkaroundBuffer = + codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_ALWAYS + || (codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION + && newFormat.width == codecFormat.width + && newFormat.height == codecFormat.height); codecFormat = newFormat; updateCodecOperatingRate(); - break; - case KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION: - if (codecNeedsReconfigureWorkaround) { - drainAndReinitializeCodec(); - } else { - codecReconfigured = true; - codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; - codecNeedsAdaptationWorkaroundBuffer = - codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_ALWAYS - || (codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION - && newFormat.width == codecFormat.width - && newFormat.height == codecFormat.height); - codecFormat = newFormat; - updateCodecOperatingRate(); + if (sourceDrmSession != codecDrmSession) { + drainAndUpdateCodecDrmSession(); } - break; - case KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION: - codecFormat = newFormat; - updateCodecOperatingRate(); - break; - default: - throw new IllegalStateException(); // Never happens. - } + } + break; + case KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION: + codecFormat = newFormat; + updateCodecOperatingRate(); + if (sourceDrmSession != codecDrmSession) { + drainAndUpdateCodecDrmSession(); + } + break; + default: + throw new IllegalStateException(); // Never happens. } } @@ -1331,6 +1359,27 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } } + /** + * Starts draining the codec to update its DRM session. The update may occur immediately if no + * buffers have been queued to the codec. + * + * @throws ExoPlaybackException If an error occurs updating the codec's DRM session. + */ + private void drainAndUpdateCodecDrmSession() throws ExoPlaybackException { + if (Util.SDK_INT < 23) { + // The codec needs to be re-initialized to switch to the source DRM session. + drainAndReinitializeCodec(); + return; + } + if (codecReceivedBuffers) { + codecDrainState = DRAIN_STATE_SIGNAL_END_OF_STREAM; + codecDrainAction = DRAIN_ACTION_UPDATE_DRM_SESSION; + } else { + // Nothing has been queued to the decoder, so we can do the update immediately. + updateDrmSessionOrReinitializeCodecV23(); + } + } + /** * Starts draining the codec for re-initialization. Re-initialization may occur immediately if no * buffers have been queued to the codec. @@ -1343,8 +1392,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecDrainAction = DRAIN_ACTION_REINITIALIZE; } else { // Nothing has been queued to the decoder, so we can re-initialize immediately. - releaseCodec(); - maybeInitCodec(); + reinitializeCodec(); } } @@ -1548,11 +1596,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private void processEndOfStream() throws ExoPlaybackException { switch (codecDrainAction) { case DRAIN_ACTION_REINITIALIZE: - releaseCodec(); - maybeInitCodec(); + reinitializeCodec(); + break; + case DRAIN_ACTION_UPDATE_DRM_SESSION: + updateDrmSessionOrReinitializeCodecV23(); break; case DRAIN_ACTION_FLUSH: - flushOrReinitCodec(); + flushOrReinitializeCodec(); break; case DRAIN_ACTION_NONE: default: @@ -1562,6 +1612,41 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } } + private void reinitializeCodec() throws ExoPlaybackException { + releaseCodec(); + maybeInitCodec(); + } + + @TargetApi(23) + private void updateDrmSessionOrReinitializeCodecV23() throws ExoPlaybackException { + FrameworkMediaCrypto sessionMediaCrypto = sourceDrmSession.getMediaCrypto(); + if (sessionMediaCrypto == null) { + // We'd only expect this to happen if the CDM from which the pending session is obtained needs + // provisioning. This is unlikely to happen (it probably requires a switch from one DRM scheme + // to another, where the new CDM hasn't been used before and needs provisioning). It would be + // possible to handle this case more efficiently (i.e. with a new renderer state that waits + // for provisioning to finish and then calls mediaCrypto.setMediaDrmSession), but the extra + // complexity is not warranted given how unlikely the case is to occur. + reinitializeCodec(); + return; + } + + if (flushOrReinitializeCodec()) { + // The codec was reinitialized. The new codec will be using the new DRM session, so there's + // nothing more to do. + return; + } + + try { + mediaCrypto.setMediaDrmSession(sessionMediaCrypto.sessionId); + } catch (MediaCryptoException e) { + throw ExoPlaybackException.createForRenderer(e, getIndex()); + } + setCodecDrmSession(sourceDrmSession); + codecDrainState = DRAIN_STATE_NONE; + codecDrainAction = DRAIN_ACTION_NONE; + } + private boolean shouldSkipOutputBuffer(long presentationTimeUs) { // We avoid using decodeOnlyPresentationTimestamps.remove(presentationTimeUs) because it would // box presentationTimeUs, creating a Long object that would need to be garbage collected. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index b92dd44eb2..9084547d0f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -867,7 +867,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // We dropped some buffers to catch up, so update the decoder counters and flush the codec, // which releases all pending buffers buffers including the current output buffer. updateDroppedBufferCounters(buffersInCodecCount + droppedSourceBufferCount); - flushOrReinitCodec(); + flushOrReinitializeCodec(); return true; } From 57ed00873ff948370d38901b5b0fd73992abfc7e Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 18 Jan 2019 16:41:06 +0000 Subject: [PATCH 030/110] bazel build and workspace to build cast receiver PiperOrigin-RevId: 229934333 --- .gitignore | 9 +++++++++ .hgignore | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/.gitignore b/.gitignore index db5a8c4305..4731d5ba99 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,12 @@ local.properties proguard.cfg proguard-project.txt +# Bazel +bazel-bin +bazel-genfiles +bazel-out +bazel-testlogs + # Other .DS_Store cmake-build-debug @@ -66,3 +72,6 @@ extensions/cronet/jniLibs/* extensions/cronet/libs/* !extensions/cronet/libs/README.md +# Cast receiver +cast_receiver_app/external-js +cast_receiver_app/bazel-cast_receiver_app diff --git a/.hgignore b/.hgignore index f7c3656f65..36d3268005 100644 --- a/.hgignore +++ b/.hgignore @@ -44,6 +44,12 @@ local.properties proguard.cfg proguard-project.txt +# Bazel +bazel-bin +bazel-genfiles +bazel-out +bazel-testlogs + # Other .DS_Store cmake-build-debug @@ -69,3 +75,7 @@ extensions/cronet/jniLibs/* !extensions/cronet/jniLibs/README.md extensions/cronet/libs/* !extensions/cronet/libs/README.md + +# Cast receiver +cast_receiver_app/external-js +cast_receiver_app/bazel-cast_receiver_app From 02dc937c78370240ffa763e9b5c89fa002a36cbd Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 18 Jan 2019 16:44:29 +0000 Subject: [PATCH 031/110] Fix flaky DownloadIndexUtilTest. PiperOrigin-RevId: 229934901 --- .../android/exoplayer2/offline/DownloadIndexUtilTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadIndexUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadIndexUtilTest.java index 376c840296..bc93b40265 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadIndexUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadIndexUtilTest.java @@ -97,7 +97,7 @@ public class DownloadIndexUtilTest { assertThat(downloadState.cacheKey).isEqualTo(action2.customCacheKey); assertThat(downloadState.customMetadata).isEqualTo(action2.data); assertThat(downloadState.uri).isEqualTo(action2.uri); - assertThat(downloadState.streamKeys).isEqualTo(new StreamKey[] {streamKey2, streamKey1}); + assertThat(Arrays.asList(downloadState.streamKeys)).containsExactly(streamKey1, streamKey2); assertThat(downloadState.state).isEqualTo(DownloadState.STATE_QUEUED); } @@ -142,7 +142,7 @@ public class DownloadIndexUtilTest { assertThat(downloadState.cacheKey).isEqualTo(action.customCacheKey); assertThat(downloadState.customMetadata).isEqualTo(action.data); assertThat(downloadState.uri).isEqualTo(action.uri); - assertThat(downloadState.streamKeys).isEqualTo(action.keys.toArray(new StreamKey[0])); + assertThat(Arrays.asList(downloadState.streamKeys)).containsExactlyElementsIn(action.keys); assertThat(downloadState.state).isEqualTo(state); } From 82da627c1a2273021fc1df027e879db71907b630 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 18 Jan 2019 18:03:15 +0000 Subject: [PATCH 032/110] Split out shared database components for reuse in caching PiperOrigin-RevId: 229946997 --- .../exoplayer2/database/DatabaseProvider.java | 56 +++++ .../database/DefaultDatabaseProvider.java | 42 ++++ .../database/ExoDatabaseProvider.java | 55 +++++ .../exoplayer2/database/VersionTable.java | 116 ++++++++++ .../offline/DefaultDownloadIndex.java | 213 ++---------------- .../exoplayer2/offline/DownloadIndex.java | 2 - .../exoplayer2/database/VersionTableTest.java | 78 +++++++ .../offline/DefaultDownloadIndexTest.java | 173 +++----------- .../offline/DownloadIndexUtilTest.java | 9 +- 9 files changed, 406 insertions(+), 338 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/database/DatabaseProvider.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/database/DefaultDatabaseProvider.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/database/VersionTableTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/database/DatabaseProvider.java b/library/core/src/main/java/com/google/android/exoplayer2/database/DatabaseProvider.java new file mode 100644 index 0000000000..2bb5f260ba --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/database/DatabaseProvider.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2018 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.database; + +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; + +/** + * Provides {@link SQLiteDatabase} instances to ExoPlayer components, which may read and write + * tables prefixed with {@link #TABLE_PREFIX}. + */ +public interface DatabaseProvider { + + /** Prefix for tables that can be read and written by ExoPlayer components. */ + String TABLE_PREFIX = "ExoPlayer"; + + /** + * Creates and/or opens a database that will be used for reading and writing. + * + *

    Once opened successfully, the database is cached, so you can call this method every time you + * need to write to the database. Errors such as bad permissions or a full disk may cause this + * method to fail, but future attempts may succeed if the problem is fixed. + * + * @throws SQLiteException If the database cannot be opened for writing. + * @return A read/write database object. + */ + SQLiteDatabase getWritableDatabase(); + + /** + * Creates and/or opens a database. This will be the same object returned by {@link + * #getWritableDatabase()} unless some problem, such as a full disk, requires the database to be + * opened read-only. In that case, a read-only database object will be returned. If the problem is + * fixed, a future call to {@link #getWritableDatabase()} may succeed, in which case the read-only + * database object will be closed and the read/write object will be returned in the future. + * + *

    Once opened successfully, the database is cached, so you can call this method every time you + * need to read from the database. + * + * @throws SQLiteException If the database cannot be opened. + * @return A database object valid until {@link #getWritableDatabase()} is called. + */ + SQLiteDatabase getReadableDatabase(); +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/database/DefaultDatabaseProvider.java b/library/core/src/main/java/com/google/android/exoplayer2/database/DefaultDatabaseProvider.java new file mode 100644 index 0000000000..c04683b434 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/database/DefaultDatabaseProvider.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2018 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.database; + +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +/** A {@link DatabaseProvider} that provides instances obtained from a {@link SQLiteOpenHelper}. */ +public final class DefaultDatabaseProvider implements DatabaseProvider { + + private final SQLiteOpenHelper sqliteOpenHelper; + + /** + * @param sqliteOpenHelper An {@link SQLiteOpenHelper} from which to obtain database instances. + */ + public DefaultDatabaseProvider(SQLiteOpenHelper sqliteOpenHelper) { + this.sqliteOpenHelper = sqliteOpenHelper; + } + + @Override + public SQLiteDatabase getWritableDatabase() { + return sqliteOpenHelper.getWritableDatabase(); + } + + @Override + public SQLiteDatabase getReadableDatabase() { + return sqliteOpenHelper.getReadableDatabase(); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java b/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java new file mode 100644 index 0000000000..b64bad2ad6 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2018 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.database; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +/** + * An {@link SQLiteOpenHelper} that provides instances of a standalone ExoPlayer database. + * + *

    Suitable for use by applications that do not already have their own database, or which would + * prefer to keep ExoPlayer tables isolated in their own database. Other applications should prefer + * to use {@link DefaultDatabaseProvider} with their own {@link SQLiteOpenHelper}. + */ +public final class ExoDatabaseProvider extends SQLiteOpenHelper implements DatabaseProvider { + + /** The file name used for the standalone ExoPlayer database. */ + public static final String DATABASE_NAME = "exoplayer_internal.db"; + + private static final int VERSION = 1; + + public ExoDatabaseProvider(Context context) { + super(context.getApplicationContext(), DATABASE_NAME, /* factory= */ null, VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + // Features create their own tables. + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // Features handle their own upgrades. + } + + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + // TODO: Wipe the database. + super.onDowngrade(db, oldVersion, newVersion); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java b/library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java new file mode 100644 index 0000000000..0b6ef3d816 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2018 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.database; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.support.annotation.IntDef; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A table that holds version information about other ExoPlayer tables. This allows ExoPlayer tables + * to be versioned independently to the version of the containing database. + */ +public final class VersionTable { + + /** Returned by {@link #getVersion(int)} if the version is unset. */ + public static final int VERSION_UNSET = -1; + /** Version of tables used for offline functionality. */ + public static final int FEATURE_OFFLINE = 0; + /** Version of tables used for cache functionality. */ + public static final int FEATURE_CACHE = 1; + + private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "Versions"; + + private static final String COLUMN_FEATURE = "feature"; + private static final String COLUMN_VERSION = "version"; + + private static final String SQL_CREATE_TABLE_IF_NOT_EXISTS = + "CREATE TABLE IF NOT EXISTS " + + TABLE_NAME + + " (" + + COLUMN_FEATURE + + " INTEGER PRIMARY KEY NOT NULL," + + COLUMN_VERSION + + " INTEGER NOT NULL)"; + + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({FEATURE_OFFLINE, FEATURE_CACHE}) + private @interface Feature {} + + private final DatabaseProvider databaseProvider; + + public VersionTable(DatabaseProvider databaseProvider) { + this.databaseProvider = databaseProvider; + // Check whether the table exists to avoid getting a writable database if we don't need one. + if (!doesTableExist(databaseProvider, TABLE_NAME)) { + databaseProvider.getWritableDatabase().execSQL(SQL_CREATE_TABLE_IF_NOT_EXISTS); + } + } + + /** + * Sets the version of tables belonging to the specified feature. + * + * @param feature The feature. + * @param version The version. + */ + public void setVersion(@Feature int feature, int version) { + ContentValues values = new ContentValues(); + values.put(COLUMN_FEATURE, feature); + values.put(COLUMN_VERSION, version); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values); + } + + /** + * Returns the version of tables belonging to the specified feature, or {@link #VERSION_UNSET} if + * no version information is available. + */ + public int getVersion(@Feature int feature) { + String selection = COLUMN_FEATURE + " = ?"; + String[] selectionArgs = {Integer.toString(feature)}; + try (Cursor cursor = + databaseProvider + .getReadableDatabase() + .query( + TABLE_NAME, + new String[] {COLUMN_VERSION}, + selection, + selectionArgs, + /* groupBy= */ null, + /* having= */ null, + /* orderBy= */ null)) { + if (cursor.getCount() == 0) { + return VERSION_UNSET; + } + cursor.moveToNext(); + return cursor.getInt(/* COLUMN_VERSION index */ 0); + } + } + + /* package */ static boolean doesTableExist(DatabaseProvider databaseProvider, String tableName) { + SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase(); + long count = + DatabaseUtils.queryNumEntries( + readableDatabase, "sqlite_master", "tbl_name = ?", new String[] {tableName}); + return count > 0; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java index 8881a038f9..28a5abafb9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java @@ -16,21 +16,15 @@ package com.google.android.exoplayer2.offline; import android.content.ContentValues; -import android.content.Context; import android.database.Cursor; -import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteException; -import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; -import android.support.annotation.IntDef; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; +import com.google.android.exoplayer2.database.DatabaseProvider; +import com.google.android.exoplayer2.database.VersionTable; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; /** * A {@link DownloadIndex} which uses SQLite to persist {@link DownloadState}s. @@ -40,55 +34,13 @@ import java.lang.annotation.RetentionPolicy; */ public final class DefaultDownloadIndex implements DownloadIndex { - /** Provides {@link SQLiteDatabase} instances. */ - public interface DatabaseProvider { - /** Closes any open database object. */ - void close(); + @VisibleForTesting + /* package */ static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "Downloads"; - /** - * Creates and/or opens a database that will be used for reading and writing. - * - *

    Once opened successfully, the database is cached, so you can call this method every time - * you need to write to the database. (Make sure to call {@link #close} when you no longer need - * the database.) Errors such as bad permissions or a full disk may cause this method to fail, - * but future attempts may succeed if the problem is fixed. - * - * @throws SQLiteException If the database cannot be opened for writing. - * @return A read/write database object valid until {@link #close} is called. - */ - SQLiteDatabase getWritableDatabase(); - - /** - * Creates and/or opens a database. This will be the same object returned by {@link - * #getWritableDatabase} unless some problem, such as a full disk, requires the database to be - * opened read-only. In that case, a read-only database object will be returned. If the problem - * is fixed, a future call to {@link #getWritableDatabase} may succeed, in which case the - * read-only database object will be closed and the read/write object will be returned in the - * future. - * - *

    Once opened successfully, the database should be cached. When the database is no longer - * needed, {@link #close} will be called. - * - * @throws SQLiteException If the database cannot be opened. - * @return A database object valid until {@link #getWritableDatabase} or {@link #close} is - * called. - */ - SQLiteDatabase getReadableDatabase(); - } - - private static final String DATABASE_NAME = "exoplayer_internal.db"; + @VisibleForTesting /* package */ static final int TABLE_VERSION = 1; private final DatabaseProvider databaseProvider; - @Nullable private DownloadStateTable downloadStateTable; - - /** - * Creates a DefaultDownloadIndex which stores the {@link DownloadState}s on a SQLite database. - * - * @param context A Context. - */ - public DefaultDownloadIndex(Context context) { - this(new DefaultDatabaseProvider(context)); - } + @Nullable private DownloadsTable downloadTable; /** * Creates a DefaultDownloadIndex which stores the {@link DownloadState}s on a SQLite database @@ -101,46 +53,32 @@ public final class DefaultDownloadIndex implements DownloadIndex { this.databaseProvider = databaseProvider; } - @Override - public void release() { - databaseProvider.close(); - } - @Override @Nullable public DownloadState getDownloadState(String id) { - return getDownloadStateTable().get(id); + return getDownloadTable().get(id); } @Override public DownloadStateCursor getDownloadStates(@DownloadState.State int... states) { - return getDownloadStateTable().get(states); + return getDownloadTable().get(states); } @Override public void putDownloadState(DownloadState downloadState) { - getDownloadStateTable().replace(downloadState); + getDownloadTable().replace(downloadState); } @Override public void removeDownloadState(String id) { - getDownloadStateTable().delete(id); + getDownloadTable().delete(id); } - private DownloadStateTable getDownloadStateTable() { - if (downloadStateTable == null) { - downloadStateTable = new DownloadStateTable(databaseProvider); + private DownloadsTable getDownloadTable() { + if (downloadTable == null) { + downloadTable = new DownloadsTable(databaseProvider); } - return downloadStateTable; - } - - @VisibleForTesting - /* package */ static boolean doesTableExist(DatabaseProvider databaseProvider, String tableName) { - SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase(); - long count = - DatabaseUtils.queryNumEntries( - readableDatabase, "sqlite_master", "tbl_name = ?", new String[] {tableName}); - return count > 0; + return downloadTable; } private static final class DownloadStateCursorImpl implements DownloadStateCursor { @@ -153,7 +91,7 @@ public final class DefaultDownloadIndex implements DownloadIndex { @Override public DownloadState getDownloadState() { - return DownloadStateTable.getDownloadState(cursor); + return DownloadsTable.getDownloadState(cursor); } @Override @@ -182,10 +120,7 @@ public final class DefaultDownloadIndex implements DownloadIndex { } } - @VisibleForTesting - /* package */ static final class DownloadStateTable { - @VisibleForTesting /* package */ static final String TABLE_NAME = "ExoPlayerDownloadStates"; - @VisibleForTesting /* package */ static final int TABLE_VERSION = 1; + private static final class DownloadsTable { private static final String COLUMN_ID = "id"; private static final String COLUMN_TYPE = "title"; @@ -237,9 +172,9 @@ public final class DefaultDownloadIndex implements DownloadIndex { COLUMN_CUSTOM_METADATA }; - private static final String SQL_DROP_TABLE = "DROP TABLE IF EXISTS " + TABLE_NAME; + private static final String SQL_DROP_TABLE_IF_EXISTS = "DROP TABLE IF EXISTS " + TABLE_NAME; private static final String SQL_CREATE_TABLE = - "CREATE TABLE IF NOT EXISTS " + "CREATE TABLE " + TABLE_NAME + " (" + COLUMN_ID @@ -273,17 +208,15 @@ public final class DefaultDownloadIndex implements DownloadIndex { private final DatabaseProvider databaseProvider; - public DownloadStateTable(DatabaseProvider databaseProvider) { + public DownloadsTable(DatabaseProvider databaseProvider) { this.databaseProvider = databaseProvider; VersionTable versionTable = new VersionTable(databaseProvider); int version = versionTable.getVersion(VersionTable.FEATURE_OFFLINE); - if (!doesTableExist(databaseProvider, TABLE_NAME) - || version == 0 - || version > TABLE_VERSION) { + if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); writableDatabase.beginTransaction(); try { - writableDatabase.execSQL(SQL_DROP_TABLE); + writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS); writableDatabase.execSQL(SQL_CREATE_TABLE); versionTable.setVersion(VersionTable.FEATURE_OFFLINE, TABLE_VERSION); writableDatabase.setTransactionSuccessful(); @@ -421,108 +354,4 @@ public final class DefaultDownloadIndex implements DownloadIndex { return streamKeys; } } - - @VisibleForTesting - /* package */ static final class VersionTable { - private static final String TABLE_NAME = "ExoPlayerVersions"; - - private static final String COLUMN_FEATURE = "feature"; - private static final String COLUMN_VERSION = "version"; - - private static final String SQL_CREATE_TABLE = - "CREATE TABLE IF NOT EXISTS " - + TABLE_NAME - + " (" - + COLUMN_FEATURE - + " INTEGER PRIMARY KEY NOT NULL," - + COLUMN_VERSION - + " INTEGER NOT NULL)"; - - @Documented - @Retention(RetentionPolicy.SOURCE) - @IntDef({FEATURE_OFFLINE, FEATURE_CACHE}) - private @interface Feature {} - - public static final int FEATURE_OFFLINE = 0; - public static final int FEATURE_CACHE = 1; - - private final DatabaseProvider databaseProvider; - - public VersionTable(DatabaseProvider databaseProvider) { - this.databaseProvider = databaseProvider; - if (!doesTableExist(databaseProvider, TABLE_NAME)) { - databaseProvider.getWritableDatabase().execSQL(SQL_CREATE_TABLE); - } - } - - public void setVersion(@Feature int feature, int version) { - ContentValues values = new ContentValues(); - values.put(COLUMN_FEATURE, feature); - values.put(COLUMN_VERSION, version); - SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values); - } - - public int getVersion(@Feature int feature) { - String selection = COLUMN_FEATURE + " = ?"; - String[] selectionArgs = {Integer.toString(feature)}; - try (Cursor cursor = - databaseProvider - .getReadableDatabase() - .query( - TABLE_NAME, - new String[] {COLUMN_VERSION}, - selection, - selectionArgs, - /* groupBy= */ null, - /* having= */ null, - /* orderBy= */ null)) { - if (cursor.getCount() == 0) { - return 0; - } - cursor.moveToNext(); - return cursor.getInt(/* COLUMN_VERSION index */ 0); - } - } - } - - private static final class DefaultDatabaseProvider extends SQLiteOpenHelper - implements DatabaseProvider { - public DefaultDatabaseProvider(Context context) { - super(context, DATABASE_NAME, /* factory= */ null, /* version= */ 1); - } - - @Override - public void onCreate(SQLiteDatabase db) { - // Table creation is done in DownloadStateTable constructor. - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - // Upgrade is handled in DownloadStateTable constructor. - } - - @Override - public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { - // TODO: Wipe the database. - super.onDowngrade(db, oldVersion, newVersion); - } - - // DatabaseProvider implementation. - - @Override - public synchronized void close() { - super.close(); - } - - @Override - public SQLiteDatabase getWritableDatabase() { - return super.getWritableDatabase(); - } - - @Override - public SQLiteDatabase getReadableDatabase() { - return super.getReadableDatabase(); - } - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java index 71726b64a9..7b903d3321 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndex.java @@ -19,8 +19,6 @@ import android.support.annotation.Nullable; /** Persists {@link DownloadState}s. */ interface DownloadIndex { - /** Releases the used resources. */ - void release(); /** * Returns the {@link DownloadState} with the given {@code id}, or null. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/database/VersionTableTest.java b/library/core/src/test/java/com/google/android/exoplayer2/database/VersionTableTest.java new file mode 100644 index 0000000000..dd9184ccdd --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/database/VersionTableTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2018 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.database; + +import static com.google.android.exoplayer2.database.VersionTable.FEATURE_CACHE; +import static com.google.android.exoplayer2.database.VersionTable.FEATURE_OFFLINE; +import static com.google.common.truth.Truth.assertThat; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +/** Unit tests for {@link VersionTable}. */ +@RunWith(RobolectricTestRunner.class) +public class VersionTableTest { + + private ExoDatabaseProvider databaseProvider; + + @Before + public void setUp() { + databaseProvider = new ExoDatabaseProvider(RuntimeEnvironment.application); + } + + @After + public void tearDown() { + databaseProvider.close(); + } + + @Test + public void getVersion_nonExistingTable_returnsVersionUnset() { + VersionTable versionTable = new VersionTable(databaseProvider); + int version = versionTable.getVersion(FEATURE_OFFLINE); + assertThat(version).isEqualTo(VersionTable.VERSION_UNSET); + } + + @Test + public void getVersion_returnsSetVersion() { + VersionTable versionTable = new VersionTable(databaseProvider); + + versionTable.setVersion(FEATURE_OFFLINE, 1); + assertThat(versionTable.getVersion(FEATURE_OFFLINE)).isEqualTo(1); + + versionTable.setVersion(FEATURE_OFFLINE, 10); + assertThat(versionTable.getVersion(FEATURE_OFFLINE)).isEqualTo(10); + + versionTable.setVersion(FEATURE_CACHE, 5); + assertThat(versionTable.getVersion(FEATURE_CACHE)).isEqualTo(5); + assertThat(versionTable.getVersion(FEATURE_OFFLINE)).isEqualTo(10); + } + + @Test + public void doesTableExist_nonExistingTable_returnsFalse() { + assertThat(VersionTable.doesTableExist(databaseProvider, "NonExistingTable")).isFalse(); + } + + @Test + public void doesTableExist_existingTable_returnsTrue() { + String table = "TestTable"; + databaseProvider.getWritableDatabase().execSQL("CREATE TABLE " + table + " (dummy INTEGER)"); + assertThat(VersionTable.doesTableExist(databaseProvider, table)).isTrue(); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java index fe32415ffa..6f6786c068 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java @@ -15,15 +15,13 @@ */ package com.google.android.exoplayer2.offline; -import static com.google.android.exoplayer2.offline.DefaultDownloadIndex.VersionTable.FEATURE_CACHE; -import static com.google.android.exoplayer2.offline.DefaultDownloadIndex.VersionTable.FEATURE_OFFLINE; import static com.google.common.truth.Truth.assertThat; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.database.ExoDatabaseProvider; +import com.google.android.exoplayer2.database.VersionTable; import java.util.Arrays; import org.junit.After; import org.junit.Before; @@ -36,16 +34,18 @@ import org.robolectric.RuntimeEnvironment; @RunWith(RobolectricTestRunner.class) public class DefaultDownloadIndexTest { + private ExoDatabaseProvider databaseProvider; private DefaultDownloadIndex downloadIndex; @Before public void setUp() { - downloadIndex = new DefaultDownloadIndex(RuntimeEnvironment.application); + databaseProvider = new ExoDatabaseProvider(RuntimeEnvironment.application); + downloadIndex = new DefaultDownloadIndex(databaseProvider); } @After public void tearDown() { - downloadIndex.release(); + databaseProvider.close(); } @Test @@ -99,29 +99,12 @@ public class DefaultDownloadIndexTest { public void releaseAndRecreateDownloadIndex_returnsTheSameDownloadState() { String id = "id"; DownloadState downloadState = new DownloadStateBuilder(id).build(); - downloadIndex.putDownloadState(downloadState); - downloadIndex.release(); - downloadIndex = new DefaultDownloadIndex(RuntimeEnvironment.application); - DownloadState readDownloadState = downloadIndex.getDownloadState(id); - assertThat(readDownloadState).isNotNull(); - assertEqual(readDownloadState, downloadState); - } - - @Test - public void customDatabaseProvider_getDownloadStateReturnsNull() { - String id = "id"; - DownloadState downloadState = new DownloadStateBuilder(id).build(); - - downloadIndex.putDownloadState(downloadState); - downloadIndex.release(); - DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl(); downloadIndex = new DefaultDownloadIndex(databaseProvider); DownloadState readDownloadState = downloadIndex.getDownloadState(id); - - assertThat(readDownloadState).isNull(); - databaseProvider.close(); + assertThat(readDownloadState).isNotNull(); + assertEqual(readDownloadState, downloadState); } @Test @@ -134,10 +117,9 @@ public class DefaultDownloadIndexTest { String id = "id"; DownloadState downloadState = new DownloadStateBuilder(id).build(); downloadIndex.putDownloadState(downloadState); - downloadIndex.removeDownloadState(id); - DownloadState readDownloadState = downloadIndex.getDownloadState(id); + DownloadState readDownloadState = downloadIndex.getDownloadState(id); assertThat(readDownloadState).isNull(); } @@ -160,6 +142,7 @@ public class DefaultDownloadIndexTest { assertEqual(cursor.getDownloadState(), downloadState2); cursor.moveToNext(); assertEqual(cursor.getDownloadState(), downloadState1); + cursor.close(); } @Test @@ -191,111 +174,39 @@ public class DefaultDownloadIndexTest { assertEqual(cursor.getDownloadState(), downloadState1); cursor.moveToNext(); assertEqual(cursor.getDownloadState(), downloadState3); + cursor.close(); } @Test - public void doesTableExist_nonExistingTable_returnsFalse() { - DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl(); + public void putDownloadState_setsVersion() { + VersionTable versionTable = new VersionTable(databaseProvider); + assertThat(versionTable.getVersion(VersionTable.FEATURE_OFFLINE)) + .isEqualTo(VersionTable.VERSION_UNSET); - assertThat(DefaultDownloadIndex.doesTableExist(databaseProvider, "NonExistingTable")).isFalse(); + downloadIndex.putDownloadState(new DownloadStateBuilder("id1").build()); - databaseProvider.close(); + assertThat(versionTable.getVersion(VersionTable.FEATURE_OFFLINE)) + .isEqualTo(DefaultDownloadIndex.TABLE_VERSION); } @Test - public void doesTableExist_existingTable_returnsTrue() { - DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl(); - String tableName = "ExistingTable"; - databaseProvider.getWritableDatabase().execSQL("CREATE TABLE " + tableName + "(dummy)"); + public void downloadIndex_versionDowngradeWipesData() { + DownloadState downloadState1 = new DownloadStateBuilder("id1").build(); + downloadIndex.putDownloadState(downloadState1); + DownloadStateCursor cursor = downloadIndex.getDownloadStates(); + assertThat(cursor.getCount()).isEqualTo(1); + cursor.close(); - assertThat(DefaultDownloadIndex.doesTableExist(databaseProvider, tableName)).isTrue(); + VersionTable versionTable = new VersionTable(databaseProvider); + versionTable.setVersion(VersionTable.FEATURE_OFFLINE, Integer.MAX_VALUE); - databaseProvider.close(); - } + downloadIndex = new DefaultDownloadIndex(databaseProvider); - @Test - public void getVersion_nonExistingTable_returnsZero() { - DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl(); - DefaultDownloadIndex.VersionTable versionTable = - new DefaultDownloadIndex.VersionTable(databaseProvider); - - int version = versionTable.getVersion(FEATURE_OFFLINE); - - assertThat(version).isEqualTo(0); - databaseProvider.close(); - } - - @Test - public void getVersion_returnsSetVersion() { - DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl(); - DefaultDownloadIndex.VersionTable versionTable = - new DefaultDownloadIndex.VersionTable(databaseProvider); - - versionTable.setVersion(FEATURE_OFFLINE, 1); - assertThat(versionTable.getVersion(FEATURE_OFFLINE)).isEqualTo(1); - - versionTable.setVersion(FEATURE_OFFLINE, 10); - assertThat(versionTable.getVersion(FEATURE_OFFLINE)).isEqualTo(10); - - versionTable.setVersion(FEATURE_CACHE, 5); - assertThat(versionTable.getVersion(FEATURE_CACHE)).isEqualTo(5); - assertThat(versionTable.getVersion(FEATURE_OFFLINE)).isEqualTo(10); - - databaseProvider.close(); - } - - @Test - public void downloadStateTableConstructor_noTable_createsTable() { - DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl(); - assertThat( - DefaultDownloadIndex.doesTableExist( - databaseProvider, DefaultDownloadIndex.DownloadStateTable.TABLE_NAME)) - .isFalse(); - - new DefaultDownloadIndex.DownloadStateTable(databaseProvider); - - assertThat( - DefaultDownloadIndex.doesTableExist( - databaseProvider, DefaultDownloadIndex.DownloadStateTable.TABLE_NAME)) - .isTrue(); - - databaseProvider.close(); - } - - @Test - public void downloadStateTableConstructor_versionZero_versionSet() { - DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl(); - - new DefaultDownloadIndex.DownloadStateTable(databaseProvider); - - DefaultDownloadIndex.VersionTable versionTable = - new DefaultDownloadIndex.VersionTable(databaseProvider); - assertThat(versionTable.getVersion(FEATURE_OFFLINE)) - .isEqualTo(DefaultDownloadIndex.DownloadStateTable.TABLE_VERSION); - databaseProvider.close(); - } - - @Test - public void downloadStateTableConstructor_greaterVersion_tableRecreated() { - DatabaseProviderImpl databaseProvider = new DatabaseProviderImpl(); - databaseProvider - .getWritableDatabase() - .execSQL("CREATE TABLE " + DefaultDownloadIndex.DownloadStateTable.TABLE_NAME + "(dummy)"); - DefaultDownloadIndex.VersionTable versionTable = - new DefaultDownloadIndex.VersionTable(databaseProvider); - versionTable.setVersion(FEATURE_OFFLINE, Integer.MAX_VALUE); - - DefaultDownloadIndex.DownloadStateTable downloadStateTable = - new DefaultDownloadIndex.DownloadStateTable(databaseProvider); - String id = "id"; - DownloadState downloadState = new DownloadStateBuilder(id).build(); - downloadStateTable.replace(downloadState); - DownloadState readDownloadState = downloadStateTable.get(id); - assertEqual(readDownloadState, downloadState); - - assertThat(versionTable.getVersion(FEATURE_OFFLINE)) - .isEqualTo(DefaultDownloadIndex.DownloadStateTable.TABLE_VERSION); - databaseProvider.close(); + cursor = downloadIndex.getDownloadStates(); + assertThat(cursor.getCount()).isEqualTo(0); + cursor.close(); + assertThat(versionTable.getVersion(VersionTable.FEATURE_OFFLINE)) + .isEqualTo(DefaultDownloadIndex.TABLE_VERSION); } private static void assertEqual(DownloadState downloadState, DownloadState expected) { @@ -468,24 +379,4 @@ public class DefaultDownloadIndexTest { customMetadata); } } - - private static final class DatabaseProviderImpl extends SQLiteOpenHelper - implements DefaultDownloadIndex.DatabaseProvider { - private static final int DATABASE_VERSION = 1; - private static final String DATABASE_NAME = "TestExoPlayerDownloadIndex.db"; - - public DatabaseProviderImpl() { - super(RuntimeEnvironment.application, DATABASE_NAME, null, DATABASE_VERSION); - } - - @Override - public void onCreate(SQLiteDatabase db) { - // Do nothing. - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - // Do nothing. - } - } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadIndexUtilTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadIndexUtilTest.java index bc93b40265..a2b560cecd 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadIndexUtilTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadIndexUtilTest.java @@ -19,6 +19,7 @@ import static com.google.android.exoplayer2.offline.DownloadAction.TYPE_DASH; import static com.google.common.truth.Truth.assertThat; import android.net.Uri; +import com.google.android.exoplayer2.database.ExoDatabaseProvider; import com.google.android.exoplayer2.util.Util; import java.io.File; import java.util.Arrays; @@ -34,18 +35,20 @@ import org.robolectric.RuntimeEnvironment; @RunWith(RobolectricTestRunner.class) public class DownloadIndexUtilTest { - private DefaultDownloadIndex downloadIndex; private File tempFile; + private ExoDatabaseProvider databaseProvider; + private DefaultDownloadIndex downloadIndex; @Before public void setUp() throws Exception { tempFile = Util.createTempFile(RuntimeEnvironment.application, "ExoPlayerTest"); - downloadIndex = new DefaultDownloadIndex(RuntimeEnvironment.application); + databaseProvider = new ExoDatabaseProvider(RuntimeEnvironment.application); + downloadIndex = new DefaultDownloadIndex(databaseProvider); } @After public void tearDown() { - downloadIndex.release(); + databaseProvider.close(); tempFile.delete(); } From 93e24561bbc642fa1abf62f4c1ecdacff403edfe Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Mon, 21 Jan 2019 10:18:01 +0000 Subject: [PATCH 033/110] Add DefaultTsPayloadReaderFactory flag to ignore HDMV DTS streams Prevents collisions with SCTE-35 subtitles. Issue:#5330 PiperOrigin-RevId: 230195494 --- .../ts/DefaultTsPayloadReaderFactory.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java index a5506e2cfb..88805d9362 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/DefaultTsPayloadReaderFactory.java @@ -50,7 +50,8 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact FLAG_IGNORE_H264_STREAM, FLAG_DETECT_ACCESS_UNITS, FLAG_IGNORE_SPLICE_INFO_STREAM, - FLAG_OVERRIDE_CAPTION_DESCRIPTORS + FLAG_OVERRIDE_CAPTION_DESCRIPTORS, + FLAG_IGNORE_HDMV_DTS_STREAM }) public @interface Flags {} @@ -86,6 +87,12 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact * closedCaptionFormats} will be ignored if the PMT contains closed captions service descriptors. */ public static final int FLAG_OVERRIDE_CAPTION_DESCRIPTORS = 1 << 5; + /** + * Prevents the creation of {@link DtsReader} instances when receiving {@link + * TsExtractor#TS_STREAM_TYPE_HDMV_DTS} as stream type. Enabling this flag prevents a stream type + * collision between HDMV DTS audio and SCTE-35 subtitles. + */ + public static final int FLAG_IGNORE_HDMV_DTS_STREAM = 1 << 6; private static final int DESCRIPTOR_TAG_CAPTION_SERVICE = 0x86; @@ -142,8 +149,12 @@ public final class DefaultTsPayloadReaderFactory implements TsPayloadReader.Fact case TsExtractor.TS_STREAM_TYPE_AC3: case TsExtractor.TS_STREAM_TYPE_E_AC3: return new PesReader(new Ac3Reader(esInfo.language)); - case TsExtractor.TS_STREAM_TYPE_DTS: case TsExtractor.TS_STREAM_TYPE_HDMV_DTS: + if (isSet(FLAG_IGNORE_HDMV_DTS_STREAM)) { + return null; + } + // Fall through. + case TsExtractor.TS_STREAM_TYPE_DTS: return new PesReader(new DtsReader(esInfo.language)); case TsExtractor.TS_STREAM_TYPE_H262: return new PesReader(new H262Reader(buildUserDataReader(esInfo))); From fb6154a905cdc7c111c7343a7a6a331dfc5527d1 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 21 Jan 2019 11:42:47 +0000 Subject: [PATCH 034/110] Wipe database on downgrade PiperOrigin-RevId: 230203593 --- .../database/ExoDatabaseProvider.java | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java b/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java index b64bad2ad6..e5bdfbb499 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java @@ -16,8 +16,11 @@ package com.google.android.exoplayer2.database; import android.content.Context; +import android.database.Cursor; +import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import com.google.android.exoplayer2.util.Log; /** * An {@link SQLiteOpenHelper} that provides instances of a standalone ExoPlayer database. @@ -32,6 +35,7 @@ public final class ExoDatabaseProvider extends SQLiteOpenHelper implements Datab public static final String DATABASE_NAME = "exoplayer_internal.db"; private static final int VERSION = 1; + private static final String TAG = "ExoDatabaseProvider"; public ExoDatabaseProvider(Context context) { super(context.getApplicationContext(), DATABASE_NAME, /* factory= */ null, VERSION); @@ -49,7 +53,37 @@ public final class ExoDatabaseProvider extends SQLiteOpenHelper implements Datab @Override public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { - // TODO: Wipe the database. - super.onDowngrade(db, oldVersion, newVersion); + wipeDatabase(db); + } + + /** + * Makes a best effort to wipe the existing database. The wipe may be incomplete if the database + * contains foreign key constraints. + */ + private static void wipeDatabase(SQLiteDatabase db) { + String[] columns = {"type", "name"}; + try (Cursor cursor = + db.query( + "sqlite_master", + columns, + /* selection= */ null, + /* selectionArgs= */ null, + /* groupBy= */ null, + /* having= */ null, + /* orderBy= */ null)) { + while (cursor.moveToNext()) { + String type = cursor.getString(0); + String name = cursor.getString(1); + if (!"sqlite_sequence".equals(name)) { + // If it's not an SQL-controlled entity, drop it + String sql = "DROP " + type + " IF EXISTS " + name; + try { + db.execSQL(sql); + } catch (SQLException e) { + Log.e(TAG, "Error executing " + sql, e); + } + } + } + } } } From e4eaaedad32945aa25e9148c7c495f764ca66d0b Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 21 Jan 2019 12:15:30 +0000 Subject: [PATCH 035/110] Add max video size workaround for Amlogic decoder. The Amlogic awesome decoder reduces the video size of interlaced videos by half if the internal configuration isn't force reset with new maximum input size values. The product of these new values must exceed 1920x1088 to force the reset. Issue:#5003 PiperOrigin-RevId: 230206675 --- RELEASENOTES.md | 2 ++ .../video/MediaCodecVideoRenderer.java | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index cfded4c9d6..bfb84d2504 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -41,6 +41,8 @@ * Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`. * Change signature of `PlayerNotificationManager.NotificationListener` to better fit service requirements. Remove ability to set a custom stop action. +* Add workaround for video quality problems with Amlogic decoders + ([#5003](https://github.com/google/ExoPlayer/issues/5003)). ### 2.9.4 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 9084547d0f..6943fea7b6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -1095,6 +1095,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { throws DecoderQueryException { int maxWidth = format.width; int maxHeight = format.height; + if (codecNeedsMaxVideoSizeResetWorkaround(codecInfo.name)) { + maxWidth = Math.max(maxWidth, 1920); + maxHeight = Math.max(maxHeight, 1089); + } int maxInputSize = getMaxInputSize(codecInfo, format); if (streamFormats.length == 1) { // The single entry in streamFormats must correspond to the format for which the codec is @@ -1282,6 +1286,18 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return "NVIDIA".equals(Util.MANUFACTURER); } + /** + * Returns whether the codec is known to have problems with the configuration for interlaced + * content and needs minimum values for the maximum video size to force reset the configuration. + * + *

    See https://github.com/google/ExoPlayer/issues/5003. + * + * @param name The name of the codec. + */ + private static boolean codecNeedsMaxVideoSizeResetWorkaround(String name) { + return "OMX.amlogic.avc.decoder.awesome".equals(name) && Util.SDK_INT <= 25; + } + /* * TODO: * From c2f6dd6b0aab6c35424ea6a47974c99bc60b9fc3 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 21 Jan 2019 12:44:07 +0000 Subject: [PATCH 036/110] Only allow one layer of sub-directories in the cache for now PiperOrigin-RevId: 230209898 --- .../google/android/exoplayer2/upstream/cache/SimpleCache.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index 80eb779e39..7f9bdde5c1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -405,7 +405,7 @@ public final class SimpleCache implements Cache { } for (File file : files) { String fileName = file.getName(); - if (fileName.indexOf('.') == -1) { + if (isRootDirectory && fileName.indexOf('.') == -1) { loadDirectory(file, /* isRootDirectory= */ false); } else { if (isRootDirectory && CachedContentIndex.FILE_NAME.equals(fileName)) { From e671dac9a2c31d2670470f7a6d9c0cd7ada5030b Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 21 Jan 2019 13:23:52 +0000 Subject: [PATCH 037/110] Fix GVR dependency PiperOrigin-RevId: 230213842 --- extensions/gvr/build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/extensions/gvr/build.gradle b/extensions/gvr/build.gradle index ba6bc8afda..6c0ec05bfb 100644 --- a/extensions/gvr/build.gradle +++ b/extensions/gvr/build.gradle @@ -33,8 +33,7 @@ dependencies { implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-ui') implementation 'com.android.support:support-annotations:' + supportLibraryVersion - implementation 'com.google.vr:sdk-base:1.190.0' - api 'com.google.vr:sdk-base:1.80.0' + api 'com.google.vr:sdk-base:1.190.0' compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion } From 923aa420dfe6eb19eb05647d90d7c5aff62198fb Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 21 Jan 2019 13:52:14 +0000 Subject: [PATCH 038/110] Use loading period event time for fatal load errors. ExoPlaybackExceptions of type SOURCE are always associated with the loading period and thus we can use the event time for the loading period in onPlayerError. Renderer and unexpected exceptions are still associated with the currently playing period. Issue:#5407 PiperOrigin-RevId: 230216253 --- RELEASENOTES.md | 3 +++ .../android/exoplayer2/analytics/AnalyticsCollector.java | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index bfb84d2504..053198de57 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -43,6 +43,9 @@ fit service requirements. Remove ability to set a custom stop action. * Add workaround for video quality problems with Amlogic decoders ([#5003](https://github.com/google/ExoPlayer/issues/5003)). +* Associate fatal player errors of type SOURCE with the loading source in + `AnalyticsListener.EventTime` + ([#5407](https://github.com/google/ExoPlayer/issues/5407)). ### 2.9.4 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java index 113add612a..55031e2d12 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -488,7 +488,10 @@ public class AnalyticsCollector @Override public final void onPlayerError(ExoPlaybackException error) { - EventTime eventTime = generatePlayingMediaPeriodEventTime(); + EventTime eventTime = + error.type == ExoPlaybackException.TYPE_SOURCE + ? generateLoadingMediaPeriodEventTime() + : generatePlayingMediaPeriodEventTime(); for (AnalyticsListener listener : listeners) { listener.onPlayerError(eventTime, error); } From 9fec49f326d62c7ea8137635101a4f6dceed9d82 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 21 Jan 2019 14:38:00 +0000 Subject: [PATCH 039/110] Update 2.9.4 release notes PiperOrigin-RevId: 230220448 --- RELEASENOTES.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 053198de57..5894e991d0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -41,11 +41,6 @@ * Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`. * Change signature of `PlayerNotificationManager.NotificationListener` to better fit service requirements. Remove ability to set a custom stop action. -* Add workaround for video quality problems with Amlogic decoders - ([#5003](https://github.com/google/ExoPlayer/issues/5003)). -* Associate fatal player errors of type SOURCE with the loading source in - `AnalyticsListener.EventTime` - ([#5407](https://github.com/google/ExoPlayer/issues/5407)). ### 2.9.4 ### @@ -56,6 +51,17 @@ * FFmpeg extension: Treat invalid data errors as non-fatal to match the behavior of MediaCodec ([#5293](https://github.com/google/ExoPlayer/issues/5293)). * GVR extension: upgrade GVR SDK dependency to 1.190.0. +* Associate fatal player errors of type SOURCE with the loading source in + `AnalyticsListener.EventTime` + ([#5407](https://github.com/google/ExoPlayer/issues/5407)). +* Add `startPositionUs` to `MediaSource.createPeriod`. This fixes an issue where + using lazy preparation in `ConcatenatingMediaSource` with an + `ExtractorMediaSource` overrides initial seek positions + ([#5350](https://github.com/google/ExoPlayer/issues/5350)). +* Add subtext to the `MediaDescriptionAdapter` of the + `PlayerNotificationManager`. +* Add workaround for video quality problems with Amlogic decoders + ([#5003](https://github.com/google/ExoPlayer/issues/5003)). * Fix issue where sending callbacks for playlist changes may cause problems because of parallel player access ([#5240](https://github.com/google/ExoPlayer/issues/5240)). @@ -64,12 +70,6 @@ ([#5351](https://github.com/google/ExoPlayer/issues/5351)). * Fix issue where uneven track durations in MP4 streams can cause OOM problems ([#3670](https://github.com/google/ExoPlayer/issues/3670)). -* Add `startPositionUs` to `MediaSource.createPeriod`. This fixes an issue where - using lazy preparation in `ConcatenatingMediaSource` with an - `ExtractorMediaSource` overrides initial seek positions - ([#5350](https://github.com/google/ExoPlayer/issues/5350)). -* Add subtext to the `MediaDescriptionAdapter` of the - `PlayerNotificationManager`. ### 2.9.3 ### From 29711b922d2b14cf89819af06a0a4dc2561bec7d Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 21 Jan 2019 21:06:03 +0000 Subject: [PATCH 040/110] Cancel notification on dismiss This doesn't happen automatically after all on older devices PiperOrigin-RevId: 230251258 --- .../android/exoplayer2/ui/PlayerNotificationManager.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index c01aabc9f2..97e36a17b3 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -928,10 +928,8 @@ public class PlayerNotificationManager { private void stopNotification(boolean dismissedByUser) { if (isNotificationStarted) { - if (!dismissedByUser) { - notificationManager.cancel(notificationId); - } isNotificationStarted = false; + notificationManager.cancel(notificationId); context.unregisterReceiver(notificationBroadcastReceiver); if (notificationListener != null) { notificationListener.onNotificationCancelled(notificationId, dismissedByUser); From de3a749b970b22258b2ab50c707fa6dbdb09dc1a Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 21 Jan 2019 23:52:18 +0000 Subject: [PATCH 041/110] Fix scheduler NPE in DownloadService PiperOrigin-RevId: 230260266 --- .../com/google/android/exoplayer2/demo/DemoDownloadService.java | 2 +- .../com/google/android/exoplayer2/offline/DownloadService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java index 70cbe43dd8..dcccd884ec 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java @@ -65,7 +65,7 @@ public class DemoDownloadService extends DownloadService { @Override protected void onDownloadStateChanged(DownloadState downloadState) { - Notification notification = null; + Notification notification; if (downloadState.state == DownloadState.STATE_COMPLETED) { notification = DownloadNotificationUtil.buildDownloadCompletedNotification( diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java index d424ed5ef0..305620d5f3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadService.java @@ -462,7 +462,7 @@ public abstract class DownloadService extends Service { public void detachService(DownloadService downloadService, boolean unschedule) { Assertions.checkState(this.downloadService == downloadService); this.downloadService = null; - if (unschedule) { + if (scheduler != null && unschedule) { scheduler.cancel(); } } From 4182f37b6e28869debe19c004cddc6681c3c18dd Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 22 Jan 2019 17:43:55 +0000 Subject: [PATCH 042/110] Make notification dismissible iff player is active Seems like more useful default behaviour PiperOrigin-RevId: 230356813 --- .../ui/PlayerNotificationManager.java | 49 +++++++------------ 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index 97e36a17b3..597f0dbd40 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -235,15 +235,15 @@ public class PlayerNotificationManager { /** * Called each time after the notification has been posted. * - *

    The {@code isPlayerActive} flag indicates whether a service in which the player may run - * needs to be in the foreground. + *

    For a service, the {@code ongoing} flag can be used as an indicator as to whether it + * should be in the foreground. * * @param notificationId The id of the notification which has been posted. * @param notification The {@link Notification}. - * @param isPlayerActive {@code true} if the player is active. + * @param ongoing Whether the notification is ongoing. */ default void onNotificationPosted( - int notificationId, Notification notification, boolean isPlayerActive) {} + int notificationId, Notification notification, boolean ongoing) {} } /** Receives a {@link Bitmap}. */ @@ -371,7 +371,6 @@ public class PlayerNotificationManager { private @DrawableRes int smallIconResourceId; private int visibility; private @Priority int priority; - private boolean ongoing; private boolean useChronometer; private boolean wasPlayWhenReady; private int lastPlaybackState; @@ -547,7 +546,6 @@ public class PlayerNotificationManager { intentFilter = new IntentFilter(); useNavigationActions = true; usePlayPauseActions = true; - ongoing = true; colorized = true; useChronometer = true; color = Color.TRANSPARENT; @@ -789,22 +787,6 @@ public class PlayerNotificationManager { } } - /** - * Sets whether the notification should be ongoing. If {@code false} the user can dismiss the - * notification by swiping. If in addition the stop action is enabled dismissing the notification - * triggers the stop action. - * - *

    See {@link NotificationCompat.Builder#setOngoing(boolean)}. - * - * @param ongoing Whether {@code true} the notification is ongoing and not dismissible. - */ - public final void setOngoing(boolean ongoing) { - if (this.ongoing != ongoing) { - this.ongoing = ongoing; - invalidate(); - } - } - /** * Sets the priority of the notification required for API 25 and lower. * @@ -904,7 +886,8 @@ public class PlayerNotificationManager { @Nullable private Notification startOrUpdateNotification(@Nullable Bitmap bitmap) { Player player = this.player; - Notification notification = createNotification(player, bitmap); + boolean ongoing = getOngoing(player); + Notification notification = createNotification(player, ongoing, bitmap); if (notification == null) { stopNotification(/* dismissedByUser= */ false); return null; @@ -919,9 +902,7 @@ public class PlayerNotificationManager { } NotificationListener listener = notificationListener; if (listener != null) { - boolean isPlayerActive = - player.getPlayWhenReady() && player.getPlaybackState() != Player.STATE_IDLE; - listener.onNotificationPosted(notificationId, notification, isPlayerActive); + listener.onNotificationPosted(notificationId, notification, ongoing); } return notification; } @@ -942,12 +923,14 @@ public class PlayerNotificationManager { * Creates the notification given the current player state. * * @param player The player for which state to build a notification. + * @param ongoing Whether the notification should be ongoing. * @param largeIcon The large icon to be used. * @return The {@link Notification} which has been built, or {@code null} if no notification * should be displayed. */ @Nullable - protected Notification createNotification(Player player, @Nullable Bitmap largeIcon) { + protected Notification createNotification( + Player player, boolean ongoing, @Nullable Bitmap largeIcon) { if (player.getPlaybackState() == Player.STATE_IDLE) { return null; } @@ -970,12 +953,11 @@ public class PlayerNotificationManager { } mediaStyle.setShowActionsInCompactView(getActionIndicesForCompactView(actionNames, player)); // Configure dismiss action prior to API 21 ('x' button). - mediaStyle.setShowCancelButton(true); + mediaStyle.setShowCancelButton(!ongoing); mediaStyle.setCancelButtonIntent(dismissPendingIntent); // Set intent which is sent if the user selects 'clear all' builder.setDeleteIntent(dismissPendingIntent); builder.setStyle(mediaStyle); - // Set notification properties from getters. builder .setBadgeIconType(badgeIconType) @@ -1086,7 +1068,7 @@ public class PlayerNotificationManager { * first parameter. * * @param actionNames The names of the actions included in the notification. - * @param player The player for which state to build a notification. + * @param player The player for which a notification is being built. */ @SuppressWarnings("unused") protected int[] getActionIndicesForCompactView(List actionNames, Player player) { @@ -1097,6 +1079,13 @@ public class PlayerNotificationManager { : (playActionIndex != -1 ? new int[] {playActionIndex} : new int[0]); } + /** Returns whether the generated notification should be ongoing. */ + protected boolean getOngoing(Player player) { + int playbackState = player.getPlaybackState(); + return (playbackState == Player.STATE_BUFFERING || playbackState == Player.STATE_READY) + && player.getPlayWhenReady(); + } + private static Map createPlaybackActions( Context context, int instanceId) { Map actions = new HashMap<>(); From 52f25f6ea800e98063b5cabdcdc6d9467ccf065b Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 23 Jan 2019 10:40:56 +0000 Subject: [PATCH 043/110] Add HLS stream and remove non-working content from Cast demo app PiperOrigin-RevId: 230496581 --- .../android/exoplayer2/castdemo/DemoUtil.java | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java index e45ceb7c83..735084495f 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DemoUtil.java @@ -88,23 +88,12 @@ import java.util.UUID; // App samples. ArrayList samples = new ArrayList<>(); + // Clear content. samples.add( new Sample( "https://storage.googleapis.com/wvmedia/clear/h264/tears/tears.mpd", "Clear DASH: Tears", MIME_TYPE_DASH)); - samples.add( - new Sample( - "https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/" - + "hls/TearsOfSteel.m3u8", - "Clear HLS: Tears of Steel", - MIME_TYPE_HLS)); - samples.add( - new Sample( - "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3" - + "/bipbop_4x3_variant.m3u8", - "Clear HLS: Basic 4x3", - MIME_TYPE_HLS)); samples.add( new Sample( "https://html5demos.com/assets/dizzy.mp4", "Clear MP4: Dizzy", MIME_TYPE_VIDEO_MP4)); From 3a54d744b94f38e626af696d7083d71b679d3494 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 23 Jan 2019 10:51:02 +0000 Subject: [PATCH 044/110] Default to respecting the DataSpec cache fragmentation flag Issue: #4253 PiperOrigin-RevId: 230497544 --- .../offline/DownloaderConstructorHelper.java | 4 +--- .../upstream/cache/CacheDataSink.java | 20 +------------------ .../upstream/cache/CacheDataSinkFactory.java | 13 ------------ 3 files changed, 2 insertions(+), 35 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java index 59a11934b1..48e70e37fc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloaderConstructorHelper.java @@ -110,10 +110,8 @@ public final class DownloaderConstructorHelper { ? cacheReadDataSourceFactory : new FileDataSourceFactory(); if (cacheWriteDataSinkFactory == null) { - CacheDataSinkFactory factory = + cacheWriteDataSinkFactory = new CacheDataSinkFactory(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE); - factory.experimental_setRespectCacheFragmentationFlag(true); - cacheWriteDataSinkFactory = factory; } onlineCacheDataSourceFactory = new CacheDataSourceFactory( diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index d527805120..2caf4c92f8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -50,7 +50,6 @@ public final class CacheDataSink implements DataSink { private final int bufferSize; private boolean syncFileDescriptor; - private boolean respectCacheFragmentationFlag; private DataSpec dataSpec; private long dataSpecFragmentSize; private File file; @@ -123,20 +122,6 @@ public final class CacheDataSink implements DataSink { this.syncFileDescriptor = syncFileDescriptor; } - /** - * Sets whether this instance respects the {@link DataSpec#FLAG_ALLOW_CACHE_FRAGMENTATION} flag. - * If set to {@code false} requests will always be fragmented. If set to {@code true} requests - * will be fragmented only if the flag is set. - * - *

    This method is experimental, and will be renamed or removed in a future release. - * - * @param respectCacheFragmentationFlag Whether to respect the {@link - * DataSpec#FLAG_ALLOW_CACHE_FRAGMENTATION} flag. - */ - public void experimental_setRespectCacheFragmentationFlag(boolean respectCacheFragmentationFlag) { - this.respectCacheFragmentationFlag = respectCacheFragmentationFlag; - } - @Override public void open(DataSpec dataSpec) throws CacheDataSinkException { if (dataSpec.length == C.LENGTH_UNSET @@ -146,10 +131,7 @@ public final class CacheDataSink implements DataSink { } this.dataSpec = dataSpec; this.dataSpecFragmentSize = - !respectCacheFragmentationFlag - || dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION) - ? fragmentSize - : Long.MAX_VALUE; + dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION) ? fragmentSize : Long.MAX_VALUE; dataSpecBytesWritten = 0; try { openNextOutputStream(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java index 9540597c2e..856e9db168 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSinkFactory.java @@ -27,7 +27,6 @@ public final class CacheDataSinkFactory implements DataSink.Factory { private final int bufferSize; private boolean syncFileDescriptor; - private boolean respectCacheFragmentationFlag; /** @see CacheDataSink#CacheDataSink(Cache, long) */ public CacheDataSinkFactory(Cache cache, long fragmentSize) { @@ -51,22 +50,10 @@ public final class CacheDataSinkFactory implements DataSink.Factory { return this; } - /** - * See {@link CacheDataSink#experimental_setRespectCacheFragmentationFlag(boolean)}. - * - *

    This method is experimental, and will be renamed or removed in a future release. - */ - public CacheDataSinkFactory experimental_setRespectCacheFragmentationFlag( - boolean respectCacheFragmentationFlag) { - this.respectCacheFragmentationFlag = respectCacheFragmentationFlag; - return this; - } - @Override public DataSink createDataSink() { CacheDataSink dataSink = new CacheDataSink(cache, fragmentSize, bufferSize); dataSink.experimental_setSyncFileDescriptor(syncFileDescriptor); - dataSink.experimental_setRespectCacheFragmentationFlag(respectCacheFragmentationFlag); return dataSink; } } From d7b382017506382531c492e5a27218d7317c7366 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 23 Jan 2019 14:30:01 +0000 Subject: [PATCH 045/110] Make updateQueuedPeriods more readable and fix bug. 1. The method kept track of the current period index to check if the next period is still in the correct period. This is unneccessary since we no longer use the period index but the actual uid in MediaPeriodId and mismatches are already detected by canKeepMediaPeriodHolder. 2. We updated the MediaPeriodIndfo twice: once in getFollowingMediaPeriodInfo and once in getUpdatedMediaPeriodInfo. That's confusing and difficult to follow. The only difference is that getUpdatedMediaPeriodInfo keeps the content position while getFollowingMediaPeriodInfo resets it. This is made more explicit for readability. 3. The durations compatibility check for all following periods was broken as it compared the same durations (partly due to the confusion caused by 2.) PiperOrigin-RevId: 230519295 --- .../exoplayer2/ExoPlayerImplInternal.java | 2 +- .../android/exoplayer2/MediaPeriodInfo.java | 27 ++++--- .../android/exoplayer2/MediaPeriodQueue.java | 71 ++++++++----------- .../android/exoplayer2/ExoPlayerTest.java | 20 +++--- 4 files changed, 59 insertions(+), 61 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index b4549362f3..bebc6224ec 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -1376,7 +1376,7 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - if (!queue.updateQueuedPeriods(playingPeriodId, rendererPositionUs)) { + if (!queue.updateQueuedPeriods(rendererPositionUs)) { seekToCurrentPosition(/* sendDiscontinuity= */ false); } handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java index e57100931e..01de53c4fb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java @@ -64,15 +64,26 @@ import com.google.android.exoplayer2.util.Util; this.isFinal = isFinal; } - /** Returns a copy of this instance with the start position set to the specified value. */ + /** + * Returns a copy of this instance with the start position set to the specified value. May return + * the same instance if nothing changed. + */ public MediaPeriodInfo copyWithStartPositionUs(long startPositionUs) { - return new MediaPeriodInfo( - id, - startPositionUs, - contentPositionUs, - durationUs, - isLastInTimelinePeriod, - isFinal); + return startPositionUs == this.startPositionUs + ? this + : new MediaPeriodInfo( + id, startPositionUs, contentPositionUs, durationUs, isLastInTimelinePeriod, isFinal); + } + + /** + * Returns a copy of this instance with the content position set to the specified value. May + * return the same instance if nothing changed. + */ + public MediaPeriodInfo copyWithContentPositionUs(long contentPositionUs) { + return contentPositionUs == this.contentPositionUs + ? this + : new MediaPeriodInfo( + id, startPositionUs, contentPositionUs, durationUs, isLastInTimelinePeriod, isFinal); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java index 7fa2abe149..4585b97529 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java @@ -61,8 +61,8 @@ import com.google.android.exoplayer2.util.Assertions; } /** - * Sets the {@link Timeline}. Call {@link #updateQueuedPeriods(MediaPeriodId, long)} to update the - * queued media periods to take into account the new timeline. + * Sets the {@link Timeline}. Call {@link #updateQueuedPeriods(long)} to update the queued media + * periods to take into account the new timeline. */ public void setTimeline(Timeline timeline) { this.timeline = timeline; @@ -292,54 +292,43 @@ import com.google.android.exoplayer2.util.Assertions; * current playback position. The method assumes that the first media period in the queue is still * consistent with the new timeline. * - * @param playingPeriodId The current playing media period identifier. * @param rendererPositionUs The current renderer position in microseconds. * @return Whether the timeline change has been handled completely. */ - public boolean updateQueuedPeriods(MediaPeriodId playingPeriodId, long rendererPositionUs) { + public boolean updateQueuedPeriods(long rendererPositionUs) { // TODO: Merge this into setTimeline so that the queue gets updated as soon as the new timeline // is set, once all cases handled by ExoPlayerImplInternal.handleSourceInfoRefreshed can be // handled here. - int periodIndex = timeline.getIndexOfPeriod(playingPeriodId.periodUid); - // The front period is either playing now, or is being loaded and will become the playing - // period. MediaPeriodHolder previousPeriodHolder = null; MediaPeriodHolder periodHolder = getFrontPeriod(); while (periodHolder != null) { + MediaPeriodInfo oldPeriodInfo = periodHolder.info; + + // Get period info based on new timeline. + MediaPeriodInfo newPeriodInfo; if (previousPeriodHolder == null) { - long previousDurationUs = periodHolder.info.durationUs; - periodHolder.info = getUpdatedMediaPeriodInfo(periodHolder.info); - if (!canKeepAfterMediaPeriodHolder(periodHolder, previousDurationUs)) { - return !removeAfter(periodHolder); - } + // The id and start position of the first period have already been verified by + // ExoPlayerImplInternal.handleSourceInfoRefreshed. Just update duration, isLastInTimeline + // and isLastInPeriod flags. + newPeriodInfo = getUpdatedMediaPeriodInfo(oldPeriodInfo); } else { - // Check this period holder still follows the previous one, based on the new timeline. - if (periodIndex == C.INDEX_UNSET - || !periodHolder.uid.equals(timeline.getUidOfPeriod(periodIndex))) { - // The holder uid is inconsistent with the new timeline. - return !removeAfter(previousPeriodHolder); - } - MediaPeriodInfo periodInfo = - getFollowingMediaPeriodInfo(previousPeriodHolder, rendererPositionUs); - if (periodInfo == null) { + newPeriodInfo = getFollowingMediaPeriodInfo(previousPeriodHolder, rendererPositionUs); + if (newPeriodInfo == null) { // We've loaded a next media period that is not in the new timeline. return !removeAfter(previousPeriodHolder); } - // Update the period holder. - periodHolder.info = getUpdatedMediaPeriodInfo(periodHolder.info); - // Check the media period information matches the new timeline. - if (!canKeepMediaPeriodHolder(periodHolder, periodInfo)) { + if (!canKeepMediaPeriodHolder(oldPeriodInfo, newPeriodInfo)) { + // The new media period has a different id or start position. return !removeAfter(previousPeriodHolder); - } else if (!canKeepAfterMediaPeriodHolder(periodHolder, periodInfo.durationUs)) { - return !removeAfter(periodHolder); } } - if (periodHolder.info.isLastInTimelinePeriod) { - // Move on to the next timeline period index, if there is one. - periodIndex = - timeline.getNextPeriodIndex( - periodIndex, period, window, repeatMode, shuffleModeEnabled); + // Use new period info, but keep old content position. + periodHolder.info = newPeriodInfo.copyWithContentPositionUs(oldPeriodInfo.contentPositionUs); + + if (!areDurationsCompatible(oldPeriodInfo.durationUs, newPeriodInfo.durationUs)) { + // The period duration changed. Remove all subsequent periods. + return !removeAfter(periodHolder); } previousPeriodHolder = periodHolder; @@ -465,22 +454,18 @@ import com.google.android.exoplayer2.util.Assertions; } /** - * Returns whether {@code periodHolder} can be kept for playing the media period described by - * {@code info}. + * Returns whether a period described by {@code oldInfo} can be kept for playing the media period + * described by {@code newInfo}. */ - private boolean canKeepMediaPeriodHolder(MediaPeriodHolder periodHolder, MediaPeriodInfo info) { - MediaPeriodInfo periodHolderInfo = periodHolder.info; - return periodHolderInfo.startPositionUs == info.startPositionUs - && periodHolderInfo.id.equals(info.id); + private boolean canKeepMediaPeriodHolder(MediaPeriodInfo oldInfo, MediaPeriodInfo newInfo) { + return oldInfo.startPositionUs == newInfo.startPositionUs && oldInfo.id.equals(newInfo.id); } /** - * Returns whether periods after {@code periodHolder} can be kept for playing given its previous - * duration. + * Returns whether a duration change of a period is compatible with keeping the following periods. */ - private boolean canKeepAfterMediaPeriodHolder( - MediaPeriodHolder periodHolder, long previousDurationUs) { - return previousDurationUs == C.TIME_UNSET || previousDurationUs == periodHolder.info.durationUs; + private boolean areDurationsCompatible(long previousDurationUs, long newDurationUs) { + return previousDurationUs == C.TIME_UNSET || previousDurationUs == newDurationUs; } /** diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index f957ac104c..7f01c02b49 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -2028,15 +2028,17 @@ public final class ExoPlayerTest { .blockUntilEnded(TIMEOUT_MS); testRunner.assertPlayedPeriodIndices(0, 1); // Assert that the second period was re-created from the new timeline. - assertThat(mediaSource.getCreatedMediaPeriods()) - .containsExactly( - new MediaPeriodId( - timeline1.getUidOfPeriod(/* periodIndex= */ 0), /* windowSequenceNumber= */ 0), - new MediaPeriodId( - timeline1.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 1), - new MediaPeriodId( - timeline2.getUidOfPeriod(/* periodIndex= */ 1), /* windowSequenceNumber= */ 2)) - .inOrder(); + assertThat(mediaSource.getCreatedMediaPeriods()).hasSize(3); + assertThat(mediaSource.getCreatedMediaPeriods().get(0).periodUid) + .isEqualTo(timeline1.getUidOfPeriod(/* periodIndex= */ 0)); + assertThat(mediaSource.getCreatedMediaPeriods().get(1).periodUid) + .isEqualTo(timeline1.getUidOfPeriod(/* periodIndex= */ 1)); + assertThat(mediaSource.getCreatedMediaPeriods().get(2).periodUid) + .isEqualTo(timeline2.getUidOfPeriod(/* periodIndex= */ 1)); + assertThat(mediaSource.getCreatedMediaPeriods().get(1).windowSequenceNumber) + .isGreaterThan(mediaSource.getCreatedMediaPeriods().get(0).windowSequenceNumber); + assertThat(mediaSource.getCreatedMediaPeriods().get(2).windowSequenceNumber) + .isGreaterThan(mediaSource.getCreatedMediaPeriods().get(1).windowSequenceNumber); } @Test From fd081d49c7f29baaea8e51eb5eb8721a96c4941b Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 23 Jan 2019 17:46:44 +0000 Subject: [PATCH 046/110] Make the Cast demo app absorb remote queue changes Necessary in two scenarios: + When the demo app starts casting to a receiver app that already had a queue. + When two demo apps are connected to the same receiver app and both make modifications. PiperOrigin-RevId: 230546851 --- .../DefaultReceiverPlayerManager.java | 33 ++++++------------- .../exoplayer2/castdemo/MainActivity.java | 13 +++++--- .../exoplayer2/castdemo/PlayerManager.java | 10 +++--- 3 files changed, 24 insertions(+), 32 deletions(-) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java index f4678fc541..563efea11f 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java @@ -63,7 +63,7 @@ import java.util.ArrayList; private final SimpleExoPlayer exoPlayer; private final CastPlayer castPlayer; private final ArrayList mediaQueue; - private final QueuePositionListener queuePositionListener; + private final QueueChangesListener queueChangesListener; private final ConcatenatingMediaSource concatenatingMediaSource; private boolean castMediaQueueCreationPending; @@ -71,32 +71,21 @@ import java.util.ArrayList; private Player currentPlayer; /** - * @param queuePositionListener A {@link QueuePositionListener} for queue position changes. + * Creates a new manager for {@link SimpleExoPlayer} and {@link CastPlayer}. + * + * @param queueChangesListener A {@link QueueChangesListener} for queue position changes. * @param localPlayerView The {@link PlayerView} for local playback. * @param castControlView The {@link PlayerControlView} to control remote playback. * @param context A {@link Context}. * @param castContext The {@link CastContext}. */ - public static DefaultReceiverPlayerManager createPlayerManager( - QueuePositionListener queuePositionListener, + public DefaultReceiverPlayerManager( + QueueChangesListener queueChangesListener, PlayerView localPlayerView, PlayerControlView castControlView, Context context, CastContext castContext) { - DefaultReceiverPlayerManager defaultReceiverPlayerManager = - new DefaultReceiverPlayerManager( - queuePositionListener, localPlayerView, castControlView, context, castContext); - defaultReceiverPlayerManager.init(); - return defaultReceiverPlayerManager; - } - - private DefaultReceiverPlayerManager( - QueuePositionListener queuePositionListener, - PlayerView localPlayerView, - PlayerControlView castControlView, - Context context, - CastContext castContext) { - this.queuePositionListener = queuePositionListener; + this.queueChangesListener = queueChangesListener; this.localPlayerView = localPlayerView; this.castControlView = castControlView; mediaQueue = new ArrayList<>(); @@ -113,6 +102,8 @@ import java.util.ArrayList; castPlayer.addListener(this); castPlayer.setSessionAvailabilityListener(this); castControlView.setPlayer(castPlayer); + + setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer); } // Queue manipulation methods. @@ -287,10 +278,6 @@ import java.util.ArrayList; // Internal methods. - private void init() { - setCurrentPlayer(castPlayer.isCastSessionAvailable() ? castPlayer : exoPlayer); - } - private void updateCurrentItemIndex() { int playbackState = currentPlayer.getPlaybackState(); maybeSetCurrentItemAndNotify( @@ -372,7 +359,7 @@ import java.util.ArrayList; if (this.currentItemIndex != currentItemIndex) { int oldIndex = this.currentItemIndex; this.currentItemIndex = currentItemIndex; - queuePositionListener.onQueuePositionChanged(oldIndex, currentItemIndex); + queueChangesListener.onQueuePositionChanged(oldIndex, currentItemIndex); } } diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 058adf7c9c..46e8273947 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -49,7 +49,7 @@ import java.util.Collections; * Cast extension. */ public class MainActivity extends AppCompatActivity - implements OnClickListener, PlayerManager.QueuePositionListener { + implements OnClickListener, PlayerManager.QueueChangesListener { private final MediaItem.Builder mediaItemBuilder; @@ -121,8 +121,8 @@ public class MainActivity extends AppCompatActivity switch (applicationId) { case CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID: playerManager = - DefaultReceiverPlayerManager.createPlayerManager( - /* queuePositionListener= */ this, + new DefaultReceiverPlayerManager( + /* queueChangesListener= */ this, localPlayerView, castControlView, /* context= */ this, @@ -162,7 +162,7 @@ public class MainActivity extends AppCompatActivity .show(); } - // PlayerManager.QueuePositionListener implementation. + // PlayerManager.QueueChangesListener implementation. @Override public void onQueuePositionChanged(int previousIndex, int newIndex) { @@ -174,6 +174,11 @@ public class MainActivity extends AppCompatActivity } } + @Override + public void onQueueContentsExternallyChanged() { + mediaQueueListAdapter.notifyDataSetChanged(); + } + // Internal methods. private View buildSampleListView() { diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index c56f0eb855..184dfe29b3 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -22,14 +22,14 @@ import com.google.android.exoplayer2.ext.cast.MediaItem; /** Manages the players in the Cast demo app. */ interface PlayerManager { - /** Listener for changes in the media queue playback position. */ - interface QueuePositionListener { + /** Listener for changes in the media queue. */ + interface QueueChangesListener { - /** - * Called when the currently played item of the media queue changes. - */ + /** Called when the currently played item of the media queue changes. */ void onQueuePositionChanged(int previousIndex, int newIndex); + /** Called when the media queue changes due to modifications not caused by this manager. */ + void onQueueContentsExternallyChanged(); } /** Redirects the given {@code keyEvent} to the active player. */ From 49b9775d0822c2310fb633a8e3cd779d11b9e029 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 24 Jan 2019 11:02:39 +0000 Subject: [PATCH 047/110] Add getReadingPositionUs to Renderer. This method is a generalization of the existing hasReadStreamToEnd. It is useful to determine whether a renderer already read beyond a new duration of a period. PiperOrigin-RevId: 230689165 --- .../android/exoplayer2/BaseRenderer.java | 20 ++++++++++++------- .../android/exoplayer2/NoSampleRenderer.java | 5 +++++ .../google/android/exoplayer2/Renderer.java | 10 ++++++++++ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java index 73602d85aa..79192ade15 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BaseRenderer.java @@ -37,7 +37,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { private SampleStream stream; private Format[] streamFormats; private long streamOffsetUs; - private boolean readEndOfStream; + private long readingPositionUs; private boolean streamIsFinal; /** @@ -46,7 +46,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { */ public BaseRenderer(int trackType) { this.trackType = trackType; - readEndOfStream = true; + readingPositionUs = C.TIME_END_OF_SOURCE; } @Override @@ -98,7 +98,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { throws ExoPlaybackException { Assertions.checkState(!streamIsFinal); this.stream = stream; - readEndOfStream = false; + readingPositionUs = offsetUs; streamFormats = formats; streamOffsetUs = offsetUs; onStreamChanged(formats, offsetUs); @@ -111,7 +111,12 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { @Override public final boolean hasReadStreamToEnd() { - return readEndOfStream; + return readingPositionUs == C.TIME_END_OF_SOURCE; + } + + @Override + public final long getReadingPositionUs() { + return readingPositionUs; } @Override @@ -132,7 +137,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { @Override public final void resetPosition(long positionUs) throws ExoPlaybackException { streamIsFinal = false; - readEndOfStream = false; + readingPositionUs = positionUs; onPositionReset(positionUs, false); } @@ -303,10 +308,11 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { int result = stream.readData(formatHolder, buffer, formatRequired); if (result == C.RESULT_BUFFER_READ) { if (buffer.isEndOfStream()) { - readEndOfStream = true; + readingPositionUs = C.TIME_END_OF_SOURCE; return streamIsFinal ? C.RESULT_BUFFER_READ : C.RESULT_NOTHING_READ; } buffer.timeUs += streamOffsetUs; + readingPositionUs = Math.max(readingPositionUs, buffer.timeUs); } else if (result == C.RESULT_FORMAT_READ) { Format format = formatHolder.format; if (format.subsampleOffsetUs != Format.OFFSET_SAMPLE_RELATIVE) { @@ -332,7 +338,7 @@ public abstract class BaseRenderer implements Renderer, RendererCapabilities { * Returns whether the upstream source is ready. */ protected final boolean isSourceReady() { - return readEndOfStream ? streamIsFinal : stream.isReady(); + return hasReadStreamToEnd() ? streamIsFinal : stream.isReady(); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java index 6645850d3b..e6223dfe16 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/NoSampleRenderer.java @@ -122,6 +122,11 @@ public abstract class NoSampleRenderer implements Renderer, RendererCapabilities return true; } + @Override + public long getReadingPositionUs() { + return C.TIME_END_OF_SOURCE; + } + @Override public final void setCurrentStreamFinal() { streamIsFinal = true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java index 1d4d587aeb..3434cc7603 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Renderer.java @@ -160,6 +160,16 @@ public interface Renderer extends PlayerMessage.Target { */ boolean hasReadStreamToEnd(); + /** + * Returns the playback position up to which the renderer has read samples from the current {@link + * SampleStream}, in microseconds, or {@link C#TIME_END_OF_SOURCE} if the renderer has read the + * current {@link SampleStream} to the end. + * + *

    This method may be called when the renderer is in the following states: {@link + * #STATE_ENABLED}, {@link #STATE_STARTED}. + */ + long getReadingPositionUs(); + /** * Signals to the renderer that the current {@link SampleStream} will be the final one supplied * before it is next disabled or reset. From f182c0c1169cba7c22280058368127c24609054f Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 24 Jan 2019 11:33:44 +0000 Subject: [PATCH 048/110] Centralize serialization in CachedContentIndex We need to support serialization to/from an SQLite table. The model of passing something around for each class to write into doesn't work well for SQL, and it would be messy to have two different structural designs for serialization. This change centralizes the logic in CachedContentIndex, where a centralized SQL based version can more easily sit alongside it. PiperOrigin-RevId: 230692291 --- .../upstream/cache/CachedContent.java | 70 ++------- .../upstream/cache/CachedContentIndex.java | 134 ++++++++++++++++-- .../cache/DefaultContentMetadata.java | 61 +------- .../cache/DefaultContentMetadataTest.java | 22 --- 4 files changed, 137 insertions(+), 150 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java index 5494454d54..64ef33e3c9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java @@ -18,17 +18,11 @@ package com.google.android.exoplayer2.upstream.cache; import android.support.annotation.Nullable; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.util.Assertions; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; import java.util.TreeSet; /** Defines the cached content for a single stream. */ /* package */ final class CachedContent { - private static final int VERSION_METADATA_INTRODUCED = 2; - private static final int VERSION_MAX = Integer.MAX_VALUE; - /** The cache file id that uniquely identifies the original stream. */ public final int id; /** The cache key that uniquely identifies the original stream. */ @@ -40,29 +34,6 @@ import java.util.TreeSet; /** Whether the content is locked. */ private boolean locked; - /** - * Reads an instance from a {@link DataInputStream}. - * - * @param version Version of the encoded data. - * @param input Input stream containing values needed to initialize CachedContent instance. - * @throws IOException If an error occurs during reading values. - */ - public static CachedContent readFromStream(int version, DataInputStream input) - throws IOException { - int id = input.readInt(); - String key = input.readUTF(); - CachedContent cachedContent = new CachedContent(id, key); - if (version < VERSION_METADATA_INTRODUCED) { - long length = input.readLong(); - ContentMetadataMutations mutations = new ContentMetadataMutations(); - ContentMetadataMutations.setContentLength(mutations, length); - cachedContent.applyMetadataMutations(mutations); - } else { - cachedContent.metadata = DefaultContentMetadata.readFromStream(input); - } - return cachedContent; - } - /** * Creates a CachedContent. * @@ -70,26 +41,18 @@ import java.util.TreeSet; * @param key The cache stream key. */ public CachedContent(int id, String key) { + this(id, key, DefaultContentMetadata.EMPTY); + } + + public CachedContent(int id, String key, DefaultContentMetadata metadata) { this.id = id; this.key = key; - this.metadata = DefaultContentMetadata.EMPTY; + this.metadata = metadata; this.cachedSpans = new TreeSet<>(); } - /** - * Writes the instance to a {@link DataOutputStream}. - * - * @param output Output stream to store the values. - * @throws IOException If an error occurs during writing values to output. - */ - public void writeToStream(DataOutputStream output) throws IOException { - output.writeInt(id); - output.writeUTF(key); - metadata.writeToStream(output); - } - /** Returns the metadata. */ - public ContentMetadata getMetadata() { + public DefaultContentMetadata getMetadata() { return metadata; } @@ -208,26 +171,11 @@ import java.util.TreeSet; return false; } - /** - * Calculates a hash code for the header of this {@code CachedContent} which is compatible with - * the index file with {@code version}. - */ - public int headerHashCode(int version) { - int result = id; - result = 31 * result + key.hashCode(); - if (version < VERSION_METADATA_INTRODUCED) { - long length = ContentMetadata.getContentLength(metadata); - result = 31 * result + (int) (length ^ (length >>> 32)); - } else { - result = 31 * result + metadata.hashCode(); - } - return result; - } - @Override public int hashCode() { - int result = headerHashCode(VERSION_MAX); - result = 31 * result + cachedSpans.hashCode(); + int result = id; + result = 31 * result + key.hashCode(); + result = 31 * result + metadata.hashCode(); return result; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index a744917230..f2def1c110 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -33,8 +33,10 @@ import java.io.OutputStream; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; +import java.util.Map; import java.util.Random; import java.util.Set; import javax.crypto.Cipher; @@ -51,6 +53,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; public static final String FILE_NAME = "cached_content_index.exi"; private static final int VERSION = 2; + private static final int VERSION_METADATA_INTRODUCED = 2; + private static final int INCREMENTAL_METADATA_READ_LENGTH = 10 * 1024 * 1024; private static final int FLAG_ENCRYPTED_INDEX = 1; @@ -245,6 +249,19 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return cachedContent != null ? cachedContent.getMetadata() : DefaultContentMetadata.EMPTY; } + private CachedContent addNew(String key) { + int id = getNewId(idToKey); + CachedContent cachedContent = new CachedContent(id, key); + add(cachedContent); + changed = true; + return cachedContent; + } + + private void add(CachedContent cachedContent) { + keyToContent.put(cachedContent.key, cachedContent); + idToKey.put(cachedContent.id, cachedContent.key); + } + private boolean readFile() { DataInputStream input = null; try { @@ -276,9 +293,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; int count = input.readInt(); int hashCode = 0; for (int i = 0; i < count; i++) { - CachedContent cachedContent = CachedContent.readFromStream(version, input); + CachedContent cachedContent = readCachedContent(version, input); add(cachedContent); - hashCode += cachedContent.headerHashCode(version); + hashCode += hashCachedContent(cachedContent, version); } int fileHashCode = input.readInt(); boolean isEOF = input.read() == -1; @@ -327,8 +344,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; output.writeInt(keyToContent.size()); int hashCode = 0; for (CachedContent cachedContent : keyToContent.values()) { - cachedContent.writeToStream(output); - hashCode += cachedContent.headerHashCode(VERSION); + writeCachedContent(cachedContent, output); + hashCode += hashCachedContent(cachedContent, VERSION); } output.writeInt(hashCode); atomicFile.endWrite(output); @@ -342,17 +359,108 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } } - private CachedContent addNew(String key) { - int id = getNewId(idToKey); - CachedContent cachedContent = new CachedContent(id, key); - add(cachedContent); - changed = true; - return cachedContent; + /** + * Calculates a hash code for a {@link CachedContent} which is compatible with a particular index + * version. + */ + private int hashCachedContent(CachedContent cachedContent, int version) { + int result = cachedContent.id; + result = 31 * result + cachedContent.key.hashCode(); + if (version < VERSION_METADATA_INTRODUCED) { + long length = ContentMetadata.getContentLength(cachedContent.getMetadata()); + result = 31 * result + (int) (length ^ (length >>> 32)); + } else { + result = 31 * result + cachedContent.getMetadata().hashCode(); + } + return result; } - private void add(CachedContent cachedContent) { - keyToContent.put(cachedContent.key, cachedContent); - idToKey.put(cachedContent.id, cachedContent.key); + /** + * Reads a {@link CachedContent} from a {@link DataInputStream}. + * + * @param version Version of the encoded data. + * @param input Input stream containing values needed to initialize CachedContent instance. + * @throws IOException If an error occurs during reading values. + */ + private static CachedContent readCachedContent(int version, DataInputStream input) + throws IOException { + int id = input.readInt(); + String key = input.readUTF(); + DefaultContentMetadata metadata; + if (version < VERSION_METADATA_INTRODUCED) { + long length = input.readLong(); + ContentMetadataMutations mutations = new ContentMetadataMutations(); + ContentMetadataMutations.setContentLength(mutations, length); + metadata = DefaultContentMetadata.EMPTY.copyWithMutationsApplied(mutations); + } else { + metadata = readContentMetadata(input); + } + return new CachedContent(id, key, metadata); + } + + /** + * Writes a {@link CachedContent} to a {@link DataOutputStream}. + * + * @param output Output stream to store the values. + * @throws IOException If an error occurs during writing values to output. + */ + private static void writeCachedContent(CachedContent cachedContent, DataOutputStream output) + throws IOException { + output.writeInt(cachedContent.id); + output.writeUTF(cachedContent.key); + writeContentMetadata(cachedContent.getMetadata(), output); + } + + /** + * Deserializes a {@link DefaultContentMetadata} from the given input stream. + * + * @param input Input stream to read from. + * @return a {@link DefaultContentMetadata} instance. + * @throws IOException If an error occurs during reading from input. + */ + private static DefaultContentMetadata readContentMetadata(DataInputStream input) + throws IOException { + int size = input.readInt(); + HashMap metadata = new HashMap<>(); + for (int i = 0; i < size; i++) { + String name = input.readUTF(); + int valueSize = input.readInt(); + if (valueSize < 0) { + throw new IOException("Invalid value size: " + valueSize); + } + // Grow the array incrementally to avoid OutOfMemoryError in the case that a corrupt (and very + // large) valueSize was read. In such cases the implementation below is expected to throw + // IOException from one of the readFully calls, due to the end of the input being reached. + int bytesRead = 0; + int nextBytesToRead = Math.min(valueSize, INCREMENTAL_METADATA_READ_LENGTH); + byte[] value = Util.EMPTY_BYTE_ARRAY; + while (bytesRead != valueSize) { + value = Arrays.copyOf(value, bytesRead + nextBytesToRead); + input.readFully(value, bytesRead, nextBytesToRead); + bytesRead += nextBytesToRead; + nextBytesToRead = Math.min(valueSize - bytesRead, INCREMENTAL_METADATA_READ_LENGTH); + } + metadata.put(name, value); + } + return new DefaultContentMetadata(metadata); + } + + /** + * Serializes itself to a {@link DataOutputStream}. + * + * @param output Output stream to store the values. + * @throws IOException If an error occurs during writing values to output. + */ + private static void writeContentMetadata(DefaultContentMetadata metadata, DataOutputStream output) + throws IOException { + Set> entrySet = metadata.entrySet(); + output.writeInt(entrySet.size()); + for (Map.Entry entry : entrySet) { + output.writeUTF(entry.getKey()); + byte[] value = entry.getValue(); + output.writeInt(value.length); + output.write(value); + } } private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java index 843dd19444..9e878ebfbd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadata.java @@ -17,9 +17,6 @@ package com.google.android.exoplayer2.upstream.cache; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.Arrays; @@ -28,6 +25,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; /** Default implementation of {@link ContentMetadata}. Values are stored as byte arrays. */ public final class DefaultContentMetadata implements ContentMetadata { @@ -36,39 +34,16 @@ public final class DefaultContentMetadata implements ContentMetadata { public static final DefaultContentMetadata EMPTY = new DefaultContentMetadata(Collections.emptyMap()); - private static final int MAX_VALUE_LENGTH = 10 * 1024 * 1024; private int hashCode; - /** - * Deserializes a {@link DefaultContentMetadata} from the given input stream. - * - * @param input Input stream to read from. - * @return a {@link DefaultContentMetadata} instance. - * @throws IOException If an error occurs during reading from input. - */ - public static DefaultContentMetadata readFromStream(DataInputStream input) throws IOException { - int size = input.readInt(); - HashMap metadata = new HashMap<>(); - for (int i = 0; i < size; i++) { - String name = input.readUTF(); - int valueSize = input.readInt(); - if (valueSize < 0 || valueSize > MAX_VALUE_LENGTH) { - throw new IOException("Invalid value size: " + valueSize); - } - byte[] value = new byte[valueSize]; - input.readFully(value); - metadata.put(name, value); - } - return new DefaultContentMetadata(metadata); - } - private final Map metadata; public DefaultContentMetadata() { this(Collections.emptyMap()); } - private DefaultContentMetadata(Map metadata) { + /** @param metadata The metadata entries in their raw byte array form. */ + public DefaultContentMetadata(Map metadata) { this.metadata = Collections.unmodifiableMap(metadata); } @@ -84,20 +59,9 @@ public final class DefaultContentMetadata implements ContentMetadata { return new DefaultContentMetadata(mutatedMetadata); } - /** - * Serializes itself to a {@link DataOutputStream}. - * - * @param output Output stream to store the values. - * @throws IOException If an error occurs during writing values to output. - */ - public void writeToStream(DataOutputStream output) throws IOException { - output.writeInt(metadata.size()); - for (Entry entry : metadata.entrySet()) { - output.writeUTF(entry.getKey()); - byte[] value = entry.getValue(); - output.writeInt(value.length); - output.write(value); - } + /** Returns the set of metadata entries in their raw byte array form. */ + public Set> entrySet() { + return metadata.entrySet(); } @Override @@ -190,18 +154,7 @@ public final class DefaultContentMetadata implements ContentMetadata { private static void addValues(HashMap metadata, Map values) { for (String name : values.keySet()) { - Object value = values.get(name); - byte[] bytes = getBytes(value); - if (bytes.length > MAX_VALUE_LENGTH) { - throw new IllegalArgumentException( - "The size of " - + name - + " (" - + bytes.length - + ") is greater than maximum allowed: " - + MAX_VALUE_LENGTH); - } - metadata.put(name, bytes); + metadata.put(name, getBytes(values.get(name))); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadataTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadataTest.java index e1dc68eac6..e4ec278c22 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadataTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/DefaultContentMetadataTest.java @@ -17,10 +17,6 @@ package com.google.android.exoplayer2.upstream.cache; import static com.google.common.truth.Truth.assertThat; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -133,24 +129,6 @@ public class DefaultContentMetadataTest { assertThat(contentMetadata.get("metadata name", "default value")).isEqualTo("value"); } - @Test - public void testSerializeDeserialize() throws Exception { - byte[] metadata3 = {1, 2, 3}; - contentMetadata = - createContentMetadata( - "metadata1 name", "value", "metadata2 name", 12345, "metadata3 name", metadata3); - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - contentMetadata.writeToStream(new DataOutputStream(outputStream)); - ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); - DefaultContentMetadata contentMetadata2 = - DefaultContentMetadata.readFromStream(new DataInputStream(inputStream)); - - assertThat(contentMetadata2.get("metadata1 name", "default value")).isEqualTo("value"); - assertThat(contentMetadata2.get("metadata2 name", 0)).isEqualTo(12345); - assertThat(contentMetadata2.get("metadata3 name", new byte[] {})).isEqualTo(metadata3); - } - @Test public void testEqualsStringValues() throws Exception { DefaultContentMetadata metadata1 = createContentMetadata("metadata1", "value"); From 2c54b8346466128b81edcac879d728d6196e5eda Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 24 Jan 2019 11:50:58 +0000 Subject: [PATCH 049/110] Move CachedContentIndex storage behind an interface This interface will get an SQLite implementation in a subsequent CL PiperOrigin-RevId: 230693881 --- .../upstream/cache/CachedContentIndex.java | 461 ++++++++++-------- 1 file changed, 268 insertions(+), 193 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index f2def1c110..e11b5b922c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream.cache; +import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.util.SparseArray; import android.util.SparseBooleanArray; @@ -83,12 +84,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; */ private final SparseBooleanArray removedIds; - private final AtomicFile atomicFile; - private final Cipher cipher; - private final SecretKeySpec secretKeySpec; - private final boolean encrypt; - private boolean changed; - private ReusableBufferedOutputStream bufferedOutputStream; + private final Storage storage; /** * Creates a CachedContentIndex which works on the index file in the given cacheDir. @@ -118,7 +114,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; * secretKey} is null. */ public CachedContentIndex(File cacheDir, byte[] secretKey, boolean encrypt) { - this.encrypt = encrypt; + Cipher cipher = null; + SecretKeySpec secretKeySpec = null; if (secretKey != null) { Assertions.checkArgument(secretKey.length == 16); try { @@ -129,20 +126,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } } else { Assertions.checkState(!encrypt); - cipher = null; - secretKeySpec = null; } keyToContent = new HashMap<>(); idToKey = new SparseArray<>(); removedIds = new SparseBooleanArray(); - atomicFile = new AtomicFile(new File(cacheDir, FILE_NAME)); + storage = new AtomicFileStorage(new File(cacheDir, FILE_NAME), encrypt, cipher, secretKeySpec); } /** Loads the index file. */ public void load() { - Assertions.checkState(!changed); - if (!readFile()) { - atomicFile.delete(); + if (!storage.load(keyToContent, idToKey)) { keyToContent.clear(); idToKey.clear(); } @@ -150,11 +143,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Stores the index data to index file if there is a change. */ public void store() throws CacheException { - if (!changed) { - return; - } - writeFile(); - changed = false; + storage.store(keyToContent); // Make ids that were removed since the index was last stored eligible for re-use. int removedIdCount = removedIds.size(); for (int i = 0; i < removedIdCount; i++) { @@ -205,7 +194,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; CachedContent cachedContent = keyToContent.get(key); if (cachedContent != null && cachedContent.isEmpty() && !cachedContent.isLocked()) { keyToContent.remove(key); - changed = true; + storage.onRemove(cachedContent); // Keep an entry in idToKey to stop the id from being reused until the index is next stored. idToKey.put(cachedContent.id, /* value= */ null); // Track that the entry should be removed from idToKey when the index is next stored. @@ -239,7 +228,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; public void applyContentMetadataMutations(String key, ContentMetadataMutations mutations) { CachedContent cachedContent = getOrAdd(key); if (cachedContent.applyMetadataMutations(mutations)) { - changed = true; + storage.onUpdate(cachedContent); } } @@ -252,163 +241,42 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private CachedContent addNew(String key) { int id = getNewId(idToKey); CachedContent cachedContent = new CachedContent(id, key); - add(cachedContent); - changed = true; + keyToContent.put(cachedContent.key, cachedContent); + idToKey.put(cachedContent.id, cachedContent.key); + storage.onUpdate(cachedContent); return cachedContent; } - private void add(CachedContent cachedContent) { - keyToContent.put(cachedContent.key, cachedContent); - idToKey.put(cachedContent.id, cachedContent.key); - } - - private boolean readFile() { - DataInputStream input = null; - try { - InputStream inputStream = new BufferedInputStream(atomicFile.openRead()); - input = new DataInputStream(inputStream); - int version = input.readInt(); - if (version < 0 || version > VERSION) { - return false; - } - - int flags = input.readInt(); - if ((flags & FLAG_ENCRYPTED_INDEX) != 0) { - if (cipher == null) { - return false; - } - byte[] initializationVector = new byte[16]; - input.readFully(initializationVector); - IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector); - try { - cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); - } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { - throw new IllegalStateException(e); - } - input = new DataInputStream(new CipherInputStream(inputStream, cipher)); - } else if (encrypt) { - changed = true; // Force index to be rewritten encrypted after read. - } - - int count = input.readInt(); - int hashCode = 0; - for (int i = 0; i < count; i++) { - CachedContent cachedContent = readCachedContent(version, input); - add(cachedContent); - hashCode += hashCachedContent(cachedContent, version); - } - int fileHashCode = input.readInt(); - boolean isEOF = input.read() == -1; - if (fileHashCode != hashCode || !isEOF) { - return false; - } - } catch (IOException e) { - return false; - } finally { - if (input != null) { - Util.closeQuietly(input); + private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException { + // Workaround for https://issuetracker.google.com/issues/36976726 + if (Util.SDK_INT == 18) { + try { + return Cipher.getInstance("AES/CBC/PKCS5PADDING", "BC"); + } catch (Throwable ignored) { + // ignored } } - return true; - } - - private void writeFile() throws CacheException { - DataOutputStream output = null; - try { - OutputStream outputStream = atomicFile.startWrite(); - if (bufferedOutputStream == null) { - bufferedOutputStream = new ReusableBufferedOutputStream(outputStream); - } else { - bufferedOutputStream.reset(outputStream); - } - output = new DataOutputStream(bufferedOutputStream); - output.writeInt(VERSION); - - int flags = encrypt ? FLAG_ENCRYPTED_INDEX : 0; - output.writeInt(flags); - - if (encrypt) { - byte[] initializationVector = new byte[16]; - new Random().nextBytes(initializationVector); - output.write(initializationVector); - IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector); - try { - cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); - } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { - throw new IllegalStateException(e); // Should never happen. - } - output.flush(); - output = new DataOutputStream(new CipherOutputStream(bufferedOutputStream, cipher)); - } - - output.writeInt(keyToContent.size()); - int hashCode = 0; - for (CachedContent cachedContent : keyToContent.values()) { - writeCachedContent(cachedContent, output); - hashCode += hashCachedContent(cachedContent, VERSION); - } - output.writeInt(hashCode); - atomicFile.endWrite(output); - // Avoid calling close twice. Duplicate CipherOutputStream.close calls did - // not used to be no-ops: https://android-review.googlesource.com/#/c/272799/ - output = null; - } catch (IOException e) { - throw new CacheException(e); - } finally { - Util.closeQuietly(output); - } + return Cipher.getInstance("AES/CBC/PKCS5PADDING"); } /** - * Calculates a hash code for a {@link CachedContent} which is compatible with a particular index - * version. + * Returns an id which isn't used in the given array. If the maximum id in the array is smaller + * than {@link java.lang.Integer#MAX_VALUE} it just returns the next bigger integer. Otherwise it + * returns the smallest unused non-negative integer. */ - private int hashCachedContent(CachedContent cachedContent, int version) { - int result = cachedContent.id; - result = 31 * result + cachedContent.key.hashCode(); - if (version < VERSION_METADATA_INTRODUCED) { - long length = ContentMetadata.getContentLength(cachedContent.getMetadata()); - result = 31 * result + (int) (length ^ (length >>> 32)); - } else { - result = 31 * result + cachedContent.getMetadata().hashCode(); + @VisibleForTesting + /* package */ static int getNewId(SparseArray idToKey) { + int size = idToKey.size(); + int id = size == 0 ? 0 : (idToKey.keyAt(size - 1) + 1); + if (id < 0) { // In case if we pass max int value. + // TODO optimization: defragmentation or binary search? + for (id = 0; id < size; id++) { + if (id != idToKey.keyAt(id)) { + break; + } + } } - return result; - } - - /** - * Reads a {@link CachedContent} from a {@link DataInputStream}. - * - * @param version Version of the encoded data. - * @param input Input stream containing values needed to initialize CachedContent instance. - * @throws IOException If an error occurs during reading values. - */ - private static CachedContent readCachedContent(int version, DataInputStream input) - throws IOException { - int id = input.readInt(); - String key = input.readUTF(); - DefaultContentMetadata metadata; - if (version < VERSION_METADATA_INTRODUCED) { - long length = input.readLong(); - ContentMetadataMutations mutations = new ContentMetadataMutations(); - ContentMetadataMutations.setContentLength(mutations, length); - metadata = DefaultContentMetadata.EMPTY.copyWithMutationsApplied(mutations); - } else { - metadata = readContentMetadata(input); - } - return new CachedContent(id, key, metadata); - } - - /** - * Writes a {@link CachedContent} to a {@link DataOutputStream}. - * - * @param output Output stream to store the values. - * @throws IOException If an error occurs during writing values to output. - */ - private static void writeCachedContent(CachedContent cachedContent, DataOutputStream output) - throws IOException { - output.writeInt(cachedContent.id); - output.writeUTF(cachedContent.key); - writeContentMetadata(cachedContent.getMetadata(), output); + return id; } /** @@ -463,36 +331,243 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } } - private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException { - // Workaround for https://issuetracker.google.com/issues/36976726 - if (Util.SDK_INT == 18) { - try { - return Cipher.getInstance("AES/CBC/PKCS5PADDING", "BC"); - } catch (Throwable ignored) { - // ignored - } - } - return Cipher.getInstance("AES/CBC/PKCS5PADDING"); + /** Interface for the persistent index. */ + private interface Storage { + + /** + * Loads the persisted index into {@code content} and {@code idToKey}. + * + * @param content The key to content map to populate with persisted data. + * @param idToKey The id to key map to populate with persisted data. + * @return Whether the load was successful. + */ + boolean load(HashMap content, SparseArray<@NullableType String> idToKey); + + /** + * Ensures all changes in the in-memory table are persisted. + * + * @param content The key to content map to persist. + * @throws CacheException If an error occurs persisting the index. + */ + void store(HashMap content) throws CacheException; + + /** + * Called when a {@link CachedContent} is added or updated in the in-memory index. + * + * @param cachedContent The updated {@link CachedContent}. + */ + void onUpdate(CachedContent cachedContent); + + /** + * Called when a {@link CachedContent} is removed from the in-memory index. + * + * @param cachedContent The removed {@link CachedContent}. + */ + void onRemove(CachedContent cachedContent); } - /** - * Returns an id which isn't used in the given array. If the maximum id in the array is smaller - * than {@link java.lang.Integer#MAX_VALUE} it just returns the next bigger integer. Otherwise it - * returns the smallest unused non-negative integer. - */ - @VisibleForTesting - public static int getNewId(SparseArray idToKey) { - int size = idToKey.size(); - int id = size == 0 ? 0 : (idToKey.keyAt(size - 1) + 1); - if (id < 0) { // In case if we pass max int value. - // TODO optimization: defragmentation or binary search? - for (id = 0; id < size; id++) { - if (id != idToKey.keyAt(id)) { - break; + /** {@link Storage} implementation that uses an {@link AtomicFile}. */ + private static class AtomicFileStorage implements Storage { + + private final boolean encrypt; + @Nullable private final Cipher cipher; + @Nullable private final SecretKeySpec secretKeySpec; + private final AtomicFile atomicFile; + private final Random random; + + private boolean changed; + @Nullable private ReusableBufferedOutputStream bufferedOutputStream; + + public AtomicFileStorage( + File fileName, + boolean encrypt, + @Nullable Cipher cipher, + @Nullable SecretKeySpec secretKeySpec) { + this.encrypt = encrypt; + this.cipher = cipher; + this.secretKeySpec = secretKeySpec; + atomicFile = new AtomicFile(fileName); + random = new Random(); + } + + @Override + public boolean load( + HashMap content, SparseArray<@NullableType String> idToKey) { + Assertions.checkState(!changed); + if (!readFile(content, idToKey)) { + atomicFile.delete(); + return false; + } + return true; + } + + @Override + public void store(HashMap content) throws CacheException { + if (!changed) { + return; + } + writeFile(content); + changed = false; + } + + @Override + public void onUpdate(CachedContent cachedContent) { + changed = true; + } + + @Override + public void onRemove(CachedContent cachedContent) { + changed = true; + } + + private boolean readFile( + HashMap content, SparseArray<@NullableType String> idToKey) { + DataInputStream input = null; + try { + InputStream inputStream = new BufferedInputStream(atomicFile.openRead()); + input = new DataInputStream(inputStream); + int version = input.readInt(); + if (version < 0 || version > VERSION) { + return false; + } + + int flags = input.readInt(); + if ((flags & FLAG_ENCRYPTED_INDEX) != 0) { + if (cipher == null) { + return false; + } + byte[] initializationVector = new byte[16]; + input.readFully(initializationVector); + IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector); + try { + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + throw new IllegalStateException(e); + } + input = new DataInputStream(new CipherInputStream(inputStream, cipher)); + } else if (encrypt) { + changed = true; // Force index to be rewritten encrypted after read. + } + + int count = input.readInt(); + int hashCode = 0; + for (int i = 0; i < count; i++) { + CachedContent cachedContent = readCachedContent(version, input); + content.put(cachedContent.key, cachedContent); + idToKey.put(cachedContent.id, cachedContent.key); + hashCode += hashCachedContent(cachedContent, version); + } + int fileHashCode = input.readInt(); + boolean isEOF = input.read() == -1; + if (fileHashCode != hashCode || !isEOF) { + return false; + } + } catch (IOException e) { + return false; + } finally { + if (input != null) { + Util.closeQuietly(input); } } + return true; } - return id; - } + private void writeFile(HashMap content) throws CacheException { + DataOutputStream output = null; + try { + OutputStream outputStream = atomicFile.startWrite(); + if (bufferedOutputStream == null) { + bufferedOutputStream = new ReusableBufferedOutputStream(outputStream); + } else { + bufferedOutputStream.reset(outputStream); + } + output = new DataOutputStream(bufferedOutputStream); + output.writeInt(VERSION); + + int flags = encrypt ? FLAG_ENCRYPTED_INDEX : 0; + output.writeInt(flags); + + if (encrypt) { + byte[] initializationVector = new byte[16]; + random.nextBytes(initializationVector); + output.write(initializationVector); + IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector); + try { + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + throw new IllegalStateException(e); // Should never happen. + } + output.flush(); + output = new DataOutputStream(new CipherOutputStream(bufferedOutputStream, cipher)); + } + + output.writeInt(content.size()); + int hashCode = 0; + for (CachedContent cachedContent : content.values()) { + writeCachedContent(cachedContent, output); + hashCode += hashCachedContent(cachedContent, VERSION); + } + output.writeInt(hashCode); + atomicFile.endWrite(output); + // Avoid calling close twice. Duplicate CipherOutputStream.close calls did + // not used to be no-ops: https://android-review.googlesource.com/#/c/272799/ + output = null; + } catch (IOException e) { + throw new CacheException(e); + } finally { + Util.closeQuietly(output); + } + } + + /** + * Calculates a hash code for a {@link CachedContent} which is compatible with a particular + * index version. + */ + private int hashCachedContent(CachedContent cachedContent, int version) { + int result = cachedContent.id; + result = 31 * result + cachedContent.key.hashCode(); + if (version < VERSION_METADATA_INTRODUCED) { + long length = ContentMetadata.getContentLength(cachedContent.getMetadata()); + result = 31 * result + (int) (length ^ (length >>> 32)); + } else { + result = 31 * result + cachedContent.getMetadata().hashCode(); + } + return result; + } + + /** + * Reads a {@link CachedContent} from a {@link DataInputStream}. + * + * @param version Version of the encoded data. + * @param input Input stream containing values needed to initialize CachedContent instance. + * @throws IOException If an error occurs during reading values. + */ + private CachedContent readCachedContent(int version, DataInputStream input) throws IOException { + int id = input.readInt(); + String key = input.readUTF(); + DefaultContentMetadata metadata; + if (version < VERSION_METADATA_INTRODUCED) { + long length = input.readLong(); + ContentMetadataMutations mutations = new ContentMetadataMutations(); + ContentMetadataMutations.setContentLength(mutations, length); + metadata = DefaultContentMetadata.EMPTY.copyWithMutationsApplied(mutations); + } else { + metadata = readContentMetadata(input); + } + return new CachedContent(id, key, metadata); + } + + /** + * Writes a {@link CachedContent} to a {@link DataOutputStream}. + * + * @param output Output stream to store the values. + * @throws IOException If an error occurs during writing values to output. + */ + private void writeCachedContent(CachedContent cachedContent, DataOutputStream output) + throws IOException { + output.writeInt(cachedContent.id); + output.writeUTF(cachedContent.key); + writeContentMetadata(cachedContent.getMetadata(), output); + } + } } From 355b3d6334f48e185d4d471a4d154147d551b471 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 24 Jan 2019 13:01:36 +0000 Subject: [PATCH 050/110] Increase minSdkVersion to 16 The combination of pre-16 API levels accounting for ~0.5% of the device population, and that the most important components in ExoPlayer (e.g. the MediaCodec renderers) have always required API level 16, mean it's very unlikely this will negatively impact on anyone. PiperOrigin-RevId: 230701808 --- RELEASENOTES.md | 1 + constants.gradle | 6 +-- demos/cast/build.gradle | 2 +- demos/ima/build.gradle | 2 +- demos/main/build.gradle | 2 +- extensions/cast/build.gradle | 2 +- extensions/cronet/build.gradle | 2 +- extensions/rtmp/build.gradle | 2 +- .../android/exoplayer2/SimpleExoPlayer.java | 1 - .../audio/MediaCodecAudioRenderer.java | 2 - .../exoplayer2/decoder/CryptoInfo.java | 51 ++++++++----------- .../android/exoplayer2/drm/DrmSession.java | 2 - .../exoplayer2/drm/DrmSessionManager.java | 2 - .../exoplayer2/drm/FrameworkMediaCrypto.java | 2 - .../exoplayer2/mediacodec/MediaCodecInfo.java | 1 - .../mediacodec/MediaCodecRenderer.java | 4 +- .../exoplayer2/mediacodec/MediaCodecUtil.java | 1 - .../mediacodec/MediaFormatUtil.java | 2 - .../exoplayer2/scheduler/Requirements.java | 13 +---- .../android/exoplayer2/util/GlUtil.java | 2 - .../google/android/exoplayer2/util/Util.java | 11 +--- .../video/MediaCodecVideoRenderer.java | 1 - .../video/VideoFrameReleaseTimeHelper.java | 1 - .../extractor/mp4/Mp4ExtractorTest.java | 2 - .../extractor/rawcc/RawCcExtractorTest.java | 2 - .../android/exoplayer2/ui/DefaultTimeBar.java | 14 ++--- .../exoplayer2/ui/SimpleExoPlayerView.java | 2 - .../ui/spherical/CanvasRenderer.java | 2 - .../ui/spherical/ProjectionRenderer.java | 2 - .../ui/spherical/SphericalSurfaceView.java | 2 - .../playbacktests/gts/DashDownloadTest.java | 4 -- .../playbacktests/gts/DashStreamingTest.java | 14 ++--- .../playbacktests/gts/DashTestRunner.java | 1 - .../testutil/DebugRenderersFactory.java | 1 - 34 files changed, 39 insertions(+), 122 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5894e991d0..6afe9bda35 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,7 @@ ### dev-v2 (not yet released) ### +* Increase `minSdkVersion` to 16 (Jellybean). * Support for playing spherical videos on Daydream. * Improve decoder re-use between playbacks. TODO: Write and link a blog post here ([#2826](https://github.com/google/ExoPlayer/issues/2826)). diff --git a/constants.gradle b/constants.gradle index 716ddbadba..062438fa30 100644 --- a/constants.gradle +++ b/constants.gradle @@ -15,11 +15,7 @@ project.ext { // ExoPlayer version and version code. releaseVersion = '2.9.4' releaseVersionCode = 2009004 - // Important: ExoPlayer specifies a minSdkVersion of 14 because various - // components provided by the library may be of use on older devices. - // However, please note that the core media playback functionality provided - // by the library requires API level 16 or greater. - minSdkVersion = 14 + minSdkVersion = 16 targetSdkVersion = 28 compileSdkVersion = 28 buildToolsVersion = '28.0.2' diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle index 8af52a787e..e056530d45 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -26,7 +26,7 @@ android { defaultConfig { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode - minSdkVersion 16 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion } diff --git a/demos/ima/build.gradle b/demos/ima/build.gradle index 33cca6ef46..0530c42eb7 100644 --- a/demos/ima/build.gradle +++ b/demos/ima/build.gradle @@ -26,7 +26,7 @@ android { defaultConfig { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode - minSdkVersion 16 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion } diff --git a/demos/main/build.gradle b/demos/main/build.gradle index c516ba297f..e3e382f6d0 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -26,7 +26,7 @@ android { defaultConfig { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode - minSdkVersion 16 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion } diff --git a/extensions/cast/build.gradle b/extensions/cast/build.gradle index 0baa074d4a..fea8363960 100644 --- a/extensions/cast/build.gradle +++ b/extensions/cast/build.gradle @@ -24,7 +24,7 @@ android { } defaultConfig { - minSdkVersion 14 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion consumerProguardFiles 'proguard-rules.txt' } diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index 7d8c217b58..f77e84c816 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -19,7 +19,7 @@ android { buildToolsVersion project.ext.buildToolsVersion defaultConfig { - minSdkVersion 16 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion } diff --git a/extensions/rtmp/build.gradle b/extensions/rtmp/build.gradle index af02ee2eaa..4869df7a1a 100644 --- a/extensions/rtmp/build.gradle +++ b/extensions/rtmp/build.gradle @@ -24,7 +24,7 @@ android { } defaultConfig { - minSdkVersion 15 + minSdkVersion project.ext.minSdkVersion targetSdkVersion project.ext.targetSdkVersion } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index e498038fde..35924a01fb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -63,7 +63,6 @@ import java.util.concurrent.CopyOnWriteArraySet; * An {@link ExoPlayer} implementation that uses default {@link Renderer} components. Instances can * be obtained from {@link ExoPlayerFactory}. */ -@TargetApi(16) public class SimpleExoPlayer extends BasePlayer implements ExoPlayer, Player.AudioComponent, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 7fc6c16db8..04c6b2ec9c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.audio; import android.annotation.SuppressLint; -import android.annotation.TargetApi; import android.content.Context; import android.media.MediaCodec; import android.media.MediaCrypto; @@ -66,7 +65,6 @@ import java.util.List; * underlying audio track. * */ -@TargetApi(16) public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock { /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java index ec17de8d74..379ca971b5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/CryptoInfo.java @@ -62,7 +62,7 @@ public final class CryptoInfo { private final PatternHolderV24 patternHolder; public CryptoInfo() { - frameworkCryptoInfo = Util.SDK_INT >= 16 ? newFrameworkCryptoInfoV16() : null; + frameworkCryptoInfo = new android.media.MediaCodec.CryptoInfo(); patternHolder = Util.SDK_INT >= 24 ? new PatternHolderV24(frameworkCryptoInfo) : null; } @@ -79,34 +79,8 @@ public final class CryptoInfo { this.mode = mode; this.encryptedBlocks = encryptedBlocks; this.clearBlocks = clearBlocks; - if (Util.SDK_INT >= 16) { - updateFrameworkCryptoInfoV16(); - } - } - - /** - * Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance. - *

    - * Successive calls to this method on a single {@link CryptoInfo} will return the same instance. - * Changes to the {@link CryptoInfo} will be reflected in the returned object. The return object - * should not be modified directly. - * - * @return The equivalent {@link android.media.MediaCodec.CryptoInfo} instance. - */ - @TargetApi(16) - public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfoV16() { - return frameworkCryptoInfo; - } - - @TargetApi(16) - private android.media.MediaCodec.CryptoInfo newFrameworkCryptoInfoV16() { - return new android.media.MediaCodec.CryptoInfo(); - } - - @TargetApi(16) - private void updateFrameworkCryptoInfoV16() { - // Update fields directly because the framework's CryptoInfo.set performs an unnecessary object - // allocation on Android N. + // Update frameworkCryptoInfo fields directly because CryptoInfo.set performs an unnecessary + // object allocation on Android N. frameworkCryptoInfo.numSubSamples = numSubSamples; frameworkCryptoInfo.numBytesOfClearData = numBytesOfClearData; frameworkCryptoInfo.numBytesOfEncryptedData = numBytesOfEncryptedData; @@ -118,6 +92,25 @@ public final class CryptoInfo { } } + /** + * Returns an equivalent {@link android.media.MediaCodec.CryptoInfo} instance. + * + *

    Successive calls to this method on a single {@link CryptoInfo} will return the same + * instance. Changes to the {@link CryptoInfo} will be reflected in the returned object. The + * return object should not be modified directly. + * + * @return The equivalent {@link android.media.MediaCodec.CryptoInfo} instance. + */ + public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfo() { + return frameworkCryptoInfo; + } + + /** @deprecated Use {@link #getFrameworkCryptoInfo()}. */ + @Deprecated + public android.media.MediaCodec.CryptoInfo getFrameworkCryptoInfoV16() { + return getFrameworkCryptoInfo(); + } + @TargetApi(24) private static final class PatternHolderV24 { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java index a68415287e..a23f26f067 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSession.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.drm; -import android.annotation.TargetApi; import android.media.MediaDrm; import android.support.annotation.IntDef; import android.support.annotation.Nullable; @@ -27,7 +26,6 @@ import java.util.Map; /** * A DRM session. */ -@TargetApi(16) public interface DrmSession { /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java index cf3d97d0b2..d8093507a4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/DrmSessionManager.java @@ -15,14 +15,12 @@ */ package com.google.android.exoplayer2.drm; -import android.annotation.TargetApi; import android.os.Looper; import com.google.android.exoplayer2.drm.DrmInitData.SchemeData; /** * Manages a DRM session. */ -@TargetApi(16) public interface DrmSessionManager { /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java index 156138ab9b..7211b5fcde 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/drm/FrameworkMediaCrypto.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.drm; -import android.annotation.TargetApi; import android.media.MediaCrypto; import java.util.UUID; @@ -23,7 +22,6 @@ import java.util.UUID; * An {@link ExoMediaCrypto} implementation that contains the necessary information to build or * update a framework {@link MediaCrypto}. */ -@TargetApi(16) public final class FrameworkMediaCrypto implements ExoMediaCrypto { /** The DRM scheme UUID. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index 107ab9efd8..c9493e1208 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -31,7 +31,6 @@ import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; /** Information about a {@link MediaCodec} for a given mime type. */ -@TargetApi(16) @SuppressWarnings("InlinedApi") public final class MediaCodecInfo { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 35f5c14f3f..e3fcf9397b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -58,7 +58,6 @@ import java.util.List; /** * An abstract renderer that uses {@link MediaCodec} to decode samples for rendering. */ -@TargetApi(16) public abstract class MediaCodecRenderer extends BaseRenderer { /** @@ -366,7 +365,6 @@ public abstract class MediaCodecRenderer extends BaseRenderer { boolean playClearSamplesWithoutKeys, float assumedMinimumCodecOperatingRate) { super(trackType); - Assertions.checkState(Util.SDK_INT >= 16); this.mediaCodecSelector = Assertions.checkNotNull(mediaCodecSelector); this.drmSessionManager = drmSessionManager; this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; @@ -1662,7 +1660,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { private static MediaCodec.CryptoInfo getFrameworkCryptoInfo( DecoderInputBuffer buffer, int adaptiveReconfigurationBytes) { - MediaCodec.CryptoInfo cryptoInfo = buffer.cryptoInfo.getFrameworkCryptoInfoV16(); + MediaCodec.CryptoInfo cryptoInfo = buffer.cryptoInfo.getFrameworkCryptoInfo(); if (adaptiveReconfigurationBytes == 0) { return cryptoInfo; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 9ae50179c3..ae3a9f123f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -39,7 +39,6 @@ import java.util.regex.Pattern; /** * A utility class for querying the available codecs. */ -@TargetApi(16) @SuppressLint("InlinedApi") public final class MediaCodecUtil { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java index 3cfefc0736..95cc5d4a37 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaFormatUtil.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.mediacodec; -import android.annotation.TargetApi; import android.media.MediaFormat; import android.support.annotation.Nullable; import com.google.android.exoplayer2.Format; @@ -24,7 +23,6 @@ import java.nio.ByteBuffer; import java.util.List; /** Helper class for configuring {@link MediaFormat} instances. */ -@TargetApi(16) public final class MediaFormatUtil { private MediaFormatUtil() {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java index 77630a4543..508c3393c5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/Requirements.java @@ -200,7 +200,7 @@ public final class Requirements { logd("Roaming: " + roaming); return !roaming; } - boolean activeNetworkMetered = isActiveNetworkMetered(connectivityManager, networkInfo); + boolean activeNetworkMetered = connectivityManager.isActiveNetworkMetered(); logd("Metered network: " + activeNetworkMetered); if (networkRequirement == NETWORK_TYPE_UNMETERED) { return !activeNetworkMetered; @@ -255,17 +255,6 @@ public final class Requirements { return !validated; } - private static boolean isActiveNetworkMetered( - ConnectivityManager connectivityManager, NetworkInfo networkInfo) { - if (Util.SDK_INT >= 16) { - return connectivityManager.isActiveNetworkMetered(); - } - int type = networkInfo.getType(); - return type != ConnectivityManager.TYPE_WIFI - && type != ConnectivityManager.TYPE_BLUETOOTH - && type != ConnectivityManager.TYPE_ETHERNET; - } - private static void logd(String message) { if (Scheduler.DEBUG) { Log.d(TAG, message); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java index 9387392ec4..915e855d23 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/GlUtil.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.util; import static android.opengl.GLU.gluErrorString; -import android.annotation.TargetApi; import android.opengl.GLES11Ext; import android.opengl.GLES20; import android.text.TextUtils; @@ -114,7 +113,6 @@ public final class GlUtil { * Creates a GL_TEXTURE_EXTERNAL_OES with default configuration of GL_LINEAR filtering and * GL_CLAMP_TO_EDGE wrapping. */ - @TargetApi(15) public static int createExternalTexture() { int[] texId = new int[1]; GLES20.glGenTextures(1, IntBuffer.wrap(texId)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 1e1153d367..34237dddf6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -1844,10 +1844,8 @@ public final class Util { getDisplaySizeV23(display, displaySize); } else if (Util.SDK_INT >= 17) { getDisplaySizeV17(display, displaySize); - } else if (Util.SDK_INT >= 16) { - getDisplaySizeV16(display, displaySize); } else { - getDisplaySizeV9(display, displaySize); + getDisplaySizeV16(display, displaySize); } return displaySize; } @@ -1903,17 +1901,10 @@ public final class Util { display.getRealSize(outSize); } - @TargetApi(16) private static void getDisplaySizeV16(Display display, Point outSize) { display.getSize(outSize); } - @SuppressWarnings("deprecation") - private static void getDisplaySizeV9(Display display, Point outSize) { - outSize.x = display.getWidth(); - outSize.y = display.getHeight(); - } - private static @C.NetworkType int getMobileNetworkType(NetworkInfo networkInfo) { switch (networkInfo.getSubtype()) { case TelephonyManager.NETWORK_TYPE_EDGE: diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index 6943fea7b6..d8ad2a840b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -68,7 +68,6 @@ import java.util.List; * a {@link android.view.SurfaceView}. * */ -@TargetApi(16) public class MediaCodecVideoRenderer extends MediaCodecRenderer { private static final String TAG = "MediaCodecVideoRenderer"; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java index 3c0fb92191..c7e34d00e9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoFrameReleaseTimeHelper.java @@ -32,7 +32,6 @@ import com.google.android.exoplayer2.util.Util; /** * Makes a best effort to adjust frame release timestamps for a smoother visual result. */ -@TargetApi(16) public final class VideoFrameReleaseTimeHelper { private static final long CHOREOGRAPHER_SAMPLE_DELAY_MILLIS = 500; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java index 8850a755be..3c84214686 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/mp4/Mp4ExtractorTest.java @@ -15,14 +15,12 @@ */ package com.google.android.exoplayer2.extractor.mp4; -import android.annotation.TargetApi; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; /** Tests for {@link Mp4Extractor}. */ -@TargetApi(16) @RunWith(RobolectricTestRunner.class) public final class Mp4ExtractorTest { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java index 62ad774fd3..9551c9df29 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/extractor/rawcc/RawCcExtractorTest.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.extractor.rawcc; -import android.annotation.TargetApi; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.util.MimeTypes; @@ -24,7 +23,6 @@ import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; /** Tests for {@link RawCcExtractor}. */ -@TargetApi(16) @RunWith(RobolectricTestRunner.class) public final class RawCcExtractorTest { diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java index 75c4f71b64..0c3d39a13a 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DefaultTimeBar.java @@ -319,8 +319,8 @@ public class DefaultTimeBar extends View implements TimeBar { keyTimeIncrement = C.TIME_UNSET; keyCountIncrement = DEFAULT_INCREMENT_COUNT; setFocusable(true); - if (Util.SDK_INT >= 16) { - maybeSetImportantForAccessibilityV16(); + if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); } } @@ -611,13 +611,12 @@ public class DefaultTimeBar extends View implements TimeBar { if (Util.SDK_INT >= 21) { info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD); info.addAction(AccessibilityAction.ACTION_SCROLL_BACKWARD); - } else if (Util.SDK_INT >= 16) { + } else { info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); } } - @TargetApi(16) @Override public boolean performAccessibilityAction(int action, @Nullable Bundle args) { if (super.performAccessibilityAction(action, args)) { @@ -643,13 +642,6 @@ public class DefaultTimeBar extends View implements TimeBar { // Internal methods. - @TargetApi(16) - private void maybeSetImportantForAccessibilityV16() { - if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { - setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); - } - } - private void startScrubbing() { scrubbing = true; setPressed(true); diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 55745a7cb5..b01e7a308c 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.ui; -import android.annotation.TargetApi; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -25,7 +24,6 @@ import com.google.android.exoplayer2.SimpleExoPlayer; /** @deprecated Use {@link PlayerView}. */ @Deprecated -@TargetApi(16) public final class SimpleExoPlayerView extends PlayerView { public SimpleExoPlayerView(Context context) { diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/CanvasRenderer.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/CanvasRenderer.java index 5538390d3b..955418acf7 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/CanvasRenderer.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/CanvasRenderer.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.ui.spherical; import static com.google.android.exoplayer2.util.GlUtil.checkGlError; -import android.annotation.TargetApi; import android.graphics.Canvas; import android.graphics.PointF; import android.graphics.Rect; @@ -39,7 +38,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; *

    A CanvasRenderer can be created on any thread, but {@link #init()} needs to be called on the * GL thread before it can be rendered. */ -@TargetApi(15) public final class CanvasRenderer { private static final float WIDTH_UNIT = 0.8f; diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/ProjectionRenderer.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/ProjectionRenderer.java index 5e8a6d71d2..f24bcce3ce 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/ProjectionRenderer.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/ProjectionRenderer.java @@ -17,7 +17,6 @@ package com.google.android.exoplayer2.ui.spherical; import static com.google.android.exoplayer2.util.GlUtil.checkGlError; -import android.annotation.TargetApi; import android.opengl.GLES11Ext; import android.opengl.GLES20; import com.google.android.exoplayer2.C; @@ -30,7 +29,6 @@ import org.checkerframework.checker.nullness.qual.Nullable; * Utility class to render spherical meshes for video or images. Call {@link #init()} on the GL * thread when ready. */ -@TargetApi(15) /* package */ final class ProjectionRenderer { /** diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java index 36589c5e34..adbeb7773d 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/spherical/SphericalSurfaceView.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer2.ui.spherical; -import android.annotation.TargetApi; import android.content.Context; import android.graphics.PointF; import android.graphics.SurfaceTexture; @@ -52,7 +51,6 @@ import javax.microedition.khronos.opengles.GL10; * apply the touch and sensor rotations in the correct order or the user's touch manipulations won't * match what they expect. */ -@TargetApi(15) public final class SphericalSurfaceView extends GLSurfaceView { /** diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java index 0dd05e7fd3..7d000de4b0 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashDownloadTest.java @@ -89,10 +89,6 @@ public final class DashDownloadTest { @Test public void testDownload() throws Exception { - if (Util.SDK_INT < 16) { - return; // Pass. - } - DashDownloader dashDownloader = downloadContent(); dashDownloader.download(); diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java index 9a54ffd07c..56806183cc 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java @@ -103,10 +103,6 @@ public final class DashStreamingTest { @Test public void testH264Fixed() { - if (Util.SDK_INT < 16) { - // Pass. - return; - } testRunner .setStreamName("test_h264_fixed") .setManifestUrl(DashTestData.H264_MANIFEST) @@ -118,7 +114,7 @@ public final class DashStreamingTest { @Test public void testH264Adaptive() throws DecoderQueryException { - if (Util.SDK_INT < 16 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { + if (shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { // Pass. return; } @@ -134,7 +130,7 @@ public final class DashStreamingTest { @Test public void testH264AdaptiveWithSeeking() throws DecoderQueryException { - if (Util.SDK_INT < 16 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { + if (shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { // Pass. return; } @@ -152,7 +148,7 @@ public final class DashStreamingTest { @Test public void testH264AdaptiveWithRendererDisabling() throws DecoderQueryException { - if (Util.SDK_INT < 16 || shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { + if (shouldSkipAdaptiveTest(MimeTypes.VIDEO_H264)) { // Pass. return; } @@ -633,10 +629,6 @@ public final class DashStreamingTest { @Test public void testDecoderInfoH264() throws DecoderQueryException { - if (Util.SDK_INT < 16) { - // Pass. - return; - } MediaCodecInfo decoderInfo = MediaCodecUtil.getDecoderInfo(MimeTypes.VIDEO_H264, false); assertThat(decoderInfo).isNotNull(); assertThat(Util.SDK_INT < 21 || decoderInfo.adaptive).isTrue(); diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java index eb69cc88da..2446094136 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java @@ -205,7 +205,6 @@ public final class DashTestRunner { /** * A {@link HostedTest} for DASH playback tests. */ - @TargetApi(16) private static final class DashHostedTest extends ExoHostedTest { private final String streamName; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java index 39194d48fe..d480d50b98 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java @@ -39,7 +39,6 @@ import java.util.ArrayList; * A debug extension of {@link DefaultRenderersFactory}. Provides a video renderer that performs * video buffer timestamp assertions. */ -@TargetApi(16) public class DebugRenderersFactory extends DefaultRenderersFactory { public DebugRenderersFactory(Context context) { From 642a0275d84b060f11e19122dca69498a1792a25 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 24 Jan 2019 15:26:59 +0000 Subject: [PATCH 051/110] Propagate ExoCast errors receiver from the receiver app PiperOrigin-RevId: 230717561 --- .../exoplayer2/ExoPlaybackException.java | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java index 6b84245141..d5ceb3db30 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2; import android.support.annotation.IntDef; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; @@ -34,7 +35,7 @@ public final class ExoPlaybackException extends Exception { */ @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({TYPE_SOURCE, TYPE_RENDERER, TYPE_UNEXPECTED}) + @IntDef({TYPE_SOURCE, TYPE_RENDERER, TYPE_UNEXPECTED, TYPE_REMOTE}) public @interface Type {} /** * The error occurred loading data from a {@link MediaSource}. @@ -54,6 +55,12 @@ public final class ExoPlaybackException extends Exception { * Call {@link #getUnexpectedException()} to retrieve the underlying cause. */ public static final int TYPE_UNEXPECTED = 2; + /** + * The error occurred in a remote component. + * + *

    Call {@link #getMessage()} to retrieve the message associated with the error. + */ + public static final int TYPE_REMOTE = 3; /** * The type of the playback failure. One of {@link #TYPE_SOURCE}, {@link #TYPE_RENDERER} and @@ -66,7 +73,7 @@ public final class ExoPlaybackException extends Exception { */ public final int rendererIndex; - private final Throwable cause; + @Nullable private final Throwable cause; /** * Creates an instance of type {@link #TYPE_SOURCE}. @@ -99,6 +106,16 @@ public final class ExoPlaybackException extends Exception { return new ExoPlaybackException(TYPE_UNEXPECTED, cause, C.INDEX_UNSET); } + /** + * Creates an instance of type {@link #TYPE_REMOTE}. + * + * @param message The message associated with the error. + * @return The created instance. + */ + public static ExoPlaybackException createForRemote(String message) { + return new ExoPlaybackException(TYPE_REMOTE, message); + } + private ExoPlaybackException(@Type int type, Throwable cause, int rendererIndex) { super(cause); this.type = type; @@ -106,6 +123,13 @@ public final class ExoPlaybackException extends Exception { this.rendererIndex = rendererIndex; } + private ExoPlaybackException(@Type int type, String message) { + super(message); + this.type = type; + rendererIndex = C.INDEX_UNSET; + cause = null; + } + /** * Retrieves the underlying error when {@link #type} is {@link #TYPE_SOURCE}. * @@ -113,7 +137,7 @@ public final class ExoPlaybackException extends Exception { */ public IOException getSourceException() { Assertions.checkState(type == TYPE_SOURCE); - return (IOException) cause; + return (IOException) Assertions.checkNotNull(cause); } /** @@ -123,7 +147,7 @@ public final class ExoPlaybackException extends Exception { */ public Exception getRendererException() { Assertions.checkState(type == TYPE_RENDERER); - return (Exception) cause; + return (Exception) Assertions.checkNotNull(cause); } /** @@ -133,7 +157,7 @@ public final class ExoPlaybackException extends Exception { */ public RuntimeException getUnexpectedException() { Assertions.checkState(type == TYPE_UNEXPECTED); - return (RuntimeException) cause; + return (RuntimeException) Assertions.checkNotNull(cause); } } From 9d19a7a4b67e8ad3e3b3cec0ebc883367e45a261 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 24 Jan 2019 17:19:51 +0000 Subject: [PATCH 052/110] Move parseSelectionFlags with the rest of the parse{attribute} methods PiperOrigin-RevId: 230734189 --- .../hls/playlist/HlsPlaylistParser.java | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 242711431c..7dba626e70 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -426,21 +426,6 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variableDefinitions) throws ParserException { String keyFormatVersions = From 6dde1e67d3a3462cf6a40d290a76bcf32147652e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 24 Jan 2019 18:12:27 +0000 Subject: [PATCH 053/110] Parse CHANNELS attribute from EXT-X-MEDIA PiperOrigin-RevId: 230743198 --- RELEASENOTES.md | 3 ++- .../hls/playlist/HlsPlaylistParser.java | 11 ++++++++- .../playlist/HlsMasterPlaylistParserTest.java | 23 +++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6afe9bda35..91e3c1127b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,7 +2,8 @@ ### dev-v2 (not yet released) ### -* Increase `minSdkVersion` to 16 (Jellybean). +* HLS: + * Parse CHANNELS attribute from EXT-X-MEDIA. * Support for playing spherical videos on Daydream. * Improve decoder re-use between playbacks. TODO: Write and link a blog post here ([#2826](https://github.com/google/ExoPlayer/issues/2826)). diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java index 7dba626e70..9e13d6fa0f 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/playlist/HlsPlaylistParser.java @@ -101,6 +101,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser variableDefinitions) { + String channelsString = parseOptionalStringAttr(line, REGEX_CHANNELS, variableDefinitions); + return channelsString != null + ? Integer.parseInt(Util.splitAtFirst(channelsString, "/")[0]) + : Format.NO_VALUE; + } + private static @Nullable SchemeData parsePlayReadySchemeData( String line, Map variableDefinitions) throws ParserException { String keyFormatVersions = diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index 9701171ce9..8b69ba0db2 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -81,6 +81,18 @@ public class HlsMasterPlaylistParserTest { + "CODECS=\"mp4a.40.2,avc1.66.30\",RESOLUTION=304x128\n" + "http://example.com/low.m3u8\n"; + private static final String PLAYLIST_WITH_CHANNELS_ATTRIBUTE = + " #EXTM3U \n" + + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"audio\",CHANNELS=\"6\",NAME=\"Eng6\"," + + "URI=\"something.m3u8\"\n" + + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"audio\",CHANNELS=\"2/6\",NAME=\"Eng26\"," + + "URI=\"something2.m3u8\"\n" + + "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"audio\",NAME=\"Eng\"," + + "URI=\"something3.m3u8\"\n" + + "#EXT-X-STREAM-INF:BANDWIDTH=1280000," + + "CODECS=\"mp4a.40.2,avc1.66.30\",AUDIO=\"audio\",RESOLUTION=304x128\n" + + "http://example.com/low.m3u8\n"; + private static final String PLAYLIST_WITHOUT_CC = " #EXTM3U \n" + "#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS," @@ -216,6 +228,17 @@ public class HlsMasterPlaylistParserTest { assertThat(closedCaptionFormat.language).isEqualTo("es"); } + @Test + public void testPlaylistWithChannelsAttribute() throws IOException { + HlsMasterPlaylist playlist = + parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITH_CHANNELS_ATTRIBUTE); + List audios = playlist.audios; + assertThat(audios).hasSize(3); + assertThat(audios.get(0).format.channelCount).isEqualTo(6); + assertThat(audios.get(1).format.channelCount).isEqualTo(2); + assertThat(audios.get(2).format.channelCount).isEqualTo(Format.NO_VALUE); + } + @Test public void testPlaylistWithoutClosedCaptions() throws IOException { HlsMasterPlaylist playlist = parseMasterPlaylist(PLAYLIST_URI, PLAYLIST_WITHOUT_CC); From 582adb748ae1c1beff071b962f5a3ef08d6f01b7 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 25 Jan 2019 11:42:22 +0000 Subject: [PATCH 054/110] Decouple end position from MediaPeriodId again. If are allowing changing durations of periods, we shouldn't use the end position of clipped content as part of the id as it may change. This change moves the end position back to MediaPeriodInfo and adds the next ad group index to the id instead to ensure we still have unique ids for all content parts. PiperOrigin-RevId: 230878389 --- .../android/exoplayer2/MediaPeriodHolder.java | 23 +++++---- .../android/exoplayer2/MediaPeriodInfo.java | 29 ++++++++++-- .../android/exoplayer2/MediaPeriodQueue.java | 23 +++++---- .../exoplayer2/source/MediaSource.java | 47 ++++++++++++------- .../exoplayer2/MediaPeriodQueueTest.java | 40 +++++++++++----- 5 files changed, 108 insertions(+), 54 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java index 19622c6801..be3fde0fca 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodHolder.java @@ -89,7 +89,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; this.info = info; sampleStreams = new SampleStream[rendererCapabilities.length]; mayRetainStreamFlags = new boolean[rendererCapabilities.length]; - mediaPeriod = createMediaPeriod(info.id, mediaSource, allocator, info.startPositionUs); + mediaPeriod = + createMediaPeriod( + info.id, mediaSource, allocator, info.startPositionUs, info.endPositionUs); } /** @@ -294,7 +296,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; public void release() { disableTrackSelectionsInResult(); trackSelectorResult = null; - releaseMediaPeriod(info.id, mediaSource, mediaPeriod); + releaseMediaPeriod(info.endPositionUs, mediaSource, mediaPeriod); } /** @@ -399,24 +401,25 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Returns a media period corresponding to the given {@code id}. */ private static MediaPeriod createMediaPeriod( - MediaPeriodId id, MediaSource mediaSource, Allocator allocator, long startPositionUs) { + MediaPeriodId id, + MediaSource mediaSource, + Allocator allocator, + long startPositionUs, + long endPositionUs) { MediaPeriod mediaPeriod = mediaSource.createPeriod(id, allocator, startPositionUs); - if (id.endPositionUs != C.TIME_UNSET && id.endPositionUs != C.TIME_END_OF_SOURCE) { + if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) { mediaPeriod = new ClippingMediaPeriod( - mediaPeriod, - /* enableInitialDiscontinuity= */ true, - /* startUs= */ 0, - id.endPositionUs); + mediaPeriod, /* enableInitialDiscontinuity= */ true, /* startUs= */ 0, endPositionUs); } return mediaPeriod; } /** Releases the given {@code mediaPeriod}, logging and suppressing any errors. */ private static void releaseMediaPeriod( - MediaPeriodId id, MediaSource mediaSource, MediaPeriod mediaPeriod) { + long endPositionUs, MediaSource mediaSource, MediaPeriod mediaPeriod) { try { - if (id.endPositionUs != C.TIME_UNSET && id.endPositionUs != C.TIME_END_OF_SOURCE) { + if (endPositionUs != C.TIME_UNSET && endPositionUs != C.TIME_END_OF_SOURCE) { mediaSource.releasePeriod(((ClippingMediaPeriod) mediaPeriod).mediaPeriod); } else { mediaSource.releasePeriod(mediaPeriod); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java index 01de53c4fb..cd4e74b2ef 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodInfo.java @@ -33,7 +33,14 @@ import com.google.android.exoplayer2.util.Util; */ public final long contentPositionUs; /** - * The duration of the media period, like {@link MediaPeriodId#endPositionUs} but with {@link + * The end position to which the media period's content is clipped in order to play a following ad + * group, in microseconds, or {@link C#TIME_UNSET} if there is no following ad group or if this + * media period is an ad. The value {@link C#TIME_END_OF_SOURCE} indicates that a postroll ad + * follows at the end of this content media period. + */ + public final long endPositionUs; + /** + * The duration of the media period, like {@link #endPositionUs} but with {@link * C#TIME_END_OF_SOURCE} and {@link C#TIME_UNSET} resolved to the timeline period duration if * known. */ @@ -53,12 +60,14 @@ import com.google.android.exoplayer2.util.Util; MediaPeriodId id, long startPositionUs, long contentPositionUs, + long endPositionUs, long durationUs, boolean isLastInTimelinePeriod, boolean isFinal) { this.id = id; this.startPositionUs = startPositionUs; this.contentPositionUs = contentPositionUs; + this.endPositionUs = endPositionUs; this.durationUs = durationUs; this.isLastInTimelinePeriod = isLastInTimelinePeriod; this.isFinal = isFinal; @@ -72,7 +81,13 @@ import com.google.android.exoplayer2.util.Util; return startPositionUs == this.startPositionUs ? this : new MediaPeriodInfo( - id, startPositionUs, contentPositionUs, durationUs, isLastInTimelinePeriod, isFinal); + id, + startPositionUs, + contentPositionUs, + endPositionUs, + durationUs, + isLastInTimelinePeriod, + isFinal); } /** @@ -83,7 +98,13 @@ import com.google.android.exoplayer2.util.Util; return contentPositionUs == this.contentPositionUs ? this : new MediaPeriodInfo( - id, startPositionUs, contentPositionUs, durationUs, isLastInTimelinePeriod, isFinal); + id, + startPositionUs, + contentPositionUs, + endPositionUs, + durationUs, + isLastInTimelinePeriod, + isFinal); } @Override @@ -97,6 +118,7 @@ import com.google.android.exoplayer2.util.Util; MediaPeriodInfo that = (MediaPeriodInfo) o; return startPositionUs == that.startPositionUs && contentPositionUs == that.contentPositionUs + && endPositionUs == that.endPositionUs && durationUs == that.durationUs && isLastInTimelinePeriod == that.isLastInTimelinePeriod && isFinal == that.isFinal @@ -109,6 +131,7 @@ import com.google.android.exoplayer2.util.Util; result = 31 * result + id.hashCode(); result = 31 * result + (int) startPositionUs; result = 31 * result + (int) contentPositionUs; + result = 31 * result + (int) endPositionUs; result = 31 * result + (int) durationUs; result = 31 * result + (isLastInTimelinePeriod ? 1 : 0); result = 31 * result + (isFinal ? 1 : 0); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java index 4585b97529..d6ff320295 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java @@ -353,13 +353,14 @@ import com.google.android.exoplayer2.util.Assertions; long durationUs = id.isAd() ? period.getAdDurationUs(id.adGroupIndex, id.adIndexInAdGroup) - : (id.endPositionUs == C.TIME_UNSET || id.endPositionUs == C.TIME_END_OF_SOURCE + : (info.endPositionUs == C.TIME_UNSET || info.endPositionUs == C.TIME_END_OF_SOURCE ? period.getDurationUs() - : id.endPositionUs); + : info.endPositionUs); return new MediaPeriodInfo( id, info.startPositionUs, info.contentPositionUs, + info.endPositionUs, durationUs, isLastInPeriod, isLastInTimeline); @@ -398,11 +399,7 @@ import com.google.android.exoplayer2.util.Assertions; int adGroupIndex = period.getAdGroupIndexForPositionUs(positionUs); if (adGroupIndex == C.INDEX_UNSET) { int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(positionUs); - long endPositionUs = - nextAdGroupIndex == C.INDEX_UNSET - ? C.TIME_UNSET - : period.getAdGroupTimeUs(nextAdGroupIndex); - return new MediaPeriodId(periodUid, windowSequenceNumber, endPositionUs); + return new MediaPeriodId(periodUid, windowSequenceNumber, nextAdGroupIndex); } else { int adIndexInAdGroup = period.getFirstAdIndexToPlay(adGroupIndex); return new MediaPeriodId(periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber); @@ -630,7 +627,7 @@ import com.google.android.exoplayer2.util.Assertions; } } else { // Play the next ad group if it's available. - int nextAdGroupIndex = period.getAdGroupIndexForPositionUs(mediaPeriodInfo.id.endPositionUs); + int nextAdGroupIndex = period.getAdGroupIndexForPositionUs(mediaPeriodInfo.endPositionUs); if (nextAdGroupIndex == C.INDEX_UNSET) { // The next ad group can't be played. Play content from the previous end position instead. return getMediaPeriodInfoForContent( @@ -688,6 +685,7 @@ import com.google.android.exoplayer2.util.Assertions; id, startPositionUs, contentPositionUs, + /* endPositionUs= */ C.TIME_UNSET, durationUs, /* isLastInTimelinePeriod= */ false, /* isFinal= */ false); @@ -696,13 +694,13 @@ import com.google.android.exoplayer2.util.Assertions; private MediaPeriodInfo getMediaPeriodInfoForContent( Object periodUid, long startPositionUs, long windowSequenceNumber) { int nextAdGroupIndex = period.getAdGroupIndexAfterPositionUs(startPositionUs); + MediaPeriodId id = new MediaPeriodId(periodUid, windowSequenceNumber, nextAdGroupIndex); + boolean isLastInPeriod = isLastInPeriod(id); + boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); long endPositionUs = nextAdGroupIndex != C.INDEX_UNSET ? period.getAdGroupTimeUs(nextAdGroupIndex) : C.TIME_UNSET; - MediaPeriodId id = new MediaPeriodId(periodUid, windowSequenceNumber, endPositionUs); - boolean isLastInPeriod = isLastInPeriod(id); - boolean isLastInTimeline = isLastInTimeline(id, isLastInPeriod); long durationUs = endPositionUs == C.TIME_UNSET || endPositionUs == C.TIME_END_OF_SOURCE ? period.durationUs @@ -711,13 +709,14 @@ import com.google.android.exoplayer2.util.Assertions; id, startPositionUs, /* contentPositionUs= */ C.TIME_UNSET, + endPositionUs, durationUs, isLastInPeriod, isLastInTimeline); } private boolean isLastInPeriod(MediaPeriodId id) { - return !id.isAd() && id.endPositionUs == C.TIME_UNSET; + return !id.isAd() && id.nextAdGroupIndex == C.INDEX_UNSET; } private boolean isLastInTimeline(MediaPeriodId id, boolean isLastMediaPeriodInPeriod) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 1419f9a98f..20346b781f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -89,12 +89,10 @@ public interface MediaSource { public final long windowSequenceNumber; /** - * The end position to which the media period's content is clipped in order to play a following - * ad group, in microseconds, or {@link C#TIME_UNSET} if there is no following ad group or if - * this media period is an ad. The value {@link C#TIME_END_OF_SOURCE} indicates that a postroll - * ad follows at the end of this content media period. + * The index of the next ad group to which the media period's content is clipped, or {@link + * C#INDEX_UNSET} if there is no following ad group or if this media period is an ad. */ - public final long endPositionUs; + public final int nextAdGroupIndex; /** * Creates a media period identifier for a dummy period which is not part of a buffered sequence @@ -103,7 +101,7 @@ public interface MediaSource { * @param periodUid The unique id of the timeline period. */ public MediaPeriodId(Object periodUid) { - this(periodUid, C.INDEX_UNSET); + this(periodUid, /* windowSequenceNumber= */ C.INDEX_UNSET); } /** @@ -114,7 +112,12 @@ public interface MediaSource { * windows this media period is part of. */ public MediaPeriodId(Object periodUid, long windowSequenceNumber) { - this(periodUid, C.INDEX_UNSET, C.INDEX_UNSET, windowSequenceNumber, C.TIME_UNSET); + this( + periodUid, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET, + windowSequenceNumber, + /* nextAdGroupIndex= */ C.INDEX_UNSET); } /** @@ -123,11 +126,16 @@ public interface MediaSource { * @param periodUid The unique id of the timeline period. * @param windowSequenceNumber The sequence number of the window in the buffered sequence of * windows this media period is part of. - * @param endPositionUs The end position of the media period within the timeline period, in - * microseconds. + * @param nextAdGroupIndex The index of the next ad group to which the media period's content is + * clipped. */ - public MediaPeriodId(Object periodUid, long windowSequenceNumber, long endPositionUs) { - this(periodUid, C.INDEX_UNSET, C.INDEX_UNSET, windowSequenceNumber, endPositionUs); + public MediaPeriodId(Object periodUid, long windowSequenceNumber, int nextAdGroupIndex) { + this( + periodUid, + /* adGroupIndex= */ C.INDEX_UNSET, + /* adIndexInAdGroup= */ C.INDEX_UNSET, + windowSequenceNumber, + nextAdGroupIndex); } /** @@ -142,7 +150,12 @@ public interface MediaSource { */ public MediaPeriodId( Object periodUid, int adGroupIndex, int adIndexInAdGroup, long windowSequenceNumber) { - this(periodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, C.TIME_UNSET); + this( + periodUid, + adGroupIndex, + adIndexInAdGroup, + windowSequenceNumber, + /* nextAdGroupIndex= */ C.INDEX_UNSET); } private MediaPeriodId( @@ -150,12 +163,12 @@ public interface MediaSource { int adGroupIndex, int adIndexInAdGroup, long windowSequenceNumber, - long endPositionUs) { + int nextAdGroupIndex) { this.periodUid = periodUid; this.adGroupIndex = adGroupIndex; this.adIndexInAdGroup = adIndexInAdGroup; this.windowSequenceNumber = windowSequenceNumber; - this.endPositionUs = endPositionUs; + this.nextAdGroupIndex = nextAdGroupIndex; } /** Returns a copy of this period identifier but with {@code newPeriodUid} as its period uid. */ @@ -163,7 +176,7 @@ public interface MediaSource { return periodUid.equals(newPeriodUid) ? this : new MediaPeriodId( - newPeriodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, endPositionUs); + newPeriodUid, adGroupIndex, adIndexInAdGroup, windowSequenceNumber, nextAdGroupIndex); } /** @@ -187,7 +200,7 @@ public interface MediaSource { && adGroupIndex == periodId.adGroupIndex && adIndexInAdGroup == periodId.adIndexInAdGroup && windowSequenceNumber == periodId.windowSequenceNumber - && endPositionUs == periodId.endPositionUs; + && nextAdGroupIndex == periodId.nextAdGroupIndex; } @Override @@ -197,7 +210,7 @@ public interface MediaSource { result = 31 * result + adGroupIndex; result = 31 * result + adIndexInAdGroup; result = 31 * result + (int) windowSequenceNumber; - result = 31 * result + (int) endPositionUs; + result = 31 * result + nextAdGroupIndex; return result; } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java index e8f43e3fe6..6016ec1db7 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java @@ -71,7 +71,8 @@ public final class MediaPeriodQueueTest { /* startPositionUs= */ 0, /* endPositionUs= */ C.TIME_UNSET, /* durationUs= */ CONTENT_DURATION_US, - /* isLast= */ true); + /* isLast= */ true, + /* nextAdGroupIndex= */ C.INDEX_UNSET); } @Test @@ -84,7 +85,8 @@ public final class MediaPeriodQueueTest { /* startPositionUs= */ 0, /* endPositionUs= */ C.TIME_UNSET, /* durationUs= */ CONTENT_DURATION_US, - /* isLast= */ true); + /* isLast= */ true, + /* nextAdGroupIndex= */ C.INDEX_UNSET); } @Test @@ -97,7 +99,8 @@ public final class MediaPeriodQueueTest { /* startPositionUs= */ 0, /* endPositionUs= */ FIRST_AD_START_TIME_US, /* durationUs= */ FIRST_AD_START_TIME_US, - /* isLast= */ false); + /* isLast= */ false, + /* nextAdGroupIndex= */ 0); // The next media period info should be null as we haven't loaded the ad yet. advance(); assertNull(getNextMediaPeriodInfo()); @@ -109,7 +112,8 @@ public final class MediaPeriodQueueTest { /* startPositionUs= */ FIRST_AD_START_TIME_US, /* endPositionUs= */ SECOND_AD_START_TIME_US, /* durationUs= */ SECOND_AD_START_TIME_US, - /* isLast= */ false); + /* isLast= */ false, + /* nextAdGroupIndex= */ 1); advance(); setAdGroupLoaded(/* adGroupIndex= */ 1); assertNextMediaPeriodInfoIsAd( @@ -119,7 +123,8 @@ public final class MediaPeriodQueueTest { /* startPositionUs= */ SECOND_AD_START_TIME_US, /* endPositionUs= */ C.TIME_UNSET, /* durationUs= */ CONTENT_DURATION_US, - /* isLast= */ true); + /* isLast= */ true, + /* nextAdGroupIndex= */ C.INDEX_UNSET); } @Test @@ -132,7 +137,8 @@ public final class MediaPeriodQueueTest { /* startPositionUs= */ 0, /* endPositionUs= */ FIRST_AD_START_TIME_US, /* durationUs= */ FIRST_AD_START_TIME_US, - /* isLast= */ false); + /* isLast= */ false, + /* nextAdGroupIndex= */ 0); advance(); setAdGroupLoaded(/* adGroupIndex= */ 0); assertNextMediaPeriodInfoIsAd( @@ -142,7 +148,8 @@ public final class MediaPeriodQueueTest { /* startPositionUs= */ FIRST_AD_START_TIME_US, /* endPositionUs= */ C.TIME_END_OF_SOURCE, /* durationUs= */ CONTENT_DURATION_US, - /* isLast= */ false); + /* isLast= */ false, + /* nextAdGroupIndex= */ 1); advance(); setAdGroupLoaded(/* adGroupIndex= */ 1); assertNextMediaPeriodInfoIsAd( @@ -152,7 +159,8 @@ public final class MediaPeriodQueueTest { /* startPositionUs= */ CONTENT_DURATION_US, /* endPositionUs= */ C.TIME_UNSET, /* durationUs= */ CONTENT_DURATION_US, - /* isLast= */ true); + /* isLast= */ true, + /* nextAdGroupIndex= */ C.INDEX_UNSET); } @Test @@ -162,14 +170,16 @@ public final class MediaPeriodQueueTest { /* startPositionUs= */ 0, /* endPositionUs= */ C.TIME_END_OF_SOURCE, /* durationUs= */ CONTENT_DURATION_US, - /* isLast= */ false); + /* isLast= */ false, + /* nextAdGroupIndex= */ 0); advance(); setAdGroupFailedToLoad(/* adGroupIndex= */ 0); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* startPositionUs= */ CONTENT_DURATION_US, /* endPositionUs= */ C.TIME_UNSET, /* durationUs= */ CONTENT_DURATION_US, - /* isLast= */ true); + /* isLast= */ true, + /* nextAdGroupIndex= */ C.INDEX_UNSET); } private void setupInitialTimeline(long initialPositionUs, long... adGroupTimesUs) { @@ -227,13 +237,18 @@ public final class MediaPeriodQueueTest { } private void assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( - long startPositionUs, long endPositionUs, long durationUs, boolean isLast) { + long startPositionUs, + long endPositionUs, + long durationUs, + boolean isLast, + int nextAdGroupIndex) { assertThat(getNextMediaPeriodInfo()) .isEqualTo( new MediaPeriodInfo( - new MediaPeriodId(periodUid, /* windowSequenceNumber= */ 0, endPositionUs), + new MediaPeriodId(periodUid, /* windowSequenceNumber= */ 0, nextAdGroupIndex), startPositionUs, /* contentPositionUs= */ C.TIME_UNSET, + endPositionUs, durationUs, /* isLastInTimelinePeriod= */ isLast, /* isFinal= */ isLast)); @@ -250,6 +265,7 @@ public final class MediaPeriodQueueTest { /* windowSequenceNumber= */ 0), /* startPositionUs= */ 0, contentPositionUs, + /* endPositionUs= */ C.TIME_UNSET, /* durationUs= */ C.TIME_UNSET, /* isLastInTimelinePeriod= */ false, /* isFinal= */ false)); From 0cf43fc64ea80ff34ed4dc650260286c5535fd1f Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 25 Jan 2019 12:07:37 +0000 Subject: [PATCH 055/110] Update IMA to 3.10.6 This brings in a memory leak fix. Issue: #4114 PiperOrigin-RevId: 230880521 --- extensions/ima/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index 22196ff3ab..4d6302c898 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -31,13 +31,13 @@ android { } dependencies { - api 'com.google.ads.interactivemedia.v3:interactivemedia:3.10.2' + api 'com.google.ads.interactivemedia.v3:interactivemedia:3.10.6' implementation project(modulePrefix + 'library-core') - implementation 'com.google.android.gms:play-services-ads:17.1.1' + implementation 'com.google.android.gms:play-services-ads:17.1.2' // These dependencies are necessary to force the supportLibraryVersion of // com.android.support:support-v4 and com.android.support:customtabs to be // used. Else older versions are used, for example via: - // com.google.android.gms:play-services-ads:17.1.1 + // com.google.android.gms:play-services-ads:17.1.2 // |-- com.android.support:customtabs:26.1.0 implementation 'com.android.support:support-v4:' + supportLibraryVersion implementation 'com.android.support:customtabs:' + supportLibraryVersion From a1c13ca6f5033f713b499e841d407c6e45e161a1 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 25 Jan 2019 12:57:32 +0000 Subject: [PATCH 056/110] Hack to instantiate ExoDatabaseProvider without a context This will allow CachedContentIndex to start using database storage without us having to change the SimpleCache and CachedContentIndex constructors to require a Context or a DatabaseProvider. PiperOrigin-RevId: 230884501 --- .../database/ExoDatabaseProvider.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java b/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java index e5bdfbb499..cded324de1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java @@ -16,11 +16,13 @@ package com.google.android.exoplayer2.database; import android.content.Context; +import android.content.ContextWrapper; import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import com.google.android.exoplayer2.util.Log; +import java.io.File; /** * An {@link SQLiteOpenHelper} that provides instances of a standalone ExoPlayer database. @@ -37,10 +39,25 @@ public final class ExoDatabaseProvider extends SQLiteOpenHelper implements Datab private static final int VERSION = 1; private static final String TAG = "ExoDatabaseProvider"; + /** + * Provides instances of the database located by passing {@link #DATABASE_NAME} to {@link + * Context#getDatabasePath(String)}. + * + * @param context Any context. + */ public ExoDatabaseProvider(Context context) { super(context.getApplicationContext(), DATABASE_NAME, /* factory= */ null, VERSION); } + /** + * Provides instances of the database located at the specified file. + * + * @param file The database file. + */ + public ExoDatabaseProvider(File file) { + super(new DatabaseFileProvidingContext(file), file.getName(), /* factory= */ null, VERSION); + } + @Override public void onCreate(SQLiteDatabase db) { // Features create their own tables. @@ -86,4 +103,26 @@ public final class ExoDatabaseProvider extends SQLiteOpenHelper implements Datab } } } + + // TODO: This is fragile. Stop using it if/when SQLiteOpenHelper can be instantiated without a + // context [Internal ref: b/123351819], or by injecting a Context into all components that need + // to instantiate an ExoDatabaseProvider. + /** + * A {@link Context} that only implements {@link #getDatabasePath(String)}. This is the only + * method used by {@link SQLiteOpenHelper}. + */ + private static class DatabaseFileProvidingContext extends ContextWrapper { + + private final File file; + + public DatabaseFileProvidingContext(File file) { + super(/* base= */ null); + this.file = file; + } + + @Override + public File getDatabasePath(String name) { + return file; + } + } } From 3c6b72ee29f5cebc1079648a73c461a26e035fa8 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 25 Jan 2019 13:54:57 +0000 Subject: [PATCH 057/110] Fix nullness test PiperOrigin-RevId: 230889470 --- .../google/android/exoplayer2/database/ExoDatabaseProvider.java | 1 + 1 file changed, 1 insertion(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java b/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java index cded324de1..49c16ebbbf 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java @@ -115,6 +115,7 @@ public final class ExoDatabaseProvider extends SQLiteOpenHelper implements Datab private final File file; + @SuppressWarnings("nullness:argument.type.incompatible") public DatabaseFileProvidingContext(File file) { super(/* base= */ null); this.file = file; From 6b068c6ffbd0cd418cfe865ba6c4329f361ba8e4 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 28 Jan 2019 08:52:42 +0000 Subject: [PATCH 058/110] Clarify LoopingMediaSource documentation PiperOrigin-RevId: 231171425 --- .../google/android/exoplayer2/source/LoopingMediaSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java index 0cd87561e7..e19a02b7b3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/LoopingMediaSource.java @@ -31,7 +31,7 @@ import java.util.Map; * Loops a {@link MediaSource} a specified number of times. * *

    Note: To loop a {@link MediaSource} indefinitely, it is usually better to use {@link - * ExoPlayer#setRepeatMode(int)}. + * ExoPlayer#setRepeatMode(int)} instead of this class. */ public final class LoopingMediaSource extends CompositeMediaSource { From 866835ea03d1ee908a4a8d4fd3b8c69357679285 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 28 Jan 2019 09:02:05 +0000 Subject: [PATCH 059/110] Fix float output capability check Float output is only possible from API 21, but the high-res int to float conversion path was checking for 32-bit PCM not float output capability. PiperOrigin-RevId: 231172495 --- .../com/google/android/exoplayer2/audio/DefaultAudioSink.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index fc515dbdb3..6f3ee63d3d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -418,7 +418,7 @@ public final class DefaultAudioSink implements AudioSink { isInputPcm = Util.isEncodingLinearPcm(inputEncoding); shouldConvertHighResIntPcmToFloat = enableConvertHighResIntPcmToFloat - && supportsOutput(channelCount, C.ENCODING_PCM_32BIT) + && supportsOutput(channelCount, C.ENCODING_PCM_FLOAT) && Util.isEncodingHighResolutionIntegerPcm(inputEncoding); if (isInputPcm) { pcmFrameSize = Util.getPcmFrameSize(inputEncoding, channelCount); From 19144c4c7361d47ee99799fa2b53a134160f3e4f Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 28 Jan 2019 12:51:48 +0000 Subject: [PATCH 060/110] Allow to disable libyuv dependency on LIBVPX Goal: reduce binary size. PiperOrigin-RevId: 231198579 --- RELEASENOTES.md | 3 + extensions/vp9/README.md | 24 ------- .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 2 +- .../ext/vp9/LibvpxVideoRenderer.java | 60 ++++------------- .../exoplayer2/ext/vp9/VpxDecoder.java | 13 ++-- .../exoplayer2/ext/vp9/VpxOutputBuffer.java | 24 ++----- extensions/vp9/src/main/jni/Android.mk | 8 +-- extensions/vp9/src/main/jni/vpx_jni.cc | 64 ++++++------------- library/core/proguard-rules.txt | 2 +- .../exoplayer2/DefaultRenderersFactory.java | 2 - 10 files changed, 47 insertions(+), 155 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 91e3c1127b..6c72dae654 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -41,6 +41,9 @@ * Add `Handler` parameter to `ConcatenatingMediaSource` methods which take a callback `Runnable`. * Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`. +* VP9 extension: Remove RGB output mode and libyuv dependency, and switch to + surface YUV output as the default. Remove constructor parameters `scaleToFit` + and `useSurfaceYuvOutput`. * Change signature of `PlayerNotificationManager.NotificationListener` to better fit service requirements. Remove ability to set a custom stop action. diff --git a/extensions/vp9/README.md b/extensions/vp9/README.md index 306f04d0e2..6f46a4e6ad 100644 --- a/extensions/vp9/README.md +++ b/extensions/vp9/README.md @@ -34,26 +34,6 @@ VP9_EXT_PATH="${EXOPLAYER_ROOT}/extensions/vp9/src/main" NDK_PATH="" ``` -* Fetch libvpx and libyuv: - -``` -cd "${VP9_EXT_PATH}/jni" && \ -git clone https://chromium.googlesource.com/webm/libvpx libvpx && \ -git clone https://chromium.googlesource.com/libyuv/libyuv libyuv -``` - -* Checkout the appropriate branches of libvpx and libyuv (the scripts and - makefiles bundled in this repo are known to work only at these versions of the - libraries - we will update this periodically as newer versions of - libvpx/libyuv are released): - -``` -cd "${VP9_EXT_PATH}/jni/libvpx" && \ -git checkout tags/v1.7.0 -b v1.7.0 && \ -cd "${VP9_EXT_PATH}/jni/libyuv" && \ -git checkout 996a2bbd -``` - * Run a script that generates necessary configuration files for libvpx: ``` @@ -78,10 +58,6 @@ ${NDK_PATH}/ndk-build APP_ABI=all -j4 * Android config scripts should be re-generated by running `generate_libvpx_android_configs.sh` * Clean and re-build the project. -* If you want to use your own version of libvpx or libyuv, place it in - `${VP9_EXT_PATH}/jni/libvpx` or `${VP9_EXT_PATH}/jni/libyuv` respectively. But - please note that `generate_libvpx_android_configs.sh` and the makefiles need - to be modified to work with arbitrary versions of libvpx and libyuv. ## Using the extension ## diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java index c6d1e667e0..1de461e374 100644 --- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java @@ -114,7 +114,7 @@ public class VpxPlaybackTest { @Override public void run() { Looper.prepare(); - LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(true, 0); + LibvpxVideoRenderer videoRenderer = new LibvpxVideoRenderer(0); DefaultTrackSelector trackSelector = new DefaultTrackSelector(); player = ExoPlayerFactory.newInstance(context, new Renderer[] {videoRenderer}, trackSelector); player.addListener(this); diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index e61030a2e1..54ccbb40ad 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -15,8 +15,6 @@ */ package com.google.android.exoplayer2.ext.vp9; -import android.graphics.Bitmap; -import android.graphics.Canvas; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; @@ -109,7 +107,6 @@ public class LibvpxVideoRenderer extends BaseRenderer { /** The default input buffer size. */ private static final int DEFAULT_INPUT_BUFFER_SIZE = 768 * 1024; // Value based on cs/SoftVpx.cpp. - private final boolean scaleToFit; private final boolean disableLoopFilter; private final long allowedJoiningTimeMs; private final int maxDroppedFramesToNotify; @@ -119,7 +116,6 @@ public class LibvpxVideoRenderer extends BaseRenderer { private final TimedValueQueue formatQueue; private final DecoderInputBuffer flagsOnlyBuffer; private final DrmSessionManager drmSessionManager; - private final boolean useSurfaceYuvOutput; private Format format; private Format pendingFormat; @@ -133,7 +129,6 @@ public class LibvpxVideoRenderer extends BaseRenderer { private @ReinitializationState int decoderReinitializationState; private boolean decoderReceivedBuffers; - private Bitmap bitmap; private boolean renderedFirstFrame; private long initialPositionUs; private long joiningDeadlineMs; @@ -158,16 +153,14 @@ public class LibvpxVideoRenderer extends BaseRenderer { protected DecoderCounters decoderCounters; /** - * @param scaleToFit Whether video frames should be scaled to fit when rendering. * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer * can attempt to seamlessly join an ongoing playback. */ - public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs) { - this(scaleToFit, allowedJoiningTimeMs, null, null, 0); + public LibvpxVideoRenderer(long allowedJoiningTimeMs) { + this(allowedJoiningTimeMs, null, null, 0); } /** - * @param scaleToFit Whether video frames should be scaled to fit when rendering. * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer * can attempt to seamlessly join an ongoing playback. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be @@ -176,23 +169,22 @@ public class LibvpxVideoRenderer extends BaseRenderer { * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. */ - public LibvpxVideoRenderer(boolean scaleToFit, long allowedJoiningTimeMs, - Handler eventHandler, VideoRendererEventListener eventListener, + public LibvpxVideoRenderer( + long allowedJoiningTimeMs, + Handler eventHandler, + VideoRendererEventListener eventListener, int maxDroppedFramesToNotify) { this( - scaleToFit, allowedJoiningTimeMs, eventHandler, eventListener, maxDroppedFramesToNotify, /* drmSessionManager= */ null, /* playClearSamplesWithoutKeys= */ false, - /* disableLoopFilter= */ false, - /* useSurfaceYuvOutput= */ false); + /* disableLoopFilter= */ false); } /** - * @param scaleToFit Whether video frames should be scaled to fit when rendering. * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer * can attempt to seamlessly join an ongoing playback. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be @@ -208,26 +200,21 @@ public class LibvpxVideoRenderer extends BaseRenderer { * permitted to play clear regions of encrypted media files before {@code drmSessionManager} * has obtained the keys necessary to decrypt encrypted regions of the media. * @param disableLoopFilter Disable the libvpx in-loop smoothing filter. - * @param useSurfaceYuvOutput Directly output YUV to the Surface via ANativeWindow. */ public LibvpxVideoRenderer( - boolean scaleToFit, long allowedJoiningTimeMs, Handler eventHandler, VideoRendererEventListener eventListener, int maxDroppedFramesToNotify, DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys, - boolean disableLoopFilter, - boolean useSurfaceYuvOutput) { + boolean disableLoopFilter) { super(C.TRACK_TYPE_VIDEO); - this.scaleToFit = scaleToFit; this.disableLoopFilter = disableLoopFilter; this.allowedJoiningTimeMs = allowedJoiningTimeMs; this.maxDroppedFramesToNotify = maxDroppedFramesToNotify; this.drmSessionManager = drmSessionManager; this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; - this.useSurfaceYuvOutput = useSurfaceYuvOutput; joiningDeadlineMs = C.TIME_UNSET; clearReportedVideoSize(); formatHolder = new FormatHolder(); @@ -586,18 +573,14 @@ public class LibvpxVideoRenderer extends BaseRenderer { */ protected void renderOutputBuffer(VpxOutputBuffer outputBuffer) throws VpxDecoderException { int bufferMode = outputBuffer.mode; - boolean renderRgb = bufferMode == VpxDecoder.OUTPUT_MODE_RGB && surface != null; boolean renderSurface = bufferMode == VpxDecoder.OUTPUT_MODE_SURFACE_YUV && surface != null; boolean renderYuv = bufferMode == VpxDecoder.OUTPUT_MODE_YUV && outputBufferRenderer != null; lastRenderTimeUs = SystemClock.elapsedRealtime() * 1000; - if (!renderRgb && !renderYuv && !renderSurface) { + if (!renderYuv && !renderSurface) { dropOutputBuffer(outputBuffer); } else { maybeNotifyVideoSizeChanged(outputBuffer.width, outputBuffer.height); - if (renderRgb) { - renderRgbFrame(outputBuffer, scaleToFit); - outputBuffer.release(); - } else if (renderYuv) { + if (renderYuv) { outputBufferRenderer.setOutputBuffer(outputBuffer); // The renderer will release the buffer. } else { // renderSurface @@ -675,8 +658,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { this.surface = surface; this.outputBufferRenderer = outputBufferRenderer; if (surface != null) { - outputMode = - useSurfaceYuvOutput ? VpxDecoder.OUTPUT_MODE_SURFACE_YUV : VpxDecoder.OUTPUT_MODE_RGB; + outputMode = VpxDecoder.OUTPUT_MODE_SURFACE_YUV; } else { outputMode = outputBufferRenderer != null ? VpxDecoder.OUTPUT_MODE_YUV : VpxDecoder.OUTPUT_MODE_NONE; @@ -739,8 +721,7 @@ public class LibvpxVideoRenderer extends BaseRenderer { NUM_OUTPUT_BUFFERS, initialInputBufferSize, mediaCrypto, - disableLoopFilter, - useSurfaceYuvOutput); + disableLoopFilter); decoder.setOutputMode(outputMode); TraceUtil.endSection(); long decoderInitializedTimestamp = SystemClock.elapsedRealtime(); @@ -940,23 +921,6 @@ public class LibvpxVideoRenderer extends BaseRenderer { return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS; } - private void renderRgbFrame(VpxOutputBuffer outputBuffer, boolean scale) { - if (bitmap == null - || bitmap.getWidth() != outputBuffer.width - || bitmap.getHeight() != outputBuffer.height) { - bitmap = Bitmap.createBitmap(outputBuffer.width, outputBuffer.height, Bitmap.Config.RGB_565); - } - bitmap.copyPixelsFromBuffer(outputBuffer.data); - Canvas canvas = surface.lockCanvas(null); - if (scale) { - canvas.scale( - ((float) canvas.getWidth()) / outputBuffer.width, - ((float) canvas.getHeight()) / outputBuffer.height); - } - canvas.drawBitmap(bitmap, 0, 0, null); - surface.unlockCanvasAndPost(canvas); - } - private void setJoiningDeadlineMs() { joiningDeadlineMs = allowedJoiningTimeMs > 0 ? (SystemClock.elapsedRealtime() + allowedJoiningTimeMs) : C.TIME_UNSET; diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java index 51ef8e9bcf..f1de63df01 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java @@ -31,8 +31,7 @@ import java.nio.ByteBuffer; public static final int OUTPUT_MODE_NONE = -1; public static final int OUTPUT_MODE_YUV = 0; - public static final int OUTPUT_MODE_RGB = 1; - public static final int OUTPUT_MODE_SURFACE_YUV = 2; + public static final int OUTPUT_MODE_SURFACE_YUV = 1; private static final int NO_ERROR = 0; private static final int DECODE_ERROR = 1; @@ -52,7 +51,6 @@ import java.nio.ByteBuffer; * @param exoMediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted * content. Maybe null and can be ignored if decoder does not handle encrypted content. * @param disableLoopFilter Disable the libvpx in-loop smoothing filter. - * @param enableSurfaceYuvOutputMode Whether OUTPUT_MODE_SURFACE_YUV is allowed. * @throws VpxDecoderException Thrown if an exception occurs when initializing the decoder. */ public VpxDecoder( @@ -60,8 +58,7 @@ import java.nio.ByteBuffer; int numOutputBuffers, int initialInputBufferSize, ExoMediaCrypto exoMediaCrypto, - boolean disableLoopFilter, - boolean enableSurfaceYuvOutputMode) + boolean disableLoopFilter) throws VpxDecoderException { super(new VpxInputBuffer[numInputBuffers], new VpxOutputBuffer[numOutputBuffers]); if (!VpxLibrary.isAvailable()) { @@ -71,7 +68,7 @@ import java.nio.ByteBuffer; if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) { throw new VpxDecoderException("Vpx decoder does not support secure decode."); } - vpxDecContext = vpxInit(disableLoopFilter, enableSurfaceYuvOutputMode); + vpxDecContext = vpxInit(disableLoopFilter, true); if (vpxDecContext == 0) { throw new VpxDecoderException("Failed to initialize decoder"); } @@ -86,8 +83,8 @@ import java.nio.ByteBuffer; /** * Sets the output mode for frames rendered by the decoder. * - * @param outputMode The output mode. One of {@link #OUTPUT_MODE_NONE}, {@link #OUTPUT_MODE_RGB} - * and {@link #OUTPUT_MODE_YUV}. + * @param outputMode The output mode. One of {@link #OUTPUT_MODE_NONE} and {@link + * #OUTPUT_MODE_YUV}. */ public void setOutputMode(int outputMode) { this.outputMode = outputMode; diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java index 725d94819b..22330e0a05 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java @@ -60,36 +60,20 @@ public final class VpxOutputBuffer extends OutputBuffer { * Initializes the buffer. * * @param timeUs The presentation timestamp for the buffer, in microseconds. - * @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE}, - * {@link VpxDecoder#OUTPUT_MODE_RGB} and {@link VpxDecoder#OUTPUT_MODE_YUV}. + * @param mode The output mode. One of {@link VpxDecoder#OUTPUT_MODE_NONE} and {@link + * VpxDecoder#OUTPUT_MODE_YUV}. */ public void init(long timeUs, int mode) { this.timeUs = timeUs; this.mode = mode; } - /** - * Resizes the buffer based on the given dimensions. Called via JNI after decoding completes. - * @return Whether the buffer was resized successfully. - */ - public boolean initForRgbFrame(int width, int height) { - this.width = width; - this.height = height; - this.yuvPlanes = null; - if (!isSafeToMultiply(width, height) || !isSafeToMultiply(width * height, 2)) { - return false; - } - int minimumRgbSize = width * height * 2; - initData(minimumRgbSize); - return true; - } - /** * Resizes the buffer based on the given stride. Called via JNI after decoding completes. + * * @return Whether the buffer was resized successfully. */ - public boolean initForYuvFrame(int width, int height, int yStride, int uvStride, - int colorspace) { + public boolean initForYuvFrame(int width, int height, int yStride, int uvStride, int colorspace) { this.width = width; this.height = height; this.colorspace = colorspace; diff --git a/extensions/vp9/src/main/jni/Android.mk b/extensions/vp9/src/main/jni/Android.mk index fdcdc57b41..cb7571a1b0 100644 --- a/extensions/vp9/src/main/jni/Android.mk +++ b/extensions/vp9/src/main/jni/Android.mk @@ -17,12 +17,6 @@ WORKING_DIR := $(call my-dir) include $(CLEAR_VARS) LIBVPX_ROOT := $(WORKING_DIR)/libvpx -LIBYUV_ROOT := $(WORKING_DIR)/libyuv - -# build libyuv_static.a -LOCAL_PATH := $(WORKING_DIR) -LIBYUV_DISABLE_JPEG := "yes" -include $(LIBYUV_ROOT)/Android.mk # build libvpx.so LOCAL_PATH := $(WORKING_DIR) @@ -37,7 +31,7 @@ LOCAL_CPP_EXTENSION := .cc LOCAL_SRC_FILES := vpx_jni.cc LOCAL_LDLIBS := -llog -lz -lm -landroid LOCAL_SHARED_LIBRARIES := libvpx -LOCAL_STATIC_LIBRARIES := libyuv_static cpufeatures +LOCAL_STATIC_LIBRARIES := cpufeatures include $(BUILD_SHARED_LIBRARY) $(call import-module,android/cpufeatures) diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index 875e46d40f..275687261e 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -30,8 +30,6 @@ #include #include -#include "libyuv.h" // NOLINT - #define VPX_CODEC_DISABLE_COMPAT 1 #include "vpx/vpx_decoder.h" #include "vpx/vp8dx.h" @@ -61,7 +59,6 @@ (JNIEnv* env, jobject thiz, ##__VA_ARGS__)\ // JNI references for VpxOutputBuffer class. -static jmethodID initForRgbFrame; static jmethodID initForYuvFrame; static jfieldID dataField; static jfieldID outputModeField; @@ -393,11 +390,7 @@ class JniBufferManager { }; struct JniCtx { - JniCtx(bool enableBufferManager) { - if (enableBufferManager) { - buffer_manager = new JniBufferManager(); - } - } + JniCtx() { buffer_manager = new JniBufferManager(); } ~JniCtx() { if (native_window) { @@ -440,9 +433,8 @@ int vpx_release_frame_buffer(void* priv, vpx_codec_frame_buffer_t* fb) { return buffer_manager->release(*(int*)fb->priv); } -DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter, - jboolean enableBufferManager) { - JniCtx* context = new JniCtx(enableBufferManager); +jlong vpxInit(JNIEnv* env, jboolean disableLoopFilter) { + JniCtx* context = new JniCtx(); context->decoder = new vpx_codec_ctx_t(); vpx_codec_dec_cfg_t cfg = {0, 0, 0}; cfg.threads = android_getCpuCount(); @@ -469,14 +461,12 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter, } #endif } - if (enableBufferManager) { - err = vpx_codec_set_frame_buffer_functions( - context->decoder, vpx_get_frame_buffer, vpx_release_frame_buffer, - context->buffer_manager); - if (err) { - LOGE("ERROR: Failed to set libvpx frame buffer functions, error = %d.", - err); - } + err = vpx_codec_set_frame_buffer_functions( + context->decoder, vpx_get_frame_buffer, vpx_release_frame_buffer, + context->buffer_manager); + if (err) { + LOGE("ERROR: Failed to set libvpx frame buffer functions, error = %d.", + err); } // Populate JNI References. @@ -484,8 +474,6 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter, "com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer"); initForYuvFrame = env->GetMethodID(outputBufferClass, "initForYuvFrame", "(IIIII)Z"); - initForRgbFrame = env->GetMethodID(outputBufferClass, "initForRgbFrame", - "(II)Z"); dataField = env->GetFieldID(outputBufferClass, "data", "Ljava/nio/ByteBuffer;"); outputModeField = env->GetFieldID(outputBufferClass, "mode", "I"); @@ -494,6 +482,15 @@ DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter, return reinterpret_cast(context); } +DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter, + jboolean enableBufferManager) { + return vpxInit(env, disableLoopFilter); +} + +DECODER_FUNC(jlong, vpxInitilization, jboolean disableLoopFilter) { + return vpxInit(env, disableLoopFilter); +} + DECODER_FUNC(jlong, vpxDecode, jlong jContext, jobject encoded, jint len) { JniCtx* const context = reinterpret_cast(jContext); const uint8_t* const buffer = @@ -537,28 +534,10 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { } const int kOutputModeYuv = 0; - const int kOutputModeRgb = 1; - const int kOutputModeSurfaceYuv = 2; + const int kOutputModeSurfaceYuv = 1; int outputMode = env->GetIntField(jOutputBuffer, outputModeField); - if (outputMode == kOutputModeRgb) { - // resize buffer if required. - jboolean initResult = env->CallBooleanMethod(jOutputBuffer, initForRgbFrame, - img->d_w, img->d_h); - if (env->ExceptionCheck() || !initResult) { - return -1; - } - - // get pointer to the data buffer. - const jobject dataObject = env->GetObjectField(jOutputBuffer, dataField); - uint8_t* const dst = - reinterpret_cast(env->GetDirectBufferAddress(dataObject)); - - libyuv::I420ToRGB565(img->planes[VPX_PLANE_Y], img->stride[VPX_PLANE_Y], - img->planes[VPX_PLANE_U], img->stride[VPX_PLANE_U], - img->planes[VPX_PLANE_V], img->stride[VPX_PLANE_V], - dst, img->d_w * 2, img->d_w, img->d_h); - } else if (outputMode == kOutputModeYuv) { + if (outputMode == kOutputModeYuv) { const int kColorspaceUnknown = 0; const int kColorspaceBT601 = 1; const int kColorspaceBT709 = 2; @@ -616,9 +595,6 @@ DECODER_FUNC(jint, vpxGetFrame, jlong jContext, jobject jOutputBuffer) { } } else if (outputMode == kOutputModeSurfaceYuv && img->fmt != VPX_IMG_FMT_I42016) { - if (!context->buffer_manager) { - return -1; // enableBufferManager was not set in vpxInit. - } int id = *(int*)img->fb_priv; context->buffer_manager->add_ref(id); JniFrameBuffer* jfb = context->buffer_manager->get_buffer(id); diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index 5aec86ba8c..4fe182d34a 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -3,7 +3,7 @@ # Constructors accessed via reflection in DefaultRenderersFactory -dontnote com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer -keepclassmembers class com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer { - (boolean, long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int); + (long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int); } -dontnote com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer -keepclassmembers class com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java index 6ccda2b8e9..bb73353b94 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java @@ -233,7 +233,6 @@ public class DefaultRenderersFactory implements RenderersFactory { Class clazz = Class.forName("com.google.android.exoplayer2.ext.vp9.LibvpxVideoRenderer"); Constructor constructor = clazz.getConstructor( - boolean.class, long.class, android.os.Handler.class, com.google.android.exoplayer2.video.VideoRendererEventListener.class, @@ -242,7 +241,6 @@ public class DefaultRenderersFactory implements RenderersFactory { Renderer renderer = (Renderer) constructor.newInstance( - true, allowedVideoJoiningTimeMs, eventHandler, eventListener, From bac8dfea12d307c45cb59dcca55f725f043b258e Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 28 Jan 2019 16:24:47 +0000 Subject: [PATCH 061/110] Add DownloadState not met requirement stop flags PiperOrigin-RevId: 231223201 --- .../offline/DefaultDownloadIndex.java | 15 ++++-- .../exoplayer2/offline/DownloadIndexUtil.java | 2 + .../exoplayer2/offline/DownloadManager.java | 49 +++++++++++-------- .../exoplayer2/offline/DownloadState.java | 27 +++++++--- .../scheduler/RequirementsWatcher.java | 28 +++-------- .../offline/DefaultDownloadIndexTest.java | 15 +++++- 6 files changed, 81 insertions(+), 55 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java index 28a5abafb9..71ee76d0f7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java @@ -132,6 +132,7 @@ public final class DefaultDownloadIndex implements DownloadIndex { private static final String COLUMN_TOTAL_BYTES = "total_bytes"; private static final String COLUMN_FAILURE_REASON = "failure_reason"; private static final String COLUMN_STOP_FLAGS = "stop_flags"; + private static final String COLUMN_NOT_MET_REQUIREMENTS = "not_met_requirements"; private static final String COLUMN_START_TIME_MS = "start_time_ms"; private static final String COLUMN_UPDATE_TIME_MS = "update_time_ms"; private static final String COLUMN_STREAM_KEYS = "stream_keys"; @@ -147,10 +148,11 @@ public final class DefaultDownloadIndex implements DownloadIndex { private static final int COLUMN_INDEX_TOTAL_BYTES = 7; private static final int COLUMN_INDEX_FAILURE_REASON = 8; private static final int COLUMN_INDEX_STOP_FLAGS = 9; - private static final int COLUMN_INDEX_START_TIME_MS = 10; - private static final int COLUMN_INDEX_UPDATE_TIME_MS = 11; - private static final int COLUMN_INDEX_STREAM_KEYS = 12; - private static final int COLUMN_INDEX_CUSTOM_METADATA = 13; + private static final int COLUMN_INDEX_NOT_MET_REQUIREMENTS = 10; + private static final int COLUMN_INDEX_START_TIME_MS = 11; + private static final int COLUMN_INDEX_UPDATE_TIME_MS = 12; + private static final int COLUMN_INDEX_STREAM_KEYS = 13; + private static final int COLUMN_INDEX_CUSTOM_METADATA = 14; private static final String COLUMN_SELECTION_ID = COLUMN_ID + " = ?"; @@ -166,6 +168,7 @@ public final class DefaultDownloadIndex implements DownloadIndex { COLUMN_TOTAL_BYTES, COLUMN_FAILURE_REASON, COLUMN_STOP_FLAGS, + COLUMN_NOT_MET_REQUIREMENTS, COLUMN_START_TIME_MS, COLUMN_UPDATE_TIME_MS, COLUMN_STREAM_KEYS, @@ -197,6 +200,8 @@ public final class DefaultDownloadIndex implements DownloadIndex { + " INTEGER NOT NULL," + COLUMN_STOP_FLAGS + " INTEGER NOT NULL," + + COLUMN_NOT_MET_REQUIREMENTS + + " INTEGER NOT NULL," + COLUMN_START_TIME_MS + " INTEGER NOT NULL," + COLUMN_UPDATE_TIME_MS @@ -241,6 +246,7 @@ public final class DefaultDownloadIndex implements DownloadIndex { values.put(COLUMN_TOTAL_BYTES, downloadState.totalBytes); values.put(COLUMN_FAILURE_REASON, downloadState.failureReason); values.put(COLUMN_STOP_FLAGS, downloadState.stopFlags); + values.put(COLUMN_NOT_MET_REQUIREMENTS, downloadState.notMetRequirements); values.put(COLUMN_START_TIME_MS, downloadState.startTimeMs); values.put(COLUMN_UPDATE_TIME_MS, downloadState.updateTimeMs); values.put(COLUMN_STREAM_KEYS, encodeStreamKeys(downloadState.streamKeys)); @@ -312,6 +318,7 @@ public final class DefaultDownloadIndex implements DownloadIndex { cursor.getLong(COLUMN_INDEX_TOTAL_BYTES), cursor.getInt(COLUMN_INDEX_FAILURE_REASON), cursor.getInt(COLUMN_INDEX_STOP_FLAGS), + cursor.getInt(COLUMN_INDEX_NOT_MET_REQUIREMENTS), cursor.getLong(COLUMN_INDEX_START_TIME_MS), cursor.getLong(COLUMN_INDEX_UPDATE_TIME_MS), decodeStreamKeys(cursor.getString(COLUMN_INDEX_STREAM_KEYS)), diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndexUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndexUtil.java index 63602c7641..e0c914408b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndexUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndexUtil.java @@ -115,6 +115,7 @@ public final class DownloadIndexUtil { /* totalBytes= */ C.LENGTH_UNSET, downloadState.failureReason, downloadState.stopFlags, + downloadState.notMetRequirements, downloadState.startTimeMs, downloadState.updateTimeMs, newKeys, @@ -136,6 +137,7 @@ public final class DownloadIndexUtil { /* totalBytes= */ C.LENGTH_UNSET, DownloadState.FAILURE_REASON_NONE, /* stopFlags= */ 0, + /* notMetRequirements= */ 0, /* startTimeMs= */ currentTimeMs, /* updateTimeMs= */ currentTimeMs, action.keys.toArray(new StreamKey[0]), diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java index 8932140a34..f1c90e67c1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java @@ -26,6 +26,7 @@ import static com.google.android.exoplayer2.offline.DownloadState.STATE_REMOVING import static com.google.android.exoplayer2.offline.DownloadState.STATE_RESTARTING; import static com.google.android.exoplayer2.offline.DownloadState.STATE_STOPPED; import static com.google.android.exoplayer2.offline.DownloadState.STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY; +import static com.google.android.exoplayer2.offline.DownloadState.STOP_FLAG_REQUIREMENTS_NOT_MET; import static com.google.android.exoplayer2.offline.DownloadState.STOP_FLAG_STOPPED; import android.content.Context; @@ -119,6 +120,7 @@ public final class DownloadManager { private boolean initialized; private boolean released; @DownloadState.StopFlags private int stickyStopFlags; + @Requirements.RequirementFlags private int notMetRequirements; private RequirementsWatcher requirementsWatcher; /** @@ -194,7 +196,7 @@ public final class DownloadManager { return; } requirementsWatcher.stop(); - notifyListenersRequirementsStateChange(watchRequirements(requirements)); + onRequirementsStateChanged(watchRequirements(requirements)); } /** Returns the requirements needed to be met to start downloads. */ @@ -349,7 +351,8 @@ public final class DownloadManager { } } Download download = - new Download(this, downloaderFactory, action, minRetryCount, stickyStopFlags); + new Download( + this, downloaderFactory, action, minRetryCount, stickyStopFlags, notMetRequirements); downloads.add(download); logd("Download is added", download); } @@ -401,13 +404,16 @@ public final class DownloadManager { } } - private void notifyListenersRequirementsStateChange( - @Requirements.RequirementFlags int notMetRequirements) { + private void onRequirementsStateChanged(@Requirements.RequirementFlags int notMetRequirements) { + this.notMetRequirements = notMetRequirements; logdFlags("Not met requirements are changed", notMetRequirements); for (Listener listener : listeners) { listener.onRequirementsStateChanged( DownloadManager.this, requirementsWatcher.getRequirements(), notMetRequirements); } + for (int i = 0; i < downloads.size(); i++) { + downloads.get(i).setNotMetRequirements(notMetRequirements); + } } private void loadActions() { @@ -486,7 +492,9 @@ public final class DownloadManager { @Requirements.RequirementFlags private int watchRequirements(Requirements requirements) { - requirementsWatcher = new RequirementsWatcher(context, new RequirementListener(), requirements); + RequirementsWatcher.Listener listener = + (requirementsWatcher, notMetRequirements) -> onRequirementsStateChanged(notMetRequirements); + requirementsWatcher = new RequirementsWatcher(context, listener, requirements); @Requirements.RequirementFlags int notMetRequirements = requirementsWatcher.start(); if (notMetRequirements == 0) { startDownloads(); @@ -511,17 +519,23 @@ public final class DownloadManager { @MonotonicNonNull private DownloadThread downloadThread; @MonotonicNonNull @DownloadState.FailureReason private int failureReason; @DownloadState.StopFlags private int stopFlags; + @Requirements.RequirementFlags private int notMetRequirements; private Download( DownloadManager downloadManager, DownloaderFactory downloaderFactory, DownloadAction action, int minRetryCount, - int stopFlags) { + @DownloadState.StopFlags int stopFlags, + @Requirements.RequirementFlags int notMetRequirements) { this.id = action.id; this.downloadManager = downloadManager; this.downloaderFactory = downloaderFactory; this.minRetryCount = minRetryCount; + this.notMetRequirements = notMetRequirements; + if (notMetRequirements != 0) { + stopFlags |= STOP_FLAG_REQUIREMENTS_NOT_MET; + } this.stopFlags = stopFlags; this.startTimeMs = System.currentTimeMillis(); actionQueue = new ArrayDeque<>(); @@ -579,6 +593,7 @@ public final class DownloadManager { totalBytes, failureReason, stopFlags, + notMetRequirements, startTimeMs, /* updateTimeMs= */ System.currentTimeMillis(), action.keys.toArray(new StreamKey[0]), @@ -628,6 +643,13 @@ public final class DownloadManager { } } + public void setNotMetRequirements(@Requirements.RequirementFlags int notMetRequirements) { + this.notMetRequirements = notMetRequirements; + updateStopFlags( + STOP_FLAG_REQUIREMENTS_NOT_MET, + notMetRequirements != 0 ? STOP_FLAG_REQUIREMENTS_NOT_MET : 0); + } + private void initialize(boolean restart) { DownloadAction action = actionQueue.peek(); if (action.isRemoveAction) { @@ -770,19 +792,4 @@ public final class DownloadManager { } } - private class RequirementListener implements RequirementsWatcher.Listener { - @Override - public void requirementsMet(RequirementsWatcher requirementsWatcher) { - startDownloads(); - notifyListenersRequirementsStateChange(0); - } - - @Override - public void requirementsNotMet( - RequirementsWatcher requirementsWatcher, - @Requirements.RequirementFlags int notMetRequirements) { - stopDownloads(); - notifyListenersRequirementsStateChange(notMetRequirements); - } - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java index 7bbd078822..dd55d6ea31 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java @@ -19,6 +19,7 @@ import android.net.Uri; import android.support.annotation.IntDef; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.util.Assertions; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -80,12 +81,18 @@ public final class DownloadState { @Retention(RetentionPolicy.SOURCE) @IntDef( flag = true, - value = {STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY, STOP_FLAG_STOPPED}) + value = { + STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY, + STOP_FLAG_STOPPED, + STOP_FLAG_REQUIREMENTS_NOT_MET + }) public @interface StopFlags {} /** Download can't be started as the manager isn't ready. */ public static final int STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY = 1; - /** All downloads are stopped by the application. */ + /** Download is stopped by the application. */ public static final int STOP_FLAG_STOPPED = 1 << 1; + /** Download is stopped as the requirements are not met. */ + public static final int STOP_FLAG_REQUIREMENTS_NOT_MET = 1 << 2; /** Returns the state string for the given state value. */ public static String getStateString(@State int state) { @@ -153,7 +160,9 @@ public final class DownloadState { */ @FailureReason public final int failureReason; /** Download stop flags. These flags stop downloading any content. */ - public final int stopFlags; + @StopFlags public final int stopFlags; + /** Not met requirements to download. */ + @Requirements.RequirementFlags public final int notMetRequirements; /* package */ DownloadState( String id, @@ -166,13 +175,17 @@ public final class DownloadState { long totalBytes, @FailureReason int failureReason, @StopFlags int stopFlags, + @Requirements.RequirementFlags int notMetRequirements, long startTimeMs, long updateTimeMs, StreamKey[] streamKeys, byte[] customMetadata) { - this.stopFlags = stopFlags; Assertions.checkState( failureReason == FAILURE_REASON_NONE ? state != STATE_FAILED : state == STATE_FAILED); + Assertions.checkState( + (stopFlags & STOP_FLAG_REQUIREMENTS_NOT_MET) == 0 + ? notMetRequirements == 0 + : notMetRequirements != 0); // TODO enable this when we start changing state immediately // Assertions.checkState(stopFlags == 0 || (state != STATE_DOWNLOADING && state != // STATE_QUEUED)); @@ -180,14 +193,16 @@ public final class DownloadState { this.type = type; this.uri = uri; this.cacheKey = cacheKey; - this.streamKeys = streamKeys; - this.customMetadata = customMetadata; this.state = state; this.downloadPercentage = downloadPercentage; this.downloadedBytes = downloadedBytes; this.totalBytes = totalBytes; this.failureReason = failureReason; + this.stopFlags = stopFlags; + this.notMetRequirements = notMetRequirements; this.startTimeMs = startTimeMs; this.updateTimeMs = updateTimeMs; + this.streamKeys = streamKeys; + this.customMetadata = customMetadata; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java index 686f19d161..dfced7c0ab 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/scheduler/RequirementsWatcher.java @@ -42,23 +42,14 @@ public final class RequirementsWatcher { * Requirements} are met. */ public interface Listener { - /** - * Called when all of the requirements are met. - * - * @param requirementsWatcher Calling instance. - */ - void requirementsMet(RequirementsWatcher requirementsWatcher); - - /** - * Called when there is at least one not met requirement and there is a change on which of the - * requirements are not met. + * Called when there is a change on the met requirements. * * @param requirementsWatcher Calling instance. * @param notMetRequirements {@link Requirements.RequirementFlags RequirementFlags} that are not * met, or 0. */ - void requirementsNotMet( + void onRequirementsStateChanged( RequirementsWatcher requirementsWatcher, @Requirements.RequirementFlags int notMetRequirements); } @@ -172,17 +163,10 @@ public final class RequirementsWatcher { private void checkRequirements() { @Requirements.RequirementFlags int notMetRequirements = requirements.getNotMetRequirements(context); - if (this.notMetRequirements == notMetRequirements) { - logd("notMetRequirements hasn't changed: " + notMetRequirements); - return; - } - this.notMetRequirements = notMetRequirements; - if (notMetRequirements == 0) { - logd("start job"); - listener.requirementsMet(this); - } else { - logd("stop job"); - listener.requirementsNotMet(this, notMetRequirements); + if (this.notMetRequirements != notMetRequirements) { + this.notMetRequirements = notMetRequirements; + logd("notMetRequirements has changed: " + notMetRequirements); + listener.onRequirementsStateChanged(this, notMetRequirements); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java index 6f6786c068..df68f7ad34 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java @@ -80,13 +80,14 @@ public class DefaultDownloadIndexTest { .setDownloadedBytes(200) .setTotalBytes(400) .setFailureReason(DownloadState.FAILURE_REASON_UNKNOWN) - .setStopFlags(DownloadState.STOP_FLAG_STOPPED) + .setStopFlags(DownloadState.STOP_FLAG_REQUIREMENTS_NOT_MET) + .setNotMetRequirements(0x87654321) .setStartTimeMs(10) .setUpdateTimeMs(20) .setStreamKeys( new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 1, /* trackIndex= */ 2), new StreamKey(/* periodIndex= */ 3, /* groupIndex= */ 4, /* trackIndex= */ 5)) - .setCustomMetadata(new byte[] {0, 1, 2, 3}) + .setCustomMetadata(new byte[] {0, 1, 2, 3, 7, 8, 9, 10}) .build(); downloadIndex.putDownloadState(downloadState); DownloadState readDownloadState = downloadIndex.getDownloadState(id); @@ -238,6 +239,9 @@ public class DefaultDownloadIndexTest { if (downloadState.stopFlags != that.stopFlags) { return false; } + if (downloadState.notMetRequirements != that.notMetRequirements) { + return false; + } if (!downloadState.id.equals(that.id)) { return false; } @@ -269,6 +273,7 @@ public class DefaultDownloadIndexTest { private long totalBytes; private int failureReason; private int stopFlags; + private int notMetRequirements; private long startTimeMs; private long updateTimeMs; private StreamKey[] streamKeys; @@ -341,6 +346,11 @@ public class DefaultDownloadIndexTest { return this; } + public DownloadStateBuilder setNotMetRequirements(int notMetRequirements) { + this.notMetRequirements = notMetRequirements; + return this; + } + public DownloadStateBuilder setStartTimeMs(long startTimeMs) { this.startTimeMs = startTimeMs; return this; @@ -373,6 +383,7 @@ public class DefaultDownloadIndexTest { totalBytes, failureReason, stopFlags, + notMetRequirements, startTimeMs, updateTimeMs, streamKeys, From 39505452deda442d0b59d8a4224d0132e77995f1 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Jan 2019 11:01:26 +0000 Subject: [PATCH 062/110] Make VersionTable static The way it is currently, it's very unclear that an operation on the version table will correctly belong to a transaction in code such as this, taken from DefaultDownloadIndex: writableDatabase.beginTransaction(); try { writableDatabase.execSQL(...); versionTable.setVersion(...); writableDatabase.setTransactionSuccessful(); } finally { writableDatabase.endTransaction(); } This change explicitly passes the database, to make it obvious that the operation will really go into the same transaction: writableDatabase.beginTransaction(); try { writableDatabase.execSQL(....); VersionTable.setVersion(writableDatabase, ...); writableDatabase.setTransactionSuccessful(); } finally { writableDatabase.endTransaction(); } PiperOrigin-RevId: 231374933 --- .../exoplayer2/database/VersionTable.java | 53 +++++++++---------- .../offline/DefaultDownloadIndex.java | 7 +-- .../exoplayer2/database/VersionTableTest.java | 28 +++++----- .../offline/DefaultDownloadIndexTest.java | 13 ++--- 4 files changed, 52 insertions(+), 49 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java b/library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java index 0b6ef3d816..5193d1836f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java @@ -20,17 +20,18 @@ import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.support.annotation.IntDef; +import android.support.annotation.VisibleForTesting; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** - * A table that holds version information about other ExoPlayer tables. This allows ExoPlayer tables - * to be versioned independently to the version of the containing database. + * Utility methods for accessing versions of ExoPlayer database components. This allows them to be + * versioned independently to the version of the containing database. */ public final class VersionTable { - /** Returned by {@link #getVersion(int)} if the version is unset. */ + /** Returned by {@link #getVersion(SQLiteDatabase, int)} if the version is unset. */ public static final int VERSION_UNSET = -1; /** Version of tables used for offline functionality. */ public static final int FEATURE_OFFLINE = 0; @@ -56,48 +57,46 @@ public final class VersionTable { @IntDef({FEATURE_OFFLINE, FEATURE_CACHE}) private @interface Feature {} - private final DatabaseProvider databaseProvider; - - public VersionTable(DatabaseProvider databaseProvider) { - this.databaseProvider = databaseProvider; - // Check whether the table exists to avoid getting a writable database if we don't need one. - if (!doesTableExist(databaseProvider, TABLE_NAME)) { - databaseProvider.getWritableDatabase().execSQL(SQL_CREATE_TABLE_IF_NOT_EXISTS); - } - } + private VersionTable() {} /** * Sets the version of tables belonging to the specified feature. * + * @param writableDatabase The database to update. * @param feature The feature. * @param version The version. */ - public void setVersion(@Feature int feature, int version) { + public static void setVersion( + SQLiteDatabase writableDatabase, @Feature int feature, int version) { + writableDatabase.execSQL(SQL_CREATE_TABLE_IF_NOT_EXISTS); ContentValues values = new ContentValues(); values.put(COLUMN_FEATURE, feature); values.put(COLUMN_VERSION, version); - SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values); } /** * Returns the version of tables belonging to the specified feature, or {@link #VERSION_UNSET} if * no version information is available. + * + * @param database The database to query. + * @param feature The feature. */ - public int getVersion(@Feature int feature) { + public static int getVersion(SQLiteDatabase database, @Feature int feature) { + if (!tableExists(database, TABLE_NAME)) { + return VERSION_UNSET; + } String selection = COLUMN_FEATURE + " = ?"; String[] selectionArgs = {Integer.toString(feature)}; try (Cursor cursor = - databaseProvider - .getReadableDatabase() - .query( - TABLE_NAME, - new String[] {COLUMN_VERSION}, - selection, - selectionArgs, - /* groupBy= */ null, - /* having= */ null, - /* orderBy= */ null)) { + database.query( + TABLE_NAME, + new String[] {COLUMN_VERSION}, + selection, + selectionArgs, + /* groupBy= */ null, + /* having= */ null, + /* orderBy= */ null)) { if (cursor.getCount() == 0) { return VERSION_UNSET; } @@ -106,8 +105,8 @@ public final class VersionTable { } } - /* package */ static boolean doesTableExist(DatabaseProvider databaseProvider, String tableName) { - SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase(); + @VisibleForTesting + /* package */ static boolean tableExists(SQLiteDatabase readableDatabase, String tableName) { long count = DatabaseUtils.queryNumEntries( readableDatabase, "sqlite_master", "tbl_name = ?", new String[] {tableName}); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java index 71ee76d0f7..b9fae6e2db 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java @@ -215,15 +215,16 @@ public final class DefaultDownloadIndex implements DownloadIndex { public DownloadsTable(DatabaseProvider databaseProvider) { this.databaseProvider = databaseProvider; - VersionTable versionTable = new VersionTable(databaseProvider); - int version = versionTable.getVersion(VersionTable.FEATURE_OFFLINE); + int version = + VersionTable.getVersion( + databaseProvider.getReadableDatabase(), VersionTable.FEATURE_OFFLINE); if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); writableDatabase.beginTransaction(); try { + VersionTable.setVersion(writableDatabase, VersionTable.FEATURE_OFFLINE, TABLE_VERSION); writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS); writableDatabase.execSQL(SQL_CREATE_TABLE); - versionTable.setVersion(VersionTable.FEATURE_OFFLINE, TABLE_VERSION); writableDatabase.setTransactionSuccessful(); } finally { writableDatabase.endTransaction(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/database/VersionTableTest.java b/library/core/src/test/java/com/google/android/exoplayer2/database/VersionTableTest.java index dd9184ccdd..a607cc01db 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/database/VersionTableTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/database/VersionTableTest.java @@ -19,6 +19,7 @@ import static com.google.android.exoplayer2.database.VersionTable.FEATURE_CACHE; import static com.google.android.exoplayer2.database.VersionTable.FEATURE_OFFLINE; import static com.google.common.truth.Truth.assertThat; +import android.database.sqlite.SQLiteDatabase; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -31,10 +32,14 @@ import org.robolectric.RuntimeEnvironment; public class VersionTableTest { private ExoDatabaseProvider databaseProvider; + private SQLiteDatabase readableDatabase; + private SQLiteDatabase writableDatabase; @Before public void setUp() { databaseProvider = new ExoDatabaseProvider(RuntimeEnvironment.application); + readableDatabase = databaseProvider.getReadableDatabase(); + writableDatabase = databaseProvider.getWritableDatabase(); } @After @@ -44,35 +49,32 @@ public class VersionTableTest { @Test public void getVersion_nonExistingTable_returnsVersionUnset() { - VersionTable versionTable = new VersionTable(databaseProvider); - int version = versionTable.getVersion(FEATURE_OFFLINE); + int version = VersionTable.getVersion(readableDatabase, FEATURE_OFFLINE); assertThat(version).isEqualTo(VersionTable.VERSION_UNSET); } @Test public void getVersion_returnsSetVersion() { - VersionTable versionTable = new VersionTable(databaseProvider); + VersionTable.setVersion(writableDatabase, FEATURE_OFFLINE, 1); + assertThat(VersionTable.getVersion(readableDatabase, FEATURE_OFFLINE)).isEqualTo(1); - versionTable.setVersion(FEATURE_OFFLINE, 1); - assertThat(versionTable.getVersion(FEATURE_OFFLINE)).isEqualTo(1); + VersionTable.setVersion(writableDatabase, FEATURE_OFFLINE, 10); + assertThat(VersionTable.getVersion(readableDatabase, FEATURE_OFFLINE)).isEqualTo(10); - versionTable.setVersion(FEATURE_OFFLINE, 10); - assertThat(versionTable.getVersion(FEATURE_OFFLINE)).isEqualTo(10); - - versionTable.setVersion(FEATURE_CACHE, 5); - assertThat(versionTable.getVersion(FEATURE_CACHE)).isEqualTo(5); - assertThat(versionTable.getVersion(FEATURE_OFFLINE)).isEqualTo(10); + VersionTable.setVersion(writableDatabase, FEATURE_CACHE, 5); + assertThat(VersionTable.getVersion(readableDatabase, FEATURE_CACHE)).isEqualTo(5); + assertThat(VersionTable.getVersion(readableDatabase, FEATURE_OFFLINE)).isEqualTo(10); } @Test public void doesTableExist_nonExistingTable_returnsFalse() { - assertThat(VersionTable.doesTableExist(databaseProvider, "NonExistingTable")).isFalse(); + assertThat(VersionTable.tableExists(readableDatabase, "NonExistingTable")).isFalse(); } @Test public void doesTableExist_existingTable_returnsTrue() { String table = "TestTable"; databaseProvider.getWritableDatabase().execSQL("CREATE TABLE " + table + " (dummy INTEGER)"); - assertThat(VersionTable.doesTableExist(databaseProvider, table)).isTrue(); + assertThat(VersionTable.tableExists(readableDatabase, table)).isTrue(); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java index df68f7ad34..badbb58eff 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.offline; import static com.google.common.truth.Truth.assertThat; +import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; @@ -180,13 +181,13 @@ public class DefaultDownloadIndexTest { @Test public void putDownloadState_setsVersion() { - VersionTable versionTable = new VersionTable(databaseProvider); - assertThat(versionTable.getVersion(VersionTable.FEATURE_OFFLINE)) + SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase(); + assertThat(VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE)) .isEqualTo(VersionTable.VERSION_UNSET); downloadIndex.putDownloadState(new DownloadStateBuilder("id1").build()); - assertThat(versionTable.getVersion(VersionTable.FEATURE_OFFLINE)) + assertThat(VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE)) .isEqualTo(DefaultDownloadIndex.TABLE_VERSION); } @@ -198,15 +199,15 @@ public class DefaultDownloadIndexTest { assertThat(cursor.getCount()).isEqualTo(1); cursor.close(); - VersionTable versionTable = new VersionTable(databaseProvider); - versionTable.setVersion(VersionTable.FEATURE_OFFLINE, Integer.MAX_VALUE); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + VersionTable.setVersion(writableDatabase, VersionTable.FEATURE_OFFLINE, Integer.MAX_VALUE); downloadIndex = new DefaultDownloadIndex(databaseProvider); cursor = downloadIndex.getDownloadStates(); assertThat(cursor.getCount()).isEqualTo(0); cursor.close(); - assertThat(versionTable.getVersion(VersionTable.FEATURE_OFFLINE)) + assertThat(VersionTable.getVersion(writableDatabase, VersionTable.FEATURE_OFFLINE)) .isEqualTo(DefaultDownloadIndex.TABLE_VERSION); } From f74e0eb992900c8198d384168fca745b33c6db59 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 29 Jan 2019 12:01:33 +0000 Subject: [PATCH 063/110] Add back deprecated MediaSource.prepareSource for ExoPlayerSampleExtractor. This should be removed after releasing. PiperOrigin-RevId: 231380393 --- .../com/google/android/exoplayer2/source/MediaSource.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index 20346b781f..759208c67b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.source; import android.os.Handler; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.TransferListener; @@ -237,7 +238,6 @@ public interface MediaSource { default Object getTag() { return null; } - /** * Starts source preparation if not yet started, and adds a listener for timeline and/or manifest * updates. @@ -256,8 +256,7 @@ public interface MediaSource { * and other data. */ void prepareSource( - SourceInfoRefreshListener listener, - @Nullable TransferListener mediaTransferListener); + SourceInfoRefreshListener listener, @Nullable TransferListener mediaTransferListener); /** * Throws any pending error encountered while loading or refreshing source information. From 92bf8e918cc183cfaf2c6c93ff4533583c51c107 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 29 Jan 2019 12:57:22 +0000 Subject: [PATCH 064/110] Change getStreamKeys to take a list of TrackSelections. Converting a single track selection to stream keys is only possible if the output is independent from other track selections being made. This is not the case for DASH and HLS embedded track groups which should select the already selected primary track if possible (and thus needs to know whether a primary track group is selected). Also, update the test method to take a period index. PiperOrigin-RevId: 231385490 --- .../exoplayer2/source/MediaPeriod.java | 12 ++-- .../source/smoothstreaming/SsMediaPeriod.java | 13 ++-- .../smoothstreaming/SsMediaPeriodTest.java | 4 +- .../testutil/MediaPeriodAsserts.java | 72 +++++++++++++------ 4 files changed, 67 insertions(+), 34 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java index 532131ba7d..b40bbb35d1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaPeriod.java @@ -87,18 +87,18 @@ public interface MediaPeriod extends SequenceableLoader { TrackGroupArray getTrackGroups(); /** - * Returns a list of {@link StreamKey stream keys} which allow to filter the media in this period - * to load only the parts needed to play the provided {@link TrackSelection}. + * Returns a list of {@link StreamKey StreamKeys} which allow to filter the media in this period + * to load only the parts needed to play the provided {@link TrackSelection TrackSelections}. * *

    This method is only called after the period has been prepared. * - * @param trackSelection The {@link TrackSelection} describing the tracks for which stream keys - * are requested. - * @return The corresponding {@link StreamKey stream keys} for the selected tracks, or an empty + * @param trackSelections The {@link TrackSelection TrackSelections} describing the tracks for + * which stream keys are requested. + * @return The corresponding {@link StreamKey StreamKeys} for the selected tracks, or an empty * list if filtering is not possible and the entire media needs to be loaded to play the * selected tracks. */ - default List getStreamKeys(TrackSelection trackSelection) { + default List getStreamKeys(List trackSelections) { return Collections.emptyList(); } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java index 8798ea09b2..ae6b60183c 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriod.java @@ -144,11 +144,14 @@ import java.util.List; } @Override - public List getStreamKeys(TrackSelection trackSelection) { - List streamKeys = new ArrayList<>(trackSelection.length()); - int streamElementIndex = trackGroups.indexOf(trackSelection.getTrackGroup()); - for (int i = 0; i < trackSelection.length(); i++) { - streamKeys.add(new StreamKey(streamElementIndex, trackSelection.getIndexInTrackGroup(i))); + public List getStreamKeys(List trackSelections) { + List streamKeys = new ArrayList<>(); + for (int selectionIndex = 0; selectionIndex < trackSelections.size(); selectionIndex++) { + TrackSelection trackSelection = trackSelections.get(selectionIndex); + int streamElementIndex = trackGroups.indexOf(trackSelection.getTrackGroup()); + for (int i = 0; i < trackSelection.length(); i++) { + streamKeys.add(new StreamKey(streamElementIndex, trackSelection.getIndexInTrackGroup(i))); + } } return streamKeys; } diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java index f4feef3949..54de4badbd 100644 --- a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java @@ -61,7 +61,7 @@ public class SsMediaPeriodTest { createStreamElement( /* name= */ "text", C.TRACK_TYPE_TEXT, createTextFormat(/* language= */ "eng"))); FilterableManifestMediaPeriodFactory mediaPeriodFactory = - manifest -> + (manifest, periodIndex) -> new SsMediaPeriod( manifest, mock(SsChunkSource.Factory.class), @@ -77,7 +77,7 @@ public class SsMediaPeriodTest { mock(Allocator.class)); MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration( - mediaPeriodFactory, testManifest); + mediaPeriodFactory, testManifest, /* periodIndex= */ 0); } private static Format createVideoFormat(int bitrate) { diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java index 48b9128caf..5235163684 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java @@ -30,6 +30,8 @@ import com.google.android.exoplayer2.trackselection.BaseTrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.util.ConditionVariable; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.concurrent.atomic.AtomicReference; @@ -45,53 +47,81 @@ public final class MediaPeriodAsserts { public interface FilterableManifestMediaPeriodFactory> { /** Returns media period based on the provided filterable manifest. */ - MediaPeriod createMediaPeriod(T manifest); + MediaPeriod createMediaPeriod(T manifest, int periodIndex); } private MediaPeriodAsserts() {} /** - * Asserts that the values returns by {@link MediaPeriod#getStreamKeys(TrackSelection)} are - * compatible with a {@link FilterableManifest} using these stream keys. + * Asserts that the values returns by {@link MediaPeriod#getStreamKeys(List)} are compatible with + * a {@link FilterableManifest} using these stream keys. * * @param mediaPeriodFactory A factory to create a {@link MediaPeriod} based on a manifest. * @param manifest The manifest which is to be tested. + * @param periodIndex The index of period in the manifest. */ public static > void assertGetStreamKeysAndManifestFilterIntegration( - FilterableManifestMediaPeriodFactory mediaPeriodFactory, T manifest) { - MediaPeriod mediaPeriod = mediaPeriodFactory.createMediaPeriod(manifest); + FilterableManifestMediaPeriodFactory mediaPeriodFactory, T manifest, int periodIndex) { + MediaPeriod mediaPeriod = mediaPeriodFactory.createMediaPeriod(manifest, periodIndex); TrackGroupArray trackGroupArray = getTrackGroups(mediaPeriod); + // Create test vector of query test selections: + // - One selection with one track per group, two tracks or all tracks. + // - Two selections with tracks from multiple groups, or tracks from a single group. + // - Multiple selections with tracks from all groups. + List> testSelections = new ArrayList<>(); for (int i = 0; i < trackGroupArray.length; i++) { TrackGroup trackGroup = trackGroupArray.get(i); - - // For each track group, create various test selections. - List testSelections = new ArrayList<>(); for (int j = 0; j < trackGroup.length; j++) { - testSelections.add(new TestTrackSelection(trackGroup, j)); + testSelections.add(Collections.singletonList(new TestTrackSelection(trackGroup, j))); } if (trackGroup.length > 1) { - testSelections.add(new TestTrackSelection(trackGroup, 0, 1)); + testSelections.add(Collections.singletonList(new TestTrackSelection(trackGroup, 0, 1))); + testSelections.add( + Arrays.asList( + new TrackSelection[] { + new TestTrackSelection(trackGroup, 0), new TestTrackSelection(trackGroup, 1) + })); } if (trackGroup.length > 2) { int[] allTracks = new int[trackGroup.length]; for (int j = 0; j < trackGroup.length; j++) { allTracks[j] = j; } - testSelections.add(new TestTrackSelection(trackGroup, allTracks)); + testSelections.add( + Collections.singletonList(new TestTrackSelection(trackGroup, allTracks))); } + } + if (trackGroupArray.length > 1) { + testSelections.add( + Arrays.asList( + new TrackSelection[] { + new TestTrackSelection(trackGroupArray.get(0), 0), + new TestTrackSelection(trackGroupArray.get(1), 0) + })); + } + if (trackGroupArray.length > 2) { + List selectionsFromAllGroups = new ArrayList<>(); + for (int i = 0; i < trackGroupArray.length; i++) { + selectionsFromAllGroups.add(new TestTrackSelection(trackGroupArray.get(i), 0)); + } + testSelections.add(selectionsFromAllGroups); + } - // Get stream keys for each selection and check that the resulting filtered manifest includes - // at least the same subset of tracks. - for (TrackSelection testSelection : testSelections) { - List streamKeys = mediaPeriod.getStreamKeys(testSelection); - T filteredManifest = manifest.copy(streamKeys); - MediaPeriod filteredMediaPeriod = mediaPeriodFactory.createMediaPeriod(filteredManifest); - TrackGroupArray filteredTrackGroupArray = getTrackGroups(filteredMediaPeriod); - Format[] expectedFormats = new Format[testSelection.length()]; - for (int k = 0; k < testSelection.length(); k++) { - expectedFormats[k] = testSelection.getFormat(k); + // Verify for each case that stream keys can be used to create filtered tracks which still + // contain at least all requested formats. + for (List testSelection : testSelections) { + List streamKeys = mediaPeriod.getStreamKeys(testSelection); + T filteredManifest = manifest.copy(streamKeys); + // The filtered manifest should only have one period left. + MediaPeriod filteredMediaPeriod = + mediaPeriodFactory.createMediaPeriod(filteredManifest, /* periodIndex= */ 0); + TrackGroupArray filteredTrackGroupArray = getTrackGroups(filteredMediaPeriod); + for (TrackSelection trackSelection : testSelection) { + Format[] expectedFormats = new Format[trackSelection.length()]; + for (int k = 0; k < trackSelection.length(); k++) { + expectedFormats[k] = trackSelection.getFormat(k); } assertOneTrackGroupContainsFormats(filteredTrackGroupArray, expectedFormats); } From 9779f2c358913a947e02bd19aa7c423d677e5c28 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 29 Jan 2019 12:57:37 +0000 Subject: [PATCH 065/110] Add DashMediaPeriod getStreamKeys implementation and test. PiperOrigin-RevId: 231385518 --- .../source/dash/DashMediaPeriod.java | 48 +++- .../source/dash/DashMediaPeriodTest.java | 247 ++++++++++++++++++ .../smoothstreaming/SsMediaPeriodTest.java | 2 +- .../testutil/MediaPeriodAsserts.java | 47 +++- 4 files changed, 333 insertions(+), 11 deletions(-) create mode 100644 library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index cba7a9941b..fd0453e79e 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -22,6 +22,7 @@ import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SeekParameters; +import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.EmptySampleStream; import com.google.android.exoplayer2.source.MediaPeriod; @@ -192,6 +193,49 @@ import java.util.List; return trackGroups; } + @Override + public List getStreamKeys(List trackSelections) { + List manifestAdaptationSets = manifest.getPeriod(periodIndex).adaptationSets; + List streamKeys = new ArrayList<>(); + for (TrackSelection trackSelection : trackSelections) { + int trackGroupIndex = trackGroups.indexOf(trackSelection.getTrackGroup()); + TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex]; + if (trackGroupInfo.trackGroupCategory != TrackGroupInfo.CATEGORY_PRIMARY) { + // Ignore non-primary tracks. + continue; + } + int[] adaptationSetIndices = trackGroupInfo.adaptationSetIndices; + int[] trackIndices = new int[trackSelection.length()]; + for (int i = 0; i < trackSelection.length(); i++) { + trackIndices[i] = trackSelection.getIndexInTrackGroup(i); + } + Arrays.sort(trackIndices); + + int currentAdaptationSetIndex = 0; + int totalTracksInPreviousAdaptationSets = 0; + int tracksInCurrentAdaptationSet = + manifestAdaptationSets.get(adaptationSetIndices[0]).representations.size(); + for (int i = 0; i < trackIndices.length; i++) { + while (trackIndices[i] + >= totalTracksInPreviousAdaptationSets + tracksInCurrentAdaptationSet) { + currentAdaptationSetIndex++; + totalTracksInPreviousAdaptationSets += tracksInCurrentAdaptationSet; + tracksInCurrentAdaptationSet = + manifestAdaptationSets + .get(adaptationSetIndices[currentAdaptationSetIndex]) + .representations + .size(); + } + streamKeys.add( + new StreamKey( + periodIndex, + adaptationSetIndices[currentAdaptationSetIndex], + trackIndices[i] - totalTracksInPreviousAdaptationSets)); + } + } + return streamKeys; + } + @Override public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { @@ -697,7 +741,7 @@ import java.util.List; public final int[] adaptationSetIndices; public final int trackType; - public @TrackGroupCategory final int trackGroupCategory; + @TrackGroupCategory public final int trackGroupCategory; public final int eventStreamGroupIndex; public final int primaryTrackGroupIndex; @@ -748,7 +792,7 @@ import java.util.List; return new TrackGroupInfo( C.TRACK_TYPE_METADATA, CATEGORY_MANIFEST_EVENTS, - null, + new int[0], -1, C.INDEX_UNSET, C.INDEX_UNSET, diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java new file mode 100644 index 0000000000..0d9fee282c --- /dev/null +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.dash; + +import static org.mockito.Mockito.mock; + +import android.net.Uri; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.dash.PlayerEmsgHandler.PlayerEmsgCallback; +import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; +import com.google.android.exoplayer2.source.dash.manifest.DashManifest; +import com.google.android.exoplayer2.source.dash.manifest.Descriptor; +import com.google.android.exoplayer2.source.dash.manifest.Period; +import com.google.android.exoplayer2.source.dash.manifest.Representation; +import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase; +import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement; +import com.google.android.exoplayer2.testutil.MediaPeriodAsserts; +import com.google.android.exoplayer2.testutil.MediaPeriodAsserts.FilterableManifestMediaPeriodFactory; +import com.google.android.exoplayer2.testutil.RobolectricUtil; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.LoaderErrorThrower; +import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.Arrays; +import java.util.Collections; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Unit tests for {@link DashMediaPeriod}. */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +public final class DashMediaPeriodTest { + + @Test + public void getSteamKeys_isCompatibleWithDashManifestFilter() { + // Test manifest which covers various edge cases: + // - Multiple periods. + // - Single and multiple representations per adaptation set. + // - Switch descriptors combining multiple adaptations sets. + // - Embedded track groups. + // All cases are deliberately combined in one test to catch potential indexing problems which + // only occur in combination. + DashManifest testManifest = + createDashManifest( + createPeriod( + createAdaptationSet( + /* id= */ 0, + /* trackType= */ C.TRACK_TYPE_VIDEO, + /* descriptor= */ null, + createVideoRepresentation(/* bitrate= */ 1000000))), + createPeriod( + createAdaptationSet( + /* id= */ 100, + /* trackType= */ C.TRACK_TYPE_VIDEO, + /* descriptor= */ createSwitchDescriptor(/* ids= */ 103, 104), + createVideoRepresentationWithInbandEventStream(/* bitrate= */ 200000), + createVideoRepresentationWithInbandEventStream(/* bitrate= */ 400000), + createVideoRepresentationWithInbandEventStream(/* bitrate= */ 600000)), + createAdaptationSet( + /* id= */ 101, + /* trackType= */ C.TRACK_TYPE_AUDIO, + /* descriptor= */ createSwitchDescriptor(/* ids= */ 102), + createAudioRepresentation(/* bitrate= */ 48000), + createAudioRepresentation(/* bitrate= */ 96000)), + createAdaptationSet( + /* id= */ 102, + /* trackType= */ C.TRACK_TYPE_AUDIO, + /* descriptor= */ createSwitchDescriptor(/* ids= */ 101), + createAudioRepresentation(/* bitrate= */ 256000)), + createAdaptationSet( + /* id= */ 103, + /* trackType= */ C.TRACK_TYPE_VIDEO, + /* descriptor= */ createSwitchDescriptor(/* ids= */ 100, 104), + createVideoRepresentationWithInbandEventStream(/* bitrate= */ 800000), + createVideoRepresentationWithInbandEventStream(/* bitrate= */ 1000000)), + createAdaptationSet( + /* id= */ 104, + /* trackType= */ C.TRACK_TYPE_VIDEO, + /* descriptor= */ createSwitchDescriptor(/* ids= */ 100, 103), + createVideoRepresentationWithInbandEventStream(/* bitrate= */ 2000000)), + createAdaptationSet( + /* id= */ 105, + /* trackType= */ C.TRACK_TYPE_TEXT, + /* descriptor= */ null, + createTextRepresentation(/* language= */ "eng")), + createAdaptationSet( + /* id= */ 105, + /* trackType= */ C.TRACK_TYPE_TEXT, + /* descriptor= */ null, + createTextRepresentation(/* language= */ "ger")))); + FilterableManifestMediaPeriodFactory mediaPeriodFactory = + (manifest, periodIndex) -> + new DashMediaPeriod( + /* id= */ periodIndex, + manifest, + periodIndex, + mock(DashChunkSource.Factory.class), + mock(TransferListener.class), + mock(LoadErrorHandlingPolicy.class), + new EventDispatcher() + .withParameters( + /* windowIndex= */ 0, + /* mediaPeriodId= */ new MediaPeriodId(/* periodUid= */ new Object()), + /* mediaTimeOffsetMs= */ 0), + /* elapsedRealtimeOffsetMs= */ 0, + mock(LoaderErrorThrower.class), + mock(Allocator.class), + mock(CompositeSequenceableLoaderFactory.class), + mock(PlayerEmsgCallback.class)); + + // Ignore embedded metadata as we don't want to select primary group just to get embedded track. + MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration( + mediaPeriodFactory, + testManifest, + /* periodIndex= */ 1, + /* ignoredMimeType= */ "application/x-emsg"); + } + + private static DashManifest createDashManifest(Period... periods) { + return new DashManifest( + /* availabilityStartTimeMs= */ 0, + /* durationMs= */ 5000, + /* minBufferTimeMs= */ 1, + /* dynamic= */ false, + /* minUpdatePeriodMs= */ 2, + /* timeShiftBufferDepthMs= */ 3, + /* suggestedPresentationDelayMs= */ 4, + /* publishTimeMs= */ 12345, + /* programInformation= */ null, + new UtcTimingElement("", ""), + Uri.EMPTY, + Arrays.asList(periods)); + } + + private static Period createPeriod(AdaptationSet... adaptationSets) { + return new Period(/* id= */ null, /* startMs= */ 0, Arrays.asList(adaptationSets)); + } + + private static AdaptationSet createAdaptationSet( + int id, int trackType, @Nullable Descriptor descriptor, Representation... representations) { + return new AdaptationSet( + id, + trackType, + Arrays.asList(representations), + /* accessibilityDescriptors= */ Collections.emptyList(), + descriptor == null ? Collections.emptyList() : Collections.singletonList(descriptor)); + } + + private static Representation createVideoRepresentation(int bitrate) { + return Representation.newInstance( + /* revisionId= */ 0, + createVideoFormat(bitrate), + /* baseUrl= */ "", + new SingleSegmentBase()); + } + + private static Representation createVideoRepresentationWithInbandEventStream(int bitrate) { + return Representation.newInstance( + /* revisionId= */ 0, + createVideoFormat(bitrate), + /* baseUrl= */ "", + new SingleSegmentBase(), + Collections.singletonList(getInbandEventDescriptor())); + } + + private static Format createVideoFormat(int bitrate) { + return Format.createContainerFormat( + /* id= */ null, + /* label= */ null, + MimeTypes.VIDEO_MP4, + MimeTypes.VIDEO_H264, + /* codecs= */ null, + bitrate, + /* selectionFlags= */ 0, + /* language= */ null); + } + + private static Representation createAudioRepresentation(int bitrate) { + return Representation.newInstance( + /* revisionId= */ 0, + Format.createContainerFormat( + /* id= */ null, + /* label= */ null, + MimeTypes.AUDIO_MP4, + MimeTypes.AUDIO_AAC, + /* codecs= */ null, + bitrate, + /* selectionFlags= */ 0, + /* language= */ null), + /* baseUrl= */ "", + new SingleSegmentBase()); + } + + private static Representation createTextRepresentation(String language) { + return Representation.newInstance( + /* revisionId= */ 0, + Format.createContainerFormat( + /* id= */ null, + /* label= */ null, + MimeTypes.APPLICATION_MP4, + MimeTypes.TEXT_VTT, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* selectionFlags= */ 0, + language), + /* baseUrl= */ "", + new SingleSegmentBase()); + } + + private static Descriptor createSwitchDescriptor(int... ids) { + StringBuilder idString = new StringBuilder(); + idString.append(ids[0]); + for (int i = 1; i < ids.length; i++) { + idString.append(",").append(ids[i]); + } + return new Descriptor( + /* schemeIdUri= */ "urn:mpeg:dash:adaptation-set-switching:2016", + /* value= */ idString.toString(), + /* id= */ null); + } + + private static Descriptor getInbandEventDescriptor() { + return new Descriptor( + /* schemeIdUri= */ "inBandSchemeIdUri", /* value= */ "inBandValue", /* id= */ "inBandId"); + } +} diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java index 54de4badbd..bceaf8cdf2 100644 --- a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java @@ -77,7 +77,7 @@ public class SsMediaPeriodTest { mock(Allocator.class)); MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration( - mediaPeriodFactory, testManifest, /* periodIndex= */ 0); + mediaPeriodFactory, testManifest); } private static Format createVideoFormat(int bitrate) { diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java index 5235163684..b4137a41de 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java @@ -58,11 +58,30 @@ public final class MediaPeriodAsserts { * * @param mediaPeriodFactory A factory to create a {@link MediaPeriod} based on a manifest. * @param manifest The manifest which is to be tested. - * @param periodIndex The index of period in the manifest. */ public static > void assertGetStreamKeysAndManifestFilterIntegration( - FilterableManifestMediaPeriodFactory mediaPeriodFactory, T manifest, int periodIndex) { + FilterableManifestMediaPeriodFactory mediaPeriodFactory, T manifest) { + assertGetStreamKeysAndManifestFilterIntegration( + mediaPeriodFactory, manifest, /* periodIndex= */ 0, /* ignoredMimeType= */ null); + } + + /** + * Asserts that the values returns by {@link MediaPeriod#getStreamKeys(List)} are compatible with + * a {@link FilterableManifest} using these stream keys. + * + * @param mediaPeriodFactory A factory to create a {@link MediaPeriod} based on a manifest. + * @param manifest The manifest which is to be tested. + * @param periodIndex The index of period in the manifest. + * @param ignoredMimeType Optional mime type whose existence in the filtered track groups is not + * asserted. + */ + public static > + void assertGetStreamKeysAndManifestFilterIntegration( + FilterableManifestMediaPeriodFactory mediaPeriodFactory, + T manifest, + int periodIndex, + @Nullable String ignoredMimeType) { MediaPeriod mediaPeriod = mediaPeriodFactory.createMediaPeriod(manifest, periodIndex); TrackGroupArray trackGroupArray = getTrackGroups(mediaPeriod); @@ -94,12 +113,16 @@ public final class MediaPeriodAsserts { } } if (trackGroupArray.length > 1) { - testSelections.add( - Arrays.asList( - new TrackSelection[] { - new TestTrackSelection(trackGroupArray.get(0), 0), - new TestTrackSelection(trackGroupArray.get(1), 0) - })); + for (int i = 0; i < trackGroupArray.length - 1; i++) { + for (int j = i + 1; j < trackGroupArray.length; j++) { + testSelections.add( + Arrays.asList( + new TrackSelection[] { + new TestTrackSelection(trackGroupArray.get(i), 0), + new TestTrackSelection(trackGroupArray.get(j), 0) + })); + } + } } if (trackGroupArray.length > 2) { List selectionsFromAllGroups = new ArrayList<>(); @@ -113,12 +136,20 @@ public final class MediaPeriodAsserts { // contain at least all requested formats. for (List testSelection : testSelections) { List streamKeys = mediaPeriod.getStreamKeys(testSelection); + if (streamKeys.isEmpty()) { + // Manifests won't be filtered if stream key is empty. + continue; + } T filteredManifest = manifest.copy(streamKeys); // The filtered manifest should only have one period left. MediaPeriod filteredMediaPeriod = mediaPeriodFactory.createMediaPeriod(filteredManifest, /* periodIndex= */ 0); TrackGroupArray filteredTrackGroupArray = getTrackGroups(filteredMediaPeriod); for (TrackSelection trackSelection : testSelection) { + if (ignoredMimeType != null + && ignoredMimeType.equals(trackSelection.getFormat(0).sampleMimeType)) { + continue; + } Format[] expectedFormats = new Format[trackSelection.length()]; for (int k = 0; k < trackSelection.length(); k++) { expectedFormats[k] = trackSelection.getFormat(k); From 6a52cd7445f3008e5d36fdce8bb02196052b5523 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 29 Jan 2019 12:57:53 +0000 Subject: [PATCH 066/110] Ensure HlsMediaPeriod works with playlists without variants. Currently, we remove all variants if none of the stream keys contains any variants. This causes HlsMediaPeriod to throw exceptions as it expects at least one variant. Change it to support master playlists without variants. PiperOrigin-RevId: 231385547 --- .../exoplayer2/source/hls/HlsMediaPeriod.java | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index da50d7cc93..5a384efcdf 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -343,15 +343,19 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private void buildAndPrepareSampleStreamWrappers(long positionUs) { HlsMasterPlaylist masterPlaylist = playlistTracker.getMasterPlaylist(); + boolean hasVariants = !masterPlaylist.variants.isEmpty(); List audioRenditions = masterPlaylist.audios; List subtitleRenditions = masterPlaylist.subtitles; - int wrapperCount = 1 /* variants */ + audioRenditions.size() + subtitleRenditions.size(); + int wrapperCount = (hasVariants ? 1 : 0) + audioRenditions.size() + subtitleRenditions.size(); sampleStreamWrappers = new HlsSampleStreamWrapper[wrapperCount]; pendingPrepareCount = wrapperCount; - buildAndPrepareMainSampleStreamWrapper(masterPlaylist, positionUs); - int currentWrapperIndex = 1; + int currentWrapperIndex = 0; + if (hasVariants) { + buildAndPrepareMainSampleStreamWrapper(masterPlaylist, positionUs); + currentWrapperIndex++; + } // TODO: Build video stream wrappers here. @@ -370,8 +374,6 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper if (allowChunklessPreparation && renditionFormat.codecs != null) { sampleStreamWrapper.prepareWithMasterPlaylistInfo( new TrackGroupArray(new TrackGroup(audioRendition.format)), 0, TrackGroupArray.EMPTY); - } else { - sampleStreamWrapper.continuePreparing(); } } @@ -386,6 +388,12 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper new TrackGroupArray(new TrackGroup(url.format)), 0, TrackGroupArray.EMPTY); } + // Set timestamp master and trigger preparation (if not already prepared) + sampleStreamWrappers[0].setIsTimestampMaster(true); + for (HlsSampleStreamWrapper sampleStreamWrapper : sampleStreamWrappers) { + sampleStreamWrapper.continuePreparing(); + } + // All wrappers are enabled during preparation. enabledSampleStreamWrappers = sampleStreamWrappers; } @@ -503,9 +511,6 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper new TrackGroupArray(muxedTrackGroups.toArray(new TrackGroup[0])), 0, new TrackGroupArray(id3TrackGroup)); - } else { - sampleStreamWrapper.setIsTimestampMaster(true); - sampleStreamWrapper.continuePreparing(); } } @@ -566,7 +571,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper if (isPrimaryTrackInVariant) { channelCount = variantFormat.channelCount; selectionFlags = variantFormat.selectionFlags; - language = variantFormat.label; + language = variantFormat.language; label = variantFormat.label; } } From 32b40502fcc50a7287581003e3dd6e80d87bf21c Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 29 Jan 2019 12:58:10 +0000 Subject: [PATCH 067/110] Add HlsMediaPeriod getStreamKeys implementation and tests. PiperOrigin-RevId: 231385563 --- .../exoplayer2/source/hls/HlsMediaPeriod.java | 140 ++++++++++--- .../source/hls/HlsSampleStreamWrapper.java | 4 + .../source/hls/HlsMediaPeriodTest.java | 184 ++++++++++++++++++ 3 files changed, 305 insertions(+), 23 deletions(-) create mode 100644 library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 5a384efcdf..73a46b68ba 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -20,6 +20,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; @@ -68,6 +69,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private TrackGroupArray trackGroups; private HlsSampleStreamWrapper[] sampleStreamWrappers; private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; + private int[] selectedVariantIndices; private SequenceableLoader compositeSequenceableLoader; private boolean notifiedReadingStarted; @@ -112,6 +114,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper timestampAdjusterProvider = new TimestampAdjusterProvider(); sampleStreamWrappers = new HlsSampleStreamWrapper[0]; enabledSampleStreamWrappers = new HlsSampleStreamWrapper[0]; + selectedVariantIndices = new int[0]; eventDispatcher.mediaPeriodCreated(); } @@ -143,6 +146,77 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper return trackGroups; } + @Override + public List getStreamKeys(List trackSelections) { + // See HlsMasterPlaylist.copy for interpretation of StreamKeys. + HlsMasterPlaylist masterPlaylist = Assertions.checkNotNull(playlistTracker.getMasterPlaylist()); + boolean hasVariants = !masterPlaylist.variants.isEmpty(); + int audioWrapperOffset = hasVariants ? 1 : 0; + int subtitleWrapperOffset = audioWrapperOffset + masterPlaylist.audios.size(); + + TrackGroupArray mainWrapperTrackGroups; + int mainWrapperPrimaryGroupIndex; + if (hasVariants) { + HlsSampleStreamWrapper mainWrapper = sampleStreamWrappers[0]; + mainWrapperTrackGroups = mainWrapper.getTrackGroups(); + mainWrapperPrimaryGroupIndex = mainWrapper.getPrimaryTrackGroupIndex(); + } else { + mainWrapperTrackGroups = TrackGroupArray.EMPTY; + mainWrapperPrimaryGroupIndex = 0; + } + + List streamKeys = new ArrayList<>(); + boolean needsPrimaryTrackGroupSelection = false; + boolean hasPrimaryTrackGroupSelection = false; + for (TrackSelection trackSelection : trackSelections) { + TrackGroup trackSelectionGroup = trackSelection.getTrackGroup(); + int mainWrapperTrackGroupIndex = mainWrapperTrackGroups.indexOf(trackSelectionGroup); + if (mainWrapperTrackGroupIndex != C.INDEX_UNSET) { + if (mainWrapperTrackGroupIndex == mainWrapperPrimaryGroupIndex) { + // Primary group in main wrapper. + hasPrimaryTrackGroupSelection = true; + for (int i = 0; i < trackSelection.length(); i++) { + int variantIndex = selectedVariantIndices[trackSelection.getIndexInTrackGroup(i)]; + streamKeys.add(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, variantIndex)); + } + } else { + // Embedded group in main wrapper. + needsPrimaryTrackGroupSelection = true; + } + } else { + // Audio or subtitle group. + for (int i = audioWrapperOffset; i < sampleStreamWrappers.length; i++) { + TrackGroupArray wrapperTrackGroups = sampleStreamWrappers[i].getTrackGroups(); + if (wrapperTrackGroups.indexOf(trackSelectionGroup) != C.INDEX_UNSET) { + if (i < subtitleWrapperOffset) { + streamKeys.add( + new StreamKey(HlsMasterPlaylist.GROUP_INDEX_AUDIO, i - audioWrapperOffset)); + } else { + streamKeys.add( + new StreamKey(HlsMasterPlaylist.GROUP_INDEX_SUBTITLE, i - subtitleWrapperOffset)); + } + break; + } + } + } + } + if (needsPrimaryTrackGroupSelection && !hasPrimaryTrackGroupSelection) { + // A track selection includes a variant-embedded track, but no variant is added yet. We use + // the valid variant with the lowest bitrate to reduce overhead. + int lowestBitrateIndex = selectedVariantIndices[0]; + int lowestBitrate = masterPlaylist.variants.get(selectedVariantIndices[0]).format.bitrate; + for (int i = 1; i < selectedVariantIndices.length; i++) { + int variantBitrate = masterPlaylist.variants.get(selectedVariantIndices[i]).format.bitrate; + if (variantBitrate < lowestBitrate) { + lowestBitrate = variantBitrate; + lowestBitrateIndex = selectedVariantIndices[i]; + } + } + streamKeys.add(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, lowestBitrateIndex)); + } + return streamKeys; + } + @Override public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags, SampleStream[] streams, boolean[] streamResetFlags, long positionUs) { @@ -424,44 +498,64 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper */ private void buildAndPrepareMainSampleStreamWrapper( HlsMasterPlaylist masterPlaylist, long positionUs) { - List selectedVariants = new ArrayList<>(masterPlaylist.variants); - ArrayList definiteVideoVariants = new ArrayList<>(); - ArrayList definiteAudioOnlyVariants = new ArrayList<>(); - for (int i = 0; i < selectedVariants.size(); i++) { - HlsUrl variant = selectedVariants.get(i); + int[] variantTypes = new int[masterPlaylist.variants.size()]; + int videoVariantCount = 0; + int audioVariantCount = 0; + for (int i = 0; i < masterPlaylist.variants.size(); i++) { + HlsUrl variant = masterPlaylist.variants.get(i); Format format = variant.format; if (format.height > 0 || Util.getCodecsOfType(format.codecs, C.TRACK_TYPE_VIDEO) != null) { - definiteVideoVariants.add(variant); + variantTypes[i] = C.TRACK_TYPE_VIDEO; + videoVariantCount++; } else if (Util.getCodecsOfType(format.codecs, C.TRACK_TYPE_AUDIO) != null) { - definiteAudioOnlyVariants.add(variant); + variantTypes[i] = C.TRACK_TYPE_AUDIO; + audioVariantCount++; + } else { + variantTypes[i] = C.TRACK_TYPE_UNKNOWN; } } - if (!definiteVideoVariants.isEmpty()) { + boolean useVideoVariantsOnly = false; + boolean useNonAudioVariantsOnly = false; + int selectedVariantsCount = variantTypes.length; + if (videoVariantCount > 0) { // We've identified some variants as definitely containing video. Assume variants within the // master playlist are marked consistently, and hence that we have the full set. Filter out // any other variants, which are likely to be audio only. - selectedVariants = definiteVideoVariants; - } else if (definiteAudioOnlyVariants.size() < selectedVariants.size()) { + useVideoVariantsOnly = true; + selectedVariantsCount = videoVariantCount; + } else if (audioVariantCount < variantTypes.length) { // We've identified some variants, but not all, as being audio only. Filter them out to leave // the remaining variants, which are likely to contain video. - selectedVariants.removeAll(definiteAudioOnlyVariants); - } else { - // Leave the enabled variants unchanged. They're likely either all video or all audio. + useNonAudioVariantsOnly = true; + selectedVariantsCount = variantTypes.length - audioVariantCount; } - Assertions.checkArgument(!selectedVariants.isEmpty()); - HlsUrl[] variants = selectedVariants.toArray(new HlsUrl[0]); - String codecs = variants[0].format.codecs; - HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT, - variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormats, positionUs); + HlsUrl[] selectedVariants = new HlsUrl[selectedVariantsCount]; + selectedVariantIndices = new int[selectedVariantsCount]; + int outIndex = 0; + for (int i = 0; i < masterPlaylist.variants.size(); i++) { + if ((!useVideoVariantsOnly || variantTypes[i] == C.TRACK_TYPE_VIDEO) + && (!useNonAudioVariantsOnly || variantTypes[i] != C.TRACK_TYPE_AUDIO)) { + selectedVariants[outIndex] = masterPlaylist.variants.get(i); + selectedVariantIndices[outIndex++] = i; + } + } + String codecs = selectedVariants[0].format.codecs; + HlsSampleStreamWrapper sampleStreamWrapper = + buildSampleStreamWrapper( + C.TRACK_TYPE_DEFAULT, + selectedVariants, + masterPlaylist.muxedAudioFormat, + masterPlaylist.muxedCaptionFormats, + positionUs); sampleStreamWrappers[0] = sampleStreamWrapper; if (allowChunklessPreparation && codecs != null) { boolean variantsContainVideoCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_VIDEO) != null; boolean variantsContainAudioCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO) != null; List muxedTrackGroups = new ArrayList<>(); if (variantsContainVideoCodecs) { - Format[] videoFormats = new Format[selectedVariants.size()]; + Format[] videoFormats = new Format[selectedVariantsCount]; for (int i = 0; i < videoFormats.length; i++) { - videoFormats[i] = deriveVideoFormat(variants[i].format); + videoFormats[i] = deriveVideoFormat(selectedVariants[i].format); } muxedTrackGroups.add(new TrackGroup(videoFormats)); @@ -470,7 +564,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper muxedTrackGroups.add( new TrackGroup( deriveAudioFormat( - variants[0].format, + selectedVariants[0].format, masterPlaylist.muxedAudioFormat, /* isPrimaryTrackInVariant= */ false))); } @@ -482,9 +576,9 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper } } else if (variantsContainAudioCodecs) { // Variants only contain audio. - Format[] audioFormats = new Format[selectedVariants.size()]; + Format[] audioFormats = new Format[selectedVariantsCount]; for (int i = 0; i < audioFormats.length; i++) { - Format variantFormat = variants[i].format; + Format variantFormat = selectedVariants[i].format; audioFormats[i] = deriveAudioFormat( variantFormat, diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java index 39598c4cd8..4fd27ba2a0 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsSampleStreamWrapper.java @@ -214,6 +214,10 @@ import java.util.List; return trackGroups; } + public int getPrimaryTrackGroupIndex() { + return primaryTrackGroupIndex; + } + public int bindSampleQueueToSampleStream(int trackGroupIndex) { int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex]; if (sampleQueueIndex == C.INDEX_UNSET) { diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java new file mode 100644 index 0000000000..599e099b8c --- /dev/null +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriodTest.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.hls; + +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; +import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker; +import com.google.android.exoplayer2.testutil.MediaPeriodAsserts; +import com.google.android.exoplayer2.testutil.MediaPeriodAsserts.FilterableManifestMediaPeriodFactory; +import com.google.android.exoplayer2.testutil.RobolectricUtil; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.MimeTypes; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +/** Unit test for {@link HlsMediaPeriod}. */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) +public final class HlsMediaPeriodTest { + + @Test + public void getSteamKeys_isCompatibleWithhHlsMasterPlaylistFilter() { + HlsMasterPlaylist testMasterPlaylist = + createMasterPlaylist( + /* variants= */ Arrays.asList( + createAudioOnlyVariantHlsUrl(/* bitrate= */ 10000), + createMuxedVideoAudioVariantHlsUrl(/* bitrate= */ 200000), + createAudioOnlyVariantHlsUrl(/* bitrate= */ 300000), + createMuxedVideoAudioVariantHlsUrl(/* bitrate= */ 400000), + createMuxedVideoAudioVariantHlsUrl(/* bitrate= */ 600000)), + /* audios= */ Arrays.asList( + createAudioHlsUrl(/* language= */ "spa"), + createAudioHlsUrl(/* language= */ "ger"), + createAudioHlsUrl(/* language= */ "tur")), + /* subtitles= */ Arrays.asList( + createSubtitleHlsUrl(/* language= */ "spa"), + createSubtitleHlsUrl(/* language= */ "ger"), + createSubtitleHlsUrl(/* language= */ "tur")), + /* muxedAudioFormat= */ createAudioFormat("eng"), + /* muxedCaptionFormats= */ Arrays.asList( + createSubtitleFormat("eng"), createSubtitleFormat("gsw"))); + FilterableManifestMediaPeriodFactory mediaPeriodFactory = + (playlist, periodIndex) -> { + HlsDataSourceFactory mockDataSourceFactory = mock(HlsDataSourceFactory.class); + when(mockDataSourceFactory.createDataSource(anyInt())).thenReturn(mock(DataSource.class)); + HlsPlaylistTracker mockPlaylistTracker = mock(HlsPlaylistTracker.class); + when(mockPlaylistTracker.getMasterPlaylist()).thenReturn((HlsMasterPlaylist) playlist); + return new HlsMediaPeriod( + mock(HlsExtractorFactory.class), + mockPlaylistTracker, + mockDataSourceFactory, + mock(TransferListener.class), + mock(LoadErrorHandlingPolicy.class), + new EventDispatcher() + .withParameters( + /* windowIndex= */ 0, + /* mediaPeriodId= */ new MediaPeriodId(/* periodUid= */ new Object()), + /* mediaTimeOffsetMs= */ 0), + mock(Allocator.class), + mock(CompositeSequenceableLoaderFactory.class), + /* allowChunklessPreparation =*/ true); + }; + + MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration( + mediaPeriodFactory, testMasterPlaylist); + } + + private static HlsMasterPlaylist createMasterPlaylist( + List variants, + List audios, + List subtitles, + Format muxedAudioFormat, + List muxedCaptionFormats) { + return new HlsMasterPlaylist( + "http://baseUri", + /* tags= */ Collections.emptyList(), + variants, + audios, + subtitles, + muxedAudioFormat, + muxedCaptionFormats, + /* hasIndependentSegments= */ true, + /* variableDefinitions= */ Collections.emptyMap()); + } + + private static HlsUrl createMuxedVideoAudioVariantHlsUrl(int bitrate) { + return new HlsUrl( + "http://url", + Format.createVideoContainerFormat( + /* id= */ null, + /* label= */ null, + /* containerMimeType= */ MimeTypes.APPLICATION_M3U8, + /* sampleMimeType= */ null, + /* codecs= */ "avc1.100.41,mp4a.40.2", + bitrate, + /* width= */ Format.NO_VALUE, + /* height= */ Format.NO_VALUE, + /* frameRate= */ Format.NO_VALUE, + /* initializationData= */ null, + /* selectionFlags= */ 0)); + } + + private static HlsUrl createAudioOnlyVariantHlsUrl(int bitrate) { + return new HlsUrl( + "http://url", + Format.createVideoContainerFormat( + /* id= */ null, + /* label= */ null, + /* containerMimeType= */ MimeTypes.APPLICATION_M3U8, + /* sampleMimeType= */ null, + /* codecs= */ "mp4a.40.2", + bitrate, + /* width= */ Format.NO_VALUE, + /* height= */ Format.NO_VALUE, + /* frameRate= */ Format.NO_VALUE, + /* initializationData= */ null, + /* selectionFlags= */ 0)); + } + + private static HlsUrl createAudioHlsUrl(String language) { + return new HlsUrl("http://url", createAudioFormat(language)); + } + + private static HlsUrl createSubtitleHlsUrl(String language) { + return new HlsUrl("http://url", createSubtitleFormat(language)); + } + + private static Format createAudioFormat(String language) { + return Format.createAudioContainerFormat( + /* id= */ null, + /* label= */ null, + /* containerMimeType= */ MimeTypes.APPLICATION_M3U8, + MimeTypes.getMediaMimeType("mp4a.40.2"), + /* codecs= */ "mp4a.40.2", + /* bitrate= */ Format.NO_VALUE, + /* channelCount= */ Format.NO_VALUE, + /* sampleRate= */ Format.NO_VALUE, + /* initializationData= */ null, + /* selectionFlags= */ 0, + language); + } + + private static Format createSubtitleFormat(String language) { + return Format.createTextContainerFormat( + /* id= */ null, + /* label= */ null, + /* containerMimeType= */ MimeTypes.APPLICATION_M3U8, + /* sampleMimeType= */ MimeTypes.TEXT_VTT, + /* codecs= */ null, + /* bitrate= */ Format.NO_VALUE, + /* selectionFlags= */ 0, + language); + } +} From 0a8ae74217f04e981786ea6b74192a0488e6ddb4 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 29 Jan 2019 12:58:28 +0000 Subject: [PATCH 068/110] Update DownloadHelper to use MediaSource and MediaPeriod directly. This requires to prepare the media source and the periods in a small helper similar to the metadata retriever. It also gets rid of the need to have abstract protected methods to load the manifest, to extract the track groups and to convert to stream keys, as this can now be handled by the media period. PiperOrigin-RevId: 231385590 --- .../exoplayer2/demo/DownloadTracker.java | 31 +- .../exoplayer2/offline/DownloadHelper.java | 335 +++++++++++++----- .../offline/ProgressiveDownloadHelper.java | 25 +- .../offline/DownloadHelperTest.java | 64 +++- .../dash/offline/DashDownloadHelper.java | 52 +-- .../source/hls/offline/HlsDownloadHelper.java | 72 +--- .../offline/SsDownloadHelper.java | 39 +- 7 files changed, 322 insertions(+), 296 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java index 559bbcef0f..b0e56b9f92 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java @@ -210,7 +210,7 @@ public class DownloadTracker implements DownloadManager.Listener { DownloadService.startWithAction(context, DemoDownloadService.class, action, false); } - private DownloadHelper getDownloadHelper( + private DownloadHelper getDownloadHelper( Uri uri, String extension, RenderersFactory renderersFactory) { int type = Util.inferContentType(uri, extension); switch (type) { @@ -231,10 +231,11 @@ public class DownloadTracker implements DownloadManager.Listener { private final class StartDownloadDialogHelper implements DownloadHelper.Callback, DialogInterface.OnClickListener, + DialogInterface.OnDismissListener, View.OnClickListener, TrackSelectionView.DialogCallback { - private final DownloadHelper downloadHelper; + private final DownloadHelper downloadHelper; private final String name; private final LayoutInflater dialogInflater; private final AlertDialog dialog; @@ -244,20 +245,21 @@ public class DownloadTracker implements DownloadManager.Listener { private DefaultTrackSelector.Parameters parameters; private StartDownloadDialogHelper( - Activity activity, DownloadHelper downloadHelper, String name) { + Activity activity, DownloadHelper downloadHelper, String name) { this.downloadHelper = downloadHelper; this.name = name; AlertDialog.Builder builder = new AlertDialog.Builder(activity) .setTitle(R.string.download_preparing) - .setPositiveButton(android.R.string.ok, this) - .setNegativeButton(android.R.string.cancel, null); + .setPositiveButton(android.R.string.ok, /* listener= */ this) + .setNegativeButton(android.R.string.cancel, /* listener= */ null); // Inflate with the builder's context to ensure the correct style is used. dialogInflater = LayoutInflater.from(builder.getContext()); selectionList = (LinearLayout) dialogInflater.inflate(R.layout.start_download_dialog, null); builder.setView(selectionList); dialog = builder.create(); + dialog.setOnDismissListener(/* listener= */ this); dialog.show(); dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); @@ -268,19 +270,17 @@ public class DownloadTracker implements DownloadManager.Listener { // DownloadHelper.Callback implementation. @Override - public void onPrepared(DownloadHelper helper) { - if (helper.getPeriodCount() < 1) { - onPrepareError(downloadHelper, new IOException("Content is empty.")); - return; + public void onPrepared(DownloadHelper helper) { + if (helper.getPeriodCount() > 0) { + mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0); + updateSelectionList(); } - mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0); - updateSelectionList(); dialog.setTitle(R.string.exo_download_description); dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true); } @Override - public void onPrepareError(DownloadHelper helper, IOException e) { + public void onPrepareError(DownloadHelper helper, IOException e) { Toast.makeText( context.getApplicationContext(), R.string.download_start_error, Toast.LENGTH_LONG) .show(); @@ -326,6 +326,13 @@ public class DownloadTracker implements DownloadManager.Listener { startDownload(downloadAction); } + // DialogInterface.OnDismissListener implementation. + + @Override + public void onDismiss(DialogInterface dialog) { + downloadHelper.release(); + } + // Internal methods. private void updateSelectionList() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index e799aff4b2..0cd8081708 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -17,7 +17,9 @@ package com.google.android.exoplayer2.offline; import android.net.Uri; import android.os.Handler; +import android.os.HandlerThread; import android.os.Looper; +import android.os.Message; import android.support.annotation.Nullable; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; @@ -27,6 +29,8 @@ import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -36,7 +40,9 @@ import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.Paramet import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; +import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; @@ -58,19 +64,19 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; *

    A typical usage of DownloadHelper follows these steps: * *

      - *
    1. Construct the download helper with information about the {@link RenderersFactory renderers} - * and {@link DefaultTrackSelector.Parameters parameters} for track selection. + *
    2. Construct the download helper with the {@link MediaSource}, information about the {@link + * RenderersFactory renderers} and {@link DefaultTrackSelector.Parameters parameters} for + * track selection. *
    3. Prepare the helper using {@link #prepare(Callback)} and wait for the callback. *
    4. Optional: Inspect the selected tracks using {@link #getMappedTrackInfo(int)} and {@link * #getTrackSelections(int, int)}, and make adjustments using {@link * #clearTrackSelections(int)}, {@link #replaceTrackSelections(int, Parameters)} and {@link * #addTrackSelection(int, Parameters)}. - *
    5. Create download actions for the selected track using {@link #getDownloadAction(byte[])}. + *
    6. Create a download action for the selected track using {@link #getDownloadAction(byte[])}. + *
    7. Release the helper using {@link #release()}. *
    - * - * @param The manifest type. */ -public abstract class DownloadHelper { +public abstract class DownloadHelper { /** * The default parameters used for track selection for downloading. This default selects the @@ -87,7 +93,7 @@ public abstract class DownloadHelper { * * @param helper The reporting {@link DownloadHelper}. */ - void onPrepared(DownloadHelper helper); + void onPrepared(DownloadHelper helper); /** * Called when preparation fails. @@ -95,18 +101,21 @@ public abstract class DownloadHelper { * @param helper The reporting {@link DownloadHelper}. * @param e The error. */ - void onPrepareError(DownloadHelper helper, IOException e); + void onPrepareError(DownloadHelper helper, IOException e); } private final String downloadType; private final Uri uri; @Nullable private final String cacheKey; + @Nullable private final MediaSource mediaSource; private final DefaultTrackSelector trackSelector; private final RendererCapabilities[] rendererCapabilities; private final SparseIntArray scratchSet; - private int currentTrackSelectionPeriodIndex; - @Nullable private T manifest; + private boolean isPreparedWithMedia; + private @MonotonicNonNull Callback callback; + private @MonotonicNonNull Handler callbackHandler; + private @MonotonicNonNull MediaPreparer mediaPreparer; private TrackGroupArray @MonotonicNonNull [] trackGroupArrays; private MappedTrackInfo @MonotonicNonNull [] mappedTrackInfos; private List @MonotonicNonNull [][] trackSelectionsByPeriodAndRenderer; @@ -118,10 +127,12 @@ public abstract class DownloadHelper { * @param downloadType A download type. This value will be used as {@link DownloadAction#type}. * @param uri A {@link Uri}. * @param cacheKey An optional cache key. + * @param mediaSource A {@link MediaSource} for which tracks are selected, or null if no track + * selection needs to be made. * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for * downloading. * @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks - * are selected. + * are selected, or null if no track selection needs to be made. * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by * {@code renderersFactory}. */ @@ -129,14 +140,19 @@ public abstract class DownloadHelper { String downloadType, Uri uri, @Nullable String cacheKey, + @Nullable MediaSource mediaSource, DefaultTrackSelector.Parameters trackSelectorParameters, - RenderersFactory renderersFactory, + @Nullable RenderersFactory renderersFactory, @Nullable DrmSessionManager drmSessionManager) { this.downloadType = downloadType; this.uri = uri; this.cacheKey = cacheKey; + this.mediaSource = mediaSource; this.trackSelector = new DefaultTrackSelector(new DownloadTrackSelection.Factory()); - this.rendererCapabilities = Util.getRendererCapabilities(renderersFactory, drmSessionManager); + this.rendererCapabilities = + renderersFactory == null + ? new RendererCapabilities[0] + : Util.getRendererCapabilities(renderersFactory, drmSessionManager); this.scratchSet = new SparseIntArray(); trackSelector.setParameters(trackSelectorParameters); trackSelector.init(/* listener= */ () -> {}, new DummyBandwidthMeter()); @@ -148,35 +164,38 @@ public abstract class DownloadHelper { * @param callback A callback to be notified when preparation completes or fails. The callback * will be invoked on the calling thread unless that thread does not have an associated {@link * Looper}, in which case it will be called on the application's main thread. + * @throws IllegalStateException If the download helper has already been prepared. */ public final void prepare(Callback callback) { - Handler handler = + Assertions.checkState(this.callback == null); + this.callback = callback; + callbackHandler = new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper()); - new Thread( - () -> { - try { - manifest = loadManifest(uri); - trackGroupArrays = getTrackGroupArrays(manifest); - initializeTrackSelectionLists(trackGroupArrays.length, rendererCapabilities.length); - mappedTrackInfos = new MappedTrackInfo[trackGroupArrays.length]; - for (int i = 0; i < trackGroupArrays.length; i++) { - TrackSelectorResult trackSelectorResult = runTrackSelection(/* periodIndex= */ i); - trackSelector.onSelectionActivated(trackSelectorResult.info); - mappedTrackInfos[i] = - Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo()); - } - handler.post(() -> callback.onPrepared(DownloadHelper.this)); - } catch (final IOException e) { - handler.post(() -> callback.onPrepareError(DownloadHelper.this, e)); - } - }) - .start(); + if (mediaSource != null) { + mediaPreparer = new MediaPreparer(mediaSource, /* downloadHelper= */ this); + } else { + callbackHandler.post(() -> callback.onPrepared(this)); + } } - /** Returns the manifest. Must not be called until after preparation completes. */ - public final T getManifest() { - Assertions.checkNotNull(manifest); - return manifest; + /** Releases the helper and all resources it is holding. */ + public final void release() { + if (mediaPreparer != null) { + mediaPreparer.release(); + } + } + + /** + * Returns the manifest, or null if no manifest is loaded. Must not be called until after + * preparation completes. + */ + @Nullable + public final Object getManifest() { + if (mediaSource == null) { + return null; + } + assertPreparedWithMedia(); + return mediaPreparer.manifest; } /** @@ -184,7 +203,10 @@ public abstract class DownloadHelper { * preparation completes. */ public final int getPeriodCount() { - Assertions.checkNotNull(trackGroupArrays); + if (mediaSource == null) { + return 0; + } + assertPreparedWithMedia(); return trackGroupArrays.length; } @@ -199,7 +221,7 @@ public abstract class DownloadHelper { * content. */ public final TrackGroupArray getTrackGroups(int periodIndex) { - Assertions.checkNotNull(trackGroupArrays); + assertPreparedWithMedia(); return trackGroupArrays[periodIndex]; } @@ -211,7 +233,7 @@ public abstract class DownloadHelper { * @return The {@link MappedTrackInfo} for the period. */ public final MappedTrackInfo getMappedTrackInfo(int periodIndex) { - Assertions.checkNotNull(mappedTrackInfos); + assertPreparedWithMedia(); return mappedTrackInfos[periodIndex]; } @@ -224,7 +246,7 @@ public abstract class DownloadHelper { * @return A list of selected {@link TrackSelection track selections}. */ public final List getTrackSelections(int periodIndex, int rendererIndex) { - Assertions.checkNotNull(immutableTrackSelectionsByPeriodAndRenderer); + assertPreparedWithMedia(); return immutableTrackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex]; } @@ -235,7 +257,7 @@ public abstract class DownloadHelper { * @param periodIndex The period index for which track selections are cleared. */ public final void clearTrackSelections(int periodIndex) { - Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer); + assertPreparedWithMedia(); for (int i = 0; i < rendererCapabilities.length; i++) { trackSelectionsByPeriodAndRenderer[periodIndex][i].clear(); } @@ -265,8 +287,7 @@ public abstract class DownloadHelper { */ public final void addTrackSelection( int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) { - Assertions.checkNotNull(trackGroupArrays); - Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer); + assertPreparedWithMedia(); trackSelector.setParameters(trackSelectorParameters); runTrackSelection(periodIndex); } @@ -279,26 +300,21 @@ public abstract class DownloadHelper { * @return The built {@link DownloadAction}. */ public final DownloadAction getDownloadAction(@Nullable byte[] data) { - Assertions.checkNotNull(trackSelectionsByPeriodAndRenderer); - Assertions.checkNotNull(trackGroupArrays); + if (mediaSource == null) { + return DownloadAction.createDownloadAction( + downloadType, uri, /* keys= */ Collections.emptyList(), cacheKey, data); + } + assertPreparedWithMedia(); List streamKeys = new ArrayList<>(); + List allSelections = new ArrayList<>(); int periodCount = trackSelectionsByPeriodAndRenderer.length; for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) { + allSelections.clear(); int rendererCount = trackSelectionsByPeriodAndRenderer[periodIndex].length; for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { - List trackSelectionList = - trackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex]; - for (int selectionIndex = 0; selectionIndex < trackSelectionList.size(); selectionIndex++) { - TrackSelection trackSelection = trackSelectionList.get(selectionIndex); - int trackGroupIndex = - trackGroupArrays[periodIndex].indexOf(trackSelection.getTrackGroup()); - int trackCount = trackSelection.length(); - for (int trackListIndex = 0; trackListIndex < trackCount; trackListIndex++) { - int trackIndex = trackSelection.getIndexInTrackGroup(trackListIndex); - streamKeys.add(toStreamKey(periodIndex, trackGroupIndex, trackIndex)); - } - } + allSelections.addAll(trackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex]); } + streamKeys.addAll(mediaPreparer.mediaPeriods[periodIndex].getStreamKeys(allSelections)); } return DownloadAction.createDownloadAction(downloadType, uri, streamKeys, cacheKey, data); } @@ -312,36 +328,14 @@ public abstract class DownloadHelper { return DownloadAction.createRemoveAction(downloadType, uri, cacheKey); } - /** - * Loads the manifest. This method is called on a background thread. - * - * @param uri The manifest uri. - * @throws IOException If loading fails. - */ - protected abstract T loadManifest(Uri uri) throws IOException; - - /** - * Returns the track group arrays for each period in the manifest. - * - * @param manifest The manifest. - * @return An array of {@link TrackGroupArray}s. One for each period in the manifest. - */ - protected abstract TrackGroupArray[] getTrackGroupArrays(T manifest); - - /** - * Converts a track of a track group of a period to the corresponding {@link StreamKey}. - * - * @param periodIndex The index of the containing period. - * @param trackGroupIndex The index of the containing track group within the period. - * @param trackIndexInTrackGroup The index of the track within the track group. - * @return The corresponding {@link StreamKey}. - */ - protected abstract StreamKey toStreamKey( - int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup); - + // Initialization of array of Lists. @SuppressWarnings("unchecked") - @EnsuresNonNull("trackSelectionsByPeriodAndRenderer") - private void initializeTrackSelectionLists(int periodCount, int rendererCount) { + private void onMediaPrepared() { + Assertions.checkNotNull(mediaPreparer); + Assertions.checkNotNull(mediaPreparer.mediaPeriods); + Assertions.checkNotNull(mediaPreparer.timeline); + int periodCount = mediaPreparer.mediaPeriods.length; + int rendererCount = rendererCapabilities.length; trackSelectionsByPeriodAndRenderer = (List[][]) new List[periodCount][rendererCount]; immutableTrackSelectionsByPeriodAndRenderer = @@ -353,6 +347,49 @@ public abstract class DownloadHelper { Collections.unmodifiableList(trackSelectionsByPeriodAndRenderer[i][j]); } } + trackGroupArrays = new TrackGroupArray[periodCount]; + mappedTrackInfos = new MappedTrackInfo[periodCount]; + for (int i = 0; i < periodCount; i++) { + trackGroupArrays[i] = mediaPreparer.mediaPeriods[i].getTrackGroups(); + TrackSelectorResult trackSelectorResult = runTrackSelection(/* periodIndex= */ i); + trackSelector.onSelectionActivated(trackSelectorResult.info); + mappedTrackInfos[i] = Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo()); + } + setPreparedWithMedia(); + Assertions.checkNotNull(callbackHandler) + .post(() -> Assertions.checkNotNull(callback).onPrepared(this)); + } + + private void onMediaPreparationFailed(IOException error) { + Assertions.checkNotNull(callbackHandler) + .post(() -> Assertions.checkNotNull(callback).onPrepareError(this, error)); + } + + @RequiresNonNull({ + "trackGroupArrays", + "mappedTrackInfos", + "trackSelectionsByPeriodAndRenderer", + "immutableTrackSelectionsByPeriodAndRenderer", + "mediaPreparer", + "mediaPreparer.timeline", + "mediaPreparer.mediaPeriods" + }) + private void setPreparedWithMedia() { + isPreparedWithMedia = true; + } + + @EnsuresNonNull({ + "trackGroupArrays", + "mappedTrackInfos", + "trackSelectionsByPeriodAndRenderer", + "immutableTrackSelectionsByPeriodAndRenderer", + "mediaPreparer", + "mediaPreparer.timeline", + "mediaPreparer.mediaPeriods" + }) + @SuppressWarnings("nullness:contracts.postcondition.not.satisfied") + private void assertPreparedWithMedia() { + Assertions.checkState(isPreparedWithMedia); } /** @@ -361,26 +398,27 @@ public abstract class DownloadHelper { */ // Intentional reference comparison of track group instances. @SuppressWarnings("ReferenceEquality") - @RequiresNonNull({"trackGroupArrays", "trackSelectionsByPeriodAndRenderer"}) + @RequiresNonNull({ + "trackGroupArrays", + "trackSelectionsByPeriodAndRenderer", + "mediaPreparer", + "mediaPreparer.timeline" + }) private TrackSelectorResult runTrackSelection(int periodIndex) { - // TODO: Use actual timeline and media period id. - MediaPeriodId dummyMediaPeriodId = new MediaPeriodId(new Object()); - Timeline dummyTimeline = Timeline.EMPTY; - currentTrackSelectionPeriodIndex = periodIndex; try { TrackSelectorResult trackSelectorResult = trackSelector.selectTracks( rendererCapabilities, trackGroupArrays[periodIndex], - dummyMediaPeriodId, - dummyTimeline); + new MediaPeriodId(mediaPreparer.timeline.getUidOfPeriod(periodIndex)), + mediaPreparer.timeline); for (int i = 0; i < trackSelectorResult.length; i++) { TrackSelection newSelection = trackSelectorResult.selections.get(i); if (newSelection == null) { continue; } List existingSelectionList = - trackSelectionsByPeriodAndRenderer[currentTrackSelectionPeriodIndex][i]; + trackSelectionsByPeriodAndRenderer[periodIndex][i]; boolean mergedWithExistingSelection = false; for (int j = 0; j < existingSelectionList.size(); j++) { TrackSelection existingSelection = existingSelectionList.get(j); @@ -414,6 +452,113 @@ public abstract class DownloadHelper { } } + private static final class MediaPreparer + implements MediaSource.SourceInfoRefreshListener, MediaPeriod.Callback, Handler.Callback { + + private static final int MESSAGE_PREPARE_SOURCE = 0; + private static final int MESSAGE_CHECK_FOR_FAILURE = 1; + + private final MediaSource mediaSource; + private final DownloadHelper downloadHelper; + private final Allocator allocator; + private final HandlerThread mediaSourceThread; + private final Handler mediaSourceHandler; + + @Nullable public Object manifest; + public @MonotonicNonNull Timeline timeline; + public MediaPeriod @MonotonicNonNull [] mediaPeriods; + + private int pendingPreparations; + + public MediaPreparer(MediaSource mediaSource, DownloadHelper downloadHelper) { + this.mediaSource = mediaSource; + this.downloadHelper = downloadHelper; + allocator = new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE); + mediaSourceThread = new HandlerThread("DownloadHelper"); + mediaSourceThread.start(); + mediaSourceHandler = Util.createHandler(mediaSourceThread.getLooper(), /* callback= */ this); + mediaSourceHandler.sendEmptyMessage(MESSAGE_PREPARE_SOURCE); + } + + public void release() { + if (mediaPeriods != null) { + for (MediaPeriod mediaPeriod : mediaPeriods) { + mediaSource.releasePeriod(mediaPeriod); + } + } + mediaSource.releaseSource(this); + mediaSourceThread.quit(); + } + + // Handler.Callback + + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_PREPARE_SOURCE: + mediaSource.prepareSource(/* listener= */ this, /* mediaTransferListener= */ null); + mediaSourceHandler.sendEmptyMessage(MESSAGE_CHECK_FOR_FAILURE); + return true; + case MESSAGE_CHECK_FOR_FAILURE: + try { + if (mediaPeriods == null) { + mediaSource.maybeThrowSourceInfoRefreshError(); + } else { + for (MediaPeriod mediaPeriod : mediaPeriods) { + mediaPeriod.maybeThrowPrepareError(); + } + } + mediaSourceHandler.sendEmptyMessageDelayed( + MESSAGE_CHECK_FOR_FAILURE, /* delayMillis= */ 100); + } catch (IOException e) { + downloadHelper.onMediaPreparationFailed(e); + } + return true; + default: + return false; + } + } + + // MediaSource.SourceInfoRefreshListener implementation. + + @Override + public void onSourceInfoRefreshed( + MediaSource source, Timeline timeline, @Nullable Object manifest) { + if (this.timeline != null) { + // Ignore dynamic updates. + return; + } + this.timeline = timeline; + this.manifest = manifest; + mediaPeriods = new MediaPeriod[timeline.getPeriodCount()]; + pendingPreparations = mediaPeriods.length; + for (int i = 0; i < mediaPeriods.length; i++) { + mediaPeriods[i] = + mediaSource.createPeriod( + new MediaPeriodId(timeline.getUidOfPeriod(/* periodIndex= */ i)), + allocator, + /* startPositionUs= */ 0); + mediaPeriods[i].prepare(/* callback= */ this, /* positionUs= */ 0); + } + } + + // MediaPeriod.Callback implementation. + + @Override + public void onPrepared(MediaPeriod mediaPeriod) { + pendingPreparations--; + if (pendingPreparations == 0) { + mediaSourceHandler.removeMessages(MESSAGE_CHECK_FOR_FAILURE); + downloadHelper.onMediaPrepared(); + } + } + + @Override + public void onContinueLoadingRequested(MediaPeriod source) { + // Ignore. + } + } + private static final class DownloadTrackSelection extends BaseTrackSelection { private static final class Factory implements TrackSelection.Factory { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java index 2ec14368ca..1850eaebf2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java @@ -17,11 +17,9 @@ package com.google.android.exoplayer2.offline; import android.net.Uri; import android.support.annotation.Nullable; -import com.google.android.exoplayer2.Renderer; -import com.google.android.exoplayer2.source.TrackGroupArray; /** A {@link DownloadHelper} for progressive streams. */ -public final class ProgressiveDownloadHelper extends DownloadHelper { +public final class ProgressiveDownloadHelper extends DownloadHelper { /** * Creates download helper for progressive streams. @@ -43,24 +41,9 @@ public final class ProgressiveDownloadHelper extends DownloadHelper { DownloadAction.TYPE_PROGRESSIVE, uri, cacheKey, - DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, - (handler, videoListener, audioListener, metadata, text, drm) -> new Renderer[0], + /* mediaSource= */ null, + /* trackSelectorParameters= */ DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, + /* renderersFactory= */ null, /* drmSessionManager= */ null); } - - @Override - protected Void loadManifest(Uri uri) { - return null; - } - - @Override - protected TrackGroupArray[] getTrackGroupArrays(Void manifest) { - return new TrackGroupArray[] {TrackGroupArray.EMPTY}; - } - - @Override - protected StreamKey toStreamKey( - int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) { - return new StreamKey(periodIndex, trackGroupIndex, trackIndexInTrackGroup); - } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java index 5f287d8685..e6cca02140 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java @@ -22,17 +22,28 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.offline.DownloadHelper.Callback; +import com.google.android.exoplayer2.source.MediaPeriod; +import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; +import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.testutil.FakeMediaPeriod; +import com.google.android.exoplayer2.testutil.FakeMediaSource; import com.google.android.exoplayer2.testutil.FakeRenderer; +import com.google.android.exoplayer2.testutil.FakeTimeline; +import com.google.android.exoplayer2.testutil.FakeTimeline.TimelineWindowDefinition; +import com.google.android.exoplayer2.testutil.RobolectricUtil; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.ParametersBuilder; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.ConditionVariable; import com.google.android.exoplayer2.util.MimeTypes; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicReference; @@ -40,15 +51,19 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowLooper; /** Unit tests for {@link DownloadHelper}. */ @RunWith(RobolectricTestRunner.class) +@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class}) public class DownloadHelperTest { private static final String TEST_DOWNLOAD_TYPE = "downloadType"; private static final String TEST_CACHE_KEY = "cacheKey"; - private static final ManifestType TEST_MANIFEST = new ManifestType(); + private static final Timeline TEST_TIMELINE = + new FakeTimeline(new TimelineWindowDefinition(/* periodCount= */ 2, /* id= */ new Object())); + private static final Object TEST_MANIFEST = new Object(); private static final Format VIDEO_FORMAT_LOW = createVideoFormat(/* bitrate= */ 200_000); private static final Format VIDEO_FORMAT_HIGH = createVideoFormat(/* bitrate= */ 800_000); @@ -98,7 +113,7 @@ public class DownloadHelperTest { public void getManifest_returnsManifest() throws Exception { prepareDownloadHelper(downloadHelper); - ManifestType manifest = downloadHelper.getManifest(); + Object manifest = downloadHelper.getManifest(); assertThat(manifest).isEqualTo(TEST_MANIFEST); } @@ -337,12 +352,12 @@ public class DownloadHelperTest { downloadHelper.prepare( new Callback() { @Override - public void onPrepared(DownloadHelper helper) { + public void onPrepared(DownloadHelper helper) { preparedCondition.open(); } @Override - public void onPrepareError(DownloadHelper helper, IOException e) { + public void onPrepareError(DownloadHelper helper, IOException e) { prepareException.set(e); preparedCondition.open(); } @@ -411,35 +426,52 @@ public class DownloadHelperTest { assertThat(selectedTracksInGroup).isEqualTo(tracks); } - private static final class ManifestType {} - - private static final class FakeDownloadHelper extends DownloadHelper { + private static final class FakeDownloadHelper extends DownloadHelper { public FakeDownloadHelper(Uri testUri, RenderersFactory renderersFactory) { super( TEST_DOWNLOAD_TYPE, testUri, TEST_CACHE_KEY, + new TestMediaSource(), DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, renderersFactory, /* drmSessionManager= */ null); } + } - @Override - protected ManifestType loadManifest(Uri uri) throws IOException { - return TEST_MANIFEST; + private static final class TestMediaSource extends FakeMediaSource { + + public TestMediaSource() { + super(TEST_TIMELINE, TEST_MANIFEST); } @Override - protected TrackGroupArray[] getTrackGroupArrays(ManifestType manifest) { - assertThat(manifest).isEqualTo(TEST_MANIFEST); - return TRACK_GROUP_ARRAYS; + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + int periodIndex = TEST_TIMELINE.getIndexOfPeriod(id.periodUid); + return new FakeMediaPeriod( + TRACK_GROUP_ARRAYS[periodIndex], + new EventDispatcher() + .withParameters(/* windowIndex= */ 0, id, /* mediaTimeOffsetMs= */ 0)) { + @Override + public List getStreamKeys(List trackSelections) { + List result = new ArrayList<>(); + for (TrackSelection trackSelection : trackSelections) { + int groupIndex = + TRACK_GROUP_ARRAYS[periodIndex].indexOf(trackSelection.getTrackGroup()); + for (int i = 0; i < trackSelection.length(); i++) { + result.add( + new StreamKey(periodIndex, groupIndex, trackSelection.getIndexInTrackGroup(i))); + } + } + return result; + } + }; } @Override - protected StreamKey toStreamKey( - int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) { - return new StreamKey(periodIndex, trackGroupIndex, trackIndexInTrackGroup); + public void releasePeriod(MediaPeriod mediaPeriod) { + // Do nothing. } } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java index f86e47ed3d..b611cf0d5f 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java @@ -17,30 +17,17 @@ package com.google.android.exoplayer2.source.dash.offline; import android.net.Uri; import android.support.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadHelper; -import com.google.android.exoplayer2.offline.StreamKey; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; -import com.google.android.exoplayer2.source.dash.manifest.DashManifest; -import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; -import com.google.android.exoplayer2.source.dash.manifest.Representation; +import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.ParsingLoadable; -import java.io.IOException; -import java.util.List; /** A {@link DownloadHelper} for DASH streams. */ -public final class DashDownloadHelper extends DownloadHelper { - - private final DataSource.Factory manifestDataSourceFactory; +public final class DashDownloadHelper extends DownloadHelper { /** * Creates a DASH download helper. @@ -85,42 +72,9 @@ public final class DashDownloadHelper extends DownloadHelper { DownloadAction.TYPE_DASH, uri, /* cacheKey= */ null, + new DashMediaSource.Factory(manifestDataSourceFactory).createMediaSource(uri), trackSelectorParameters, renderersFactory, drmSessionManager); - this.manifestDataSourceFactory = manifestDataSourceFactory; - } - - @Override - protected DashManifest loadManifest(Uri uri) throws IOException { - DataSource dataSource = manifestDataSourceFactory.createDataSource(); - return ParsingLoadable.load(dataSource, new DashManifestParser(), uri, C.DATA_TYPE_MANIFEST); - } - - @Override - public TrackGroupArray[] getTrackGroupArrays(DashManifest manifest) { - int periodCount = manifest.getPeriodCount(); - TrackGroupArray[] trackGroupArrays = new TrackGroupArray[periodCount]; - for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) { - List adaptationSets = manifest.getPeriod(periodIndex).adaptationSets; - TrackGroup[] trackGroups = new TrackGroup[adaptationSets.size()]; - for (int i = 0; i < trackGroups.length; i++) { - List representations = adaptationSets.get(i).representations; - Format[] formats = new Format[representations.size()]; - int representationsCount = representations.size(); - for (int j = 0; j < representationsCount; j++) { - formats[j] = representations.get(j).format; - } - trackGroups[i] = new TrackGroup(formats); - } - trackGroupArrays[periodIndex] = new TrackGroupArray(trackGroups); - } - return trackGroupArrays; - } - - @Override - protected StreamKey toStreamKey( - int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) { - return new StreamKey(periodIndex, trackGroupIndex, trackIndexInTrackGroup); } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java index e0f55aa738..ee6bbe333a 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java @@ -17,34 +17,17 @@ package com.google.android.exoplayer2.source.hls.offline; import android.net.Uri; import android.support.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadHelper; -import com.google.android.exoplayer2.offline.StreamKey; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist; -import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser; +import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.ParsingLoadable; -import com.google.android.exoplayer2.util.Assertions; -import java.io.IOException; -import java.util.Arrays; -import java.util.List; /** A {@link DownloadHelper} for HLS streams. */ -public final class HlsDownloadHelper extends DownloadHelper { - - private final DataSource.Factory manifestDataSourceFactory; - - private int[] renditionGroups; +public final class HlsDownloadHelper extends DownloadHelper { /** * Creates a HLS download helper. @@ -89,56 +72,11 @@ public final class HlsDownloadHelper extends DownloadHelper { DownloadAction.TYPE_HLS, uri, /* cacheKey= */ null, + new HlsMediaSource.Factory(manifestDataSourceFactory) + .setAllowChunklessPreparation(true) + .createMediaSource(uri), trackSelectorParameters, renderersFactory, drmSessionManager); - this.manifestDataSourceFactory = manifestDataSourceFactory; - } - - @Override - protected HlsPlaylist loadManifest(Uri uri) throws IOException { - DataSource dataSource = manifestDataSourceFactory.createDataSource(); - return ParsingLoadable.load(dataSource, new HlsPlaylistParser(), uri, C.DATA_TYPE_MANIFEST); - } - - @Override - protected TrackGroupArray[] getTrackGroupArrays(HlsPlaylist playlist) { - Assertions.checkNotNull(playlist); - if (playlist instanceof HlsMediaPlaylist) { - renditionGroups = new int[0]; - return new TrackGroupArray[] {TrackGroupArray.EMPTY}; - } - // TODO: Generate track groups as in playback. Reverse the mapping in toStreamKey. - HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; - TrackGroup[] trackGroups = new TrackGroup[3]; - renditionGroups = new int[3]; - int trackGroupIndex = 0; - if (!masterPlaylist.variants.isEmpty()) { - renditionGroups[trackGroupIndex] = HlsMasterPlaylist.GROUP_INDEX_VARIANT; - trackGroups[trackGroupIndex++] = new TrackGroup(toFormats(masterPlaylist.variants)); - } - if (!masterPlaylist.audios.isEmpty()) { - renditionGroups[trackGroupIndex] = HlsMasterPlaylist.GROUP_INDEX_AUDIO; - trackGroups[trackGroupIndex++] = new TrackGroup(toFormats(masterPlaylist.audios)); - } - if (!masterPlaylist.subtitles.isEmpty()) { - renditionGroups[trackGroupIndex] = HlsMasterPlaylist.GROUP_INDEX_SUBTITLE; - trackGroups[trackGroupIndex++] = new TrackGroup(toFormats(masterPlaylist.subtitles)); - } - return new TrackGroupArray[] {new TrackGroupArray(Arrays.copyOf(trackGroups, trackGroupIndex))}; - } - - @Override - protected StreamKey toStreamKey( - int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) { - return new StreamKey(renditionGroups[trackGroupIndex], trackIndexInTrackGroup); - } - - private static Format[] toFormats(List hlsUrls) { - Format[] formats = new Format[hlsUrls.size()]; - for (int i = 0; i < hlsUrls.size(); i++) { - formats[i] = hlsUrls.get(i).format; - } - return formats; } } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java index b17768f202..f76fb4ee90 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java @@ -17,27 +17,17 @@ package com.google.android.exoplayer2.source.smoothstreaming.offline; import android.net.Uri; import android.support.annotation.Nullable; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadHelper; -import com.google.android.exoplayer2.offline.StreamKey; -import com.google.android.exoplayer2.source.TrackGroup; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; -import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsUtil; +import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DataSource; -import com.google.android.exoplayer2.upstream.ParsingLoadable; -import java.io.IOException; /** A {@link DownloadHelper} for SmoothStreaming streams. */ -public final class SsDownloadHelper extends DownloadHelper { - - private final DataSource.Factory manifestDataSourceFactory; +public final class SsDownloadHelper extends DownloadHelper { /** * Creates a SmoothStreaming download helper. @@ -82,32 +72,9 @@ public final class SsDownloadHelper extends DownloadHelper { DownloadAction.TYPE_SS, uri, /* cacheKey= */ null, + new SsMediaSource.Factory(manifestDataSourceFactory).createMediaSource(uri), trackSelectorParameters, renderersFactory, drmSessionManager); - this.manifestDataSourceFactory = manifestDataSourceFactory; - } - - @Override - protected SsManifest loadManifest(Uri uri) throws IOException { - DataSource dataSource = manifestDataSourceFactory.createDataSource(); - Uri fixedUri = SsUtil.fixManifestUri(uri); - return ParsingLoadable.load(dataSource, new SsManifestParser(), fixedUri, C.DATA_TYPE_MANIFEST); - } - - @Override - protected TrackGroupArray[] getTrackGroupArrays(SsManifest manifest) { - SsManifest.StreamElement[] streamElements = manifest.streamElements; - TrackGroup[] trackGroups = new TrackGroup[streamElements.length]; - for (int i = 0; i < streamElements.length; i++) { - trackGroups[i] = new TrackGroup(streamElements[i].formats); - } - return new TrackGroupArray[] {new TrackGroupArray(trackGroups)}; - } - - @Override - protected StreamKey toStreamKey( - int periodIndex, int trackGroupIndex, int trackIndexInTrackGroup) { - return new StreamKey(trackGroupIndex, trackIndexInTrackGroup); } } From 71d77d7fa07521ee37436423d8fd28004e995bbf Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 29 Jan 2019 12:58:45 +0000 Subject: [PATCH 069/110] Remove DownloadHelper subclasses and use static methods instead. The subclasses only call specific constructor combinations and can easily replaced by static methods. PiperOrigin-RevId: 231385606 --- .../exoplayer2/demo/DownloadTracker.java | 12 +- library/core/proguard-rules.txt | 17 ++ .../exoplayer2/offline/DownloadHelper.java | 281 ++++++++++++++++-- .../offline/ProgressiveDownloadHelper.java | 49 --- .../offline/DownloadHelperTest.java | 28 +- .../dash/offline/DashDownloadHelper.java | 80 ----- .../dash/offline/DownloadHelperTest.java | 43 +++ .../source/hls/offline/HlsDownloadHelper.java | 82 ----- .../hls/offline/DownloadHelperTest.java | 43 +++ .../offline/SsDownloadHelper.java | 80 ----- .../offline/DownloadHelperTest.java | 43 +++ 11 files changed, 416 insertions(+), 342 deletions(-) delete mode 100644 library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java delete mode 100644 library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java create mode 100644 library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java delete mode 100644 library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java create mode 100644 library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/DownloadHelperTest.java delete mode 100644 library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java create mode 100644 library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/DownloadHelperTest.java diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java index b0e56b9f92..83c8a76812 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java @@ -39,13 +39,9 @@ import com.google.android.exoplayer2.offline.DownloadHelper; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadState; -import com.google.android.exoplayer2.offline.ProgressiveDownloadHelper; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.source.dash.offline.DashDownloadHelper; -import com.google.android.exoplayer2.source.hls.offline.HlsDownloadHelper; -import com.google.android.exoplayer2.source.smoothstreaming.offline.SsDownloadHelper; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; import com.google.android.exoplayer2.trackselection.TrackSelection; @@ -215,13 +211,13 @@ public class DownloadTracker implements DownloadManager.Listener { int type = Util.inferContentType(uri, extension); switch (type) { case C.TYPE_DASH: - return new DashDownloadHelper(uri, dataSourceFactory, renderersFactory); + return DownloadHelper.forDash(uri, dataSourceFactory, renderersFactory); case C.TYPE_SS: - return new SsDownloadHelper(uri, dataSourceFactory, renderersFactory); + return DownloadHelper.forSmoothStreaming(uri, dataSourceFactory, renderersFactory); case C.TYPE_HLS: - return new HlsDownloadHelper(uri, dataSourceFactory, renderersFactory); + return DownloadHelper.forHls(uri, dataSourceFactory, renderersFactory); case C.TYPE_OTHER: - return new ProgressiveDownloadHelper(uri); + return DownloadHelper.forProgressive(uri); default: throw new IllegalStateException("Unsupported type: " + type); } diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index 4fe182d34a..d18c34b86d 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -44,5 +44,22 @@ (android.net.Uri, java.util.List, com.google.android.exoplayer2.offlineDownloaderConstructorHelper); } +# Constructors accessed via reflection in DownloadHelper +-dontnote com.google.android.exoplayer2.source.dash.DashMediaSource$Factory +-keepclassmembers class com.google.android.exoplayer2.source.dash.DashMediaSource$Factory { + (com.google.android.exoplayer2.upstream.DataSource$Factory); + DashMediaSource createMediaSource(android.net.Uri); +} +-dontnote com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory +-keepclassmembers class com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory { + (com.google.android.exoplayer2.upstream.DataSource$Factory); + HlsMediaSource createMediaSource(android.net.Uri); +} +-dontnote com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory +-keepclassmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory { + (com.google.android.exoplayer2.upstream.DataSource$Factory); + SsMediaSource createMediaSource(android.net.Uri); +} + # Don't warn about checkerframework -dontwarn org.checkerframework.** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index 0cd8081708..a3c2278b32 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -21,6 +21,7 @@ import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.support.annotation.Nullable; +import android.util.Pair; import android.util.SparseIntArray; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; @@ -42,11 +43,14 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.BandwidthMeter; +import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultAllocator; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -64,9 +68,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; *

    A typical usage of DownloadHelper follows these steps: * *

      - *
    1. Construct the download helper with the {@link MediaSource}, information about the {@link - * RenderersFactory renderers} and {@link DefaultTrackSelector.Parameters parameters} for - * track selection. + *
    2. Build the helper using one of the {@code forXXX} methods. *
    3. Prepare the helper using {@link #prepare(Callback)} and wait for the callback. *
    4. Optional: Inspect the selected tracks using {@link #getMappedTrackInfo(int)} and {@link * #getTrackSelections(int, int)}, and make adjustments using {@link @@ -76,7 +78,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; *
    5. Release the helper using {@link #release()}. *
    */ -public abstract class DownloadHelper { +public final class DownloadHelper { /** * The default parameters used for track selection for downloading. This default selects the @@ -104,6 +106,207 @@ public abstract class DownloadHelper { void onPrepareError(DownloadHelper helper, IOException e); } + @Nullable private static final Constructor DASH_FACTORY_CONSTRUCTOR; + @Nullable private static final Constructor HLS_FACTORY_CONSTRUCTOR; + @Nullable private static final Constructor SS_FACTORY_CONSTRUCTOR; + @Nullable private static final Method DASH_FACTORY_CREATE_METHOD; + @Nullable private static final Method HLS_FACTORY_CREATE_METHOD; + @Nullable private static final Method SS_FACTORY_CREATE_METHOD; + + static { + Pair<@NullableType Constructor, @NullableType Method> dashFactoryMethods = + getMediaSourceFactoryMethods( + "com.google.android.exoplayer2.source.dash.DashMediaSource$Factory"); + DASH_FACTORY_CONSTRUCTOR = dashFactoryMethods.first; + DASH_FACTORY_CREATE_METHOD = dashFactoryMethods.second; + Pair<@NullableType Constructor, @NullableType Method> hlsFactoryMethods = + getMediaSourceFactoryMethods( + "com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory"); + HLS_FACTORY_CONSTRUCTOR = hlsFactoryMethods.first; + HLS_FACTORY_CREATE_METHOD = hlsFactoryMethods.second; + Pair<@NullableType Constructor, @NullableType Method> ssFactoryMethods = + getMediaSourceFactoryMethods( + "com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory"); + SS_FACTORY_CONSTRUCTOR = ssFactoryMethods.first; + SS_FACTORY_CREATE_METHOD = ssFactoryMethods.second; + } + + /** + * Creates a {@link DownloadHelper} for progressive streams. + * + * @param uri A stream {@link Uri}. + * @return A {@link DownloadHelper} for progressive streams. + */ + public static DownloadHelper forProgressive(Uri uri) { + return forProgressive(uri, /* cacheKey= */ null); + } + + /** + * Creates a {@link DownloadHelper} for progressive streams. + * + * @param uri A stream {@link Uri}. + * @param cacheKey An optional cache key. + * @return A {@link DownloadHelper} for progressive streams. + */ + public static DownloadHelper forProgressive(Uri uri, @Nullable String cacheKey) { + return new DownloadHelper( + DownloadAction.TYPE_PROGRESSIVE, + uri, + cacheKey, + /* mediaSource= */ null, + DEFAULT_TRACK_SELECTOR_PARAMETERS, + /* rendererCapabilities= */ new RendererCapabilities[0]); + } + + /** + * Creates a {@link DownloadHelper} for DASH streams. + * + * @param uri A manifest {@link Uri}. + * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. + * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are + * selected. + * @return A {@link DownloadHelper} for DASH streams. + * @throws IllegalStateException If the DASH module is missing. + */ + public static DownloadHelper forDash( + Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + return forDash( + uri, + dataSourceFactory, + renderersFactory, + /* drmSessionManager= */ null, + DEFAULT_TRACK_SELECTOR_PARAMETERS); + } + + /** + * Creates a {@link DownloadHelper} for DASH streams. + * + * @param uri A manifest {@link Uri}. + * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. + * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are + * selected. + * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by + * {@code renderersFactory}. + * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for + * downloading. + * @return A {@link DownloadHelper} for DASH streams. + * @throws IllegalStateException If the DASH module is missing. + */ + public static DownloadHelper forDash( + Uri uri, + DataSource.Factory dataSourceFactory, + RenderersFactory renderersFactory, + @Nullable DrmSessionManager drmSessionManager, + DefaultTrackSelector.Parameters trackSelectorParameters) { + return new DownloadHelper( + DownloadAction.TYPE_DASH, + uri, + /* cacheKey= */ null, + createMediaSource( + uri, dataSourceFactory, DASH_FACTORY_CONSTRUCTOR, DASH_FACTORY_CREATE_METHOD), + trackSelectorParameters, + Util.getRendererCapabilities(renderersFactory, drmSessionManager)); + } + + /** + * Creates a {@link DownloadHelper} for HLS streams. + * + * @param uri A playlist {@link Uri}. + * @param dataSourceFactory A {@link DataSource.Factory} used to load the playlist. + * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are + * selected. + * @return A {@link DownloadHelper} for HLS streams. + * @throws IllegalStateException If the HLS module is missing. + */ + public static DownloadHelper forHls( + Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + return forHls( + uri, + dataSourceFactory, + renderersFactory, + /* drmSessionManager= */ null, + DEFAULT_TRACK_SELECTOR_PARAMETERS); + } + + /** + * Creates a {@link DownloadHelper} for HLS streams. + * + * @param uri A playlist {@link Uri}. + * @param dataSourceFactory A {@link DataSource.Factory} used to load the playlist. + * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are + * selected. + * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by + * {@code renderersFactory}. + * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for + * downloading. + * @return A {@link DownloadHelper} for HLS streams. + * @throws IllegalStateException If the HLS module is missing. + */ + public static DownloadHelper forHls( + Uri uri, + DataSource.Factory dataSourceFactory, + RenderersFactory renderersFactory, + @Nullable DrmSessionManager drmSessionManager, + DefaultTrackSelector.Parameters trackSelectorParameters) { + return new DownloadHelper( + DownloadAction.TYPE_HLS, + uri, + /* cacheKey= */ null, + createMediaSource( + uri, dataSourceFactory, HLS_FACTORY_CONSTRUCTOR, HLS_FACTORY_CREATE_METHOD), + trackSelectorParameters, + Util.getRendererCapabilities(renderersFactory, drmSessionManager)); + } + + /** + * Creates a {@link DownloadHelper} for SmoothStreaming streams. + * + * @param uri A manifest {@link Uri}. + * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. + * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are + * selected. + * @return A {@link DownloadHelper} for SmoothStreaming streams. + * @throws IllegalStateException If the SmoothStreaming module is missing. + */ + public static DownloadHelper forSmoothStreaming( + Uri uri, DataSource.Factory dataSourceFactory, RenderersFactory renderersFactory) { + return forSmoothStreaming( + uri, + dataSourceFactory, + renderersFactory, + /* drmSessionManager= */ null, + DEFAULT_TRACK_SELECTOR_PARAMETERS); + } + + /** + * Creates a {@link DownloadHelper} for SmoothStreaming streams. + * + * @param uri A manifest {@link Uri}. + * @param dataSourceFactory A {@link DataSource.Factory} used to load the manifest. + * @param renderersFactory A {@link RenderersFactory} creating the renderers for which tracks are + * selected. + * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by + * {@code renderersFactory}. + * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for + * downloading. + * @return A {@link DownloadHelper} for SmoothStreaming streams. + * @throws IllegalStateException If the SmoothStreaming module is missing. + */ + public static DownloadHelper forSmoothStreaming( + Uri uri, + DataSource.Factory dataSourceFactory, + RenderersFactory renderersFactory, + @Nullable DrmSessionManager drmSessionManager, + DefaultTrackSelector.Parameters trackSelectorParameters) { + return new DownloadHelper( + DownloadAction.TYPE_SS, + uri, + /* cacheKey= */ null, + createMediaSource(uri, dataSourceFactory, SS_FACTORY_CONSTRUCTOR, SS_FACTORY_CREATE_METHOD), + trackSelectorParameters, + Util.getRendererCapabilities(renderersFactory, drmSessionManager)); + } + private final String downloadType; private final Uri uri; @Nullable private final String cacheKey; @@ -131,10 +334,8 @@ public abstract class DownloadHelper { * selection needs to be made. * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for * downloading. - * @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks - * are selected, or null if no track selection needs to be made. - * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by - * {@code renderersFactory}. + * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which tracks + * are selected. */ public DownloadHelper( String downloadType, @@ -142,17 +343,13 @@ public abstract class DownloadHelper { @Nullable String cacheKey, @Nullable MediaSource mediaSource, DefaultTrackSelector.Parameters trackSelectorParameters, - @Nullable RenderersFactory renderersFactory, - @Nullable DrmSessionManager drmSessionManager) { + RendererCapabilities[] rendererCapabilities) { this.downloadType = downloadType; this.uri = uri; this.cacheKey = cacheKey; this.mediaSource = mediaSource; this.trackSelector = new DefaultTrackSelector(new DownloadTrackSelection.Factory()); - this.rendererCapabilities = - renderersFactory == null - ? new RendererCapabilities[0] - : Util.getRendererCapabilities(renderersFactory, drmSessionManager); + this.rendererCapabilities = rendererCapabilities; this.scratchSet = new SparseIntArray(); trackSelector.setParameters(trackSelectorParameters); trackSelector.init(/* listener= */ () -> {}, new DummyBandwidthMeter()); @@ -166,7 +363,7 @@ public abstract class DownloadHelper { * Looper}, in which case it will be called on the application's main thread. * @throws IllegalStateException If the download helper has already been prepared. */ - public final void prepare(Callback callback) { + public void prepare(Callback callback) { Assertions.checkState(this.callback == null); this.callback = callback; callbackHandler = @@ -179,7 +376,7 @@ public abstract class DownloadHelper { } /** Releases the helper and all resources it is holding. */ - public final void release() { + public void release() { if (mediaPreparer != null) { mediaPreparer.release(); } @@ -190,7 +387,7 @@ public abstract class DownloadHelper { * preparation completes. */ @Nullable - public final Object getManifest() { + public Object getManifest() { if (mediaSource == null) { return null; } @@ -202,7 +399,7 @@ public abstract class DownloadHelper { * Returns the number of periods for which media is available. Must not be called until after * preparation completes. */ - public final int getPeriodCount() { + public int getPeriodCount() { if (mediaSource == null) { return 0; } @@ -220,7 +417,7 @@ public abstract class DownloadHelper { * @return The track groups for the period. May be {@link TrackGroupArray#EMPTY} for single stream * content. */ - public final TrackGroupArray getTrackGroups(int periodIndex) { + public TrackGroupArray getTrackGroups(int periodIndex) { assertPreparedWithMedia(); return trackGroupArrays[periodIndex]; } @@ -232,7 +429,7 @@ public abstract class DownloadHelper { * @param periodIndex The period index. * @return The {@link MappedTrackInfo} for the period. */ - public final MappedTrackInfo getMappedTrackInfo(int periodIndex) { + public MappedTrackInfo getMappedTrackInfo(int periodIndex) { assertPreparedWithMedia(); return mappedTrackInfos[periodIndex]; } @@ -245,7 +442,7 @@ public abstract class DownloadHelper { * @param rendererIndex The renderer index. * @return A list of selected {@link TrackSelection track selections}. */ - public final List getTrackSelections(int periodIndex, int rendererIndex) { + public List getTrackSelections(int periodIndex, int rendererIndex) { assertPreparedWithMedia(); return immutableTrackSelectionsByPeriodAndRenderer[periodIndex][rendererIndex]; } @@ -256,7 +453,7 @@ public abstract class DownloadHelper { * * @param periodIndex The period index for which track selections are cleared. */ - public final void clearTrackSelections(int periodIndex) { + public void clearTrackSelections(int periodIndex) { assertPreparedWithMedia(); for (int i = 0; i < rendererCapabilities.length; i++) { trackSelectionsByPeriodAndRenderer[periodIndex][i].clear(); @@ -271,7 +468,7 @@ public abstract class DownloadHelper { * @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new * selection of tracks. */ - public final void replaceTrackSelections( + public void replaceTrackSelections( int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) { clearTrackSelections(periodIndex); addTrackSelection(periodIndex, trackSelectorParameters); @@ -285,7 +482,7 @@ public abstract class DownloadHelper { * @param trackSelectorParameters The {@link DefaultTrackSelector.Parameters} to obtain the new * selection of tracks. */ - public final void addTrackSelection( + public void addTrackSelection( int periodIndex, DefaultTrackSelector.Parameters trackSelectorParameters) { assertPreparedWithMedia(); trackSelector.setParameters(trackSelectorParameters); @@ -299,7 +496,7 @@ public abstract class DownloadHelper { * @param data Application provided data to store in {@link DownloadAction#data}. * @return The built {@link DownloadAction}. */ - public final DownloadAction getDownloadAction(@Nullable byte[] data) { + public DownloadAction getDownloadAction(@Nullable byte[] data) { if (mediaSource == null) { return DownloadAction.createDownloadAction( downloadType, uri, /* keys= */ Collections.emptyList(), cacheKey, data); @@ -324,7 +521,7 @@ public abstract class DownloadHelper { * * @return The built {@link DownloadAction}. */ - public final DownloadAction getRemoveAction() { + public DownloadAction getRemoveAction() { return DownloadAction.createRemoveAction(downloadType, uri, cacheKey); } @@ -452,6 +649,38 @@ public abstract class DownloadHelper { } } + private static Pair<@NullableType Constructor, @NullableType Method> + getMediaSourceFactoryMethods(String className) { + Constructor constructor = null; + Method createMethod = null; + try { + // LINT.IfChange + Class factoryClazz = Class.forName(className); + constructor = factoryClazz.getConstructor(DataSource.Factory.class); + createMethod = factoryClazz.getMethod("createMediaSource", Uri.class); + // LINT.ThenChange(../../../../../../../../proguard-rules.txt) + } catch (Exception e) { + // Expected if the app was built without the respective module. + } + return Pair.create(constructor, createMethod); + } + + private static MediaSource createMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + @Nullable Constructor factoryConstructor, + @Nullable Method createMediaSourceMethod) { + if (factoryConstructor == null || createMediaSourceMethod == null) { + throw new IllegalStateException("Module missing to create media source."); + } + try { + Object factory = factoryConstructor.newInstance(dataSourceFactory); + return (MediaSource) Assertions.checkNotNull(createMediaSourceMethod.invoke(factory, uri)); + } catch (Exception e) { + throw new IllegalStateException("Failed to instantiate media source.", e); + } + } + private static final class MediaPreparer implements MediaSource.SourceInfoRefreshListener, MediaPeriod.Callback, Handler.Callback { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java deleted file mode 100644 index 1850eaebf2..0000000000 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2018 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.offline; - -import android.net.Uri; -import android.support.annotation.Nullable; - -/** A {@link DownloadHelper} for progressive streams. */ -public final class ProgressiveDownloadHelper extends DownloadHelper { - - /** - * Creates download helper for progressive streams. - * - * @param uri The stream {@link Uri}. - */ - public ProgressiveDownloadHelper(Uri uri) { - this(uri, /* cacheKey= */ null); - } - - /** - * Creates download helper for progressive streams. - * - * @param uri The stream {@link Uri}. - * @param cacheKey An optional cache key. - */ - public ProgressiveDownloadHelper(Uri uri, @Nullable String cacheKey) { - super( - DownloadAction.TYPE_PROGRESSIVE, - uri, - cacheKey, - /* mediaSource= */ null, - /* trackSelectorParameters= */ DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, - /* renderersFactory= */ null, - /* drmSessionManager= */ null); - } -} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java index e6cca02140..eb0303178e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java @@ -42,6 +42,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelection; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.util.ConditionVariable; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -93,7 +94,7 @@ public class DownloadHelperTest { private Uri testUri; - private FakeDownloadHelper downloadHelper; + private DownloadHelper downloadHelper; @Before public void setUp() { @@ -106,7 +107,14 @@ public class DownloadHelperTest { (handler, videoListener, audioListener, metadata, text, drm) -> new Renderer[] {textRenderer, audioRenderer, videoRenderer}; - downloadHelper = new FakeDownloadHelper(testUri, renderersFactory); + downloadHelper = + new DownloadHelper( + TEST_DOWNLOAD_TYPE, + testUri, + TEST_CACHE_KEY, + new TestMediaSource(), + DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, + Util.getRendererCapabilities(renderersFactory, /* drmSessionManager= */ null)); } @Test @@ -346,7 +354,7 @@ public class DownloadHelperTest { assertThat(removeAction.isRemoveAction).isTrue(); } - private static void prepareDownloadHelper(FakeDownloadHelper downloadHelper) throws Exception { + private static void prepareDownloadHelper(DownloadHelper downloadHelper) throws Exception { AtomicReference prepareException = new AtomicReference<>(null); ConditionVariable preparedCondition = new ConditionVariable(); downloadHelper.prepare( @@ -426,20 +434,6 @@ public class DownloadHelperTest { assertThat(selectedTracksInGroup).isEqualTo(tracks); } - private static final class FakeDownloadHelper extends DownloadHelper { - - public FakeDownloadHelper(Uri testUri, RenderersFactory renderersFactory) { - super( - TEST_DOWNLOAD_TYPE, - testUri, - TEST_CACHE_KEY, - new TestMediaSource(), - DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, - renderersFactory, - /* drmSessionManager= */ null); - } - } - private static final class TestMediaSource extends FakeMediaSource { public TestMediaSource() { diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java deleted file mode 100644 index b611cf0d5f..0000000000 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.source.dash.offline; - -import android.net.Uri; -import android.support.annotation.Nullable; -import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; -import com.google.android.exoplayer2.offline.DownloadAction; -import com.google.android.exoplayer2.offline.DownloadHelper; -import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.upstream.DataSource; - -/** A {@link DownloadHelper} for DASH streams. */ -public final class DashDownloadHelper extends DownloadHelper { - - /** - * Creates a DASH download helper. - * - *

    The helper uses {@link DownloadHelper#DEFAULT_TRACK_SELECTOR_PARAMETERS} for track selection - * and does not support drm protected content. - * - * @param uri A manifest {@link Uri}. - * @param manifestDataSourceFactory A {@link DataSource.Factory} used to load the manifest. - * @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks - * are selected. - */ - public DashDownloadHelper( - Uri uri, DataSource.Factory manifestDataSourceFactory, RenderersFactory renderersFactory) { - this( - uri, - manifestDataSourceFactory, - DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, - renderersFactory, - /* drmSessionManager= */ null); - } - - /** - * Creates a DASH download helper. - * - * @param uri A manifest {@link Uri}. - * @param manifestDataSourceFactory A {@link DataSource.Factory} used to load the manifest. - * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for - * downloading. - * @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks - * are selected. - * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by - * {@code renderersFactory}. - */ - public DashDownloadHelper( - Uri uri, - DataSource.Factory manifestDataSourceFactory, - DefaultTrackSelector.Parameters trackSelectorParameters, - RenderersFactory renderersFactory, - @Nullable DrmSessionManager drmSessionManager) { - super( - DownloadAction.TYPE_DASH, - uri, - /* cacheKey= */ null, - new DashMediaSource.Factory(manifestDataSourceFactory).createMediaSource(uri), - trackSelectorParameters, - renderersFactory, - drmSessionManager); - } -} diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java new file mode 100644 index 0000000000..eb4af58675 --- /dev/null +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadHelperTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.dash.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.testutil.FakeDataSource; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit test to verify creation of a DASH {@link DownloadHelper}. */ +@RunWith(RobolectricTestRunner.class) +public final class DownloadHelperTest { + + @Test + public void staticDownloadHelperForDash_doesNotThrow() { + DownloadHelper.forDash( + Uri.parse("http://uri"), + new FakeDataSource.Factory(), + (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0]); + DownloadHelper.forDash( + Uri.parse("http://uri"), + new FakeDataSource.Factory(), + (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0], + /* drmSessionManager= */ null, + DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS); + } +} diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java deleted file mode 100644 index ee6bbe333a..0000000000 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.source.hls.offline; - -import android.net.Uri; -import android.support.annotation.Nullable; -import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; -import com.google.android.exoplayer2.offline.DownloadAction; -import com.google.android.exoplayer2.offline.DownloadHelper; -import com.google.android.exoplayer2.source.hls.HlsMediaSource; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.upstream.DataSource; - -/** A {@link DownloadHelper} for HLS streams. */ -public final class HlsDownloadHelper extends DownloadHelper { - - /** - * Creates a HLS download helper. - * - *

    The helper uses {@link DownloadHelper#DEFAULT_TRACK_SELECTOR_PARAMETERS} for track selection - * and does not support drm protected content. - * - * @param uri A manifest {@link Uri}. - * @param manifestDataSourceFactory A {@link DataSource.Factory} used to load the manifest. - * @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks - * are selected. - */ - public HlsDownloadHelper( - Uri uri, DataSource.Factory manifestDataSourceFactory, RenderersFactory renderersFactory) { - this( - uri, - manifestDataSourceFactory, - DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, - renderersFactory, - /* drmSessionManager= */ null); - } - - /** - * Creates a HLS download helper. - * - * @param uri A manifest {@link Uri}. - * @param manifestDataSourceFactory A {@link DataSource.Factory} used to load the manifest. - * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for - * downloading. - * @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks - * are selected. - * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by - * {@code renderersFactory}. - */ - public HlsDownloadHelper( - Uri uri, - DataSource.Factory manifestDataSourceFactory, - DefaultTrackSelector.Parameters trackSelectorParameters, - RenderersFactory renderersFactory, - @Nullable DrmSessionManager drmSessionManager) { - super( - DownloadAction.TYPE_HLS, - uri, - /* cacheKey= */ null, - new HlsMediaSource.Factory(manifestDataSourceFactory) - .setAllowChunklessPreparation(true) - .createMediaSource(uri), - trackSelectorParameters, - renderersFactory, - drmSessionManager); - } -} diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/DownloadHelperTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/DownloadHelperTest.java new file mode 100644 index 0000000000..dca8f9c3e8 --- /dev/null +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/offline/DownloadHelperTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.hls.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.testutil.FakeDataSource; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit test to verify creation of a HLS {@link DownloadHelper}. */ +@RunWith(RobolectricTestRunner.class) +public final class DownloadHelperTest { + + @Test + public void staticDownloadHelperForHls_doesNotThrow() { + DownloadHelper.forHls( + Uri.parse("http://uri"), + new FakeDataSource.Factory(), + (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0]); + DownloadHelper.forHls( + Uri.parse("http://uri"), + new FakeDataSource.Factory(), + (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0], + /* drmSessionManager= */ null, + DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS); + } +} diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java deleted file mode 100644 index f76fb4ee90..0000000000 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.source.smoothstreaming.offline; - -import android.net.Uri; -import android.support.annotation.Nullable; -import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.drm.DrmSessionManager; -import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; -import com.google.android.exoplayer2.offline.DownloadAction; -import com.google.android.exoplayer2.offline.DownloadHelper; -import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.upstream.DataSource; - -/** A {@link DownloadHelper} for SmoothStreaming streams. */ -public final class SsDownloadHelper extends DownloadHelper { - - /** - * Creates a SmoothStreaming download helper. - * - *

    The helper uses {@link DownloadHelper#DEFAULT_TRACK_SELECTOR_PARAMETERS} for track selection - * and does not support drm protected content. - * - * @param uri A manifest {@link Uri}. - * @param manifestDataSourceFactory A {@link DataSource.Factory} used to load the manifest. - * @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks - * are selected. - */ - public SsDownloadHelper( - Uri uri, DataSource.Factory manifestDataSourceFactory, RenderersFactory renderersFactory) { - this( - uri, - manifestDataSourceFactory, - DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS, - renderersFactory, - /* drmSessionManager= */ null); - } - - /** - * Creates a SmoothStreaming download helper. - * - * @param uri A manifest {@link Uri}. - * @param manifestDataSourceFactory A {@link DataSource.Factory} used to load the manifest. - * @param trackSelectorParameters {@link DefaultTrackSelector.Parameters} for selecting tracks for - * downloading. - * @param renderersFactory The {@link RenderersFactory} creating the renderers for which tracks - * are selected. - * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers created by - * {@code renderersFactory}. - */ - public SsDownloadHelper( - Uri uri, - DataSource.Factory manifestDataSourceFactory, - DefaultTrackSelector.Parameters trackSelectorParameters, - RenderersFactory renderersFactory, - @Nullable DrmSessionManager drmSessionManager) { - super( - DownloadAction.TYPE_SS, - uri, - /* cacheKey= */ null, - new SsMediaSource.Factory(manifestDataSourceFactory).createMediaSource(uri), - trackSelectorParameters, - renderersFactory, - drmSessionManager); - } -} diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/DownloadHelperTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/DownloadHelperTest.java new file mode 100644 index 0000000000..071fc46313 --- /dev/null +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/offline/DownloadHelperTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source.smoothstreaming.offline; + +import android.net.Uri; +import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.offline.DownloadHelper; +import com.google.android.exoplayer2.testutil.FakeDataSource; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit test to verify creation of a SmoothStreaming {@link DownloadHelper}. */ +@RunWith(RobolectricTestRunner.class) +public final class DownloadHelperTest { + + @Test + public void staticDownloadHelperForSmoothStreaming_doesNotThrow() { + DownloadHelper.forSmoothStreaming( + Uri.parse("http://uri"), + new FakeDataSource.Factory(), + (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0]); + DownloadHelper.forSmoothStreaming( + Uri.parse("http://uri"), + new FakeDataSource.Factory(), + (handler, videoListener, audioListener, text, metadata, drm) -> new Renderer[0], + /* drmSessionManager= */ null, + DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS); + } +} From 6983f92ffd4b287be53dcac2e9289ba86aa429e2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 29 Jan 2019 12:59:04 +0000 Subject: [PATCH 070/110] Add convenience methods to select multiple audio or text tracks for downloading. These methods take a list of languages to be downloaded and add selections for each of the languages. PiperOrigin-RevId: 231385632 --- .../exoplayer2/offline/DownloadHelper.java | 58 ++++++++++++++++ .../offline/DownloadHelperTest.java | 67 +++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index a3c2278b32..21e1858bdd 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -489,6 +489,64 @@ public final class DownloadHelper { runTrackSelection(periodIndex); } + /** + * Convenience method to add selections of tracks for all specified audio languages. If an audio + * track in one of the specified languages is not available, the default fallback audio track is + * used instead. Must not be called until after preparation completes. + * + * @param languages A list of audio languages for which tracks should be added to the download + * selection, as ISO 639-2/T tags. + */ + public void addAudioLanguagesToSelection(String... languages) { + assertPreparedWithMedia(); + for (int periodIndex = 0; periodIndex < mappedTrackInfos.length; periodIndex++) { + DefaultTrackSelector.ParametersBuilder parametersBuilder = + DEFAULT_TRACK_SELECTOR_PARAMETERS.buildUpon(); + MappedTrackInfo mappedTrackInfo = mappedTrackInfos[periodIndex]; + int rendererCount = mappedTrackInfo.getRendererCount(); + for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { + if (mappedTrackInfo.getRendererType(rendererIndex) != C.TRACK_TYPE_AUDIO) { + parametersBuilder.setRendererDisabled(rendererIndex, /* disabled= */ true); + } + } + for (String language : languages) { + parametersBuilder.setPreferredAudioLanguage(language); + addTrackSelection(periodIndex, parametersBuilder.build()); + } + } + } + + /** + * Convenience method to add selections of tracks for all specified text languages. Must not be + * called until after preparation completes. + * + * @param selectUndeterminedTextLanguage Whether a text track with undetermined language should be + * selected for downloading if no track with one of the specified {@code languages} is + * available. + * @param languages A list of text languages for which tracks should be added to the download + * selection, as ISO 639-2/T tags. + */ + public void addTextLanguagesToSelection( + boolean selectUndeterminedTextLanguage, String... languages) { + assertPreparedWithMedia(); + for (int periodIndex = 0; periodIndex < mappedTrackInfos.length; periodIndex++) { + DefaultTrackSelector.ParametersBuilder parametersBuilder = + DEFAULT_TRACK_SELECTOR_PARAMETERS.buildUpon(); + MappedTrackInfo mappedTrackInfo = mappedTrackInfos[periodIndex]; + int rendererCount = mappedTrackInfo.getRendererCount(); + for (int rendererIndex = 0; rendererIndex < rendererCount; rendererIndex++) { + if (mappedTrackInfo.getRendererType(rendererIndex) != C.TRACK_TYPE_TEXT) { + parametersBuilder.setRendererDisabled(rendererIndex, /* disabled= */ true); + } + } + parametersBuilder.setSelectUndeterminedTextLanguage(selectUndeterminedTextLanguage); + for (String language : languages) { + parametersBuilder.setPreferredTextLanguage(language); + addTrackSelection(periodIndex, parametersBuilder.build()); + } + } + } + /** * Builds a {@link DownloadAction} for downloading the selected tracks. Must not be called until * after preparation completes. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java index eb0303178e..6f41e10046 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadHelperTest.java @@ -312,6 +312,73 @@ public class DownloadHelperTest { assertSingleTrackSelectionEquals(selectedVideo1, TRACK_GROUP_VIDEO_SINGLE, 0); } + @Test + public void getTrackSelections_afterAddAudioLanguagesToSelection_returnsCombinedSelections() + throws Exception { + prepareDownloadHelper(downloadHelper); + downloadHelper.clearTrackSelections(/* periodIndex= */ 0); + downloadHelper.clearTrackSelections(/* periodIndex= */ 1); + + // Add a non-default language, and a non-existing language (which will select the default). + downloadHelper.addAudioLanguagesToSelection("ZH", "Klingonese"); + List selectedText0 = + downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0); + List selectedAudio0 = + downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1); + List selectedVideo0 = + downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2); + List selectedText1 = + downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0); + List selectedAudio1 = + downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1); + List selectedVideo1 = + downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2); + + assertThat(selectedVideo0).isEmpty(); + assertThat(selectedText0).isEmpty(); + assertThat(selectedAudio0).hasSize(2); + assertTrackSelectionEquals(selectedAudio0.get(0), TRACK_GROUP_AUDIO_ZH, 0); + assertTrackSelectionEquals(selectedAudio0.get(1), TRACK_GROUP_AUDIO_US, 0); + + assertThat(selectedVideo1).isEmpty(); + assertThat(selectedText1).isEmpty(); + assertSingleTrackSelectionEquals(selectedAudio1, TRACK_GROUP_AUDIO_US, 0); + } + + @Test + public void getTrackSelections_afterAddTextLanguagesToSelection_returnsCombinedSelections() + throws Exception { + prepareDownloadHelper(downloadHelper); + downloadHelper.clearTrackSelections(/* periodIndex= */ 0); + downloadHelper.clearTrackSelections(/* periodIndex= */ 1); + + // Add a non-default language, and a non-existing language (which will select the default). + downloadHelper.addTextLanguagesToSelection( + /* selectUndeterminedTextLanguage= */ true, "ZH", "Klingonese"); + List selectedText0 = + downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 0); + List selectedAudio0 = + downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 1); + List selectedVideo0 = + downloadHelper.getTrackSelections(/* periodIndex= */ 0, /* rendererIndex= */ 2); + List selectedText1 = + downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 0); + List selectedAudio1 = + downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 1); + List selectedVideo1 = + downloadHelper.getTrackSelections(/* periodIndex= */ 1, /* rendererIndex= */ 2); + + assertThat(selectedVideo0).isEmpty(); + assertThat(selectedAudio0).isEmpty(); + assertThat(selectedText0).hasSize(2); + assertTrackSelectionEquals(selectedText0.get(0), TRACK_GROUP_TEXT_ZH, 0); + assertTrackSelectionEquals(selectedText0.get(1), TRACK_GROUP_TEXT_US, 0); + + assertThat(selectedVideo1).isEmpty(); + assertThat(selectedAudio1).isEmpty(); + assertThat(selectedText1).isEmpty(); + } + @Test public void getDownloadAction_createsDownloadAction_withAllSelectedTracks() throws Exception { prepareDownloadHelper(downloadHelper); From ef8335fc506f846527807a51e73ef74dc9428158 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 29 Jan 2019 12:59:29 +0000 Subject: [PATCH 071/110] Ensure normalized language code is used everywhere and update documentation. Issue:#2867 PiperOrigin-RevId: 231385677 --- .../src/main/java/com/google/android/exoplayer2/C.java | 4 +--- .../main/java/com/google/android/exoplayer2/Format.java | 4 ++-- .../android/exoplayer2/extractor/ts/TsPayloadReader.java | 2 +- .../google/android/exoplayer2/offline/DownloadHelper.java | 4 ++-- .../exoplayer2/trackselection/DefaultTrackSelector.java | 7 +++++-- .../main/java/com/google/android/exoplayer2/util/Util.java | 5 +++-- .../source/hls/playlist/HlsMasterPlaylistParserTest.java | 2 +- 7 files changed, 15 insertions(+), 13 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/C.java b/library/core/src/main/java/com/google/android/exoplayer2/C.java index 8810b51000..d163fabd60 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/C.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/C.java @@ -536,9 +536,7 @@ public final class C { */ public static final int SELECTION_FLAG_AUTOSELECT = 1 << 2; // 4 - /** - * Represents an undetermined language as an ISO 639 alpha-3 language code. - */ + /** Represents an undetermined language as an ISO 639-2 language code. */ public static final String LANGUAGE_UNDETERMINED = "und"; /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Format.java b/library/core/src/main/java/com/google/android/exoplayer2/Format.java index 6c54c07cde..c3028e153c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Format.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Format.java @@ -159,7 +159,7 @@ public final class Format implements Parcelable { @C.SelectionFlags public final int selectionFlags; - /** The language, or null if unknown or not applicable. */ + /** The language as ISO 639-2/T three-letter code, or null if unknown or not applicable. */ public final @Nullable String language; /** @@ -932,7 +932,7 @@ public final class Format implements Parcelable { this.encoderDelay = encoderDelay == Format.NO_VALUE ? 0 : encoderDelay; this.encoderPadding = encoderPadding == Format.NO_VALUE ? 0 : encoderPadding; this.selectionFlags = selectionFlags; - this.language = language; + this.language = Util.normalizeLanguageCode(language); this.accessibilityChannel = accessibilityChannel; this.subsampleOffsetUs = subsampleOffsetUs; this.initializationData = diff --git a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java index a034b05696..9289ba4d2e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/extractor/ts/TsPayloadReader.java @@ -100,7 +100,7 @@ public interface TsPayloadReader { public final byte[] initializationData; /** - * @param language The ISO 639-2 three character language. + * @param language The ISO 639-2 three-letter language code. * @param type The subtitling type. * @param initializationData The composition and ancillary page ids. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index 21e1858bdd..25c4eca5eb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -495,7 +495,7 @@ public final class DownloadHelper { * used instead. Must not be called until after preparation completes. * * @param languages A list of audio languages for which tracks should be added to the download - * selection, as ISO 639-2/T tags. + * selection, as ISO 639-1 two-letter or ISO 639-2 three-letter codes. */ public void addAudioLanguagesToSelection(String... languages) { assertPreparedWithMedia(); @@ -524,7 +524,7 @@ public final class DownloadHelper { * selected for downloading if no track with one of the specified {@code languages} is * available. * @param languages A list of text languages for which tracks should be added to the download - * selection, as ISO 639-2/T tags. + * selection, as ISO 639-1 two-letter or ISO 639-2 three-letter codes. */ public void addTextLanguagesToSelection( boolean selectUndeterminedTextLanguage, String... languages) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 06b11b3d67..28242167ad 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -365,6 +365,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** * See {@link Parameters#preferredAudioLanguage}. * + * @param preferredAudioLanguage Preferred audio language as an ISO 639-1 two-letter or ISO + * 639-2 three-letter code. * @return This builder. */ public ParametersBuilder setPreferredAudioLanguage(String preferredAudioLanguage) { @@ -430,6 +432,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { /** * See {@link Parameters#preferredTextLanguage}. * + * @param preferredTextLanguage Preferred text language as an ISO 639-1 two-letter or ISO 639-2 + * three-letter code. * @return This builder. */ public ParametersBuilder setPreferredTextLanguage(String preferredTextLanguage) { @@ -2313,8 +2317,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * null. */ protected static boolean formatHasLanguage(Format format, @Nullable String language) { - return language != null - && TextUtils.equals(language, Util.normalizeLanguageCode(format.language)); + return language != null && TextUtils.equals(language, format.language); } private static List getViewportFilteredTrackIndices(TrackGroup group, int viewportWidth, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 34237dddf6..b9d78f8af2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -418,9 +418,10 @@ public final class Util { } /** - * Returns a normalized RFC 639-2/T code for {@code language}. + * Returns a normalized ISO 639-2/T code for {@code language}. * - * @param language A case-insensitive ISO 639 alpha-2 or alpha-3 language code. + * @param language A case-insensitive ISO 639-1 two-letter or ISO 639-2 three-letter language + * code. * @return The all-lowercase normalized code, or null if the input was null, or {@code * language.toLowerCase()} if the language could not be normalized. */ diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java index 8b69ba0db2..80a9bd3eab 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/playlist/HlsMasterPlaylistParserTest.java @@ -225,7 +225,7 @@ public class HlsMasterPlaylistParserTest { Format closedCaptionFormat = playlist.muxedCaptionFormats.get(0); assertThat(closedCaptionFormat.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_CEA708); assertThat(closedCaptionFormat.accessibilityChannel).isEqualTo(4); - assertThat(closedCaptionFormat.language).isEqualTo("es"); + assertThat(closedCaptionFormat.language).isEqualTo("spa"); } @Test From 45433869e5069019fc48c69589c14331d889bd60 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Jan 2019 13:14:39 +0000 Subject: [PATCH 072/110] Rename ExtractorMediaSource to ProgressiveMediaSource It better describes what the class does. More importantly, we've had inconsistent class names since we added offline support, for which we added ProgressiveDownloader ("ExtractorDownloader" doesn't make any sense). We could really do with aligning the names for clarity. (Sorry) PiperOrigin-RevId: 231387268 --- RELEASENOTES.md | 1 + .../DefaultReceiverPlayerManager.java | 4 +- .../exoplayer2/imademo/PlayerManager.java | 4 +- .../exoplayer2/demo/PlayerActivity.java | 4 +- .../exoplayer2/ext/flac/FlacPlaybackTest.java | 4 +- .../exoplayer2/ext/opus/OpusPlaybackTest.java | 4 +- .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 4 +- .../google/android/exoplayer2/ExoPlayer.java | 4 +- .../source/ExtractorMediaSource.java | 123 ++------ ...eriod.java => ProgressiveMediaPeriod.java} | 28 +- .../source/ProgressiveMediaSource.java | 281 ++++++++++++++++++ .../source/SingleSampleMediaSource.java | 4 +- .../exoplayer2/source/ads/AdsMediaSource.java | 10 +- 13 files changed, 349 insertions(+), 126 deletions(-) rename library/core/src/main/java/com/google/android/exoplayer2/source/{ExtractorMediaPeriod.java => ProgressiveMediaPeriod.java} (98%) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6c72dae654..65d4e9696e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,7 @@ ### dev-v2 (not yet released) ### +* `ExtractorMediaSource` renamed to `ProgressiveMediaSource`. * HLS: * Parse CHANNELS attribute from EXT-X-MEDIA. * Support for playing spherical videos on Daydream. diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java index 563efea11f..1db68ca08d 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/DefaultReceiverPlayerManager.java @@ -35,8 +35,8 @@ import com.google.android.exoplayer2.ext.cast.CastPlayer; import com.google.android.exoplayer2.ext.cast.MediaItem; import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; @@ -373,7 +373,7 @@ import java.util.ArrayList; case DemoUtil.MIME_TYPE_HLS: return new HlsMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); case DemoUtil.MIME_TYPE_VIDEO_MP4: - return new ExtractorMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); + return new ProgressiveMediaSource.Factory(DATA_SOURCE_FACTORY).createMediaSource(uri); default: { throw new IllegalStateException("Unsupported type: " + item.mimeType); } diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java index d67c4549d8..740c1a0af6 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java @@ -23,8 +23,8 @@ import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; -import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; @@ -125,7 +125,7 @@ import com.google.android.exoplayer2.util.Util; case C.TYPE_HLS: return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri); case C.TYPE_OTHER: - return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri); + return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri); default: throw new IllegalStateException("Unsupported type: " + type); } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 582638b460..2d4efd7f3d 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -51,8 +51,8 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryExcep import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; @@ -483,7 +483,7 @@ public class PlayerActivity extends Activity .setStreamKeys(offlineStreamKeys) .createMediaSource(uri); case C.TYPE_OTHER: - return new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri); + return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri); default: { throw new IllegalStateException("Unsupported type: " + type); } diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java index 2efdde4e58..5448919626 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java @@ -28,8 +28,8 @@ import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; -import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import org.junit.Before; @@ -86,7 +86,7 @@ public class FlacPlaybackTest { player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector); player.addListener(this); MediaSource mediaSource = - new ExtractorMediaSource.Factory( + new ProgressiveMediaSource.Factory( new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest")) .setExtractorsFactory(MatroskaExtractor.FACTORY) .createMediaSource(uri); diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java index 5ad864c597..6a254c8230 100644 --- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java +++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java @@ -28,8 +28,8 @@ import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; -import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import org.junit.Before; @@ -86,7 +86,7 @@ public class OpusPlaybackTest { player = ExoPlayerFactory.newInstance(context, new Renderer[] {audioRenderer}, trackSelector); player.addListener(this); MediaSource mediaSource = - new ExtractorMediaSource.Factory( + new ProgressiveMediaSource.Factory( new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest")) .setExtractorsFactory(MatroskaExtractor.FACTORY) .createMediaSource(uri); diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java index 1de461e374..a36b578588 100644 --- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java @@ -29,8 +29,8 @@ import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; -import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.util.Log; @@ -119,7 +119,7 @@ public class VpxPlaybackTest { player = ExoPlayerFactory.newInstance(context, new Renderer[] {videoRenderer}, trackSelector); player.addListener(this); MediaSource mediaSource = - new ExtractorMediaSource.Factory( + new ProgressiveMediaSource.Factory( new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test")) .setExtractorsFactory(MatroskaExtractor.FACTORY) .createMediaSource(uri); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 5ba2394c3f..db168d9c29 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -21,10 +21,10 @@ import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer; import com.google.android.exoplayer2.source.ClippingMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource; -import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.LoopingMediaSource; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MergingMediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.SingleSampleMediaSource; import com.google.android.exoplayer2.text.TextRenderer; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; @@ -48,7 +48,7 @@ import com.google.android.exoplayer2.video.MediaCodecVideoRenderer; *

  • A {@link MediaSource} that defines the media to be played, loads the media, and from * which the loaded media can be read. A MediaSource is injected via {@link * #prepare(MediaSource)} at the start of playback. The library modules provide default - * implementations for regular media files ({@link ExtractorMediaSource}), DASH + * implementations for progressive media files ({@link ProgressiveMediaSource}), DASH * (DashMediaSource), SmoothStreaming (SsMediaSource) and HLS (HlsMediaSource), an * implementation for loading single media samples ({@link SingleSampleMediaSource}) that's * most often used for side-loaded subtitle files, and implementations for building more diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java index 5403f9f33b..5e0960aaf0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaSource.java @@ -20,6 +20,7 @@ import android.os.Handler; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.Extractor; import com.google.android.exoplayer2.extractor.ExtractorsFactory; @@ -32,25 +33,13 @@ import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; -/** - * Provides one period that loads data from a {@link Uri} and extracted using an {@link Extractor}. - * - *

    If the possible input stream container formats are known, pass a factory that instantiates - * extractors for them to the constructor. Otherwise, pass a {@link DefaultExtractorsFactory} to use - * the default extractors. When reading a new stream, the first {@link Extractor} in the array of - * extractors created by the factory that returns {@code true} from {@link Extractor#sniff} will be - * used to extract samples from the input stream. - * - *

    Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking. - */ +/** @deprecated Use {@link ProgressiveMediaSource} instead. */ +@Deprecated +@SuppressWarnings("deprecation") public final class ExtractorMediaSource extends BaseMediaSource - implements ExtractorMediaPeriod.Listener { + implements MediaSource.SourceInfoRefreshListener { - /** - * Listener of {@link ExtractorMediaSource} events. - * - * @deprecated Use {@link MediaSourceEventListener}. - */ + /** @deprecated Use {@link MediaSourceEventListener} instead. */ @Deprecated public interface EventListener { @@ -70,7 +59,8 @@ public final class ExtractorMediaSource extends BaseMediaSource } - /** Factory for {@link ExtractorMediaSource}s. */ + /** Use {@link ProgressiveMediaSource.Factory} instead. */ + @Deprecated public static final class Factory implements AdsMediaSource.MediaSourceFactory { private final DataSource.Factory dataSourceFactory; @@ -232,23 +222,11 @@ public final class ExtractorMediaSource extends BaseMediaSource } } - /** - * The default number of bytes that should be loaded between each each invocation of {@link - * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. - */ - public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024; + @Deprecated + public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES = + ProgressiveMediaSource.DEFAULT_LOADING_CHECK_INTERVAL_BYTES; - private final Uri uri; - private final DataSource.Factory dataSourceFactory; - private final ExtractorsFactory extractorsFactory; - private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy; - private final String customCacheKey; - private final int continueLoadingCheckIntervalBytes; - private final @Nullable Object tag; - - private long timelineDurationUs; - private boolean timelineIsSeekable; - private @Nullable TransferListener transferListener; + private final ProgressiveMediaSource progressiveMediaSource; /** * @param uri The {@link Uri} of the media stream. @@ -261,7 +239,6 @@ public final class ExtractorMediaSource extends BaseMediaSource * @deprecated Use {@link Factory} instead. */ @Deprecated - @SuppressWarnings("deprecation") public ExtractorMediaSource( Uri uri, DataSource.Factory dataSourceFactory, @@ -284,7 +261,6 @@ public final class ExtractorMediaSource extends BaseMediaSource * @deprecated Use {@link Factory} instead. */ @Deprecated - @SuppressWarnings("deprecation") public ExtractorMediaSource( Uri uri, DataSource.Factory dataSourceFactory, @@ -317,7 +293,6 @@ public final class ExtractorMediaSource extends BaseMediaSource * @deprecated Use {@link Factory} instead. */ @Deprecated - @SuppressWarnings("deprecation") public ExtractorMediaSource( Uri uri, DataSource.Factory dataSourceFactory, @@ -347,93 +322,57 @@ public final class ExtractorMediaSource extends BaseMediaSource @Nullable String customCacheKey, int continueLoadingCheckIntervalBytes, @Nullable Object tag) { - this.uri = uri; - this.dataSourceFactory = dataSourceFactory; - this.extractorsFactory = extractorsFactory; - this.loadableLoadErrorHandlingPolicy = loadableLoadErrorHandlingPolicy; - this.customCacheKey = customCacheKey; - this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; - this.timelineDurationUs = C.TIME_UNSET; - this.tag = tag; + progressiveMediaSource = + new ProgressiveMediaSource( + uri, + dataSourceFactory, + extractorsFactory, + loadableLoadErrorHandlingPolicy, + customCacheKey, + continueLoadingCheckIntervalBytes, + tag); } @Override @Nullable public Object getTag() { - return tag; + return progressiveMediaSource.getTag(); } @Override public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { - transferListener = mediaTransferListener; - notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable); + progressiveMediaSource.prepareSource(/* listener= */ this, mediaTransferListener); } @Override public void maybeThrowSourceInfoRefreshError() throws IOException { - // Do nothing. + progressiveMediaSource.maybeThrowSourceInfoRefreshError(); } @Override public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { - DataSource dataSource = dataSourceFactory.createDataSource(); - if (transferListener != null) { - dataSource.addTransferListener(transferListener); - } - return new ExtractorMediaPeriod( - uri, - dataSource, - extractorsFactory.createExtractors(), - loadableLoadErrorHandlingPolicy, - createEventDispatcher(id), - this, - allocator, - customCacheKey, - continueLoadingCheckIntervalBytes); + return progressiveMediaSource.createPeriod(id, allocator, startPositionUs); } @Override public void releasePeriod(MediaPeriod mediaPeriod) { - ((ExtractorMediaPeriod) mediaPeriod).release(); + progressiveMediaSource.releasePeriod(mediaPeriod); } @Override public void releaseSourceInternal() { - // Do nothing. + progressiveMediaSource.releaseSource(/* listener= */ this); } - // ExtractorMediaPeriod.Listener implementation. - @Override - public void onSourceInfoRefreshed(long durationUs, boolean isSeekable) { - // If we already have the duration from a previous source info refresh, use it. - durationUs = durationUs == C.TIME_UNSET ? timelineDurationUs : durationUs; - if (timelineDurationUs == durationUs && timelineIsSeekable == isSeekable) { - // Suppress no-op source info changes. - return; - } - notifySourceInfoRefreshed(durationUs, isSeekable); + public void onSourceInfoRefreshed( + MediaSource source, Timeline timeline, @Nullable Object manifest) { + refreshSourceInfo(timeline, manifest); } - // Internal methods. - - private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) { - timelineDurationUs = durationUs; - timelineIsSeekable = isSeekable; - // TODO: Make timeline dynamic until its duration is known. This is non-trivial. See b/69703223. - refreshSourceInfo( - new SinglePeriodTimeline( - timelineDurationUs, timelineIsSeekable, /* isDynamic= */ false, tag), - /* manifest= */ null); - } - - /** - * Wraps a deprecated {@link EventListener}, invoking its callback from the equivalent callback in - * {@link MediaSourceEventListener}. - */ @Deprecated - @SuppressWarnings("deprecation") private static final class EventListenerWrapper extends DefaultMediaSourceEventListener { + private final EventListener eventListener; public EventListenerWrapper(EventListener eventListener) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java similarity index 98% rename from library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java rename to library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java index e842d4f253..41f6e986c6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ExtractorMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java @@ -54,12 +54,13 @@ import java.io.IOException; import java.util.Arrays; import org.checkerframework.checker.nullness.compatqual.NullableType; -/** - * A {@link MediaPeriod} that extracts data using an {@link Extractor}. - */ -/* package */ final class ExtractorMediaPeriod implements MediaPeriod, ExtractorOutput, - Loader.Callback, Loader.ReleaseCallback, - UpstreamFormatChangedListener { +/** A {@link MediaPeriod} that extracts data using an {@link Extractor}. */ +/* package */ final class ProgressiveMediaPeriod + implements MediaPeriod, + ExtractorOutput, + Loader.Callback, + Loader.ReleaseCallback, + UpstreamFormatChangedListener { /** * Listener for information about the period. @@ -145,7 +146,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; "nullness:argument.type.incompatible", "nullness:methodref.receiver.bound.invalid" }) - public ExtractorMediaPeriod( + public ProgressiveMediaPeriod( Uri uri, DataSource dataSource, Extractor[] extractors, @@ -163,14 +164,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; this.allocator = allocator; this.customCacheKey = customCacheKey; this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; - loader = new Loader("Loader:ExtractorMediaPeriod"); + loader = new Loader("Loader:ProgressiveMediaPeriod"); extractorHolder = new ExtractorHolder(extractors); loadCondition = new ConditionVariable(); maybeFinishPrepareRunnable = this::maybeFinishPrepare; onContinueLoadingRequestedRunnable = () -> { if (!released) { - Assertions.checkNotNull(callback).onContinueLoadingRequested(ExtractorMediaPeriod.this); + Assertions.checkNotNull(callback) + .onContinueLoadingRequested(ProgressiveMediaPeriod.this); } }; handler = new Handler(); @@ -852,23 +854,23 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Override public boolean isReady() { - return ExtractorMediaPeriod.this.isReady(track); + return ProgressiveMediaPeriod.this.isReady(track); } @Override public void maybeThrowError() throws IOException { - ExtractorMediaPeriod.this.maybeThrowError(); + ProgressiveMediaPeriod.this.maybeThrowError(); } @Override public int readData(FormatHolder formatHolder, DecoderInputBuffer buffer, boolean formatRequired) { - return ExtractorMediaPeriod.this.readData(track, formatHolder, buffer, formatRequired); + return ProgressiveMediaPeriod.this.readData(track, formatHolder, buffer, formatRequired); } @Override public int skipData(long positionUs) { - return ExtractorMediaPeriod.this.skipData(track, positionUs); + return ProgressiveMediaPeriod.this.skipData(track, positionUs); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java new file mode 100644 index 0000000000..a44e99b9c4 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.source; + +import android.net.Uri; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.ExtractorsFactory; +import com.google.android.exoplayer2.source.ads.AdsMediaSource; +import com.google.android.exoplayer2.upstream.Allocator; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; +import com.google.android.exoplayer2.upstream.TransferListener; +import com.google.android.exoplayer2.util.Assertions; +import java.io.IOException; + +/** + * Provides one period that loads data from a {@link Uri} and extracted using an {@link Extractor}. + * + *

    If the possible input stream container formats are known, pass a factory that instantiates + * extractors for them to the constructor. Otherwise, pass a {@link DefaultExtractorsFactory} to use + * the default extractors. When reading a new stream, the first {@link Extractor} in the array of + * extractors created by the factory that returns {@code true} from {@link Extractor#sniff} will be + * used to extract samples from the input stream. + * + *

    Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking. + */ +public final class ProgressiveMediaSource extends BaseMediaSource + implements ProgressiveMediaPeriod.Listener { + + /** Factory for {@link ProgressiveMediaSource}s. */ + public static final class Factory implements AdsMediaSource.MediaSourceFactory { + + private final DataSource.Factory dataSourceFactory; + + @Nullable private ExtractorsFactory extractorsFactory; + @Nullable private String customCacheKey; + @Nullable private Object tag; + private LoadErrorHandlingPolicy loadErrorHandlingPolicy; + private int continueLoadingCheckIntervalBytes; + private boolean isCreateCalled; + + /** + * Creates a new factory for {@link ProgressiveMediaSource}s. + * + * @param dataSourceFactory A factory for {@link DataSource}s to read the media. + */ + public Factory(DataSource.Factory dataSourceFactory) { + this.dataSourceFactory = dataSourceFactory; + loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy(); + continueLoadingCheckIntervalBytes = DEFAULT_LOADING_CHECK_INTERVAL_BYTES; + } + + /** + * Sets the factory for {@link Extractor}s to process the media stream. The default value is an + * instance of {@link DefaultExtractorsFactory}. + * + * @param extractorsFactory A factory for {@link Extractor}s to process the media stream. If the + * possible formats are known, pass a factory that instantiates extractors for those + * formats. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setExtractorsFactory(ExtractorsFactory extractorsFactory) { + Assertions.checkState(!isCreateCalled); + this.extractorsFactory = extractorsFactory; + return this; + } + + /** + * Sets the custom key that uniquely identifies the original stream. Used for cache indexing. + * The default value is {@code null}. + * + * @param customCacheKey A custom key that uniquely identifies the original stream. Used for + * cache indexing. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setCustomCacheKey(String customCacheKey) { + Assertions.checkState(!isCreateCalled); + this.customCacheKey = customCacheKey; + return this; + } + + /** + * Sets a tag for the media source which will be published in the {@link + * com.google.android.exoplayer2.Timeline} of the source as {@link + * com.google.android.exoplayer2.Timeline.Window#tag}. + * + * @param tag A tag for the media source. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setTag(Object tag) { + Assertions.checkState(!isCreateCalled); + this.tag = tag; + return this; + } + + /** + * Sets the {@link LoadErrorHandlingPolicy}. The default value is created by calling {@link + * DefaultLoadErrorHandlingPolicy#DefaultLoadErrorHandlingPolicy()}. + * + * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + Assertions.checkState(!isCreateCalled); + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; + return this; + } + + /** + * Sets the number of bytes that should be loaded between each invocation of {@link + * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. The default value is + * {@link #DEFAULT_LOADING_CHECK_INTERVAL_BYTES}. + * + * @param continueLoadingCheckIntervalBytes The number of bytes that should be loaded between + * each invocation of {@link + * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. + * @return This factory, for convenience. + * @throws IllegalStateException If one of the {@code create} methods has already been called. + */ + public Factory setContinueLoadingCheckIntervalBytes(int continueLoadingCheckIntervalBytes) { + Assertions.checkState(!isCreateCalled); + this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; + return this; + } + + /** + * Returns a new {@link ProgressiveMediaSource} using the current parameters. + * + * @param uri The {@link Uri}. + * @return The new {@link ProgressiveMediaSource}. + */ + @Override + public ProgressiveMediaSource createMediaSource(Uri uri) { + isCreateCalled = true; + if (extractorsFactory == null) { + extractorsFactory = new DefaultExtractorsFactory(); + } + return new ProgressiveMediaSource( + uri, + dataSourceFactory, + extractorsFactory, + loadErrorHandlingPolicy, + customCacheKey, + continueLoadingCheckIntervalBytes, + tag); + } + + @Override + public int[] getSupportedTypes() { + return new int[] {C.TYPE_OTHER}; + } + } + + /** + * The default number of bytes that should be loaded between each each invocation of {@link + * MediaPeriod.Callback#onContinueLoadingRequested(SequenceableLoader)}. + */ + public static final int DEFAULT_LOADING_CHECK_INTERVAL_BYTES = 1024 * 1024; + + private final Uri uri; + private final DataSource.Factory dataSourceFactory; + private final ExtractorsFactory extractorsFactory; + private final LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy; + @Nullable private final String customCacheKey; + private final int continueLoadingCheckIntervalBytes; + @Nullable private final Object tag; + + private long timelineDurationUs; + private boolean timelineIsSeekable; + @Nullable private TransferListener transferListener; + + // TODO: Make private when ExtractorMediaSource is deleted. + /* package */ ProgressiveMediaSource( + Uri uri, + DataSource.Factory dataSourceFactory, + ExtractorsFactory extractorsFactory, + LoadErrorHandlingPolicy loadableLoadErrorHandlingPolicy, + @Nullable String customCacheKey, + int continueLoadingCheckIntervalBytes, + @Nullable Object tag) { + this.uri = uri; + this.dataSourceFactory = dataSourceFactory; + this.extractorsFactory = extractorsFactory; + this.loadableLoadErrorHandlingPolicy = loadableLoadErrorHandlingPolicy; + this.customCacheKey = customCacheKey; + this.continueLoadingCheckIntervalBytes = continueLoadingCheckIntervalBytes; + this.timelineDurationUs = C.TIME_UNSET; + this.tag = tag; + } + + @Override + @Nullable + public Object getTag() { + return tag; + } + + @Override + public void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { + transferListener = mediaTransferListener; + notifySourceInfoRefreshed(timelineDurationUs, timelineIsSeekable); + } + + @Override + public void maybeThrowSourceInfoRefreshError() throws IOException { + // Do nothing. + } + + @Override + public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) { + DataSource dataSource = dataSourceFactory.createDataSource(); + if (transferListener != null) { + dataSource.addTransferListener(transferListener); + } + return new ProgressiveMediaPeriod( + uri, + dataSource, + extractorsFactory.createExtractors(), + loadableLoadErrorHandlingPolicy, + createEventDispatcher(id), + this, + allocator, + customCacheKey, + continueLoadingCheckIntervalBytes); + } + + @Override + public void releasePeriod(MediaPeriod mediaPeriod) { + ((ProgressiveMediaPeriod) mediaPeriod).release(); + } + + @Override + public void releaseSourceInternal() { + // Do nothing. + } + + // ProgressiveMediaPeriod.Listener implementation. + + @Override + public void onSourceInfoRefreshed(long durationUs, boolean isSeekable) { + // If we already have the duration from a previous source info refresh, use it. + durationUs = durationUs == C.TIME_UNSET ? timelineDurationUs : durationUs; + if (timelineDurationUs == durationUs && timelineIsSeekable == isSeekable) { + // Suppress no-op source info changes. + return; + } + notifySourceInfoRefreshed(durationUs, isSeekable); + } + + // Internal methods. + + private void notifySourceInfoRefreshed(long durationUs, boolean isSeekable) { + timelineDurationUs = durationUs; + timelineIsSeekable = isSeekable; + // TODO: Make timeline dynamic until its duration is known. This is non-trivial. See b/69703223. + refreshSourceInfo( + new SinglePeriodTimeline( + timelineDurationUs, timelineIsSeekable, /* isDynamic= */ false, tag), + /* manifest= */ null); + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index d13fa06434..7611d76260 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -138,12 +138,12 @@ public final class SingleSampleMediaSource extends BaseMediaSource { } /** - * Returns a new {@link ExtractorMediaSource} using the current parameters. + * Returns a new {@link SingleSampleMediaSource} using the current parameters. * * @param uri The {@link Uri}. * @param format The {@link Format} of the media stream. * @param durationUs The duration of the media stream in microseconds. - * @return The new {@link ExtractorMediaSource}. + * @return The new {@link SingleSampleMediaSource}. */ public SingleSampleMediaSource createMediaSource(Uri uri, Format format, long durationUs) { isCreateCalled = true; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 4754466235..e80c797eb9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -25,13 +25,13 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.source.CompositeMediaSource; import com.google.android.exoplayer2.source.DeferredMediaPeriod; -import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import com.google.android.exoplayer2.source.MediaSourceEventListener; import com.google.android.exoplayer2.source.MediaSourceEventListener.LoadEventInfo; import com.google.android.exoplayer2.source.MediaSourceEventListener.MediaLoadData; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; @@ -203,7 +203,7 @@ public final class AdsMediaSource extends CompositeMediaSource { /** * Constructs a new source that inserts ads linearly with the content specified by {@code - * contentMediaSource}. Ad media is loaded using {@link ExtractorMediaSource}. + * contentMediaSource}. Ad media is loaded using {@link ProgressiveMediaSource}. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. @@ -217,7 +217,7 @@ public final class AdsMediaSource extends CompositeMediaSource { ViewGroup adUiViewGroup) { this( contentMediaSource, - new ExtractorMediaSource.Factory(dataSourceFactory), + new ProgressiveMediaSource.Factory(dataSourceFactory), adsLoader, adUiViewGroup, /* eventHandler= */ null, @@ -249,7 +249,7 @@ public final class AdsMediaSource extends CompositeMediaSource { /** * Constructs a new source that inserts ads linearly with the content specified by {@code - * contentMediaSource}. Ad media is loaded using {@link ExtractorMediaSource}. + * contentMediaSource}. Ad media is loaded using {@link ProgressiveMediaSource}. * * @param contentMediaSource The {@link MediaSource} providing the content to play. * @param dataSourceFactory Factory for data sources used to load ad media. @@ -273,7 +273,7 @@ public final class AdsMediaSource extends CompositeMediaSource { @Nullable EventListener eventListener) { this( contentMediaSource, - new ExtractorMediaSource.Factory(dataSourceFactory), + new ProgressiveMediaSource.Factory(dataSourceFactory), adsLoader, adUiViewGroup, eventHandler, From e0711c64b84cd86a75f00ff659af19ea52d41f7f Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Jan 2019 13:18:25 +0000 Subject: [PATCH 073/110] DatabaseFileProvidingContext: Support older API levels On older API levels it's also necessary to implement openOrCreateDatabase, to be called by SQLiteOpenHelper. PiperOrigin-RevId: 231387559 --- .../database/ExoDatabaseProvider.java | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java b/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java index 49c16ebbbf..9306b22353 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.database; import android.content.Context; import android.content.ContextWrapper; import android.database.Cursor; +import android.database.DatabaseErrorHandler; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; @@ -107,10 +108,7 @@ public final class ExoDatabaseProvider extends SQLiteOpenHelper implements Datab // TODO: This is fragile. Stop using it if/when SQLiteOpenHelper can be instantiated without a // context [Internal ref: b/123351819], or by injecting a Context into all components that need // to instantiate an ExoDatabaseProvider. - /** - * A {@link Context} that only implements {@link #getDatabasePath(String)}. This is the only - * method used by {@link SQLiteOpenHelper}. - */ + /** A {@link Context} that implements methods called by {@link SQLiteOpenHelper}. */ private static class DatabaseFileProvidingContext extends ContextWrapper { private final File file; @@ -125,5 +123,28 @@ public final class ExoDatabaseProvider extends SQLiteOpenHelper implements Datab public File getDatabasePath(String name) { return file; } + + @Override + public SQLiteDatabase openOrCreateDatabase( + String name, int mode, SQLiteDatabase.CursorFactory factory) { + return openOrCreateDatabase(name, mode, factory, /* errorHandler= */ null); + } + + @Override + public SQLiteDatabase openOrCreateDatabase( + String name, + int mode, + SQLiteDatabase.CursorFactory factory, + DatabaseErrorHandler errorHandler) { + File databasePath = getDatabasePath(name); + int flags = SQLiteDatabase.CREATE_IF_NECESSARY; + if ((mode & MODE_ENABLE_WRITE_AHEAD_LOGGING) != 0) { + flags |= SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING; + } + if ((mode & MODE_NO_LOCALIZED_COLLATORS) != 0) { + flags |= SQLiteDatabase.NO_LOCALIZED_COLLATORS; + } + return SQLiteDatabase.openDatabase(databasePath.getPath(), factory, flags, errorHandler); + } } } From d49e7ebae2c09cde5a0ed5af488555e4fdd62fd1 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Jan 2019 14:47:13 +0000 Subject: [PATCH 074/110] Fix nullness checks (pending stub to fix it properly) PiperOrigin-RevId: 231396213 --- .../android/exoplayer2/database/ExoDatabaseProvider.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java b/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java index 9306b22353..ece8e57ae7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/database/ExoDatabaseProvider.java @@ -22,6 +22,7 @@ import android.database.DatabaseErrorHandler; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.util.Log; import java.io.File; @@ -131,11 +132,12 @@ public final class ExoDatabaseProvider extends SQLiteOpenHelper implements Datab } @Override + @SuppressWarnings("nullness:argument.type.incompatible") public SQLiteDatabase openOrCreateDatabase( String name, int mode, SQLiteDatabase.CursorFactory factory, - DatabaseErrorHandler errorHandler) { + @Nullable DatabaseErrorHandler errorHandler) { File databasePath = getDatabasePath(name); int flags = SQLiteDatabase.CREATE_IF_NECESSARY; if ((mode & MODE_ENABLE_WRITE_AHEAD_LOGGING) != 0) { From f4e7af3fd0b22f211da06c9c5d63eac753cf000e Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 29 Jan 2019 16:13:55 +0000 Subject: [PATCH 075/110] Handle duration change in updateQueuedPeriods. We can keep the reading media period and continue playing if we haven't read beyond the new duration. Otherwise, we can keep the period, but need to reset the renderers as we already read too far. PiperOrigin-RevId: 231406252 --- .../exoplayer2/ExoPlayerImplInternal.java | 24 +- .../android/exoplayer2/MediaPeriodQueue.java | 23 +- .../exoplayer2/MediaPeriodQueueTest.java | 218 ++++++++++++++++-- 3 files changed, 245 insertions(+), 20 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index bebc6224ec..670353c4f9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -1376,12 +1376,34 @@ import java.util.concurrent.atomic.AtomicBoolean; } } - if (!queue.updateQueuedPeriods(rendererPositionUs)) { + if (!queue.updateQueuedPeriods(rendererPositionUs, getMaxRendererReadPositionUs())) { seekToCurrentPosition(/* sendDiscontinuity= */ false); } handleLoadingMediaPeriodChanged(/* loadingTrackSelectionChanged= */ false); } + private long getMaxRendererReadPositionUs() { + MediaPeriodHolder readingHolder = queue.getReadingPeriod(); + if (readingHolder == null) { + return 0; + } + long maxReadPositionUs = readingHolder.getRendererOffset(); + for (int i = 0; i < renderers.length; i++) { + if (renderers[i].getState() == Renderer.STATE_DISABLED + || renderers[i].getStream() != readingHolder.sampleStreams[i]) { + // Ignore disabled renderers and renderers with sample streams from previous periods. + continue; + } + long readingPositionUs = renderers[i].getReadingPositionUs(); + if (readingPositionUs == C.TIME_END_OF_SOURCE) { + return C.TIME_END_OF_SOURCE; + } else { + maxReadPositionUs = Math.max(readingPositionUs, maxReadPositionUs); + } + } + return maxReadPositionUs; + } + private void handleSourceInfoRefreshEndedPlayback() { setState(Player.STATE_ENDED); // Reset, but retain the source so that it can still be used should a seek occur. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java index d6ff320295..64719a0ab4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java @@ -61,8 +61,8 @@ import com.google.android.exoplayer2.util.Assertions; } /** - * Sets the {@link Timeline}. Call {@link #updateQueuedPeriods(long)} to update the queued media - * periods to take into account the new timeline. + * Sets the {@link Timeline}. Call {@link #updateQueuedPeriods(long, long)} to update the queued + * media periods to take into account the new timeline. */ public void setTimeline(Timeline timeline) { this.timeline = timeline; @@ -293,9 +293,12 @@ import com.google.android.exoplayer2.util.Assertions; * consistent with the new timeline. * * @param rendererPositionUs The current renderer position in microseconds. + * @param maxRendererReadPositionUs The maximum renderer position up to which renderers have read + * the current reading media period in microseconds, or {@link C#TIME_END_OF_SOURCE} if they + * have read to the end. * @return Whether the timeline change has been handled completely. */ - public boolean updateQueuedPeriods(long rendererPositionUs) { + public boolean updateQueuedPeriods(long rendererPositionUs, long maxRendererReadPositionUs) { // TODO: Merge this into setTimeline so that the queue gets updated as soon as the new timeline // is set, once all cases handled by ExoPlayerImplInternal.handleSourceInfoRefreshed can be // handled here. @@ -327,8 +330,18 @@ import com.google.android.exoplayer2.util.Assertions; periodHolder.info = newPeriodInfo.copyWithContentPositionUs(oldPeriodInfo.contentPositionUs); if (!areDurationsCompatible(oldPeriodInfo.durationUs, newPeriodInfo.durationUs)) { - // The period duration changed. Remove all subsequent periods. - return !removeAfter(periodHolder); + // The period duration changed. Remove all subsequent periods and check whether we read + // beyond the new duration. + long newDurationInRendererTime = + newPeriodInfo.durationUs == C.TIME_UNSET + ? Long.MAX_VALUE + : periodHolder.toRendererTime(newPeriodInfo.durationUs); + boolean isReadingAndReadBeyondNewDuration = + periodHolder == reading + && (maxRendererReadPositionUs == C.TIME_END_OF_SOURCE + || maxRendererReadPositionUs >= newDurationInRendererTime); + boolean readingPeriodRemoved = removeAfter(periodHolder); + return !readingPeriodRemoved && !isReadingAndReadBeyondNewDuration; } previousPeriodHolder = periodHolder; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java index 6016ec1db7..37f8a05790 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/MediaPeriodQueueTest.java @@ -37,6 +37,7 @@ import org.robolectric.RobolectricTestRunner; public final class MediaPeriodQueueTest { private static final long CONTENT_DURATION_US = 30 * C.MICROS_PER_SECOND; + private static final long AD_DURATION_US = 10 * C.MICROS_PER_SECOND; private static final long FIRST_AD_START_TIME_US = 10 * C.MICROS_PER_SECOND; private static final long SECOND_AD_START_TIME_US = 20 * C.MICROS_PER_SECOND; @@ -65,8 +66,8 @@ public final class MediaPeriodQueueTest { } @Test - public void testGetNextMediaPeriodInfo_withoutAds_returnsLastMediaPeriodInfo() { - setupInitialTimeline(/* initialPositionUs= */ 0); + public void getNextMediaPeriodInfo_withoutAds_returnsLastMediaPeriodInfo() { + setupTimeline(/* initialPositionUs= */ 0); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* startPositionUs= */ 0, /* endPositionUs= */ C.TIME_UNSET, @@ -76,8 +77,8 @@ public final class MediaPeriodQueueTest { } @Test - public void testGetNextMediaPeriodInfo_withPrerollAd_returnsCorrectMediaPeriodInfos() { - setupInitialTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs= */ 0); + public void getNextMediaPeriodInfo_withPrerollAd_returnsCorrectMediaPeriodInfos() { + setupTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs= */ 0); setAdGroupLoaded(/* adGroupIndex= */ 0); assertNextMediaPeriodInfoIsAd(/* adGroupIndex= */ 0, /* contentPositionUs= */ 0); advance(); @@ -90,8 +91,8 @@ public final class MediaPeriodQueueTest { } @Test - public void testGetNextMediaPeriodInfo_withMidrollAds_returnsCorrectMediaPeriodInfos() { - setupInitialTimeline( + public void getNextMediaPeriodInfo_withMidrollAds_returnsCorrectMediaPeriodInfos() { + setupTimeline( /* initialPositionUs= */ 0, /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, SECOND_AD_START_TIME_US); @@ -128,8 +129,8 @@ public final class MediaPeriodQueueTest { } @Test - public void testGetNextMediaPeriodInfo_withMidrollAndPostroll_returnsCorrectMediaPeriodInfos() { - setupInitialTimeline( + public void getNextMediaPeriodInfo_withMidrollAndPostroll_returnsCorrectMediaPeriodInfos() { + setupTimeline( /* initialPositionUs= */ 0, /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, C.TIME_END_OF_SOURCE); @@ -164,8 +165,8 @@ public final class MediaPeriodQueueTest { } @Test - public void testGetNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaPeriodInfo() { - setupInitialTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs= */ C.TIME_END_OF_SOURCE); + public void getNextMediaPeriodInfo_withPostrollLoadError_returnsEmptyFinalMediaPeriodInfo() { + setupTimeline(/* initialPositionUs= */ 0, /* adGroupTimesUs= */ C.TIME_END_OF_SOURCE); assertGetNextMediaPeriodInfoReturnsContentMediaPeriod( /* startPositionUs= */ 0, /* endPositionUs= */ C.TIME_END_OF_SOURCE, @@ -182,7 +183,168 @@ public final class MediaPeriodQueueTest { /* nextAdGroupIndex= */ C.INDEX_UNSET); } - private void setupInitialTimeline(long initialPositionUs, long... adGroupTimesUs) { + @Test + public void + updateQueuedPeriods_withDurationChangeAfterReadingPeriod_handlesChangeAndRemovesPeriodsAfterChangedPeriod() { + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US); + setAdGroupLoaded(/* adGroupIndex= */ 0); + setAdGroupLoaded(/* adGroupIndex= */ 1); + enqueueNext(); // Content before first ad. + advancePlaying(); + enqueueNext(); // First ad. + enqueueNext(); // Content between ads. + enqueueNext(); // Second ad. + + // Change position of second ad (= change duration of content between ads). + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US + 1); + setAdGroupLoaded(/* adGroupIndex= */ 0); + setAdGroupLoaded(/* adGroupIndex= */ 1); + boolean changeHandled = + mediaPeriodQueue.updateQueuedPeriods( + /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ 0); + + assertThat(changeHandled).isTrue(); + assertThat(getQueueLength()).isEqualTo(3); + } + + @Test + public void + updateQueuedPeriods_withDurationChangeBeforeReadingPeriod_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US); + setAdGroupLoaded(/* adGroupIndex= */ 0); + setAdGroupLoaded(/* adGroupIndex= */ 1); + enqueueNext(); // Content before first ad. + advancePlaying(); + enqueueNext(); // First ad. + enqueueNext(); // Content between ads. + enqueueNext(); // Second ad. + advanceReading(); // Reading first ad. + + // Change position of first ad (= change duration of content before first ad). + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US + 1, + SECOND_AD_START_TIME_US); + setAdGroupLoaded(/* adGroupIndex= */ 0); + setAdGroupLoaded(/* adGroupIndex= */ 1); + boolean changeHandled = + mediaPeriodQueue.updateQueuedPeriods( + /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ FIRST_AD_START_TIME_US); + + assertThat(changeHandled).isFalse(); + assertThat(getQueueLength()).isEqualTo(1); + } + + @Test + public void + updateQueuedPeriods_withDurationChangeInReadingPeriodAfterReadingPosition_handlesChangeAndRemovesPeriodsAfterChangedPeriod() { + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US); + setAdGroupLoaded(/* adGroupIndex= */ 0); + setAdGroupLoaded(/* adGroupIndex= */ 1); + enqueueNext(); // Content before first ad. + advancePlaying(); + enqueueNext(); // First ad. + enqueueNext(); // Content between ads. + enqueueNext(); // Second ad. + advanceReading(); // Reading first ad. + advanceReading(); // Reading content between ads. + + // Change position of second ad (= change duration of content between ads). + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US - 1000); + setAdGroupLoaded(/* adGroupIndex= */ 0); + setAdGroupLoaded(/* adGroupIndex= */ 1); + long readingPositionAtStartOfContentBetweenAds = FIRST_AD_START_TIME_US + AD_DURATION_US; + boolean changeHandled = + mediaPeriodQueue.updateQueuedPeriods( + /* rendererPositionUs= */ 0, + /* maxRendererReadPositionUs= */ readingPositionAtStartOfContentBetweenAds); + + assertThat(changeHandled).isTrue(); + assertThat(getQueueLength()).isEqualTo(3); + } + + @Test + public void + updateQueuedPeriods_withDurationChangeInReadingPeriodBeforeReadingPosition_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US); + setAdGroupLoaded(/* adGroupIndex= */ 0); + setAdGroupLoaded(/* adGroupIndex= */ 1); + enqueueNext(); // Content before first ad. + advancePlaying(); + enqueueNext(); // First ad. + enqueueNext(); // Content between ads. + enqueueNext(); // Second ad. + advanceReading(); // Reading first ad. + advanceReading(); // Reading content between ads. + + // Change position of second ad (= change duration of content between ads). + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US - 1000); + setAdGroupLoaded(/* adGroupIndex= */ 0); + setAdGroupLoaded(/* adGroupIndex= */ 1); + long readingPositionAtEndOfContentBetweenAds = SECOND_AD_START_TIME_US + AD_DURATION_US; + boolean changeHandled = + mediaPeriodQueue.updateQueuedPeriods( + /* rendererPositionUs= */ 0, + /* maxRendererReadPositionUs= */ readingPositionAtEndOfContentBetweenAds); + + assertThat(changeHandled).isFalse(); + assertThat(getQueueLength()).isEqualTo(3); + } + + @Test + public void + updateQueuedPeriods_withDurationChangeInReadingPeriodReadToEnd_doesntHandleChangeAndRemovesPeriodsAfterChangedPeriod() { + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US); + setAdGroupLoaded(/* adGroupIndex= */ 0); + setAdGroupLoaded(/* adGroupIndex= */ 1); + enqueueNext(); // Content before first ad. + advancePlaying(); + enqueueNext(); // First ad. + enqueueNext(); // Content between ads. + enqueueNext(); // Second ad. + advanceReading(); // Reading first ad. + advanceReading(); // Reading content between ads. + + // Change position of second ad (= change duration of content between ads). + setupTimeline( + /* initialPositionUs= */ 0, + /* adGroupTimesUs= */ FIRST_AD_START_TIME_US, + SECOND_AD_START_TIME_US - 1000); + setAdGroupLoaded(/* adGroupIndex= */ 0); + setAdGroupLoaded(/* adGroupIndex= */ 1); + boolean changeHandled = + mediaPeriodQueue.updateQueuedPeriods( + /* rendererPositionUs= */ 0, /* maxRendererReadPositionUs= */ C.TIME_END_OF_SOURCE); + + assertThat(changeHandled).isFalse(); + assertThat(getQueueLength()).isEqualTo(3); + } + + private void setupTimeline(long initialPositionUs, long... adGroupTimesUs) { adPlaybackState = new AdPlaybackState(adGroupTimesUs).withContentDurationUs(CONTENT_DURATION_US); timeline = new SinglePeriodAdTimeline(CONTENT_TIMELINE, adPlaybackState); @@ -206,9 +368,21 @@ public final class MediaPeriodQueueTest { } private void advance() { + enqueueNext(); + advancePlaying(); + } + + private void advancePlaying() { + mediaPeriodQueue.advancePlayingPeriod(); + } + + private void advanceReading() { + mediaPeriodQueue.advanceReadingPeriod(); + } + + private void enqueueNext() { mediaPeriodQueue.enqueueNextMediaPeriod( rendererCapabilities, trackSelector, allocator, mediaSource, getNextMediaPeriodInfo()); - mediaPeriodQueue.advancePlayingPeriod(); } private MediaPeriodInfo getNextMediaPeriodInfo() { @@ -216,10 +390,16 @@ public final class MediaPeriodQueueTest { } private void setAdGroupLoaded(int adGroupIndex) { + long[][] newDurations = new long[adPlaybackState.adGroupCount][]; + for (int i = 0; i < adPlaybackState.adGroupCount; i++) { + newDurations[i] = + i == adGroupIndex ? new long[] {AD_DURATION_US} : adPlaybackState.adGroups[i].durationsUs; + } adPlaybackState = adPlaybackState .withAdCount(adGroupIndex, /* adCount= */ 1) - .withAdUri(adGroupIndex, /* adIndexInAdGroup= */ 0, AD_URI); + .withAdUri(adGroupIndex, /* adIndexInAdGroup= */ 0, AD_URI) + .withAdDurationsUs(newDurations); updateTimeline(); } @@ -266,8 +446,18 @@ public final class MediaPeriodQueueTest { /* startPositionUs= */ 0, contentPositionUs, /* endPositionUs= */ C.TIME_UNSET, - /* durationUs= */ C.TIME_UNSET, + /* durationUs= */ AD_DURATION_US, /* isLastInTimelinePeriod= */ false, /* isFinal= */ false)); } + + private int getQueueLength() { + int length = 0; + MediaPeriodHolder periodHolder = mediaPeriodQueue.getFrontPeriod(); + while (periodHolder != null) { + length++; + periodHolder = periodHolder.getNext(); + } + return length; + } } From 52ff1820dfbb1933794c1f46c95c9bf1fdd8c7f5 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 29 Jan 2019 16:19:39 +0000 Subject: [PATCH 076/110] Fix buffered position when loading has not completed If there is data after the last samples in the container, we may request continue loading after the last samples have been read but before the load has completed. In this situation the buffered position is returned as Long.MAX_VALUE, which prevents continuing loading, yet the media period is not treated as fully buffered because its buffered position is not C.TIME_END_OF_SOURCE. PiperOrigin-RevId: 231406964 --- .../android/exoplayer2/source/ProgressiveMediaPeriod.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java index 41f6e986c6..ab14554a21 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaPeriod.java @@ -358,10 +358,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } else if (isPendingReset()) { return pendingResetPositionUs; } - long largestQueuedTimestampUs = C.TIME_UNSET; + long largestQueuedTimestampUs = Long.MAX_VALUE; if (haveAudioVideoTracks) { // Ignore non-AV tracks, which may be sparse or poorly interleaved. - largestQueuedTimestampUs = Long.MAX_VALUE; int trackCount = sampleQueues.length; for (int i = 0; i < trackCount; i++) { if (trackIsAudioVideoFlags[i] && !sampleQueues[i].isLastSampleQueued()) { @@ -370,7 +369,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } } } - if (largestQueuedTimestampUs == C.TIME_UNSET) { + if (largestQueuedTimestampUs == Long.MAX_VALUE) { largestQueuedTimestampUs = getLargestQueuedTimestampUs(); } return largestQueuedTimestampUs == Long.MIN_VALUE ? lastSeekPositionUs From 0dd305461d12da68667afe2155f19b143ad25ce5 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 29 Jan 2019 18:16:25 +0000 Subject: [PATCH 077/110] Clarity improvement: Use named variables PiperOrigin-RevId: 231425073 --- .../exoplayer2/upstream/cache/SimpleCacheSpan.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java index dfa553ffe4..c35e3974f6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java @@ -111,12 +111,16 @@ import java.util.regex.Pattern; if (!matcher.matches()) { return null; } + int id = Integer.parseInt(matcher.group(1)); String key = index.getKeyForId(id); - return key == null - ? null - : new SimpleCacheSpan( - key, Long.parseLong(matcher.group(2)), length, Long.parseLong(matcher.group(3)), file); + if (key == null) { + return null; + } + + long position = Long.parseLong(matcher.group(2)); + long lastAccessTime = Long.parseLong(matcher.group(3)); + return new SimpleCacheSpan(key, position, length, lastAccessTime, file); } /** From dc7bc4785ebce95a80db53d143d75d74fdcd8e8f Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 30 Jan 2019 11:42:53 +0000 Subject: [PATCH 078/110] Update ProgressiveMediaSource note PiperOrigin-RevId: 231567138 --- .../android/exoplayer2/source/ProgressiveMediaSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java index a44e99b9c4..dac340cede 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java @@ -39,7 +39,7 @@ import java.io.IOException; * extractors created by the factory that returns {@code true} from {@link Extractor#sniff} will be * used to extract samples from the input stream. * - *

    Note that the built-in extractors for AAC, MPEG PS/TS and FLV streams do not support seeking. + *

    Note that the built-in extractor for FLV streams does not support seeking. */ public final class ProgressiveMediaSource extends BaseMediaSource implements ProgressiveMediaPeriod.Listener { From 55b5814842091af13d5ddc0d540f431c9d31142d Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 30 Jan 2019 13:48:30 +0000 Subject: [PATCH 079/110] Remove unused vpxInit methods PiperOrigin-RevId: 231578828 --- .../google/android/exoplayer2/ext/vp9/VpxDecoder.java | 4 ++-- .../android/exoplayer2/ext/vp9/VpxOutputBuffer.java | 1 - extensions/vp9/src/main/jni/vpx_jni.cc | 11 +---------- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java index f1de63df01..b157981487 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxDecoder.java @@ -68,7 +68,7 @@ import java.nio.ByteBuffer; if (exoMediaCrypto != null && !VpxLibrary.vpxIsSecureDecodeSupported()) { throw new VpxDecoderException("Vpx decoder does not support secure decode."); } - vpxDecContext = vpxInit(disableLoopFilter, true); + vpxDecContext = vpxInit(disableLoopFilter); if (vpxDecContext == 0) { throw new VpxDecoderException("Failed to initialize decoder"); } @@ -165,7 +165,7 @@ import java.nio.ByteBuffer; } } - private native long vpxInit(boolean disableLoopFilter, boolean enableSurfaceYuvOutputMode); + private native long vpxInit(boolean disableLoopFilter); private native long vpxClose(long context); private native long vpxDecode(long context, ByteBuffer encoded, int length); diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java index 22330e0a05..f5cdb6e11a 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/VpxOutputBuffer.java @@ -67,7 +67,6 @@ public final class VpxOutputBuffer extends OutputBuffer { this.timeUs = timeUs; this.mode = mode; } - /** * Resizes the buffer based on the given stride. Called via JNI after decoding completes. * diff --git a/extensions/vp9/src/main/jni/vpx_jni.cc b/extensions/vp9/src/main/jni/vpx_jni.cc index 275687261e..c37545190c 100644 --- a/extensions/vp9/src/main/jni/vpx_jni.cc +++ b/extensions/vp9/src/main/jni/vpx_jni.cc @@ -433,7 +433,7 @@ int vpx_release_frame_buffer(void* priv, vpx_codec_frame_buffer_t* fb) { return buffer_manager->release(*(int*)fb->priv); } -jlong vpxInit(JNIEnv* env, jboolean disableLoopFilter) { +DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter) { JniCtx* context = new JniCtx(); context->decoder = new vpx_codec_ctx_t(); vpx_codec_dec_cfg_t cfg = {0, 0, 0}; @@ -482,15 +482,6 @@ jlong vpxInit(JNIEnv* env, jboolean disableLoopFilter) { return reinterpret_cast(context); } -DECODER_FUNC(jlong, vpxInit, jboolean disableLoopFilter, - jboolean enableBufferManager) { - return vpxInit(env, disableLoopFilter); -} - -DECODER_FUNC(jlong, vpxInitilization, jboolean disableLoopFilter) { - return vpxInit(env, disableLoopFilter); -} - DECODER_FUNC(jlong, vpxDecode, jlong jContext, jobject encoded, jint len) { JniCtx* const context = reinterpret_cast(jContext); const uint8_t* const buffer = From c9b848e5006ebcdadc218b4e3ec7d481db3ee839 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 30 Jan 2019 14:51:56 +0000 Subject: [PATCH 080/110] Synchronously change to next state from downloading state PiperOrigin-RevId: 231586206 --- .../exoplayer2/offline/DownloadManager.java | 95 +++++++++++-------- .../exoplayer2/offline/DownloadState.java | 4 +- .../dash/offline/DownloadManagerDashTest.java | 2 + 3 files changed, 59 insertions(+), 42 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java index f1c90e67c1..d4ea9fc3ff 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java @@ -310,9 +310,10 @@ public final class DownloadManager { /** Returns whether there are no active downloads. */ public boolean isIdle() { Assertions.checkState(!released); - if (!initialized) { + if (!initialized || !activeDownloads.isEmpty()) { return false; } + // Still need to check all downloads as there might be remove tasks going on. for (int i = 0; i < downloads.size(); i++) { if (!downloads.get(i).isIdle()) { return false; @@ -365,6 +366,14 @@ public final class DownloadManager { } } + private void maybeRestartDownload(Download download) { + if (activeDownloads.contains(download)) { + download.start(); + } else { + maybeStartDownload(download); + } + } + private void maybeNotifyListenersIdle() { if (!isIdle()) { return; @@ -540,7 +549,7 @@ public final class DownloadManager { this.startTimeMs = System.currentTimeMillis(); actionQueue = new ArrayDeque<>(); actionQueue.add(action); - initialize(/* restart= */ false); + initialize(); } public boolean addAction(DownloadAction newAction) { @@ -562,12 +571,12 @@ public final class DownloadManager { setState(STATE_REMOVING); } } else if (!action.equals(updatedAction)) { + Assertions.checkState( + state == STATE_DOWNLOADING || state == STATE_QUEUED || state == STATE_STOPPED); if (state == STATE_DOWNLOADING) { stopDownloadThread(); - } else { - Assertions.checkState(state == STATE_QUEUED || state == STATE_STOPPED); - initialize(/* restart= */ false); } + initialize(); } return true; } @@ -633,13 +642,14 @@ public final class DownloadManager { public void updateStopFlags(int flags, int values) { stopFlags = (values & flags) | (stopFlags & ~flags); if (stopFlags != 0) { - if (state == STATE_DOWNLOADING) { - stopDownloadThread(); - } else if (state == STATE_QUEUED) { + if (state == STATE_DOWNLOADING || state == STATE_QUEUED) { + if (state == STATE_DOWNLOADING) { + stopDownloadThread(); + } setState(STATE_STOPPED); } } else if (state == STATE_STOPPED) { - startOrQueue(/* restart= */ false); + startOrQueue(); } } @@ -650,7 +660,7 @@ public final class DownloadManager { notMetRequirements != 0 ? STOP_FLAG_REQUIREMENTS_NOT_MET : 0); } - private void initialize(boolean restart) { + private void initialize() { DownloadAction action = actionQueue.peek(); if (action.isRemoveAction) { if (!downloadManager.released) { @@ -660,18 +670,14 @@ public final class DownloadManager { } else if (stopFlags != 0) { setState(STATE_STOPPED); } else { - startOrQueue(restart); + startOrQueue(); } } - private void startOrQueue(boolean restart) { + private void startOrQueue() { // Set to queued state but don't notify listeners until we make sure we can't start now. state = STATE_QUEUED; - if (restart) { - start(); - } else { - downloadManager.maybeStartDownload(this); - } + downloadManager.maybeRestartDownload(this); if (state == STATE_QUEUED) { downloadManager.onDownloadStateChange(this); } @@ -683,6 +689,9 @@ public final class DownloadManager { } private void startDownloadThread(DownloadAction action) { + if (downloadThread != null) { + return; + } downloader = downloaderFactory.createDownloader(action); downloadThread = new DownloadThread( @@ -690,29 +699,37 @@ public final class DownloadManager { } private void stopDownloadThread() { - Assertions.checkNotNull(downloadThread).cancel(); + if (!downloadThread.isRemoveThread) { + Assertions.checkNotNull(downloadThread).cancel(); + } } - private void onDownloadThreadStopped(@Nullable Throwable finalError) { + private void onDownloadThreadStopped(@Nullable Throwable error) { failureReason = FAILURE_REASON_NONE; - if (!downloadThread.isCanceled) { - if (finalError != null && state != STATE_REMOVING && state != STATE_RESTARTING) { - failureReason = FAILURE_REASON_UNKNOWN; - setState(STATE_FAILED); - return; + boolean isCanceled = downloadThread.isCanceled; + downloadThread = null; + if (isCanceled) { + if (!isIdle()) { + startDownloadThread(actionQueue.peek()); } - if (actionQueue.size() == 1) { - if (state == STATE_REMOVING) { - setState(STATE_REMOVED); - } else { - Assertions.checkState(state == STATE_DOWNLOADING); - setState(STATE_COMPLETED); - } - return; - } - actionQueue.remove(); + return; } - initialize(/* restart= */ state == STATE_DOWNLOADING); + if (error != null && state == STATE_DOWNLOADING) { + failureReason = FAILURE_REASON_UNKNOWN; + setState(STATE_FAILED); + return; + } + if (actionQueue.size() == 1) { + if (state == STATE_REMOVING) { + setState(STATE_REMOVED); + } else { + Assertions.checkState(state == STATE_DOWNLOADING); + setState(STATE_COMPLETED); + } + return; + } + actionQueue.remove(); + initialize(); } } @@ -720,7 +737,7 @@ public final class DownloadManager { private final Download download; private final Downloader downloader; - private final boolean remove; + private final boolean isRemoveThread; private final int minRetryCount; private final Handler callbackHandler; private final Thread thread; @@ -729,12 +746,12 @@ public final class DownloadManager { private DownloadThread( Download download, Downloader downloader, - boolean remove, + boolean isRemoveThread, int minRetryCount, Handler callbackHandler) { this.download = download; this.downloader = downloader; - this.remove = remove; + this.isRemoveThread = isRemoveThread; this.minRetryCount = minRetryCount; this.callbackHandler = callbackHandler; thread = new Thread(this); @@ -754,7 +771,7 @@ public final class DownloadManager { logd("Download is started", download); Throwable error = null; try { - if (remove) { + if (isRemoveThread) { downloader.remove(); } else { int errorCount = 0; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java index dd55d6ea31..f5b3287b32 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java @@ -182,13 +182,11 @@ public final class DownloadState { byte[] customMetadata) { Assertions.checkState( failureReason == FAILURE_REASON_NONE ? state != STATE_FAILED : state == STATE_FAILED); + Assertions.checkState(stopFlags == 0 || (state != STATE_DOWNLOADING && state != STATE_QUEUED)); Assertions.checkState( (stopFlags & STOP_FLAG_REQUIREMENTS_NOT_MET) == 0 ? notMetRequirements == 0 : notMetRequirements != 0); - // TODO enable this when we start changing state immediately - // Assertions.checkState(stopFlags == 0 || (state != STATE_DOWNLOADING && state != - // STATE_QUEUED)); this.id = id; this.type = type; this.uri = uri; diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java index 28040ec538..8d36ea68a4 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/offline/DownloadManagerDashTest.java @@ -53,6 +53,7 @@ import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLog; /** Tests {@link DownloadManager}. */ @RunWith(RobolectricTestRunner.class) @@ -73,6 +74,7 @@ public class DownloadManagerDashTest { @Before public void setUp() throws Exception { + ShadowLog.stream = System.out; dummyMainThread = new DummyMainThread(); Context context = RuntimeEnvironment.application; tempFolder = Util.createTempDirectory(context, "ExoPlayerTest"); From ab67ab1aeab47fe3d68ebe354a8f9f6cf6a381e1 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 30 Jan 2019 16:34:23 +0000 Subject: [PATCH 081/110] Implement database CachedContentIndex.Storage PiperOrigin-RevId: 231600104 --- .../upstream/cache/CachedContentIndex.java | 249 +++++++++++++++++- .../upstream/cache/SimpleCache.java | 4 +- .../cache/CachedContentIndexTest.java | 8 +- 3 files changed, 250 insertions(+), 11 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index e11b5b922c..e31c60e752 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -15,16 +15,25 @@ */ package com.google.android.exoplayer2.upstream.cache; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.util.SparseArray; import android.util.SparseBooleanArray; +import com.google.android.exoplayer2.database.DatabaseProvider; +import com.google.android.exoplayer2.database.ExoDatabaseProvider; +import com.google.android.exoplayer2.database.VersionTable; import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.AtomicFile; import com.google.android.exoplayer2.util.ReusableBufferedOutputStream; import com.google.android.exoplayer2.util.Util; import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; @@ -51,7 +60,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Maintains the index of cached content. */ /* package */ class CachedContentIndex { - public static final String FILE_NAME = "cached_content_index.exi"; + /* package */ static final String FILE_NAME_ATOMIC = "cached_content_index.exi"; + private static final String FILE_NAME_DATABASE = "cached_content_index.db"; private static final int VERSION = 2; private static final int VERSION_METADATA_INTRODUCED = 2; @@ -86,6 +96,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private final Storage storage; + /** + * Returns whether the file is an index file, or an auxiliary file associated with an index file + * (e.g. an atomic file backup or auxiliary database file). + */ + public static final boolean isIndexFile(String fileName) { + // Atomic file backups and auxiliary database files add additional suffixes to the file name. + return fileName.startsWith(FILE_NAME_ATOMIC) || fileName.startsWith(FILE_NAME_DATABASE); + } + /** * Creates a CachedContentIndex which works on the index file in the given cacheDir. * @@ -130,7 +149,17 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; keyToContent = new HashMap<>(); idToKey = new SparseArray<>(); removedIds = new SparseBooleanArray(); - storage = new AtomicFileStorage(new File(cacheDir, FILE_NAME), encrypt, cipher, secretKeySpec); + Random random = new Random(); + storage = + new AtomicFileStorage( + new File(cacheDir, FILE_NAME_ATOMIC), random, encrypt, cipher, secretKeySpec); + // storage = + // new SQLiteStorage( + // new File(cacheDir, FILE_NAME_DATABASE), + // random, + // encrypt, + // cipher, + // secretKeySpec); } /** Loads the index file. */ @@ -369,25 +398,26 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** {@link Storage} implementation that uses an {@link AtomicFile}. */ private static class AtomicFileStorage implements Storage { + private final Random random; private final boolean encrypt; @Nullable private final Cipher cipher; @Nullable private final SecretKeySpec secretKeySpec; private final AtomicFile atomicFile; - private final Random random; private boolean changed; @Nullable private ReusableBufferedOutputStream bufferedOutputStream; public AtomicFileStorage( - File fileName, + File file, + Random random, boolean encrypt, @Nullable Cipher cipher, @Nullable SecretKeySpec secretKeySpec) { + this.random = random; this.encrypt = encrypt; this.cipher = cipher; this.secretKeySpec = secretKeySpec; - atomicFile = new AtomicFile(fileName); - random = new Random(); + atomicFile = new AtomicFile(file); } @Override @@ -570,4 +600,211 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; writeContentMetadata(cachedContent.getMetadata(), output); } } + + /** {@link Storage} implementation that uses an SQL database. */ + // TODO: + // 1. Implement upgrade/downgrade paths from/to AtomicFileStorage. + // 2. If encryption is enabled having previously written data, decide whether we need to rewrite + // the entire table. Currently this implementation only encrypts new and updated entries. + private static final class SQLiteStorage implements Storage { + + private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "Cache"; + private static final int TABLE_VERSION = 1; + + private static final String COLUMN_ID = "id"; + private static final String COLUMN_FLAGS = "flags"; + private static final String COLUMN_DATA = "data"; + + private static final int COLUMN_INDEX_ID = 0; + private static final int COLUMN_INDEX_FLAGS = 1; + private static final int COLUMN_INDEX_DATA = 2; + + private static final String COLUMN_SELECTION_ID = COLUMN_ID + " = ?"; + + private static final String[] COLUMNS = new String[] {COLUMN_ID, COLUMN_FLAGS, COLUMN_DATA}; + + private static final String SQL_DROP_TABLE_IF_EXISTS = "DROP TABLE IF EXISTS " + TABLE_NAME; + private static final String SQL_CREATE_TABLE = + "CREATE TABLE " + + TABLE_NAME + + " (" + + COLUMN_ID + + " INTEGER PRIMARY KEY NOT NULL," + + COLUMN_FLAGS + + " INTEGER NOT NULL," + + COLUMN_DATA + + " BLOB NOT NULL)"; + + private static final int FLAG_ENCRYPTED = 1; + + private final Random random; + private final boolean encrypt; + @Nullable private final Cipher cipher; + @Nullable private final SecretKeySpec secretKeySpec; + private final DatabaseProvider databaseProvider; + private final SparseArray pendingUpdates; + + @Nullable private ReusableBufferedOutputStream bufferedOutputStream; + + public SQLiteStorage( + File file, + Random random, + boolean encrypt, + @Nullable Cipher cipher, + @Nullable SecretKeySpec secretKeySpec) { + this.random = random; + this.encrypt = encrypt; + this.cipher = cipher; + this.secretKeySpec = secretKeySpec; + databaseProvider = new ExoDatabaseProvider(file); + pendingUpdates = new SparseArray<>(); + } + + @Override + public boolean load( + HashMap content, SparseArray<@NullableType String> idToKey) { + try { + int version = + VersionTable.getVersion( + databaseProvider.getReadableDatabase(), VersionTable.FEATURE_CACHE); + if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) { + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.beginTransaction(); + try { + writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS); + writableDatabase.execSQL(SQL_CREATE_TABLE); + VersionTable.setVersion(writableDatabase, VersionTable.FEATURE_CACHE, TABLE_VERSION); + writableDatabase.setTransactionSuccessful(); + } finally { + writableDatabase.endTransaction(); + } + } else if (version < TABLE_VERSION) { + // There is no previous version currently. + throw new IllegalStateException(); + } + + try (Cursor cursor = getCursor()) { + while (cursor.moveToNext()) { + int id = cursor.getInt(COLUMN_INDEX_ID); + boolean encrypted = (cursor.getInt(COLUMN_INDEX_FLAGS) & FLAG_ENCRYPTED) != 0; + byte[] data = cursor.getBlob(COLUMN_INDEX_DATA); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(data); + DataInputStream input = new DataInputStream(inputStream); + if (encrypted) { + byte[] initializationVector = new byte[16]; + input.readFully(initializationVector); + IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector); + try { + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + throw new IllegalStateException(e); + } + input = new DataInputStream(new CipherInputStream(inputStream, cipher)); + } + String key = input.readUTF(); + DefaultContentMetadata metadata = readContentMetadata(input); + + CachedContent cachedContent = new CachedContent(id, key, metadata); + content.put(cachedContent.key, cachedContent); + idToKey.put(cachedContent.id, cachedContent.key); + } + } + return true; + } catch (IOException | SQLiteException e) { + return false; + } + } + + @Override + public void store(HashMap content) throws CacheException { + if (pendingUpdates.size() == 0) { + return; + } + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.beginTransaction(); + try { + for (int i = 0; i < pendingUpdates.size(); i++) { + CachedContent cachedContent = pendingUpdates.valueAt(i); + if (cachedContent == null) { + deleteRow(writableDatabase, pendingUpdates.keyAt(i)); + } else { + addOrUpdateRow(writableDatabase, cachedContent); + } + } + writableDatabase.setTransactionSuccessful(); + pendingUpdates.clear(); + } catch (IOException | SQLiteException e) { + throw new CacheException(e); + } finally { + writableDatabase.endTransaction(); + } + } + + @Override + public void onUpdate(CachedContent cachedContent) { + pendingUpdates.put(cachedContent.id, cachedContent); + } + + @Override + public void onRemove(CachedContent cachedContent) { + pendingUpdates.put(cachedContent.id, null); + } + + private Cursor getCursor() { + return databaseProvider + .getReadableDatabase() + .query( + TABLE_NAME, + COLUMNS, + /* selection= */ null, + /* selectionArgs= */ null, + /* groupBy= */ null, + /* having= */ null, + /* orderBy= */ null); + } + + private void deleteRow(SQLiteDatabase writableDatabase, int key) { + String[] selectionArgs = {Integer.toString(key)}; + writableDatabase.delete(TABLE_NAME, COLUMN_SELECTION_ID, selectionArgs); + } + + private void addOrUpdateRow(SQLiteDatabase writableDatabase, CachedContent cachedContent) + throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + if (bufferedOutputStream == null) { + bufferedOutputStream = new ReusableBufferedOutputStream(outputStream); + } else { + bufferedOutputStream.reset(outputStream); + } + DataOutputStream output = new DataOutputStream(bufferedOutputStream); + try { + if (encrypt) { + byte[] initializationVector = new byte[16]; + random.nextBytes(initializationVector); + output.write(initializationVector); + IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector); + try { + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + throw new IllegalStateException(e); // Should never happen. + } + output.flush(); + output = new DataOutputStream(new CipherOutputStream(bufferedOutputStream, cipher)); + } + output.writeUTF(cachedContent.key); + writeContentMetadata(cachedContent.getMetadata(), output); + } finally { + // Necessary to finalize the cipher. + Util.closeQuietly(output); + } + byte[] data = outputStream.toByteArray(); + + ContentValues values = new ContentValues(); + values.put(COLUMN_ID, cachedContent.id); + values.put(COLUMN_FLAGS, encrypt ? FLAG_ENCRYPTED : 0); + values.put(COLUMN_DATA, data); + writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values); + } + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index 7f9bdde5c1..75236ad9c7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -408,8 +408,8 @@ public final class SimpleCache implements Cache { if (isRootDirectory && fileName.indexOf('.') == -1) { loadDirectory(file, /* isRootDirectory= */ false); } else { - if (isRootDirectory && CachedContentIndex.FILE_NAME.equals(fileName)) { - // Skip the (expected) index file in the root directory. + if (isRootDirectory && CachedContentIndex.isIndexFile(fileName)) { + // Skip the (expected) index files in the root directory. continue; } long fileLength = file.length(); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java index e7bdb0743e..d86e76d147 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndexTest.java @@ -151,7 +151,8 @@ public class CachedContentIndexTest { @Test public void testLoadV1() throws Exception { - FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME)); + FileOutputStream fos = + new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME_ATOMIC)); fos.write(testIndexV1File); fos.close(); @@ -169,7 +170,8 @@ public class CachedContentIndexTest { @Test public void testLoadV2() throws Exception { - FileOutputStream fos = new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME)); + FileOutputStream fos = + new FileOutputStream(new File(cacheDir, CachedContentIndex.FILE_NAME_ATOMIC)); fos.write(testIndexV2File); fos.close(); @@ -220,7 +222,7 @@ public class CachedContentIndexTest { new CachedContentIndex(cacheDir, key), new CachedContentIndex(cacheDir, key)); // Rename the index file from the test above - File file1 = new File(cacheDir, CachedContentIndex.FILE_NAME); + File file1 = new File(cacheDir, CachedContentIndex.FILE_NAME_ATOMIC); File file2 = new File(cacheDir, "file2compare"); assertThat(file1.renameTo(file2)).isTrue(); From 2bd12c2270c6d5e335e01cd25f0391fecdc3228e Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 30 Jan 2019 17:32:03 +0000 Subject: [PATCH 082/110] Move DefaultTrackSelector.Parameters out of DefaultTrackSelector Including ParametersBuilder and TrackSelectionOverride. PiperOrigin-RevId: 231609249 --- .../trackselection/DefaultTrackSelector.java | 169 ++++-------- .../TrackSelectionParameters.java | 240 ++++++++++++++++++ 2 files changed, 287 insertions(+), 122 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 28242167ad..afb2175972 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -159,10 +159,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * A builder for {@link Parameters}. See the {@link Parameters} documentation for explanations of * the parameters that can be configured using this builder. */ - public static final class ParametersBuilder { - - private final SparseArray> selectionOverrides; - private final SparseBooleanArray rendererDisabledFlags; + public static final class ParametersBuilder extends TrackSelectionParameters.Builder { // Video private int maxVideoWidth; @@ -176,22 +173,20 @@ public class DefaultTrackSelector extends MappingTrackSelector { private int viewportHeight; private boolean viewportOrientationMayChange; // Audio - @Nullable private String preferredAudioLanguage; private int maxAudioChannelCount; private int maxAudioBitrate; private boolean exceedAudioConstraintsIfNecessary; private boolean allowAudioMixedMimeTypeAdaptiveness; private boolean allowAudioMixedSampleRateAdaptiveness; - // Text - @Nullable private String preferredTextLanguage; - private boolean selectUndeterminedTextLanguage; - private int disabledTextTrackSelectionFlags; // General private boolean forceLowestBitrate; private boolean forceHighestSupportedBitrate; private boolean exceedRendererCapabilitiesIfNecessary; private int tunnelingAudioSessionId; + private final SparseArray> selectionOverrides; + private final SparseBooleanArray rendererDisabledFlags; + /** Creates a builder with default initial values. */ public ParametersBuilder() { this(Parameters.DEFAULT); @@ -202,6 +197,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * obtained. */ private ParametersBuilder(Parameters initialValues) { + super(initialValues); // Video maxVideoWidth = initialValues.maxVideoWidth; maxVideoHeight = initialValues.maxVideoHeight; @@ -214,16 +210,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { viewportHeight = initialValues.viewportHeight; viewportOrientationMayChange = initialValues.viewportOrientationMayChange; // Audio - preferredAudioLanguage = initialValues.preferredAudioLanguage; maxAudioChannelCount = initialValues.maxAudioChannelCount; maxAudioBitrate = initialValues.maxAudioBitrate; exceedAudioConstraintsIfNecessary = initialValues.exceedAudioConstraintsIfNecessary; allowAudioMixedMimeTypeAdaptiveness = initialValues.allowAudioMixedMimeTypeAdaptiveness; allowAudioMixedSampleRateAdaptiveness = initialValues.allowAudioMixedSampleRateAdaptiveness; - // Text - preferredTextLanguage = initialValues.preferredTextLanguage; - selectUndeterminedTextLanguage = initialValues.selectUndeterminedTextLanguage; - disabledTextTrackSelectionFlags = initialValues.disabledTextTrackSelectionFlags; // General forceLowestBitrate = initialValues.forceLowestBitrate; forceHighestSupportedBitrate = initialValues.forceHighestSupportedBitrate; @@ -362,15 +353,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Audio - /** - * See {@link Parameters#preferredAudioLanguage}. - * - * @param preferredAudioLanguage Preferred audio language as an ISO 639-1 two-letter or ISO - * 639-2 three-letter code. - * @return This builder. - */ + @Override public ParametersBuilder setPreferredAudioLanguage(String preferredAudioLanguage) { - this.preferredAudioLanguage = preferredAudioLanguage; + super.setPreferredAudioLanguage(preferredAudioLanguage); return this; } @@ -429,40 +414,25 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Text - /** - * See {@link Parameters#preferredTextLanguage}. - * - * @param preferredTextLanguage Preferred text language as an ISO 639-1 two-letter or ISO 639-2 - * three-letter code. - * @return This builder. - */ + @Override public ParametersBuilder setPreferredTextLanguage(String preferredTextLanguage) { - this.preferredTextLanguage = preferredTextLanguage; + super.setPreferredTextLanguage(preferredTextLanguage); return this; } - /** - * See {@link Parameters#selectUndeterminedTextLanguage}. - * - * @return This builder. - */ + @Override public ParametersBuilder setSelectUndeterminedTextLanguage( boolean selectUndeterminedTextLanguage) { - this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage; + super.setSelectUndeterminedTextLanguage(selectUndeterminedTextLanguage); return this; } - /** - * See {@link Parameters#disabledTextTrackSelectionFlags}. - * - * @return This builder. - */ + @Override public ParametersBuilder setDisabledTextTrackSelectionFlags( - int disabledTextTrackSelectionFlags) { - this.disabledTextTrackSelectionFlags = disabledTextTrackSelectionFlags; + @C.SelectionFlags int disabledTextTrackSelectionFlags) { + super.setDisabledTextTrackSelectionFlags(disabledTextTrackSelectionFlags); return this; } - // General /** @@ -526,10 +496,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * C#AUDIO_SESSION_ID_UNSET} to disable tunneling. */ public ParametersBuilder setTunnelingAudioSessionId(int tunnelingAudioSessionId) { - if (this.tunnelingAudioSessionId != tunnelingAudioSessionId) { - this.tunnelingAudioSessionId = tunnelingAudioSessionId; - return this; - } + this.tunnelingAudioSessionId = tunnelingAudioSessionId; return this; } @@ -670,7 +637,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { forceHighestSupportedBitrate, exceedRendererCapabilitiesIfNecessary, tunnelingAudioSessionId, - // Overrides selectionOverrides, rendererDisabledFlags); } @@ -685,16 +651,15 @@ public class DefaultTrackSelector extends MappingTrackSelector { } } - /** Constraint parameters for {@link DefaultTrackSelector}. */ - public static final class Parameters implements Parcelable { + /** + * Extends {@link TrackSelectionParameters} by adding fields that are specific to {@link + * DefaultTrackSelector}. + */ + public static final class Parameters extends TrackSelectionParameters { /** An instance with default values. */ public static final Parameters DEFAULT = new Parameters(); - // Overrides - private final SparseArray> selectionOverrides; - private final SparseBooleanArray rendererDisabledFlags; - // Video /** * Maximum allowed video width. The default value is {@link Integer#MAX_VALUE} (i.e. no @@ -759,14 +724,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { * The default value is {@code true}. */ public final boolean viewportOrientationMayChange; - // Audio - /** - * The preferred language for audio and forced text tracks, as an ISO 639-2/T tag. {@code null} - * selects the default track, or the first track if there's no default. The default value is - * {@code null}. - */ - @Nullable public final String preferredAudioLanguage; /** * Maximum allowed audio channel count. The default value is {@link Integer#MAX_VALUE} (i.e. no * constraint). @@ -792,24 +750,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final boolean allowAudioMixedSampleRateAdaptiveness; - // Text - /** - * The preferred language for text tracks as an ISO 639-2/T tag. {@code null} selects the - * default track if there is one, or no track otherwise. The default value is {@code null}. - */ - @Nullable public final String preferredTextLanguage; - /** - * Whether a text track with undetermined language should be selected if no track with {@link - * #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. The - * default value is {@code false}. - */ - public final boolean selectUndeterminedTextLanguage; - /** - * Bitmask of selection flags that are disabled for text track selections. See {@link - * C.SelectionFlags}. The default value is {@code 0} (i.e. no flags). - */ - public final int disabledTextTrackSelectionFlags; - // General /** * Whether to force selection of the single lowest bitrate audio and video tracks that comply @@ -845,6 +785,10 @@ public class DefaultTrackSelector extends MappingTrackSelector { */ public final int tunnelingAudioSessionId; + // Overrides + private final SparseArray> selectionOverrides; + private final SparseBooleanArray rendererDisabledFlags; + private Parameters() { this( // Video @@ -859,24 +803,23 @@ public class DefaultTrackSelector extends MappingTrackSelector { /* viewportHeight= */ Integer.MAX_VALUE, /* viewportOrientationMayChange= */ true, // Audio - /* preferredAudioLanguage= */ null, + TrackSelectionParameters.DEFAULT.preferredAudioLanguage, /* maxAudioChannelCount= */ Integer.MAX_VALUE, /* maxAudioBitrate= */ Integer.MAX_VALUE, /* exceedAudioConstraintsIfNecessary= */ true, /* allowAudioMixedMimeTypeAdaptiveness= */ false, /* allowAudioMixedSampleRateAdaptiveness= */ false, // Text - /* preferredTextLanguage= */ null, - /* selectUndeterminedTextLanguage= */ false, - /* disabledTextTrackSelectionFlags= */ 0, + TrackSelectionParameters.DEFAULT.preferredTextLanguage, + TrackSelectionParameters.DEFAULT.selectUndeterminedTextLanguage, + TrackSelectionParameters.DEFAULT.disabledTextTrackSelectionFlags, // General /* forceLowestBitrate= */ false, /* forceHighestSupportedBitrate= */ false, /* exceedRendererCapabilitiesIfNecessary= */ true, /* tunnelingAudioSessionId= */ C.AUDIO_SESSION_ID_UNSET, - // Overrides - /* selectionOverrides= */ new SparseArray<>(), - /* rendererDisabledFlags= */ new SparseBooleanArray()); + new SparseArray<>(), + new SparseBooleanArray()); } /* package */ Parameters( @@ -901,7 +844,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Text @Nullable String preferredTextLanguage, boolean selectUndeterminedTextLanguage, - int disabledTextTrackSelectionFlags, + @C.SelectionFlags int disabledTextTrackSelectionFlags, // General boolean forceLowestBitrate, boolean forceHighestSupportedBitrate, @@ -910,6 +853,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Overrides SparseArray> selectionOverrides, SparseBooleanArray rendererDisabledFlags) { + super( + preferredAudioLanguage, + preferredTextLanguage, + selectUndeterminedTextLanguage, + disabledTextTrackSelectionFlags); // Video this.maxVideoWidth = maxVideoWidth; this.maxVideoHeight = maxVideoHeight; @@ -922,30 +870,26 @@ public class DefaultTrackSelector extends MappingTrackSelector { this.viewportHeight = viewportHeight; this.viewportOrientationMayChange = viewportOrientationMayChange; // Audio - this.preferredAudioLanguage = Util.normalizeLanguageCode(preferredAudioLanguage); this.maxAudioChannelCount = maxAudioChannelCount; this.maxAudioBitrate = maxAudioBitrate; this.exceedAudioConstraintsIfNecessary = exceedAudioConstraintsIfNecessary; this.allowAudioMixedMimeTypeAdaptiveness = allowAudioMixedMimeTypeAdaptiveness; this.allowAudioMixedSampleRateAdaptiveness = allowAudioMixedSampleRateAdaptiveness; - // Text - this.preferredTextLanguage = Util.normalizeLanguageCode(preferredTextLanguage); - this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage; - this.disabledTextTrackSelectionFlags = disabledTextTrackSelectionFlags; // General this.forceLowestBitrate = forceLowestBitrate; this.forceHighestSupportedBitrate = forceHighestSupportedBitrate; this.exceedRendererCapabilitiesIfNecessary = exceedRendererCapabilitiesIfNecessary; this.tunnelingAudioSessionId = tunnelingAudioSessionId; - // Overrides - this.selectionOverrides = selectionOverrides; - this.rendererDisabledFlags = rendererDisabledFlags; // Deprecated fields. this.allowMixedMimeAdaptiveness = allowVideoMixedMimeTypeAdaptiveness; this.allowNonSeamlessAdaptiveness = allowVideoNonSeamlessAdaptiveness; + // Overrides + this.selectionOverrides = selectionOverrides; + this.rendererDisabledFlags = rendererDisabledFlags; } /* package */ Parameters(Parcel in) { + super(in); // Video this.maxVideoWidth = in.readInt(); this.maxVideoHeight = in.readInt(); @@ -958,16 +902,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { this.viewportHeight = in.readInt(); this.viewportOrientationMayChange = Util.readBoolean(in); // Audio - this.preferredAudioLanguage = in.readString(); this.maxAudioChannelCount = in.readInt(); this.maxAudioBitrate = in.readInt(); this.exceedAudioConstraintsIfNecessary = Util.readBoolean(in); this.allowAudioMixedMimeTypeAdaptiveness = Util.readBoolean(in); this.allowAudioMixedSampleRateAdaptiveness = Util.readBoolean(in); - // Text - this.preferredTextLanguage = in.readString(); - this.selectUndeterminedTextLanguage = Util.readBoolean(in); - this.disabledTextTrackSelectionFlags = in.readInt(); // General this.forceLowestBitrate = Util.readBoolean(in); this.forceHighestSupportedBitrate = Util.readBoolean(in); @@ -1016,9 +955,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { return overrides != null ? overrides.get(groups) : null; } - /** - * Creates a new {@link ParametersBuilder}, copying the initial values from this instance. - */ + /** Creates a new {@link ParametersBuilder}, copying the initial values from this instance. */ + @Override public ParametersBuilder buildUpon() { return new ParametersBuilder(this); } @@ -1032,7 +970,9 @@ public class DefaultTrackSelector extends MappingTrackSelector { return false; } Parameters other = (Parameters) obj; - return maxVideoWidth == other.maxVideoWidth + return super.equals(obj) + // Video + && maxVideoWidth == other.maxVideoWidth && maxVideoHeight == other.maxVideoHeight && maxVideoFrameRate == other.maxVideoFrameRate && maxVideoBitrate == other.maxVideoBitrate @@ -1043,16 +983,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { && viewportWidth == other.viewportWidth && viewportHeight == other.viewportHeight // Audio - && TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage) && maxAudioChannelCount == other.maxAudioChannelCount && maxAudioBitrate == other.maxAudioBitrate && exceedAudioConstraintsIfNecessary == other.exceedAudioConstraintsIfNecessary && allowAudioMixedMimeTypeAdaptiveness == other.allowAudioMixedMimeTypeAdaptiveness && allowAudioMixedSampleRateAdaptiveness == other.allowAudioMixedSampleRateAdaptiveness - // Text - && TextUtils.equals(preferredTextLanguage, other.preferredTextLanguage) - && selectUndeterminedTextLanguage == other.selectUndeterminedTextLanguage - && disabledTextTrackSelectionFlags == other.disabledTextTrackSelectionFlags // General && forceLowestBitrate == other.forceLowestBitrate && forceHighestSupportedBitrate == other.forceHighestSupportedBitrate @@ -1065,7 +1000,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Override public int hashCode() { - int result = 1; + int result = super.hashCode(); // Video result = 31 * result + maxVideoWidth; result = 31 * result + maxVideoHeight; @@ -1078,17 +1013,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { result = 31 * result + viewportWidth; result = 31 * result + viewportHeight; // Audio - result = - 31 * result + (preferredAudioLanguage == null ? 0 : preferredAudioLanguage.hashCode()); result = 31 * result + maxAudioChannelCount; result = 31 * result + maxAudioBitrate; result = 31 * result + (exceedAudioConstraintsIfNecessary ? 1 : 0); result = 31 * result + (allowAudioMixedMimeTypeAdaptiveness ? 1 : 0); result = 31 * result + (allowAudioMixedSampleRateAdaptiveness ? 1 : 0); - // Text - result = 31 * result + (preferredTextLanguage == null ? 0 : preferredTextLanguage.hashCode()); - result = 31 * result + (selectUndeterminedTextLanguage ? 1 : 0); - result = 31 * result + disabledTextTrackSelectionFlags; // General result = 31 * result + (forceLowestBitrate ? 1 : 0); result = 31 * result + (forceHighestSupportedBitrate ? 1 : 0); @@ -1107,6 +1036,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { @Override public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); // Video dest.writeInt(maxVideoWidth); dest.writeInt(maxVideoHeight); @@ -1119,16 +1049,11 @@ public class DefaultTrackSelector extends MappingTrackSelector { dest.writeInt(viewportHeight); Util.writeBoolean(dest, viewportOrientationMayChange); // Audio - dest.writeString(preferredAudioLanguage); dest.writeInt(maxAudioChannelCount); dest.writeInt(maxAudioBitrate); Util.writeBoolean(dest, exceedAudioConstraintsIfNecessary); Util.writeBoolean(dest, allowAudioMixedMimeTypeAdaptiveness); Util.writeBoolean(dest, allowAudioMixedSampleRateAdaptiveness); - // Text - dest.writeString(preferredTextLanguage); - Util.writeBoolean(dest, selectUndeterminedTextLanguage); - dest.writeInt(disabledTextTrackSelectionFlags); // General Util.writeBoolean(dest, forceLowestBitrate); Util.writeBoolean(dest, forceHighestSupportedBitrate); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java new file mode 100644 index 0000000000..2bbd81854c --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.trackselection; + +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.Util; + +/** Constraint parameters for track selection. */ +public class TrackSelectionParameters implements Parcelable { + + /** + * A builder for {@link TrackSelectionParameters}. See the {@link TrackSelectionParameters} + * documentation for explanations of the parameters that can be configured using this builder. + */ + public static class Builder { + + // Audio + @Nullable /* package */ String preferredAudioLanguage; + // Text + @Nullable /* package */ String preferredTextLanguage; + /* package */ boolean selectUndeterminedTextLanguage; + @C.SelectionFlags /* package */ int disabledTextTrackSelectionFlags; + + /** Creates a builder with default initial values. */ + public Builder() { + this(DEFAULT); + } + + /** + * @param initialValues The {@link TrackSelectionParameters} from which the initial values of + * the builder are obtained. + */ + /* package */ Builder(TrackSelectionParameters initialValues) { + // Audio + preferredAudioLanguage = initialValues.preferredAudioLanguage; + // Text + preferredTextLanguage = initialValues.preferredTextLanguage; + selectUndeterminedTextLanguage = initialValues.selectUndeterminedTextLanguage; + disabledTextTrackSelectionFlags = initialValues.disabledTextTrackSelectionFlags; + } + + /** + * See {@link TrackSelectionParameters#preferredAudioLanguage}. + * + * @param preferredAudioLanguage Preferred audio language as an ISO 639-1 two-letter or ISO + * 639-2 three-letter code. + * @return This builder. + */ + public Builder setPreferredAudioLanguage(String preferredAudioLanguage) { + this.preferredAudioLanguage = preferredAudioLanguage; + return this; + } + + // Text + + /** + * See {@link TrackSelectionParameters#preferredTextLanguage}. + * + * @param preferredTextLanguage Preferred text language as an ISO 639-1 two-letter or ISO 639-2 + * three-letter code. + * @return This builder. + */ + public Builder setPreferredTextLanguage(String preferredTextLanguage) { + this.preferredTextLanguage = preferredTextLanguage; + return this; + } + + /** + * See {@link TrackSelectionParameters#selectUndeterminedTextLanguage}. + * + * @return This builder. + */ + public Builder setSelectUndeterminedTextLanguage(boolean selectUndeterminedTextLanguage) { + this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage; + return this; + } + + /** + * See {@link TrackSelectionParameters#disabledTextTrackSelectionFlags}. + * + * @return This builder. + */ + public Builder setDisabledTextTrackSelectionFlags( + @C.SelectionFlags int disabledTextTrackSelectionFlags) { + this.disabledTextTrackSelectionFlags = disabledTextTrackSelectionFlags; + return this; + } + + /** Builds a {@link TrackSelectionParameters} instance with the selected values. */ + public TrackSelectionParameters build() { + return new TrackSelectionParameters( + // Audio + preferredAudioLanguage, + // Text + preferredTextLanguage, + selectUndeterminedTextLanguage, + disabledTextTrackSelectionFlags); + } + } + + /** An instance with default values. */ + public static final TrackSelectionParameters DEFAULT = new TrackSelectionParameters(); + + /** + * The preferred language for audio and forced text tracks, as an ISO 639-2/T tag. {@code null} + * selects the default track, or the first track if there's no default. The default value is + * {@code null}. + */ + @Nullable public final String preferredAudioLanguage; + // Text + /** + * The preferred language for text tracks as an ISO 639-2/T tag. {@code null} selects the default + * track if there is one, or no track otherwise. The default value is {@code null}. + */ + @Nullable public final String preferredTextLanguage; + /** + * Whether a text track with undetermined language should be selected if no track with {@link + * #preferredTextLanguage} is available, or if {@link #preferredTextLanguage} is unset. The + * default value is {@code false}. + */ + public final boolean selectUndeterminedTextLanguage; + /** + * Bitmask of selection flags that are disabled for text track selections. See {@link + * C.SelectionFlags}. The default value is {@code 0} (i.e. no flags). + */ + @C.SelectionFlags public final int disabledTextTrackSelectionFlags; + + /* package */ TrackSelectionParameters() { + this( + /* preferredAudioLanguage= */ null, + // Text + /* preferredTextLanguage= */ null, + /* selectUndeterminedTextLanguage= */ false, + /* disabledTextTrackSelectionFlags= */ 0); + } + + /* package */ TrackSelectionParameters( + @Nullable String preferredAudioLanguage, + @Nullable String preferredTextLanguage, + boolean selectUndeterminedTextLanguage, + @C.SelectionFlags int disabledTextTrackSelectionFlags) { + // Audio + this.preferredAudioLanguage = Util.normalizeLanguageCode(preferredAudioLanguage); + // Text + this.preferredTextLanguage = Util.normalizeLanguageCode(preferredTextLanguage); + this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage; + this.disabledTextTrackSelectionFlags = disabledTextTrackSelectionFlags; + } + + /* package */ TrackSelectionParameters(Parcel in) { + // Audio + this.preferredAudioLanguage = in.readString(); + // Text + this.preferredTextLanguage = in.readString(); + this.selectUndeterminedTextLanguage = Util.readBoolean(in); + this.disabledTextTrackSelectionFlags = in.readInt(); + } + + /** Creates a new {@link Builder}, copying the initial values from this instance. */ + public Builder buildUpon() { + return new Builder(this); + } + + @Override + @SuppressWarnings("EqualsGetClass") + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + TrackSelectionParameters other = (TrackSelectionParameters) obj; + return TextUtils.equals(preferredAudioLanguage, other.preferredAudioLanguage) + // Text + && TextUtils.equals(preferredTextLanguage, other.preferredTextLanguage) + && selectUndeterminedTextLanguage == other.selectUndeterminedTextLanguage + && disabledTextTrackSelectionFlags == other.disabledTextTrackSelectionFlags; + } + + @Override + public int hashCode() { + int result = 1; + // Audio + result = 31 * result + (preferredAudioLanguage == null ? 0 : preferredAudioLanguage.hashCode()); + // Text + result = 31 * result + (preferredTextLanguage == null ? 0 : preferredTextLanguage.hashCode()); + result = 31 * result + (selectUndeterminedTextLanguage ? 1 : 0); + result = 31 * result + disabledTextTrackSelectionFlags; + return result; + } + + // Parcelable implementation. + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + // Audio + dest.writeString(preferredAudioLanguage); + // Text + dest.writeString(preferredTextLanguage); + Util.writeBoolean(dest, selectUndeterminedTextLanguage); + dest.writeInt(disabledTextTrackSelectionFlags); + } + + public static final Creator CREATOR = + new Creator() { + + @Override + public TrackSelectionParameters createFromParcel(Parcel in) { + return new TrackSelectionParameters(in); + } + + @Override + public TrackSelectionParameters[] newArray(int size) { + return new TrackSelectionParameters[size]; + } + }; +} From 2ab91bf08a1fee65cb3164fc92619daa513123c5 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 30 Jan 2019 20:22:33 +0000 Subject: [PATCH 083/110] Update OkHttp and Cronet dependencies PiperOrigin-RevId: 231644908 --- extensions/cronet/build.gradle | 2 +- extensions/okhttp/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index f77e84c816..1c1c099e7b 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -30,7 +30,7 @@ android { } dependencies { - api 'org.chromium.net:cronet-embedded:66.3359.158' + api 'org.chromium.net:cronet-embedded:71.3578.98' implementation project(modulePrefix + 'library-core') implementation 'com.android.support:support-annotations:' + supportLibraryVersion testImplementation project(modulePrefix + 'library') diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index 4e6b11c495..78825a6277 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -34,7 +34,7 @@ dependencies { implementation project(modulePrefix + 'library-core') implementation 'com.android.support:support-annotations:' + supportLibraryVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion - api 'com.squareup.okhttp3:okhttp:3.11.0' + api 'com.squareup.okhttp3:okhttp:3.12.1' } ext { From ef6725bf4827148526f016467f26398fd20d0013 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 31 Jan 2019 09:13:11 +0000 Subject: [PATCH 084/110] Bump version for 2.9.5 release PiperOrigin-RevId: 231744540 --- RELEASENOTES.md | 13 +++++++++++-- constants.gradle | 4 ++-- .../android/exoplayer2/ExoPlayerLibraryInfo.java | 6 +++--- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 65d4e9696e..43952f29f4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,8 +3,6 @@ ### dev-v2 (not yet released) ### * `ExtractorMediaSource` renamed to `ProgressiveMediaSource`. -* HLS: - * Parse CHANNELS attribute from EXT-X-MEDIA. * Support for playing spherical videos on Daydream. * Improve decoder re-use between playbacks. TODO: Write and link a blog post here ([#2826](https://github.com/google/ExoPlayer/issues/2826)). @@ -48,6 +46,17 @@ * Change signature of `PlayerNotificationManager.NotificationListener` to better fit service requirements. Remove ability to set a custom stop action. +### 2.9.5 ### + +* HLS: Parse `CHANNELS` attribute from `EXT-X-MEDIA` tag. +* ExtractorMediaSource: Fix issue that could cause the player to get stuck + buffering at the end of the media. +* PlayerView: Fix issue preventing `OnClickListener` from receiving events + ([#5433](https://github.com/google/ExoPlayer/issues/5433)). +* IMA extension: Upgrade IMA dependency to 3.10.6. +* Cronet extension: Upgrade Cronet dependency to 71.3578.98. +* OkHttp extension: Upgrade OkHttp dependency to 3.12.1. + ### 2.9.4 ### * IMA extension: Clear ads loader listeners on release diff --git a/constants.gradle b/constants.gradle index 062438fa30..a932ef218f 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.9.4' - releaseVersionCode = 2009004 + releaseVersion = '2.9.5' + releaseVersionCode = 2009005 minSdkVersion = 16 targetSdkVersion = 28 compileSdkVersion = 28 diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 36723c5d73..e3a2e1cd27 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo { /** The version of the library expressed as a string, for example "1.2.3". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "2.9.4"; + public static final String VERSION = "2.9.5"; /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.4"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.9.5"; /** * The version of the library expressed as an integer, for example 1002003. @@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo { * integer version 123045006 (123-045-006). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 2009004; + public static final int VERSION_INT = 2009005; /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} From d0b3d1cf635d1150793a535b443263633a8ea601 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 31 Jan 2019 11:45:08 +0000 Subject: [PATCH 085/110] Add additional device to output surface workaround Issue: #4468 PiperOrigin-RevId: 231759438 --- .../android/exoplayer2/video/MediaCodecVideoRenderer.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index d8ad2a840b..68e98633d6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -1326,8 +1326,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } synchronized (MediaCodecVideoRenderer.class) { if (!evaluatedDeviceNeedsSetOutputSurfaceWorkaround) { - if (Util.SDK_INT <= 27 && "dangal".equals(Util.DEVICE)) { - // Dangal is affected on API level 27: https://github.com/google/ExoPlayer/issues/5169. + if (Util.SDK_INT <= 27 && ("dangal".equals(Util.DEVICE) || "HWEML".equals(Util.DEVICE))) { + // A small number of devices are affected on API level 27: + // https://github.com/google/ExoPlayer/issues/5169. deviceNeedsSetOutputSurfaceWorkaround = true; } else if (Util.SDK_INT >= 27) { // In general, devices running API level 27 or later should be unaffected. Do nothing. From 503c17d2ed9003562f0dd73716b7aace8970e0a5 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 31 Jan 2019 12:39:14 +0000 Subject: [PATCH 086/110] Add constraint based track selection for ExoCast PiperOrigin-RevId: 231764284 --- .../exoplayer2/trackselection/DefaultTrackSelector.java | 4 ++-- .../exoplayer2/trackselection/TrackSelectionParameters.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index afb2175972..43157c5866 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -354,7 +354,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Audio @Override - public ParametersBuilder setPreferredAudioLanguage(String preferredAudioLanguage) { + public ParametersBuilder setPreferredAudioLanguage(@Nullable String preferredAudioLanguage) { super.setPreferredAudioLanguage(preferredAudioLanguage); return this; } @@ -415,7 +415,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { // Text @Override - public ParametersBuilder setPreferredTextLanguage(String preferredTextLanguage) { + public ParametersBuilder setPreferredTextLanguage(@Nullable String preferredTextLanguage) { super.setPreferredTextLanguage(preferredTextLanguage); return this; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java index 2bbd81854c..f411d431e2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectionParameters.java @@ -63,7 +63,7 @@ public class TrackSelectionParameters implements Parcelable { * 639-2 three-letter code. * @return This builder. */ - public Builder setPreferredAudioLanguage(String preferredAudioLanguage) { + public Builder setPreferredAudioLanguage(@Nullable String preferredAudioLanguage) { this.preferredAudioLanguage = preferredAudioLanguage; return this; } @@ -77,7 +77,7 @@ public class TrackSelectionParameters implements Parcelable { * three-letter code. * @return This builder. */ - public Builder setPreferredTextLanguage(String preferredTextLanguage) { + public Builder setPreferredTextLanguage(@Nullable String preferredTextLanguage) { this.preferredTextLanguage = preferredTextLanguage; return this; } From a73819162751116acd3863cf5473b0ff78fac805 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 31 Jan 2019 14:03:11 +0000 Subject: [PATCH 087/110] Remove remainder of skip button focus hack PiperOrigin-RevId: 231772920 --- .../java/com/google/android/exoplayer2/ui/PlayerView.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java index f7f509dfa7..9742d0005a 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -758,10 +758,6 @@ public class PlayerView extends FrameLayout { @Override public boolean dispatchKeyEvent(KeyEvent event) { if (player != null && player.isPlayingAd()) { - // Focus any overlay UI now, in case it's provided by a WebView whose contents may update - // dynamically. This is needed to make the "Skip ad" button focused on Android TV when using - // IMA [Internal: b/62371030]. - overlayFrameLayout.requestFocus(); return super.dispatchKeyEvent(event); } boolean isDpadWhenControlHidden = From c61c0bd1ac429429f4264f061a5aebb96b172208 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Thu, 31 Jan 2019 17:38:48 +0000 Subject: [PATCH 088/110] Cast demo: Fix a few miscellaneous bugs PiperOrigin-RevId: 231801562 --- .../com/google/android/exoplayer2/castdemo/MainActivity.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 46e8273947..48934abd2c 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -279,6 +279,8 @@ public class MainActivity extends AppCompatActivity int position = viewHolder.getAdapterPosition(); if (playerManager.removeItem(position)) { mediaQueueListAdapter.notifyItemRemoved(position); + // Update whichever item took its place, in case it became the new selected item. + mediaQueueListAdapter.notifyItemChanged(position); } } From 5e311fc82ac6996f2bb634f3f466f1bb4dd2f96f Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 4 Feb 2019 13:06:09 +0000 Subject: [PATCH 089/110] Refactor DownloadManage to simplify DownloadThread management Now DownloadManager is responsible for starting and stopping DownloadThreads. PiperOrigin-RevId: 232278072 --- .../exoplayer2/offline/DownloadManager.java | 224 ++++++++++-------- 1 file changed, 119 insertions(+), 105 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java index d4ea9fc3ff..9e6aec0a86 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java @@ -34,6 +34,7 @@ import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; +import android.support.annotation.IntDef; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.scheduler.Requirements; @@ -42,8 +43,11 @@ import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import java.io.File; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.HashMap; import java.util.concurrent.CopyOnWriteArraySet; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -101,16 +105,30 @@ public final class DownloadManager { public static final Requirements DEFAULT_REQUIREMENTS = new Requirements(Requirements.NETWORK_TYPE_ANY, false, false); + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + START_THREAD_SUCCEEDED, + START_THREAD_WAIT_REMOVAL_TO_FINISH, + START_THREAD_WAIT_DOWNLOAD_CANCELATION, + START_THREAD_TOO_MANY_DOWNLOADS + }) + private @interface StartThreadResults {} + + private static final int START_THREAD_SUCCEEDED = 0; + private static final int START_THREAD_WAIT_REMOVAL_TO_FINISH = 1; + private static final int START_THREAD_WAIT_DOWNLOAD_CANCELATION = 2; + private static final int START_THREAD_TOO_MANY_DOWNLOADS = 3; + private static final String TAG = "DownloadManager"; private static final boolean DEBUG = false; - private final int maxActiveDownloads; + private final int maxSimultaneousDownloads; private final int minRetryCount; private final Context context; private final ActionFile actionFile; private final DownloaderFactory downloaderFactory; private final ArrayList downloads; - private final ArrayList activeDownloads; + private final HashMap activeDownloads; private final Handler handler; private final HandlerThread fileIOThread; private final Handler fileIOHandler; @@ -122,6 +140,7 @@ public final class DownloadManager { @DownloadState.StopFlags private int stickyStopFlags; @Requirements.RequirementFlags private int notMetRequirements; private RequirementsWatcher requirementsWatcher; + private int simultaneousDownloads; /** * Constructs a {@link DownloadManager}. @@ -160,12 +179,12 @@ public final class DownloadManager { this.context = context.getApplicationContext(); this.actionFile = new ActionFile(actionFile); this.downloaderFactory = downloaderFactory; - this.maxActiveDownloads = maxSimultaneousDownloads; + this.maxSimultaneousDownloads = maxSimultaneousDownloads; this.minRetryCount = minRetryCount; this.stickyStopFlags = STOP_FLAG_STOPPED | STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY; downloads = new ArrayList<>(); - activeDownloads = new ArrayList<>(); + activeDownloads = new HashMap<>(); Looper looper = Looper.myLooper(); if (looper == null) { @@ -310,16 +329,7 @@ public final class DownloadManager { /** Returns whether there are no active downloads. */ public boolean isIdle() { Assertions.checkState(!released); - if (!initialized || !activeDownloads.isEmpty()) { - return false; - } - // Still need to check all downloads as there might be remove tasks going on. - for (int i = 0; i < downloads.size(); i++) { - if (!downloads.get(i).isIdle()) { - return false; - } - } - return true; + return initialized && activeDownloads.isEmpty(); } /** @@ -351,29 +361,11 @@ public final class DownloadManager { return; } } - Download download = - new Download( - this, downloaderFactory, action, minRetryCount, stickyStopFlags, notMetRequirements); + Download download = new Download(this, action, stickyStopFlags, notMetRequirements); downloads.add(download); logd("Download is added", download); } - private void maybeStartDownload(Download download) { - if (activeDownloads.size() < maxActiveDownloads) { - if (download.start()) { - activeDownloads.add(download); - } - } - } - - private void maybeRestartDownload(Download download) { - if (activeDownloads.contains(download)) { - download.start(); - } else { - maybeStartDownload(download); - } - } - private void maybeNotifyListenersIdle() { if (!isIdle()) { return; @@ -388,21 +380,11 @@ public final class DownloadManager { if (released) { return; } - boolean idle = download.isIdle(); - if (idle) { - activeDownloads.remove(download); - } notifyListenersDownloadStateChange(download); if (download.isFinished()) { downloads.remove(download); saveActions(); } - if (idle) { - for (int i = 0; i < downloads.size(); i++) { - maybeStartDownload(downloads.get(i)); - } - maybeNotifyListenersIdle(); - } } private void notifyListenersDownloadStateChange(Download download) { @@ -513,34 +495,86 @@ public final class DownloadManager { return notMetRequirements; } + @StartThreadResults + private int startDownloadThread(Download download, DownloadAction action) { + if (activeDownloads.containsKey(download)) { + if (stopDownloadThread(download)) { + return START_THREAD_WAIT_DOWNLOAD_CANCELATION; + } + return START_THREAD_WAIT_REMOVAL_TO_FINISH; + } + if (!action.isRemoveAction) { + if (simultaneousDownloads == maxSimultaneousDownloads) { + return START_THREAD_TOO_MANY_DOWNLOADS; + } + simultaneousDownloads++; + } + Downloader downloader = downloaderFactory.createDownloader(action); + DownloadThread downloadThread = new DownloadThread(download, downloader, action.isRemoveAction); + activeDownloads.put(download, downloadThread); + logd("Download is started", download); + return START_THREAD_SUCCEEDED; + } + + private boolean stopDownloadThread(Download download) { + DownloadThread downloadThread = activeDownloads.get(download); + if (downloadThread != null && !downloadThread.isRemoveThread) { + downloadThread.cancel(); + logd("Download is cancelled", download); + return true; + } + return false; + } + + private void onDownloadThreadStopped(DownloadThread downloadThread, Throwable finalError) { + Download download = downloadThread.download; + logd("Download is stopped", download); + activeDownloads.remove(download); + boolean tryToStartDownloads = false; + if (!downloadThread.isRemoveThread) { + // If maxSimultaneousDownloads was hit, there might be a download waiting for a slot. + tryToStartDownloads = simultaneousDownloads == maxSimultaneousDownloads; + simultaneousDownloads--; + } + download.onDownloadThreadStopped(downloadThread.isCanceled, finalError); + if (tryToStartDownloads) { + for (int i = 0; + simultaneousDownloads < maxSimultaneousDownloads && i < downloads.size(); + i++) { + downloads.get(i).start(); + } + } + maybeNotifyListenersIdle(); + } + + @Nullable + private Downloader getDownloader(Download download) { + DownloadThread downloadThread = activeDownloads.get(download); + if (downloadThread != null) { + return downloadThread.downloader; + } + return null; + } + private static final class Download { private final String id; private final DownloadManager downloadManager; - private final DownloaderFactory downloaderFactory; - private final int minRetryCount; private final long startTimeMs; private final ArrayDeque actionQueue; - /** The current state of the download. */ - @DownloadState.State private int state; - @MonotonicNonNull private Downloader downloader; - @MonotonicNonNull private DownloadThread downloadThread; + @DownloadState.State private int state; @MonotonicNonNull @DownloadState.FailureReason private int failureReason; @DownloadState.StopFlags private int stopFlags; @Requirements.RequirementFlags private int notMetRequirements; private Download( DownloadManager downloadManager, - DownloaderFactory downloaderFactory, DownloadAction action, - int minRetryCount, @DownloadState.StopFlags int stopFlags, @Requirements.RequirementFlags int notMetRequirements) { this.id = action.id; this.downloadManager = downloadManager; - this.downloaderFactory = downloaderFactory; - this.minRetryCount = minRetryCount; this.notMetRequirements = notMetRequirements; if (notMetRequirements != 0) { stopFlags |= STOP_FLAG_REQUIREMENTS_NOT_MET; @@ -549,7 +583,14 @@ public final class DownloadManager { this.startTimeMs = System.currentTimeMillis(); actionQueue = new ArrayDeque<>(); actionQueue.add(action); + + // Set to queued state but don't notify listeners until we make sure we don't switch to + // another state immediately. + state = STATE_QUEUED; initialize(); + if (state == STATE_QUEUED) { + downloadManager.onDownloadStateChange(this); + } } public boolean addAction(DownloadAction newAction) { @@ -573,9 +614,6 @@ public final class DownloadManager { } else if (!action.equals(updatedAction)) { Assertions.checkState( state == STATE_DOWNLOADING || state == STATE_QUEUED || state == STATE_STOPPED); - if (state == STATE_DOWNLOADING) { - stopDownloadThread(); - } initialize(); } return true; @@ -585,6 +623,7 @@ public final class DownloadManager { float downloadPercentage = C.PERCENTAGE_UNSET; long downloadedBytes = 0; long totalBytes = C.LENGTH_UNSET; + Downloader downloader = downloadManager.getDownloader(this); if (downloader != null) { downloadPercentage = downloader.getDownloadPercentage(); downloadedBytes = downloader.getDownloadedBytes(); @@ -622,13 +661,10 @@ public final class DownloadManager { return id + ' ' + DownloadState.getStateString(state); } - public boolean start() { - if (state != STATE_QUEUED) { - return false; + public void start() { + if (state == STATE_QUEUED) { + startOrQueue(); } - startDownloadThread(actionQueue.peek()); - setState(STATE_DOWNLOADING); - return true; } public void setStopFlags(int flags) { @@ -643,9 +679,7 @@ public final class DownloadManager { stopFlags = (values & flags) | (stopFlags & ~flags); if (stopFlags != 0) { if (state == STATE_DOWNLOADING || state == STATE_QUEUED) { - if (state == STATE_DOWNLOADING) { - stopDownloadThread(); - } + downloadManager.stopDownloadThread(this); setState(STATE_STOPPED); } } else if (state == STATE_STOPPED) { @@ -664,7 +698,9 @@ public final class DownloadManager { DownloadAction action = actionQueue.peek(); if (action.isRemoveAction) { if (!downloadManager.released) { - startDownloadThread(action); + int result = downloadManager.startDownloadThread(this, action); + Assertions.checkState( + result == START_THREAD_SUCCEEDED || result == START_THREAD_WAIT_DOWNLOAD_CANCELATION); } setState(actionQueue.size() == 1 ? STATE_REMOVING : STATE_RESTARTING); } else if (stopFlags != 0) { @@ -675,42 +711,29 @@ public final class DownloadManager { } private void startOrQueue() { - // Set to queued state but don't notify listeners until we make sure we can't start now. - state = STATE_QUEUED; - downloadManager.maybeRestartDownload(this); - if (state == STATE_QUEUED) { - downloadManager.onDownloadStateChange(this); + DownloadAction action = Assertions.checkNotNull(actionQueue.peek()); + Assertions.checkState(!action.isRemoveAction); + @StartThreadResults int result = downloadManager.startDownloadThread(this, action); + Assertions.checkState(result != START_THREAD_WAIT_REMOVAL_TO_FINISH); + if (result == START_THREAD_TOO_MANY_DOWNLOADS) { + setState(STATE_QUEUED); + } else { + setState(STATE_DOWNLOADING); } } private void setState(@DownloadState.State int newState) { - state = newState; - downloadManager.onDownloadStateChange(this); - } - - private void startDownloadThread(DownloadAction action) { - if (downloadThread != null) { - return; - } - downloader = downloaderFactory.createDownloader(action); - downloadThread = - new DownloadThread( - this, downloader, action.isRemoveAction, minRetryCount, downloadManager.handler); - } - - private void stopDownloadThread() { - if (!downloadThread.isRemoveThread) { - Assertions.checkNotNull(downloadThread).cancel(); + if (state != newState) { + state = newState; + downloadManager.onDownloadStateChange(this); } } - private void onDownloadThreadStopped(@Nullable Throwable error) { + private void onDownloadThreadStopped(boolean isCanceled, @Nullable Throwable error) { failureReason = FAILURE_REASON_NONE; - boolean isCanceled = downloadThread.isCanceled; - downloadThread = null; if (isCanceled) { if (!isIdle()) { - startDownloadThread(actionQueue.peek()); + downloadManager.startDownloadThread(this, actionQueue.peek()); } return; } @@ -733,27 +756,18 @@ public final class DownloadManager { } } - private static class DownloadThread implements Runnable { + private class DownloadThread implements Runnable { private final Download download; private final Downloader downloader; private final boolean isRemoveThread; - private final int minRetryCount; - private final Handler callbackHandler; private final Thread thread; private volatile boolean isCanceled; - private DownloadThread( - Download download, - Downloader downloader, - boolean isRemoveThread, - int minRetryCount, - Handler callbackHandler) { + private DownloadThread(Download download, Downloader downloader, boolean isRemoveThread) { this.download = download; this.downloader = downloader; this.isRemoveThread = isRemoveThread; - this.minRetryCount = minRetryCount; - this.callbackHandler = callbackHandler; thread = new Thread(this); thread.start(); } @@ -768,7 +782,7 @@ public final class DownloadManager { @Override public void run() { - logd("Download is started", download); + logd("Download started", download); Throwable error = null; try { if (isRemoveThread) { @@ -801,7 +815,7 @@ public final class DownloadManager { error = e; } final Throwable finalError = error; - callbackHandler.post(() -> download.onDownloadThreadStopped(isCanceled ? null : finalError)); + handler.post(() -> onDownloadThreadStopped(this, finalError)); } private int getRetryDelayMillis(int errorCount) { From f1ded9c3c2192c0f72678df0ea0cf32f6cc1465e Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Feb 2019 16:11:26 +0000 Subject: [PATCH 090/110] Wider fix for OMX.SEC.mp3.dec issue Issue: #4519 PiperOrigin-RevId: 232299233 --- RELEASENOTES.md | 2 + .../exoplayer2/mediacodec/MediaCodecUtil.java | 53 +++++++++---------- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 43952f29f4..98f8e85857 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -56,6 +56,8 @@ * IMA extension: Upgrade IMA dependency to 3.10.6. * Cronet extension: Upgrade Cronet dependency to 71.3578.98. * OkHttp extension: Upgrade OkHttp dependency to 3.12.1. +* MP3: Wider fix for issue where streams would play twice on some Samsung + devices ([#4519](https://github.com/google/ExoPlayer/issues/4519)). ### 2.9.4 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index ae3a9f123f..55459692c9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -58,8 +58,6 @@ public final class MediaCodecUtil { private static final String TAG = "MediaCodecUtil"; private static final Pattern PROFILE_PATTERN = Pattern.compile("^\\D?(\\d+)$"); - private static final RawAudioCodecComparator RAW_AUDIO_CODEC_COMPARATOR = - new RawAudioCodecComparator(); private static final HashMap> decoderInfosCache = new HashMap<>(); @@ -311,32 +309,6 @@ public final class MediaCodecUtil { return false; } - // Work around https://github.com/google/ExoPlayer/issues/398. - if (Util.SDK_INT < 18 && "OMX.SEC.MP3.Decoder".equals(name)) { - return false; - } - - // Work around https://github.com/google/ExoPlayer/issues/4519. - if ("OMX.SEC.mp3.dec".equals(name) - && (Util.MODEL.startsWith("GT-I9152") - || Util.MODEL.startsWith("GT-I9515") - || Util.MODEL.startsWith("GT-P5220") - || Util.MODEL.startsWith("GT-S7580") - || Util.MODEL.startsWith("SM-G350") - || Util.MODEL.startsWith("SM-G386") - || Util.MODEL.startsWith("SM-T231") - || Util.MODEL.startsWith("SM-T530") - || Util.MODEL.startsWith("SCH-I535") - || Util.MODEL.startsWith("SPH-L710"))) { - return false; - } - if ("OMX.brcm.audio.mp3.decoder".equals(name) - && (Util.MODEL.startsWith("GT-I9152") - || Util.MODEL.startsWith("GT-S7580") - || Util.MODEL.startsWith("SM-G350"))) { - return false; - } - // Work around https://github.com/google/ExoPlayer/issues/1528 and // https://github.com/google/ExoPlayer/issues/3171. if (Util.SDK_INT < 18 && "OMX.MTK.AUDIO.DECODER.AAC".equals(name) @@ -423,7 +395,18 @@ public final class MediaCodecUtil { */ private static void applyWorkarounds(String mimeType, List decoderInfos) { if (MimeTypes.AUDIO_RAW.equals(mimeType)) { - Collections.sort(decoderInfos, RAW_AUDIO_CODEC_COMPARATOR); + Collections.sort(decoderInfos, new RawAudioCodecComparator()); + } else if (Util.SDK_INT < 21 && decoderInfos.size() > 1) { + String firstCodecName = decoderInfos.get(0).name; + if ("OMX.SEC.mp3.dec".equals(firstCodecName) + || "OMX.SEC.MP3.Decoder".equals(firstCodecName) + || "OMX.brcm.audio.mp3.decoder".equals(firstCodecName)) { + // Prefer OMX.google codecs over OMX.SEC.mp3.dec, OMX.SEC.MP3.Decoder and + // OMX.brcm.audio.mp3.decoder on older devices. See: + // https://github.com/google/ExoPlayer/issues/398 and + // https://github.com/google/ExoPlayer/issues/4519. + Collections.sort(decoderInfos, new PreferOmxGoogleCodecComparator()); + } } } @@ -729,6 +712,18 @@ public final class MediaCodecUtil { } } + /** Comparator for preferring OMX.google media codecs. */ + private static final class PreferOmxGoogleCodecComparator implements Comparator { + @Override + public int compare(MediaCodecInfo a, MediaCodecInfo b) { + return scoreMediaCodecInfo(a) - scoreMediaCodecInfo(b); + } + + private static int scoreMediaCodecInfo(MediaCodecInfo mediaCodecInfo) { + return mediaCodecInfo.name.startsWith("OMX.google") ? -1 : 0; + } + } + static { AVC_PROFILE_NUMBER_TO_CONST = new SparseIntArray(); AVC_PROFILE_NUMBER_TO_CONST.put(66, CodecProfileLevel.AVCProfileBaseline); From fb99c26426b2f5b82b8528f52f6890c7ed2ef23b Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Feb 2019 16:48:34 +0000 Subject: [PATCH 091/110] Implement CacheContentIndex storage switching This change enables transitioning to/from different Storage implementations, to allow experimentally enabling (and if necessary, disabling) SQLiteStorage. All that's left to do is the final wiring to turn it on PiperOrigin-RevId: 232304458 --- .../upstream/cache/CachedContentIndex.java | 161 +++++++++++++++--- .../upstream/cache/SimpleCache.java | 1 + .../android/exoplayer2/util/AtomicFile.java | 5 + 3 files changed, 142 insertions(+), 25 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index e31c60e752..0888290e1b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -94,7 +94,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; */ private final SparseBooleanArray removedIds; - private final Storage storage; + private Storage storage; + @Nullable private Storage previousStorage; /** * Returns whether the file is an index file, or an auxiliary file associated with an index file @@ -150,29 +151,42 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; idToKey = new SparseArray<>(); removedIds = new SparseBooleanArray(); Random random = new Random(); - storage = + Storage atomicFileStorage = new AtomicFileStorage( new File(cacheDir, FILE_NAME_ATOMIC), random, encrypt, cipher, secretKeySpec); - // storage = + // Storage sqliteStorage = // new SQLiteStorage( - // new File(cacheDir, FILE_NAME_DATABASE), - // random, - // encrypt, - // cipher, - // secretKeySpec); + // new File(cacheDir, FILE_NAME_DATABASE), random, encrypt, cipher, secretKeySpec); + storage = atomicFileStorage; + previousStorage = null; } /** Loads the index file. */ public void load() { - if (!storage.load(keyToContent, idToKey)) { - keyToContent.clear(); - idToKey.clear(); + if (!storage.exists() && previousStorage != null && previousStorage.exists()) { + // Copy from previous storage into current storage. + loadFrom(previousStorage); + try { + storage.storeFully(keyToContent); + } catch (CacheException e) { + // We failed to copy into current storage, so keep using previous storage. + storage.release(); + storage = previousStorage; + previousStorage = null; + } + } else { + // Load from the current storage. + loadFrom(storage); + } + if (previousStorage != null) { + previousStorage.release(/* delete= */ true); + previousStorage = null; } } /** Stores the index data to index file if there is a change. */ public void store() throws CacheException { - storage.store(keyToContent); + storage.storeIncremental(keyToContent); // Make ids that were removed since the index was last stored eligible for re-use. int removedIdCount = removedIds.size(); for (int i = 0; i < removedIdCount; i++) { @@ -181,6 +195,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; removedIds.clear(); } + /** Releases any underlying resources. */ + public void release() { + storage.release(); + if (previousStorage != null) { + previousStorage.release(); + } + } + /** * Adds the given key to the index if it isn't there already. * @@ -267,6 +289,14 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return cachedContent != null ? cachedContent.getMetadata() : DefaultContentMetadata.EMPTY; } + /** Loads the index from the specified storage. */ + private void loadFrom(Storage storage) { + if (!storage.load(keyToContent, idToKey)) { + keyToContent.clear(); + idToKey.clear(); + } + } + private CachedContent addNew(String key) { int id = getNewId(idToKey); CachedContent cachedContent = new CachedContent(id, key); @@ -363,8 +393,20 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /** Interface for the persistent index. */ private interface Storage { + /** Returns whether the persisted index exists. */ + boolean exists(); + + /** Releases any held resources. */ + default void release() { + release(/* delete= */ false); + } + + /** Releases and held resources and optionally deletes the persisted index. */ + void release(boolean delete); + /** - * Loads the persisted index into {@code content} and {@code idToKey}. + * Loads the persisted index into {@code content} and {@code idToKey}, creating it if it doesn't + * already exist. * * @param content The key to content map to populate with persisted data. * @param idToKey The id to key map to populate with persisted data. @@ -373,22 +415,33 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; boolean load(HashMap content, SparseArray<@NullableType String> idToKey); /** - * Ensures all changes in the in-memory table are persisted. + * Writes the persisted index, creating it if it doesn't already exist and replacing any + * existing content if it does. * * @param content The key to content map to persist. * @throws CacheException If an error occurs persisting the index. */ - void store(HashMap content) throws CacheException; + void storeFully(HashMap content) throws CacheException; /** - * Called when a {@link CachedContent} is added or updated in the in-memory index. + * Ensures incremental changes to the index since the last {@link #load()} or {@link + * #storeFully(HashMap)} are persisted. The storage will have been notified of all such changes + * via {@link #onUpdate(CachedContent)} and {@link #onRemove(CachedContent)}. + * + * @param content The key to content map to persist. + * @throws CacheException If an error occurs persisting the index. + */ + void storeIncremental(HashMap content) throws CacheException; + + /** + * Called when a {@link CachedContent} is added or updated. * * @param cachedContent The updated {@link CachedContent}. */ void onUpdate(CachedContent cachedContent); /** - * Called when a {@link CachedContent} is removed from the in-memory index. + * Called when a {@link CachedContent} is removed. * * @param cachedContent The removed {@link CachedContent}. */ @@ -420,6 +473,18 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; atomicFile = new AtomicFile(file); } + @Override + public boolean exists() { + return atomicFile.exists(); + } + + @Override + public void release(boolean delete) { + if (delete) { + atomicFile.delete(); + } + } + @Override public boolean load( HashMap content, SparseArray<@NullableType String> idToKey) { @@ -432,12 +497,17 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } @Override - public void store(HashMap content) throws CacheException { + public void storeFully(HashMap content) throws CacheException { + writeFile(content); + changed = false; + } + + @Override + public void storeIncremental(HashMap content) throws CacheException { if (!changed) { return; } - writeFile(content); - changed = false; + storeFully(content); } @Override @@ -637,11 +707,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; private static final int FLAG_ENCRYPTED = 1; + private final File file; private final Random random; private final boolean encrypt; @Nullable private final Cipher cipher; @Nullable private final SecretKeySpec secretKeySpec; - private final DatabaseProvider databaseProvider; + private final ExoDatabaseProvider databaseProvider; private final SparseArray pendingUpdates; @Nullable private ReusableBufferedOutputStream bufferedOutputStream; @@ -652,6 +723,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; boolean encrypt, @Nullable Cipher cipher, @Nullable SecretKeySpec secretKeySpec) { + this.file = file; this.random = random; this.encrypt = encrypt; this.cipher = cipher; @@ -660,9 +732,26 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; pendingUpdates = new SparseArray<>(); } + @Override + public boolean exists() { + return file.exists() + && VersionTable.getVersion( + databaseProvider.getReadableDatabase(), VersionTable.FEATURE_CACHE) + != VersionTable.VERSION_UNSET; + } + + @Override + public void release(boolean delete) { + release(); + if (delete) { + SQLiteDatabase.deleteDatabase(file); + } + } + @Override public boolean load( HashMap content, SparseArray<@NullableType String> idToKey) { + Assertions.checkState(pendingUpdates.size() == 0); try { int version = VersionTable.getVersion( @@ -671,9 +760,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); writableDatabase.beginTransaction(); try { - writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS); - writableDatabase.execSQL(SQL_CREATE_TABLE); - VersionTable.setVersion(writableDatabase, VersionTable.FEATURE_CACHE, TABLE_VERSION); + initializeTable(writableDatabase); writableDatabase.setTransactionSuccessful(); } finally { writableDatabase.endTransaction(); @@ -717,7 +804,25 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } @Override - public void store(HashMap content) throws CacheException { + public void storeFully(HashMap content) throws CacheException { + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.beginTransaction(); + try { + initializeTable(writableDatabase); + for (CachedContent cachedContent : content.values()) { + addOrUpdateRow(writableDatabase, cachedContent); + } + writableDatabase.setTransactionSuccessful(); + pendingUpdates.clear(); + } catch (IOException | SQLiteException e) { + throw new CacheException(e); + } finally { + writableDatabase.endTransaction(); + } + } + + @Override + public void storeIncremental(HashMap content) throws CacheException { if (pendingUpdates.size() == 0) { return; } @@ -764,6 +869,12 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; /* orderBy= */ null); } + private void initializeTable(SQLiteDatabase writableDatabase) { + VersionTable.setVersion(writableDatabase, VersionTable.FEATURE_CACHE, TABLE_VERSION); + writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS); + writableDatabase.execSQL(SQL_CREATE_TABLE); + } + private void deleteRow(SQLiteDatabase writableDatabase, int key) { String[] selectionArgs = {Integer.toString(key)}; writableDatabase.delete(TABLE_NAME, COLUMN_SELECTION_ID, selectionArgs); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index 75236ad9c7..d2091c362a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -157,6 +157,7 @@ public final class SimpleCache implements Cache { } catch (CacheException e) { Log.e(TAG, "Storing index file failed", e); } finally { + index.release(); unlockFolder(cacheDir); released = true; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java b/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java index 2466d5a049..b7b6c05d82 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/AtomicFile.java @@ -52,6 +52,11 @@ public final class AtomicFile { backupName = new File(baseName.getPath() + ".bak"); } + /** Whether the file or its backup exists. */ + public boolean exists() { + return baseName.exists() || backupName.exists(); + } + /** Delete the atomic file. This deletes both the base and backup files. */ public void delete() { baseName.delete(); From a5d64463c781092f60664d9b86d9e0867dec0561 Mon Sep 17 00:00:00 2001 From: eguven Date: Mon, 4 Feb 2019 17:03:12 +0000 Subject: [PATCH 092/110] Make DownloadTracker use DownloadIndex DownloadTracker will stop updating DownloadIndex when DownloadManager starts using the same DownloadIndex. PiperOrigin-RevId: 232306803 --- .../exoplayer2/demo/DemoApplication.java | 22 ++++++-- .../exoplayer2/demo/DownloadTracker.java | 51 ++++++++++--------- .../exoplayer2/offline/DownloadIndexUtil.java | 24 +-------- .../exoplayer2/offline/DownloadState.java | 28 ++++++++++ .../offline/DownloadStateCursor.java | 2 +- 5 files changed, 74 insertions(+), 53 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java index 560a9be58a..27033bb03e 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoApplication.java @@ -18,7 +18,11 @@ package com.google.android.exoplayer2.demo; import android.app.Application; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.database.ExoDatabaseProvider; +import com.google.android.exoplayer2.offline.ActionFile; +import com.google.android.exoplayer2.offline.DefaultDownloadIndex; import com.google.android.exoplayer2.offline.DefaultDownloaderFactory; +import com.google.android.exoplayer2.offline.DownloadIndexUtil; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloaderConstructorHelper; import com.google.android.exoplayer2.upstream.DataSource; @@ -31,14 +35,17 @@ import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.CacheDataSourceFactory; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.SimpleCache; +import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.io.File; +import java.io.IOException; /** * Placeholder application to facilitate overriding Application methods for debugging and testing. */ public class DemoApplication extends Application { + private static final String TAG = "DemoApplication"; private static final String DOWNLOAD_ACTION_FILE = "actions"; private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions"; private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; @@ -97,6 +104,16 @@ public class DemoApplication extends Application { private synchronized void initDownloadManager() { if (downloadManager == null) { + DefaultDownloadIndex downloadIndex = new DefaultDownloadIndex(new ExoDatabaseProvider(this)); + File actionFile = new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE); + if (actionFile.exists()) { + try { + DownloadIndexUtil.upgradeActionFile(new ActionFile(actionFile), downloadIndex, null); + } catch (IOException e) { + Log.e(TAG, "Upgrading action file failed", e); + } + actionFile.delete(); + } DownloaderConstructorHelper downloaderConstructorHelper = new DownloaderConstructorHelper(getDownloadCache(), buildHttpDataSourceFactory()); downloadManager = @@ -108,10 +125,7 @@ public class DemoApplication extends Application { DownloadManager.DEFAULT_MIN_RETRY_COUNT, DownloadManager.DEFAULT_REQUIREMENTS); downloadTracker = - new DownloadTracker( - /* context= */ this, - buildDataSourceFactory(), - new File(getDownloadDirectory(), DOWNLOAD_TRACKER_ACTION_FILE)); + new DownloadTracker(/* context= */ this, buildDataSourceFactory(), downloadIndex); downloadManager.addListener(downloadTracker); } } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java index 83c8a76812..689e7241f7 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java @@ -34,11 +34,13 @@ import android.widget.Toast; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.offline.ActionFile; +import com.google.android.exoplayer2.offline.DefaultDownloadIndex; import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadHelper; import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadState; +import com.google.android.exoplayer2.offline.DownloadStateCursor; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.scheduler.Requirements; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -51,8 +53,8 @@ import com.google.android.exoplayer2.ui.TrackSelectionView; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; -import java.io.File; import java.io.IOException; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -80,20 +82,21 @@ public class DownloadTracker implements DownloadManager.Listener { private final DataSource.Factory dataSourceFactory; private final TrackNameProvider trackNameProvider; private final CopyOnWriteArraySet listeners; - private final HashMap trackedDownloadStates; - private final ActionFile actionFile; - private final Handler actionFileWriteHandler; + private final HashMap trackedDownloadStates; + private final DefaultDownloadIndex downloadIndex; + private final Handler actionFileIOHandler; - public DownloadTracker(Context context, DataSource.Factory dataSourceFactory, File actionFile) { + public DownloadTracker( + Context context, DataSource.Factory dataSourceFactory, DefaultDownloadIndex downloadIndex) { this.context = context.getApplicationContext(); this.dataSourceFactory = dataSourceFactory; - this.actionFile = new ActionFile(actionFile); + this.downloadIndex = downloadIndex; trackNameProvider = new DefaultTrackNameProvider(context.getResources()); listeners = new CopyOnWriteArraySet<>(); trackedDownloadStates = new HashMap<>(); HandlerThread actionFileWriteThread = new HandlerThread("DownloadTracker"); actionFileWriteThread.start(); - actionFileWriteHandler = new Handler(actionFileWriteThread.getLooper()); + actionFileIOHandler = new Handler(actionFileWriteThread.getLooper()); loadTrackedActions(); } @@ -114,7 +117,7 @@ public class DownloadTracker implements DownloadManager.Listener { if (!trackedDownloadStates.containsKey(uri)) { return Collections.emptyList(); } - return trackedDownloadStates.get(uri).getKeys(); + return Arrays.asList(trackedDownloadStates.get(uri).streamKeys); } public void toggleDownload( @@ -146,7 +149,7 @@ public class DownloadTracker implements DownloadManager.Listener { || downloadState.state == DownloadState.STATE_FAILED) { // A download has been removed, or has failed. Stop tracking it. if (trackedDownloadStates.remove(downloadState.uri) != null) { - handleTrackedDownloadStatesChanged(); + handleTrackedDownloadStateChanged(downloadState); } } } @@ -167,27 +170,24 @@ public class DownloadTracker implements DownloadManager.Listener { // Internal methods private void loadTrackedActions() { - try { - DownloadAction[] allActions = actionFile.load(); - for (DownloadAction action : allActions) { - trackedDownloadStates.put(action.uri, action); - } - } catch (IOException e) { - Log.e(TAG, "Failed to load tracked actions", e); + DownloadStateCursor downloadStates = downloadIndex.getDownloadStates(); + while (downloadStates.moveToNext()) { + DownloadState downloadState = downloadStates.getDownloadState(); + trackedDownloadStates.put(downloadState.uri, downloadState); } + downloadStates.close(); } - private void handleTrackedDownloadStatesChanged() { + private void handleTrackedDownloadStateChanged(DownloadState downloadState) { for (Listener listener : listeners) { listener.onDownloadsChanged(); } - final DownloadAction[] actions = trackedDownloadStates.values().toArray(new DownloadAction[0]); - actionFileWriteHandler.post( + actionFileIOHandler.post( () -> { - try { - actionFile.store(actions); - } catch (IOException e) { - Log.e(TAG, "Failed to store tracked actions", e); + if (downloadState.state == DownloadState.STATE_REMOVED) { + downloadIndex.removeDownloadState(downloadState.id); + } else { + downloadIndex.putDownloadState(downloadState); } }); } @@ -197,8 +197,9 @@ public class DownloadTracker implements DownloadManager.Listener { // This content is already being downloaded. Do nothing. return; } - trackedDownloadStates.put(action.uri, action); - handleTrackedDownloadStatesChanged(); + DownloadState downloadState = new DownloadState(action); + trackedDownloadStates.put(downloadState.uri, downloadState); + handleTrackedDownloadStateChanged(downloadState); startServiceWithAction(action); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndexUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndexUtil.java index e0c914408b..0d6d2a36b5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndexUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndexUtil.java @@ -81,7 +81,7 @@ public final class DownloadIndexUtil { if (downloadState != null) { downloadState = merge(downloadState, action); } else { - downloadState = convert(action); + downloadState = new DownloadState(action); } downloadIndex.putDownloadState(downloadState); } @@ -121,26 +121,4 @@ public final class DownloadIndexUtil { newKeys, action.data); } - - private static DownloadState convert(DownloadAction action) { - long currentTimeMs = System.currentTimeMillis(); - return new DownloadState( - action.id, - action.type, - action.uri, - action.customCacheKey, - /* state= */ action.isRemoveAction - ? DownloadState.STATE_REMOVING - : DownloadState.STATE_QUEUED, - /* downloadPercentage= */ C.PERCENTAGE_UNSET, - /* downloadedBytes= */ 0, - /* totalBytes= */ C.LENGTH_UNSET, - DownloadState.FAILURE_REASON_NONE, - /* stopFlags= */ 0, - /* notMetRequirements= */ 0, - /* startTimeMs= */ currentTimeMs, - /* updateTimeMs= */ currentTimeMs, - action.keys.toArray(new StreamKey[0]), - action.data); - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java index f5b3287b32..c59b7b87b6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java @@ -164,6 +164,34 @@ public final class DownloadState { /** Not met requirements to download. */ @Requirements.RequirementFlags public final int notMetRequirements; + /** + * Creates a {@link DownloadState} using a {@link DownloadAction}. + * + * @param action The {@link DownloadAction}. + */ + public DownloadState(DownloadAction action) { + this(action, System.currentTimeMillis()); + } + + private DownloadState(DownloadAction action, long currentTimeMs) { + this( + action.id, + action.type, + action.uri, + action.customCacheKey, + /* state= */ action.isRemoveAction ? STATE_REMOVING : STATE_QUEUED, + /* downloadPercentage= */ C.PERCENTAGE_UNSET, + /* downloadedBytes= */ 0, + /* totalBytes= */ C.LENGTH_UNSET, + FAILURE_REASON_NONE, + /* stopFlags= */ 0, + /* notMetRequirements= */ 0, + /* startTimeMs= */ currentTimeMs, + /* updateTimeMs= */ currentTimeMs, + action.keys.toArray(new StreamKey[0]), + action.data); + } + /* package */ DownloadState( String id, String type, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadStateCursor.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadStateCursor.java index 680976c77b..06511c8930 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadStateCursor.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadStateCursor.java @@ -16,7 +16,7 @@ package com.google.android.exoplayer2.offline; /** Provides random read-write access to the result set returned by a database query. */ -interface DownloadStateCursor { +public interface DownloadStateCursor { /** Returns the DownloadState at the current position. */ DownloadState getDownloadState(); From cd536a73b19f41d25846ae6cbc57443547043a71 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 4 Feb 2019 17:39:35 +0000 Subject: [PATCH 093/110] Add missing removeCallbacksAndMessages to RobolectricUtil. The Util class already handles removeMessages calls but so far ignored calls to removeCallbacksAndMessages. PiperOrigin-RevId: 232312458 --- .../exoplayer2/testutil/RobolectricUtil.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java index dc7781fd90..1e7f09bacf 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/RobolectricUtil.java @@ -37,6 +37,7 @@ import org.robolectric.shadows.ShadowMessageQueue; public final class RobolectricUtil { private static final AtomicLong sequenceNumberGenerator = new AtomicLong(0); + private static final int ANY_MESSAGE = Integer.MIN_VALUE; private RobolectricUtil() {} @@ -110,7 +111,8 @@ public final class RobolectricUtil { boolean isRemoved = false; for (RemovedMessage removedMessage : removedMessages) { if (removedMessage.handler == target - && removedMessage.what == pendingMessage.message.what + && (removedMessage.what == ANY_MESSAGE + || removedMessage.what == pendingMessage.message.what) && (removedMessage.object == null || removedMessage.object == pendingMessage.message.obj) && pendingMessage.sequenceNumber < removedMessage.sequenceNumber) { @@ -179,6 +181,15 @@ public final class RobolectricUtil { ((CustomLooper) shadowOf(looper)).removeMessages(handler, what, object); } } + + @Implementation + public void removeCallbacksAndMessages(Handler handler, Object object) { + Looper looper = ShadowLooper.getLooperForThread(looperThread); + if (shadowOf(looper) instanceof CustomLooper + && shadowOf(looper) != ShadowLooper.getShadowMainLooper()) { + ((CustomLooper) shadowOf(looper)).removeMessages(handler, ANY_MESSAGE, object); + } + } } private static final class PendingMessage implements Comparable { From 7a4cf96f4a91a51c42a83e0ba70eed6a2d5d9199 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 4 Feb 2019 17:40:01 +0000 Subject: [PATCH 094/110] Improve housekeeping of ConcatenatingMediaSource callbacks. When calling releaseSource(), all pending messages will be removed. That means that all action-on-completion callbacks which are somewhere in flight are just dropped without being called. This change adds code to keep track of the current state of each callback to allow all of them being called when the source is released. Issue:#5464 PiperOrigin-RevId: 232312528 --- RELEASENOTES.md | 2 + .../source/ConcatenatingMediaSource.java | 266 +++++++++++------- .../source/ConcatenatingMediaSourceTest.java | 40 ++- 3 files changed, 192 insertions(+), 116 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 98f8e85857..9ec6015240 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -58,6 +58,8 @@ * OkHttp extension: Upgrade OkHttp dependency to 3.12.1. * MP3: Wider fix for issue where streams would play twice on some Samsung devices ([#4519](https://github.com/google/ExoPlayer/issues/4519)). +* Fix issue with dropped messages when releasing a `ConcatenatingMediaSource` + ([#5464](https://github.com/google/ExoPlayer/issues/5464)). ### 2.9.4 ### diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index 6dc7a0a327..d223f653e1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -28,7 +28,6 @@ import com.google.android.exoplayer2.source.ShuffleOrder.DefaultShuffleOrder; import com.google.android.exoplayer2.upstream.Allocator; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.EventDispatcher; import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.ArrayList; @@ -36,9 +35,11 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import java.util.Set; /** * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified @@ -51,12 +52,19 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSourcesPublic; - @Nullable private Handler playbackThreadHandler; + + @GuardedBy("this") + private final Set pendingOnCompletionActions; + + @GuardedBy("this") + @Nullable + private Handler playbackThreadHandler; // Accessed on the playback thread only. private final List mediaSourceHolders; @@ -67,8 +75,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource pendingOnCompletionActions; + private boolean timelineUpdateScheduled; + private Set nextTimelineUpdateOnCompletionActions; private ShuffleOrder shuffleOrder; private int windowCount; private int periodCount; @@ -127,7 +135,8 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(); this.mediaSourcesPublic = new ArrayList<>(); this.mediaSourceHolders = new ArrayList<>(); - this.pendingOnCompletionActions = new EventDispatcher<>(); + this.nextTimelineUpdateOnCompletionActions = new HashSet<>(); + this.pendingOnCompletionActions = new HashSet<>(); this.isAtomic = isAtomic; this.useLazyPreparation = useLazyPreparation; window = new Timeline.Window(); @@ -148,13 +157,13 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources, Handler handler, Runnable actionOnCompletion) { - addPublicMediaSources(mediaSourcesPublic.size(), mediaSources, handler, actionOnCompletion); + Collection mediaSources, Handler handler, Runnable onCompletionAction) { + addPublicMediaSources(mediaSourcesPublic.size(), mediaSources, handler, onCompletionAction); } /** @@ -226,7 +235,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources) { - addPublicMediaSources(index, mediaSources, /* handler= */ null, /* actionOnCompletion= */ null); + addPublicMediaSources(index, mediaSources, /* handler= */ null, /* onCompletionAction= */ null); } /** @@ -236,16 +245,16 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources, Handler handler, - Runnable actionOnCompletion) { - addPublicMediaSources(index, mediaSources, handler, actionOnCompletion); + Runnable onCompletionAction) { + addPublicMediaSources(index, mediaSources, handler, onCompletionAction); } /** @@ -261,7 +270,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource mediaSources, @Nullable Handler handler, - @Nullable Runnable actionOnCompletion) { - Assertions.checkArgument((handler == null) == (actionOnCompletion == null)); + @Nullable Runnable onCompletionAction) { + Assertions.checkArgument((handler == null) == (onCompletionAction == null)); + Handler playbackThreadHandler = this.playbackThreadHandler; for (MediaSource mediaSource : mediaSources) { Assertions.checkNotNull(mediaSource); } @@ -532,12 +545,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(index, mediaSourceHolders, handler, actionOnCompletion)) + .obtainMessage(MSG_ADD, new MessageData<>(index, mediaSourceHolders, callbackAction)) .sendToTarget(); - } else if (actionOnCompletion != null && handler != null) { - handler.post(actionOnCompletion); + } else if (onCompletionAction != null && handler != null) { + handler.post(onCompletionAction); } } @@ -546,16 +559,17 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(fromIndex, toIndex, handler, actionOnCompletion)) + .obtainMessage(MSG_REMOVE, new MessageData<>(fromIndex, toIndex, callbackAction)) .sendToTarget(); - } else if (actionOnCompletion != null && handler != null) { - handler.post(actionOnCompletion); + } else if (onCompletionAction != null && handler != null) { + handler.post(onCompletionAction); } } @@ -564,23 +578,24 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(currentIndex, newIndex, handler, actionOnCompletion)) + .obtainMessage(MSG_MOVE, new MessageData<>(currentIndex, newIndex, callbackAction)) .sendToTarget(); - } else if (actionOnCompletion != null && handler != null) { - handler.post(actionOnCompletion); + } else if (onCompletionAction != null && handler != null) { + handler.post(onCompletionAction); } } @GuardedBy("this") private void setPublicShuffleOrder( - ShuffleOrder shuffleOrder, @Nullable Handler handler, @Nullable Runnable actionOnCompletion) { - Assertions.checkArgument((handler == null) == (actionOnCompletion == null)); + ShuffleOrder shuffleOrder, @Nullable Handler handler, @Nullable Runnable onCompletionAction) { + Assertions.checkArgument((handler == null) == (onCompletionAction == null)); Handler playbackThreadHandler = this.playbackThreadHandler; if (playbackThreadHandler != null) { int size = getSize(); @@ -590,20 +605,33 @@ public class ConcatenatingMediaSource extends CompositeMediaSource(/* index= */ 0, shuffleOrder, handler, actionOnCompletion)) + new MessageData<>(/* index= */ 0, shuffleOrder, callbackAction)) .sendToTarget(); } else { this.shuffleOrder = shuffleOrder.getLength() > 0 ? shuffleOrder.cloneAndClear() : shuffleOrder; - if (actionOnCompletion != null && handler != null) { - handler.post(actionOnCompletion); + if (onCompletionAction != null && handler != null) { + handler.post(onCompletionAction); } } } + @GuardedBy("this") + @Nullable + private HandlerAndRunnable createOnCompletionAction( + @Nullable Handler handler, @Nullable Runnable runnable) { + if (handler == null || runnable == null) { + return null; + } + HandlerAndRunnable handlerAndRunnable = new HandlerAndRunnable(handler, runnable); + pendingOnCompletionActions.add(handlerAndRunnable); + return handlerAndRunnable; + } + // Internal methods. Called on the playback thread. @SuppressWarnings("unchecked") @@ -614,7 +642,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource>) Util.castNonNull(msg.obj); shuffleOrder = shuffleOrder.cloneAndInsert(addMessage.index, addMessage.customData.size()); addMediaSourcesInternal(addMessage.index, addMessage.customData); - scheduleListenerNotification(addMessage.handler, addMessage.actionOnCompletion); + scheduleTimelineUpdate(addMessage.onCompletionAction); break; case MSG_REMOVE: MessageData removeMessage = (MessageData) Util.castNonNull(msg.obj); @@ -628,29 +656,27 @@ public class ConcatenatingMediaSource extends CompositeMediaSource= fromIndex; index--) { removeMediaSourceInternal(index); } - scheduleListenerNotification(removeMessage.handler, removeMessage.actionOnCompletion); + scheduleTimelineUpdate(removeMessage.onCompletionAction); break; case MSG_MOVE: MessageData moveMessage = (MessageData) Util.castNonNull(msg.obj); shuffleOrder = shuffleOrder.cloneAndRemove(moveMessage.index, moveMessage.index + 1); shuffleOrder = shuffleOrder.cloneAndInsert(moveMessage.customData, 1); moveMediaSourceInternal(moveMessage.index, moveMessage.customData); - scheduleListenerNotification(moveMessage.handler, moveMessage.actionOnCompletion); + scheduleTimelineUpdate(moveMessage.onCompletionAction); break; case MSG_SET_SHUFFLE_ORDER: MessageData shuffleOrderMessage = (MessageData) Util.castNonNull(msg.obj); shuffleOrder = shuffleOrderMessage.customData; - scheduleListenerNotification( - shuffleOrderMessage.handler, shuffleOrderMessage.actionOnCompletion); + scheduleTimelineUpdate(shuffleOrderMessage.onCompletionAction); break; - case MSG_NOTIFY_LISTENER: - notifyListener(); + case MSG_UPDATE_TIMELINE: + updateTimelineAndScheduleOnCompletionActions(); break; case MSG_ON_COMPLETION: - EventDispatcher actionsOnCompletion = - (EventDispatcher) Util.castNonNull(msg.obj); - actionsOnCompletion.dispatch(Runnable::run); + Set actions = (Set) Util.castNonNull(msg.obj); + dispatchOnCompletionActions(actions); break; default: throw new IllegalStateException(); @@ -658,36 +684,48 @@ public class ConcatenatingMediaSource extends CompositeMediaSource actionsOnCompletion = pendingOnCompletionActions; - pendingOnCompletionActions = new EventDispatcher<>(); + private void updateTimelineAndScheduleOnCompletionActions() { + timelineUpdateScheduled = false; + Set onCompletionActions = nextTimelineUpdateOnCompletionActions; + nextTimelineUpdateOnCompletionActions = new HashSet<>(); refreshSourceInfo( new ConcatenatedTimeline( mediaSourceHolders, windowCount, periodCount, shuffleOrder, isAtomic), /* manifest= */ null); - Assertions.checkNotNull(playbackThreadHandler) - .obtainMessage(MSG_ON_COMPLETION, actionsOnCompletion) + getPlaybackThreadHandlerOnPlaybackThread() + .obtainMessage(MSG_ON_COMPLETION, onCompletionActions) .sendToTarget(); } + @SuppressWarnings("GuardedBy") + private Handler getPlaybackThreadHandlerOnPlaybackThread() { + // Write access to this value happens on the playback thread only, so playback thread reads + // don't need to be synchronized. + return Assertions.checkNotNull(playbackThreadHandler); + } + + private synchronized void dispatchOnCompletionActions( + Set onCompletionActions) { + for (HandlerAndRunnable pendingAction : onCompletionActions) { + pendingAction.dispatch(); + } + pendingOnCompletionActions.removeAll(onCompletionActions); + } + private void addMediaSourcesInternal( int index, Collection mediaSourceHolders) { for (MediaSourceHolder mediaSourceHolder : mediaSourceHolders) { @@ -784,7 +822,7 @@ public class ConcatenatingMediaSource extends CompositeMediaSource { + SourceInfoRefreshListener listener = mock(SourceInfoRefreshListener.class); + mediaSource.addMediaSources(Arrays.asList(createMediaSources(2))); + mediaSource.prepareSource(listener, /* mediaTransferListener= */ null); + mediaSource.moveMediaSource( + /* currentIndex= */ 0, + /* newIndex= */ 1, + new Handler(), + callbackCalledCondition::open); + mediaSource.releaseSource(listener); + }); + assertThat(callbackCalledCondition.block(MediaSourceTestRunner.TIMEOUT_MS)).isTrue(); + } finally { + dummyMainThread.release(); + } + } + @Test public void testPeriodCreationWithAds() throws IOException, InterruptedException { // Create concatenated media source with ad child source. @@ -973,7 +997,7 @@ public final class ConcatenatingMediaSourceTest { @Test public void testCustomCallbackBeforePreparationSetShuffleOrder() throws Exception { - Runnable runnable = Mockito.mock(Runnable.class); + Runnable runnable = mock(Runnable.class); mediaSource.setShuffleOrder( new ShuffleOrder.UnshuffledShuffleOrder(/* length= */ 0), new Handler(), runnable); From 3818d7329d19cdcd2d87bce812794a4ca77c9d30 Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 4 Feb 2019 18:26:40 +0000 Subject: [PATCH 095/110] Use ExoPlayer's AnalyticsListener and AnalyticsCollector. This combines all the different listeners in ExoVideosPlayerV2 into one and moves the PlaybackLogger class towards ExoPlayer's AnalyticsCollector with the same purpose. In the future this allows two things: 1. Gradually move LogginClient implementations to AnalyticsListener to prevent custom event forwarding. 2. Using ExoPlayer's QoE extension (which is also an AnalyticsListener). PiperOrigin-RevId: 232321182 --- .../android/exoplayer2/analytics/AnalyticsCollector.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java index 55031e2d12..154cc11dca 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -129,12 +129,13 @@ public class AnalyticsCollector /** * Sets the player for which data will be collected. Must only be called if no player has been set - * yet. + * yet or the current player is idle. * * @param player The {@link Player} for which data will be collected. */ public void setPlayer(Player player) { - Assertions.checkState(this.player == null); + Assertions.checkState( + this.player == null || mediaPeriodQueueTracker.mediaPeriodInfoQueue.isEmpty()); this.player = Assertions.checkNotNull(player); } From 67be9e77834c0dc9d2b5d462c01c2f39718c8817 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 4 Feb 2019 19:35:06 +0000 Subject: [PATCH 096/110] Merge #5462: Making easier to set the playClearSampleWithoutKeys to renderers Imported from GitHub PR https://github.com/google/ExoPlayer/pull/5462 Pull request for the following issue: #5421 Merge d9d88b079c4ca0533a836b2715a65b924babbb89 into a73819162751116acd3863cf5473b0ff78fac805 PiperOrigin-RevId: 232335113 --- .../exoplayer2/DefaultRenderersFactory.java | 197 +++++++++++++----- .../android/exoplayer2/ExoPlayerFactory.java | 7 +- .../testutil/DebugRenderersFactory.java | 56 +++-- 3 files changed, 195 insertions(+), 65 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java index bb73353b94..ef0a008849 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2; import android.content.Context; +import android.media.MediaCodec; import android.os.Handler; import android.os.Looper; import android.support.annotation.IntDef; @@ -85,15 +86,18 @@ public class DefaultRenderersFactory implements RenderersFactory { protected static final int MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY = 50; private final Context context; - private final @Nullable DrmSessionManager drmSessionManager; - private final @ExtensionRendererMode int extensionRendererMode; - private final long allowedVideoJoiningTimeMs; + @Nullable private DrmSessionManager drmSessionManager; + @ExtensionRendererMode private int extensionRendererMode; + private long allowedVideoJoiningTimeMs; + private boolean playClearSamplesWithoutKeys; + private MediaCodecSelector mediaCodecSelector; - /** - * @param context A {@link Context}. - */ + /** @param context A {@link Context}. */ public DefaultRenderersFactory(Context context) { - this(context, EXTENSION_RENDERER_MODE_OFF); + this.context = context; + extensionRendererMode = EXTENSION_RENDERER_MODE_OFF; + allowedVideoJoiningTimeMs = DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS; + mediaCodecSelector = MediaCodecSelector.DEFAULT; } /** @@ -108,19 +112,20 @@ public class DefaultRenderersFactory implements RenderersFactory { } /** - * @param context A {@link Context}. - * @param extensionRendererMode The extension renderer mode, which determines if and how available - * extension renderers are used. Note that extensions must be included in the application - * build for them to be considered available. + * @deprecated Use {@link #DefaultRenderersFactory(Context)} and {@link + * #setExtensionRendererMode(int)}. */ + @Deprecated + @SuppressWarnings("deprecation") public DefaultRenderersFactory( Context context, @ExtensionRendererMode int extensionRendererMode) { this(context, extensionRendererMode, DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS); } /** - * @deprecated Use {@link #DefaultRenderersFactory(Context, int)} and pass {@link - * DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}. + * @deprecated Use {@link #DefaultRenderersFactory(Context)} and {@link + * #setExtensionRendererMode(int)}, and pass {@link DrmSessionManager} directly to {@link + * SimpleExoPlayer} or {@link ExoPlayerFactory}. */ @Deprecated @SuppressWarnings("deprecation") @@ -132,26 +137,22 @@ public class DefaultRenderersFactory implements RenderersFactory { } /** - * @param context A {@link Context}. - * @param extensionRendererMode The extension renderer mode, which determines if and how available - * extension renderers are used. Note that extensions must be included in the application - * build for them to be considered available. - * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to - * seamlessly join an ongoing playback. + * @deprecated Use {@link #DefaultRenderersFactory(Context)}, {@link + * #setExtensionRendererMode(int)} and {@link #setAllowedVideoJoiningTimeMs(long)}. */ + @Deprecated + @SuppressWarnings("deprecation") public DefaultRenderersFactory( Context context, @ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) { - this.context = context; - this.extensionRendererMode = extensionRendererMode; - this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs; - this.drmSessionManager = null; + this(context, null, extensionRendererMode, allowedVideoJoiningTimeMs); } /** - * @deprecated Use {@link #DefaultRenderersFactory(Context, int, long)} and pass {@link - * DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}. + * @deprecated Use {@link #DefaultRenderersFactory(Context)}, {@link + * #setExtensionRendererMode(int)} and {@link #setAllowedVideoJoiningTimeMs(long)}, and pass + * {@link DrmSessionManager} directly to {@link SimpleExoPlayer} or {@link ExoPlayerFactory}. */ @Deprecated public DefaultRenderersFactory( @@ -163,6 +164,70 @@ public class DefaultRenderersFactory implements RenderersFactory { this.extensionRendererMode = extensionRendererMode; this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs; this.drmSessionManager = drmSessionManager; + mediaCodecSelector = MediaCodecSelector.DEFAULT; + } + + /** + * Sets the extension renderer mode, which determines if and how available extension renderers are + * used. Note that extensions must be included in the application build for them to be considered + * available. + * + *

    The default value is {@link #EXTENSION_RENDERER_MODE_OFF}. + * + * @param extensionRendererMode The extension renderer mode. + * @return This factory, for convenience. + */ + public DefaultRenderersFactory setExtensionRendererMode( + @ExtensionRendererMode int extensionRendererMode) { + this.extensionRendererMode = extensionRendererMode; + return this; + } + + /** + * Sets whether renderers are permitted to play clear regions of encrypted media prior to having + * obtained the keys necessary to decrypt encrypted regions of the media. For encrypted media that + * starts with a short clear region, this allows playback to begin in parallel with key + * acquisition, which can reduce startup latency. + * + *

    The default value is {@code false}. + * + * @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of + * encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of + * the media. + * @return This factory, for convenience. + */ + public DefaultRenderersFactory setPlayClearSamplesWithoutKeys( + boolean playClearSamplesWithoutKeys) { + this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys; + return this; + } + + /** + * Sets a {@link MediaCodecSelector} for use by {@link MediaCodec} based renderers. + * + *

    The default value is {@link MediaCodecSelector#DEFAULT}. + * + * @param mediaCodecSelector The {@link MediaCodecSelector}. + * @return This factory, for convenience. + */ + public DefaultRenderersFactory setMediaCodecSelector(MediaCodecSelector mediaCodecSelector) { + this.mediaCodecSelector = mediaCodecSelector; + return this; + } + + /** + * Sets the maximum duration for which video renderers can attempt to seamlessly join an ongoing + * playback. + * + *

    The default value is {@link #DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS}. + * + * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to + * seamlessly join an ongoing playback, in milliseconds. + * @return This factory, for convenience. + */ + public DefaultRenderersFactory setAllowedVideoJoiningTimeMs(long allowedVideoJoiningTimeMs) { + this.allowedVideoJoiningTimeMs = allowedVideoJoiningTimeMs; + return this; } @Override @@ -177,10 +242,26 @@ public class DefaultRenderersFactory implements RenderersFactory { drmSessionManager = this.drmSessionManager; } ArrayList renderersList = new ArrayList<>(); - buildVideoRenderers(context, drmSessionManager, allowedVideoJoiningTimeMs, - eventHandler, videoRendererEventListener, extensionRendererMode, renderersList); - buildAudioRenderers(context, drmSessionManager, buildAudioProcessors(), - eventHandler, audioRendererEventListener, extensionRendererMode, renderersList); + buildVideoRenderers( + context, + extensionRendererMode, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + eventHandler, + videoRendererEventListener, + allowedVideoJoiningTimeMs, + renderersList); + buildAudioRenderers( + context, + extensionRendererMode, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + buildAudioProcessors(), + eventHandler, + audioRendererEventListener, + renderersList); buildTextRenderers(context, textRendererOutput, eventHandler.getLooper(), extensionRendererMode, renderersList); buildMetadataRenderers(context, metadataRendererOutput, eventHandler.getLooper(), @@ -194,27 +275,36 @@ public class DefaultRenderersFactory implements RenderersFactory { * Builds video renderers for use by the player. * * @param context The {@link Context} associated with the player. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player - * will not be used for DRM protected playbacks. - * @param allowedVideoJoiningTimeMs The maximum duration in milliseconds for which video - * renderers can attempt to seamlessly join an ongoing playback. + * @param extensionRendererMode The extension renderer mode. + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will + * not be used for DRM protected playbacks. + * @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of + * encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of + * the media. * @param eventHandler A handler associated with the main thread's looper. * @param eventListener An event listener. - * @param extensionRendererMode The extension renderer mode. + * @param allowedVideoJoiningTimeMs The maximum duration for which video renderers can attempt to + * seamlessly join an ongoing playback, in milliseconds. * @param out An array to which the built renderers should be appended. */ - protected void buildVideoRenderers(Context context, + protected void buildVideoRenderers( + Context context, + @ExtensionRendererMode int extensionRendererMode, + MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, - long allowedVideoJoiningTimeMs, Handler eventHandler, - VideoRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode, + boolean playClearSamplesWithoutKeys, + Handler eventHandler, + VideoRendererEventListener eventListener, + long allowedVideoJoiningTimeMs, ArrayList out) { out.add( new MediaCodecVideoRenderer( context, - MediaCodecSelector.DEFAULT, + mediaCodecSelector, allowedVideoJoiningTimeMs, drmSessionManager, - /* playClearSamplesWithoutKeys= */ false, + playClearSamplesWithoutKeys, eventHandler, eventListener, MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); @@ -259,26 +349,35 @@ public class DefaultRenderersFactory implements RenderersFactory { * Builds audio renderers for use by the player. * * @param context The {@link Context} associated with the player. - * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player - * will not be used for DRM protected playbacks. - * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio - * buffers before output. May be empty. + * @param extensionRendererMode The extension renderer mode. + * @param mediaCodecSelector A decoder selector. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the player will + * not be used for DRM protected playbacks. + * @param playClearSamplesWithoutKeys Whether renderers are permitted to play clear regions of + * encrypted media prior to having obtained the keys necessary to decrypt encrypted regions of + * the media. + * @param audioProcessors An array of {@link AudioProcessor}s that will process PCM audio buffers + * before output. May be empty. * @param eventHandler A handler to use when invoking event listeners and outputs. * @param eventListener An event listener. - * @param extensionRendererMode The extension renderer mode. * @param out An array to which the built renderers should be appended. */ - protected void buildAudioRenderers(Context context, + protected void buildAudioRenderers( + Context context, + @ExtensionRendererMode int extensionRendererMode, + MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, - AudioProcessor[] audioProcessors, Handler eventHandler, - AudioRendererEventListener eventListener, @ExtensionRendererMode int extensionRendererMode, + boolean playClearSamplesWithoutKeys, + AudioProcessor[] audioProcessors, + Handler eventHandler, + AudioRendererEventListener eventListener, ArrayList out) { out.add( new MediaCodecAudioRenderer( context, - MediaCodecSelector.DEFAULT, + mediaCodecSelector, drmSessionManager, - /* playClearSamplesWithoutKeys= */ false, + playClearSamplesWithoutKeys, eventHandler, eventListener, AudioCapabilities.getCapabilities(context), diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java index c63dbc04d0..551895ad93 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java @@ -58,7 +58,8 @@ public final class ExoPlayerFactory { LoadControl loadControl, @Nullable DrmSessionManager drmSessionManager, @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode) { - RenderersFactory renderersFactory = new DefaultRenderersFactory(context, extensionRendererMode); + RenderersFactory renderersFactory = + new DefaultRenderersFactory(context).setExtensionRendererMode(extensionRendererMode); return newSimpleInstance( context, renderersFactory, trackSelector, loadControl, drmSessionManager); } @@ -88,7 +89,9 @@ public final class ExoPlayerFactory { @DefaultRenderersFactory.ExtensionRendererMode int extensionRendererMode, long allowedVideoJoiningTimeMs) { RenderersFactory renderersFactory = - new DefaultRenderersFactory(context, extensionRendererMode, allowedVideoJoiningTimeMs); + new DefaultRenderersFactory(context) + .setExtensionRendererMode(extensionRendererMode) + .setAllowedVideoJoiningTimeMs(allowedVideoJoiningTimeMs); return newSimpleInstance( context, renderersFactory, trackSelector, loadControl, drmSessionManager); } diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java index d480d50b98..54e6088168 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/DebugRenderersFactory.java @@ -20,6 +20,7 @@ import android.content.Context; import android.media.MediaCodec; import android.media.MediaCrypto; import android.os.Handler; +import android.support.annotation.Nullable; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.Format; @@ -37,22 +38,37 @@ import java.util.ArrayList; /** * A debug extension of {@link DefaultRenderersFactory}. Provides a video renderer that performs - * video buffer timestamp assertions. + * video buffer timestamp assertions, and modifies the default value for {@link + * #setAllowedVideoJoiningTimeMs(long)} to be {@code 0}. */ public class DebugRenderersFactory extends DefaultRenderersFactory { public DebugRenderersFactory(Context context) { - super(context, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF, 0); + super(context); + setAllowedVideoJoiningTimeMs(0); } @Override - protected void buildVideoRenderers(Context context, - DrmSessionManager drmSessionManager, long allowedVideoJoiningTimeMs, - Handler eventHandler, VideoRendererEventListener eventListener, - @ExtensionRendererMode int extensionRendererMode, ArrayList out) { - out.add(new DebugMediaCodecVideoRenderer(context, MediaCodecSelector.DEFAULT, - allowedVideoJoiningTimeMs, drmSessionManager, eventHandler, eventListener, - MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); + protected void buildVideoRenderers( + Context context, + @ExtensionRendererMode int extensionRendererMode, + MediaCodecSelector mediaCodecSelector, + @Nullable DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, + Handler eventHandler, + VideoRendererEventListener eventListener, + long allowedVideoJoiningTimeMs, + ArrayList out) { + out.add( + new DebugMediaCodecVideoRenderer( + context, + mediaCodecSelector, + allowedVideoJoiningTimeMs, + drmSessionManager, + playClearSamplesWithoutKeys, + eventHandler, + eventListener, + MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY)); } /** @@ -71,12 +87,24 @@ public class DebugRenderersFactory extends DefaultRenderersFactory { private int minimumInsertIndex; private boolean skipToPositionBeforeRenderingFirstFrame; - public DebugMediaCodecVideoRenderer(Context context, MediaCodecSelector mediaCodecSelector, - long allowedJoiningTimeMs, DrmSessionManager drmSessionManager, - Handler eventHandler, VideoRendererEventListener eventListener, + public DebugMediaCodecVideoRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + long allowedJoiningTimeMs, + DrmSessionManager drmSessionManager, + boolean playClearSamplesWithoutKeys, + Handler eventHandler, + VideoRendererEventListener eventListener, int maxDroppedFrameCountToNotify) { - super(context, mediaCodecSelector, allowedJoiningTimeMs, drmSessionManager, false, - eventHandler, eventListener, maxDroppedFrameCountToNotify); + super( + context, + mediaCodecSelector, + allowedJoiningTimeMs, + drmSessionManager, + playClearSamplesWithoutKeys, + eventHandler, + eventListener, + maxDroppedFrameCountToNotify); } @Override From c0e6cd1b179ba4b5c705c59ffa7c79649398d1e0 Mon Sep 17 00:00:00 2001 From: eguven Date: Tue, 5 Feb 2019 10:09:00 +0000 Subject: [PATCH 097/110] Add DownloadState.mergeAction method This method is needed by DownloadManager. PiperOrigin-RevId: 232447145 --- .../exoplayer2/offline/DownloadIndexUtil.java | 43 +-- .../exoplayer2/offline/DownloadState.java | 60 +++ .../offline/DefaultDownloadIndexTest.java | 198 +--------- .../offline/DownloadStateBuilder.java | 181 +++++++++ .../exoplayer2/offline/DownloadStateTest.java | 344 ++++++++++++++++++ 5 files changed, 593 insertions(+), 233 deletions(-) create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadStateBuilder.java create mode 100644 library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadStateTest.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndexUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndexUtil.java index 0d6d2a36b5..f9a33f3e7b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndexUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadIndexUtil.java @@ -16,12 +16,7 @@ package com.google.android.exoplayer2.offline; import android.support.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.offline.DownloadState.State; -import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; -import java.util.Collections; -import java.util.HashSet; /** {@link DownloadIndex} related utility methods. */ public final class DownloadIndexUtil { @@ -79,46 +74,10 @@ public final class DownloadIndexUtil { DownloadIndex downloadIndex, @Nullable String id, DownloadAction action) { DownloadState downloadState = downloadIndex.getDownloadState(id != null ? id : action.id); if (downloadState != null) { - downloadState = merge(downloadState, action); + downloadState = downloadState.mergeAction(action); } else { downloadState = new DownloadState(action); } downloadIndex.putDownloadState(downloadState); } - - private static DownloadState merge(DownloadState downloadState, DownloadAction action) { - Assertions.checkArgument(action.type.equals(downloadState.type)); - @State int newState; - if (action.isRemoveAction) { - newState = DownloadState.STATE_REMOVING; - } else { - if (downloadState.state == DownloadState.STATE_REMOVING - || downloadState.state == DownloadState.STATE_RESTARTING) { - newState = DownloadState.STATE_RESTARTING; - } else if (downloadState.state == DownloadState.STATE_STOPPED) { - newState = DownloadState.STATE_STOPPED; - } else { - newState = DownloadState.STATE_QUEUED; - } - } - HashSet keys = new HashSet<>(action.keys); - Collections.addAll(keys, downloadState.streamKeys); - StreamKey[] newKeys = keys.toArray(new StreamKey[0]); - return new DownloadState( - downloadState.id, - downloadState.type, - action.uri, - action.customCacheKey, - newState, - /* downloadPercentage= */ C.PERCENTAGE_UNSET, - downloadState.downloadedBytes, - /* totalBytes= */ C.LENGTH_UNSET, - downloadState.failureReason, - downloadState.stopFlags, - downloadState.notMetRequirements, - downloadState.startTimeMs, - downloadState.updateTimeMs, - newKeys, - action.data); - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java index c59b7b87b6..35518af38d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java @@ -24,6 +24,8 @@ import com.google.android.exoplayer2.util.Assertions; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Collections; +import java.util.HashSet; /** Represents state of a download. */ public final class DownloadState { @@ -231,4 +233,62 @@ public final class DownloadState { this.streamKeys = streamKeys; this.customMetadata = customMetadata; } + + /** + * Merges the given {@link DownloadAction} and creates a new {@link DownloadState}. The action + * must have the same id and type. + * + * @param action The {@link DownloadAction} to be merged. + * @return A new {@link DownloadState}. + */ + public DownloadState mergeAction(DownloadAction action) { + Assertions.checkArgument(action.id.equals(id)); + Assertions.checkArgument(action.type.equals(type)); + return new DownloadState( + id, + type, + action.uri, + action.customCacheKey, + getNextState(action, state), + /* downloadPercentage= */ C.PERCENTAGE_UNSET, + downloadedBytes, + /* totalBytes= */ C.LENGTH_UNSET, + FAILURE_REASON_NONE, + stopFlags, + notMetRequirements, + startTimeMs, + updateTimeMs, + mergeStreamKeys(this, action), + action.data); + } + + private static int getNextState(DownloadAction action, int currentState) { + int newState; + if (action.isRemoveAction) { + newState = STATE_REMOVING; + } else { + if (currentState == STATE_REMOVING || currentState == STATE_RESTARTING) { + newState = STATE_RESTARTING; + } else if (currentState == STATE_STOPPED) { + newState = STATE_STOPPED; + } else { + newState = STATE_QUEUED; + } + } + return newState; + } + + private static StreamKey[] mergeStreamKeys(DownloadState downloadState, DownloadAction action) { + StreamKey[] streamKeys = downloadState.streamKeys; + if (!action.isRemoveAction && streamKeys.length > 0) { + if (action.keys.isEmpty()) { + streamKeys = new StreamKey[0]; + } else { + HashSet keys = new HashSet<>(action.keys); + Collections.addAll(keys, downloadState.streamKeys); + streamKeys = keys.toArray(new StreamKey[0]); + } + } + return streamKeys; + } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java index badbb58eff..8789a7ef78 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java @@ -18,12 +18,8 @@ package com.google.android.exoplayer2.offline; import static com.google.common.truth.Truth.assertThat; import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.support.annotation.Nullable; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.database.ExoDatabaseProvider; import com.google.android.exoplayer2.database.VersionTable; -import java.util.Arrays; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -62,7 +58,7 @@ public class DefaultDownloadIndexTest { downloadIndex.putDownloadState(downloadState); DownloadState readDownloadState = downloadIndex.getDownloadState(id); - assertEqual(readDownloadState, downloadState); + DownloadStateTest.assertEqual(readDownloadState, downloadState); } @Test @@ -94,7 +90,7 @@ public class DefaultDownloadIndexTest { DownloadState readDownloadState = downloadIndex.getDownloadState(id); assertThat(readDownloadState).isNotNull(); - assertEqual(readDownloadState, downloadState); + DownloadStateTest.assertEqual(readDownloadState, downloadState); } @Test @@ -106,7 +102,7 @@ public class DefaultDownloadIndexTest { downloadIndex = new DefaultDownloadIndex(databaseProvider); DownloadState readDownloadState = downloadIndex.getDownloadState(id); assertThat(readDownloadState).isNotNull(); - assertEqual(readDownloadState, downloadState); + DownloadStateTest.assertEqual(readDownloadState, downloadState); } @Test @@ -141,9 +137,9 @@ public class DefaultDownloadIndexTest { assertThat(cursor.getCount()).isEqualTo(2); cursor.moveToNext(); - assertEqual(cursor.getDownloadState(), downloadState2); + DownloadStateTest.assertEqual(cursor.getDownloadState(), downloadState2); cursor.moveToNext(); - assertEqual(cursor.getDownloadState(), downloadState1); + DownloadStateTest.assertEqual(cursor.getDownloadState(), downloadState1); cursor.close(); } @@ -173,9 +169,9 @@ public class DefaultDownloadIndexTest { assertThat(cursor.getCount()).isEqualTo(2); cursor.moveToNext(); - assertEqual(cursor.getDownloadState(), downloadState1); + DownloadStateTest.assertEqual(cursor.getDownloadState(), downloadState1); cursor.moveToNext(); - assertEqual(cursor.getDownloadState(), downloadState3); + DownloadStateTest.assertEqual(cursor.getDownloadState(), downloadState3); cursor.close(); } @@ -211,184 +207,4 @@ public class DefaultDownloadIndexTest { .isEqualTo(DefaultDownloadIndex.TABLE_VERSION); } - private static void assertEqual(DownloadState downloadState, DownloadState expected) { - assertThat(areEqual(downloadState, expected)).isTrue(); - } - - private static boolean areEqual(DownloadState downloadState, DownloadState that) { - if (downloadState.state != that.state) { - return false; - } - if (Float.compare(that.downloadPercentage, downloadState.downloadPercentage) != 0) { - return false; - } - if (downloadState.downloadedBytes != that.downloadedBytes) { - return false; - } - if (downloadState.totalBytes != that.totalBytes) { - return false; - } - if (downloadState.startTimeMs != that.startTimeMs) { - return false; - } - if (downloadState.updateTimeMs != that.updateTimeMs) { - return false; - } - if (downloadState.failureReason != that.failureReason) { - return false; - } - if (downloadState.stopFlags != that.stopFlags) { - return false; - } - if (downloadState.notMetRequirements != that.notMetRequirements) { - return false; - } - if (!downloadState.id.equals(that.id)) { - return false; - } - if (!downloadState.type.equals(that.type)) { - return false; - } - if (!downloadState.uri.equals(that.uri)) { - return false; - } - if (downloadState.cacheKey != null - ? !downloadState.cacheKey.equals(that.cacheKey) - : that.cacheKey != null) { - return false; - } - if (!Arrays.equals(downloadState.streamKeys, that.streamKeys)) { - return false; - } - return Arrays.equals(downloadState.customMetadata, that.customMetadata); - } - - private static class DownloadStateBuilder { - private String id; - private String type; - private String uri; - @Nullable private String cacheKey; - private int state; - private float downloadPercentage; - private long downloadedBytes; - private long totalBytes; - private int failureReason; - private int stopFlags; - private int notMetRequirements; - private long startTimeMs; - private long updateTimeMs; - private StreamKey[] streamKeys; - private byte[] customMetadata; - - private DownloadStateBuilder(String id) { - this.id = id; - this.type = "type"; - this.uri = "uri"; - this.cacheKey = null; - this.state = DownloadState.STATE_QUEUED; - this.downloadPercentage = (float) C.PERCENTAGE_UNSET; - this.downloadedBytes = (long) 0; - this.totalBytes = (long) C.LENGTH_UNSET; - this.failureReason = DownloadState.FAILURE_REASON_NONE; - this.stopFlags = 0; - this.startTimeMs = (long) 0; - this.updateTimeMs = (long) 0; - this.streamKeys = new StreamKey[0]; - this.customMetadata = new byte[0]; - } - - public DownloadStateBuilder setId(String id) { - this.id = id; - return this; - } - - public DownloadStateBuilder setType(String type) { - this.type = type; - return this; - } - - public DownloadStateBuilder setUri(String uri) { - this.uri = uri; - return this; - } - - public DownloadStateBuilder setCacheKey(@Nullable String cacheKey) { - this.cacheKey = cacheKey; - return this; - } - - public DownloadStateBuilder setState(int state) { - this.state = state; - return this; - } - - public DownloadStateBuilder setDownloadPercentage(float downloadPercentage) { - this.downloadPercentage = downloadPercentage; - return this; - } - - public DownloadStateBuilder setDownloadedBytes(long downloadedBytes) { - this.downloadedBytes = downloadedBytes; - return this; - } - - public DownloadStateBuilder setTotalBytes(long totalBytes) { - this.totalBytes = totalBytes; - return this; - } - - public DownloadStateBuilder setFailureReason(int failureReason) { - this.failureReason = failureReason; - return this; - } - - public DownloadStateBuilder setStopFlags(int stopFlags) { - this.stopFlags = stopFlags; - return this; - } - - public DownloadStateBuilder setNotMetRequirements(int notMetRequirements) { - this.notMetRequirements = notMetRequirements; - return this; - } - - public DownloadStateBuilder setStartTimeMs(long startTimeMs) { - this.startTimeMs = startTimeMs; - return this; - } - - public DownloadStateBuilder setUpdateTimeMs(long updateTimeMs) { - this.updateTimeMs = updateTimeMs; - return this; - } - - public DownloadStateBuilder setStreamKeys(StreamKey... streamKeys) { - this.streamKeys = streamKeys; - return this; - } - - public DownloadStateBuilder setCustomMetadata(byte[] customMetadata) { - this.customMetadata = customMetadata; - return this; - } - - public DownloadState build() { - return new DownloadState( - id, - type, - Uri.parse(uri), - cacheKey, - state, - downloadPercentage, - downloadedBytes, - totalBytes, - failureReason, - stopFlags, - notMetRequirements, - startTimeMs, - updateTimeMs, - streamKeys, - customMetadata); - } - } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadStateBuilder.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadStateBuilder.java new file mode 100644 index 0000000000..84b51c5572 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadStateBuilder.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.offline; + +import android.net.Uri; +import android.support.annotation.Nullable; +import com.google.android.exoplayer2.C; + +/** + * Builder for DownloadState. + * + *

    Defines default values for each field (except {@code id}) to facilitate DownloadState creation + * for tests. Tests must avoid depending on the default values but explicitly set tested parameters + * during test initialization. + */ +class DownloadStateBuilder { + private String id; + private String type; + private Uri uri; + @Nullable private String cacheKey; + private int state; + private float downloadPercentage; + private long downloadedBytes; + private long totalBytes; + private int failureReason; + private int stopFlags; + private int notMetRequirements; + private long startTimeMs; + private long updateTimeMs; + private StreamKey[] streamKeys; + private byte[] customMetadata; + + DownloadStateBuilder(String id) { + this(id, "type", Uri.parse("uri"), /* cacheKey= */ null, new byte[0], new StreamKey[0]); + } + + DownloadStateBuilder(DownloadAction action) { + this( + action.id, + action.type, + action.uri, + action.customCacheKey, + action.data, + action.keys.toArray(new StreamKey[0])); + } + + DownloadStateBuilder( + String id, + String type, + Uri uri, + String cacheKey, + byte[] customMetadata, + StreamKey[] streamKeys) { + this.id = id; + this.type = type; + this.uri = uri; + this.cacheKey = cacheKey; + this.state = DownloadState.STATE_QUEUED; + this.downloadPercentage = (float) C.PERCENTAGE_UNSET; + this.downloadedBytes = (long) 0; + this.totalBytes = (long) C.LENGTH_UNSET; + this.failureReason = DownloadState.FAILURE_REASON_NONE; + this.stopFlags = 0; + this.startTimeMs = (long) 0; + this.updateTimeMs = (long) 0; + this.streamKeys = streamKeys; + this.customMetadata = customMetadata; + } + + public DownloadStateBuilder setId(String id) { + this.id = id; + return this; + } + + public DownloadStateBuilder setType(String type) { + this.type = type; + return this; + } + + public DownloadStateBuilder setUri(String uri) { + this.uri = Uri.parse(uri); + return this; + } + + public DownloadStateBuilder setUri(Uri uri) { + this.uri = uri; + return this; + } + + public DownloadStateBuilder setCacheKey(@Nullable String cacheKey) { + this.cacheKey = cacheKey; + return this; + } + + public DownloadStateBuilder setState(int state) { + this.state = state; + return this; + } + + public DownloadStateBuilder setDownloadPercentage(float downloadPercentage) { + this.downloadPercentage = downloadPercentage; + return this; + } + + public DownloadStateBuilder setDownloadedBytes(long downloadedBytes) { + this.downloadedBytes = downloadedBytes; + return this; + } + + public DownloadStateBuilder setTotalBytes(long totalBytes) { + this.totalBytes = totalBytes; + return this; + } + + public DownloadStateBuilder setFailureReason(int failureReason) { + this.failureReason = failureReason; + return this; + } + + public DownloadStateBuilder setStopFlags(int stopFlags) { + this.stopFlags = stopFlags; + return this; + } + + public DownloadStateBuilder setNotMetRequirements(int notMetRequirements) { + this.notMetRequirements = notMetRequirements; + return this; + } + + public DownloadStateBuilder setStartTimeMs(long startTimeMs) { + this.startTimeMs = startTimeMs; + return this; + } + + public DownloadStateBuilder setUpdateTimeMs(long updateTimeMs) { + this.updateTimeMs = updateTimeMs; + return this; + } + + public DownloadStateBuilder setStreamKeys(StreamKey... streamKeys) { + this.streamKeys = streamKeys; + return this; + } + + public DownloadStateBuilder setCustomMetadata(byte[] customMetadata) { + this.customMetadata = customMetadata; + return this; + } + + public DownloadState build() { + return new DownloadState( + id, + type, + uri, + cacheKey, + state, + downloadPercentage, + downloadedBytes, + totalBytes, + failureReason, + stopFlags, + notMetRequirements, + startTimeMs, + updateTimeMs, + streamKeys, + customMetadata); + } +} diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadStateTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadStateTest.java new file mode 100644 index 0000000000..f6d008a0e3 --- /dev/null +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadStateTest.java @@ -0,0 +1,344 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.offline; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import android.net.Uri; +import java.util.Arrays; +import java.util.Collections; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +/** Unit tests for {@link DownloadState}. */ +@RunWith(RobolectricTestRunner.class) +public class DownloadStateTest { + + private Uri testUri; + + @Before + public void setUp() throws Exception { + testUri = Uri.parse("https://www.test.com/download1"); + } + + @Test + public void mergeAction_actionHaveDifferentType_throwsException() { + DownloadAction downloadAction = createDownloadAction(); + DownloadState downloadState = + new DownloadStateBuilder(downloadAction) + .setType(downloadAction.type + "_different") + .setState(DownloadState.STATE_QUEUED) + .build(); + + try { + downloadState.mergeAction(downloadAction); + fail(); + } catch (Exception e) { + // Expected. + } + } + + @Test + public void mergeAction_actionHaveDifferentId_throwsException() { + DownloadAction downloadAction = createDownloadAction(); + DownloadState downloadState = + new DownloadStateBuilder(downloadAction) + .setId(downloadAction.id + "_different") + .setState(DownloadState.STATE_QUEUED) + .build(); + + try { + downloadState.mergeAction(downloadAction); + fail(); + } catch (Exception e) { + // Expected. + } + } + + @Test + public void mergeAction_actionsWithSameIdAndType_doesNotFail() { + DownloadAction downloadAction = createDownloadAction(); + DownloadStateBuilder downloadStateBuilder = + new DownloadStateBuilder(downloadAction).setState(DownloadState.STATE_QUEUED); + DownloadState downloadState = downloadStateBuilder.build(); + + downloadState.mergeAction(downloadAction); + } + + @Test + public void mergeAction_actionHaveDifferentUri_downloadStateUriIsUpdated() { + DownloadAction downloadAction = createDownloadAction(); + DownloadStateBuilder downloadStateBuilder = + new DownloadStateBuilder(downloadAction) + .setUri(downloadAction.uri + "_different") + .setState(DownloadState.STATE_QUEUED); + DownloadState downloadState = downloadStateBuilder.build(); + + DownloadState mergedDownloadState = downloadState.mergeAction(downloadAction); + + DownloadState expectedDownloadState = downloadStateBuilder.setUri(downloadAction.uri).build(); + assertEqual(mergedDownloadState, expectedDownloadState); + } + + @Test + public void mergeAction_actionHaveDifferentData_downloadStateDataIsUpdated() { + DownloadAction downloadAction = + DownloadAction.createDownloadAction( + DownloadAction.TYPE_DASH, + testUri, + Collections.emptyList(), + /* customCacheKey= */ null, + /* data= */ new byte[] {1, 2, 3, 4}); + DownloadStateBuilder downloadStateBuilder = + new DownloadStateBuilder(downloadAction) + .setState(DownloadState.STATE_QUEUED) + .setCustomMetadata(new byte[0]); + DownloadState downloadState = downloadStateBuilder.build(); + + DownloadState mergedDownloadState = downloadState.mergeAction(downloadAction); + + DownloadState expectedDownloadState = + downloadStateBuilder.setCustomMetadata(downloadAction.data).build(); + assertEqual(mergedDownloadState, expectedDownloadState); + } + + @Test + public void mergeAction_queuedDownloadRemoveAction_stateBecomesRemoving() { + DownloadAction downloadAction = createRemoveAction(); + DownloadStateBuilder downloadStateBuilder = + new DownloadStateBuilder(downloadAction).setState(DownloadState.STATE_QUEUED); + DownloadState downloadState = downloadStateBuilder.build(); + + DownloadState mergedDownloadState = downloadState.mergeAction(downloadAction); + + DownloadState expectedDownloadState = + downloadStateBuilder.setState(DownloadState.STATE_REMOVING).build(); + assertEqual(mergedDownloadState, expectedDownloadState); + } + + @Test + public void mergeAction_removingDownloadDownloadAction_stateBecomesRestarting() { + DownloadAction downloadAction = createDownloadAction(); + DownloadStateBuilder downloadStateBuilder = + new DownloadStateBuilder(downloadAction).setState(DownloadState.STATE_REMOVING); + DownloadState downloadState = downloadStateBuilder.build(); + + DownloadState mergedDownloadState = downloadState.mergeAction(downloadAction); + + DownloadState expectedDownloadState = + downloadStateBuilder.setState(DownloadState.STATE_RESTARTING).build(); + assertEqual(mergedDownloadState, expectedDownloadState); + } + + @Test + public void mergeAction_failedDownloadDownloadAction_stateBecomesQueued() { + DownloadAction downloadAction = createDownloadAction(); + DownloadStateBuilder downloadStateBuilder = + new DownloadStateBuilder(downloadAction) + .setState(DownloadState.STATE_FAILED) + .setFailureReason(DownloadState.FAILURE_REASON_UNKNOWN); + DownloadState downloadState = downloadStateBuilder.build(); + + DownloadState mergedDownloadState = downloadState.mergeAction(downloadAction); + + DownloadState expectedDownloadState = + downloadStateBuilder + .setState(DownloadState.STATE_QUEUED) + .setFailureReason(DownloadState.FAILURE_REASON_NONE) + .build(); + assertEqual(mergedDownloadState, expectedDownloadState); + } + + @Test + public void mergeAction_stoppedDownloadDownloadAction_stateStaysStopped() { + DownloadAction downloadAction = createDownloadAction(); + DownloadStateBuilder downloadStateBuilder = + new DownloadStateBuilder(downloadAction) + .setState(DownloadState.STATE_STOPPED) + .setStopFlags(DownloadState.STOP_FLAG_STOPPED); + DownloadState downloadState = downloadStateBuilder.build(); + + DownloadState mergedDownloadState = downloadState.mergeAction(downloadAction); + + assertEqual(mergedDownloadState, downloadState); + } + + @Test + public void mergeAction_stoppedDownloadRemoveAction_stateBecomesRemoving() { + DownloadAction downloadAction = createRemoveAction(); + DownloadStateBuilder downloadStateBuilder = + new DownloadStateBuilder(downloadAction) + .setState(DownloadState.STATE_STOPPED) + .setStopFlags(DownloadState.STOP_FLAG_STOPPED); + DownloadState downloadState = downloadStateBuilder.build(); + + DownloadState mergedDownloadState = downloadState.mergeAction(downloadAction); + + DownloadState expectedDownloadState = + downloadStateBuilder.setState(DownloadState.STATE_REMOVING).build(); + assertEqual(mergedDownloadState, expectedDownloadState); + } + + @Test + public void mergeAction_restartingDownloadRemoveAction_stateBecomesRemoving() { + DownloadAction downloadAction = createRemoveAction(); + DownloadStateBuilder downloadStateBuilder = + new DownloadStateBuilder(downloadAction).setState(DownloadState.STATE_RESTARTING); + DownloadState downloadState = downloadStateBuilder.build(); + + DownloadState mergedDownloadState = downloadState.mergeAction(downloadAction); + + DownloadState expectedDownloadState = + downloadStateBuilder.setState(DownloadState.STATE_REMOVING).build(); + assertEqual(mergedDownloadState, expectedDownloadState); + } + + @Test + public void mergeAction_returnsMergedKeys() { + StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0); + StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1); + StreamKey[] keys1 = new StreamKey[] {streamKey1}; + StreamKey[] keys2 = new StreamKey[] {streamKey2}; + StreamKey[] expectedKeys = new StreamKey[] {streamKey1, streamKey2}; + + doTestMergeActionReturnsMergedKeys(keys1, keys2, expectedKeys); + } + + @Test + public void mergeAction_returnsUniqueKeys() { + StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0); + StreamKey streamKey1Copy = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0); + StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1); + StreamKey[] keys1 = new StreamKey[] {streamKey1}; + StreamKey[] keys2 = new StreamKey[] {streamKey2, streamKey1Copy}; + StreamKey[] expectedKeys = new StreamKey[] {streamKey1, streamKey2}; + + doTestMergeActionReturnsMergedKeys(keys1, keys2, expectedKeys); + } + + @Test + public void mergeAction_ifFirstActionKeysEmpty_returnsEmptyKeys() { + StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0); + StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1); + StreamKey[] keys1 = new StreamKey[] {}; + StreamKey[] keys2 = new StreamKey[] {streamKey2, streamKey1}; + StreamKey[] expectedKeys = new StreamKey[] {}; + + doTestMergeActionReturnsMergedKeys(keys1, keys2, expectedKeys); + } + + @Test + public void mergeAction_ifNotFirstActionKeysEmpty_returnsEmptyKeys() { + StreamKey streamKey1 = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0); + StreamKey streamKey2 = new StreamKey(/* groupIndex= */ 1, /* trackIndex= */ 1); + StreamKey[] keys1 = new StreamKey[] {streamKey2, streamKey1}; + StreamKey[] keys2 = new StreamKey[] {}; + StreamKey[] expectedKeys = new StreamKey[] {}; + + doTestMergeActionReturnsMergedKeys(keys1, keys2, expectedKeys); + } + + private void doTestMergeActionReturnsMergedKeys( + StreamKey[] keys1, StreamKey[] keys2, StreamKey[] expectedKeys) { + DownloadAction downloadAction = + DownloadAction.createDownloadAction( + DownloadAction.TYPE_DASH, + testUri, + Arrays.asList(keys2), + /* customCacheKey= */ null, + /* data= */ null); + DownloadStateBuilder downloadStateBuilder = + new DownloadStateBuilder(downloadAction) + .setState(DownloadState.STATE_QUEUED) + .setStreamKeys(keys1); + DownloadState downloadState = downloadStateBuilder.build(); + + DownloadState mergedDownloadState = downloadState.mergeAction(downloadAction); + + DownloadState expectedDownloadState = downloadStateBuilder.setStreamKeys(expectedKeys).build(); + assertEqual(mergedDownloadState, expectedDownloadState); + } + + static void assertEqual(DownloadState downloadState, DownloadState expected) { + assertThat(areEqual(downloadState, expected)).isTrue(); + } + + private static boolean areEqual(DownloadState downloadState, DownloadState that) { + if (downloadState.state != that.state) { + return false; + } + if (Float.compare(that.downloadPercentage, downloadState.downloadPercentage) != 0) { + return false; + } + if (downloadState.downloadedBytes != that.downloadedBytes) { + return false; + } + if (downloadState.totalBytes != that.totalBytes) { + return false; + } + if (downloadState.startTimeMs != that.startTimeMs) { + return false; + } + if (downloadState.updateTimeMs != that.updateTimeMs) { + return false; + } + if (downloadState.failureReason != that.failureReason) { + return false; + } + if (downloadState.stopFlags != that.stopFlags) { + return false; + } + if (downloadState.notMetRequirements != that.notMetRequirements) { + return false; + } + if (!downloadState.id.equals(that.id)) { + return false; + } + if (!downloadState.type.equals(that.type)) { + return false; + } + if (!downloadState.uri.equals(that.uri)) { + return false; + } + if (downloadState.cacheKey != null + ? !downloadState.cacheKey.equals(that.cacheKey) + : that.cacheKey != null) { + return false; + } + if (downloadState.streamKeys.length != that.streamKeys.length + || !Arrays.asList(downloadState.streamKeys).containsAll(Arrays.asList(that.streamKeys))) { + return false; + } + return Arrays.equals(downloadState.customMetadata, that.customMetadata); + } + + private DownloadAction createDownloadAction() { + return DownloadAction.createDownloadAction( + DownloadAction.TYPE_DASH, + testUri, + Collections.emptyList(), + /* customCacheKey= */ null, + /* data= */ null); + } + + private DownloadAction createRemoveAction() { + return DownloadAction.createRemoveAction( + DownloadAction.TYPE_DASH, testUri, /* customCacheKey= */ null); + } +} From 784906b87209328e1f208cf833ff6cc426c77040 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Feb 2019 15:25:01 +0000 Subject: [PATCH 098/110] Update release notes PiperOrigin-RevId: 232481580 --- RELEASENOTES.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9ec6015240..817f9f2645 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -37,8 +37,6 @@ ([#3860](https://github.com/google/ExoPlayer/issues/3860)). * IMA extension: Require setting the `Player` on `AdsLoader` instances before playback. -* Add `Handler` parameter to `ConcatenatingMediaSource` methods which take a - callback `Runnable`. * Remove `player` and `isTopLevelSource` parameters from `MediaSource.prepare`. * VP9 extension: Remove RGB output mode and libyuv dependency, and switch to surface YUV output as the default. Remove constructor parameters `scaleToFit` @@ -49,6 +47,10 @@ ### 2.9.5 ### * HLS: Parse `CHANNELS` attribute from `EXT-X-MEDIA` tag. +* ConcatenatingMediaSource: + * Add `Handler` parameter to methods that take a callback `Runnable`. + * Fix issue with dropped messages when releasing the source + ([#5464](https://github.com/google/ExoPlayer/issues/5464)). * ExtractorMediaSource: Fix issue that could cause the player to get stuck buffering at the end of the media. * PlayerView: Fix issue preventing `OnClickListener` from receiving events @@ -58,8 +60,6 @@ * OkHttp extension: Upgrade OkHttp dependency to 3.12.1. * MP3: Wider fix for issue where streams would play twice on some Samsung devices ([#4519](https://github.com/google/ExoPlayer/issues/4519)). -* Fix issue with dropped messages when releasing a `ConcatenatingMediaSource` - ([#5464](https://github.com/google/ExoPlayer/issues/5464)). ### 2.9.4 ### From 391f2bb6c2433895d48e3c79ac3d00d38633b9f6 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Feb 2019 15:30:13 +0000 Subject: [PATCH 099/110] Remove indirection in DefaultDownloadIndex DefaultDownloadIndex is currently just forwarding all calls to its inner class. getDownloadTable() handles initialization, but this doesn't really seem different to having an initialization method. It doesn't guarantee initialization happens, since a bad method implementation could try and access downloadTable directly, just as a bad method implementation could forget to call the initialization method. Hence this change removes the indirection. PiperOrigin-RevId: 232482228 --- .../offline/DefaultDownloadIndex.java | 484 +++++++++--------- 1 file changed, 231 insertions(+), 253 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java index b9fae6e2db..2ff85c041b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java @@ -39,8 +39,98 @@ public final class DefaultDownloadIndex implements DownloadIndex { @VisibleForTesting /* package */ static final int TABLE_VERSION = 1; + private static final String COLUMN_ID = "id"; + private static final String COLUMN_TYPE = "title"; + private static final String COLUMN_URI = "subtitle"; + private static final String COLUMN_CACHE_KEY = "cache_key"; + private static final String COLUMN_STATE = "state"; + private static final String COLUMN_DOWNLOAD_PERCENTAGE = "download_percentage"; + private static final String COLUMN_DOWNLOADED_BYTES = "downloaded_bytes"; + private static final String COLUMN_TOTAL_BYTES = "total_bytes"; + private static final String COLUMN_FAILURE_REASON = "failure_reason"; + private static final String COLUMN_STOP_FLAGS = "stop_flags"; + private static final String COLUMN_NOT_MET_REQUIREMENTS = "not_met_requirements"; + private static final String COLUMN_START_TIME_MS = "start_time_ms"; + private static final String COLUMN_UPDATE_TIME_MS = "update_time_ms"; + private static final String COLUMN_STREAM_KEYS = "stream_keys"; + private static final String COLUMN_CUSTOM_METADATA = "custom_metadata"; + + private static final int COLUMN_INDEX_ID = 0; + private static final int COLUMN_INDEX_TYPE = 1; + private static final int COLUMN_INDEX_URI = 2; + private static final int COLUMN_INDEX_CACHE_KEY = 3; + private static final int COLUMN_INDEX_STATE = 4; + private static final int COLUMN_INDEX_DOWNLOAD_PERCENTAGE = 5; + private static final int COLUMN_INDEX_DOWNLOADED_BYTES = 6; + private static final int COLUMN_INDEX_TOTAL_BYTES = 7; + private static final int COLUMN_INDEX_FAILURE_REASON = 8; + private static final int COLUMN_INDEX_STOP_FLAGS = 9; + private static final int COLUMN_INDEX_NOT_MET_REQUIREMENTS = 10; + private static final int COLUMN_INDEX_START_TIME_MS = 11; + private static final int COLUMN_INDEX_UPDATE_TIME_MS = 12; + private static final int COLUMN_INDEX_STREAM_KEYS = 13; + private static final int COLUMN_INDEX_CUSTOM_METADATA = 14; + + private static final String COLUMN_SELECTION_ID = COLUMN_ID + " = ?"; + + private static final String[] COLUMNS = + new String[] { + COLUMN_ID, + COLUMN_TYPE, + COLUMN_URI, + COLUMN_CACHE_KEY, + COLUMN_STATE, + COLUMN_DOWNLOAD_PERCENTAGE, + COLUMN_DOWNLOADED_BYTES, + COLUMN_TOTAL_BYTES, + COLUMN_FAILURE_REASON, + COLUMN_STOP_FLAGS, + COLUMN_NOT_MET_REQUIREMENTS, + COLUMN_START_TIME_MS, + COLUMN_UPDATE_TIME_MS, + COLUMN_STREAM_KEYS, + COLUMN_CUSTOM_METADATA + }; + + private static final String SQL_DROP_TABLE_IF_EXISTS = "DROP TABLE IF EXISTS " + TABLE_NAME; + private static final String SQL_CREATE_TABLE = + "CREATE TABLE " + + TABLE_NAME + + " (" + + COLUMN_ID + + " TEXT PRIMARY KEY NOT NULL," + + COLUMN_TYPE + + " TEXT NOT NULL," + + COLUMN_URI + + " TEXT NOT NULL," + + COLUMN_CACHE_KEY + + " TEXT," + + COLUMN_STATE + + " INTEGER NOT NULL," + + COLUMN_DOWNLOAD_PERCENTAGE + + " REAL NOT NULL," + + COLUMN_DOWNLOADED_BYTES + + " INTEGER NOT NULL," + + COLUMN_TOTAL_BYTES + + " INTEGER NOT NULL," + + COLUMN_FAILURE_REASON + + " INTEGER NOT NULL," + + COLUMN_STOP_FLAGS + + " INTEGER NOT NULL," + + COLUMN_NOT_MET_REQUIREMENTS + + " INTEGER NOT NULL," + + COLUMN_START_TIME_MS + + " INTEGER NOT NULL," + + COLUMN_UPDATE_TIME_MS + + " INTEGER NOT NULL," + + COLUMN_STREAM_KEYS + + " TEXT NOT NULL," + + COLUMN_CUSTOM_METADATA + + " BLOB NOT NULL)"; + private final DatabaseProvider databaseProvider; - @Nullable private DownloadsTable downloadTable; + + private boolean initialized; /** * Creates a DefaultDownloadIndex which stores the {@link DownloadState}s on a SQLite database @@ -56,29 +146,160 @@ public final class DefaultDownloadIndex implements DownloadIndex { @Override @Nullable public DownloadState getDownloadState(String id) { - return getDownloadTable().get(id); + ensureInitialized(); + try (Cursor cursor = getCursor(COLUMN_SELECTION_ID, new String[] {id})) { + if (cursor.getCount() == 0) { + return null; + } + cursor.moveToNext(); + DownloadState downloadState = getDownloadStateForCurrentRow(cursor); + Assertions.checkState(id.equals(downloadState.id)); + return downloadState; + } } @Override public DownloadStateCursor getDownloadStates(@DownloadState.State int... states) { - return getDownloadTable().get(states); + ensureInitialized(); + String selection = null; + if (states.length > 0) { + StringBuilder selectionBuilder = new StringBuilder(); + selectionBuilder.append(COLUMN_STATE).append(" IN ("); + for (int i = 0; i < states.length; i++) { + if (i > 0) { + selectionBuilder.append(','); + } + selectionBuilder.append(states[i]); + } + selectionBuilder.append(')'); + selection = selectionBuilder.toString(); + } + Cursor cursor = getCursor(selection, /* selectionArgs= */ null); + return new DownloadStateCursorImpl(cursor); } @Override public void putDownloadState(DownloadState downloadState) { - getDownloadTable().replace(downloadState); + ensureInitialized(); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(COLUMN_ID, downloadState.id); + values.put(COLUMN_TYPE, downloadState.type); + values.put(COLUMN_URI, downloadState.uri.toString()); + values.put(COLUMN_CACHE_KEY, downloadState.cacheKey); + values.put(COLUMN_STATE, downloadState.state); + values.put(COLUMN_DOWNLOAD_PERCENTAGE, downloadState.downloadPercentage); + values.put(COLUMN_DOWNLOADED_BYTES, downloadState.downloadedBytes); + values.put(COLUMN_TOTAL_BYTES, downloadState.totalBytes); + values.put(COLUMN_FAILURE_REASON, downloadState.failureReason); + values.put(COLUMN_STOP_FLAGS, downloadState.stopFlags); + values.put(COLUMN_NOT_MET_REQUIREMENTS, downloadState.notMetRequirements); + values.put(COLUMN_START_TIME_MS, downloadState.startTimeMs); + values.put(COLUMN_UPDATE_TIME_MS, downloadState.updateTimeMs); + values.put(COLUMN_STREAM_KEYS, encodeStreamKeys(downloadState.streamKeys)); + values.put(COLUMN_CUSTOM_METADATA, downloadState.customMetadata); + writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values); } @Override public void removeDownloadState(String id) { - getDownloadTable().delete(id); + ensureInitialized(); + databaseProvider + .getWritableDatabase() + .delete(TABLE_NAME, COLUMN_SELECTION_ID, new String[] {id}); } - private DownloadsTable getDownloadTable() { - if (downloadTable == null) { - downloadTable = new DownloadsTable(databaseProvider); + private void ensureInitialized() { + if (initialized) { + return; } - return downloadTable; + SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase(); + int version = VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_OFFLINE); + if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) { + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.beginTransaction(); + try { + VersionTable.setVersion(writableDatabase, VersionTable.FEATURE_OFFLINE, TABLE_VERSION); + writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS); + writableDatabase.execSQL(SQL_CREATE_TABLE); + writableDatabase.setTransactionSuccessful(); + } finally { + writableDatabase.endTransaction(); + } + } else if (version < TABLE_VERSION) { + // There is no previous version currently. + throw new IllegalStateException(); + } + initialized = true; + } + + private Cursor getCursor(@Nullable String selection, @Nullable String[] selectionArgs) { + String sortOrder = COLUMN_START_TIME_MS + " ASC"; + return databaseProvider + .getReadableDatabase() + .query( + TABLE_NAME, + COLUMNS, + selection, + selectionArgs, + /* groupBy= */ null, + /* having= */ null, + sortOrder); + } + + private static DownloadState getDownloadStateForCurrentRow(Cursor cursor) { + return new DownloadState( + cursor.getString(COLUMN_INDEX_ID), + cursor.getString(COLUMN_INDEX_TYPE), + Uri.parse(cursor.getString(COLUMN_INDEX_URI)), + cursor.getString(COLUMN_INDEX_CACHE_KEY), + cursor.getInt(COLUMN_INDEX_STATE), + cursor.getFloat(COLUMN_INDEX_DOWNLOAD_PERCENTAGE), + cursor.getLong(COLUMN_INDEX_DOWNLOADED_BYTES), + cursor.getLong(COLUMN_INDEX_TOTAL_BYTES), + cursor.getInt(COLUMN_INDEX_FAILURE_REASON), + cursor.getInt(COLUMN_INDEX_STOP_FLAGS), + cursor.getInt(COLUMN_INDEX_NOT_MET_REQUIREMENTS), + cursor.getLong(COLUMN_INDEX_START_TIME_MS), + cursor.getLong(COLUMN_INDEX_UPDATE_TIME_MS), + decodeStreamKeys(cursor.getString(COLUMN_INDEX_STREAM_KEYS)), + cursor.getBlob(COLUMN_INDEX_CUSTOM_METADATA)); + } + + private static String encodeStreamKeys(StreamKey[] streamKeys) { + StringBuilder stringBuilder = new StringBuilder(); + for (StreamKey streamKey : streamKeys) { + stringBuilder + .append(streamKey.periodIndex) + .append('.') + .append(streamKey.groupIndex) + .append('.') + .append(streamKey.trackIndex) + .append(','); + } + if (stringBuilder.length() > 0) { + stringBuilder.setLength(stringBuilder.length() - 1); + } + return stringBuilder.toString(); + } + + private static StreamKey[] decodeStreamKeys(String encodedStreamKeys) { + if (encodedStreamKeys.isEmpty()) { + return new StreamKey[0]; + } + String[] streamKeysStrings = Util.split(encodedStreamKeys, ","); + int streamKeysCount = streamKeysStrings.length; + StreamKey[] streamKeys = new StreamKey[streamKeysCount]; + for (int i = 0; i < streamKeysCount; i++) { + String[] indices = Util.split(streamKeysStrings[i], "\\."); + Assertions.checkState(indices.length == 3); + streamKeys[i] = + new StreamKey( + Integer.parseInt(indices[0]), + Integer.parseInt(indices[1]), + Integer.parseInt(indices[2])); + } + return streamKeys; } private static final class DownloadStateCursorImpl implements DownloadStateCursor { @@ -91,7 +312,7 @@ public final class DefaultDownloadIndex implements DownloadIndex { @Override public DownloadState getDownloadState() { - return DownloadsTable.getDownloadState(cursor); + return getDownloadStateForCurrentRow(cursor); } @Override @@ -119,247 +340,4 @@ public final class DefaultDownloadIndex implements DownloadIndex { return cursor.isClosed(); } } - - private static final class DownloadsTable { - - private static final String COLUMN_ID = "id"; - private static final String COLUMN_TYPE = "title"; - private static final String COLUMN_URI = "subtitle"; - private static final String COLUMN_CACHE_KEY = "cache_key"; - private static final String COLUMN_STATE = "state"; - private static final String COLUMN_DOWNLOAD_PERCENTAGE = "download_percentage"; - private static final String COLUMN_DOWNLOADED_BYTES = "downloaded_bytes"; - private static final String COLUMN_TOTAL_BYTES = "total_bytes"; - private static final String COLUMN_FAILURE_REASON = "failure_reason"; - private static final String COLUMN_STOP_FLAGS = "stop_flags"; - private static final String COLUMN_NOT_MET_REQUIREMENTS = "not_met_requirements"; - private static final String COLUMN_START_TIME_MS = "start_time_ms"; - private static final String COLUMN_UPDATE_TIME_MS = "update_time_ms"; - private static final String COLUMN_STREAM_KEYS = "stream_keys"; - private static final String COLUMN_CUSTOM_METADATA = "custom_metadata"; - - private static final int COLUMN_INDEX_ID = 0; - private static final int COLUMN_INDEX_TYPE = 1; - private static final int COLUMN_INDEX_URI = 2; - private static final int COLUMN_INDEX_CACHE_KEY = 3; - private static final int COLUMN_INDEX_STATE = 4; - private static final int COLUMN_INDEX_DOWNLOAD_PERCENTAGE = 5; - private static final int COLUMN_INDEX_DOWNLOADED_BYTES = 6; - private static final int COLUMN_INDEX_TOTAL_BYTES = 7; - private static final int COLUMN_INDEX_FAILURE_REASON = 8; - private static final int COLUMN_INDEX_STOP_FLAGS = 9; - private static final int COLUMN_INDEX_NOT_MET_REQUIREMENTS = 10; - private static final int COLUMN_INDEX_START_TIME_MS = 11; - private static final int COLUMN_INDEX_UPDATE_TIME_MS = 12; - private static final int COLUMN_INDEX_STREAM_KEYS = 13; - private static final int COLUMN_INDEX_CUSTOM_METADATA = 14; - - private static final String COLUMN_SELECTION_ID = COLUMN_ID + " = ?"; - - private static final String[] COLUMNS = - new String[] { - COLUMN_ID, - COLUMN_TYPE, - COLUMN_URI, - COLUMN_CACHE_KEY, - COLUMN_STATE, - COLUMN_DOWNLOAD_PERCENTAGE, - COLUMN_DOWNLOADED_BYTES, - COLUMN_TOTAL_BYTES, - COLUMN_FAILURE_REASON, - COLUMN_STOP_FLAGS, - COLUMN_NOT_MET_REQUIREMENTS, - COLUMN_START_TIME_MS, - COLUMN_UPDATE_TIME_MS, - COLUMN_STREAM_KEYS, - COLUMN_CUSTOM_METADATA - }; - - private static final String SQL_DROP_TABLE_IF_EXISTS = "DROP TABLE IF EXISTS " + TABLE_NAME; - private static final String SQL_CREATE_TABLE = - "CREATE TABLE " - + TABLE_NAME - + " (" - + COLUMN_ID - + " TEXT PRIMARY KEY NOT NULL," - + COLUMN_TYPE - + " TEXT NOT NULL," - + COLUMN_URI - + " TEXT NOT NULL," - + COLUMN_CACHE_KEY - + " TEXT," - + COLUMN_STATE - + " INTEGER NOT NULL," - + COLUMN_DOWNLOAD_PERCENTAGE - + " REAL NOT NULL," - + COLUMN_DOWNLOADED_BYTES - + " INTEGER NOT NULL," - + COLUMN_TOTAL_BYTES - + " INTEGER NOT NULL," - + COLUMN_FAILURE_REASON - + " INTEGER NOT NULL," - + COLUMN_STOP_FLAGS - + " INTEGER NOT NULL," - + COLUMN_NOT_MET_REQUIREMENTS - + " INTEGER NOT NULL," - + COLUMN_START_TIME_MS - + " INTEGER NOT NULL," - + COLUMN_UPDATE_TIME_MS - + " INTEGER NOT NULL," - + COLUMN_STREAM_KEYS - + " TEXT NOT NULL," - + COLUMN_CUSTOM_METADATA - + " BLOB NOT NULL)"; - - private final DatabaseProvider databaseProvider; - - public DownloadsTable(DatabaseProvider databaseProvider) { - this.databaseProvider = databaseProvider; - int version = - VersionTable.getVersion( - databaseProvider.getReadableDatabase(), VersionTable.FEATURE_OFFLINE); - if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) { - SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.beginTransaction(); - try { - VersionTable.setVersion(writableDatabase, VersionTable.FEATURE_OFFLINE, TABLE_VERSION); - writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS); - writableDatabase.execSQL(SQL_CREATE_TABLE); - writableDatabase.setTransactionSuccessful(); - } finally { - writableDatabase.endTransaction(); - } - } else if (version < TABLE_VERSION) { - // There is no previous version currently. - throw new IllegalStateException(); - } - } - - public void replace(DownloadState downloadState) { - ContentValues values = new ContentValues(); - values.put(COLUMN_ID, downloadState.id); - values.put(COLUMN_TYPE, downloadState.type); - values.put(COLUMN_URI, downloadState.uri.toString()); - values.put(COLUMN_CACHE_KEY, downloadState.cacheKey); - values.put(COLUMN_STATE, downloadState.state); - values.put(COLUMN_DOWNLOAD_PERCENTAGE, downloadState.downloadPercentage); - values.put(COLUMN_DOWNLOADED_BYTES, downloadState.downloadedBytes); - values.put(COLUMN_TOTAL_BYTES, downloadState.totalBytes); - values.put(COLUMN_FAILURE_REASON, downloadState.failureReason); - values.put(COLUMN_STOP_FLAGS, downloadState.stopFlags); - values.put(COLUMN_NOT_MET_REQUIREMENTS, downloadState.notMetRequirements); - values.put(COLUMN_START_TIME_MS, downloadState.startTimeMs); - values.put(COLUMN_UPDATE_TIME_MS, downloadState.updateTimeMs); - values.put(COLUMN_STREAM_KEYS, encodeStreamKeys(downloadState.streamKeys)); - values.put(COLUMN_CUSTOM_METADATA, downloadState.customMetadata); - SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); - writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values); - } - - @Nullable - public DownloadState get(String id) { - String[] selectionArgs = {id}; - try (Cursor cursor = query(COLUMN_SELECTION_ID, selectionArgs)) { - if (cursor.getCount() == 0) { - return null; - } - cursor.moveToNext(); - DownloadState downloadState = getDownloadState(cursor); - Assertions.checkState(id.equals(downloadState.id)); - return downloadState; - } - } - - public DownloadStateCursor get(@DownloadState.State int... states) { - String selection = null; - if (states.length > 0) { - StringBuilder selectionBuilder = new StringBuilder(); - selectionBuilder.append(COLUMN_STATE).append(" IN ("); - for (int i = 0; i < states.length; i++) { - if (i > 0) { - selectionBuilder.append(','); - } - selectionBuilder.append(states[i]); - } - selectionBuilder.append(')'); - selection = selectionBuilder.toString(); - } - Cursor cursor = query(selection, /* selectionArgs= */ null); - return new DownloadStateCursorImpl(cursor); - } - - public void delete(String id) { - String[] selectionArgs = {id}; - databaseProvider.getWritableDatabase().delete(TABLE_NAME, COLUMN_SELECTION_ID, selectionArgs); - } - - private Cursor query(@Nullable String selection, @Nullable String[] selectionArgs) { - String sortOrder = COLUMN_START_TIME_MS + " ASC"; - return databaseProvider - .getReadableDatabase() - .query( - TABLE_NAME, - COLUMNS, - selection, - selectionArgs, - /* groupBy= */ null, - /* having= */ null, - sortOrder); - } - - private static DownloadState getDownloadState(Cursor cursor) { - return new DownloadState( - cursor.getString(COLUMN_INDEX_ID), - cursor.getString(COLUMN_INDEX_TYPE), - Uri.parse(cursor.getString(COLUMN_INDEX_URI)), - cursor.getString(COLUMN_INDEX_CACHE_KEY), - cursor.getInt(COLUMN_INDEX_STATE), - cursor.getFloat(COLUMN_INDEX_DOWNLOAD_PERCENTAGE), - cursor.getLong(COLUMN_INDEX_DOWNLOADED_BYTES), - cursor.getLong(COLUMN_INDEX_TOTAL_BYTES), - cursor.getInt(COLUMN_INDEX_FAILURE_REASON), - cursor.getInt(COLUMN_INDEX_STOP_FLAGS), - cursor.getInt(COLUMN_INDEX_NOT_MET_REQUIREMENTS), - cursor.getLong(COLUMN_INDEX_START_TIME_MS), - cursor.getLong(COLUMN_INDEX_UPDATE_TIME_MS), - decodeStreamKeys(cursor.getString(COLUMN_INDEX_STREAM_KEYS)), - cursor.getBlob(COLUMN_INDEX_CUSTOM_METADATA)); - } - - private static String encodeStreamKeys(StreamKey[] streamKeys) { - StringBuilder stringBuilder = new StringBuilder(); - for (StreamKey streamKey : streamKeys) { - stringBuilder - .append(streamKey.periodIndex) - .append('.') - .append(streamKey.groupIndex) - .append('.') - .append(streamKey.trackIndex) - .append(','); - } - if (stringBuilder.length() > 0) { - stringBuilder.setLength(stringBuilder.length() - 1); - } - return stringBuilder.toString(); - } - - private static StreamKey[] decodeStreamKeys(String encodedStreamKeys) { - if (encodedStreamKeys.isEmpty()) { - return new StreamKey[0]; - } - String[] streamKeysStrings = Util.split(encodedStreamKeys, ","); - int streamKeysCount = streamKeysStrings.length; - StreamKey[] streamKeys = new StreamKey[streamKeysCount]; - for (int i = 0; i < streamKeysCount; i++) { - String[] indices = Util.split(streamKeysStrings[i], "\\."); - Assertions.checkState(indices.length == 3); - streamKeys[i] = - new StreamKey( - Integer.parseInt(indices[0]), - Integer.parseInt(indices[1]), - Integer.parseInt(indices[2])); - } - return streamKeys; - } - } } From e3981ec48404ee440dd4ac2cd53ee90e0785300a Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Feb 2019 17:45:39 +0000 Subject: [PATCH 100/110] Fix notifications to avoid flicker on KitKat On KitKat you need to reuse the same notification builder when generating a notification that's intended to replace a previous one. See: https://stackoverflow.com/questions/6406730/updating-an-ongoing-notification-quietly PiperOrigin-RevId: 232503682 --- RELEASENOTES.md | 4 + .../exoplayer2/demo/DemoDownloadService.java | 27 ++- .../ui/DownloadNotificationHelper.java | 171 ++++++++++++++++++ .../ui/DownloadNotificationUtil.java | 100 ++-------- .../ui/PlayerNotificationManager.java | 143 ++++++++------- 5 files changed, 284 insertions(+), 161 deletions(-) create mode 100644 library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationHelper.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 817f9f2645..1278d36600 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -43,6 +43,10 @@ and `useSurfaceYuvOutput`. * Change signature of `PlayerNotificationManager.NotificationListener` to better fit service requirements. Remove ability to set a custom stop action. +* Fix issues with flickering notifications on KitKat. + `PlayerNotificationManager` has been fixed. Apps using + `DownloadNotificationUtil` should switch to using + `DownloadNotificationHelper`. ### 2.9.5 ### diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java index dcccd884ec..91e2aa5bcc 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoDownloadService.java @@ -20,7 +20,7 @@ import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.offline.DownloadState; import com.google.android.exoplayer2.scheduler.PlatformScheduler; -import com.google.android.exoplayer2.ui.DownloadNotificationUtil; +import com.google.android.exoplayer2.ui.DownloadNotificationHelper; import com.google.android.exoplayer2.util.NotificationUtil; import com.google.android.exoplayer2.util.Util; @@ -33,6 +33,8 @@ public class DemoDownloadService extends DownloadService { private static int nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1; + private DownloadNotificationHelper notificationHelper; + public DemoDownloadService() { super( FOREGROUND_NOTIFICATION_ID, @@ -42,6 +44,12 @@ public class DemoDownloadService extends DownloadService { nextNotificationId = FOREGROUND_NOTIFICATION_ID + 1; } + @Override + public void onCreate() { + super.onCreate(); + notificationHelper = new DownloadNotificationHelper(this, CHANNEL_ID); + } + @Override protected DownloadManager getDownloadManager() { return ((DemoApplication) getApplication()).getDownloadManager(); @@ -54,13 +62,8 @@ public class DemoDownloadService extends DownloadService { @Override protected Notification getForegroundNotification(DownloadState[] downloadStates) { - return DownloadNotificationUtil.buildProgressNotification( - /* context= */ this, - R.drawable.ic_download, - CHANNEL_ID, - /* contentIntent= */ null, - /* message= */ null, - downloadStates); + return notificationHelper.buildProgressNotification( + R.drawable.ic_download, /* contentIntent= */ null, /* message= */ null, downloadStates); } @Override @@ -68,18 +71,14 @@ public class DemoDownloadService extends DownloadService { Notification notification; if (downloadState.state == DownloadState.STATE_COMPLETED) { notification = - DownloadNotificationUtil.buildDownloadCompletedNotification( - /* context= */ this, + notificationHelper.buildDownloadCompletedNotification( R.drawable.ic_download_done, - CHANNEL_ID, /* contentIntent= */ null, Util.fromUtf8Bytes(downloadState.customMetadata)); } else if (downloadState.state == DownloadState.STATE_FAILED) { notification = - DownloadNotificationUtil.buildDownloadFailedNotification( - /* context= */ this, + notificationHelper.buildDownloadFailedNotification( R.drawable.ic_download_done, - CHANNEL_ID, /* contentIntent= */ null, Util.fromUtf8Bytes(downloadState.customMetadata)); } else { diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationHelper.java new file mode 100644 index 0000000000..b65c4fca40 --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationHelper.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ui; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.support.annotation.DrawableRes; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.v4.app.NotificationCompat; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.offline.DownloadState; + +/** Helper for creating download notifications. */ +public final class DownloadNotificationHelper { + + private static final @StringRes int NULL_STRING_ID = 0; + + private final Context context; + private final NotificationCompat.Builder notificationBuilder; + + /** + * @param context A context. + * @param channelId The id of the notification channel to use. + */ + public DownloadNotificationHelper(Context context, String channelId) { + context = context.getApplicationContext(); + this.context = context; + this.notificationBuilder = new NotificationCompat.Builder(context, channelId); + } + + /** + * Returns a progress notification for the given download states. + * + * @param smallIcon A small icon for the notification. + * @param contentIntent An optional content intent to send when the notification is clicked. + * @param message An optional message to display on the notification. + * @param downloadStates The download states. + * @return The notification. + */ + public Notification buildProgressNotification( + @DrawableRes int smallIcon, + @Nullable PendingIntent contentIntent, + @Nullable String message, + DownloadState[] downloadStates) { + float totalPercentage = 0; + int downloadTaskCount = 0; + boolean allDownloadPercentagesUnknown = true; + boolean haveDownloadedBytes = false; + boolean haveDownloadTasks = false; + boolean haveRemoveTasks = false; + for (DownloadState downloadState : downloadStates) { + if (downloadState.state == DownloadState.STATE_REMOVING + || downloadState.state == DownloadState.STATE_RESTARTING + || downloadState.state == DownloadState.STATE_REMOVED) { + haveRemoveTasks = true; + continue; + } + if (downloadState.state != DownloadState.STATE_DOWNLOADING + && downloadState.state != DownloadState.STATE_COMPLETED) { + continue; + } + haveDownloadTasks = true; + if (downloadState.downloadPercentage != C.PERCENTAGE_UNSET) { + allDownloadPercentagesUnknown = false; + totalPercentage += downloadState.downloadPercentage; + } + haveDownloadedBytes |= downloadState.downloadedBytes > 0; + downloadTaskCount++; + } + + int titleStringId = + haveDownloadTasks + ? R.string.exo_download_downloading + : (haveRemoveTasks ? R.string.exo_download_removing : NULL_STRING_ID); + int progress = 0; + boolean indeterminate = true; + if (haveDownloadTasks) { + progress = (int) (totalPercentage / downloadTaskCount); + indeterminate = allDownloadPercentagesUnknown && haveDownloadedBytes; + } + return buildNotification( + smallIcon, + contentIntent, + message, + titleStringId, + progress, + indeterminate, + /* ongoing= */ true, + /* showWhen= */ false); + } + + /** + * Returns a notification for a completed download. + * + * @param smallIcon A small icon for the notifications. + * @param contentIntent An optional content intent to send when the notification is clicked. + * @param message An optional message to display on the notification. + * @return The notification. + */ + public Notification buildDownloadCompletedNotification( + @DrawableRes int smallIcon, @Nullable PendingIntent contentIntent, @Nullable String message) { + int titleStringId = R.string.exo_download_completed; + return buildEndStateNotification(smallIcon, contentIntent, message, titleStringId); + } + + /** + * Returns a notification for a failed download. + * + * @param smallIcon A small icon for the notifications. + * @param contentIntent An optional content intent to send when the notification is clicked. + * @param message An optional message to display on the notification. + * @return The notification. + */ + public Notification buildDownloadFailedNotification( + @DrawableRes int smallIcon, @Nullable PendingIntent contentIntent, @Nullable String message) { + @StringRes int titleStringId = R.string.exo_download_failed; + return buildEndStateNotification(smallIcon, contentIntent, message, titleStringId); + } + + private Notification buildEndStateNotification( + @DrawableRes int smallIcon, + @Nullable PendingIntent contentIntent, + @Nullable String message, + @StringRes int titleStringId) { + return buildNotification( + smallIcon, + contentIntent, + message, + titleStringId, + /* progress= */ 0, + /* indeterminateProgress= */ false, + /* ongoing= */ false, + /* showWhen= */ true); + } + + private Notification buildNotification( + @DrawableRes int smallIcon, + @Nullable PendingIntent contentIntent, + @Nullable String message, + @StringRes int titleStringId, + int progress, + boolean indeterminateProgress, + boolean ongoing, + boolean showWhen) { + notificationBuilder.setSmallIcon(smallIcon); + notificationBuilder.setContentTitle( + titleStringId == NULL_STRING_ID ? null : context.getResources().getString(titleStringId)); + notificationBuilder.setContentIntent(contentIntent); + notificationBuilder.setStyle( + message == null ? null : new NotificationCompat.BigTextStyle().bigText(message)); + notificationBuilder.setProgress(/* max= */ 100, progress, indeterminateProgress); + notificationBuilder.setOngoing(ongoing); + notificationBuilder.setShowWhen(showWhen); + return notificationBuilder.build(); + } +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java index 2d1656af57..b9e952e62f 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationUtil.java @@ -20,16 +20,16 @@ import android.app.PendingIntent; import android.content.Context; import android.support.annotation.DrawableRes; import android.support.annotation.Nullable; -import android.support.annotation.StringRes; -import android.support.v4.app.NotificationCompat; -import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.offline.DownloadState; +import com.google.android.exoplayer2.util.Util; -/** Helper for creating download notifications. */ +/** + * @deprecated Using this class can cause notifications to flicker on devices with {@link + * Util#SDK_INT} < 21. Use {@link DownloadNotificationHelper} instead. + */ +@Deprecated public final class DownloadNotificationUtil { - private static final @StringRes int NULL_STRING_ID = 0; - private DownloadNotificationUtil() {} /** @@ -37,8 +37,7 @@ public final class DownloadNotificationUtil { * * @param context A context for accessing resources. * @param smallIcon A small icon for the notification. - * @param channelId The id of the notification channel to use. Only required for API level 26 and - * above. + * @param channelId The id of the notification channel to use. * @param contentIntent An optional content intent to send when the notification is clicked. * @param message An optional message to display on the notification. * @param downloadStates The download states. @@ -51,50 +50,8 @@ public final class DownloadNotificationUtil { @Nullable PendingIntent contentIntent, @Nullable String message, DownloadState[] downloadStates) { - float totalPercentage = 0; - int downloadTaskCount = 0; - boolean allDownloadPercentagesUnknown = true; - boolean haveDownloadedBytes = false; - boolean haveDownloadTasks = false; - boolean haveRemoveTasks = false; - for (DownloadState downloadState : downloadStates) { - if (downloadState.state == DownloadState.STATE_REMOVING - || downloadState.state == DownloadState.STATE_RESTARTING - || downloadState.state == DownloadState.STATE_REMOVED) { - haveRemoveTasks = true; - continue; - } - if (downloadState.state != DownloadState.STATE_DOWNLOADING - && downloadState.state != DownloadState.STATE_COMPLETED) { - continue; - } - haveDownloadTasks = true; - if (downloadState.downloadPercentage != C.PERCENTAGE_UNSET) { - allDownloadPercentagesUnknown = false; - totalPercentage += downloadState.downloadPercentage; - } - haveDownloadedBytes |= downloadState.downloadedBytes > 0; - downloadTaskCount++; - } - - int titleStringId = - haveDownloadTasks - ? R.string.exo_download_downloading - : (haveRemoveTasks ? R.string.exo_download_removing : NULL_STRING_ID); - NotificationCompat.Builder notificationBuilder = - newNotificationBuilder( - context, smallIcon, channelId, contentIntent, message, titleStringId); - - int progress = 0; - boolean indeterminate = true; - if (haveDownloadTasks) { - progress = (int) (totalPercentage / downloadTaskCount); - indeterminate = allDownloadPercentagesUnknown && haveDownloadedBytes; - } - notificationBuilder.setProgress(/* max= */ 100, progress, indeterminate); - notificationBuilder.setOngoing(true); - notificationBuilder.setShowWhen(false); - return notificationBuilder.build(); + return new DownloadNotificationHelper(context, channelId) + .buildProgressNotification(smallIcon, contentIntent, message, downloadStates); } /** @@ -102,8 +59,7 @@ public final class DownloadNotificationUtil { * * @param context A context for accessing resources. * @param smallIcon A small icon for the notifications. - * @param channelId The id of the notification channel to use. Only required for API level 26 and - * above. + * @param channelId The id of the notification channel to use. * @param contentIntent An optional content intent to send when the notification is clicked. * @param message An optional message to display on the notification. * @return The notification. @@ -114,10 +70,8 @@ public final class DownloadNotificationUtil { String channelId, @Nullable PendingIntent contentIntent, @Nullable String message) { - int titleStringId = R.string.exo_download_completed; - return newNotificationBuilder( - context, smallIcon, channelId, contentIntent, message, titleStringId) - .build(); + return new DownloadNotificationHelper(context, channelId) + .buildDownloadCompletedNotification(smallIcon, contentIntent, message); } /** @@ -125,8 +79,7 @@ public final class DownloadNotificationUtil { * * @param context A context for accessing resources. * @param smallIcon A small icon for the notifications. - * @param channelId The id of the notification channel to use. Only required for API level 26 and - * above. + * @param channelId The id of the notification channel to use. * @param contentIntent An optional content intent to send when the notification is clicked. * @param message An optional message to display on the notification. * @return The notification. @@ -137,30 +90,7 @@ public final class DownloadNotificationUtil { String channelId, @Nullable PendingIntent contentIntent, @Nullable String message) { - @StringRes int titleStringId = R.string.exo_download_failed; - return newNotificationBuilder( - context, smallIcon, channelId, contentIntent, message, titleStringId) - .build(); - } - - private static NotificationCompat.Builder newNotificationBuilder( - Context context, - @DrawableRes int smallIcon, - String channelId, - @Nullable PendingIntent contentIntent, - @Nullable String message, - @StringRes int titleStringId) { - NotificationCompat.Builder notificationBuilder = - new NotificationCompat.Builder(context, channelId).setSmallIcon(smallIcon); - if (titleStringId != NULL_STRING_ID) { - notificationBuilder.setContentTitle(context.getResources().getString(titleStringId)); - } - if (contentIntent != null) { - notificationBuilder.setContentIntent(contentIntent); - } - if (message != null) { - notificationBuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(message)); - } - return notificationBuilder; + return new DownloadNotificationHelper(context, channelId) + .buildDownloadFailedNotification(smallIcon, contentIntent, message); } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index 597f0dbd40..6634fdf820 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -338,6 +338,7 @@ public class PlayerNotificationManager { private final Context context; private final String channelId; + private final NotificationCompat.Builder builder; private final int notificationId; private final MediaDescriptionAdapter mediaDescriptionAdapter; private final @Nullable CustomActionReceiver customActionReceiver; @@ -530,12 +531,14 @@ public class PlayerNotificationManager { MediaDescriptionAdapter mediaDescriptionAdapter, @Nullable NotificationListener notificationListener, @Nullable CustomActionReceiver customActionReceiver) { - this.context = context.getApplicationContext(); + context = context.getApplicationContext(); + this.context = context; this.channelId = channelId; this.notificationId = notificationId; this.mediaDescriptionAdapter = mediaDescriptionAdapter; this.notificationListener = notificationListener; this.customActionReceiver = customActionReceiver; + builder = new NotificationCompat.Builder(context, channelId); controlDispatcher = new DefaultControlDispatcher(); window = new Timeline.Window(); instanceId = instanceIdCounter++; @@ -887,7 +890,7 @@ public class PlayerNotificationManager { private Notification startOrUpdateNotification(@Nullable Bitmap bitmap) { Player player = this.player; boolean ongoing = getOngoing(player); - Notification notification = createNotification(player, ongoing, bitmap); + Notification notification = createNotification(player, builder, ongoing, bitmap); if (notification == null) { stopNotification(/* dismissedByUser= */ false); return null; @@ -923,6 +926,11 @@ public class PlayerNotificationManager { * Creates the notification given the current player state. * * @param player The player for which state to build a notification. + * @param builder A builder that can optionally be used for creating the notification. The same + * builder is passed each time this method is called, since reusing the same builder can + * prevent notification flicker when {@code Util#SDK_INT} < 21. This means implementations + * must take care to ensure anything set on the builder during a previous call is cleared, if + * no longer required. * @param ongoing Whether the notification should be ongoing. * @param largeIcon The large icon to be used. * @return The {@link Notification} which has been built, or {@code null} if no notification @@ -930,11 +938,15 @@ public class PlayerNotificationManager { */ @Nullable protected Notification createNotification( - Player player, boolean ongoing, @Nullable Bitmap largeIcon) { + Player player, + NotificationCompat.Builder builder, + boolean ongoing, + @Nullable Bitmap largeIcon) { if (player.getPlaybackState() == Player.STATE_IDLE) { return null; } - NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId); + + builder.mActions.clear(); List actionNames = getActions(player); for (int i = 0; i < actionNames.size(); i++) { String actionName = actionNames.get(i); @@ -946,7 +958,7 @@ public class PlayerNotificationManager { builder.addAction(action); } } - // Create a media style notification. + MediaStyle mediaStyle = new MediaStyle(); if (mediaSessionToken != null) { mediaStyle.setMediaSession(mediaSessionToken); @@ -955,9 +967,11 @@ public class PlayerNotificationManager { // Configure dismiss action prior to API 21 ('x' button). mediaStyle.setShowCancelButton(!ongoing); mediaStyle.setCancelButtonIntent(dismissPendingIntent); + builder.setStyle(mediaStyle); + // Set intent which is sent if the user selects 'clear all' builder.setDeleteIntent(dismissPendingIntent); - builder.setStyle(mediaStyle); + // Set notification properties from getters. builder .setBadgeIconType(badgeIconType) @@ -968,7 +982,10 @@ public class PlayerNotificationManager { .setVisibility(visibility) .setPriority(priority) .setDefaults(defaults); - if (useChronometer + + // Changing "showWhen" causes notification flicker if SDK_INT < 21. + if (Util.SDK_INT >= 21 + && useChronometer && !player.isPlayingAd() && !player.isCurrentWindowDynamic() && player.getPlayWhenReady() @@ -980,6 +997,7 @@ public class PlayerNotificationManager { } else { builder.setShowWhen(false).setUsesChronometer(false); } + // Set media specific notification properties from MediaDescriptionAdapter. builder.setContentTitle(mediaDescriptionAdapter.getCurrentContentTitle(player)); builder.setContentText(mediaDescriptionAdapter.getCurrentContentText(player)); @@ -989,13 +1007,9 @@ public class PlayerNotificationManager { mediaDescriptionAdapter.getCurrentLargeIcon( player, new BitmapCallback(++currentNotificationTag)); } - if (largeIcon != null) { - builder.setLargeIcon(largeIcon); - } - PendingIntent contentIntent = mediaDescriptionAdapter.createCurrentContentIntent(player); - if (contentIntent != null) { - builder.setContentIntent(contentIntent); - } + setLargeIcon(builder, largeIcon); + builder.setContentIntent(mediaDescriptionAdapter.createCurrentContentIntent(player)); + return builder.build(); } @@ -1086,54 +1100,6 @@ public class PlayerNotificationManager { && player.getPlayWhenReady(); } - private static Map createPlaybackActions( - Context context, int instanceId) { - Map actions = new HashMap<>(); - actions.put( - ACTION_PLAY, - new NotificationCompat.Action( - R.drawable.exo_notification_play, - context.getString(R.string.exo_controls_play_description), - createBroadcastIntent(ACTION_PLAY, context, instanceId))); - actions.put( - ACTION_PAUSE, - new NotificationCompat.Action( - R.drawable.exo_notification_pause, - context.getString(R.string.exo_controls_pause_description), - createBroadcastIntent(ACTION_PAUSE, context, instanceId))); - actions.put( - ACTION_STOP, - new NotificationCompat.Action( - R.drawable.exo_notification_stop, - context.getString(R.string.exo_controls_stop_description), - createBroadcastIntent(ACTION_STOP, context, instanceId))); - actions.put( - ACTION_REWIND, - new NotificationCompat.Action( - R.drawable.exo_notification_rewind, - context.getString(R.string.exo_controls_rewind_description), - createBroadcastIntent(ACTION_REWIND, context, instanceId))); - actions.put( - ACTION_FAST_FORWARD, - new NotificationCompat.Action( - R.drawable.exo_notification_fastforward, - context.getString(R.string.exo_controls_fastforward_description), - createBroadcastIntent(ACTION_FAST_FORWARD, context, instanceId))); - actions.put( - ACTION_PREVIOUS, - new NotificationCompat.Action( - R.drawable.exo_notification_previous, - context.getString(R.string.exo_controls_previous_description), - createBroadcastIntent(ACTION_PREVIOUS, context, instanceId))); - actions.put( - ACTION_NEXT, - new NotificationCompat.Action( - R.drawable.exo_notification_next, - context.getString(R.string.exo_controls_next_description), - createBroadcastIntent(ACTION_NEXT, context, instanceId))); - return actions; - } - private void previous(Player player) { Timeline timeline = player.getCurrentTimeline(); if (timeline.isEmpty() || player.isPlayingAd()) { @@ -1196,6 +1162,54 @@ public class PlayerNotificationManager { && player.getPlayWhenReady(); } + private static Map createPlaybackActions( + Context context, int instanceId) { + Map actions = new HashMap<>(); + actions.put( + ACTION_PLAY, + new NotificationCompat.Action( + R.drawable.exo_notification_play, + context.getString(R.string.exo_controls_play_description), + createBroadcastIntent(ACTION_PLAY, context, instanceId))); + actions.put( + ACTION_PAUSE, + new NotificationCompat.Action( + R.drawable.exo_notification_pause, + context.getString(R.string.exo_controls_pause_description), + createBroadcastIntent(ACTION_PAUSE, context, instanceId))); + actions.put( + ACTION_STOP, + new NotificationCompat.Action( + R.drawable.exo_notification_stop, + context.getString(R.string.exo_controls_stop_description), + createBroadcastIntent(ACTION_STOP, context, instanceId))); + actions.put( + ACTION_REWIND, + new NotificationCompat.Action( + R.drawable.exo_notification_rewind, + context.getString(R.string.exo_controls_rewind_description), + createBroadcastIntent(ACTION_REWIND, context, instanceId))); + actions.put( + ACTION_FAST_FORWARD, + new NotificationCompat.Action( + R.drawable.exo_notification_fastforward, + context.getString(R.string.exo_controls_fastforward_description), + createBroadcastIntent(ACTION_FAST_FORWARD, context, instanceId))); + actions.put( + ACTION_PREVIOUS, + new NotificationCompat.Action( + R.drawable.exo_notification_previous, + context.getString(R.string.exo_controls_previous_description), + createBroadcastIntent(ACTION_PREVIOUS, context, instanceId))); + actions.put( + ACTION_NEXT, + new NotificationCompat.Action( + R.drawable.exo_notification_next, + context.getString(R.string.exo_controls_next_description), + createBroadcastIntent(ACTION_NEXT, context, instanceId))); + return actions; + } + private static PendingIntent createBroadcastIntent( String action, Context context, int instanceId) { Intent intent = new Intent(action).setPackage(context.getPackageName()); @@ -1204,6 +1218,11 @@ public class PlayerNotificationManager { context, instanceId, intent, PendingIntent.FLAG_CANCEL_CURRENT); } + @SuppressWarnings("nullness:argument.type.incompatible") + private static void setLargeIcon(NotificationCompat.Builder builder, @Nullable Bitmap largeIcon) { + builder.setLargeIcon(largeIcon); + } + private class PlayerListener implements Player.EventListener { @Override From 589af35c65b98cbb2ef921dd0d2120b1b04d251e Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 5 Feb 2019 17:46:03 +0000 Subject: [PATCH 101/110] Set player on ads loader in IMA demo Issue: #5476 PiperOrigin-RevId: 232503736 --- .../android/exoplayer2/imademo/PlayerManager.java | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java index 740c1a0af6..97c3299a4a 100644 --- a/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java +++ b/demos/ima/src/main/java/com/google/android/exoplayer2/imademo/PlayerManager.java @@ -29,10 +29,6 @@ import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -56,14 +52,9 @@ import com.google.android.exoplayer2.util.Util; } public void init(Context context, PlayerView playerView) { - // Create a default track selector. - TrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory(); - TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); - // Create a player instance. - player = ExoPlayerFactory.newSimpleInstance(context, trackSelector); - - // Bind the player to the view. + player = ExoPlayerFactory.newSimpleInstance(context); + adsLoader.setPlayer(player); playerView.setPlayer(player); // This is the MediaSource representing the content media (i.e. not the ad). @@ -89,6 +80,7 @@ import com.google.android.exoplayer2.util.Util; contentPosition = player.getContentPosition(); player.release(); player = null; + adsLoader.setPlayer(null); } } From 6b81d9e7a4f6c8cfdc309c4c61af362fe03051c3 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Feb 2019 18:04:25 +0000 Subject: [PATCH 102/110] Fix DownloadHelper for some HLS streams onContinueLoadingRequested can occur during preparation, so MediaPreparer needs to handle it. PiperOrigin-RevId: 232507267 --- .../exoplayer2/offline/DownloadHelper.java | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index 25c4eca5eb..d12013673f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -744,6 +744,7 @@ public final class DownloadHelper { private static final int MESSAGE_PREPARE_SOURCE = 0; private static final int MESSAGE_CHECK_FOR_FAILURE = 1; + private static final int MESSAGE_CONTINUE_LOADING = 2; private final MediaSource mediaSource; private final DownloadHelper downloadHelper; @@ -755,7 +756,7 @@ public final class DownloadHelper { public @MonotonicNonNull Timeline timeline; public MediaPeriod @MonotonicNonNull [] mediaPeriods; - private int pendingPreparations; + private final ArrayList pendingMediaPeriods; public MediaPreparer(MediaSource mediaSource, DownloadHelper downloadHelper) { this.mediaSource = mediaSource; @@ -765,6 +766,7 @@ public final class DownloadHelper { mediaSourceThread.start(); mediaSourceHandler = Util.createHandler(mediaSourceThread.getLooper(), /* callback= */ this); mediaSourceHandler.sendEmptyMessage(MESSAGE_PREPARE_SOURCE); + pendingMediaPeriods = new ArrayList<>(); } public void release() { @@ -791,8 +793,8 @@ public final class DownloadHelper { if (mediaPeriods == null) { mediaSource.maybeThrowSourceInfoRefreshError(); } else { - for (MediaPeriod mediaPeriod : mediaPeriods) { - mediaPeriod.maybeThrowPrepareError(); + for (int i = 0; i < pendingMediaPeriods.size(); i++) { + pendingMediaPeriods.get(i).maybeThrowPrepareError(); } } mediaSourceHandler.sendEmptyMessageDelayed( @@ -801,6 +803,12 @@ public final class DownloadHelper { downloadHelper.onMediaPreparationFailed(e); } return true; + case MESSAGE_CONTINUE_LOADING: + MediaPeriod mediaPeriod = (MediaPeriod) msg.obj; + if (pendingMediaPeriods.contains(mediaPeriod)) { + mediaPeriod.continueLoading(/* positionUs= */ 0); + } + return true; default: return false; } @@ -818,14 +826,15 @@ public final class DownloadHelper { this.timeline = timeline; this.manifest = manifest; mediaPeriods = new MediaPeriod[timeline.getPeriodCount()]; - pendingPreparations = mediaPeriods.length; for (int i = 0; i < mediaPeriods.length; i++) { - mediaPeriods[i] = + MediaPeriod mediaPeriod = mediaSource.createPeriod( new MediaPeriodId(timeline.getUidOfPeriod(/* periodIndex= */ i)), allocator, /* startPositionUs= */ 0); - mediaPeriods[i].prepare(/* callback= */ this, /* positionUs= */ 0); + mediaPeriods[i] = mediaPeriod; + pendingMediaPeriods.add(mediaPeriod); + mediaPeriod.prepare(/* callback= */ this, /* positionUs= */ 0); } } @@ -833,16 +842,18 @@ public final class DownloadHelper { @Override public void onPrepared(MediaPeriod mediaPeriod) { - pendingPreparations--; - if (pendingPreparations == 0) { + pendingMediaPeriods.remove(mediaPeriod); + if (pendingMediaPeriods.isEmpty()) { mediaSourceHandler.removeMessages(MESSAGE_CHECK_FOR_FAILURE); downloadHelper.onMediaPrepared(); } } @Override - public void onContinueLoadingRequested(MediaPeriod source) { - // Ignore. + public void onContinueLoadingRequested(MediaPeriod mediaPeriod) { + if (pendingMediaPeriods.contains(mediaPeriod)) { + mediaSourceHandler.obtainMessage(MESSAGE_CONTINUE_LOADING, mediaPeriod).sendToTarget(); + } } } From fc2a99a68881d0381d81e1dbcded1b0d808355c1 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Tue, 5 Feb 2019 18:04:30 +0000 Subject: [PATCH 103/110] Generalize selectedVariantIndices in preparation for non-main adaptation After this change, multiple HlsSampleStreamWrappers may contain an apdaptive track group. PiperOrigin-RevId: 232507292 --- .../exoplayer2/source/hls/HlsMediaPeriod.java | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java index 73a46b68ba..0c4ebcb508 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaPeriod.java @@ -69,7 +69,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper private TrackGroupArray trackGroups; private HlsSampleStreamWrapper[] sampleStreamWrappers; private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; - private int[] selectedVariantIndices; + // Maps sample stream wrappers to variant/rendition index by matching array positions. + private int[][] manifestUrlsIndicesPerWrapper; private SequenceableLoader compositeSequenceableLoader; private boolean notifiedReadingStarted; @@ -114,7 +115,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper timestampAdjusterProvider = new TimestampAdjusterProvider(); sampleStreamWrappers = new HlsSampleStreamWrapper[0]; enabledSampleStreamWrappers = new HlsSampleStreamWrapper[0]; - selectedVariantIndices = new int[0]; + manifestUrlsIndicesPerWrapper = new int[0][]; eventDispatcher.mediaPeriodCreated(); } @@ -156,11 +157,14 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper TrackGroupArray mainWrapperTrackGroups; int mainWrapperPrimaryGroupIndex; + int[] mainWrapperVariantIndices; if (hasVariants) { HlsSampleStreamWrapper mainWrapper = sampleStreamWrappers[0]; + mainWrapperVariantIndices = manifestUrlsIndicesPerWrapper[0]; mainWrapperTrackGroups = mainWrapper.getTrackGroups(); mainWrapperPrimaryGroupIndex = mainWrapper.getPrimaryTrackGroupIndex(); } else { + mainWrapperVariantIndices = new int[0]; mainWrapperTrackGroups = TrackGroupArray.EMPTY; mainWrapperPrimaryGroupIndex = 0; } @@ -176,7 +180,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper // Primary group in main wrapper. hasPrimaryTrackGroupSelection = true; for (int i = 0; i < trackSelection.length(); i++) { - int variantIndex = selectedVariantIndices[trackSelection.getIndexInTrackGroup(i)]; + int variantIndex = mainWrapperVariantIndices[trackSelection.getIndexInTrackGroup(i)]; streamKeys.add(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, variantIndex)); } } else { @@ -188,13 +192,11 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper for (int i = audioWrapperOffset; i < sampleStreamWrappers.length; i++) { TrackGroupArray wrapperTrackGroups = sampleStreamWrappers[i].getTrackGroups(); if (wrapperTrackGroups.indexOf(trackSelectionGroup) != C.INDEX_UNSET) { - if (i < subtitleWrapperOffset) { - streamKeys.add( - new StreamKey(HlsMasterPlaylist.GROUP_INDEX_AUDIO, i - audioWrapperOffset)); - } else { - streamKeys.add( - new StreamKey(HlsMasterPlaylist.GROUP_INDEX_SUBTITLE, i - subtitleWrapperOffset)); - } + int groupIndexType = + i < subtitleWrapperOffset + ? HlsMasterPlaylist.GROUP_INDEX_AUDIO + : HlsMasterPlaylist.GROUP_INDEX_SUBTITLE; + streamKeys.add(new StreamKey(groupIndexType, manifestUrlsIndicesPerWrapper[i][0])); break; } } @@ -203,13 +205,14 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper if (needsPrimaryTrackGroupSelection && !hasPrimaryTrackGroupSelection) { // A track selection includes a variant-embedded track, but no variant is added yet. We use // the valid variant with the lowest bitrate to reduce overhead. - int lowestBitrateIndex = selectedVariantIndices[0]; - int lowestBitrate = masterPlaylist.variants.get(selectedVariantIndices[0]).format.bitrate; - for (int i = 1; i < selectedVariantIndices.length; i++) { - int variantBitrate = masterPlaylist.variants.get(selectedVariantIndices[i]).format.bitrate; + int lowestBitrateIndex = mainWrapperVariantIndices[0]; + int lowestBitrate = masterPlaylist.variants.get(mainWrapperVariantIndices[0]).format.bitrate; + for (int i = 1; i < mainWrapperVariantIndices.length; i++) { + int variantBitrate = + masterPlaylist.variants.get(mainWrapperVariantIndices[i]).format.bitrate; if (variantBitrate < lowestBitrate) { lowestBitrate = variantBitrate; - lowestBitrateIndex = selectedVariantIndices[i]; + lowestBitrateIndex = mainWrapperVariantIndices[i]; } } streamKeys.add(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, lowestBitrateIndex)); @@ -423,6 +426,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper int wrapperCount = (hasVariants ? 1 : 0) + audioRenditions.size() + subtitleRenditions.size(); sampleStreamWrappers = new HlsSampleStreamWrapper[wrapperCount]; + manifestUrlsIndicesPerWrapper = new int[wrapperCount][]; pendingPrepareCount = wrapperCount; int currentWrapperIndex = 0; @@ -443,6 +447,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper null, Collections.emptyList(), positionUs); + manifestUrlsIndicesPerWrapper[currentWrapperIndex] = new int[] {i}; sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; Format renditionFormat = audioRendition.format; if (allowChunklessPreparation && renditionFormat.codecs != null) { @@ -457,6 +462,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper( C.TRACK_TYPE_TEXT, new HlsUrl[] {url}, null, Collections.emptyList(), positionUs); + manifestUrlsIndicesPerWrapper[currentWrapperIndex] = new int[] {i}; sampleStreamWrappers[currentWrapperIndex++] = sampleStreamWrapper; sampleStreamWrapper.prepareWithMasterPlaylistInfo( new TrackGroupArray(new TrackGroup(url.format)), 0, TrackGroupArray.EMPTY); @@ -530,13 +536,13 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper selectedVariantsCount = variantTypes.length - audioVariantCount; } HlsUrl[] selectedVariants = new HlsUrl[selectedVariantsCount]; - selectedVariantIndices = new int[selectedVariantsCount]; + manifestUrlsIndicesPerWrapper[0] = new int[selectedVariantsCount]; int outIndex = 0; for (int i = 0; i < masterPlaylist.variants.size(); i++) { if ((!useVideoVariantsOnly || variantTypes[i] == C.TRACK_TYPE_VIDEO) && (!useNonAudioVariantsOnly || variantTypes[i] != C.TRACK_TYPE_AUDIO)) { selectedVariants[outIndex] = masterPlaylist.variants.get(i); - selectedVariantIndices[outIndex++] = i; + manifestUrlsIndicesPerWrapper[0][outIndex++] = i; } } String codecs = selectedVariants[0].format.codecs; From 0bf9ebf91e30e2699f4e9eef723c65f457070dd7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 5 Feb 2019 18:05:09 +0000 Subject: [PATCH 104/110] Switch to non-deprecated way of setting AdDisplayContainer PiperOrigin-RevId: 232507469 --- .../android/exoplayer2/ext/ima/ImaAdsLoader.java | 12 ++++++------ .../exoplayer2/ext/ima/SingletonImaFactory.java | 5 +++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 311752c7ab..4bdec23804 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -466,11 +466,11 @@ public final class ImaAdsLoader } imaSdkSettings.setPlayerType(IMA_SDK_SETTINGS_PLAYER_TYPE); imaSdkSettings.setPlayerVersion(IMA_SDK_SETTINGS_PLAYER_VERSION); - adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings); period = new Timeline.Period(); adCallbacks = new ArrayList<>(/* initialCapacity= */ 1); adDisplayContainer = imaFactory.createAdDisplayContainer(); adDisplayContainer.setPlayer(/* videoAdPlayer= */ this); + adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer); adsLoader.addAdErrorListener(/* adErrorListener= */ this); adsLoader.addAdsLoadedListener(/* adsLoadedListener= */ this); fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; @@ -524,7 +524,6 @@ public final class ImaAdsLoader if (vastLoadTimeoutMs != TIMEOUT_UNSET) { request.setVastLoadTimeout(vastLoadTimeoutMs); } - request.setAdDisplayContainer(adDisplayContainer); request.setContentProgressProvider(this); request.setUserRequestContext(pendingAdRequestContext); adsLoader.requestAds(request); @@ -1374,9 +1373,9 @@ public final class ImaAdsLoader AdDisplayContainer createAdDisplayContainer(); /** @see com.google.ads.interactivemedia.v3.api.ImaSdkFactory#createAdsRequest() */ AdsRequest createAdsRequest(); - /** @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings) */ + /** @see ImaSdkFactory#createAdsLoader(Context, ImaSdkSettings, AdDisplayContainer) */ com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader( - Context context, ImaSdkSettings imaSdkSettings); + Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer); } /** Default {@link ImaFactory} for non-test usage, which delegates to {@link ImaSdkFactory}. */ @@ -1403,8 +1402,9 @@ public final class ImaAdsLoader @Override public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader( - Context context, ImaSdkSettings imaSdkSettings) { - return ImaSdkFactory.getInstance().createAdsLoader(context, imaSdkSettings); + Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) { + return ImaSdkFactory.getInstance() + .createAdsLoader(context, imaSdkSettings, adDisplayContainer); } } } diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java index dd46d8a68b..4efd8cf38c 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/SingletonImaFactory.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.ext.ima; import android.content.Context; import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; +import com.google.ads.interactivemedia.v3.api.AdsLoader; import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; import com.google.ads.interactivemedia.v3.api.AdsRequest; import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; @@ -64,8 +65,8 @@ final class SingletonImaFactory implements ImaAdsLoader.ImaFactory { } @Override - public com.google.ads.interactivemedia.v3.api.AdsLoader createAdsLoader( - Context context, ImaSdkSettings imaSdkSettings) { + public AdsLoader createAdsLoader( + Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer) { return adsLoader; } } From ebfbb4f915d5dce884e31c4f7685fb9bbf25c552 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Feb 2019 19:05:59 +0000 Subject: [PATCH 105/110] Hide progress bar for end state download notifications Now we're reusing the builder, it's necessary to set maxProgress back to 0 to avoid the progress bar appearing for end state notifications. PiperOrigin-RevId: 232520007 --- .../exoplayer2/ui/DownloadNotificationHelper.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationHelper.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationHelper.java index b65c4fca40..94bd0b81c5 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationHelper.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/DownloadNotificationHelper.java @@ -98,6 +98,7 @@ public final class DownloadNotificationHelper { contentIntent, message, titleStringId, + /* maxProgress= */ 100, progress, indeterminate, /* ongoing= */ true, @@ -142,7 +143,8 @@ public final class DownloadNotificationHelper { contentIntent, message, titleStringId, - /* progress= */ 0, + /* maxProgress= */ 0, + /* currentProgress= */ 0, /* indeterminateProgress= */ false, /* ongoing= */ false, /* showWhen= */ true); @@ -153,7 +155,8 @@ public final class DownloadNotificationHelper { @Nullable PendingIntent contentIntent, @Nullable String message, @StringRes int titleStringId, - int progress, + int maxProgress, + int currentProgress, boolean indeterminateProgress, boolean ongoing, boolean showWhen) { @@ -163,7 +166,7 @@ public final class DownloadNotificationHelper { notificationBuilder.setContentIntent(contentIntent); notificationBuilder.setStyle( message == null ? null : new NotificationCompat.BigTextStyle().bigText(message)); - notificationBuilder.setProgress(/* max= */ 100, progress, indeterminateProgress); + notificationBuilder.setProgress(maxProgress, currentProgress, indeterminateProgress); notificationBuilder.setOngoing(ongoing); notificationBuilder.setShowWhen(showWhen); return notificationBuilder.build(); From 2169b9417faf249dc154acf969d749f6e1abf4b9 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 6 Feb 2019 12:23:57 +0000 Subject: [PATCH 106/110] Fix HEVC level error logging PiperOrigin-RevId: 232651944 --- .../google/android/exoplayer2/mediacodec/MediaCodecUtil.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java index 55459692c9..95cf82ff6c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecUtil.java @@ -445,9 +445,10 @@ public final class MediaCodecUtil { Log.w(TAG, "Unknown HEVC profile string: " + profileString); return null; } - Integer level = HEVC_CODEC_STRING_TO_PROFILE_LEVEL.get(parts[3]); + String levelString = parts[3]; + Integer level = HEVC_CODEC_STRING_TO_PROFILE_LEVEL.get(levelString); if (level == null) { - Log.w(TAG, "Unknown HEVC level string: " + matcher.group(1)); + Log.w(TAG, "Unknown HEVC level string: " + levelString); return null; } return new Pair<>(profile, level); From 3845304e5812717d82e2f28b3e100370069eab8a Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Feb 2019 13:45:55 +0000 Subject: [PATCH 107/110] Shard SimpleCache files into 10 sub-directories Issue: #4253 PiperOrigin-RevId: 232659869 --- .../upstream/cache/SimpleCache.java | 22 ++++++++++++++++--- .../upstream/cache/SimpleCacheSpan.java | 4 +++- .../upstream/cache/SimpleCacheTest.java | 20 ++++++++++++++--- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index d2091c362a..72b39e24d1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.NavigableSet; +import java.util.Random; import java.util.Set; import java.util.TreeSet; @@ -36,6 +37,14 @@ import java.util.TreeSet; public final class SimpleCache implements Cache { private static final String TAG = "SimpleCache"; + /** + * Cache files are distributed between a number of subdirectories. This helps to avoid poor + * performance in cases where the performance of the underlying file system (e.g. FAT32) scales + * badly with the number of files per directory. See + * https://github.com/google/ExoPlayer/issues/4253. + */ + private static final int SUBDIRECTORY_COUNT = 10; + private static final HashSet lockedCacheDirs = new HashSet<>(); private static boolean cacheFolderLockingDisabled; @@ -44,6 +53,7 @@ public final class SimpleCache implements Cache { private final CacheEvictor evictor; private final CachedContentIndex index; private final HashMap> listeners; + private final Random random; private long totalSpace; private boolean released; @@ -128,7 +138,8 @@ public final class SimpleCache implements Cache { this.cacheDir = cacheDir; this.evictor = evictor; this.index = index; - this.listeners = new HashMap<>(); + listeners = new HashMap<>(); + random = new Random(); // Start cache initialization. final ConditionVariable conditionVariable = new ConditionVariable(); @@ -271,8 +282,13 @@ public final class SimpleCache implements Cache { removeStaleSpans(); } evictor.onStartFile(this, key, position, length); - return SimpleCacheSpan.getCacheFile( - cacheDir, cachedContent.id, position, System.currentTimeMillis()); + // Randomly distribute files into subdirectories with a uniform distribution. + File fileDir = new File(cacheDir, Integer.toString(random.nextInt(SUBDIRECTORY_COUNT))); + if (!fileDir.exists()) { + fileDir.mkdir(); + } + long lastAccessTimestamp = System.currentTimeMillis(); + return SimpleCacheSpan.getCacheFile(fileDir, cachedContent.id, position, lastAccessTimestamp); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java index c35e3974f6..decbe80c84 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java @@ -26,7 +26,9 @@ import java.util.regex.Pattern; /** This class stores span metadata in filename. */ /* package */ final class SimpleCacheSpan extends CacheSpan { - private static final String SUFFIX = ".v3.exo"; + /* package */ static final String COMMON_SUFFIX = ".exo"; + + private static final String SUFFIX = ".v3" + COMMON_SUFFIX; private static final Pattern CACHE_FILE_PATTERN_V1 = Pattern.compile( "^(.+)\\.(\\d+)\\.(\\d+)\\.v1\\.exo$", Pattern.DOTALL); private static final Pattern CACHE_FILE_PATTERN_V2 = Pattern.compile( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java index bdb9d4f9d9..6140d0ac82 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheTest.java @@ -75,7 +75,7 @@ public class SimpleCacheTest { NavigableSet cachedSpans = simpleCache.getCachedSpans(KEY_1); assertThat(cachedSpans.isEmpty()).isTrue(); assertThat(simpleCache.getCacheSpace()).isEqualTo(0); - assertThat(cacheDir.listFiles()).hasLength(0); + assertNoCacheFiles(cacheDir); addCache(simpleCache, KEY_1, 0, 15); @@ -233,7 +233,7 @@ public class SimpleCacheTest { // Cache should be cleared assertThat(simpleCache.getKeys()).isEmpty(); - assertThat(cacheDir.listFiles()).hasLength(0); + assertNoCacheFiles(cacheDir); } @Test @@ -252,7 +252,7 @@ public class SimpleCacheTest { // Cache should be cleared assertThat(simpleCache.getKeys()).isEmpty(); - assertThat(cacheDir.listFiles()).hasLength(0); + assertNoCacheFiles(cacheDir); } @Test @@ -391,6 +391,20 @@ public class SimpleCacheTest { } } + private static void assertNoCacheFiles(File dir) { + File[] files = dir.listFiles(); + if (files == null) { + return; + } + for (File file : files) { + if (file.isDirectory()) { + assertNoCacheFiles(file); + } else { + assertThat(file.getName().endsWith(SimpleCacheSpan.COMMON_SUFFIX)).isFalse(); + } + } + } + private static byte[] generateData(String key, int position, int length) { byte[] bytes = new byte[length]; new Random((long) (key.hashCode() ^ position)).nextBytes(bytes); From bdc87a4fc7fc07ca4446c790a644f5d60e42109d Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Feb 2019 14:26:02 +0000 Subject: [PATCH 108/110] API and plumbing for indexing file metadata (length + timestamp) When SimpleCache uses a CacheFileMetadataIndex, it will be able to avoid querying file.length() and renaming files, both of which are expensive operations on some file systems. PiperOrigin-RevId: 232664255 --- .../upstream/cache/CacheFileMetadata.java | 28 ++++ .../cache/CacheFileMetadataIndex.java | 63 ++++++++ .../upstream/cache/CachedContent.java | 36 +++-- .../upstream/cache/SimpleCache.java | 137 ++++++++++++------ .../upstream/cache/SimpleCacheSpan.java | 65 ++++++--- 5 files changed, 247 insertions(+), 82 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java new file mode 100644 index 0000000000..492b98a0de --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadata.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2018 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.upstream.cache; + +/** Metadata associated with a cache file. */ +/* package */ final class CacheFileMetadata { + + public final long length; + public final long lastAccessTimestamp; + + public CacheFileMetadata(long length, long lastAccessTimestamp) { + this.length = length; + this.lastAccessTimestamp = lastAccessTimestamp; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java new file mode 100644 index 0000000000..b25eb91810 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2018 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.upstream.cache; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +/** Maintains an index of cache file metadata. */ +/* package */ class CacheFileMetadataIndex { + + /** + * Returns all file metadata keyed by file name. The returned map is mutable and may be modified + * by the caller. + */ + public Map getAll() { + return Collections.emptyMap(); + } + + /** + * Sets metadata for a given file. + * + * @param name The name of the file. + * @param length The file length. + * @param lastAccessTimestamp The file last access timestamp. + * @return Whether the index was updated successfully. + */ + public boolean set(String name, long length, long lastAccessTimestamp) { + // TODO. + return false; + } + + /** + * Removes metadata. + * + * @param name The name of the file whose metadata is to be removed. + */ + public void remove(String name) { + // TODO. + } + + /** + * Removes metadata. + * + * @param names The names of the files whose metadata is to be removed. + */ + public void removeAll(Set names) { + // TODO. + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java index 64ef33e3c9..80b50d862a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContent.java @@ -16,13 +16,16 @@ package com.google.android.exoplayer2.upstream.cache; import android.support.annotation.Nullable; -import com.google.android.exoplayer2.upstream.cache.Cache.CacheException; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Log; +import java.io.File; import java.util.TreeSet; /** Defines the cached content for a single stream. */ /* package */ final class CachedContent { + private static final String TAG = "CachedContent"; + /** The cache file id that uniquely identifies the original stream. */ public final int id; /** The cache key that uniquely identifies the original stream. */ @@ -138,21 +141,30 @@ import java.util.TreeSet; } /** - * Copies the given span with an updated last access time. Passed span becomes invalid after this - * call. + * Sets the given span's last access timestamp. The passed span becomes invalid after this call. * * @param cacheSpan Span to be copied and updated. - * @return a span with the updated last access time. - * @throws CacheException If renaming of the underlying span file failed. + * @param lastAccessTimestamp The new last access timestamp. + * @param updateFile Whether the span file should be renamed to have its timestamp match the new + * last access time. + * @return A span with the updated last access timestamp. */ - public SimpleCacheSpan touch(SimpleCacheSpan cacheSpan) throws CacheException { - SimpleCacheSpan newCacheSpan = cacheSpan.copyWithUpdatedLastAccessTime(id); - if (!cacheSpan.file.renameTo(newCacheSpan.file)) { - throw new CacheException("Renaming of " + cacheSpan.file + " to " + newCacheSpan.file - + " failed."); - } - // Replace the in-memory representation of the span. + public SimpleCacheSpan setLastAccessTimestamp( + SimpleCacheSpan cacheSpan, long lastAccessTimestamp, boolean updateFile) { Assertions.checkState(cachedSpans.remove(cacheSpan)); + File file = cacheSpan.file; + if (updateFile) { + File directory = file.getParentFile(); + long position = cacheSpan.position; + File newFile = SimpleCacheSpan.getCacheFile(directory, id, position, lastAccessTimestamp); + if (file.renameTo(newFile)) { + file = newFile; + } else { + Log.w(TAG, "Failed to rename " + file + " to " + newFile + "."); + } + } + SimpleCacheSpan newCacheSpan = + cacheSpan.copyWithFileAndLastAccessTimestamp(file, lastAccessTimestamp); cachedSpans.add(newCacheSpan); return newCacheSpan; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index 72b39e24d1..dcdedfc32d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -25,6 +25,7 @@ import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.NavigableSet; import java.util.Random; import java.util.Set; @@ -51,7 +52,8 @@ public final class SimpleCache implements Cache { private final File cacheDir; private final CacheEvictor evictor; - private final CachedContentIndex index; + private final CachedContentIndex contentIndex; + @Nullable private final CacheFileMetadataIndex fileIndex; private final HashMap> listeners; private final Random random; @@ -128,16 +130,17 @@ public final class SimpleCache implements Cache { * * @param cacheDir A dedicated cache directory. * @param evictor The evictor to be used. - * @param index The CachedContentIndex to be used. + * @param contentIndex The content index to be used. */ - /* package */ SimpleCache(File cacheDir, CacheEvictor evictor, CachedContentIndex index) { + /* package */ SimpleCache(File cacheDir, CacheEvictor evictor, CachedContentIndex contentIndex) { if (!lockFolder(cacheDir)) { throw new IllegalStateException("Another SimpleCache instance uses the folder: " + cacheDir); } this.cacheDir = cacheDir; this.evictor = evictor; - this.index = index; + this.contentIndex = contentIndex; + this.fileIndex = null; listeners = new HashMap<>(); random = new Random(); @@ -164,11 +167,11 @@ public final class SimpleCache implements Cache { listeners.clear(); removeStaleSpans(); try { - index.store(); + contentIndex.store(); } catch (CacheException e) { Log.e(TAG, "Storing index file failed", e); } finally { - index.release(); + contentIndex.release(); unlockFolder(cacheDir); released = true; } @@ -204,7 +207,7 @@ public final class SimpleCache implements Cache { @Override public synchronized NavigableSet getCachedSpans(String key) { Assertions.checkState(!released); - CachedContent cachedContent = index.get(key); + CachedContent cachedContent = contentIndex.get(key); return cachedContent == null || cachedContent.isEmpty() ? new TreeSet<>() : new TreeSet(cachedContent.getSpans()); @@ -213,7 +216,7 @@ public final class SimpleCache implements Cache { @Override public synchronized Set getKeys() { Assertions.checkState(!released); - return new HashSet<>(index.getKeys()); + return new HashSet<>(contentIndex.getKeys()); } @Override @@ -243,27 +246,33 @@ public final class SimpleCache implements Cache { public synchronized @Nullable SimpleCacheSpan startReadWriteNonBlocking(String key, long position) throws CacheException { Assertions.checkState(!released); - SimpleCacheSpan cacheSpan = getSpan(key, position); + SimpleCacheSpan span = getSpan(key, position); // Read case. - if (cacheSpan.isCached) { - try { - // Obtain a new span with updated last access timestamp. - SimpleCacheSpan newCacheSpan = index.get(key).touch(cacheSpan); - notifySpanTouched(cacheSpan, newCacheSpan); - return newCacheSpan; - } catch (CacheException e) { - // Ignore. In worst case the cache span is evicted early. - // This happens very rarely [Internal: b/38351639] - return cacheSpan; + if (span.isCached) { + String fileName = span.file.getName(); + long length = span.length; + long lastAccessTimestamp = System.currentTimeMillis(); + // Updating the file itself to incorporate the new last access timestamp is much slower than + // updating the file index. Hence we only update the file if we don't have a file index, or if + // updating the file index failed. + boolean updateFile; + if (fileIndex != null) { + updateFile = !fileIndex.set(fileName, length, lastAccessTimestamp); + } else { + updateFile = true; } + SimpleCacheSpan newSpan = + contentIndex.get(key).setLastAccessTimestamp(span, lastAccessTimestamp, updateFile); + notifySpanTouched(span, newSpan); + return newSpan; } - CachedContent cachedContent = index.getOrAdd(key); + CachedContent cachedContent = contentIndex.getOrAdd(key); if (!cachedContent.isLocked()) { // Write case, lock available. cachedContent.setLocked(true); - return cacheSpan; + return span; } // Write case, lock not available. @@ -273,7 +282,7 @@ public final class SimpleCache implements Cache { @Override public synchronized File startFile(String key, long position, long length) throws CacheException { Assertions.checkState(!released); - CachedContent cachedContent = index.get(key); + CachedContent cachedContent = contentIndex.get(key); Assertions.checkNotNull(cachedContent); Assertions.checkState(cachedContent.isLocked()); if (!cacheDir.exists()) { @@ -301,29 +310,35 @@ public final class SimpleCache implements Cache { file.delete(); return; } - SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(file, length, index); + + SimpleCacheSpan span = SimpleCacheSpan.createCacheEntry(file, length, contentIndex); Assertions.checkState(span != null); - CachedContent cachedContent = index.get(span.key); + CachedContent cachedContent = contentIndex.get(span.key); Assertions.checkNotNull(cachedContent); Assertions.checkState(cachedContent.isLocked()); + // Check if the span conflicts with the set content length long contentLength = ContentMetadata.getContentLength(cachedContent.getMetadata()); if (contentLength != C.LENGTH_UNSET) { Assertions.checkState((span.position + span.length) <= contentLength); } + + if (fileIndex != null) { + fileIndex.set(file.getName(), span.length, span.lastAccessTimestamp); + } addSpan(span); - index.store(); + contentIndex.store(); notifyAll(); } @Override public synchronized void releaseHoleSpan(CacheSpan holeSpan) { Assertions.checkState(!released); - CachedContent cachedContent = index.get(holeSpan.key); + CachedContent cachedContent = contentIndex.get(holeSpan.key); Assertions.checkNotNull(cachedContent); Assertions.checkState(cachedContent.isLocked()); cachedContent.setLocked(false); - index.maybeRemove(cachedContent.key); + contentIndex.maybeRemove(cachedContent.key); notifyAll(); } @@ -336,14 +351,14 @@ public final class SimpleCache implements Cache { @Override public synchronized boolean isCached(String key, long position, long length) { Assertions.checkState(!released); - CachedContent cachedContent = index.get(key); + CachedContent cachedContent = contentIndex.get(key); return cachedContent != null && cachedContent.getCachedBytesLength(position, length) >= length; } @Override public synchronized long getCachedLength(String key, long position, long length) { Assertions.checkState(!released); - CachedContent cachedContent = index.get(key); + CachedContent cachedContent = contentIndex.get(key); return cachedContent != null ? cachedContent.getCachedBytesLength(position, length) : -length; } @@ -351,14 +366,14 @@ public final class SimpleCache implements Cache { public synchronized void applyContentMetadataMutations( String key, ContentMetadataMutations mutations) throws CacheException { Assertions.checkState(!released); - index.applyContentMetadataMutations(key, mutations); - index.store(); + contentIndex.applyContentMetadataMutations(key, mutations); + contentIndex.store(); } @Override public synchronized ContentMetadata getContentMetadata(String key) { Assertions.checkState(!released); - return index.getContentMetadata(key); + return contentIndex.getContentMetadata(key); } /** @@ -375,7 +390,7 @@ public final class SimpleCache implements Cache { * @return The corresponding cache {@link SimpleCacheSpan}. */ private SimpleCacheSpan getSpan(String key, long position) throws CacheException { - CachedContent cachedContent = index.get(key); + CachedContent cachedContent = contentIndex.get(key); if (cachedContent == null) { return SimpleCacheSpan.createOpenHole(key, position); } @@ -398,40 +413,63 @@ public final class SimpleCache implements Cache { return; } - index.load(); - loadDirectory(cacheDir, /* isRootDirectory= */ true); - index.removeEmpty(); + contentIndex.load(); + if (fileIndex != null) { + Map fileMetadata = fileIndex.getAll(); + loadDirectory(cacheDir, /* isRoot= */ true, fileMetadata); + fileIndex.removeAll(fileMetadata.keySet()); + } else { + loadDirectory(cacheDir, /* isRoot= */ true, /* fileMetadata= */ null); + } + contentIndex.removeEmpty(); try { - index.store(); + contentIndex.store(); } catch (CacheException e) { Log.e(TAG, "Storing index file failed", e); } } - private void loadDirectory(File directory, boolean isRootDirectory) { + /** + * Loads a cache directory. If the root directory is passed, also loads any subdirectories. + * + * @param directory The directory to load. + * @param isRoot Whether the directory is the root directory. + * @param fileMetadata A mutable map containing cache file metadata, keyed by file name. The map + * is modified by removing entries for all loaded files. When the method call returns, the map + * will contain only metadata that was unused. May be null if no file metadata is available. + */ + private void loadDirectory( + File directory, boolean isRoot, @Nullable Map fileMetadata) { File[] files = directory.listFiles(); if (files == null) { // Not a directory. return; } - if (!isRootDirectory && files.length == 0) { + if (!isRoot && files.length == 0) { // Empty non-root directory. directory.delete(); return; } for (File file : files) { String fileName = file.getName(); - if (isRootDirectory && fileName.indexOf('.') == -1) { - loadDirectory(file, /* isRootDirectory= */ false); + if (isRoot && fileName.indexOf('.') == -1) { + loadDirectory(file, /* isRoot= */ false, fileMetadata); } else { - if (isRootDirectory && CachedContentIndex.isIndexFile(fileName)) { + if (isRoot && CachedContentIndex.isIndexFile(fileName)) { // Skip the (expected) index files in the root directory. continue; } - long fileLength = file.length(); + CacheFileMetadata metadata = + fileMetadata != null ? fileMetadata.remove(file.getName()) : null; + long length = C.LENGTH_UNSET; + long lastAccessTimestamp = C.TIME_UNSET; + if (metadata != null) { + length = metadata.length; + lastAccessTimestamp = metadata.lastAccessTimestamp; + } SimpleCacheSpan span = - fileLength > 0 ? SimpleCacheSpan.createCacheEntry(file, fileLength, index) : null; + SimpleCacheSpan.createCacheEntry(file, length, lastAccessTimestamp, contentIndex); if (span != null) { addSpan(span); } else { @@ -447,18 +485,21 @@ public final class SimpleCache implements Cache { * @param span The span to be added. */ private void addSpan(SimpleCacheSpan span) { - index.getOrAdd(span.key).addSpan(span); + contentIndex.getOrAdd(span.key).addSpan(span); totalSpace += span.length; notifySpanAdded(span); } private void removeSpanInternal(CacheSpan span) { - CachedContent cachedContent = index.get(span.key); + CachedContent cachedContent = contentIndex.get(span.key); if (cachedContent == null || !cachedContent.removeSpan(span)) { return; } totalSpace -= span.length; - index.maybeRemove(cachedContent.key); + if (fileIndex != null) { + fileIndex.remove(span.file.getName()); + } + contentIndex.maybeRemove(cachedContent.key); notifySpanRemoved(span); } @@ -468,7 +509,7 @@ public final class SimpleCache implements Cache { */ private void removeStaleSpans() { ArrayList spansToBeRemoved = new ArrayList<>(); - for (CachedContent cachedContent : index.getAll()) { + for (CachedContent cachedContent : contentIndex.getAll()) { for (CacheSpan span : cachedContent.getSpans()) { if (!span.file.exists()) { spansToBeRemoved.add(span); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java index decbe80c84..82563af01c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCacheSpan.java @@ -38,16 +38,16 @@ import java.util.regex.Pattern; /** * Returns a new {@link File} instance from {@code cacheDir}, {@code id}, {@code position}, {@code - * lastAccessTimestamp}. + * timestamp}. * * @param cacheDir The parent abstract pathname. * @param id The cache file id. * @param position The position of the stored data in the original stream. - * @param lastAccessTimestamp The last access timestamp. + * @param timestamp The file timestamp. * @return The cache file. */ - public static File getCacheFile(File cacheDir, int id, long position, long lastAccessTimestamp) { - return new File(cacheDir, id + "." + position + "." + lastAccessTimestamp + SUFFIX); + public static File getCacheFile(File cacheDir, int id, long position, long timestamp) { + return new File(cacheDir, id + "." + position + "." + timestamp + SUFFIX); } /** @@ -84,22 +84,36 @@ import java.util.regex.Pattern; return new SimpleCacheSpan(key, position, length, C.TIME_UNSET, null); } - /* - * Note: {@code fileLength} is equivalent to {@code file.length()}, but passing it as an explicit - * argument can reduce the number of calls to this method if the calling code already knows the - * file length. This is preferable because calling {@code file.length()} can be expensive. See: - * https://github.com/google/ExoPlayer/issues/4253#issuecomment-451593889. - */ /** * Creates a cache span from an underlying cache file. Upgrades the file if necessary. * * @param file The cache file. - * @param length The length of the cache file in bytes. + * @param length The length of the cache file in bytes, or {@link C#LENGTH_UNSET} to query the + * underlying file system. Querying the underlying file system can be expensive, so callers + * that already know the length of the file should pass it explicitly. * @return The span, or null if the file name is not correctly formatted, or if the id is not - * present in the content index. + * present in the content index, or if the length is 0. */ @Nullable public static SimpleCacheSpan createCacheEntry(File file, long length, CachedContentIndex index) { + return createCacheEntry(file, length, /* lastAccessTimestamp= */ C.TIME_UNSET, index); + } + + /** + * Creates a cache span from an underlying cache file. Upgrades the file if necessary. + * + * @param file The cache file. + * @param length The length of the cache file in bytes, or {@link C#LENGTH_UNSET} to query the + * underlying file system. Querying the underlying file system can be expensive, so callers + * that already know the length of the file should pass it explicitly. + * @param lastAccessTimestamp The last access timestamp, or {@link C#TIME_UNSET} to use the file + * timestamp. + * @return The span, or null if the file name is not correctly formatted, or if the id is not + * present in the content index, or if the length is 0. + */ + @Nullable + public static SimpleCacheSpan createCacheEntry( + File file, long length, long lastAccessTimestamp, CachedContentIndex index) { String name = file.getName(); if (!name.endsWith(SUFFIX)) { file = upgradeFile(file, index); @@ -120,9 +134,18 @@ import java.util.regex.Pattern; return null; } + if (length == C.LENGTH_UNSET) { + length = file.length(); + } + if (length == 0) { + return null; + } + long position = Long.parseLong(matcher.group(2)); - long lastAccessTime = Long.parseLong(matcher.group(3)); - return new SimpleCacheSpan(key, position, length, lastAccessTime, file); + if (lastAccessTimestamp == C.TIME_UNSET) { + lastAccessTimestamp = Long.parseLong(matcher.group(3)); + } + return new SimpleCacheSpan(key, position, length, lastAccessTimestamp, file); } /** @@ -174,18 +197,16 @@ import java.util.regex.Pattern; } /** - * Returns a copy of this CacheSpan whose last access time stamp is set to current time. This - * doesn't copy or change the underlying cache file. + * Returns a copy of this CacheSpan with a new file and last access timestamp. * - * @param id The cache file id. - * @return A {@link SimpleCacheSpan} with updated last access time stamp. + * @param file The new file. + * @param lastAccessTimestamp The new last access time. + * @return A copy with the new file and last access timestamp. * @throws IllegalStateException If called on a non-cached span (i.e. {@link #isCached} is false). */ - public SimpleCacheSpan copyWithUpdatedLastAccessTime(int id) { + public SimpleCacheSpan copyWithFileAndLastAccessTimestamp(File file, long lastAccessTimestamp) { Assertions.checkState(isCached); - long now = System.currentTimeMillis(); - File newCacheFile = getCacheFile(file.getParentFile(), id, position, now); - return new SimpleCacheSpan(key, position, length, now, newCacheFile); + return new SimpleCacheSpan(key, position, length, lastAccessTimestamp, file); } } From 7eed60c9e31492e0ba18ce634b03482ddb340801 Mon Sep 17 00:00:00 2001 From: eguven Date: Wed, 6 Feb 2019 15:11:33 +0000 Subject: [PATCH 109/110] Add DownloadManager.stopDownloads(int manualStopReason) Also removed STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY. PiperOrigin-RevId: 232669463 --- .../offline/DefaultDownloadIndex.java | 15 +- .../exoplayer2/offline/DownloadManager.java | 155 +++++++++++------- .../exoplayer2/offline/DownloadState.java | 33 ++-- .../offline/DefaultDownloadIndexTest.java | 5 +- .../offline/DownloadStateBuilder.java | 7 + .../exoplayer2/offline/DownloadStateTest.java | 4 +- 6 files changed, 132 insertions(+), 87 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java index 2ff85c041b..c30260b11b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DefaultDownloadIndex.java @@ -50,6 +50,7 @@ public final class DefaultDownloadIndex implements DownloadIndex { private static final String COLUMN_FAILURE_REASON = "failure_reason"; private static final String COLUMN_STOP_FLAGS = "stop_flags"; private static final String COLUMN_NOT_MET_REQUIREMENTS = "not_met_requirements"; + private static final String COLUMN_MANUAL_STOP_REASON = "manual_stop_reason"; private static final String COLUMN_START_TIME_MS = "start_time_ms"; private static final String COLUMN_UPDATE_TIME_MS = "update_time_ms"; private static final String COLUMN_STREAM_KEYS = "stream_keys"; @@ -66,10 +67,11 @@ public final class DefaultDownloadIndex implements DownloadIndex { private static final int COLUMN_INDEX_FAILURE_REASON = 8; private static final int COLUMN_INDEX_STOP_FLAGS = 9; private static final int COLUMN_INDEX_NOT_MET_REQUIREMENTS = 10; - private static final int COLUMN_INDEX_START_TIME_MS = 11; - private static final int COLUMN_INDEX_UPDATE_TIME_MS = 12; - private static final int COLUMN_INDEX_STREAM_KEYS = 13; - private static final int COLUMN_INDEX_CUSTOM_METADATA = 14; + private static final int COLUMN_INDEX_MANUAL_STOP_REASON = 11; + private static final int COLUMN_INDEX_START_TIME_MS = 12; + private static final int COLUMN_INDEX_UPDATE_TIME_MS = 13; + private static final int COLUMN_INDEX_STREAM_KEYS = 14; + private static final int COLUMN_INDEX_CUSTOM_METADATA = 15; private static final String COLUMN_SELECTION_ID = COLUMN_ID + " = ?"; @@ -86,6 +88,7 @@ public final class DefaultDownloadIndex implements DownloadIndex { COLUMN_FAILURE_REASON, COLUMN_STOP_FLAGS, COLUMN_NOT_MET_REQUIREMENTS, + COLUMN_MANUAL_STOP_REASON, COLUMN_START_TIME_MS, COLUMN_UPDATE_TIME_MS, COLUMN_STREAM_KEYS, @@ -119,6 +122,8 @@ public final class DefaultDownloadIndex implements DownloadIndex { + " INTEGER NOT NULL," + COLUMN_NOT_MET_REQUIREMENTS + " INTEGER NOT NULL," + + COLUMN_MANUAL_STOP_REASON + + " INTEGER NOT NULL," + COLUMN_START_TIME_MS + " INTEGER NOT NULL," + COLUMN_UPDATE_TIME_MS @@ -194,6 +199,7 @@ public final class DefaultDownloadIndex implements DownloadIndex { values.put(COLUMN_FAILURE_REASON, downloadState.failureReason); values.put(COLUMN_STOP_FLAGS, downloadState.stopFlags); values.put(COLUMN_NOT_MET_REQUIREMENTS, downloadState.notMetRequirements); + values.put(COLUMN_MANUAL_STOP_REASON, downloadState.manualStopReason); values.put(COLUMN_START_TIME_MS, downloadState.startTimeMs); values.put(COLUMN_UPDATE_TIME_MS, downloadState.updateTimeMs); values.put(COLUMN_STREAM_KEYS, encodeStreamKeys(downloadState.streamKeys)); @@ -260,6 +266,7 @@ public final class DefaultDownloadIndex implements DownloadIndex { cursor.getInt(COLUMN_INDEX_FAILURE_REASON), cursor.getInt(COLUMN_INDEX_STOP_FLAGS), cursor.getInt(COLUMN_INDEX_NOT_MET_REQUIREMENTS), + cursor.getInt(COLUMN_INDEX_MANUAL_STOP_REASON), cursor.getLong(COLUMN_INDEX_START_TIME_MS), cursor.getLong(COLUMN_INDEX_UPDATE_TIME_MS), decodeStreamKeys(cursor.getString(COLUMN_INDEX_STREAM_KEYS)), diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java index 9e6aec0a86..731f7fc43e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java @@ -25,9 +25,8 @@ import static com.google.android.exoplayer2.offline.DownloadState.STATE_REMOVED; import static com.google.android.exoplayer2.offline.DownloadState.STATE_REMOVING; import static com.google.android.exoplayer2.offline.DownloadState.STATE_RESTARTING; import static com.google.android.exoplayer2.offline.DownloadState.STATE_STOPPED; -import static com.google.android.exoplayer2.offline.DownloadState.STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY; +import static com.google.android.exoplayer2.offline.DownloadState.STOP_FLAG_MANUAL; import static com.google.android.exoplayer2.offline.DownloadState.STOP_FLAG_REQUIREMENTS_NOT_MET; -import static com.google.android.exoplayer2.offline.DownloadState.STOP_FLAG_STOPPED; import android.content.Context; import android.os.ConditionVariable; @@ -109,15 +108,17 @@ public final class DownloadManager { @IntDef({ START_THREAD_SUCCEEDED, START_THREAD_WAIT_REMOVAL_TO_FINISH, - START_THREAD_WAIT_DOWNLOAD_CANCELATION, - START_THREAD_TOO_MANY_DOWNLOADS + START_THREAD_WAIT_DOWNLOAD_CANCELLATION, + START_THREAD_TOO_MANY_DOWNLOADS, + START_THREAD_NOT_ALLOWED }) private @interface StartThreadResults {} private static final int START_THREAD_SUCCEEDED = 0; private static final int START_THREAD_WAIT_REMOVAL_TO_FINISH = 1; - private static final int START_THREAD_WAIT_DOWNLOAD_CANCELATION = 2; + private static final int START_THREAD_WAIT_DOWNLOAD_CANCELLATION = 2; private static final int START_THREAD_TOO_MANY_DOWNLOADS = 3; + private static final int START_THREAD_NOT_ALLOWED = 4; private static final String TAG = "DownloadManager"; private static final boolean DEBUG = false; @@ -137,8 +138,9 @@ public final class DownloadManager { private boolean initialized; private boolean released; - @DownloadState.StopFlags private int stickyStopFlags; + @DownloadState.StopFlags private int stopFlags; @Requirements.RequirementFlags private int notMetRequirements; + private int manualStopReason; private RequirementsWatcher requirementsWatcher; private int simultaneousDownloads; @@ -181,8 +183,8 @@ public final class DownloadManager { this.downloaderFactory = downloaderFactory; this.maxSimultaneousDownloads = maxSimultaneousDownloads; this.minRetryCount = minRetryCount; - this.stickyStopFlags = STOP_FLAG_STOPPED | STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY; + stopFlags = STOP_FLAG_MANUAL; downloads = new ArrayList<>(); activeDownloads = new HashMap<>(); @@ -199,7 +201,7 @@ public final class DownloadManager { listeners = new CopyOnWriteArraySet<>(); actionQueue = new ArrayDeque<>(); - watchRequirements(requirements); + setNotMetRequirements(watchRequirements(requirements)); loadActions(); logd("Created"); } @@ -241,33 +243,35 @@ public final class DownloadManager { listeners.remove(listener); } - /** Starts the downloads. */ + /** + * Clears {@link DownloadState#STOP_FLAG_MANUAL} flag of all downloads. Downloads are started if + * the requirements are met. + */ public void startDownloads() { - clearStopFlags(STOP_FLAG_STOPPED); + logd("manual stopped is cancelled"); + manualStopReason = 0; + stopFlags &= ~STOP_FLAG_MANUAL; + for (int i = 0; i < downloads.size(); i++) { + downloads.get(i).clearManualStopReason(); + } } - /** Stops all of the downloads. Call {@link #startDownloads()} to restart downloads. */ + /** Signals all downloads to stop. Call {@link #startDownloads()} to let them to be started. */ public void stopDownloads() { - setStopFlags(STOP_FLAG_STOPPED); + stopDownloads(0); } - private void setStopFlags(int flags) { - updateStopFlags(flags, flags); - } - - private void clearStopFlags(int flags) { - updateStopFlags(flags, 0); - } - - private void updateStopFlags(int flags, int values) { - Assertions.checkState(!released); - int updatedStickyStopFlags = (values & flags) | (stickyStopFlags & ~flags); - if (stickyStopFlags != updatedStickyStopFlags) { - stickyStopFlags = updatedStickyStopFlags; - for (int i = 0; i < downloads.size(); i++) { - downloads.get(i).updateStopFlags(flags, values); - } - logdFlags("Sticky stop flags are updated", updatedStickyStopFlags); + /** + * Signals all downloads to stop. Call {@link #startDownloads()} to let them to be started. + * + * @param manualStopReason An application defined stop reason. + */ + public void stopDownloads(int manualStopReason) { + logd("downloads are stopped manually"); + this.manualStopReason = manualStopReason; + stopFlags |= STOP_FLAG_MANUAL; + for (int i = 0; i < downloads.size(); i++) { + downloads.get(i).setManualStopReason(this.manualStopReason); } } @@ -341,8 +345,8 @@ public final class DownloadManager { if (released) { return; } - setStopFlags(STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY); released = true; + stopAllDownloadThreads(); if (requirementsWatcher != null) { requirementsWatcher.stop(); } @@ -361,7 +365,7 @@ public final class DownloadManager { return; } } - Download download = new Download(this, action, stickyStopFlags, notMetRequirements); + Download download = new Download(this, action, stopFlags, notMetRequirements, manualStopReason); downloads.add(download); logd("Download is added", download); } @@ -396,17 +400,26 @@ public final class DownloadManager { } private void onRequirementsStateChanged(@Requirements.RequirementFlags int notMetRequirements) { - this.notMetRequirements = notMetRequirements; + setNotMetRequirements(notMetRequirements); logdFlags("Not met requirements are changed", notMetRequirements); + Requirements requirements = requirementsWatcher.getRequirements(); for (Listener listener : listeners) { - listener.onRequirementsStateChanged( - DownloadManager.this, requirementsWatcher.getRequirements(), notMetRequirements); + listener.onRequirementsStateChanged(DownloadManager.this, requirements, notMetRequirements); } for (int i = 0; i < downloads.size(); i++) { downloads.get(i).setNotMetRequirements(notMetRequirements); } } + private void setNotMetRequirements(@Requirements.RequirementFlags int notMetRequirements) { + this.notMetRequirements = notMetRequirements; + if (notMetRequirements == 0) { + stopFlags &= ~STOP_FLAG_REQUIREMENTS_NOT_MET; + } else { + stopFlags |= STOP_FLAG_REQUIREMENTS_NOT_MET; + } + } + private void loadActions() { fileIOHandler.post( () -> { @@ -438,7 +451,9 @@ public final class DownloadManager { for (Listener listener : listeners) { listener.onInitialized(DownloadManager.this); } - clearStopFlags(STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY); + for (int i = 0; i < downloads.size(); i++) { + downloads.get(i).start(); + } }); }); } @@ -497,9 +512,12 @@ public final class DownloadManager { @StartThreadResults private int startDownloadThread(Download download, DownloadAction action) { + if (!initialized || released) { + return START_THREAD_NOT_ALLOWED; + } if (activeDownloads.containsKey(download)) { if (stopDownloadThread(download)) { - return START_THREAD_WAIT_DOWNLOAD_CANCELATION; + return START_THREAD_WAIT_DOWNLOAD_CANCELLATION; } return START_THREAD_WAIT_REMOVAL_TO_FINISH; } @@ -526,6 +544,12 @@ public final class DownloadManager { return false; } + private void stopAllDownloadThreads() { + for (Download download : activeDownloads.keySet()) { + stopDownloadThread(download); + } + } + private void onDownloadThreadStopped(DownloadThread downloadThread, Throwable finalError) { Download download = downloadThread.download; logd("Download is stopped", download); @@ -567,18 +591,18 @@ public final class DownloadManager { @MonotonicNonNull @DownloadState.FailureReason private int failureReason; @DownloadState.StopFlags private int stopFlags; @Requirements.RequirementFlags private int notMetRequirements; + private int manualStopReason; private Download( DownloadManager downloadManager, DownloadAction action, @DownloadState.StopFlags int stopFlags, - @Requirements.RequirementFlags int notMetRequirements) { + @Requirements.RequirementFlags int notMetRequirements, + int manualStopReason) { this.id = action.id; this.downloadManager = downloadManager; this.notMetRequirements = notMetRequirements; - if (notMetRequirements != 0) { - stopFlags |= STOP_FLAG_REQUIREMENTS_NOT_MET; - } + this.manualStopReason = manualStopReason; this.stopFlags = stopFlags; this.startTimeMs = System.currentTimeMillis(); actionQueue = new ArrayDeque<>(); @@ -642,6 +666,7 @@ public final class DownloadManager { failureReason, stopFlags, notMetRequirements, + manualStopReason, startTimeMs, /* updateTimeMs= */ System.currentTimeMillis(), action.keys.toArray(new StreamKey[0]), @@ -662,21 +687,34 @@ public final class DownloadManager { } public void start() { - if (state == STATE_QUEUED) { + if (state == STATE_QUEUED || state == STATE_DOWNLOADING) { startOrQueue(); + } else if (state == STATE_REMOVING || state == STATE_RESTARTING) { + downloadManager.startDownloadThread(this, actionQueue.peek()); } } - public void setStopFlags(int flags) { - updateStopFlags(flags, flags); + public void setNotMetRequirements(@Requirements.RequirementFlags int notMetRequirements) { + this.notMetRequirements = notMetRequirements; + updateStopFlags(STOP_FLAG_REQUIREMENTS_NOT_MET, /* setFlags= */ notMetRequirements != 0); } - public void clearStopFlags(int flags) { - updateStopFlags(flags, 0); + public void setManualStopReason(int manualStopReason) { + this.manualStopReason = manualStopReason; + updateStopFlags(STOP_FLAG_MANUAL, /* setFlags= */ true); } - public void updateStopFlags(int flags, int values) { - stopFlags = (values & flags) | (stopFlags & ~flags); + public void clearManualStopReason() { + this.manualStopReason = 0; + updateStopFlags(STOP_FLAG_MANUAL, /* setFlags= */ false); + } + + private void updateStopFlags(int flags, boolean setFlags) { + if (setFlags) { + stopFlags |= flags; + } else { + stopFlags &= ~flags; + } if (stopFlags != 0) { if (state == STATE_DOWNLOADING || state == STATE_QUEUED) { downloadManager.stopDownloadThread(this); @@ -687,21 +725,14 @@ public final class DownloadManager { } } - public void setNotMetRequirements(@Requirements.RequirementFlags int notMetRequirements) { - this.notMetRequirements = notMetRequirements; - updateStopFlags( - STOP_FLAG_REQUIREMENTS_NOT_MET, - notMetRequirements != 0 ? STOP_FLAG_REQUIREMENTS_NOT_MET : 0); - } - private void initialize() { DownloadAction action = actionQueue.peek(); if (action.isRemoveAction) { - if (!downloadManager.released) { - int result = downloadManager.startDownloadThread(this, action); - Assertions.checkState( - result == START_THREAD_SUCCEEDED || result == START_THREAD_WAIT_DOWNLOAD_CANCELATION); - } + int result = downloadManager.startDownloadThread(this, action); + Assertions.checkState( + result == START_THREAD_SUCCEEDED + || result == START_THREAD_WAIT_DOWNLOAD_CANCELLATION + || result == START_THREAD_NOT_ALLOWED); setState(actionQueue.size() == 1 ? STATE_REMOVING : STATE_RESTARTING); } else if (stopFlags != 0) { setState(STATE_STOPPED); @@ -715,10 +746,10 @@ public final class DownloadManager { Assertions.checkState(!action.isRemoveAction); @StartThreadResults int result = downloadManager.startDownloadThread(this, action); Assertions.checkState(result != START_THREAD_WAIT_REMOVAL_TO_FINISH); - if (result == START_THREAD_TOO_MANY_DOWNLOADS) { - setState(STATE_QUEUED); - } else { + if (result == START_THREAD_SUCCEEDED || result == START_THREAD_WAIT_DOWNLOAD_CANCELLATION) { setState(STATE_DOWNLOADING); + } else { + setState(STATE_QUEUED); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java index 35518af38d..b32288fa3f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadState.java @@ -20,6 +20,7 @@ import android.support.annotation.IntDef; import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.scheduler.Requirements; +import com.google.android.exoplayer2.scheduler.Requirements.RequirementFlags; import com.google.android.exoplayer2.util.Assertions; import java.lang.annotation.Documented; import java.lang.annotation.Retention; @@ -76,25 +77,19 @@ public final class DownloadState { public static final int FAILURE_REASON_UNKNOWN = 1; /** - * Download stop flags. Possible flag values are {@link #STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY} and - * {@link #STOP_FLAG_STOPPED}. + * Download stop flags. Possible flag values are {@link #STOP_FLAG_MANUAL} and {@link + * #STOP_FLAG_REQUIREMENTS_NOT_MET}. */ @Documented @Retention(RetentionPolicy.SOURCE) @IntDef( flag = true, - value = { - STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY, - STOP_FLAG_STOPPED, - STOP_FLAG_REQUIREMENTS_NOT_MET - }) + value = {STOP_FLAG_MANUAL, STOP_FLAG_REQUIREMENTS_NOT_MET}) public @interface StopFlags {} - /** Download can't be started as the manager isn't ready. */ - public static final int STOP_FLAG_DOWNLOAD_MANAGER_NOT_READY = 1; /** Download is stopped by the application. */ - public static final int STOP_FLAG_STOPPED = 1 << 1; + public static final int STOP_FLAG_MANUAL = 1; /** Download is stopped as the requirements are not met. */ - public static final int STOP_FLAG_REQUIREMENTS_NOT_MET = 1 << 2; + public static final int STOP_FLAG_REQUIREMENTS_NOT_MET = 1 << 1; /** Returns the state string for the given state value. */ public static String getStateString(@State int state) { @@ -165,6 +160,8 @@ public final class DownloadState { @StopFlags public final int stopFlags; /** Not met requirements to download. */ @Requirements.RequirementFlags public final int notMetRequirements; + /** If {@link #STOP_FLAG_MANUAL} is set then this field holds the manual stop reason. */ + public final int manualStopReason; /** * Creates a {@link DownloadState} using a {@link DownloadAction}. @@ -188,6 +185,7 @@ public final class DownloadState { FAILURE_REASON_NONE, /* stopFlags= */ 0, /* notMetRequirements= */ 0, + /* manualStopReason= */ 0, /* startTimeMs= */ currentTimeMs, /* updateTimeMs= */ currentTimeMs, action.keys.toArray(new StreamKey[0]), @@ -205,18 +203,17 @@ public final class DownloadState { long totalBytes, @FailureReason int failureReason, @StopFlags int stopFlags, - @Requirements.RequirementFlags int notMetRequirements, + @RequirementFlags int notMetRequirements, + int manualStopReason, long startTimeMs, long updateTimeMs, StreamKey[] streamKeys, byte[] customMetadata) { - Assertions.checkState( - failureReason == FAILURE_REASON_NONE ? state != STATE_FAILED : state == STATE_FAILED); + Assertions.checkState((failureReason == FAILURE_REASON_NONE) == (state != STATE_FAILED)); Assertions.checkState(stopFlags == 0 || (state != STATE_DOWNLOADING && state != STATE_QUEUED)); Assertions.checkState( - (stopFlags & STOP_FLAG_REQUIREMENTS_NOT_MET) == 0 - ? notMetRequirements == 0 - : notMetRequirements != 0); + ((stopFlags & STOP_FLAG_REQUIREMENTS_NOT_MET) == 0) == (notMetRequirements == 0)); + Assertions.checkState(((stopFlags & STOP_FLAG_MANUAL) != 0) || (manualStopReason == 0)); this.id = id; this.type = type; this.uri = uri; @@ -228,6 +225,7 @@ public final class DownloadState { this.failureReason = failureReason; this.stopFlags = stopFlags; this.notMetRequirements = notMetRequirements; + this.manualStopReason = manualStopReason; this.startTimeMs = startTimeMs; this.updateTimeMs = updateTimeMs; this.streamKeys = streamKeys; @@ -256,6 +254,7 @@ public final class DownloadState { FAILURE_REASON_NONE, stopFlags, notMetRequirements, + manualStopReason, startTimeMs, updateTimeMs, mergeStreamKeys(this, action), diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java index 8789a7ef78..07254ef7f9 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DefaultDownloadIndexTest.java @@ -77,8 +77,10 @@ public class DefaultDownloadIndexTest { .setDownloadedBytes(200) .setTotalBytes(400) .setFailureReason(DownloadState.FAILURE_REASON_UNKNOWN) - .setStopFlags(DownloadState.STOP_FLAG_REQUIREMENTS_NOT_MET) + .setStopFlags( + DownloadState.STOP_FLAG_REQUIREMENTS_NOT_MET | DownloadState.STOP_FLAG_MANUAL) .setNotMetRequirements(0x87654321) + .setManualStopReason(0x12345678) .setStartTimeMs(10) .setUpdateTimeMs(20) .setStreamKeys( @@ -206,5 +208,4 @@ public class DefaultDownloadIndexTest { assertThat(VersionTable.getVersion(writableDatabase, VersionTable.FEATURE_OFFLINE)) .isEqualTo(DefaultDownloadIndex.TABLE_VERSION); } - } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadStateBuilder.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadStateBuilder.java index 84b51c5572..501042e69c 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadStateBuilder.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadStateBuilder.java @@ -38,6 +38,7 @@ class DownloadStateBuilder { private int failureReason; private int stopFlags; private int notMetRequirements; + private int manualStopReason; private long startTimeMs; private long updateTimeMs; private StreamKey[] streamKeys; @@ -140,6 +141,11 @@ class DownloadStateBuilder { return this; } + public DownloadStateBuilder setManualStopReason(int manualStopReason) { + this.manualStopReason = manualStopReason; + return this; + } + public DownloadStateBuilder setStartTimeMs(long startTimeMs) { this.startTimeMs = startTimeMs; return this; @@ -173,6 +179,7 @@ class DownloadStateBuilder { failureReason, stopFlags, notMetRequirements, + manualStopReason, startTimeMs, updateTimeMs, streamKeys, diff --git a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadStateTest.java b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadStateTest.java index f6d008a0e3..982a6e8ef9 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadStateTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/offline/DownloadStateTest.java @@ -171,7 +171,7 @@ public class DownloadStateTest { DownloadStateBuilder downloadStateBuilder = new DownloadStateBuilder(downloadAction) .setState(DownloadState.STATE_STOPPED) - .setStopFlags(DownloadState.STOP_FLAG_STOPPED); + .setStopFlags(DownloadState.STOP_FLAG_MANUAL); DownloadState downloadState = downloadStateBuilder.build(); DownloadState mergedDownloadState = downloadState.mergeAction(downloadAction); @@ -185,7 +185,7 @@ public class DownloadStateTest { DownloadStateBuilder downloadStateBuilder = new DownloadStateBuilder(downloadAction) .setState(DownloadState.STATE_STOPPED) - .setStopFlags(DownloadState.STOP_FLAG_STOPPED); + .setStopFlags(DownloadState.STOP_FLAG_MANUAL); DownloadState downloadState = downloadStateBuilder.build(); DownloadState mergedDownloadState = downloadState.mergeAction(downloadAction); From a6c1dbe1563fc77cdc3b389ce3f1cca1cefa0aba Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 6 Feb 2019 15:17:00 +0000 Subject: [PATCH 110/110] Implement CacheFileMetadataIndex using SQLite PiperOrigin-RevId: 232670039 --- .../exoplayer2/database/VersionTable.java | 8 +- .../cache/CacheFileMetadataIndex.java | 126 ++++++++++++++++-- .../upstream/cache/CachedContentIndex.java | 11 +- .../upstream/cache/SimpleCache.java | 9 +- .../exoplayer2/database/VersionTableTest.java | 7 +- 5 files changed, 137 insertions(+), 24 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java b/library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java index 5193d1836f..cdcca7a350 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/database/VersionTable.java @@ -35,8 +35,10 @@ public final class VersionTable { public static final int VERSION_UNSET = -1; /** Version of tables used for offline functionality. */ public static final int FEATURE_OFFLINE = 0; - /** Version of tables used for cache functionality. */ - public static final int FEATURE_CACHE = 1; + /** Version of tables used for cache content metadata. */ + public static final int FEATURE_CACHE_CONTENT_METADATA = 1; + /** Version of tables used for cache file metadata. */ + public static final int FEATURE_CACHE_FILE_METADATA = 2; private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "Versions"; @@ -54,7 +56,7 @@ public final class VersionTable { @Documented @Retention(RetentionPolicy.SOURCE) - @IntDef({FEATURE_OFFLINE, FEATURE_CACHE}) + @IntDef({FEATURE_OFFLINE, FEATURE_CACHE_CONTENT_METADATA, FEATURE_CACHE_FILE_METADATA}) private @interface Feature {} private VersionTable() {} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java index b25eb91810..96213b4bbc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheFileMetadataIndex.java @@ -15,19 +15,72 @@ */ package com.google.android.exoplayer2.upstream.cache; -import java.util.Collections; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import com.google.android.exoplayer2.database.DatabaseProvider; +import com.google.android.exoplayer2.database.VersionTable; +import java.util.HashMap; import java.util.Map; import java.util.Set; /** Maintains an index of cache file metadata. */ -/* package */ class CacheFileMetadataIndex { +/* package */ final class CacheFileMetadataIndex { + + private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "CacheFileMetadata"; + private static final int TABLE_VERSION = 1; + + private static final String COLUMN_NAME = "name"; + private static final String COLUMN_LENGTH = "length"; + private static final String COLUMN_LAST_ACCESS_TIMESTAMP = "last_access_timestamp"; + + private static final int COLUMN_INDEX_NAME = 0; + private static final int COLUMN_INDEX_LENGTH = 1; + private static final int COLUMN_INDEX_LAST_ACCESS_TIMESTAMP = 2; + + private static final String WHERE_NAME_EQUALS = COLUMN_INDEX_NAME + " = ?"; + + private static final String[] COLUMNS = + new String[] { + COLUMN_NAME, COLUMN_LENGTH, COLUMN_LAST_ACCESS_TIMESTAMP, + }; + + private static final String SQL_DROP_TABLE_IF_EXISTS = "DROP TABLE IF EXISTS " + TABLE_NAME; + private static final String SQL_CREATE_TABLE = + "CREATE TABLE " + + TABLE_NAME + + " (" + + COLUMN_NAME + + " TEXT PRIMARY KEY NOT NULL," + + COLUMN_LENGTH + + " INTEGER NOT NULL," + + COLUMN_LAST_ACCESS_TIMESTAMP + + " INTEGER NOT NULL)"; + + private final DatabaseProvider databaseProvider; + + private boolean initialized; + + public CacheFileMetadataIndex(DatabaseProvider databaseProvider) { + this.databaseProvider = databaseProvider; + } /** * Returns all file metadata keyed by file name. The returned map is mutable and may be modified * by the caller. */ public Map getAll() { - return Collections.emptyMap(); + ensureInitialized(); + try (Cursor cursor = getCursor()) { + Map fileMetadata = new HashMap<>(cursor.getCount()); + while (cursor.moveToNext()) { + String name = cursor.getString(COLUMN_INDEX_NAME); + long length = cursor.getLong(COLUMN_INDEX_LENGTH); + long lastAccessTimestamp = cursor.getLong(COLUMN_INDEX_LAST_ACCESS_TIMESTAMP); + fileMetadata.put(name, new CacheFileMetadata(length, lastAccessTimestamp)); + } + return fileMetadata; + } } /** @@ -36,11 +89,15 @@ import java.util.Set; * @param name The name of the file. * @param length The file length. * @param lastAccessTimestamp The file last access timestamp. - * @return Whether the index was updated successfully. */ - public boolean set(String name, long length, long lastAccessTimestamp) { - // TODO. - return false; + public void set(String name, long length, long lastAccessTimestamp) { + ensureInitialized(); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(COLUMN_NAME, name); + values.put(COLUMN_LENGTH, length); + values.put(COLUMN_LAST_ACCESS_TIMESTAMP, lastAccessTimestamp); + writableDatabase.replace(TABLE_NAME, /* nullColumnHack= */ null, values); } /** @@ -49,7 +106,9 @@ import java.util.Set; * @param name The name of the file whose metadata is to be removed. */ public void remove(String name) { - // TODO. + ensureInitialized(); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.delete(TABLE_NAME, WHERE_NAME_EQUALS, new String[] {name}); } /** @@ -58,6 +117,55 @@ import java.util.Set; * @param names The names of the files whose metadata is to be removed. */ public void removeAll(Set names) { - // TODO. + ensureInitialized(); + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.beginTransaction(); + try { + for (String name : names) { + writableDatabase.delete(TABLE_NAME, WHERE_NAME_EQUALS, new String[] {name}); + } + writableDatabase.setTransactionSuccessful(); + } finally { + writableDatabase.endTransaction(); + } + } + + private void ensureInitialized() { + if (initialized) { + return; + } + SQLiteDatabase readableDatabase = databaseProvider.getReadableDatabase(); + int version = + VersionTable.getVersion(readableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA); + if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) { + SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); + writableDatabase.beginTransaction(); + try { + VersionTable.setVersion( + writableDatabase, VersionTable.FEATURE_CACHE_FILE_METADATA, TABLE_VERSION); + writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS); + writableDatabase.execSQL(SQL_CREATE_TABLE); + writableDatabase.setTransactionSuccessful(); + } finally { + writableDatabase.endTransaction(); + } + } else if (version < TABLE_VERSION) { + // There is no previous version currently. + throw new IllegalStateException(); + } + initialized = true; + } + + private Cursor getCursor() { + return databaseProvider + .getReadableDatabase() + .query( + TABLE_NAME, + COLUMNS, + /* selection */ null, + /* selectionArgs= */ null, + /* groupBy= */ null, + /* having= */ null, + /* orderBy= */ null); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java index 0888290e1b..b22645c9d2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CachedContentIndex.java @@ -678,7 +678,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; // the entire table. Currently this implementation only encrypts new and updated entries. private static final class SQLiteStorage implements Storage { - private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "Cache"; + private static final String TABLE_NAME = DatabaseProvider.TABLE_PREFIX + "CacheContentMetadata"; private static final int TABLE_VERSION = 1; private static final String COLUMN_ID = "id"; @@ -736,7 +736,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; public boolean exists() { return file.exists() && VersionTable.getVersion( - databaseProvider.getReadableDatabase(), VersionTable.FEATURE_CACHE) + databaseProvider.getReadableDatabase(), + VersionTable.FEATURE_CACHE_CONTENT_METADATA) != VersionTable.VERSION_UNSET; } @@ -755,7 +756,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; try { int version = VersionTable.getVersion( - databaseProvider.getReadableDatabase(), VersionTable.FEATURE_CACHE); + databaseProvider.getReadableDatabase(), + VersionTable.FEATURE_CACHE_CONTENT_METADATA); if (version == VersionTable.VERSION_UNSET || version > TABLE_VERSION) { SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); writableDatabase.beginTransaction(); @@ -870,7 +872,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } private void initializeTable(SQLiteDatabase writableDatabase) { - VersionTable.setVersion(writableDatabase, VersionTable.FEATURE_CACHE, TABLE_VERSION); + VersionTable.setVersion( + writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA, TABLE_VERSION); writableDatabase.execSQL(SQL_DROP_TABLE_IF_EXISTS); writableDatabase.execSQL(SQL_CREATE_TABLE); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java index dcdedfc32d..f66471ba1f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/SimpleCache.java @@ -253,13 +253,12 @@ public final class SimpleCache implements Cache { String fileName = span.file.getName(); long length = span.length; long lastAccessTimestamp = System.currentTimeMillis(); - // Updating the file itself to incorporate the new last access timestamp is much slower than - // updating the file index. Hence we only update the file if we don't have a file index, or if - // updating the file index failed. - boolean updateFile; + boolean updateFile = false; if (fileIndex != null) { - updateFile = !fileIndex.set(fileName, length, lastAccessTimestamp); + fileIndex.set(fileName, length, lastAccessTimestamp); } else { + // Updating the file itself to incorporate the new last access timestamp is much slower than + // updating the file index. Hence we only update the file if we don't have a file index. updateFile = true; } SimpleCacheSpan newSpan = diff --git a/library/core/src/test/java/com/google/android/exoplayer2/database/VersionTableTest.java b/library/core/src/test/java/com/google/android/exoplayer2/database/VersionTableTest.java index a607cc01db..44961e8681 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/database/VersionTableTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/database/VersionTableTest.java @@ -15,7 +15,7 @@ */ package com.google.android.exoplayer2.database; -import static com.google.android.exoplayer2.database.VersionTable.FEATURE_CACHE; +import static com.google.android.exoplayer2.database.VersionTable.FEATURE_CACHE_CONTENT_METADATA; import static com.google.android.exoplayer2.database.VersionTable.FEATURE_OFFLINE; import static com.google.common.truth.Truth.assertThat; @@ -61,8 +61,9 @@ public class VersionTableTest { VersionTable.setVersion(writableDatabase, FEATURE_OFFLINE, 10); assertThat(VersionTable.getVersion(readableDatabase, FEATURE_OFFLINE)).isEqualTo(10); - VersionTable.setVersion(writableDatabase, FEATURE_CACHE, 5); - assertThat(VersionTable.getVersion(readableDatabase, FEATURE_CACHE)).isEqualTo(5); + VersionTable.setVersion(writableDatabase, FEATURE_CACHE_CONTENT_METADATA, 5); + assertThat(VersionTable.getVersion(readableDatabase, FEATURE_CACHE_CONTENT_METADATA)) + .isEqualTo(5); assertThat(VersionTable.getVersion(readableDatabase, FEATURE_OFFLINE)).isEqualTo(10); }