mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Merge branch 'dev-v2' into rtsp-socket-factory
This commit is contained in:
commit
cdcf57374a
29 changed files with 754 additions and 277 deletions
|
|
@ -2,7 +2,12 @@
|
||||||
|
|
||||||
### dev-v2 (not yet released)
|
### dev-v2 (not yet released)
|
||||||
|
|
||||||
|
### 2.16.1 (2021-11-11)
|
||||||
|
|
||||||
* Core Library:
|
* Core Library:
|
||||||
|
* Fix track selection issue where overriding one track group did not
|
||||||
|
disable other track groups of the same type
|
||||||
|
([#9675](https://github.com/google/ExoPlayer/issues/9675)).
|
||||||
* Fix track selection issue where a mixture of non-empty and empty track
|
* Fix track selection issue where a mixture of non-empty and empty track
|
||||||
overrides is not applied correctly
|
overrides is not applied correctly
|
||||||
([#9649](https://github.com/google/ExoPlayer/issues/9649).
|
([#9649](https://github.com/google/ExoPlayer/issues/9649).
|
||||||
|
|
@ -14,6 +19,9 @@
|
||||||
* Extractors:
|
* Extractors:
|
||||||
* WAV: Add support for RF64 streams
|
* WAV: Add support for RF64 streams
|
||||||
([#9543](https://github.com/google/ExoPlayer/issues/9543).
|
([#9543](https://github.com/google/ExoPlayer/issues/9543).
|
||||||
|
* DASH:
|
||||||
|
* Add parsed essential and supplemental properties to the `Representation`
|
||||||
|
([#9579](https://github.com/google/ExoPlayer/issues/9579)).
|
||||||
* RTSP
|
* RTSP
|
||||||
* Provide a client API to override the `SocketFactory` used for any server
|
* Provide a client API to override the `SocketFactory` used for any server
|
||||||
connection ([#9606](https://github.com/google/ExoPlayer/pull/9606)).
|
connection ([#9606](https://github.com/google/ExoPlayer/pull/9606)).
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
project.ext {
|
project.ext {
|
||||||
// ExoPlayer version and version code.
|
// ExoPlayer version and version code.
|
||||||
releaseVersion = '2.16.0'
|
releaseVersion = '2.16.1'
|
||||||
releaseVersionCode = 2016000
|
releaseVersionCode = 2016001
|
||||||
minSdkVersion = 16
|
minSdkVersion = 16
|
||||||
appTargetSdkVersion = 29
|
appTargetSdkVersion = 29
|
||||||
// Upgrading this requires [Internal ref: b/193254928] to be fixed, or some
|
// Upgrading this requires [Internal ref: b/193254928] to be fixed, or some
|
||||||
|
|
@ -26,33 +26,32 @@ project.ext {
|
||||||
// Use the same Guava version as the Android repo:
|
// Use the same Guava version as the Android repo:
|
||||||
// https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA
|
// https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA
|
||||||
guavaVersion = '27.1-android'
|
guavaVersion = '27.1-android'
|
||||||
mockitoVersion = '3.4.0'
|
mockitoVersion = '3.12.4'
|
||||||
mockWebServerVersion = '3.12.0'
|
|
||||||
robolectricVersion = '4.6.1'
|
robolectricVersion = '4.6.1'
|
||||||
// Keep this in sync with Google's internal Checker Framework version.
|
// Keep this in sync with Google's internal Checker Framework version.
|
||||||
checkerframeworkVersion = '3.5.0'
|
checkerframeworkVersion = '3.13.0'
|
||||||
checkerframeworkCompatVersion = '2.5.0'
|
checkerframeworkCompatVersion = '2.5.5'
|
||||||
errorProneVersion = '2.9.0'
|
errorProneVersion = '2.10.0'
|
||||||
jsr305Version = '3.0.2'
|
jsr305Version = '3.0.2'
|
||||||
kotlinAnnotationsVersion = '1.5.20'
|
kotlinAnnotationsVersion = '1.5.31'
|
||||||
androidxAnnotationVersion = '1.2.0'
|
androidxAnnotationVersion = '1.3.0'
|
||||||
androidxAppCompatVersion = '1.3.0'
|
androidxAppCompatVersion = '1.3.1'
|
||||||
androidxCollectionVersion = '1.1.0'
|
androidxCollectionVersion = '1.1.0'
|
||||||
androidxCoreVersion = '1.6.0'
|
androidxCoreVersion = '1.7.0'
|
||||||
androidxFuturesVersion = '1.1.0'
|
androidxFuturesVersion = '1.1.0'
|
||||||
androidxMediaVersion = '1.4.3'
|
androidxMediaVersion = '1.4.3'
|
||||||
androidxMedia2Version = '1.1.3'
|
androidxMedia2Version = '1.2.0'
|
||||||
androidxMultidexVersion = '2.0.1'
|
androidxMultidexVersion = '2.0.1'
|
||||||
androidxRecyclerViewVersion = '1.2.1'
|
androidxRecyclerViewVersion = '1.2.1'
|
||||||
androidxMaterialVersion = '1.3.0'
|
androidxMaterialVersion = '1.4.0'
|
||||||
androidxTestCoreVersion = '1.3.0'
|
androidxTestCoreVersion = '1.4.0'
|
||||||
androidxTestJUnitVersion = '1.1.2'
|
androidxTestJUnitVersion = '1.1.3'
|
||||||
androidxTestRunnerVersion = '1.3.0'
|
androidxTestRunnerVersion = '1.4.0'
|
||||||
androidxTestRulesVersion = '1.3.0'
|
androidxTestRulesVersion = '1.4.0'
|
||||||
androidxTestServicesStorageVersion = '1.3.0'
|
androidxTestServicesStorageVersion = '1.4.0'
|
||||||
androidxTestTruthVersion = '1.3.0'
|
androidxTestTruthVersion = '1.4.0'
|
||||||
truthVersion = '1.1.3'
|
truthVersion = '1.1.3'
|
||||||
okhttpVersion = '4.9.1'
|
okhttpVersion = '4.9.2'
|
||||||
modulePrefix = ':'
|
modulePrefix = ':'
|
||||||
if (gradle.ext.has('exoplayerModulePrefix')) {
|
if (gradle.ext.has('exoplayerModulePrefix')) {
|
||||||
modulePrefix += gradle.ext.exoplayerModulePrefix
|
modulePrefix += gradle.ext.exoplayerModulePrefix
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ may result in unexpected or obscure errors. It will be removed in ExoPlayer
|
||||||
2.16.
|
2.16.
|
||||||
{:.info}
|
{:.info}
|
||||||
|
|
||||||
For more information about ExoPlayer's treading model, see the
|
For more information about ExoPlayer's threading model, see the
|
||||||
["Threading model" section of the ExoPlayer Javadoc][].
|
["Threading model" section of the ExoPlayer Javadoc][].
|
||||||
|
|
||||||
## Attaching the player to a view ##
|
## Attaching the player to a view ##
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@ Changes in player state can be received by implementing
|
||||||
`Player.Listener`. The player can be in one of four playback states:
|
`Player.Listener`. The player can be in one of four playback states:
|
||||||
|
|
||||||
* `Player.STATE_IDLE`: This is the initial state, the state when the player is
|
* `Player.STATE_IDLE`: This is the initial state, the state when the player is
|
||||||
stopped, and when playback failed.
|
stopped, and when playback failed. The player will hold only limited resources
|
||||||
|
in this state.
|
||||||
* `Player.STATE_BUFFERING`: The player is not able to immediately play from its
|
* `Player.STATE_BUFFERING`: The player is not able to immediately play from its
|
||||||
current position. This mostly happens because more data needs to be loaded.
|
current position. This mostly happens because more data needs to be loaded.
|
||||||
* `Player.STATE_READY`: The player is able to immediately play from its current
|
* `Player.STATE_READY`: The player is able to immediately play from its current
|
||||||
|
|
|
||||||
|
|
@ -392,7 +392,6 @@ public class CastPlayerTest {
|
||||||
assertThat(mediaQueueItems[1].getMedia().getContentId()).isEqualTo(uri2);
|
assertThat(mediaQueueItems[1].getMedia().getContentId()).isEqualTo(uri2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("deprecation") // Verifies deprecated callback being called correctly.
|
|
||||||
@Test
|
@Test
|
||||||
public void setMediaItems_replaceExistingPlaylist_notifiesMediaItemTransition() {
|
public void setMediaItems_replaceExistingPlaylist_notifiesMediaItemTransition() {
|
||||||
List<MediaItem> firstPlaylist = new ArrayList<>();
|
List<MediaItem> firstPlaylist = new ArrayList<>();
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ dependencies {
|
||||||
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
||||||
// Instrumentation tests assume that an app-packaged version of cronet is
|
// Instrumentation tests assume that an app-packaged version of cronet is
|
||||||
// available.
|
// available.
|
||||||
androidTestImplementation 'org.chromium.net:cronet-embedded:94.4606.61'
|
androidTestImplementation 'org.chromium.net:cronet-embedded:95.4638.50'
|
||||||
androidTestImplementation project(modulePrefix + 'testutils')
|
androidTestImplementation project(modulePrefix + 'testutils')
|
||||||
testImplementation project(modulePrefix + 'testutils')
|
testImplementation project(modulePrefix + 'testutils')
|
||||||
testImplementation 'com.squareup.okhttp3:mockwebserver:' + okhttpVersion
|
testImplementation 'com.squareup.okhttp3:mockwebserver:' + okhttpVersion
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.24.0'
|
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.25.1'
|
||||||
implementation project(modulePrefix + 'library-core')
|
implementation project(modulePrefix + 'library-core')
|
||||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||||
|
|
|
||||||
|
|
@ -134,8 +134,6 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab
|
||||||
return player.getPlaybackState() == Player.STATE_IDLE ? -1 : player.getCurrentPosition();
|
return player.getPlaybackState() == Player.STATE_IDLE ? -1 : player.getCurrentPosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calls deprecated method to provide backwards compatibility.
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
@Override
|
@Override
|
||||||
public void play() {
|
public void play() {
|
||||||
if (player.getPlaybackState() == Player.STATE_IDLE) {
|
if (player.getPlaybackState() == Player.STATE_IDLE) {
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,11 @@ public final class ExoPlayerLibraryInfo {
|
||||||
|
|
||||||
/** The version of the library expressed as a string, for example "1.2.3". */
|
/** The version of the library expressed as a string, for example "1.2.3". */
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
||||||
public static final String VERSION = "2.16.0";
|
public static final String VERSION = "2.16.1";
|
||||||
|
|
||||||
/** The version of the library expressed as {@code TAG + "/" + VERSION}. */
|
/** The version of the library expressed as {@code TAG + "/" + VERSION}. */
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.16.0";
|
public static final String VERSION_SLASHY = "ExoPlayerLib/2.16.1";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The version of the library expressed as an integer, for example 1002003.
|
* The version of the library expressed as an integer, for example 1002003.
|
||||||
|
|
@ -41,7 +41,7 @@ public final class ExoPlayerLibraryInfo {
|
||||||
* integer version 123045006 (123-045-006).
|
* integer version 123045006 (123-045-006).
|
||||||
*/
|
*/
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||||
public static final int VERSION_INT = 2016000;
|
public static final int VERSION_INT = 2016001;
|
||||||
|
|
||||||
/** Whether the library was compiled with {@link Assertions} checks enabled. */
|
/** Whether the library was compiled with {@link Assertions} checks enabled. */
|
||||||
public static final boolean ASSERTIONS_ENABLED = true;
|
public static final boolean ASSERTIONS_ENABLED = true;
|
||||||
|
|
|
||||||
|
|
@ -1087,7 +1087,10 @@ public interface Player {
|
||||||
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
|
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
|
||||||
@IntDef({STATE_IDLE, STATE_BUFFERING, STATE_READY, STATE_ENDED})
|
@IntDef({STATE_IDLE, STATE_BUFFERING, STATE_READY, STATE_ENDED})
|
||||||
@interface State {}
|
@interface State {}
|
||||||
/** The player is idle, and must be {@link #prepare() prepared} before it will play the media. */
|
/**
|
||||||
|
* The player is idle, meaning it holds only limited resources. The player must be {@link
|
||||||
|
* #prepare() prepared} before it will play the media.
|
||||||
|
*/
|
||||||
int STATE_IDLE = 1;
|
int STATE_IDLE = 1;
|
||||||
/**
|
/**
|
||||||
* The player is not able to immediately play the media, but is doing work toward being able to do
|
* The player is not able to immediately play the media, but is doing work toward being able to do
|
||||||
|
|
@ -1669,7 +1672,12 @@ public interface Player {
|
||||||
*/
|
*/
|
||||||
Commands getAvailableCommands();
|
Commands getAvailableCommands();
|
||||||
|
|
||||||
/** Prepares the player. */
|
/**
|
||||||
|
* Prepares the player.
|
||||||
|
*
|
||||||
|
* <p>This will move the player out of {@link #STATE_IDLE idle state} and the player will start
|
||||||
|
* loading media and acquire resources needed for playback.
|
||||||
|
*/
|
||||||
void prepare();
|
void prepare();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -2001,12 +2009,13 @@ public interface Player {
|
||||||
PlaybackParameters getPlaybackParameters();
|
PlaybackParameters getPlaybackParameters();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops playback without resetting the player. Use {@link #pause()} rather than this method if
|
* Stops playback without resetting the playlist. Use {@link #pause()} rather than this method if
|
||||||
* the intention is to pause playback.
|
* the intention is to pause playback.
|
||||||
*
|
*
|
||||||
* <p>Calling this method will cause the playback state to transition to {@link #STATE_IDLE}. The
|
* <p>Calling this method will cause the playback state to transition to {@link #STATE_IDLE} and
|
||||||
* player instance can still be used, and {@link #release()} must still be called on the player if
|
* the player will release the loaded media and resources required for playback. The player
|
||||||
* it's no longer required.
|
* instance can still be used by calling {@link #prepare()} again, and {@link #release()} must
|
||||||
|
* still be called on the player if it's no longer required.
|
||||||
*
|
*
|
||||||
* <p>Calling this method does not clear the playlist, reset the playback position or the playback
|
* <p>Calling this method does not clear the playlist, reset the playback position or the playback
|
||||||
* error.
|
* error.
|
||||||
|
|
|
||||||
|
|
@ -362,7 +362,6 @@ public final class Cue implements Bundleable {
|
||||||
* @param textSize See {@link #textSize}.
|
* @param textSize See {@link #textSize}.
|
||||||
* @deprecated Use {@link Builder}.
|
* @deprecated Use {@link Builder}.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public Cue(
|
public Cue(
|
||||||
CharSequence text,
|
CharSequence text,
|
||||||
|
|
|
||||||
|
|
@ -262,7 +262,6 @@ public class DefaultLoadControl implements LoadControl {
|
||||||
private boolean isLoading;
|
private boolean isLoading;
|
||||||
|
|
||||||
/** Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. */
|
/** Constructs a new instance, using the {@code DEFAULT_*} constants defined in this class. */
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
public DefaultLoadControl() {
|
public DefaultLoadControl() {
|
||||||
this(
|
this(
|
||||||
new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
|
new DefaultAllocator(true, C.DEFAULT_BUFFER_SEGMENT_SIZE),
|
||||||
|
|
|
||||||
|
|
@ -396,7 +396,6 @@ public class SimpleExoPlayer extends BasePlayer
|
||||||
|
|
||||||
/** @deprecated Use the {@link ExoPlayer.Builder}. */
|
/** @deprecated Use the {@link ExoPlayer.Builder}. */
|
||||||
@Deprecated
|
@Deprecated
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
protected SimpleExoPlayer(
|
protected SimpleExoPlayer(
|
||||||
Context context,
|
Context context,
|
||||||
RenderersFactory renderersFactory,
|
RenderersFactory renderersFactory,
|
||||||
|
|
@ -428,7 +427,6 @@ public class SimpleExoPlayer extends BasePlayer
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param builder The {@link ExoPlayer.Builder} to obtain all construction parameters. */
|
/** @param builder The {@link ExoPlayer.Builder} to obtain all construction parameters. */
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
/* package */ SimpleExoPlayer(ExoPlayer.Builder builder) {
|
/* package */ SimpleExoPlayer(ExoPlayer.Builder builder) {
|
||||||
constructorFinished = new ConditionVariable();
|
constructorFinished = new ConditionVariable();
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -753,6 +753,8 @@ public class DashManifestParser extends DefaultHandler
|
||||||
drmSchemeType,
|
drmSchemeType,
|
||||||
drmSchemeDatas,
|
drmSchemeDatas,
|
||||||
inbandEventStreams,
|
inbandEventStreams,
|
||||||
|
essentialProperties,
|
||||||
|
supplementalProperties,
|
||||||
Representation.REVISION_ID_DEFAULT);
|
Representation.REVISION_ID_DEFAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -841,7 +843,10 @@ public class DashManifestParser extends DefaultHandler
|
||||||
formatBuilder.build(),
|
formatBuilder.build(),
|
||||||
representationInfo.baseUrls,
|
representationInfo.baseUrls,
|
||||||
representationInfo.segmentBase,
|
representationInfo.segmentBase,
|
||||||
inbandEventStreams);
|
inbandEventStreams,
|
||||||
|
representationInfo.essentialProperties,
|
||||||
|
representationInfo.supplementalProperties,
|
||||||
|
/* cacheKey= */ null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// SegmentBase, SegmentList and SegmentTemplate parsing.
|
// SegmentBase, SegmentList and SegmentTemplate parsing.
|
||||||
|
|
@ -1910,6 +1915,8 @@ public class DashManifestParser extends DefaultHandler
|
||||||
public final ArrayList<SchemeData> drmSchemeDatas;
|
public final ArrayList<SchemeData> drmSchemeDatas;
|
||||||
public final ArrayList<Descriptor> inbandEventStreams;
|
public final ArrayList<Descriptor> inbandEventStreams;
|
||||||
public final long revisionId;
|
public final long revisionId;
|
||||||
|
public final List<Descriptor> essentialProperties;
|
||||||
|
public final List<Descriptor> supplementalProperties;
|
||||||
|
|
||||||
public RepresentationInfo(
|
public RepresentationInfo(
|
||||||
Format format,
|
Format format,
|
||||||
|
|
@ -1918,6 +1925,8 @@ public class DashManifestParser extends DefaultHandler
|
||||||
@Nullable String drmSchemeType,
|
@Nullable String drmSchemeType,
|
||||||
ArrayList<SchemeData> drmSchemeDatas,
|
ArrayList<SchemeData> drmSchemeDatas,
|
||||||
ArrayList<Descriptor> inbandEventStreams,
|
ArrayList<Descriptor> inbandEventStreams,
|
||||||
|
List<Descriptor> essentialProperties,
|
||||||
|
List<Descriptor> supplementalProperties,
|
||||||
long revisionId) {
|
long revisionId) {
|
||||||
this.format = format;
|
this.format = format;
|
||||||
this.baseUrls = ImmutableList.copyOf(baseUrls);
|
this.baseUrls = ImmutableList.copyOf(baseUrls);
|
||||||
|
|
@ -1925,6 +1934,8 @@ public class DashManifestParser extends DefaultHandler
|
||||||
this.drmSchemeType = drmSchemeType;
|
this.drmSchemeType = drmSchemeType;
|
||||||
this.drmSchemeDatas = drmSchemeDatas;
|
this.drmSchemeDatas = drmSchemeDatas;
|
||||||
this.inbandEventStreams = inbandEventStreams;
|
this.inbandEventStreams = inbandEventStreams;
|
||||||
|
this.essentialProperties = essentialProperties;
|
||||||
|
this.supplementalProperties = supplementalProperties;
|
||||||
this.revisionId = revisionId;
|
this.revisionId = revisionId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,10 @@ public abstract class Representation {
|
||||||
public final long presentationTimeOffsetUs;
|
public final long presentationTimeOffsetUs;
|
||||||
/** The in-band event streams in the representation. May be empty. */
|
/** The in-band event streams in the representation. May be empty. */
|
||||||
public final List<Descriptor> inbandEventStreams;
|
public final List<Descriptor> inbandEventStreams;
|
||||||
|
/** Essential properties in the representation. May be empty. */
|
||||||
|
public final List<Descriptor> essentialProperties;
|
||||||
|
/** Supplemental properties in the adaptation set. May be empty. */
|
||||||
|
public final List<Descriptor> supplementalProperties;
|
||||||
|
|
||||||
private final RangedUri initializationUri;
|
private final RangedUri initializationUri;
|
||||||
|
|
||||||
|
|
@ -64,27 +68,15 @@ public abstract class Representation {
|
||||||
*/
|
*/
|
||||||
public static Representation newInstance(
|
public static Representation newInstance(
|
||||||
long revisionId, Format format, List<BaseUrl> baseUrls, SegmentBase segmentBase) {
|
long revisionId, Format format, List<BaseUrl> baseUrls, SegmentBase segmentBase) {
|
||||||
return newInstance(revisionId, format, baseUrls, segmentBase, /* inbandEventStreams= */ null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a new instance.
|
|
||||||
*
|
|
||||||
* @param revisionId Identifies the revision of the content.
|
|
||||||
* @param format The format of the representation.
|
|
||||||
* @param baseUrls The list of base URLs of the representation.
|
|
||||||
* @param segmentBase A segment base element for the representation.
|
|
||||||
* @param inbandEventStreams The in-band event streams in the representation. May be null.
|
|
||||||
* @return The constructed instance.
|
|
||||||
*/
|
|
||||||
public static Representation newInstance(
|
|
||||||
long revisionId,
|
|
||||||
Format format,
|
|
||||||
List<BaseUrl> baseUrls,
|
|
||||||
SegmentBase segmentBase,
|
|
||||||
@Nullable List<Descriptor> inbandEventStreams) {
|
|
||||||
return newInstance(
|
return newInstance(
|
||||||
revisionId, format, baseUrls, segmentBase, inbandEventStreams, /* cacheKey= */ null);
|
revisionId,
|
||||||
|
format,
|
||||||
|
baseUrls,
|
||||||
|
segmentBase,
|
||||||
|
/* inbandEventStreams= */ null,
|
||||||
|
/* essentialProperties= */ ImmutableList.of(),
|
||||||
|
/* supplementalProperties= */ ImmutableList.of(),
|
||||||
|
/* cacheKey= */ null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -95,6 +87,8 @@ public abstract class Representation {
|
||||||
* @param baseUrls The list of base URLs of the representation.
|
* @param baseUrls The list of base URLs of the representation.
|
||||||
* @param segmentBase A segment base element for the representation.
|
* @param segmentBase A segment base element for the representation.
|
||||||
* @param inbandEventStreams The in-band event streams in the representation. May be null.
|
* @param inbandEventStreams The in-band event streams in the representation. May be null.
|
||||||
|
* @param essentialProperties Essential properties in the representation. May be empty.
|
||||||
|
* @param supplementalProperties Supplemental properties in the representation. May be empty.
|
||||||
* @param cacheKey An optional key to be returned from {@link #getCacheKey()}, or null. This
|
* @param cacheKey An optional key to be returned from {@link #getCacheKey()}, or null. This
|
||||||
* parameter is ignored if {@code segmentBase} consists of multiple segments.
|
* parameter is ignored if {@code segmentBase} consists of multiple segments.
|
||||||
* @return The constructed instance.
|
* @return The constructed instance.
|
||||||
|
|
@ -105,6 +99,8 @@ public abstract class Representation {
|
||||||
List<BaseUrl> baseUrls,
|
List<BaseUrl> baseUrls,
|
||||||
SegmentBase segmentBase,
|
SegmentBase segmentBase,
|
||||||
@Nullable List<Descriptor> inbandEventStreams,
|
@Nullable List<Descriptor> inbandEventStreams,
|
||||||
|
List<Descriptor> essentialProperties,
|
||||||
|
List<Descriptor> supplementalProperties,
|
||||||
@Nullable String cacheKey) {
|
@Nullable String cacheKey) {
|
||||||
if (segmentBase instanceof SingleSegmentBase) {
|
if (segmentBase instanceof SingleSegmentBase) {
|
||||||
return new SingleSegmentRepresentation(
|
return new SingleSegmentRepresentation(
|
||||||
|
|
@ -113,11 +109,19 @@ public abstract class Representation {
|
||||||
baseUrls,
|
baseUrls,
|
||||||
(SingleSegmentBase) segmentBase,
|
(SingleSegmentBase) segmentBase,
|
||||||
inbandEventStreams,
|
inbandEventStreams,
|
||||||
|
essentialProperties,
|
||||||
|
supplementalProperties,
|
||||||
cacheKey,
|
cacheKey,
|
||||||
C.LENGTH_UNSET);
|
/* contentLength= */ C.LENGTH_UNSET);
|
||||||
} else if (segmentBase instanceof MultiSegmentBase) {
|
} else if (segmentBase instanceof MultiSegmentBase) {
|
||||||
return new MultiSegmentRepresentation(
|
return new MultiSegmentRepresentation(
|
||||||
revisionId, format, baseUrls, (MultiSegmentBase) segmentBase, inbandEventStreams);
|
revisionId,
|
||||||
|
format,
|
||||||
|
baseUrls,
|
||||||
|
(MultiSegmentBase) segmentBase,
|
||||||
|
inbandEventStreams,
|
||||||
|
essentialProperties,
|
||||||
|
supplementalProperties);
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"segmentBase must be of type SingleSegmentBase or " + "MultiSegmentBase");
|
"segmentBase must be of type SingleSegmentBase or " + "MultiSegmentBase");
|
||||||
|
|
@ -129,7 +133,9 @@ public abstract class Representation {
|
||||||
Format format,
|
Format format,
|
||||||
List<BaseUrl> baseUrls,
|
List<BaseUrl> baseUrls,
|
||||||
SegmentBase segmentBase,
|
SegmentBase segmentBase,
|
||||||
@Nullable List<Descriptor> inbandEventStreams) {
|
@Nullable List<Descriptor> inbandEventStreams,
|
||||||
|
List<Descriptor> essentialProperties,
|
||||||
|
List<Descriptor> supplementalProperties) {
|
||||||
checkArgument(!baseUrls.isEmpty());
|
checkArgument(!baseUrls.isEmpty());
|
||||||
this.revisionId = revisionId;
|
this.revisionId = revisionId;
|
||||||
this.format = format;
|
this.format = format;
|
||||||
|
|
@ -138,6 +144,8 @@ public abstract class Representation {
|
||||||
inbandEventStreams == null
|
inbandEventStreams == null
|
||||||
? Collections.emptyList()
|
? Collections.emptyList()
|
||||||
: Collections.unmodifiableList(inbandEventStreams);
|
: Collections.unmodifiableList(inbandEventStreams);
|
||||||
|
this.essentialProperties = essentialProperties;
|
||||||
|
this.supplementalProperties = supplementalProperties;
|
||||||
initializationUri = segmentBase.getInitialization(this);
|
initializationUri = segmentBase.getInitialization(this);
|
||||||
presentationTimeOffsetUs = segmentBase.getPresentationTimeOffsetUs();
|
presentationTimeOffsetUs = segmentBase.getPresentationTimeOffsetUs();
|
||||||
}
|
}
|
||||||
|
|
@ -207,7 +215,15 @@ public abstract class Representation {
|
||||||
new SingleSegmentBase(rangedUri, 1, 0, indexStart, indexEnd - indexStart + 1);
|
new SingleSegmentBase(rangedUri, 1, 0, indexStart, indexEnd - indexStart + 1);
|
||||||
List<BaseUrl> baseUrls = ImmutableList.of(new BaseUrl(uri));
|
List<BaseUrl> baseUrls = ImmutableList.of(new BaseUrl(uri));
|
||||||
return new SingleSegmentRepresentation(
|
return new SingleSegmentRepresentation(
|
||||||
revisionId, format, baseUrls, segmentBase, inbandEventStreams, cacheKey, contentLength);
|
revisionId,
|
||||||
|
format,
|
||||||
|
baseUrls,
|
||||||
|
segmentBase,
|
||||||
|
inbandEventStreams,
|
||||||
|
/* essentialProperties= */ ImmutableList.of(),
|
||||||
|
/* supplementalProperties= */ ImmutableList.of(),
|
||||||
|
cacheKey,
|
||||||
|
contentLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -216,6 +232,8 @@ public abstract class Representation {
|
||||||
* @param baseUrls The base urls of the representation.
|
* @param baseUrls The base urls of the representation.
|
||||||
* @param segmentBase The segment base underlying the representation.
|
* @param segmentBase The segment base underlying the representation.
|
||||||
* @param inbandEventStreams The in-band event streams in the representation. May be null.
|
* @param inbandEventStreams The in-band event streams in the representation. May be null.
|
||||||
|
* @param essentialProperties Essential properties in the representation. May be empty.
|
||||||
|
* @param supplementalProperties Supplemental properties in the representation. May be empty.
|
||||||
* @param cacheKey An optional key to be returned from {@link #getCacheKey()}, or null.
|
* @param cacheKey An optional key to be returned from {@link #getCacheKey()}, or null.
|
||||||
* @param contentLength The content length, or {@link C#LENGTH_UNSET} if unknown.
|
* @param contentLength The content length, or {@link C#LENGTH_UNSET} if unknown.
|
||||||
*/
|
*/
|
||||||
|
|
@ -225,9 +243,18 @@ public abstract class Representation {
|
||||||
List<BaseUrl> baseUrls,
|
List<BaseUrl> baseUrls,
|
||||||
SingleSegmentBase segmentBase,
|
SingleSegmentBase segmentBase,
|
||||||
@Nullable List<Descriptor> inbandEventStreams,
|
@Nullable List<Descriptor> inbandEventStreams,
|
||||||
|
List<Descriptor> essentialProperties,
|
||||||
|
List<Descriptor> supplementalProperties,
|
||||||
@Nullable String cacheKey,
|
@Nullable String cacheKey,
|
||||||
long contentLength) {
|
long contentLength) {
|
||||||
super(revisionId, format, baseUrls, segmentBase, inbandEventStreams);
|
super(
|
||||||
|
revisionId,
|
||||||
|
format,
|
||||||
|
baseUrls,
|
||||||
|
segmentBase,
|
||||||
|
inbandEventStreams,
|
||||||
|
essentialProperties,
|
||||||
|
supplementalProperties);
|
||||||
this.uri = Uri.parse(baseUrls.get(0).url);
|
this.uri = Uri.parse(baseUrls.get(0).url);
|
||||||
this.indexUri = segmentBase.getIndex();
|
this.indexUri = segmentBase.getIndex();
|
||||||
this.cacheKey = cacheKey;
|
this.cacheKey = cacheKey;
|
||||||
|
|
@ -271,14 +298,25 @@ public abstract class Representation {
|
||||||
* @param baseUrls The base URLs of the representation.
|
* @param baseUrls The base URLs of the representation.
|
||||||
* @param segmentBase The segment base underlying the representation.
|
* @param segmentBase The segment base underlying the representation.
|
||||||
* @param inbandEventStreams The in-band event streams in the representation. May be null.
|
* @param inbandEventStreams The in-band event streams in the representation. May be null.
|
||||||
|
* @param essentialProperties Essential properties in the representation. May be empty.
|
||||||
|
* @param supplementalProperties Supplemental properties in the representation. May be empty.
|
||||||
*/
|
*/
|
||||||
public MultiSegmentRepresentation(
|
public MultiSegmentRepresentation(
|
||||||
long revisionId,
|
long revisionId,
|
||||||
Format format,
|
Format format,
|
||||||
List<BaseUrl> baseUrls,
|
List<BaseUrl> baseUrls,
|
||||||
MultiSegmentBase segmentBase,
|
MultiSegmentBase segmentBase,
|
||||||
@Nullable List<Descriptor> inbandEventStreams) {
|
@Nullable List<Descriptor> inbandEventStreams,
|
||||||
super(revisionId, format, baseUrls, segmentBase, inbandEventStreams);
|
List<Descriptor> essentialProperties,
|
||||||
|
List<Descriptor> supplementalProperties) {
|
||||||
|
super(
|
||||||
|
revisionId,
|
||||||
|
format,
|
||||||
|
baseUrls,
|
||||||
|
segmentBase,
|
||||||
|
inbandEventStreams,
|
||||||
|
essentialProperties,
|
||||||
|
supplementalProperties);
|
||||||
this.segmentBase = segmentBase;
|
this.segmentBase = segmentBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,8 @@ public final class DashUtilTest {
|
||||||
baseUrls,
|
baseUrls,
|
||||||
new SingleSegmentBase(),
|
new SingleSegmentBase(),
|
||||||
/* inbandEventStreams= */ null,
|
/* inbandEventStreams= */ null,
|
||||||
|
/* essentialProperties= */ ImmutableList.of(),
|
||||||
|
/* supplementalProperties= */ ImmutableList.of(),
|
||||||
/* cacheKey= */ null,
|
/* cacheKey= */ null,
|
||||||
/* contentLength= */ 1);
|
/* contentLength= */ 1);
|
||||||
RangedUri rangedUri = new RangedUri("path/to/resource", /* start= */ 0, /* length= */ 1);
|
RangedUri rangedUri = new RangedUri("path/to/resource", /* start= */ 0, /* length= */ 1);
|
||||||
|
|
@ -99,6 +101,8 @@ public final class DashUtilTest {
|
||||||
baseUrls,
|
baseUrls,
|
||||||
new SingleSegmentBase(),
|
new SingleSegmentBase(),
|
||||||
/* inbandEventStreams= */ null,
|
/* inbandEventStreams= */ null,
|
||||||
|
/* essentialProperties= */ ImmutableList.of(),
|
||||||
|
/* supplementalProperties= */ ImmutableList.of(),
|
||||||
"cacheKey",
|
"cacheKey",
|
||||||
/* contentLength= */ 1);
|
/* contentLength= */ 1);
|
||||||
RangedUri rangedUri = new RangedUri("path/to/resource", /* start= */ 0, /* length= */ 1);
|
RangedUri rangedUri = new RangedUri("path/to/resource", /* start= */ 0, /* length= */ 1);
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
|
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
|
||||||
|
import com.google.android.exoplayer2.source.dash.manifest.Representation.MultiSegmentRepresentation;
|
||||||
|
import com.google.android.exoplayer2.source.dash.manifest.Representation.SingleSegmentRepresentation;
|
||||||
import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTimelineElement;
|
import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SegmentTimelineElement;
|
||||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||||
import com.google.android.exoplayer2.util.MimeTypes;
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
|
@ -53,6 +55,8 @@ public class DashManifestParserTest {
|
||||||
private static final String SAMPLE_MPD_ASSET_IDENTIFIER = "media/mpd/sample_mpd_asset_identifier";
|
private static final String SAMPLE_MPD_ASSET_IDENTIFIER = "media/mpd/sample_mpd_asset_identifier";
|
||||||
private static final String SAMPLE_MPD_TEXT = "media/mpd/sample_mpd_text";
|
private static final String SAMPLE_MPD_TEXT = "media/mpd/sample_mpd_text";
|
||||||
private static final String SAMPLE_MPD_TRICK_PLAY = "media/mpd/sample_mpd_trick_play";
|
private static final String SAMPLE_MPD_TRICK_PLAY = "media/mpd/sample_mpd_trick_play";
|
||||||
|
private static final String SAMPLE_MPD_ESSENTIAL_SUPPLEMENTAL_PROPERTIES =
|
||||||
|
"media/mpd/sample_mpd_essential_supplemental_properties";
|
||||||
private static final String SAMPLE_MPD_AVAILABILITY_TIME_OFFSET_BASE_URL =
|
private static final String SAMPLE_MPD_AVAILABILITY_TIME_OFFSET_BASE_URL =
|
||||||
"media/mpd/sample_mpd_availabilityTimeOffset_baseUrl";
|
"media/mpd/sample_mpd_availabilityTimeOffset_baseUrl";
|
||||||
private static final String SAMPLE_MPD_MULTIPLE_BASE_URLS =
|
private static final String SAMPLE_MPD_MULTIPLE_BASE_URLS =
|
||||||
|
|
@ -504,6 +508,74 @@ public class DashManifestParserTest {
|
||||||
assertThat(assetIdentifier.id).isEqualTo("uniqueId");
|
assertThat(assetIdentifier.id).isEqualTo("uniqueId");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void parseEssentialAndSupplementalProperties() throws IOException {
|
||||||
|
DashManifestParser parser = new DashManifestParser();
|
||||||
|
DashManifest manifest =
|
||||||
|
parser.parse(
|
||||||
|
Uri.parse("https://example.com/test.mpd"),
|
||||||
|
TestUtil.getInputStream(
|
||||||
|
ApplicationProvider.getApplicationContext(),
|
||||||
|
SAMPLE_MPD_ESSENTIAL_SUPPLEMENTAL_PROPERTIES));
|
||||||
|
|
||||||
|
// Verify test setup.
|
||||||
|
assertThat(manifest.getPeriodCount()).isEqualTo(1);
|
||||||
|
assertThat(manifest.getPeriod(0).adaptationSets).hasSize(1);
|
||||||
|
AdaptationSet adaptationSet = manifest.getPeriod(0).adaptationSets.get(0);
|
||||||
|
assertThat(adaptationSet.representations).hasSize(2);
|
||||||
|
Representation representation0 = adaptationSet.representations.get(0);
|
||||||
|
Representation representation1 = adaptationSet.representations.get(1);
|
||||||
|
assertThat(representation0).isInstanceOf(SingleSegmentRepresentation.class);
|
||||||
|
assertThat(representation1).isInstanceOf(MultiSegmentRepresentation.class);
|
||||||
|
|
||||||
|
// Verify parsed properties.
|
||||||
|
assertThat(adaptationSet.essentialProperties).hasSize(1);
|
||||||
|
assertThat(adaptationSet.essentialProperties.get(0).schemeIdUri)
|
||||||
|
.isEqualTo("urn:mpeg:dash:essential-scheme:2050");
|
||||||
|
assertThat(adaptationSet.essentialProperties.get(0).value).isEqualTo("adaptationEssential");
|
||||||
|
assertThat(adaptationSet.supplementalProperties).hasSize(1);
|
||||||
|
assertThat(adaptationSet.supplementalProperties.get(0).schemeIdUri)
|
||||||
|
.isEqualTo("urn:mpeg:dash:supplemental-scheme:2050");
|
||||||
|
assertThat(adaptationSet.supplementalProperties.get(0).value)
|
||||||
|
.isEqualTo("adaptationSupplemental");
|
||||||
|
|
||||||
|
assertThat(representation0.essentialProperties).hasSize(2);
|
||||||
|
assertThat(representation0.essentialProperties.get(0).schemeIdUri)
|
||||||
|
.isEqualTo("urn:mpeg:dash:essential-scheme:2050");
|
||||||
|
assertThat(representation0.essentialProperties.get(0).value).isEqualTo("adaptationEssential");
|
||||||
|
assertThat(representation0.essentialProperties.get(1).schemeIdUri)
|
||||||
|
.isEqualTo("urn:mpeg:dash:essential-scheme:2050");
|
||||||
|
assertThat(representation0.essentialProperties.get(1).value)
|
||||||
|
.isEqualTo("representationEssential");
|
||||||
|
assertThat(representation0.supplementalProperties).hasSize(2);
|
||||||
|
assertThat(representation0.supplementalProperties.get(0).schemeIdUri)
|
||||||
|
.isEqualTo("urn:mpeg:dash:supplemental-scheme:2050");
|
||||||
|
assertThat(representation0.supplementalProperties.get(0).value)
|
||||||
|
.isEqualTo("adaptationSupplemental");
|
||||||
|
assertThat(representation0.supplementalProperties.get(1).schemeIdUri)
|
||||||
|
.isEqualTo("urn:mpeg:dash:supplemental-scheme:2050");
|
||||||
|
assertThat(representation0.supplementalProperties.get(1).value)
|
||||||
|
.isEqualTo("representationSupplemental");
|
||||||
|
|
||||||
|
assertThat(representation1.essentialProperties).hasSize(2);
|
||||||
|
assertThat(representation0.essentialProperties.get(0).schemeIdUri)
|
||||||
|
.isEqualTo("urn:mpeg:dash:essential-scheme:2050");
|
||||||
|
assertThat(representation0.essentialProperties.get(0).value).isEqualTo("adaptationEssential");
|
||||||
|
assertThat(representation1.essentialProperties.get(1).schemeIdUri)
|
||||||
|
.isEqualTo("urn:mpeg:dash:essential-scheme:2050");
|
||||||
|
assertThat(representation1.essentialProperties.get(1).value)
|
||||||
|
.isEqualTo("representationEssential");
|
||||||
|
assertThat(representation1.supplementalProperties).hasSize(2);
|
||||||
|
assertThat(representation0.supplementalProperties.get(0).schemeIdUri)
|
||||||
|
.isEqualTo("urn:mpeg:dash:supplemental-scheme:2050");
|
||||||
|
assertThat(representation0.supplementalProperties.get(0).value)
|
||||||
|
.isEqualTo("adaptationSupplemental");
|
||||||
|
assertThat(representation1.supplementalProperties.get(1).schemeIdUri)
|
||||||
|
.isEqualTo("urn:mpeg:dash:supplemental-scheme:2050");
|
||||||
|
assertThat(representation1.supplementalProperties.get(1).value)
|
||||||
|
.isEqualTo("representationSupplemental");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void availabilityTimeOffset_staticManifest_setToTimeUnset() throws IOException {
|
public void availabilityTimeOffset_staticManifest_setToTimeUnset() throws IOException {
|
||||||
DashManifestParser parser = new DashManifestParser();
|
DashManifestParser parser = new DashManifestParser();
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,27 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
// The following argument makes the Android Test Orchestrator run its
|
||||||
|
// "pm clear" command after each test invocation. This command ensures
|
||||||
|
// that the app's state is completely cleared between tests.
|
||||||
|
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||||
|
multiDexEnabled true
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
testCoverageEnabled = true
|
testCoverageEnabled = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets.test.assets.srcDir '../../testdata/src/test/assets/'
|
sourceSets {
|
||||||
|
androidTest.assets.srcDir '../test_data/src/test/assets/' //copybara:media3-only
|
||||||
|
androidTest.assets.srcDir '../../testdata/src/test/assets/'
|
||||||
|
test.assets.srcDir '../../testdata/src/test/assets/'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
@ -33,6 +45,10 @@ dependencies {
|
||||||
testImplementation project(modulePrefix + 'testutils')
|
testImplementation project(modulePrefix + 'testutils')
|
||||||
testImplementation project(modulePrefix + 'testdata')
|
testImplementation project(modulePrefix + 'testdata')
|
||||||
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
testImplementation 'org.robolectric:robolectric:' + robolectricVersion
|
||||||
|
testImplementation 'com.google.truth:truth:' + truthVersion
|
||||||
|
androidTestImplementation 'junit:junit:' + junitVersion
|
||||||
|
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
|
||||||
|
androidTestImplementation project(modulePrefix + 'testutils')
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
|
|
|
||||||
37
library/transformer/src/androidTest/AndroidManifest.xml
Normal file
37
library/transformer/src/androidTest/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="com.google.android.exoplayer2.transformer">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
|
<uses-sdk/>
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="false"
|
||||||
|
tools:ignore="MissingApplicationIcon,HardcodedDebugMode"
|
||||||
|
android:usesCleartextTraffic="true"/>
|
||||||
|
|
||||||
|
<instrumentation
|
||||||
|
android:targetPackage="com.google.android.exoplayer2.transformer"
|
||||||
|
android:name="androidx.test.runner.AndroidJUnitRunner"/>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.transformer;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry;
|
||||||
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
|
import com.google.android.exoplayer2.util.Assertions;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||||
|
|
||||||
|
/** Utility methods for instrumentation tests. */
|
||||||
|
/* package */ final class AndroidTestUtil {
|
||||||
|
public static final String MP4_ASSET_URI = "asset:///media/mp4/sample.mp4";
|
||||||
|
public static final String SEF_ASSET_URI = "asset:///media/mp4/sample_sef_slow_motion.mp4";
|
||||||
|
|
||||||
|
/** Transforms the {@code uriString} with the {@link Transformer}. */
|
||||||
|
public static void runTransformer(Context context, Transformer transformer, String uriString)
|
||||||
|
throws Exception {
|
||||||
|
AtomicReference<@NullableType Exception> exceptionReference = new AtomicReference<>();
|
||||||
|
CountDownLatch countDownLatch = new CountDownLatch(1);
|
||||||
|
|
||||||
|
Transformer testTransformer =
|
||||||
|
transformer
|
||||||
|
.buildUpon()
|
||||||
|
.setListener(
|
||||||
|
new Transformer.Listener() {
|
||||||
|
@Override
|
||||||
|
public void onTransformationCompleted(MediaItem inputMediaItem) {
|
||||||
|
countDownLatch.countDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTransformationError(MediaItem inputMediaItem, Exception exception) {
|
||||||
|
exceptionReference.set(exception);
|
||||||
|
countDownLatch.countDown();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Uri uri = Uri.parse(uriString);
|
||||||
|
File externalCacheFile = createExternalCacheFile(uri, context);
|
||||||
|
try {
|
||||||
|
InstrumentationRegistry.getInstrumentation()
|
||||||
|
.runOnMainSync(
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
testTransformer.startTransformation(
|
||||||
|
MediaItem.fromUri(uri), externalCacheFile.getAbsolutePath());
|
||||||
|
} catch (IOException e) {
|
||||||
|
exceptionReference.set(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
countDownLatch.await();
|
||||||
|
@Nullable Exception exception = exceptionReference.get();
|
||||||
|
if (exception != null) {
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
externalCacheFile.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static File createExternalCacheFile(Uri uri, Context context) throws IOException {
|
||||||
|
File file = new File(context.getExternalCacheDir(), "transformer-" + uri.hashCode());
|
||||||
|
Assertions.checkState(
|
||||||
|
!file.exists() || file.delete(), "Could not delete the previous transformer output file");
|
||||||
|
Assertions.checkState(file.createNewFile(), "Could not create the transformer output file");
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AndroidTestUtil() {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.transformer;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.transformer.AndroidTestUtil.MP4_ASSET_URI;
|
||||||
|
import static com.google.android.exoplayer2.transformer.AndroidTestUtil.runTransformer;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** {@link Transformer} instrumentation test for removing audio. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class RemoveAudioTransformationTest {
|
||||||
|
@Test
|
||||||
|
public void removeAudioTransform() throws Exception {
|
||||||
|
Context context = ApplicationProvider.getApplicationContext();
|
||||||
|
Transformer transformer =
|
||||||
|
new Transformer.Builder().setContext(context).setRemoveAudio(true).build();
|
||||||
|
runTransformer(context, transformer, MP4_ASSET_URI);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.transformer;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.transformer.AndroidTestUtil.MP4_ASSET_URI;
|
||||||
|
import static com.google.android.exoplayer2.transformer.AndroidTestUtil.runTransformer;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** {@link Transformer} instrumentation test for removing video. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class RemoveVideoTransformationTest {
|
||||||
|
@Test
|
||||||
|
public void removeVideoTransform() throws Exception {
|
||||||
|
Context context = ApplicationProvider.getApplicationContext();
|
||||||
|
Transformer transformer =
|
||||||
|
new Transformer.Builder().setContext(context).setRemoveVideo(true).build();
|
||||||
|
runTransformer(context, transformer, MP4_ASSET_URI);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.transformer;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.transformer.AndroidTestUtil.SEF_ASSET_URI;
|
||||||
|
import static com.google.android.exoplayer2.transformer.AndroidTestUtil.runTransformer;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** {@link Transformer} instrumentation test for SEF. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class SefTransformationTest {
|
||||||
|
@Test
|
||||||
|
public void sefTransform() throws Exception {
|
||||||
|
Context context = ApplicationProvider.getApplicationContext();
|
||||||
|
Transformer transformer =
|
||||||
|
new Transformer.Builder().setContext(context).setFlattenForSlowMotion(true).build();
|
||||||
|
runTransformer(context, transformer, SEF_ASSET_URI);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.exoplayer2.transformer;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.transformer.AndroidTestUtil.MP4_ASSET_URI;
|
||||||
|
import static com.google.android.exoplayer2.transformer.AndroidTestUtil.runTransformer;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** {@link Transformer} instrumentation test. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class TransformationTest {
|
||||||
|
@Test
|
||||||
|
public void transform() throws Exception {
|
||||||
|
Context context = ApplicationProvider.getApplicationContext();
|
||||||
|
Transformer transformer = new Transformer.Builder().setContext(context).build();
|
||||||
|
runTransformer(context, transformer, MP4_ASSET_URI);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,180 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2021 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.android.exoplayer2.transformer;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||||
|
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.SurfaceTexture;
|
||||||
|
import android.opengl.EGL14;
|
||||||
|
import android.opengl.EGLContext;
|
||||||
|
import android.opengl.EGLDisplay;
|
||||||
|
import android.opengl.EGLExt;
|
||||||
|
import android.opengl.EGLSurface;
|
||||||
|
import android.opengl.GLES20;
|
||||||
|
import android.view.Surface;
|
||||||
|
import androidx.annotation.RequiresApi;
|
||||||
|
import com.google.android.exoplayer2.Format;
|
||||||
|
import com.google.android.exoplayer2.util.GlUtil;
|
||||||
|
import java.io.IOException;
|
||||||
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
|
/** Applies OpenGL transformations to video frames. */
|
||||||
|
@RequiresApi(18)
|
||||||
|
/* package */ final class OpenGlFrameEditor {
|
||||||
|
|
||||||
|
static {
|
||||||
|
GlUtil.glAssertionsEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static OpenGlFrameEditor create(
|
||||||
|
Context context, Format inputFormat, Surface outputSurface) {
|
||||||
|
EGLDisplay eglDisplay = GlUtil.createEglDisplay();
|
||||||
|
EGLContext eglContext;
|
||||||
|
try {
|
||||||
|
eglContext = GlUtil.createEglContext(eglDisplay);
|
||||||
|
} catch (GlUtil.UnsupportedEglVersionException e) {
|
||||||
|
throw new IllegalStateException("EGL version is unsupported", e);
|
||||||
|
}
|
||||||
|
EGLSurface eglSurface = GlUtil.getEglSurface(eglDisplay, outputSurface);
|
||||||
|
GlUtil.focusSurface(eglDisplay, eglContext, eglSurface, inputFormat.width, inputFormat.height);
|
||||||
|
int textureId = GlUtil.createExternalTexture();
|
||||||
|
GlUtil.Program copyProgram;
|
||||||
|
try {
|
||||||
|
copyProgram = new GlUtil.Program(context, VERTEX_SHADER_FILE_PATH, FRAGMENT_SHADER_FILE_PATH);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
copyProgram.use();
|
||||||
|
GlUtil.Attribute[] copyAttributes = copyProgram.getAttributes();
|
||||||
|
checkState(
|
||||||
|
copyAttributes.length == EXPECTED_NUMBER_OF_ATTRIBUTES,
|
||||||
|
"Expected program to have " + EXPECTED_NUMBER_OF_ATTRIBUTES + " vertex attributes.");
|
||||||
|
for (GlUtil.Attribute copyAttribute : copyAttributes) {
|
||||||
|
if (copyAttribute.name.equals("a_position")) {
|
||||||
|
copyAttribute.setBuffer(
|
||||||
|
new float[] {
|
||||||
|
-1.0f, -1.0f, 0.0f, 1.0f,
|
||||||
|
1.0f, -1.0f, 0.0f, 1.0f,
|
||||||
|
-1.0f, 1.0f, 0.0f, 1.0f,
|
||||||
|
1.0f, 1.0f, 0.0f, 1.0f,
|
||||||
|
},
|
||||||
|
/* size= */ 4);
|
||||||
|
} else if (copyAttribute.name.equals("a_texcoord")) {
|
||||||
|
copyAttribute.setBuffer(
|
||||||
|
new float[] {
|
||||||
|
0.0f, 0.0f, 0.0f, 1.0f,
|
||||||
|
1.0f, 0.0f, 0.0f, 1.0f,
|
||||||
|
0.0f, 1.0f, 0.0f, 1.0f,
|
||||||
|
1.0f, 1.0f, 0.0f, 1.0f,
|
||||||
|
},
|
||||||
|
/* size= */ 4);
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Unexpected attribute name.");
|
||||||
|
}
|
||||||
|
copyAttribute.bind();
|
||||||
|
}
|
||||||
|
GlUtil.Uniform[] copyUniforms = copyProgram.getUniforms();
|
||||||
|
checkState(
|
||||||
|
copyUniforms.length == EXPECTED_NUMBER_OF_UNIFORMS,
|
||||||
|
"Expected program to have " + EXPECTED_NUMBER_OF_UNIFORMS + " uniforms.");
|
||||||
|
GlUtil.@MonotonicNonNull Uniform textureTransformUniform = null;
|
||||||
|
for (GlUtil.Uniform copyUniform : copyUniforms) {
|
||||||
|
if (copyUniform.name.equals("tex_sampler")) {
|
||||||
|
copyUniform.setSamplerTexId(textureId, 0);
|
||||||
|
copyUniform.bind();
|
||||||
|
} else if (copyUniform.name.equals("tex_transform")) {
|
||||||
|
textureTransformUniform = copyUniform;
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Unexpected uniform name.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OpenGlFrameEditor(
|
||||||
|
eglDisplay, eglContext, eglSurface, textureId, checkNotNull(textureTransformUniform));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Predefined shader values.
|
||||||
|
private static final String VERTEX_SHADER_FILE_PATH = "shaders/blit_vertex_shader.glsl";
|
||||||
|
private static final String FRAGMENT_SHADER_FILE_PATH =
|
||||||
|
"shaders/copy_external_fragment_shader.glsl";
|
||||||
|
private static final int EXPECTED_NUMBER_OF_ATTRIBUTES = 2;
|
||||||
|
private static final int EXPECTED_NUMBER_OF_UNIFORMS = 2;
|
||||||
|
|
||||||
|
private final float[] textureTransformMatrix;
|
||||||
|
private final EGLDisplay eglDisplay;
|
||||||
|
private final EGLContext eglContext;
|
||||||
|
private final EGLSurface eglSurface;
|
||||||
|
private final int textureId;
|
||||||
|
private final SurfaceTexture inputSurfaceTexture;
|
||||||
|
private final Surface inputSurface;
|
||||||
|
private final GlUtil.Uniform textureTransformUniform;
|
||||||
|
|
||||||
|
private volatile boolean hasInputData;
|
||||||
|
|
||||||
|
private OpenGlFrameEditor(
|
||||||
|
EGLDisplay eglDisplay,
|
||||||
|
EGLContext eglContext,
|
||||||
|
EGLSurface eglSurface,
|
||||||
|
int textureId,
|
||||||
|
GlUtil.Uniform textureTransformUniform) {
|
||||||
|
this.eglDisplay = eglDisplay;
|
||||||
|
this.eglContext = eglContext;
|
||||||
|
this.eglSurface = eglSurface;
|
||||||
|
this.textureId = textureId;
|
||||||
|
this.textureTransformUniform = textureTransformUniform;
|
||||||
|
textureTransformMatrix = new float[16];
|
||||||
|
inputSurfaceTexture = new SurfaceTexture(textureId);
|
||||||
|
inputSurfaceTexture.setOnFrameAvailableListener(surfaceTexture -> hasInputData = true);
|
||||||
|
inputSurface = new Surface(inputSurfaceTexture);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Releases all resources. */
|
||||||
|
public void release() {
|
||||||
|
GlUtil.destroyEglContext(eglDisplay, eglContext);
|
||||||
|
GlUtil.deleteTexture(textureId);
|
||||||
|
inputSurfaceTexture.release();
|
||||||
|
inputSurface.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Informs the editor that there is new input data available for it to process asynchronously. */
|
||||||
|
public void processData() {
|
||||||
|
inputSurfaceTexture.updateTexImage();
|
||||||
|
inputSurfaceTexture.getTransformMatrix(textureTransformMatrix);
|
||||||
|
textureTransformUniform.setFloats(textureTransformMatrix);
|
||||||
|
textureTransformUniform.bind();
|
||||||
|
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
|
||||||
|
long surfaceTextureTimestampNs = inputSurfaceTexture.getTimestamp();
|
||||||
|
EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, surfaceTextureTimestampNs);
|
||||||
|
EGL14.eglSwapBuffers(eglDisplay, eglSurface);
|
||||||
|
hasInputData = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the input {@link Surface} after configuring the editor if it has not previously been
|
||||||
|
* configured.
|
||||||
|
*/
|
||||||
|
public Surface getInputSurface() {
|
||||||
|
return inputSurface;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasInputData() {
|
||||||
|
return hasInputData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,19 +17,9 @@
|
||||||
package com.google.android.exoplayer2.transformer;
|
package com.google.android.exoplayer2.transformer;
|
||||||
|
|
||||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||||
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
|
||||||
import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull;
|
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.SurfaceTexture;
|
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.opengl.EGL14;
|
|
||||||
import android.opengl.EGLContext;
|
|
||||||
import android.opengl.EGLDisplay;
|
|
||||||
import android.opengl.EGLExt;
|
|
||||||
import android.opengl.EGLSurface;
|
|
||||||
import android.opengl.GLES20;
|
|
||||||
import android.view.Surface;
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
import com.google.android.exoplayer2.C;
|
import com.google.android.exoplayer2.C;
|
||||||
|
|
@ -37,13 +27,8 @@ import com.google.android.exoplayer2.ExoPlaybackException;
|
||||||
import com.google.android.exoplayer2.Format;
|
import com.google.android.exoplayer2.Format;
|
||||||
import com.google.android.exoplayer2.PlaybackException;
|
import com.google.android.exoplayer2.PlaybackException;
|
||||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||||
import com.google.android.exoplayer2.util.GlUtil;
|
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
|
||||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
|
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|
||||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pipeline to decode video samples, apply transformations on the raw samples, and re-encode them.
|
* Pipeline to decode video samples, apply transformations on the raw samples, and re-encode them.
|
||||||
|
|
@ -51,52 +36,24 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
@RequiresApi(18)
|
@RequiresApi(18)
|
||||||
/* package */ final class VideoSamplePipeline implements SamplePipeline {
|
/* package */ final class VideoSamplePipeline implements SamplePipeline {
|
||||||
|
|
||||||
static {
|
|
||||||
GlUtil.glAssertionsEnabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final String TAG = "VideoSamplePipeline";
|
private static final String TAG = "VideoSamplePipeline";
|
||||||
|
|
||||||
// Predefined shader values.
|
|
||||||
private static final String VERTEX_SHADER_FILE_PATH = "shaders/blit_vertex_shader.glsl";
|
|
||||||
private static final String FRAGMENT_SHADER_FILE_PATH =
|
|
||||||
"shaders/copy_external_fragment_shader.glsl";
|
|
||||||
private static final int EXPECTED_NUMBER_OF_ATTRIBUTES = 2;
|
|
||||||
private static final int EXPECTED_NUMBER_OF_UNIFORMS = 2;
|
|
||||||
|
|
||||||
private final Context context;
|
|
||||||
private final int rendererIndex;
|
|
||||||
|
|
||||||
private final MediaCodecAdapterWrapper encoder;
|
private final MediaCodecAdapterWrapper encoder;
|
||||||
private final DecoderInputBuffer encoderOutputBuffer;
|
private final DecoderInputBuffer encoderOutputBuffer;
|
||||||
|
|
||||||
private final DecoderInputBuffer decoderInputBuffer;
|
private final DecoderInputBuffer decoderInputBuffer;
|
||||||
private final float[] decoderTextureTransformMatrix;
|
private final MediaCodecAdapterWrapper decoder;
|
||||||
private final Format decoderInputFormat;
|
|
||||||
|
|
||||||
private @MonotonicNonNull EGLDisplay eglDisplay;
|
private final OpenGlFrameEditor openGlFrameEditor;
|
||||||
private @MonotonicNonNull EGLContext eglContext;
|
|
||||||
private @MonotonicNonNull EGLSurface eglSurface;
|
|
||||||
|
|
||||||
private int decoderTextureId;
|
|
||||||
private @MonotonicNonNull SurfaceTexture decoderSurfaceTexture;
|
|
||||||
private @MonotonicNonNull Surface decoderSurface;
|
|
||||||
private @MonotonicNonNull MediaCodecAdapterWrapper decoder;
|
|
||||||
private volatile boolean isDecoderSurfacePopulated;
|
|
||||||
private boolean waitingForPopulatedDecoderSurface;
|
private boolean waitingForPopulatedDecoderSurface;
|
||||||
private GlUtil.@MonotonicNonNull Uniform decoderTextureTransformUniform;
|
|
||||||
|
|
||||||
public VideoSamplePipeline(
|
public VideoSamplePipeline(
|
||||||
Context context, Format decoderInputFormat, Transformation transformation, int rendererIndex)
|
Context context, Format decoderInputFormat, Transformation transformation, int rendererIndex)
|
||||||
throws ExoPlaybackException {
|
throws ExoPlaybackException {
|
||||||
this.decoderInputFormat = decoderInputFormat;
|
|
||||||
this.rendererIndex = rendererIndex;
|
|
||||||
this.context = context;
|
|
||||||
|
|
||||||
decoderInputBuffer =
|
decoderInputBuffer =
|
||||||
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
|
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
|
||||||
decoderTextureTransformMatrix = new float[16];
|
|
||||||
decoderTextureId = GlUtil.TEXTURE_ID_UNSET;
|
|
||||||
|
|
||||||
encoderOutputBuffer =
|
encoderOutputBuffer =
|
||||||
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
|
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_DISABLED);
|
||||||
|
|
@ -114,34 +71,57 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
ImmutableMap.of());
|
ImmutableMap.of());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// TODO (internal b/184262323): Assign an adequate error code.
|
// TODO (internal b/184262323): Assign an adequate error code.
|
||||||
throw ExoPlaybackException.createForRenderer(
|
throw createRendererException(
|
||||||
e,
|
e, rendererIndex, decoderInputFormat, PlaybackException.ERROR_CODE_UNSPECIFIED);
|
||||||
TAG,
|
}
|
||||||
rendererIndex,
|
openGlFrameEditor =
|
||||||
|
OpenGlFrameEditor.create(
|
||||||
|
context,
|
||||||
decoderInputFormat,
|
decoderInputFormat,
|
||||||
/* rendererFormatSupport= */ C.FORMAT_HANDLED,
|
/* outputSurface= */ checkNotNull(encoder.getInputSurface()));
|
||||||
/* isRecoverable= */ false,
|
try {
|
||||||
PlaybackException.ERROR_CODE_UNSPECIFIED);
|
decoder =
|
||||||
|
MediaCodecAdapterWrapper.createForVideoDecoding(
|
||||||
|
decoderInputFormat, openGlFrameEditor.getInputSurface());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw createRendererException(
|
||||||
|
e, rendererIndex, decoderInputFormat, PlaybackException.ERROR_CODE_DECODER_INIT_FAILED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean processData() throws ExoPlaybackException {
|
public boolean processData() {
|
||||||
ensureOpenGlConfigured();
|
if (decoder.isEnded()) {
|
||||||
return !ensureDecoderConfigured() || feedEncoderFromDecoder();
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!openGlFrameEditor.hasInputData()) {
|
||||||
|
if (!waitingForPopulatedDecoderSurface) {
|
||||||
|
if (decoder.getOutputBufferInfo() != null) {
|
||||||
|
decoder.releaseOutputBuffer(/* render= */ true);
|
||||||
|
waitingForPopulatedDecoderSurface = true;
|
||||||
|
}
|
||||||
|
if (decoder.isEnded()) {
|
||||||
|
encoder.signalEndOfInputStream();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
waitingForPopulatedDecoderSurface = false;
|
||||||
|
openGlFrameEditor.processData();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public DecoderInputBuffer dequeueInputBuffer() {
|
public DecoderInputBuffer dequeueInputBuffer() {
|
||||||
return decoder != null && decoder.maybeDequeueInputBuffer(decoderInputBuffer)
|
return decoder.maybeDequeueInputBuffer(decoderInputBuffer) ? decoderInputBuffer : null;
|
||||||
? decoderInputBuffer
|
|
||||||
: null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void queueInputBuffer() {
|
public void queueInputBuffer() {
|
||||||
checkStateNotNull(decoder).queueInputBuffer(decoderInputBuffer);
|
decoder.queueInputBuffer(decoderInputBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -175,154 +155,13 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void release() {
|
public void release() {
|
||||||
GlUtil.destroyEglContext(eglDisplay, eglContext);
|
openGlFrameEditor.release();
|
||||||
if (decoderTextureId != GlUtil.TEXTURE_ID_UNSET) {
|
|
||||||
GlUtil.deleteTexture(decoderTextureId);
|
|
||||||
}
|
|
||||||
if (decoderSurfaceTexture != null) {
|
|
||||||
decoderSurfaceTexture.release();
|
|
||||||
}
|
|
||||||
if (decoderSurface != null) {
|
|
||||||
decoderSurface.release();
|
|
||||||
}
|
|
||||||
if (decoder != null) {
|
|
||||||
decoder.release();
|
decoder.release();
|
||||||
}
|
|
||||||
encoder.release();
|
encoder.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
@EnsuresNonNull({"eglDisplay", "eglContext", "eglSurface", "decoderTextureTransformUniform"})
|
private static ExoPlaybackException createRendererException(
|
||||||
private void ensureOpenGlConfigured() {
|
Throwable cause, int rendererIndex, Format decoderInputFormat, int errorCode) {
|
||||||
if (eglDisplay != null
|
|
||||||
&& eglContext != null
|
|
||||||
&& eglSurface != null
|
|
||||||
&& decoderTextureTransformUniform != null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
eglDisplay = GlUtil.createEglDisplay();
|
|
||||||
try {
|
|
||||||
eglContext = GlUtil.createEglContext(eglDisplay);
|
|
||||||
} catch (GlUtil.UnsupportedEglVersionException e) {
|
|
||||||
throw new IllegalStateException("EGL version is unsupported", e);
|
|
||||||
}
|
|
||||||
eglSurface = GlUtil.getEglSurface(eglDisplay, checkNotNull(encoder.getInputSurface()));
|
|
||||||
GlUtil.focusSurface(
|
|
||||||
eglDisplay, eglContext, eglSurface, decoderInputFormat.width, decoderInputFormat.height);
|
|
||||||
decoderTextureId = GlUtil.createExternalTexture();
|
|
||||||
GlUtil.Program copyProgram;
|
|
||||||
try {
|
|
||||||
copyProgram = new GlUtil.Program(context, VERTEX_SHADER_FILE_PATH, FRAGMENT_SHADER_FILE_PATH);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new IllegalStateException(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
copyProgram.use();
|
|
||||||
GlUtil.Attribute[] copyAttributes = copyProgram.getAttributes();
|
|
||||||
checkState(
|
|
||||||
copyAttributes.length == EXPECTED_NUMBER_OF_ATTRIBUTES,
|
|
||||||
"Expected program to have " + EXPECTED_NUMBER_OF_ATTRIBUTES + " vertex attributes.");
|
|
||||||
for (GlUtil.Attribute copyAttribute : copyAttributes) {
|
|
||||||
if (copyAttribute.name.equals("a_position")) {
|
|
||||||
copyAttribute.setBuffer(
|
|
||||||
new float[] {
|
|
||||||
-1.0f, -1.0f, 0.0f, 1.0f,
|
|
||||||
1.0f, -1.0f, 0.0f, 1.0f,
|
|
||||||
-1.0f, 1.0f, 0.0f, 1.0f,
|
|
||||||
1.0f, 1.0f, 0.0f, 1.0f,
|
|
||||||
},
|
|
||||||
/* size= */ 4);
|
|
||||||
} else if (copyAttribute.name.equals("a_texcoord")) {
|
|
||||||
copyAttribute.setBuffer(
|
|
||||||
new float[] {
|
|
||||||
0.0f, 0.0f, 0.0f, 1.0f,
|
|
||||||
1.0f, 0.0f, 0.0f, 1.0f,
|
|
||||||
0.0f, 1.0f, 0.0f, 1.0f,
|
|
||||||
1.0f, 1.0f, 0.0f, 1.0f,
|
|
||||||
},
|
|
||||||
/* size= */ 4);
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException("Unexpected attribute name.");
|
|
||||||
}
|
|
||||||
copyAttribute.bind();
|
|
||||||
}
|
|
||||||
GlUtil.Uniform[] copyUniforms = copyProgram.getUniforms();
|
|
||||||
checkState(
|
|
||||||
copyUniforms.length == EXPECTED_NUMBER_OF_UNIFORMS,
|
|
||||||
"Expected program to have " + EXPECTED_NUMBER_OF_UNIFORMS + " uniforms.");
|
|
||||||
for (GlUtil.Uniform copyUniform : copyUniforms) {
|
|
||||||
if (copyUniform.name.equals("tex_sampler")) {
|
|
||||||
copyUniform.setSamplerTexId(decoderTextureId, 0);
|
|
||||||
copyUniform.bind();
|
|
||||||
} else if (copyUniform.name.equals("tex_transform")) {
|
|
||||||
decoderTextureTransformUniform = copyUniform;
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException("Unexpected uniform name.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checkNotNull(decoderTextureTransformUniform);
|
|
||||||
}
|
|
||||||
|
|
||||||
@EnsuresNonNullIf(
|
|
||||||
expression = {"decoder", "decoderSurfaceTexture"},
|
|
||||||
result = true)
|
|
||||||
private boolean ensureDecoderConfigured() throws ExoPlaybackException {
|
|
||||||
if (decoder != null && decoderSurfaceTexture != null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
checkState(decoderTextureId != GlUtil.TEXTURE_ID_UNSET);
|
|
||||||
decoderSurfaceTexture = new SurfaceTexture(decoderTextureId);
|
|
||||||
decoderSurfaceTexture.setOnFrameAvailableListener(
|
|
||||||
surfaceTexture -> isDecoderSurfacePopulated = true);
|
|
||||||
decoderSurface = new Surface(decoderSurfaceTexture);
|
|
||||||
try {
|
|
||||||
decoder = MediaCodecAdapterWrapper.createForVideoDecoding(decoderInputFormat, decoderSurface);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw createRendererException(e, PlaybackException.ERROR_CODE_DECODER_INIT_FAILED);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresNonNull({
|
|
||||||
"decoder",
|
|
||||||
"decoderSurfaceTexture",
|
|
||||||
"decoderTextureTransformUniform",
|
|
||||||
"eglDisplay",
|
|
||||||
"eglSurface"
|
|
||||||
})
|
|
||||||
private boolean feedEncoderFromDecoder() {
|
|
||||||
if (decoder.isEnded()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isDecoderSurfacePopulated) {
|
|
||||||
if (!waitingForPopulatedDecoderSurface) {
|
|
||||||
if (decoder.getOutputBufferInfo() != null) {
|
|
||||||
decoder.releaseOutputBuffer(/* render= */ true);
|
|
||||||
waitingForPopulatedDecoderSurface = true;
|
|
||||||
}
|
|
||||||
if (decoder.isEnded()) {
|
|
||||||
encoder.signalEndOfInputStream();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
waitingForPopulatedDecoderSurface = false;
|
|
||||||
decoderSurfaceTexture.updateTexImage();
|
|
||||||
decoderSurfaceTexture.getTransformMatrix(decoderTextureTransformMatrix);
|
|
||||||
decoderTextureTransformUniform.setFloats(decoderTextureTransformMatrix);
|
|
||||||
decoderTextureTransformUniform.bind();
|
|
||||||
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
|
|
||||||
long decoderSurfaceTextureTimestampNs = decoderSurfaceTexture.getTimestamp();
|
|
||||||
EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, decoderSurfaceTextureTimestampNs);
|
|
||||||
EGL14.eglSwapBuffers(eglDisplay, eglSurface);
|
|
||||||
isDecoderSurfacePopulated = false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ExoPlaybackException createRendererException(Throwable cause, int errorCode) {
|
|
||||||
return ExoPlaybackException.createForRenderer(
|
return ExoPlaybackException.createForRenderer(
|
||||||
cause,
|
cause,
|
||||||
TAG,
|
TAG,
|
||||||
|
|
|
||||||
|
|
@ -1222,7 +1222,6 @@ public class PlayerControlView extends FrameLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
private void dispatchPlay(Player player) {
|
private void dispatchPlay(Player player) {
|
||||||
@State int state = player.getPlaybackState();
|
@State int state = player.getPlaybackState();
|
||||||
if (state == Player.STATE_IDLE) {
|
if (state == Player.STATE_IDLE) {
|
||||||
|
|
|
||||||
|
|
@ -2165,7 +2165,9 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||||
TrackSelectionParameters trackSelectionParameters =
|
TrackSelectionParameters trackSelectionParameters =
|
||||||
player.getTrackSelectionParameters();
|
player.getTrackSelectionParameters();
|
||||||
TrackSelectionOverrides overrides =
|
TrackSelectionOverrides overrides =
|
||||||
new TrackSelectionOverrides.Builder()
|
trackSelectionParameters
|
||||||
|
.trackSelectionOverrides
|
||||||
|
.buildUpon()
|
||||||
.setOverrideForType(
|
.setOverrideForType(
|
||||||
new TrackSelectionOverride(
|
new TrackSelectionOverride(
|
||||||
track.trackGroup, ImmutableList.of(track.trackIndex)))
|
track.trackGroup, ImmutableList.of(track.trackIndex)))
|
||||||
|
|
|
||||||
31
testdata/src/test/assets/media/mpd/sample_mpd_essential_supplemental_properties
vendored
Normal file
31
testdata/src/test/assets/media/mpd/sample_mpd_essential_supplemental_properties
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<MPD availabilityStartTime="2014-06-19T23:07:42" type="dynamic">
|
||||||
|
<Period start="PT7462826.784S" id="0">
|
||||||
|
<AdaptationSet id="0" mimeType="video/mp4">
|
||||||
|
<SupplementalProperty
|
||||||
|
schemeIdUri="urn:mpeg:dash:supplemental-scheme:2050"
|
||||||
|
value="adaptationSupplemental"/>
|
||||||
|
<EssentialProperty
|
||||||
|
schemeIdUri="urn:mpeg:dash:essential-scheme:2050"
|
||||||
|
value="adaptationEssential"/>
|
||||||
|
<Representation id="singleSegment">
|
||||||
|
<SegmentBase/>
|
||||||
|
<SupplementalProperty
|
||||||
|
schemeIdUri="urn:mpeg:dash:supplemental-scheme:2050"
|
||||||
|
value="representationSupplemental"/>
|
||||||
|
<EssentialProperty
|
||||||
|
schemeIdUri="urn:mpeg:dash:essential-scheme:2050"
|
||||||
|
value="representationEssential"/>
|
||||||
|
</Representation>
|
||||||
|
<Representation id="multiSegment">
|
||||||
|
<SegmentList/>
|
||||||
|
<SupplementalProperty
|
||||||
|
schemeIdUri="urn:mpeg:dash:supplemental-scheme:2050"
|
||||||
|
value="representationSupplemental"/>
|
||||||
|
<EssentialProperty
|
||||||
|
schemeIdUri="urn:mpeg:dash:essential-scheme:2050"
|
||||||
|
value="representationEssential"/>
|
||||||
|
</Representation>
|
||||||
|
</AdaptationSet>
|
||||||
|
</Period>
|
||||||
|
</MPD>
|
||||||
Loading…
Reference in a new issue