Commit 0ba317b1 by Marc Baechinger Committed by GitHub

Merge pull request #8816 from google/dev-v2-r2.13.3

r2.13.3
parents 4364b915 b34e0b26
Showing with 2217 additions and 502 deletions
...@@ -22,28 +22,16 @@ and extend, and can be updated through Play Store application updates. ...@@ -22,28 +22,16 @@ and extend, and can be updated through Play Store application updates.
## Using ExoPlayer ## ## Using ExoPlayer ##
ExoPlayer modules can be obtained from JCenter. It's also possible to clone the ExoPlayer modules can be obtained from [the Google Maven repository][]. It's
repository and depend on the modules locally. also possible to clone the repository and depend on the modules locally.
### From JCenter ### ### From the Google Maven repository
#### 1. Add repositories #### #### 1. Add ExoPlayer module dependencies ####
The easiest way to get started using ExoPlayer is to add it as a gradle 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 dependency in the `build.gradle` file of your app module. The following will add
included in the `build.gradle` file in the root of your project: a dependency to the full library:
```gradle
repositories {
google()
jcenter()
}
```
#### 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:
```gradle ```gradle
implementation 'com.google.android.exoplayer:exoplayer:2.X.X' implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
...@@ -51,6 +39,9 @@ implementation 'com.google.android.exoplayer:exoplayer:2.X.X' ...@@ -51,6 +39,9 @@ implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
where `2.X.X` is your preferred version. where `2.X.X` is your preferred version.
Note: old versions of ExoPlayer are available via JCenter. To use them, you need
to add `jcenter()` to your project's root build.gradle `repositories` block.
As an alternative to the full library, you can depend on only the library 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 modules that you actually need. For example the following will add dependencies
on the Core, DASH and UI library modules, as might be required for an app that on the Core, DASH and UI library modules, as might be required for an app that
...@@ -72,18 +63,19 @@ individually. ...@@ -72,18 +63,19 @@ individually.
* `exoplayer-smoothstreaming`: Support for SmoothStreaming content. * `exoplayer-smoothstreaming`: Support for SmoothStreaming content.
* `exoplayer-ui`: UI components and resources for use with ExoPlayer. * `exoplayer-ui`: UI components and resources for use with ExoPlayer.
In addition to library modules, ExoPlayer has multiple extension modules that In addition to library modules, ExoPlayer has extension modules that depend on
depend on external libraries to provide additional functionality. Some external libraries to provide additional functionality. Some extensions are
extensions are available from JCenter, whereas others must be built manually. available from the Maven repository, whereas others must be built manually.
Browse the [extensions directory][] and their individual READMEs for details. Browse the [extensions directory][] and their individual READMEs for details.
More information on the library and extension modules that are available from More information on the library and extension modules that are available can be
JCenter can be found on [Bintray][]. found on the [Google Maven ExoPlayer page][].
[extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/ [extensions directory]: https://github.com/google/ExoPlayer/tree/release-v2/extensions/
[Bintray]: https://bintray.com/google/exoplayer [the Google Maven repository]: https://developer.android.com/studio/build/dependencies#google-maven
[Google Maven ExoPlayer page]: https://maven.google.com/web/index.html#com.google.android.exoplayer
#### 3. Turn on Java 8 support #### #### 2. Turn on Java 8 support ####
If not enabled already, you also need to turn on Java 8 support in all 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 `build.gradle` files depending on ExoPlayer, by adding the following to the
......
# Release notes # Release notes
### 2.13.3 (2021-04-14)
* Published via the Google Maven repository (i.e., google()) rather than JCenter.
* Core:
* Reset playback speed when live playback speed control becomes unused
([#8664](https://github.com/google/ExoPlayer/issues/8664)).
* Fix playback position issue when re-preparing playback after a
BehindLiveWindowException
([#8675](https://github.com/google/ExoPlayer/issues/8675)).
* Assume Dolby Vision content is encoded as H264 when calculating maximum
codec input size
([#8705](https://github.com/google/ExoPlayer/issues/8705)).
* UI:
* Fix `StyledPlayerView` scrubber not reappearing correctly in some cases
([#8646](https://github.com/google/ExoPlayer/issues/8646)).
* Fix measurement of `StyledPlayerView` and `StyledPlayerControlView` when
`wrap_content` is used
([#8726](https://github.com/google/ExoPlayer/issues/8726)).
* Fix `StyledPlayerControlView` to stay in full mode (rather than minimal
mode) when possible
([#8763](https://github.com/google/ExoPlayer/issues/8763)).
* DASH:
* Parse `forced_subtitle` role from DASH manifests
([#8781](https://github.com/google/ExoPlayer/issues/8781)).
* HLS:
* Fix bug of ignoring `EXT-X-START` when setting the live target offset
([#8764](https://github.com/google/ExoPlayer/pull/8764)).
* Fix incorrect application of byte ranges to `EXT-X-MAP` tags
([#8783](https://github.com/google/ExoPlayer/issues/8783)).
* Fix issue that could cause playback to become stuck if corresponding
`EXT-X-DISCONTINUITY` tags in different media playlists occur at
different positions in time
([#8372](https://github.com/google/ExoPlayer/issues/8372)).
* Fix issue that could cause playback of on-demand content to not start in
cases where the media playlists referenced by the master playlist have
different starting `EXT-X-PROGRAM-DATE-TIME` tags.
* Fix container type detection for segments with incorrect file extension
or HTTP Content-Type
([#8733](https://github.com/google/ExoPlayer/issues/8733)).
* Extractors:
* Add support for `GContainer` and `GContainerItem` XMP namespace prefixes
in JPEG motion photo parsing.
* Allow JFIF APP0 marker segment preceding Exif APP1 segment in
`JpegExtractor`.
* Text:
* Parse SSA/ASS bold & italic info in `Style:` lines
([#8435](https://github.com/google/ExoPlayer/issues/8435)).
* Don't display subtitles after the end position of the current media
period (if known). This ensures sideloaded subtitles respect the end
point of `ClippingMediaPeriod` and prevents content subtitles from
continuing to be displayed over mid-roll ads
([#5317](https://github.com/google/ExoPlayer/issues/5317),
[#8456](https://github.com/google/ExoPlayer/issues/8456)).
* Fix CEA-708 priority handling to sort cues in the order defined by the
spec ([#8704](https://github.com/google/ExoPlayer/issues/8704)).
* Support TTML `textEmphasis` attributes, used for Japanese boutens.
* Support TTML `shear` attributes.
* Metadata:
* Ensure that timed metadata near the end of a period is not dropped
([#8710](https://github.com/google/ExoPlayer/issues/8710)).
* Cast extension:
* Fix `onPositionDiscontinuity` event so that it is not triggered with
reason `DISCONTINUITY_REASON_PERIOD_TRANSITION` after a seek to another
media item and so that it is not triggered after a timeline change.
* IMA extension:
* Fix error caused by `AdPlaybackState` ad group times being cleared,
which can occur if the `ImaAdsLoader` is released while an ad is pending
loading ([#8693](https://github.com/google/ExoPlayer/issues/8693)).
* Upgrade IMA SDK dependency to 3.23.0, fixing an issue with
`NullPointerExceptions` within `WebView` callbacks
([#8447](https://github.com/google/ExoPlayer/issues/8447)).
* FFmpeg extension: Fix playback failure when switching to TrueHD tracks
during playback ([#8616](https://github.com/google/ExoPlayer/issues/8616)).
### 2.13.2 (2021-02-25) ### 2.13.2 (2021-02-25)
* Extractors: * Extractors:
......
...@@ -18,7 +18,6 @@ buildscript { ...@@ -18,7 +18,6 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.0.1' classpath 'com.android.tools.build:gradle:4.0.1'
classpath 'com.novoda:bintray-release:0.9.1'
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.1' classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.1'
} }
} }
...@@ -27,9 +26,6 @@ allprojects { ...@@ -27,9 +26,6 @@ allprojects {
google() google()
jcenter() jcenter()
} }
project.ext {
exoplayerPublishEnabled = false
}
if (it.hasProperty('externalBuildDir')) { if (it.hasProperty('externalBuildDir')) {
if (!new File(externalBuildDir).isAbsolute()) { if (!new File(externalBuildDir).isAbsolute()) {
externalBuildDir = new File(rootDir, externalBuildDir) externalBuildDir = new File(rootDir, externalBuildDir)
......
...@@ -13,8 +13,8 @@ ...@@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.13.2' releaseVersion = '2.13.3'
releaseVersionCode = 2013002 releaseVersionCode = 2013003
minSdkVersion = 16 minSdkVersion = 16
appTargetSdkVersion = 29 appTargetSdkVersion = 29
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest. targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest.
......
...@@ -38,6 +38,7 @@ android { ...@@ -38,6 +38,7 @@ android {
"proguard-rules.txt", "proguard-rules.txt",
getDefaultProguardFile('proguard-android.txt') getDefaultProguardFile('proguard-android.txt')
] ]
signingConfig signingConfigs.debug
} }
debug { debug {
jniDebuggable = true jniDebuggable = true
......
...@@ -34,6 +34,7 @@ android { ...@@ -34,6 +34,7 @@ android {
shrinkResources true shrinkResources true
minifyEnabled true minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt') proguardFiles getDefaultProguardFile('proguard-android.txt')
signingConfig signingConfigs.debug
} }
} }
......
...@@ -38,6 +38,7 @@ android { ...@@ -38,6 +38,7 @@ android {
"proguard-rules.txt", "proguard-rules.txt",
getDefaultProguardFile('proguard-android.txt') getDefaultProguardFile('proguard-android.txt')
] ]
signingConfig signingConfigs.debug
} }
debug { debug {
jniDebuggable = true jniDebuggable = true
......
...@@ -486,6 +486,13 @@ ...@@ -486,6 +486,13 @@
"subtitle_language": "ja" "subtitle_language": "ja"
}, },
{ {
"name": "TTML Netflix Japanese examples (IMSC1.1)",
"uri": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4",
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_japanese_ttml.xml",
"subtitle_mime_type": "application/ttml+xml",
"subtitle_language": "ja"
},
{
"name": "WebVTT positioning", "name": "WebVTT positioning",
"uri": "https://html5demos.com/assets/dizzy.mp4", "uri": "https://html5demos.com/assets/dizzy.mp4",
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/webvtt/numeric-lines.vtt", "subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/webvtt/numeric-lines.vtt",
......
...@@ -23,6 +23,7 @@ import android.net.Uri; ...@@ -23,6 +23,7 @@ import android.net.Uri;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.MediaMetadata;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
...@@ -42,12 +43,12 @@ public class IntentUtil { ...@@ -42,12 +43,12 @@ public class IntentUtil {
"com.google.android.exoplayer.demo.action.VIEW_LIST"; "com.google.android.exoplayer.demo.action.VIEW_LIST";
// Activity extras. // Activity extras.
public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders"; public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders";
// Media item configuration extras. // Media item configuration extras.
public static final String URI_EXTRA = "uri"; public static final String URI_EXTRA = "uri";
public static final String TITLE_EXTRA = "title";
public static final String MIME_TYPE_EXTRA = "mime_type"; public static final String MIME_TYPE_EXTRA = "mime_type";
public static final String CLIP_START_POSITION_MS_EXTRA = "clip_start_position_ms"; public static final String CLIP_START_POSITION_MS_EXTRA = "clip_start_position_ms";
public static final String CLIP_END_POSITION_MS_EXTRA = "clip_end_position_ms"; public static final String CLIP_END_POSITION_MS_EXTRA = "clip_end_position_ms";
...@@ -89,6 +90,9 @@ public class IntentUtil { ...@@ -89,6 +90,9 @@ public class IntentUtil {
MediaItem mediaItem = mediaItems.get(0); MediaItem mediaItem = mediaItems.get(0);
MediaItem.PlaybackProperties playbackProperties = checkNotNull(mediaItem.playbackProperties); MediaItem.PlaybackProperties playbackProperties = checkNotNull(mediaItem.playbackProperties);
intent.setAction(ACTION_VIEW).setData(mediaItem.playbackProperties.uri); intent.setAction(ACTION_VIEW).setData(mediaItem.playbackProperties.uri);
if (mediaItem.mediaMetadata.title != null) {
intent.putExtra(TITLE_EXTRA, mediaItem.mediaMetadata.title);
}
addPlaybackPropertiesToIntent(playbackProperties, intent, /* extrasKeySuffix= */ ""); addPlaybackPropertiesToIntent(playbackProperties, intent, /* extrasKeySuffix= */ "");
addClippingPropertiesToIntent( addClippingPropertiesToIntent(
mediaItem.clippingProperties, intent, /* extrasKeySuffix= */ ""); mediaItem.clippingProperties, intent, /* extrasKeySuffix= */ "");
...@@ -102,6 +106,9 @@ public class IntentUtil { ...@@ -102,6 +106,9 @@ public class IntentUtil {
addPlaybackPropertiesToIntent(playbackProperties, intent, /* extrasKeySuffix= */ "_" + i); addPlaybackPropertiesToIntent(playbackProperties, intent, /* extrasKeySuffix= */ "_" + i);
addClippingPropertiesToIntent( addClippingPropertiesToIntent(
mediaItem.clippingProperties, intent, /* extrasKeySuffix= */ "_" + i); mediaItem.clippingProperties, intent, /* extrasKeySuffix= */ "_" + i);
if (mediaItem.mediaMetadata.title != null) {
intent.putExtra(TITLE_EXTRA + ("_" + i), mediaItem.mediaMetadata.title);
}
} }
} }
} }
...@@ -109,10 +116,12 @@ public class IntentUtil { ...@@ -109,10 +116,12 @@ public class IntentUtil {
private static MediaItem createMediaItemFromIntent( private static MediaItem createMediaItemFromIntent(
Uri uri, Intent intent, String extrasKeySuffix) { Uri uri, Intent intent, String extrasKeySuffix) {
@Nullable String mimeType = intent.getStringExtra(MIME_TYPE_EXTRA + extrasKeySuffix); @Nullable String mimeType = intent.getStringExtra(MIME_TYPE_EXTRA + extrasKeySuffix);
@Nullable String title = intent.getStringExtra(TITLE_EXTRA + extrasKeySuffix);
MediaItem.Builder builder = MediaItem.Builder builder =
new MediaItem.Builder() new MediaItem.Builder()
.setUri(uri) .setUri(uri)
.setMimeType(mimeType) .setMimeType(mimeType)
.setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build())
.setAdTagUri(intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix)) .setAdTagUri(intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix))
.setSubtitles(createSubtitlesFromIntent(intent, extrasKeySuffix)) .setSubtitles(createSubtitlesFromIntent(intent, extrasKeySuffix))
.setClipStartPositionMs( .setClipStartPositionMs(
......
...@@ -440,8 +440,8 @@ public class PlayerActivity extends AppCompatActivity ...@@ -440,8 +440,8 @@ public class PlayerActivity extends AppCompatActivity
@Override @Override
public void onPlayerError(@NonNull ExoPlaybackException e) { public void onPlayerError(@NonNull ExoPlaybackException e) {
if (isBehindLiveWindow(e)) { if (isBehindLiveWindow(e)) {
clearStartPosition(); player.seekToDefaultPosition();
initializePlayer(); player.prepare();
} else { } else {
updateButtonVisibility(); updateButtonVisibility();
showControls(); showControls();
......
...@@ -34,6 +34,7 @@ android { ...@@ -34,6 +34,7 @@ android {
shrinkResources true shrinkResources true
minifyEnabled true minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt') proguardFiles getDefaultProguardFile('proguard-android.txt')
signingConfig signingConfigs.debug
} }
} }
......
...@@ -653,15 +653,7 @@ public final class CastPlayer extends BasePlayer { ...@@ -653,15 +653,7 @@ public final class CastPlayer extends BasePlayer {
updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null); updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null);
updateTimelineAndNotifyIfChanged(); updateTimelineAndNotifyIfChanged();
int currentWindowIndex = C.INDEX_UNSET; int currentWindowIndex = fetchCurrentWindowIndex(remoteMediaClient, currentTimeline);
MediaQueueItem currentItem = remoteMediaClient.getCurrentItem();
if (currentItem != null) {
currentWindowIndex = currentTimeline.getIndexOfPeriod(currentItem.getItemId());
}
if (currentWindowIndex == C.INDEX_UNSET) {
// The timeline is empty. Fall back to index 0, which is what ExoPlayer would do.
currentWindowIndex = 0;
}
if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) { if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
this.currentWindowIndex = currentWindowIndex; this.currentWindowIndex = currentWindowIndex;
listeners.queueEvent( listeners.queueEvent(
...@@ -721,7 +713,9 @@ public final class CastPlayer extends BasePlayer { ...@@ -721,7 +713,9 @@ public final class CastPlayer extends BasePlayer {
} }
/** /**
* Updates the current timeline and returns whether it has changed. * Updates the current timeline. The current window index may change as a result.
*
* @return Whether the current timeline has changed.
*/ */
private boolean updateTimeline() { private boolean updateTimeline() {
CastTimeline oldTimeline = currentTimeline; CastTimeline oldTimeline = currentTimeline;
...@@ -730,7 +724,11 @@ public final class CastPlayer extends BasePlayer { ...@@ -730,7 +724,11 @@ public final class CastPlayer extends BasePlayer {
status != null status != null
? timelineTracker.getCastTimeline(remoteMediaClient) ? timelineTracker.getCastTimeline(remoteMediaClient)
: CastTimeline.EMPTY_CAST_TIMELINE; : CastTimeline.EMPTY_CAST_TIMELINE;
return !oldTimeline.equals(currentTimeline); boolean timelineChanged = !oldTimeline.equals(currentTimeline);
if (timelineChanged) {
currentWindowIndex = fetchCurrentWindowIndex(remoteMediaClient, currentTimeline);
}
return timelineChanged;
} }
/** Updates the internal tracks and selection and returns whether they have changed. */ /** Updates the internal tracks and selection and returns whether they have changed. */
...@@ -940,6 +938,24 @@ public final class CastPlayer extends BasePlayer { ...@@ -940,6 +938,24 @@ public final class CastPlayer extends BasePlayer {
} }
} }
private static int fetchCurrentWindowIndex(
@Nullable RemoteMediaClient remoteMediaClient, Timeline timeline) {
if (remoteMediaClient == null) {
return 0;
}
int currentWindowIndex = C.INDEX_UNSET;
@Nullable MediaQueueItem currentItem = remoteMediaClient.getCurrentItem();
if (currentItem != null) {
currentWindowIndex = timeline.getIndexOfPeriod(currentItem.getItemId());
}
if (currentWindowIndex == C.INDEX_UNSET) {
// The timeline is empty. Fall back to index 0, which is what ExoPlayer would do.
currentWindowIndex = 0;
}
return currentWindowIndex;
}
private static boolean isTrackActive(long id, long[] activeTrackIds) { private static boolean isTrackActive(long id, long[] activeTrackIds) {
for (long activeTrackId : activeTrackIds) { for (long activeTrackId : activeTrackIds) {
if (activeTrackId == id) { if (activeTrackId == id) {
...@@ -1078,6 +1094,7 @@ public final class CastPlayer extends BasePlayer { ...@@ -1078,6 +1094,7 @@ public final class CastPlayer extends BasePlayer {
+ CastUtils.getLogString(statusCode)); + CastUtils.getLogString(statusCode));
} }
if (--pendingSeekCount == 0) { if (--pendingSeekCount == 0) {
currentWindowIndex = pendingSeekWindowIndex;
pendingSeekWindowIndex = C.INDEX_UNSET; pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET; pendingSeekPositionMs = C.TIME_UNSET;
listeners.sendEvent(/* eventFlag= */ C.INDEX_UNSET, EventListener::onSeekProcessed); listeners.sendEvent(/* eventFlag= */ C.INDEX_UNSET, EventListener::onSeekProcessed);
......
...@@ -15,8 +15,8 @@ more external libraries as described below. These are licensed separately. ...@@ -15,8 +15,8 @@ more external libraries as described below. These are licensed separately.
To use this extension you need to clone the ExoPlayer repository and depend on To use this extension you need to clone the ExoPlayer repository and depend on
its modules locally. Instructions for doing this can be found in ExoPlayer's its modules locally. Instructions for doing this can be found in ExoPlayer's
[top level README][]. The extension is not provided via JCenter (see [#2781][] [top level README][]. The extension is not provided via Google's Maven
for more information). repository (see [#2781][] for more information).
In addition, it's necessary to manually build the FFmpeg library, so that gradle In addition, it's necessary to manually build the FFmpeg library, so that gradle
can bundle the FFmpeg binaries in the APK: can bundle the FFmpeg binaries in the APK:
......
...@@ -110,14 +110,18 @@ import java.util.List; ...@@ -110,14 +110,18 @@ import java.util.List;
int inputSize = inputData.limit(); int inputSize = inputData.limit();
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize); ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, outputBufferSize);
int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize); int result = ffmpegDecode(nativeContext, inputData, inputSize, outputData, outputBufferSize);
if (result == AUDIO_DECODER_ERROR_INVALID_DATA) { if (result == AUDIO_DECODER_ERROR_OTHER) {
return new FfmpegDecoderException("Error decoding (see logcat).");
} else if (result == AUDIO_DECODER_ERROR_INVALID_DATA) {
// Treat invalid data errors as non-fatal to match the behavior of MediaCodec. No output will // Treat invalid data errors as non-fatal to match the behavior of MediaCodec. No output will
// be produced for this buffer, so mark it as decode-only to ensure that the audio sink's // be produced for this buffer, so mark it as decode-only to ensure that the audio sink's
// position is reset when more audio is produced. // position is reset when more audio is produced.
outputBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY); outputBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY);
return null; return null;
} else if (result == AUDIO_DECODER_ERROR_OTHER) { } else if (result == 0) {
return new FfmpegDecoderException("Error decoding (see logcat)."); // There's no need to output empty buffers.
outputBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY);
return null;
} }
if (!hasOutputFormat) { if (!hasOutputFormat) {
channelCount = ffmpegGetChannelCount(nativeContext); channelCount = ffmpegGetChannelCount(nativeContext);
......
...@@ -25,7 +25,7 @@ android { ...@@ -25,7 +25,7 @@ android {
} }
dependencies { dependencies {
api 'com.google.ads.interactivemedia.v3:interactivemedia:3.22.0' api 'com.google.ads.interactivemedia.v3:interactivemedia:3.23.0'
implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-core')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0' implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0'
......
...@@ -410,7 +410,10 @@ import java.util.Map; ...@@ -410,7 +410,10 @@ import java.util.Map;
stopUpdatingAdProgress(); stopUpdatingAdProgress();
imaAdInfo = null; imaAdInfo = null;
pendingAdLoadError = null; pendingAdLoadError = null;
adPlaybackState = new AdPlaybackState(adsId); // No more ads will play once the loader is released, so mark all ad groups as skipped.
for (int i = 0; i < adPlaybackState.adGroupCount; i++) {
adPlaybackState = adPlaybackState.withSkippedAdGroup(i);
}
updateAdPlaybackState(); updateAdPlaybackState();
} }
......
...@@ -30,11 +30,11 @@ public final class ExoPlayerLibraryInfo { ...@@ -30,11 +30,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.13.2"; public static final String VERSION = "2.13.3";
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */ /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "ExoPlayerLib/2.13.2"; public static final String VERSION_SLASHY = "ExoPlayerLib/2.13.3";
/** /**
* The version of the library expressed as an integer, for example 1002003. * The version of the library expressed as an integer, for example 1002003.
...@@ -44,7 +44,7 @@ public final class ExoPlayerLibraryInfo { ...@@ -44,7 +44,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 = 2013002; public static final int VERSION_INT = 2013003;
/** /**
* The default user agent for requests made by the library. * The default user agent for requests made by the library.
......
...@@ -1133,6 +1133,7 @@ public interface Player { ...@@ -1133,6 +1133,7 @@ public interface Player {
* Returns the current {@link State playback state} of the player. * Returns the current {@link State playback state} of the player.
* *
* @return The current {@link State playback state}. * @return The current {@link State playback state}.
* @see EventListener#onPlaybackStateChanged(int)
*/ */
@State @State
int getPlaybackState(); int getPlaybackState();
...@@ -1142,6 +1143,7 @@ public interface Player { ...@@ -1142,6 +1143,7 @@ public interface Player {
* true}, or {@link #PLAYBACK_SUPPRESSION_REASON_NONE} if playback is not suppressed. * true}, or {@link #PLAYBACK_SUPPRESSION_REASON_NONE} if playback is not suppressed.
* *
* @return The current {@link PlaybackSuppressionReason playback suppression reason}. * @return The current {@link PlaybackSuppressionReason playback suppression reason}.
* @see EventListener#onPlaybackSuppressionReasonChanged(int)
*/ */
@PlaybackSuppressionReason @PlaybackSuppressionReason
int getPlaybackSuppressionReason(); int getPlaybackSuppressionReason();
...@@ -1158,6 +1160,7 @@ public interface Player { ...@@ -1158,6 +1160,7 @@ public interface Player {
* </ul> * </ul>
* *
* @return Whether the player is playing. * @return Whether the player is playing.
* @see EventListener#onIsPlayingChanged(boolean)
*/ */
boolean isPlaying(); boolean isPlaying();
...@@ -1170,6 +1173,7 @@ public interface Player { ...@@ -1170,6 +1173,7 @@ public interface Player {
* {@link #STATE_IDLE}. * {@link #STATE_IDLE}.
* *
* @return The error, or {@code null}. * @return The error, or {@code null}.
* @see EventListener#onPlayerError(ExoPlaybackException)
*/ */
@Nullable @Nullable
ExoPlaybackException getPlayerError(); ExoPlaybackException getPlayerError();
...@@ -1201,6 +1205,7 @@ public interface Player { ...@@ -1201,6 +1205,7 @@ public interface Player {
* Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}. * Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
* *
* @return Whether playback will proceed when ready. * @return Whether playback will proceed when ready.
* @see EventListener#onPlayWhenReadyChanged(boolean, int)
*/ */
boolean getPlayWhenReady(); boolean getPlayWhenReady();
...@@ -1215,6 +1220,7 @@ public interface Player { ...@@ -1215,6 +1220,7 @@ public interface Player {
* Returns the current {@link RepeatMode} used for playback. * Returns the current {@link RepeatMode} used for playback.
* *
* @return The current repeat mode. * @return The current repeat mode.
* @see EventListener#onRepeatModeChanged(int)
*/ */
@RepeatMode @RepeatMode
int getRepeatMode(); int getRepeatMode();
...@@ -1226,13 +1232,18 @@ public interface Player { ...@@ -1226,13 +1232,18 @@ public interface Player {
*/ */
void setShuffleModeEnabled(boolean shuffleModeEnabled); void setShuffleModeEnabled(boolean shuffleModeEnabled);
/** Returns whether shuffling of windows is enabled. */ /**
* Returns whether shuffling of windows is enabled.
*
* @see EventListener#onShuffleModeEnabledChanged(boolean)
*/
boolean getShuffleModeEnabled(); boolean getShuffleModeEnabled();
/** /**
* Whether the player is currently loading the source. * Whether the player is currently loading the source.
* *
* @return Whether the player is currently loading the source. * @return Whether the player is currently loading the source.
* @see EventListener#onIsLoadingChanged(boolean)
*/ */
boolean isLoading(); boolean isLoading();
...@@ -1375,10 +1386,20 @@ public interface Player { ...@@ -1375,10 +1386,20 @@ public interface Player {
*/ */
int getRendererType(int index); int getRendererType(int index);
/** Returns the available track groups. */ /**
* Returns the available track groups.
*
* @see EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)
*/
TrackGroupArray getCurrentTrackGroups(); TrackGroupArray getCurrentTrackGroups();
/** Returns the current track selections for each renderer. */ /**
* Returns the current track selections for each renderer.
*
* <p>A concrete implementation may include null elements if it has a fixed number of renderer
* components, wishes to report a TrackSelection for each of them, and has one or more renderer
* components that is not assigned any selected tracks.
*/
TrackSelectionArray getCurrentTrackSelections(); TrackSelectionArray getCurrentTrackSelections();
/** /**
...@@ -1391,6 +1412,8 @@ public interface Player { ...@@ -1391,6 +1412,8 @@ public interface Player {
* *
* <p>This metadata is considered static in that it comes from the tracks' declared Formats, * <p>This metadata is considered static in that it comes from the tracks' declared Formats,
* rather than being timed (or dynamic) metadata, which is represented within a metadata track. * rather than being timed (or dynamic) metadata, which is represented within a metadata track.
*
* @see EventListener#onStaticMetadataChanged(List)
*/ */
List<Metadata> getCurrentStaticMetadata(); List<Metadata> getCurrentStaticMetadata();
...@@ -1400,7 +1423,11 @@ public interface Player { ...@@ -1400,7 +1423,11 @@ public interface Player {
@Nullable @Nullable
Object getCurrentManifest(); Object getCurrentManifest();
/** Returns the current {@link Timeline}. Never null, but may be empty. */ /**
* Returns the current {@link Timeline}. Never null, but may be empty.
*
* @see EventListener#onTimelineChanged(Timeline, int)
*/
Timeline getCurrentTimeline(); Timeline getCurrentTimeline();
/** Returns the index of the period currently being played. */ /** Returns the index of the period currently being played. */
...@@ -1446,6 +1473,8 @@ public interface Player { ...@@ -1446,6 +1473,8 @@ public interface Player {
/** /**
* Returns the media item of the current window in the timeline. May be null if the timeline is * Returns the media item of the current window in the timeline. May be null if the timeline is
* empty. * empty.
*
* @see EventListener#onMediaItemTransition(MediaItem, int)
*/ */
@Nullable @Nullable
MediaItem getCurrentMediaItem(); MediaItem getCurrentMediaItem();
......
...@@ -270,6 +270,12 @@ public final class Cue { ...@@ -270,6 +270,12 @@ public final class Cue {
public final @VerticalType int verticalType; public final @VerticalType int verticalType;
/** /**
* The shear angle in degrees to be applied to this Cue, expressed in graphics coordinates. This
* results in a skew transform for the block along the inline progression axis.
*/
public final float shearDegrees;
/**
* Creates a text cue whose {@link #textAlignment} is null, whose type parameters are set to * Creates a text cue whose {@link #textAlignment} is null, whose type parameters are set to
* {@link #TYPE_UNSET} and whose dimension parameters are set to {@link #DIMEN_UNSET}. * {@link #TYPE_UNSET} and whose dimension parameters are set to {@link #DIMEN_UNSET}.
* *
...@@ -370,7 +376,8 @@ public final class Cue { ...@@ -370,7 +376,8 @@ public final class Cue {
/* bitmapHeight= */ DIMEN_UNSET, /* bitmapHeight= */ DIMEN_UNSET,
/* windowColorSet= */ false, /* windowColorSet= */ false,
/* windowColor= */ Color.BLACK, /* windowColor= */ Color.BLACK,
/* verticalType= */ TYPE_UNSET); /* verticalType= */ TYPE_UNSET,
/* shearDegrees= */ 0f);
} }
/** /**
...@@ -415,7 +422,8 @@ public final class Cue { ...@@ -415,7 +422,8 @@ public final class Cue {
/* bitmapHeight= */ DIMEN_UNSET, /* bitmapHeight= */ DIMEN_UNSET,
windowColorSet, windowColorSet,
windowColor, windowColor,
/* verticalType= */ TYPE_UNSET); /* verticalType= */ TYPE_UNSET,
/* shearDegrees= */ 0f);
} }
private Cue( private Cue(
...@@ -433,7 +441,8 @@ public final class Cue { ...@@ -433,7 +441,8 @@ public final class Cue {
float bitmapHeight, float bitmapHeight,
boolean windowColorSet, boolean windowColorSet,
int windowColor, int windowColor,
@VerticalType int verticalType) { @VerticalType int verticalType,
float shearDegrees) {
// Exactly one of text or bitmap should be set. // Exactly one of text or bitmap should be set.
if (text == null) { if (text == null) {
Assertions.checkNotNull(bitmap); Assertions.checkNotNull(bitmap);
...@@ -455,6 +464,7 @@ public final class Cue { ...@@ -455,6 +464,7 @@ public final class Cue {
this.textSizeType = textSizeType; this.textSizeType = textSizeType;
this.textSize = textSize; this.textSize = textSize;
this.verticalType = verticalType; this.verticalType = verticalType;
this.shearDegrees = shearDegrees;
} }
/** Returns a new {@link Cue.Builder} initialized with the same values as this Cue. */ /** Returns a new {@link Cue.Builder} initialized with the same values as this Cue. */
...@@ -479,6 +489,7 @@ public final class Cue { ...@@ -479,6 +489,7 @@ public final class Cue {
private boolean windowColorSet; private boolean windowColorSet;
@ColorInt private int windowColor; @ColorInt private int windowColor;
@VerticalType private int verticalType; @VerticalType private int verticalType;
private float shearDegrees;
public Builder() { public Builder() {
text = null; text = null;
...@@ -514,6 +525,7 @@ public final class Cue { ...@@ -514,6 +525,7 @@ public final class Cue {
windowColorSet = cue.windowColorSet; windowColorSet = cue.windowColorSet;
windowColor = cue.windowColor; windowColor = cue.windowColor;
verticalType = cue.verticalType; verticalType = cue.verticalType;
shearDegrees = cue.shearDegrees;
} }
/** /**
...@@ -794,6 +806,12 @@ public final class Cue { ...@@ -794,6 +806,12 @@ public final class Cue {
return this; return this;
} }
/** Sets the shear angle for this Cue. */
public Builder setShearDegrees(float shearDegrees) {
this.shearDegrees = shearDegrees;
return this;
}
/** /**
* Gets the vertical formatting for this Cue. * Gets the vertical formatting for this Cue.
* *
...@@ -821,7 +839,8 @@ public final class Cue { ...@@ -821,7 +839,8 @@ public final class Cue {
bitmapHeight, bitmapHeight,
windowColorSet, windowColorSet,
windowColor, windowColor,
verticalType); verticalType,
shearDegrees);
} }
} }
} }
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.util; package com.google.android.exoplayer2.util;
import androidx.annotation.GuardedBy;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
/** /**
...@@ -35,34 +36,73 @@ public final class TimestampAdjuster { ...@@ -35,34 +36,73 @@ public final class TimestampAdjuster {
*/ */
private static final long MAX_PTS_PLUS_ONE = 0x200000000L; private static final long MAX_PTS_PLUS_ONE = 0x200000000L;
@GuardedBy("this")
private boolean sharedInitializationStarted;
@GuardedBy("this")
private long firstSampleTimestampUs; private long firstSampleTimestampUs;
@GuardedBy("this")
private long timestampOffsetUs; private long timestampOffsetUs;
// Volatile to allow isInitialized to be called on a different thread to adjustSampleTimestamp. @GuardedBy("this")
private volatile long lastSampleTimestampUs; private long lastSampleTimestampUs;
/** /**
* @param firstSampleTimestampUs See {@link #setFirstSampleTimestampUs(long)}. * @param firstSampleTimestampUs The desired value of the first adjusted sample timestamp in
* microseconds, or {@link #DO_NOT_OFFSET} if timestamps should not be offset.
*/ */
public TimestampAdjuster(long firstSampleTimestampUs) { public TimestampAdjuster(long firstSampleTimestampUs) {
this.firstSampleTimestampUs = firstSampleTimestampUs;
lastSampleTimestampUs = C.TIME_UNSET; lastSampleTimestampUs = C.TIME_UNSET;
setFirstSampleTimestampUs(firstSampleTimestampUs);
} }
/** /**
* Sets the desired result of the first call to {@link #adjustSampleTimestamp(long)}. Can only be * For shared timestamp adjusters, performs necessary initialization actions for a caller.
* called before any timestamps have been adjusted.
* *
* @param firstSampleTimestampUs The first adjusted sample timestamp in microseconds, or * <ul>
* {@link #DO_NOT_OFFSET} if presentation timestamps should not be offset. * <li>If the adjuster does not yet have a target {@link #getFirstSampleTimestampUs first sample
* timestamp} and if {@code canInitialize} is {@code true}, then initialization is started
* by setting the target first sample timestamp to {@code firstSampleTimestampUs}. The call
* returns, allowing the caller to proceed. Initialization completes when a caller adjusts
* the first timestamp.
* <li>If {@code canInitialize} is {@code true} and the adjuster already has a target {@link
* #getFirstSampleTimestampUs first sample timestamp}, then the call returns to allow the
* caller to proceed only if {@code firstSampleTimestampUs} is equal to the target. This
* ensures a caller that's previously started initialization can continue to proceed. It
* also allows other callers with the same {@code firstSampleTimestampUs} to proceed, since
* in this case it doesn't matter which caller adjusts the first timestamp to complete
* initialization.
* <li>If {@code canInitialize} is {@code false} or if {@code firstSampleTimestampUs} differs
* from the target {@link #getFirstSampleTimestampUs first sample timestamp}, then the call
* blocks until initialization completes. If initialization has already been completed the
* call returns immediately.
* </ul>
*
* @param canInitialize Whether the caller is able to initialize the adjuster, if needed.
* @param startTimeUs The desired first sample timestamp of the caller, in microseconds. Only used
* if {@code canInitialize} is {@code true}.
* @throws InterruptedException If the thread is interrupted whilst blocked waiting for
* initialization to complete.
*/ */
public synchronized void setFirstSampleTimestampUs(long firstSampleTimestampUs) { public synchronized void sharedInitializeOrWait(boolean canInitialize, long startTimeUs)
Assertions.checkState(lastSampleTimestampUs == C.TIME_UNSET); throws InterruptedException {
this.firstSampleTimestampUs = firstSampleTimestampUs; if (canInitialize && !sharedInitializationStarted) {
firstSampleTimestampUs = startTimeUs;
sharedInitializationStarted = true;
}
if (!canInitialize || startTimeUs != firstSampleTimestampUs) {
while (lastSampleTimestampUs == C.TIME_UNSET) {
wait();
}
}
} }
/** Returns the last value passed to {@link #setFirstSampleTimestampUs(long)}. */ /**
public long getFirstSampleTimestampUs() { * Returns the value of the first adjusted sample timestamp in microseconds, or {@link
* #DO_NOT_OFFSET} if timestamps will not be offset.
*/
public synchronized long getFirstSampleTimestampUs() {
return firstSampleTimestampUs; return firstSampleTimestampUs;
} }
...@@ -72,22 +112,22 @@ public final class TimestampAdjuster { ...@@ -72,22 +112,22 @@ public final class TimestampAdjuster {
* #getFirstSampleTimestampUs()}. If this value is {@link #DO_NOT_OFFSET}, returns {@link * #getFirstSampleTimestampUs()}. If this value is {@link #DO_NOT_OFFSET}, returns {@link
* C#TIME_UNSET}. * C#TIME_UNSET}.
*/ */
public long getLastAdjustedTimestampUs() { public synchronized long getLastAdjustedTimestampUs() {
return lastSampleTimestampUs != C.TIME_UNSET return lastSampleTimestampUs != C.TIME_UNSET
? (lastSampleTimestampUs + timestampOffsetUs) ? (lastSampleTimestampUs + timestampOffsetUs)
: firstSampleTimestampUs != DO_NOT_OFFSET ? firstSampleTimestampUs : C.TIME_UNSET; : firstSampleTimestampUs != DO_NOT_OFFSET ? firstSampleTimestampUs : C.TIME_UNSET;
} }
/** /**
* Returns the offset between the input of {@link #adjustSampleTimestamp(long)} and its output. * Returns the offset between the input of {@link #adjustSampleTimestamp(long)} and its output. If
* If {@link #DO_NOT_OFFSET} was provided to the constructor, 0 is returned. If the timestamp * {@link #DO_NOT_OFFSET} was provided to the constructor, 0 is returned. If the timestamp
* adjuster is yet not initialized, {@link C#TIME_UNSET} is returned. * adjuster is yet not initialized, {@link C#TIME_UNSET} is returned.
* *
* @return The offset between {@link #adjustSampleTimestamp(long)}'s input and output. * @return The offset between {@link #adjustSampleTimestamp(long)}'s input and output. {@link
* {@link C#TIME_UNSET} if the adjuster is not yet initialized and 0 if timestamps should not * C#TIME_UNSET} if the adjuster is not yet initialized and 0 if timestamps should not be
* be offset. * offset.
*/ */
public long getTimestampOffsetUs() { public synchronized long getTimestampOffsetUs() {
return firstSampleTimestampUs == DO_NOT_OFFSET return firstSampleTimestampUs == DO_NOT_OFFSET
? 0 ? 0
: lastSampleTimestampUs == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs; : lastSampleTimestampUs == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs;
...@@ -95,9 +135,14 @@ public final class TimestampAdjuster { ...@@ -95,9 +135,14 @@ public final class TimestampAdjuster {
/** /**
* Resets the instance to its initial state. * Resets the instance to its initial state.
*
* @param firstSampleTimestampUs The desired value of the first adjusted sample timestamp after
* this reset, in microseconds, or {@link #DO_NOT_OFFSET} if timestamps should not be offset.
*/ */
public void reset() { public synchronized void reset(long firstSampleTimestampUs) {
this.firstSampleTimestampUs = firstSampleTimestampUs;
lastSampleTimestampUs = C.TIME_UNSET; lastSampleTimestampUs = C.TIME_UNSET;
sharedInitializationStarted = false;
} }
/** /**
...@@ -106,7 +151,7 @@ public final class TimestampAdjuster { ...@@ -106,7 +151,7 @@ public final class TimestampAdjuster {
* @param pts90Khz A 90 kHz clock MPEG-2 TS presentation timestamp. * @param pts90Khz A 90 kHz clock MPEG-2 TS presentation timestamp.
* @return The adjusted timestamp in microseconds. * @return The adjusted timestamp in microseconds.
*/ */
public long adjustTsTimestamp(long pts90Khz) { public synchronized long adjustTsTimestamp(long pts90Khz) {
if (pts90Khz == C.TIME_UNSET) { if (pts90Khz == C.TIME_UNSET) {
return C.TIME_UNSET; return C.TIME_UNSET;
} }
...@@ -131,7 +176,7 @@ public final class TimestampAdjuster { ...@@ -131,7 +176,7 @@ public final class TimestampAdjuster {
* @param timeUs The timestamp to adjust in microseconds. * @param timeUs The timestamp to adjust in microseconds.
* @return The adjusted timestamp in microseconds. * @return The adjusted timestamp in microseconds.
*/ */
public long adjustSampleTimestamp(long timeUs) { public synchronized long adjustSampleTimestamp(long timeUs) {
if (timeUs == C.TIME_UNSET) { if (timeUs == C.TIME_UNSET) {
return C.TIME_UNSET; return C.TIME_UNSET;
} }
...@@ -143,27 +188,14 @@ public final class TimestampAdjuster { ...@@ -143,27 +188,14 @@ public final class TimestampAdjuster {
// Calculate the timestamp offset. // Calculate the timestamp offset.
timestampOffsetUs = firstSampleTimestampUs - timeUs; timestampOffsetUs = firstSampleTimestampUs - timeUs;
} }
synchronized (this) { lastSampleTimestampUs = timeUs;
lastSampleTimestampUs = timeUs; // Notify threads waiting for this adjuster to be initialized.
// Notify threads waiting for this adjuster to be initialized. notifyAll();
notifyAll();
}
} }
return timeUs + timestampOffsetUs; return timeUs + timestampOffsetUs;
} }
/** /**
* Blocks the calling thread until this adjuster is initialized.
*
* @throws InterruptedException If the thread was interrupted.
*/
public synchronized void waitUntilInitialized() throws InterruptedException {
while (lastSampleTimestampUs == C.TIME_UNSET) {
wait();
}
}
/**
* Converts a 90 kHz clock timestamp to a timestamp in microseconds. * Converts a 90 kHz clock timestamp to a timestamp in microseconds.
* *
* @param pts A 90 kHz clock timestamp. * @param pts A 90 kHz clock timestamp.
......
/*
* 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;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.google.common.truth.Truth.assertThat;
import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.source.ClippingMediaSource;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.util.ConditionVariable;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumentation tests for playback of clipped items using {@link MediaItem#clippingProperties} or
* {@link ClippingMediaSource} directly.
*/
@RunWith(AndroidJUnit4.class)
public final class ClippedPlaybackTest {
@Test
public void subtitlesRespectClipping_singlePeriod() throws Exception {
MediaItem mediaItem =
new MediaItem.Builder()
.setUri("asset:///media/mp4/sample.mp4")
.setSubtitles(
ImmutableList.of(
new MediaItem.Subtitle(
Uri.parse("asset:///media/webvtt/typical"),
MimeTypes.TEXT_VTT,
"en",
C.SELECTION_FLAG_DEFAULT)))
// Expect the clipping to affect both subtitles and video.
.setClipEndPositionMs(1000)
.build();
AtomicReference<SimpleExoPlayer> player = new AtomicReference<>();
CapturingTextOutput textOutput = new CapturingTextOutput();
ConditionVariable playbackEnded = new ConditionVariable();
getInstrumentation()
.runOnMainSync(
() -> {
player.set(new SimpleExoPlayer.Builder(getInstrumentation().getContext()).build());
player.get().addTextOutput(textOutput);
player
.get()
.addListener(
new Player.EventListener() {
@Override
public void onPlaybackStateChanged(@Player.State int state) {
if (state == Player.STATE_ENDED) {
playbackEnded.open();
}
}
});
player.get().setMediaItem(mediaItem);
player.get().prepare();
player.get().play();
});
playbackEnded.block();
getInstrumentation().runOnMainSync(() -> player.get().release());
getInstrumentation().waitForIdleSync();
assertThat(Iterables.getOnlyElement(Iterables.concat(textOutput.cues)).text.toString())
.isEqualTo("This is the first subtitle.");
}
@Test
public void subtitlesRespectClipping_multiplePeriods() throws Exception {
ImmutableList<MediaItem> mediaItems =
ImmutableList.of(
new MediaItem.Builder()
.setUri("asset:///media/mp4/sample.mp4")
.setSubtitles(
ImmutableList.of(
new MediaItem.Subtitle(
Uri.parse("asset:///media/webvtt/typical"),
MimeTypes.TEXT_VTT,
"en",
C.SELECTION_FLAG_DEFAULT)))
// Expect the clipping to affect both subtitles and video.
.setClipEndPositionMs(1000)
.build(),
new MediaItem.Builder()
.setUri("asset:///media/mp4/sample.mp4")
// Not needed for correctness, just makes test run faster. Must be longer than the
// subtitle content (3.5s).
.setClipEndPositionMs(4_000)
.build());
AtomicReference<SimpleExoPlayer> player = new AtomicReference<>();
CapturingTextOutput textOutput = new CapturingTextOutput();
ConditionVariable playbackEnded = new ConditionVariable();
getInstrumentation()
.runOnMainSync(
() -> {
player.set(new SimpleExoPlayer.Builder(getInstrumentation().getContext()).build());
player.get().addTextOutput(textOutput);
player
.get()
.addListener(
new Player.EventListener() {
@Override
public void onPlaybackStateChanged(@Player.State int state) {
if (state == Player.STATE_ENDED) {
playbackEnded.open();
}
}
});
player.get().setMediaItems(mediaItems);
player.get().prepare();
player.get().play();
});
playbackEnded.block();
getInstrumentation().runOnMainSync(() -> player.get().release());
getInstrumentation().waitForIdleSync();
assertThat(Iterables.getOnlyElement(Iterables.concat(textOutput.cues)).text.toString())
.isEqualTo("This is the first subtitle.");
}
private static class CapturingTextOutput implements TextOutput {
private final List<List<Cue>> cues;
private CapturingTextOutput() {
cues = new ArrayList<>();
}
@Override
public void onCues(List<Cue> cues) {
this.cues.add(cues);
}
}
}
...@@ -682,6 +682,7 @@ public interface ExoPlayer extends Player { ...@@ -682,6 +682,7 @@ public interface ExoPlayer extends Player {
* Returns whether the player has paused its main loop to save power in offload scheduling mode. * Returns whether the player has paused its main loop to save power in offload scheduling mode.
* *
* @see #experimentalSetOffloadSchedulingEnabled(boolean) * @see #experimentalSetOffloadSchedulingEnabled(boolean)
* @see EventListener#onExperimentalSleepingForOffloadChanged(boolean)
*/ */
boolean experimentalIsSleepingForOffload(); boolean experimentalIsSleepingForOffload();
} }
...@@ -40,6 +40,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; ...@@ -40,6 +40,7 @@ import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
import com.google.android.exoplayer2.source.SampleStream; import com.google.android.exoplayer2.source.SampleStream;
import com.google.android.exoplayer2.source.ShuffleOrder; import com.google.android.exoplayer2.source.ShuffleOrder;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.TextRenderer;
import com.google.android.exoplayer2.trackselection.ExoTrackSelection; import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectorResult; import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
...@@ -1829,7 +1830,10 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1829,7 +1830,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
MediaPeriodId oldPeriodId, MediaPeriodId oldPeriodId,
long positionForTargetOffsetOverrideUs) { long positionForTargetOffsetOverrideUs) {
if (newTimeline.isEmpty() || !shouldUseLivePlaybackSpeedControl(newTimeline, newPeriodId)) { if (newTimeline.isEmpty() || !shouldUseLivePlaybackSpeedControl(newTimeline, newPeriodId)) {
// Live playback speed control is unused. // Live playback speed control is unused for the current period, reset speed if adjusted.
if (mediaClock.getPlaybackParameters().speed != playbackInfo.playbackParameters.speed) {
mediaClock.setPlaybackParameters(playbackInfo.playbackParameters);
}
return; return;
} }
int windowIndex = newTimeline.getPeriodByUid(newPeriodId.periodUid, period).windowIndex; int windowIndex = newTimeline.getPeriodByUid(newPeriodId.periodUid, period).windowIndex;
...@@ -1937,7 +1941,12 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1937,7 +1941,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
if (sampleStream != null if (sampleStream != null
&& renderer.getStream() == sampleStream && renderer.getStream() == sampleStream
&& renderer.hasReadStreamToEnd()) { && renderer.hasReadStreamToEnd()) {
renderer.setCurrentStreamFinal(); long streamEndPositionUs =
readingPeriodHolder.info.durationUs != C.TIME_UNSET
&& readingPeriodHolder.info.durationUs != C.TIME_END_OF_SOURCE
? readingPeriodHolder.getRendererOffset() + readingPeriodHolder.info.durationUs
: C.TIME_UNSET;
setCurrentStreamFinal(renderer, streamEndPositionUs);
} }
} }
} }
...@@ -1962,7 +1971,8 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1962,7 +1971,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
&& readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET) { && readingPeriodHolder.mediaPeriod.readDiscontinuity() != C.TIME_UNSET) {
// The new period starts with a discontinuity, so the renderers will play out all data, then // The new period starts with a discontinuity, so the renderers will play out all data, then
// be disabled and re-enabled when they start playing the next period. // be disabled and re-enabled when they start playing the next period.
setAllRendererStreamsFinal(); setAllRendererStreamsFinal(
/* streamEndPositionUs= */ readingPeriodHolder.getStartPositionRendererTime());
return; return;
} }
for (int i = 0; i < renderers.length; i++) { for (int i = 0; i < renderers.length; i++) {
...@@ -1978,7 +1988,9 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -1978,7 +1988,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
// it's a no-sample renderer for which rendererOffsetUs should be updated only when // it's a no-sample renderer for which rendererOffsetUs should be updated only when
// starting to play the next period. Mark the SampleStream as final to play out any // starting to play the next period. Mark the SampleStream as final to play out any
// remaining data. // remaining data.
renderers[i].setCurrentStreamFinal(); setCurrentStreamFinal(
renderers[i],
/* streamEndPositionUs= */ readingPeriodHolder.getStartPositionRendererTime());
} }
} }
} }
...@@ -2103,14 +2115,21 @@ import java.util.concurrent.atomic.AtomicBoolean; ...@@ -2103,14 +2115,21 @@ import java.util.concurrent.atomic.AtomicBoolean;
return true; return true;
} }
private void setAllRendererStreamsFinal() { private void setAllRendererStreamsFinal(long streamEndPositionUs) {
for (Renderer renderer : renderers) { for (Renderer renderer : renderers) {
if (renderer.getStream() != null) { if (renderer.getStream() != null) {
renderer.setCurrentStreamFinal(); setCurrentStreamFinal(renderer, streamEndPositionUs);
} }
} }
} }
private void setCurrentStreamFinal(Renderer renderer, long streamEndPositionUs) {
renderer.setCurrentStreamFinal();
if (renderer instanceof TextRenderer) {
((TextRenderer) renderer).setFinalStreamEndPositionUs(streamEndPositionUs);
}
}
private void handlePeriodPrepared(MediaPeriod mediaPeriod) throws ExoPlaybackException { private void handlePeriodPrepared(MediaPeriod mediaPeriod) throws ExoPlaybackException {
if (!queue.isLoading(mediaPeriod)) { if (!queue.isLoading(mediaPeriod)) {
// Stale event. // Stale event.
......
...@@ -144,7 +144,7 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -144,7 +144,7 @@ public class SimpleExoPlayer extends BasePlayer
* <li>{@link PriorityTaskManager}: {@code null} (not used) * <li>{@link PriorityTaskManager}: {@code null} (not used)
* <li>{@link AudioAttributes}: {@link AudioAttributes#DEFAULT}, not handling audio focus * <li>{@link AudioAttributes}: {@link AudioAttributes#DEFAULT}, not handling audio focus
* <li>{@link C.WakeMode}: {@link C#WAKE_MODE_NONE} * <li>{@link C.WakeMode}: {@link C#WAKE_MODE_NONE}
* <li>{@code handleAudioBecomingNoisy}: {@code true} * <li>{@code handleAudioBecomingNoisy}: {@code false}
* <li>{@code skipSilenceEnabled}: {@code false} * <li>{@code skipSilenceEnabled}: {@code false}
* <li>{@link C.VideoScalingMode}: {@link C#VIDEO_SCALING_MODE_DEFAULT} * <li>{@link C.VideoScalingMode}: {@link C#VIDEO_SCALING_MODE_DEFAULT}
* <li>{@code useLazyPreparation}: {@code true} * <li>{@code useLazyPreparation}: {@code true}
...@@ -1047,8 +1047,6 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1047,8 +1047,6 @@ public class SimpleExoPlayer extends BasePlayer
* href="https://developer.android.com/guide/topics/media-apps/volume-and-earphones#becoming-noisy">audio * href="https://developer.android.com/guide/topics/media-apps/volume-and-earphones#becoming-noisy">audio
* becoming noisy</a> documentation for more information. * becoming noisy</a> documentation for more information.
* *
* <p>This feature is not enabled by default.
*
* @param handleAudioBecomingNoisy Whether the player should pause automatically when audio is * @param handleAudioBecomingNoisy Whether the player should pause automatically when audio is
* rerouted from a headset to device speakers. * rerouted from a headset to device speakers.
*/ */
...@@ -1718,10 +1716,6 @@ public class SimpleExoPlayer extends BasePlayer ...@@ -1718,10 +1716,6 @@ public class SimpleExoPlayer extends BasePlayer
* playback can occur when the screen is off (e.g. background audio playback). It is not useful if * playback can occur when the screen is off (e.g. background audio playback). It is not useful if
* the screen will always be on during playback (e.g. foreground video playback). * the screen will always be on during playback (e.g. foreground video playback).
* *
* <p>This feature is not enabled by default. If enabled, a WakeLock is held whenever the player
* is in the {@link #STATE_READY READY} or {@link #STATE_BUFFERING BUFFERING} states with {@code
* playWhenReady = true}.
*
* @param handleWakeLock Whether the player should use a {@link android.os.PowerManager.WakeLock} * @param handleWakeLock Whether the player should use a {@link android.os.PowerManager.WakeLock}
* to ensure the device stays awake for playback, even when the screen is off. * to ensure the device stays awake for playback, even when the screen is off.
* @deprecated Use {@link #setWakeMode(int)} instead. * @deprecated Use {@link #setWakeMode(int)} instead.
......
...@@ -293,18 +293,16 @@ public final class PlaybackStatsListener ...@@ -293,18 +293,16 @@ public final class PlaybackStatsListener
} }
private void maybeAddSessions(Player player, Events events) { private void maybeAddSessions(Player player, Events events) {
if (player.getCurrentTimeline().isEmpty() && player.getPlaybackState() == Player.STATE_IDLE) { boolean isCompletelyIdle =
// Player is completely idle. Don't add new sessions. player.getCurrentTimeline().isEmpty() && player.getPlaybackState() == Player.STATE_IDLE;
return;
}
for (int i = 0; i < events.size(); i++) { for (int i = 0; i < events.size(); i++) {
@EventFlags int event = events.get(i); @EventFlags int event = events.get(i);
EventTime eventTime = events.getEventTime(event); EventTime eventTime = events.getEventTime(event);
if (event == EVENT_TIMELINE_CHANGED) { if (event == EVENT_TIMELINE_CHANGED) {
sessionManager.updateSessionsWithTimelineChange(eventTime); sessionManager.updateSessionsWithTimelineChange(eventTime);
} else if (event == EVENT_POSITION_DISCONTINUITY) { } else if (!isCompletelyIdle && event == EVENT_POSITION_DISCONTINUITY) {
sessionManager.updateSessionsWithDiscontinuity(eventTime, discontinuityReason); sessionManager.updateSessionsWithDiscontinuity(eventTime, discontinuityReason);
} else { } else if (!isCompletelyIdle) {
sessionManager.updateSessions(eventTime); sessionManager.updateSessions(eventTime);
} }
} }
......
...@@ -155,6 +155,7 @@ public final class AudioCapabilities { ...@@ -155,6 +155,7 @@ public final class AudioCapabilities {
} }
private static boolean deviceMaySetExternalSurroundSoundGlobalSetting() { private static boolean deviceMaySetExternalSurroundSoundGlobalSetting() {
return Util.SDK_INT >= 17 && "Amazon".equals(Util.MANUFACTURER); return Util.SDK_INT >= 17
&& ("Amazon".equals(Util.MANUFACTURER) || "Xiaomi".equals(Util.MANUFACTURER));
} }
} }
...@@ -1482,6 +1482,10 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -1482,6 +1482,10 @@ public final class DefaultAudioSink implements AudioSink {
&& !audioCapabilities.supportsEncoding(C.ENCODING_E_AC3_JOC)) { && !audioCapabilities.supportsEncoding(C.ENCODING_E_AC3_JOC)) {
// E-AC3 receivers support E-AC3 JOC streams (but decode only the base layer). // E-AC3 receivers support E-AC3 JOC streams (but decode only the base layer).
encoding = C.ENCODING_E_AC3; encoding = C.ENCODING_E_AC3;
} else if (encoding == C.ENCODING_DTS_HD
&& !audioCapabilities.supportsEncoding(C.ENCODING_DTS_HD)) {
// DTS receivers support DTS-HD streams (but decode only the core layer).
encoding = C.ENCODING_DTS;
} }
if (!audioCapabilities.supportsEncoding(encoding)) { if (!audioCapabilities.supportsEncoding(encoding)) {
return null; return null;
......
...@@ -58,6 +58,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { ...@@ -58,6 +58,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
private int pendingMetadataCount; private int pendingMetadataCount;
@Nullable private MetadataDecoder decoder; @Nullable private MetadataDecoder decoder;
private boolean inputStreamEnded; private boolean inputStreamEnded;
private boolean outputStreamEnded;
private long subsampleOffsetUs; private long subsampleOffsetUs;
/** /**
...@@ -118,6 +119,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { ...@@ -118,6 +119,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
protected void onPositionReset(long positionUs, boolean joining) { protected void onPositionReset(long positionUs, boolean joining) {
flushPendingMetadata(); flushPendingMetadata();
inputStreamEnded = false; inputStreamEnded = false;
outputStreamEnded = false;
} }
@Override @Override
...@@ -158,6 +160,9 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { ...@@ -158,6 +160,9 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
pendingMetadataIndex = (pendingMetadataIndex + 1) % MAX_PENDING_METADATA_COUNT; pendingMetadataIndex = (pendingMetadataIndex + 1) % MAX_PENDING_METADATA_COUNT;
pendingMetadataCount--; pendingMetadataCount--;
} }
if (inputStreamEnded && pendingMetadataCount == 0) {
outputStreamEnded = true;
}
} }
/** /**
...@@ -198,7 +203,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback { ...@@ -198,7 +203,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
@Override @Override
public boolean isEnded() { public boolean isEnded() {
return inputStreamEnded; return outputStreamEnded;
} }
@Override @Override
......
...@@ -178,13 +178,17 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> { ...@@ -178,13 +178,17 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
// anyway. // anyway.
newTimeline.getWindow(/* windowIndex= */ 0, window); newTimeline.getWindow(/* windowIndex= */ 0, window);
long windowStartPositionUs = window.getDefaultPositionUs(); long windowStartPositionUs = window.getDefaultPositionUs();
Object windowUid = window.uid;
if (unpreparedMaskingMediaPeriod != null) { if (unpreparedMaskingMediaPeriod != null) {
long periodPreparePositionUs = unpreparedMaskingMediaPeriod.getPreparePositionUs(); long periodPreparePositionUs = unpreparedMaskingMediaPeriod.getPreparePositionUs();
if (periodPreparePositionUs != 0) { timeline.getPeriodByUid(unpreparedMaskingMediaPeriod.id.periodUid, period);
windowStartPositionUs = periodPreparePositionUs; long windowPreparePositionUs = period.getPositionInWindowUs() + periodPreparePositionUs;
long oldWindowDefaultPositionUs =
timeline.getWindow(/* windowIndex= */ 0, window).getDefaultPositionUs();
if (windowPreparePositionUs != oldWindowDefaultPositionUs) {
windowStartPositionUs = windowPreparePositionUs;
} }
} }
Object windowUid = window.uid;
Pair<Object, Long> periodPosition = Pair<Object, Long> periodPosition =
newTimeline.getPeriodPosition( newTimeline.getPeriodPosition(
window, period, /* windowIndex= */ 0, windowStartPositionUs); window, period, /* windowIndex= */ 0, windowStartPositionUs);
......
...@@ -56,7 +56,8 @@ public interface AdsLoader { ...@@ -56,7 +56,8 @@ public interface AdsLoader {
interface EventListener { interface EventListener {
/** /**
* Called when the ad playback state has been updated. * Called when the ad playback state has been updated. The number of {@link
* AdPlaybackState#adGroups ad groups} may not change after the first call.
* *
* @param adPlaybackState The new ad playback state. * @param adPlaybackState The new ad playback state.
*/ */
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source.ads; package com.google.android.exoplayer2.source.ads;
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 android.net.Uri; import android.net.Uri;
import android.os.Handler; import android.os.Handler;
...@@ -290,6 +291,8 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> { ...@@ -290,6 +291,8 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
if (this.adPlaybackState == null) { if (this.adPlaybackState == null) {
adMediaSourceHolders = new AdMediaSourceHolder[adPlaybackState.adGroupCount][]; adMediaSourceHolders = new AdMediaSourceHolder[adPlaybackState.adGroupCount][];
Arrays.fill(adMediaSourceHolders, new AdMediaSourceHolder[0]); Arrays.fill(adMediaSourceHolders, new AdMediaSourceHolder[0]);
} else {
checkState(adPlaybackState.adGroupCount == this.adPlaybackState.adGroupCount);
} }
this.adPlaybackState = adPlaybackState; this.adPlaybackState = adPlaybackState;
maybeUpdateAdMediaSources(); maybeUpdateAdMediaSources();
...@@ -350,12 +353,12 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> { ...@@ -350,12 +353,12 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
private void maybeUpdateSourceInfo() { private void maybeUpdateSourceInfo() {
@Nullable Timeline contentTimeline = this.contentTimeline; @Nullable Timeline contentTimeline = this.contentTimeline;
if (adPlaybackState != null && contentTimeline != null) { if (adPlaybackState != null && contentTimeline != null) {
adPlaybackState = adPlaybackState.withAdDurationsUs(getAdDurationsUs()); if (adPlaybackState.adGroupCount == 0) {
Timeline timeline = refreshSourceInfo(contentTimeline);
adPlaybackState.adGroupCount == 0 } else {
? contentTimeline adPlaybackState = adPlaybackState.withAdDurationsUs(getAdDurationsUs());
: new SinglePeriodAdTimeline(contentTimeline, adPlaybackState); refreshSourceInfo(new SinglePeriodAdTimeline(contentTimeline, adPlaybackState));
refreshSourceInfo(timeline); }
} }
} }
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.text; package com.google.android.exoplayer2.text;
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 android.os.Handler; import android.os.Handler;
import android.os.Handler.Callback; import android.os.Handler.Callback;
...@@ -91,6 +92,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { ...@@ -91,6 +92,7 @@ public final class TextRenderer extends BaseRenderer implements Callback {
@Nullable private SubtitleOutputBuffer subtitle; @Nullable private SubtitleOutputBuffer subtitle;
@Nullable private SubtitleOutputBuffer nextSubtitle; @Nullable private SubtitleOutputBuffer nextSubtitle;
private int nextSubtitleEventIndex; private int nextSubtitleEventIndex;
private long finalStreamEndPositionUs;
/** /**
* @param output The output. * @param output The output.
...@@ -121,6 +123,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { ...@@ -121,6 +123,7 @@ public final class TextRenderer extends BaseRenderer implements Callback {
outputLooper == null ? null : Util.createHandler(outputLooper, /* callback= */ this); outputLooper == null ? null : Util.createHandler(outputLooper, /* callback= */ this);
this.decoderFactory = decoderFactory; this.decoderFactory = decoderFactory;
formatHolder = new FormatHolder(); formatHolder = new FormatHolder();
finalStreamEndPositionUs = C.TIME_UNSET;
} }
@Override @Override
...@@ -141,6 +144,21 @@ public final class TextRenderer extends BaseRenderer implements Callback { ...@@ -141,6 +144,21 @@ public final class TextRenderer extends BaseRenderer implements Callback {
} }
} }
/**
* Sets the position at which to stop rendering the current stream.
*
* <p>Must be called after {@link #setCurrentStreamFinal()}.
*
* @param streamEndPositionUs The position to stop rendering at or {@link C#LENGTH_UNSET} to
* render until the end of the current stream.
*/
// TODO(internal b/181312195): Remove this when it's no longer needed once subtitles are decoded
// on the loading side of SampleQueue.
public void setFinalStreamEndPositionUs(long streamEndPositionUs) {
checkState(isCurrentStreamFinal());
this.finalStreamEndPositionUs = streamEndPositionUs;
}
@Override @Override
protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs) { protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs) {
streamFormat = formats[0]; streamFormat = formats[0];
...@@ -156,6 +174,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { ...@@ -156,6 +174,7 @@ public final class TextRenderer extends BaseRenderer implements Callback {
clearOutput(); clearOutput();
inputStreamEnded = false; inputStreamEnded = false;
outputStreamEnded = false; outputStreamEnded = false;
finalStreamEndPositionUs = C.TIME_UNSET;
if (decoderReplacementState != REPLACEMENT_STATE_NONE) { if (decoderReplacementState != REPLACEMENT_STATE_NONE) {
replaceDecoder(); replaceDecoder();
} else { } else {
...@@ -166,6 +185,13 @@ public final class TextRenderer extends BaseRenderer implements Callback { ...@@ -166,6 +185,13 @@ public final class TextRenderer extends BaseRenderer implements Callback {
@Override @Override
public void render(long positionUs, long elapsedRealtimeUs) { public void render(long positionUs, long elapsedRealtimeUs) {
if (isCurrentStreamFinal()
&& finalStreamEndPositionUs != C.TIME_UNSET
&& positionUs >= finalStreamEndPositionUs) {
releaseBuffers();
outputStreamEnded = true;
}
if (outputStreamEnded) { if (outputStreamEnded) {
return; return;
} }
...@@ -278,6 +304,7 @@ public final class TextRenderer extends BaseRenderer implements Callback { ...@@ -278,6 +304,7 @@ public final class TextRenderer extends BaseRenderer implements Callback {
@Override @Override
protected void onDisabled() { protected void onDisabled() {
streamFormat = null; streamFormat = null;
finalStreamEndPositionUs = C.TIME_UNSET;
clearOutput(); clearOutput();
releaseDecoder(); releaseDecoder();
} }
......
...@@ -41,6 +41,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray; ...@@ -41,6 +41,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.List; import java.util.List;
import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull;
...@@ -798,9 +799,7 @@ public final class Cea708Decoder extends CeaDecoder { ...@@ -798,9 +799,7 @@ public final class Cea708Decoder extends CeaDecoder {
} }
} }
} }
Collections.sort( Collections.sort(displayCueInfos, Cea708CueInfo.LEAST_IMPORTANT_FIRST);
displayCueInfos,
(thisInfo, thatInfo) -> Integer.compare(thisInfo.priority, thatInfo.priority));
List<Cue> displayCues = new ArrayList<>(displayCueInfos.size()); List<Cue> displayCues = new ArrayList<>(displayCueInfos.size());
for (int i = 0; i < displayCueInfos.size(); i++) { for (int i = 0; i < displayCueInfos.size(); i++) {
displayCues.add(displayCueInfos.get(i).cue); displayCues.add(displayCueInfos.get(i).cue);
...@@ -1321,9 +1320,22 @@ public final class Cea708Decoder extends CeaDecoder { ...@@ -1321,9 +1320,22 @@ public final class Cea708Decoder extends CeaDecoder {
/** A {@link Cue} for CEA-708. */ /** A {@link Cue} for CEA-708. */
private static final class Cea708CueInfo { private static final class Cea708CueInfo {
/**
* Sorts cue infos in order of ascending {@link Cea708CueInfo#priority} (which is descending by
* numeric value).
*/
private static final Comparator<Cea708CueInfo> LEAST_IMPORTANT_FIRST =
(thisInfo, thatInfo) -> Integer.compare(thatInfo.priority, thisInfo.priority);
public final Cue cue; public final Cue cue;
/** The priority of the cue box. */ /**
* The priority of the cue box. Low values are higher priority.
*
* <p>If cue boxes overlap, higher priority cue boxes are drawn on top.
*
* <p>See 8.4.2 of the CEA-708B spec.
*/
public final int priority; public final int priority;
/** /**
......
...@@ -16,12 +16,6 @@ ...@@ -16,12 +16,6 @@
*/ */
package com.google.android.exoplayer2.text.span; package com.google.android.exoplayer2.text.span;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import androidx.annotation.IntDef;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
/** /**
* A styling span for ruby text. * A styling span for ruby text.
* *
...@@ -38,48 +32,13 @@ import java.lang.annotation.Retention; ...@@ -38,48 +32,13 @@ import java.lang.annotation.Retention;
// rubies (e.g. HTML <rp> tag). // rubies (e.g. HTML <rp> tag).
public final class RubySpan { public final class RubySpan {
/** The ruby position is unknown. */
public static final int POSITION_UNKNOWN = -1;
/**
* The ruby text should be positioned above the base text.
*
* <p>For vertical text it should be positioned to the right, same as CSS's <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/ruby-position">ruby-position</a>.
*/
public static final int POSITION_OVER = 1;
/**
* The ruby text should be positioned below the base text.
*
* <p>For vertical text it should be positioned to the left, same as CSS's <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/ruby-position">ruby-position</a>.
*/
public static final int POSITION_UNDER = 2;
/**
* The possible positions of the ruby text relative to the base text.
*
* <p>One of:
*
* <ul>
* <li>{@link #POSITION_UNKNOWN}
* <li>{@link #POSITION_OVER}
* <li>{@link #POSITION_UNDER}
* </ul>
*/
@Documented
@Retention(SOURCE)
@IntDef({POSITION_UNKNOWN, POSITION_OVER, POSITION_UNDER})
public @interface Position {}
/** The ruby text, i.e. the smaller explanatory characters. */ /** The ruby text, i.e. the smaller explanatory characters. */
public final String rubyText; public final String rubyText;
/** The position of the ruby text relative to the base text. */ /** The position of the ruby text relative to the base text. */
@Position public final int position; @TextAnnotation.Position public final int position;
public RubySpan(String rubyText, @Position int position) { public RubySpan(String rubyText, @TextAnnotation.Position int position) {
this.rubyText = rubyText; this.rubyText = rubyText;
this.position = position; this.position = position;
} }
......
/*
* 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.text.span;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import androidx.annotation.IntDef;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
/** Properties of a text annotation (i.e. ruby, text emphasis marks). */
public final class TextAnnotation {
/** The text annotation position is unknown. */
public static final int POSITION_UNKNOWN = -1;
/**
* For horizontal text, the text annotation should be positioned above the base text.
*
* <p>For vertical text it should be positioned to the right, same as CSS's <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/ruby-position">ruby-position</a>.
*/
public static final int POSITION_BEFORE = 1;
/**
* For horizontal text, the text annotation should be positioned below the base text.
*
* <p>For vertical text it should be positioned to the left, same as CSS's <a
* href="https://developer.mozilla.org/en-US/docs/Web/CSS/ruby-position">ruby-position</a>.
*/
public static final int POSITION_AFTER = 2;
/**
* The possible positions of the annotation text relative to the base text.
*
* <p>One of:
*
* <ul>
* <li>{@link #POSITION_UNKNOWN}
* <li>{@link #POSITION_BEFORE}
* <li>{@link #POSITION_AFTER}
* </ul>
*/
@Documented
@Retention(SOURCE)
@IntDef({POSITION_UNKNOWN, POSITION_BEFORE, POSITION_AFTER})
public @interface Position {}
private TextAnnotation() {}
}
/*
* 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.text.span;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import androidx.annotation.IntDef;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
/**
* A styling span for text emphasis marks.
*
* <p>These are pronunciation aids such as <a
* href="https://www.w3.org/TR/jlreq/?lang=en#term.emphasis-dots">Japanese boutens</a> which can be
* rendered using the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/text-emphasis">
* text-emphasis</a> CSS property.
*/
// NOTE: There's no Android layout support for text emphasis, so this span currently doesn't extend
// any styling superclasses (e.g. MetricAffectingSpan). The only way to render this emphasis is to
// extract the spans and do the layout manually.
public final class TextEmphasisSpan {
/**
* The possible mark shapes that can be used.
*
* <p>One of:
*
* <ul>
* <li>{@link #MARK_SHAPE_NONE}
* <li>{@link #MARK_SHAPE_CIRCLE}
* <li>{@link #MARK_SHAPE_DOT}
* <li>{@link #MARK_SHAPE_SESAME}
* </ul>
*/
@Documented
@Retention(SOURCE)
@IntDef({MARK_SHAPE_NONE, MARK_SHAPE_CIRCLE, MARK_SHAPE_DOT, MARK_SHAPE_SESAME})
public @interface MarkShape {}
public static final int MARK_SHAPE_NONE = 0;
public static final int MARK_SHAPE_CIRCLE = 1;
public static final int MARK_SHAPE_DOT = 2;
public static final int MARK_SHAPE_SESAME = 3;
/**
* The possible mark fills that can be used.
*
* <p>One of:
*
* <ul>
* <li>{@link #MARK_FILL_UNKNOWN}
* <li>{@link #MARK_FILL_FILLED}
* <li>{@link #MARK_FILL_OPEN}
* </ul>
*/
@Documented
@Retention(SOURCE)
@IntDef({MARK_FILL_UNKNOWN, MARK_FILL_FILLED, MARK_FILL_OPEN})
public @interface MarkFill {}
public static final int MARK_FILL_UNKNOWN = 0;
public static final int MARK_FILL_FILLED = 1;
public static final int MARK_FILL_OPEN = 2;
/** The mark shape used for text emphasis. */
@MarkShape public int markShape;
/** The mark fill for the text emphasis mark. */
@MarkShape public int markFill;
/** The position of the text emphasis relative to the base text. */
@TextAnnotation.Position public final int position;
public TextEmphasisSpan(
@MarkShape int shape, @MarkFill int fill, @TextAnnotation.Position int position) {
this.markShape = shape;
this.markFill = fill;
this.position = position;
}
}
...@@ -18,9 +18,11 @@ package com.google.android.exoplayer2.text.ssa; ...@@ -18,9 +18,11 @@ package com.google.android.exoplayer2.text.ssa;
import static com.google.android.exoplayer2.text.Cue.LINE_TYPE_FRACTION; import static com.google.android.exoplayer2.text.Cue.LINE_TYPE_FRACTION;
import static com.google.android.exoplayer2.util.Util.castNonNull; import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.graphics.Typeface;
import android.text.Layout; import android.text.Layout;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
...@@ -318,6 +320,25 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { ...@@ -318,6 +320,25 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
cue.setTextSize( cue.setTextSize(
style.fontSize / screenHeight, Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING); style.fontSize / screenHeight, Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING);
} }
if (style.bold && style.italic) {
spannableText.setSpan(
new StyleSpan(Typeface.BOLD_ITALIC),
/* start= */ 0,
/* end= */ spannableText.length(),
SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
} else if (style.bold) {
spannableText.setSpan(
new StyleSpan(Typeface.BOLD),
/* start= */ 0,
/* end= */ spannableText.length(),
SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
} else if (style.italic) {
spannableText.setSpan(
new StyleSpan(Typeface.ITALIC),
/* start= */ 0,
/* end= */ spannableText.length(),
SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} }
@SsaStyle.SsaAlignment int alignment; @SsaStyle.SsaAlignment int alignment;
......
...@@ -76,7 +76,9 @@ import com.google.android.exoplayer2.util.Util; ...@@ -76,7 +76,9 @@ import com.google.android.exoplayer2.util.Util;
break; break;
} }
} }
return (startTimeIndex != C.INDEX_UNSET && endTimeIndex != C.INDEX_UNSET) return (startTimeIndex != C.INDEX_UNSET
&& endTimeIndex != C.INDEX_UNSET
&& textIndex != C.INDEX_UNSET)
? new SsaDialogueFormat(startTimeIndex, endTimeIndex, styleIndex, textIndex, keys.length) ? new SsaDialogueFormat(startTimeIndex, endTimeIndex, styleIndex, textIndex, keys.length)
: null; : null;
} }
......
...@@ -92,16 +92,22 @@ import java.util.regex.Pattern; ...@@ -92,16 +92,22 @@ import java.util.regex.Pattern;
@SsaAlignment public final int alignment; @SsaAlignment public final int alignment;
@Nullable @ColorInt public final Integer primaryColor; @Nullable @ColorInt public final Integer primaryColor;
public final float fontSize; public final float fontSize;
public final boolean bold;
public final boolean italic;
private SsaStyle( private SsaStyle(
String name, String name,
@SsaAlignment int alignment, @SsaAlignment int alignment,
@Nullable @ColorInt Integer primaryColor, @Nullable @ColorInt Integer primaryColor,
float fontSize) { float fontSize,
boolean bold,
boolean italic) {
this.name = name; this.name = name;
this.alignment = alignment; this.alignment = alignment;
this.primaryColor = primaryColor; this.primaryColor = primaryColor;
this.fontSize = fontSize; this.fontSize = fontSize;
this.bold = bold;
this.italic = italic;
} }
@Nullable @Nullable
...@@ -119,9 +125,21 @@ import java.util.regex.Pattern; ...@@ -119,9 +125,21 @@ import java.util.regex.Pattern;
try { try {
return new SsaStyle( return new SsaStyle(
styleValues[format.nameIndex].trim(), styleValues[format.nameIndex].trim(),
parseAlignment(styleValues[format.alignmentIndex].trim()), format.alignmentIndex != C.INDEX_UNSET
parseColor(styleValues[format.primaryColorIndex].trim()), ? parseAlignment(styleValues[format.alignmentIndex].trim())
parseFontSize(styleValues[format.fontSizeIndex].trim())); : SSA_ALIGNMENT_UNKNOWN,
format.primaryColorIndex != C.INDEX_UNSET
? parseColor(styleValues[format.primaryColorIndex].trim())
: null,
format.fontSizeIndex != C.INDEX_UNSET
? parseFontSize(styleValues[format.fontSizeIndex].trim())
: Cue.DIMEN_UNSET,
format.boldIndex != C.INDEX_UNSET
? parseBoldOrItalic(styleValues[format.boldIndex].trim())
: false,
format.italicIndex != C.INDEX_UNSET
? parseBoldOrItalic(styleValues[format.italicIndex].trim())
: false);
} catch (RuntimeException e) { } catch (RuntimeException e) {
Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e); Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e);
return null; return null;
...@@ -207,6 +225,16 @@ import java.util.regex.Pattern; ...@@ -207,6 +225,16 @@ import java.util.regex.Pattern;
} }
} }
private static boolean parseBoldOrItalic(String boldOrItalic) {
try {
int value = Integer.parseInt(boldOrItalic);
return value == 1 || value == -1;
} catch (NumberFormatException e) {
Log.w(TAG, "Failed to parse bold/italic: '" + boldOrItalic + "'", e);
return false;
}
}
/** /**
* Represents a {@code Format:} line from the {@code [V4+ Styles]} section * Represents a {@code Format:} line from the {@code [V4+ Styles]} section
* *
...@@ -219,14 +247,24 @@ import java.util.regex.Pattern; ...@@ -219,14 +247,24 @@ import java.util.regex.Pattern;
public final int alignmentIndex; public final int alignmentIndex;
public final int primaryColorIndex; public final int primaryColorIndex;
public final int fontSizeIndex; public final int fontSizeIndex;
public final int boldIndex;
public final int italicIndex;
public final int length; public final int length;
private Format( private Format(
int nameIndex, int alignmentIndex, int primaryColorIndex, int fontSizeIndex, int length) { int nameIndex,
int alignmentIndex,
int primaryColorIndex,
int fontSizeIndex,
int boldIndex,
int italicIndex,
int length) {
this.nameIndex = nameIndex; this.nameIndex = nameIndex;
this.alignmentIndex = alignmentIndex; this.alignmentIndex = alignmentIndex;
this.primaryColorIndex = primaryColorIndex; this.primaryColorIndex = primaryColorIndex;
this.fontSizeIndex = fontSizeIndex; this.fontSizeIndex = fontSizeIndex;
this.boldIndex = boldIndex;
this.italicIndex = italicIndex;
this.length = length; this.length = length;
} }
...@@ -241,6 +279,8 @@ import java.util.regex.Pattern; ...@@ -241,6 +279,8 @@ import java.util.regex.Pattern;
int alignmentIndex = C.INDEX_UNSET; int alignmentIndex = C.INDEX_UNSET;
int primaryColorIndex = C.INDEX_UNSET; int primaryColorIndex = C.INDEX_UNSET;
int fontSizeIndex = C.INDEX_UNSET; int fontSizeIndex = C.INDEX_UNSET;
int boldIndex = C.INDEX_UNSET;
int italicIndex = C.INDEX_UNSET;
String[] keys = String[] keys =
TextUtils.split(styleFormatLine.substring(SsaDecoder.FORMAT_LINE_PREFIX.length()), ","); TextUtils.split(styleFormatLine.substring(SsaDecoder.FORMAT_LINE_PREFIX.length()), ",");
for (int i = 0; i < keys.length; i++) { for (int i = 0; i < keys.length; i++) {
...@@ -257,10 +297,23 @@ import java.util.regex.Pattern; ...@@ -257,10 +297,23 @@ import java.util.regex.Pattern;
case "fontsize": case "fontsize":
fontSizeIndex = i; fontSizeIndex = i;
break; break;
case "bold":
boldIndex = i;
break;
case "italic":
italicIndex = i;
break;
} }
} }
return nameIndex != C.INDEX_UNSET return nameIndex != C.INDEX_UNSET
? new Format(nameIndex, alignmentIndex, primaryColorIndex, fontSizeIndex, keys.length) ? new Format(
nameIndex,
alignmentIndex,
primaryColorIndex,
fontSizeIndex,
boldIndex,
italicIndex,
keys.length)
: null; : null;
} }
} }
......
/*
* 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.text.ttml;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.text.TextUtils;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.span.TextAnnotation;
import com.google.android.exoplayer2.text.span.TextEmphasisSpan;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Represents a <a
* href="https://www.w3.org/TR/2018/REC-ttml2-20181108/#style-attribute-textEmphasis">
* tts:textEmphasis</a> attribute.
*/
/* package */ final class TextEmphasis {
@Documented
@Retention(SOURCE)
@IntDef({
TextEmphasisSpan.MARK_SHAPE_NONE,
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_SHAPE_DOT,
TextEmphasisSpan.MARK_SHAPE_SESAME,
MARK_SHAPE_AUTO
})
@interface MarkShape {}
/**
* The "auto" mark shape is only defined in TTML and is resolved to a concrete shape when building
* the {@link Cue}. Hence, it is not defined in {@link TextEmphasisSpan.MarkShape}.
*/
public static final int MARK_SHAPE_AUTO = -1;
@Documented
@Retention(SOURCE)
@IntDef({
TextAnnotation.POSITION_UNKNOWN,
TextAnnotation.POSITION_BEFORE,
TextAnnotation.POSITION_AFTER,
POSITION_OUTSIDE
})
public @interface Position {}
/**
* The "outside" position is only defined in TTML and is resolved before outputting a {@link Cue}
* object. Hence, it is not defined in {@link TextAnnotation.Position}.
*/
public static final int POSITION_OUTSIDE = -2;
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+");
private static final ImmutableSet<String> SINGLE_STYLE_VALUES =
ImmutableSet.of(TtmlNode.TEXT_EMPHASIS_AUTO, TtmlNode.TEXT_EMPHASIS_NONE);
private static final ImmutableSet<String> MARK_SHAPE_VALUES =
ImmutableSet.of(
TtmlNode.TEXT_EMPHASIS_MARK_DOT,
TtmlNode.TEXT_EMPHASIS_MARK_SESAME,
TtmlNode.TEXT_EMPHASIS_MARK_CIRCLE);
private static final ImmutableSet<String> MARK_FILL_VALUES =
ImmutableSet.of(TtmlNode.TEXT_EMPHASIS_MARK_FILLED, TtmlNode.TEXT_EMPHASIS_MARK_OPEN);
private static final ImmutableSet<String> POSITION_VALUES =
ImmutableSet.of(
TtmlNode.ANNOTATION_POSITION_AFTER,
TtmlNode.ANNOTATION_POSITION_BEFORE,
TtmlNode.ANNOTATION_POSITION_OUTSIDE);
/** The text emphasis mark shape. */
@MarkShape public final int markShape;
/** The fill style of the text emphasis mark. */
@TextEmphasisSpan.MarkFill public final int markFill;
/** The position of the text emphasis relative to the base text. */
@Position public final int position;
private TextEmphasis(
@MarkShape int markShape,
@TextEmphasisSpan.MarkFill int markFill,
@TextAnnotation.Position int position) {
this.markShape = markShape;
this.markFill = markFill;
this.position = position;
}
/**
* Parses a TTML <a
* href="https://www.w3.org/TR/2018/REC-ttml2-20181108/#style-attribute-textEmphasis">
* tts:textEmphasis</a> attribute. Returns null if parsing fails.
*
* <p>The parser searches for {@code emphasis-style} and {@code emphasis-position} independently.
* If a valid style is not found, the default style is used. If a valid position is not found, the
* default position is used.
*
* <p>Not implemented:
*
* <ul>
* <li>{@code emphasis-color}
* <li>Quoted string {@code emphasis-style}
* </ul>
*/
@Nullable
public static TextEmphasis parse(@Nullable String value) {
if (value == null) {
return null;
}
String parsingValue = value.trim();
if (parsingValue.isEmpty()) {
return null;
}
return parseWords(ImmutableSet.copyOf(TextUtils.split(parsingValue, WHITESPACE_PATTERN)));
}
private static TextEmphasis parseWords(ImmutableSet<String> nodes) {
Set<String> matchingPositions = Sets.intersection(POSITION_VALUES, nodes);
// If no emphasis position is specified, then the emphasis position must be interpreted as if
// a position of outside were specified:
// https://www.w3.org/TR/2018/REC-ttml2-20181108/#style-attribute-textEmphasis
@Position int position;
switch (Iterables.getFirst(matchingPositions, TtmlNode.ANNOTATION_POSITION_OUTSIDE)) {
case TtmlNode.ANNOTATION_POSITION_AFTER:
position = TextAnnotation.POSITION_AFTER;
break;
case TtmlNode.ANNOTATION_POSITION_OUTSIDE:
position = POSITION_OUTSIDE;
break;
case TtmlNode.ANNOTATION_POSITION_BEFORE:
default:
// If an implementation does not recognize or otherwise distinguish an annotation position
// value, then it must be interpreted as if a position of 'before' were specified:
// https://www.w3.org/TR/2018/REC-ttml2-20181108/#style-attribute-textEmphasis
position = TextAnnotation.POSITION_BEFORE;
}
Set<String> matchingSingleStyles = Sets.intersection(SINGLE_STYLE_VALUES, nodes);
if (!matchingSingleStyles.isEmpty()) {
// If "none" or "auto" are found in the description, ignore the other style (fill, shape)
// attributes.
@MarkShape int markShape;
switch (matchingSingleStyles.iterator().next()) {
case TtmlNode.TEXT_EMPHASIS_NONE:
markShape = TextEmphasisSpan.MARK_SHAPE_NONE;
break;
case TtmlNode.TEXT_EMPHASIS_AUTO:
default:
markShape = MARK_SHAPE_AUTO;
}
// markFill is ignored when markShape is NONE or AUTO
return new TextEmphasis(markShape, TextEmphasisSpan.MARK_FILL_UNKNOWN, position);
}
Set<String> matchingFills = Sets.intersection(MARK_FILL_VALUES, nodes);
Set<String> matchingShapes = Sets.intersection(MARK_SHAPE_VALUES, nodes);
if (matchingFills.isEmpty() && matchingShapes.isEmpty()) {
// If an implementation does not recognize or otherwise distinguish an emphasis style value,
// then it must be interpreted as if a style of auto were specified; as such, an
// implementation that supports text emphasis marks must minimally support the auto value.
// https://www.w3.org/TR/ttml2/#style-value-emphasis-style.
//
// markFill is ignored when markShape is NONE or AUTO.
return new TextEmphasis(MARK_SHAPE_AUTO, TextEmphasisSpan.MARK_FILL_UNKNOWN, position);
}
@TextEmphasisSpan.MarkFill int markFill;
switch (Iterables.getFirst(matchingFills, TtmlNode.TEXT_EMPHASIS_MARK_FILLED)) {
case TtmlNode.TEXT_EMPHASIS_MARK_OPEN:
markFill = TextEmphasisSpan.MARK_FILL_OPEN;
break;
case TtmlNode.TEXT_EMPHASIS_MARK_FILLED:
default:
markFill = TextEmphasisSpan.MARK_FILL_FILLED;
}
@MarkShape int markShape;
switch (Iterables.getFirst(matchingShapes, TtmlNode.TEXT_EMPHASIS_MARK_CIRCLE)) {
case TtmlNode.TEXT_EMPHASIS_MARK_DOT:
markShape = TextEmphasisSpan.MARK_SHAPE_DOT;
break;
case TtmlNode.TEXT_EMPHASIS_MARK_SESAME:
markShape = TextEmphasisSpan.MARK_SHAPE_SESAME;
break;
case TtmlNode.TEXT_EMPHASIS_MARK_CIRCLE:
default:
markShape = TextEmphasisSpan.MARK_SHAPE_CIRCLE;
}
return new TextEmphasis(markShape, markFill, position);
}
}
...@@ -15,6 +15,9 @@ ...@@ -15,6 +15,9 @@
*/ */
package com.google.android.exoplayer2.text.ttml; package com.google.android.exoplayer2.text.ttml;
import static java.lang.Math.max;
import static java.lang.Math.min;
import android.text.Layout; import android.text.Layout;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
...@@ -22,7 +25,7 @@ import com.google.android.exoplayer2.text.Cue; ...@@ -22,7 +25,7 @@ import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.SimpleSubtitleDecoder; import com.google.android.exoplayer2.text.SimpleSubtitleDecoder;
import com.google.android.exoplayer2.text.Subtitle; import com.google.android.exoplayer2.text.Subtitle;
import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.text.SubtitleDecoderException;
import com.google.android.exoplayer2.text.span.RubySpan; import com.google.android.exoplayer2.text.span.TextAnnotation;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ColorParser; import com.google.android.exoplayer2.util.ColorParser;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
...@@ -81,7 +84,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { ...@@ -81,7 +84,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
private static final Pattern OFFSET_TIME = private static final Pattern OFFSET_TIME =
Pattern.compile("^([0-9]+(?:\\.[0-9]+)?)(h|m|s|ms|f|t)$"); Pattern.compile("^([0-9]+(?:\\.[0-9]+)?)(h|m|s|ms|f|t)$");
private static final Pattern FONT_SIZE = Pattern.compile("^(([0-9]*.)?[0-9]+)(px|em|%)$"); private static final Pattern FONT_SIZE = Pattern.compile("^(([0-9]*.)?[0-9]+)(px|em|%)$");
private static final Pattern PERCENTAGE_COORDINATES = static final Pattern SIGNED_PERCENTAGE = Pattern.compile("^([-+]?\\d+\\.?\\d*?)%$");
static final Pattern PERCENTAGE_COORDINATES =
Pattern.compile("^(\\d+\\.?\\d*?)% (\\d+\\.?\\d*?)%$"); Pattern.compile("^(\\d+\\.?\\d*?)% (\\d+\\.?\\d*?)%$");
private static final Pattern PIXEL_COORDINATES = private static final Pattern PIXEL_COORDINATES =
Pattern.compile("^(\\d+\\.?\\d*?)px (\\d+\\.?\\d*?)px$"); Pattern.compile("^(\\d+\\.?\\d*?)px (\\d+\\.?\\d*?)px$");
...@@ -582,11 +586,11 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { ...@@ -582,11 +586,11 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
break; break;
case TtmlNode.ATTR_TTS_RUBY_POSITION: case TtmlNode.ATTR_TTS_RUBY_POSITION:
switch (Util.toLowerInvariant(attributeValue)) { switch (Util.toLowerInvariant(attributeValue)) {
case TtmlNode.RUBY_BEFORE: case TtmlNode.ANNOTATION_POSITION_BEFORE:
style = createIfNull(style).setRubyPosition(RubySpan.POSITION_OVER); style = createIfNull(style).setRubyPosition(TextAnnotation.POSITION_BEFORE);
break; break;
case TtmlNode.RUBY_AFTER: case TtmlNode.ANNOTATION_POSITION_AFTER:
style = createIfNull(style).setRubyPosition(RubySpan.POSITION_UNDER); style = createIfNull(style).setRubyPosition(TextAnnotation.POSITION_AFTER);
break; break;
default: default:
// ignore // ignore
...@@ -609,6 +613,14 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { ...@@ -609,6 +613,14 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
break; break;
} }
break; break;
case TtmlNode.ATTR_TTS_TEXT_EMPHASIS:
style =
createIfNull(style)
.setTextEmphasis(TextEmphasis.parse(Util.toLowerInvariant(attributeValue)));
break;
case TtmlNode.ATTR_TTS_SHEAR:
style = createIfNull(style).setShearPercentage(parseShear(attributeValue));
break;
default: default:
// ignore // ignore
break; break;
...@@ -751,10 +763,35 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder { ...@@ -751,10 +763,35 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
} }
/** /**
* Returns the parsed shear percentage (between -100.0 and +100.0 inclusive), or {@link
* TtmlStyle#UNSPECIFIED_SHEAR} if parsing failed.
*/
private static float parseShear(String expression) {
Matcher matcher = SIGNED_PERCENTAGE.matcher(expression);
if (!matcher.matches()) {
Log.w(TAG, "Invalid value for shear: " + expression);
return TtmlStyle.UNSPECIFIED_SHEAR;
}
try {
String percentage = Assertions.checkNotNull(matcher.group(1));
float value = Float.parseFloat(percentage);
// https://www.w3.org/TR/2018/REC-ttml2-20181108/#semantics-style-procedures-shear
// If the absolute value of the specified percentage is greater than 100%, then it must be
// interpreted as if 100% were specified with the appropriate sign.
value = max(-100f, value);
value = min(100f, value);
return value;
} catch (NumberFormatException e) {
Log.w(TAG, "Failed to parse shear: " + expression, e);
return TtmlStyle.UNSPECIFIED_SHEAR;
}
}
/**
* Parses a time expression, returning the parsed timestamp. * Parses a time expression, returning the parsed timestamp.
* <p> *
* For the format of a time expression, see: * <p>For the format of a time expression, see: <a
* <a href="http://www.w3.org/TR/ttaf1-dfxp/#timing-value-timeExpression">timeExpression</a> * href="http://www.w3.org/TR/ttaf1-dfxp/#timing-value-timeExpression">timeExpression</a>
* *
* @param time A string that includes the time expression. * @param time A string that includes the time expression.
* @param frameAndTickRate The effective frame and tick rates of the stream. * @param frameAndTickRate The effective frame and tick rates of the stream.
......
...@@ -69,7 +69,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -69,7 +69,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public static final String ATTR_TTS_TEXT_DECORATION = "textDecoration"; public static final String ATTR_TTS_TEXT_DECORATION = "textDecoration";
public static final String ATTR_TTS_TEXT_ALIGN = "textAlign"; public static final String ATTR_TTS_TEXT_ALIGN = "textAlign";
public static final String ATTR_TTS_TEXT_COMBINE = "textCombine"; public static final String ATTR_TTS_TEXT_COMBINE = "textCombine";
public static final String ATTR_TTS_TEXT_EMPHASIS = "textEmphasis";
public static final String ATTR_TTS_WRITING_MODE = "writingMode"; public static final String ATTR_TTS_WRITING_MODE = "writingMode";
public static final String ATTR_TTS_SHEAR = "shear";
// Values for ruby // Values for ruby
public static final String RUBY_CONTAINER = "container"; public static final String RUBY_CONTAINER = "container";
...@@ -79,9 +81,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -79,9 +81,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public static final String RUBY_TEXT_CONTAINER = "textContainer"; public static final String RUBY_TEXT_CONTAINER = "textContainer";
public static final String RUBY_DELIMITER = "delimiter"; public static final String RUBY_DELIMITER = "delimiter";
// Values for rubyPosition // Values for text annotation (i.e. ruby, text emphasis) position
public static final String RUBY_BEFORE = "before"; public static final String ANNOTATION_POSITION_BEFORE = "before";
public static final String RUBY_AFTER = "after"; public static final String ANNOTATION_POSITION_AFTER = "after";
public static final String ANNOTATION_POSITION_OUTSIDE = "outside";
// Values for textDecoration // Values for textDecoration
public static final String LINETHROUGH = "linethrough"; public static final String LINETHROUGH = "linethrough";
public static final String NO_LINETHROUGH = "nolinethrough"; public static final String NO_LINETHROUGH = "nolinethrough";
...@@ -106,6 +110,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -106,6 +110,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public static final String VERTICAL_LR = "tblr"; public static final String VERTICAL_LR = "tblr";
public static final String VERTICAL_RL = "tbrl"; public static final String VERTICAL_RL = "tbrl";
// Values for textEmphasis
public static final String TEXT_EMPHASIS_NONE = "none";
public static final String TEXT_EMPHASIS_AUTO = "auto";
public static final String TEXT_EMPHASIS_MARK_DOT = "dot";
public static final String TEXT_EMPHASIS_MARK_SESAME = "sesame";
public static final String TEXT_EMPHASIS_MARK_CIRCLE = "circle";
public static final String TEXT_EMPHASIS_MARK_FILLED = "filled";
public static final String TEXT_EMPHASIS_MARK_OPEN = "open";
@Nullable public final String tag; @Nullable public final String tag;
@Nullable public final String text; @Nullable public final String text;
public final boolean isTextNode; public final boolean isTextNode;
...@@ -243,7 +256,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -243,7 +256,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
TreeMap<String, Cue.Builder> regionTextOutputs = new TreeMap<>(); TreeMap<String, Cue.Builder> regionTextOutputs = new TreeMap<>();
traverseForText(timeUs, false, regionId, regionTextOutputs); traverseForText(timeUs, false, regionId, regionTextOutputs);
traverseForStyle(timeUs, globalStyles, regionTextOutputs); traverseForStyle(timeUs, globalStyles, regionMap, regionId, regionTextOutputs);
List<Cue> cues = new ArrayList<>(); List<Cue> cues = new ArrayList<>();
...@@ -354,26 +367,39 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -354,26 +367,39 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
private void traverseForStyle( private void traverseForStyle(
long timeUs, Map<String, TtmlStyle> globalStyles, Map<String, Cue.Builder> regionOutputs) { long timeUs,
Map<String, TtmlStyle> globalStyles,
Map<String, TtmlRegion> regionMaps,
String inheritedRegion,
Map<String, Cue.Builder> regionOutputs) {
if (!isActive(timeUs)) { if (!isActive(timeUs)) {
return; return;
} }
String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId;
for (Map.Entry<String, Integer> entry : nodeEndsByRegion.entrySet()) { for (Map.Entry<String, Integer> entry : nodeEndsByRegion.entrySet()) {
String regionId = entry.getKey(); String regionId = entry.getKey();
int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0; int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0;
int end = entry.getValue(); int end = entry.getValue();
if (start != end) { if (start != end) {
Cue.Builder regionOutput = Assertions.checkNotNull(regionOutputs.get(regionId)); Cue.Builder regionOutput = Assertions.checkNotNull(regionOutputs.get(regionId));
applyStyleToOutput(globalStyles, regionOutput, start, end); @Cue.VerticalType
int verticalType = Assertions.checkNotNull(regionMaps.get(resolvedRegionId)).verticalType;
applyStyleToOutput(globalStyles, regionOutput, start, end, verticalType);
} }
} }
for (int i = 0; i < getChildCount(); ++i) { for (int i = 0; i < getChildCount(); ++i) {
getChild(i).traverseForStyle(timeUs, globalStyles, regionOutputs); getChild(i)
.traverseForStyle(timeUs, globalStyles, regionMaps, resolvedRegionId, regionOutputs);
} }
} }
private void applyStyleToOutput( private void applyStyleToOutput(
Map<String, TtmlStyle> globalStyles, Cue.Builder regionOutput, int start, int end) { Map<String, TtmlStyle> globalStyles,
Cue.Builder regionOutput,
int start,
int end,
@Cue.VerticalType int verticalType) {
@Nullable TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles); @Nullable TtmlStyle resolvedStyle = TtmlRenderUtil.resolveStyle(style, styleIds, globalStyles);
@Nullable SpannableStringBuilder text = (SpannableStringBuilder) regionOutput.getText(); @Nullable SpannableStringBuilder text = (SpannableStringBuilder) regionOutput.getText();
if (text == null) { if (text == null) {
...@@ -381,7 +407,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; ...@@ -381,7 +407,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
regionOutput.setText(text); regionOutput.setText(text);
} }
if (resolvedStyle != null) { if (resolvedStyle != null) {
TtmlRenderUtil.applyStylesToSpan(text, start, end, resolvedStyle, parent, globalStyles); TtmlRenderUtil.applyStylesToSpan(
text, start, end, resolvedStyle, parent, globalStyles, verticalType);
if (resolvedStyle.getShearPercentage() != TtmlStyle.UNSPECIFIED_SHEAR && TAG_P.equals(tag)) {
// Shear style should only be applied to P nodes
// https://www.w3.org/TR/2018/REC-ttml2-20181108/#style-attribute-shear
// The spec doesn't specify the coordinate system to use for block shear
// however the spec shows examples of how different values are expected to be rendered.
// See: https://www.w3.org/TR/2018/REC-ttml2-20181108/#style-attribute-shear
// https://www.w3.org/TR/2018/REC-ttml2-20181108/#style-attribute-fontShear
// This maps the shear percentage to shear angle in graphics coordinates
regionOutput.setShearDegrees((resolvedStyle.getShearPercentage() * -90) / 100);
}
regionOutput.setTextAlignment(resolvedStyle.getTextAlign()); regionOutput.setTextAlignment(resolvedStyle.getTextAlign());
} }
} }
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer2.text.ttml; package com.google.android.exoplayer2.text.ttml;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import android.text.Spannable; import android.text.Spannable;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
...@@ -27,9 +29,12 @@ import android.text.style.StyleSpan; ...@@ -27,9 +29,12 @@ import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan; import android.text.style.TypefaceSpan;
import android.text.style.UnderlineSpan; import android.text.style.UnderlineSpan;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
import com.google.android.exoplayer2.text.span.RubySpan; import com.google.android.exoplayer2.text.span.RubySpan;
import com.google.android.exoplayer2.text.span.SpanUtil; import com.google.android.exoplayer2.text.span.SpanUtil;
import com.google.android.exoplayer2.text.span.TextAnnotation;
import com.google.android.exoplayer2.text.span.TextEmphasisSpan;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.util.ArrayDeque; import java.util.ArrayDeque;
...@@ -83,7 +88,8 @@ import java.util.Map; ...@@ -83,7 +88,8 @@ import java.util.Map;
int end, int end,
TtmlStyle style, TtmlStyle style,
@Nullable TtmlNode parent, @Nullable TtmlNode parent,
Map<String, TtmlStyle> globalStyles) { Map<String, TtmlStyle> globalStyles,
@Cue.VerticalType int verticalType) {
if (style.getStyle() != TtmlStyle.UNSPECIFIED) { if (style.getStyle() != TtmlStyle.UNSPECIFIED) {
builder.setSpan(new StyleSpan(style.getStyle()), start, end, builder.setSpan(new StyleSpan(style.getStyle()), start, end,
...@@ -119,6 +125,40 @@ import java.util.Map; ...@@ -119,6 +125,40 @@ import java.util.Map;
end, end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
if (style.getTextEmphasis() != null) {
TextEmphasis textEmphasis = checkNotNull(style.getTextEmphasis());
@TextEmphasisSpan.MarkShape int markShape;
@TextEmphasisSpan.MarkFill int markFill;
if (textEmphasis.markShape == TextEmphasis.MARK_SHAPE_AUTO) {
// If a vertical writing mode applies, then 'auto' is equivalent to 'filled sesame';
// otherwise, it's equivalent to 'filled circle':
// https://www.w3.org/TR/ttml2/#style-value-emphasis-style
markShape =
(verticalType == Cue.VERTICAL_TYPE_LR || verticalType == Cue.VERTICAL_TYPE_RL)
? TextEmphasisSpan.MARK_SHAPE_SESAME
: TextEmphasisSpan.MARK_SHAPE_CIRCLE;
markFill = TextEmphasisSpan.MARK_FILL_FILLED;
} else {
markShape = textEmphasis.markShape;
markFill = textEmphasis.markFill;
}
@TextEmphasis.Position int position;
if (textEmphasis.position == TextEmphasis.POSITION_OUTSIDE) {
// 'outside' is not supported by TextEmphasisSpan, so treat it as 'before':
// https://www.w3.org/TR/ttml2/#style-value-annotation-position
position = TextAnnotation.POSITION_BEFORE;
} else {
position = textEmphasis.position;
}
SpanUtil.addOrReplaceSpan(
builder,
new TextEmphasisSpan(markShape, markFill, position),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
switch (style.getRubyType()) { switch (style.getRubyType()) {
case TtmlStyle.RUBY_TYPE_BASE: case TtmlStyle.RUBY_TYPE_BASE:
// look for the sibling RUBY_TEXT and add it as span between start & end. // look for the sibling RUBY_TEXT and add it as span between start & end.
...@@ -141,11 +181,11 @@ import java.util.Map; ...@@ -141,11 +181,11 @@ import java.util.Map;
} }
// TODO: Get rubyPosition from `textNode` when TTML inheritance is implemented. // TODO: Get rubyPosition from `textNode` when TTML inheritance is implemented.
@RubySpan.Position @TextAnnotation.Position
int rubyPosition = int rubyPosition =
containerNode.style != null containerNode.style != null
? containerNode.style.getRubyPosition() ? containerNode.style.getRubyPosition()
: RubySpan.POSITION_UNKNOWN; : TextAnnotation.POSITION_UNKNOWN;
builder.setSpan( builder.setSpan(
new RubySpan(rubyText, rubyPosition), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); new RubySpan(rubyText, rubyPosition), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
break; break;
......
...@@ -19,7 +19,7 @@ import android.graphics.Typeface; ...@@ -19,7 +19,7 @@ import android.graphics.Typeface;
import android.text.Layout; import android.text.Layout;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.span.RubySpan; import com.google.android.exoplayer2.text.span.TextAnnotation;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
...@@ -30,6 +30,7 @@ import java.lang.annotation.RetentionPolicy; ...@@ -30,6 +30,7 @@ import java.lang.annotation.RetentionPolicy;
/* package */ final class TtmlStyle { /* package */ final class TtmlStyle {
public static final int UNSPECIFIED = -1; public static final int UNSPECIFIED = -1;
public static final float UNSPECIFIED_SHEAR = Float.MAX_VALUE;
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
...@@ -83,9 +84,11 @@ import java.lang.annotation.RetentionPolicy; ...@@ -83,9 +84,11 @@ import java.lang.annotation.RetentionPolicy;
private float fontSize; private float fontSize;
@Nullable private String id; @Nullable private String id;
@RubyType private int rubyType; @RubyType private int rubyType;
@RubySpan.Position private int rubyPosition; @TextAnnotation.Position private int rubyPosition;
@Nullable private Layout.Alignment textAlign; @Nullable private Layout.Alignment textAlign;
@OptionalBoolean private int textCombine; @OptionalBoolean private int textCombine;
@Nullable private TextEmphasis textEmphasis;
private float shearPercentage;
public TtmlStyle() { public TtmlStyle() {
linethrough = UNSPECIFIED; linethrough = UNSPECIFIED;
...@@ -94,8 +97,9 @@ import java.lang.annotation.RetentionPolicy; ...@@ -94,8 +97,9 @@ import java.lang.annotation.RetentionPolicy;
italic = UNSPECIFIED; italic = UNSPECIFIED;
fontSizeUnit = UNSPECIFIED; fontSizeUnit = UNSPECIFIED;
rubyType = UNSPECIFIED; rubyType = UNSPECIFIED;
rubyPosition = RubySpan.POSITION_UNKNOWN; rubyPosition = TextAnnotation.POSITION_UNKNOWN;
textCombine = UNSPECIFIED; textCombine = UNSPECIFIED;
shearPercentage = UNSPECIFIED_SHEAR;
} }
/** /**
...@@ -184,6 +188,15 @@ import java.lang.annotation.RetentionPolicy; ...@@ -184,6 +188,15 @@ import java.lang.annotation.RetentionPolicy;
return hasBackgroundColor; return hasBackgroundColor;
} }
public TtmlStyle setShearPercentage(float shearPercentage) {
this.shearPercentage = shearPercentage;
return this;
}
public float getShearPercentage() {
return shearPercentage;
}
/** /**
* Chains this style to referential style. Local properties which are already set are never * Chains this style to referential style. Local properties which are already set are never
* overridden. * overridden.
...@@ -225,7 +238,7 @@ import java.lang.annotation.RetentionPolicy; ...@@ -225,7 +238,7 @@ import java.lang.annotation.RetentionPolicy;
if (underline == UNSPECIFIED) { if (underline == UNSPECIFIED) {
underline = ancestor.underline; underline = ancestor.underline;
} }
if (rubyPosition == RubySpan.POSITION_UNKNOWN) { if (rubyPosition == TextAnnotation.POSITION_UNKNOWN) {
rubyPosition = ancestor.rubyPosition; rubyPosition = ancestor.rubyPosition;
} }
if (textAlign == null && ancestor.textAlign != null) { if (textAlign == null && ancestor.textAlign != null) {
...@@ -238,6 +251,12 @@ import java.lang.annotation.RetentionPolicy; ...@@ -238,6 +251,12 @@ import java.lang.annotation.RetentionPolicy;
fontSizeUnit = ancestor.fontSizeUnit; fontSizeUnit = ancestor.fontSizeUnit;
fontSize = ancestor.fontSize; fontSize = ancestor.fontSize;
} }
if (textEmphasis == null) {
textEmphasis = ancestor.textEmphasis;
}
if (shearPercentage == UNSPECIFIED_SHEAR) {
shearPercentage = ancestor.shearPercentage;
}
// attributes not inherited as of http://www.w3.org/TR/ttml1/ // attributes not inherited as of http://www.w3.org/TR/ttml1/
if (chaining && !hasBackgroundColor && ancestor.hasBackgroundColor) { if (chaining && !hasBackgroundColor && ancestor.hasBackgroundColor) {
setBackgroundColor(ancestor.backgroundColor); setBackgroundColor(ancestor.backgroundColor);
...@@ -269,12 +288,12 @@ import java.lang.annotation.RetentionPolicy; ...@@ -269,12 +288,12 @@ import java.lang.annotation.RetentionPolicy;
return rubyType; return rubyType;
} }
public TtmlStyle setRubyPosition(@RubySpan.Position int position) { public TtmlStyle setRubyPosition(@TextAnnotation.Position int position) {
this.rubyPosition = position; this.rubyPosition = position;
return this; return this;
} }
@RubySpan.Position @TextAnnotation.Position
public int getRubyPosition() { public int getRubyPosition() {
return rubyPosition; return rubyPosition;
} }
...@@ -299,6 +318,16 @@ import java.lang.annotation.RetentionPolicy; ...@@ -299,6 +318,16 @@ import java.lang.annotation.RetentionPolicy;
return this; return this;
} }
@Nullable
public TextEmphasis getTextEmphasis() {
return textEmphasis;
}
public TtmlStyle setTextEmphasis(@Nullable TextEmphasis textEmphasis) {
this.textEmphasis = textEmphasis;
return this;
}
public TtmlStyle setFontSize(float fontSize) { public TtmlStyle setFontSize(float fontSize) {
this.fontSize = fontSize; this.fontSize = fontSize;
return this; return this;
......
...@@ -17,7 +17,7 @@ package com.google.android.exoplayer2.text.webvtt; ...@@ -17,7 +17,7 @@ package com.google.android.exoplayer2.text.webvtt;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.span.RubySpan; import com.google.android.exoplayer2.text.span.TextAnnotation;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ColorParser; import com.google.android.exoplayer2.util.ColorParser;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
...@@ -195,9 +195,9 @@ import java.util.regex.Pattern; ...@@ -195,9 +195,9 @@ import java.util.regex.Pattern;
style.setBackgroundColor(ColorParser.parseCssColor(value)); style.setBackgroundColor(ColorParser.parseCssColor(value));
} else if (PROPERTY_RUBY_POSITION.equals(property)) { } else if (PROPERTY_RUBY_POSITION.equals(property)) {
if (VALUE_OVER.equals(value)) { if (VALUE_OVER.equals(value)) {
style.setRubyPosition(RubySpan.POSITION_OVER); style.setRubyPosition(TextAnnotation.POSITION_BEFORE);
} else if (VALUE_UNDER.equals(value)) { } else if (VALUE_UNDER.equals(value)) {
style.setRubyPosition(RubySpan.POSITION_UNDER); style.setRubyPosition(TextAnnotation.POSITION_AFTER);
} }
} else if (PROPERTY_TEXT_COMBINE_UPRIGHT.equals(property)) { } else if (PROPERTY_TEXT_COMBINE_UPRIGHT.equals(property)) {
style.setCombineUpright(VALUE_ALL.equals(value) || value.startsWith(VALUE_DIGITS)); style.setCombineUpright(VALUE_ALL.equals(value) || value.startsWith(VALUE_DIGITS));
......
...@@ -20,7 +20,7 @@ import android.text.TextUtils; ...@@ -20,7 +20,7 @@ import android.text.TextUtils;
import androidx.annotation.ColorInt; import androidx.annotation.ColorInt;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.span.RubySpan; import com.google.android.exoplayer2.text.span.TextAnnotation;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
...@@ -95,7 +95,7 @@ public final class WebvttCssStyle { ...@@ -95,7 +95,7 @@ public final class WebvttCssStyle {
@OptionalBoolean private int italic; @OptionalBoolean private int italic;
@FontSizeUnit private int fontSizeUnit; @FontSizeUnit private int fontSizeUnit;
private float fontSize; private float fontSize;
@RubySpan.Position private int rubyPosition; @TextAnnotation.Position private int rubyPosition;
private boolean combineUpright; private boolean combineUpright;
public WebvttCssStyle() { public WebvttCssStyle() {
...@@ -111,7 +111,7 @@ public final class WebvttCssStyle { ...@@ -111,7 +111,7 @@ public final class WebvttCssStyle {
bold = UNSPECIFIED; bold = UNSPECIFIED;
italic = UNSPECIFIED; italic = UNSPECIFIED;
fontSizeUnit = UNSPECIFIED; fontSizeUnit = UNSPECIFIED;
rubyPosition = RubySpan.POSITION_UNKNOWN; rubyPosition = TextAnnotation.POSITION_UNKNOWN;
combineUpright = false; combineUpright = false;
} }
...@@ -272,12 +272,12 @@ public final class WebvttCssStyle { ...@@ -272,12 +272,12 @@ public final class WebvttCssStyle {
return fontSize; return fontSize;
} }
public WebvttCssStyle setRubyPosition(@RubySpan.Position int rubyPosition) { public WebvttCssStyle setRubyPosition(@TextAnnotation.Position int rubyPosition) {
this.rubyPosition = rubyPosition; this.rubyPosition = rubyPosition;
return this; return this;
} }
@RubySpan.Position @TextAnnotation.Position
public int getRubyPosition() { public int getRubyPosition() {
return rubyPosition; return rubyPosition;
} }
......
...@@ -39,6 +39,7 @@ import androidx.annotation.Nullable; ...@@ -39,6 +39,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
import com.google.android.exoplayer2.text.span.RubySpan; import com.google.android.exoplayer2.text.span.RubySpan;
import com.google.android.exoplayer2.text.span.TextAnnotation;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.ParsableByteArray;
...@@ -572,7 +573,7 @@ public final class WebvttCueParser { ...@@ -572,7 +573,7 @@ public final class WebvttCueParser {
StartTag startTag, StartTag startTag,
List<Element> nestedElements, List<Element> nestedElements,
List<WebvttCssStyle> styles) { List<WebvttCssStyle> styles) {
@RubySpan.Position int rubyTagPosition = getRubyPosition(styles, cueId, startTag); @TextAnnotation.Position int rubyTagPosition = getRubyPosition(styles, cueId, startTag);
List<Element> sortedNestedElements = new ArrayList<>(nestedElements.size()); List<Element> sortedNestedElements = new ArrayList<>(nestedElements.size());
sortedNestedElements.addAll(nestedElements); sortedNestedElements.addAll(nestedElements);
Collections.sort(sortedNestedElements, Element.BY_START_POSITION_ASC); Collections.sort(sortedNestedElements, Element.BY_START_POSITION_ASC);
...@@ -585,12 +586,12 @@ public final class WebvttCueParser { ...@@ -585,12 +586,12 @@ public final class WebvttCueParser {
Element rubyTextElement = sortedNestedElements.get(i); Element rubyTextElement = sortedNestedElements.get(i);
// Use the <rt> element's ruby-position if set, otherwise the <ruby> element's and otherwise // Use the <rt> element's ruby-position if set, otherwise the <ruby> element's and otherwise
// default to OVER. // default to OVER.
@RubySpan.Position @TextAnnotation.Position
int rubyPosition = int rubyPosition =
firstKnownRubyPosition( firstKnownRubyPosition(
getRubyPosition(styles, cueId, rubyTextElement.startTag), getRubyPosition(styles, cueId, rubyTextElement.startTag),
rubyTagPosition, rubyTagPosition,
RubySpan.POSITION_OVER); TextAnnotation.POSITION_BEFORE);
// Move the rubyText from spannedText into the RubySpan. // Move the rubyText from spannedText into the RubySpan.
int adjustedRubyTextStart = rubyTextElement.startTag.position - deletedCharCount; int adjustedRubyTextStart = rubyTextElement.startTag.position - deletedCharCount;
int adjustedRubyTextEnd = rubyTextElement.endPosition - deletedCharCount; int adjustedRubyTextEnd = rubyTextElement.endPosition - deletedCharCount;
...@@ -607,31 +608,31 @@ public final class WebvttCueParser { ...@@ -607,31 +608,31 @@ public final class WebvttCueParser {
} }
} }
@RubySpan.Position @TextAnnotation.Position
private static int getRubyPosition( private static int getRubyPosition(
List<WebvttCssStyle> styles, @Nullable String cueId, StartTag startTag) { List<WebvttCssStyle> styles, @Nullable String cueId, StartTag startTag) {
List<StyleMatch> styleMatches = getApplicableStyles(styles, cueId, startTag); List<StyleMatch> styleMatches = getApplicableStyles(styles, cueId, startTag);
for (int i = 0; i < styleMatches.size(); i++) { for (int i = 0; i < styleMatches.size(); i++) {
WebvttCssStyle style = styleMatches.get(i).style; WebvttCssStyle style = styleMatches.get(i).style;
if (style.getRubyPosition() != RubySpan.POSITION_UNKNOWN) { if (style.getRubyPosition() != TextAnnotation.POSITION_UNKNOWN) {
return style.getRubyPosition(); return style.getRubyPosition();
} }
} }
return RubySpan.POSITION_UNKNOWN; return TextAnnotation.POSITION_UNKNOWN;
} }
@RubySpan.Position @TextAnnotation.Position
private static int firstKnownRubyPosition( private static int firstKnownRubyPosition(
@RubySpan.Position int position1, @TextAnnotation.Position int position1,
@RubySpan.Position int position2, @TextAnnotation.Position int position2,
@RubySpan.Position int position3) { @TextAnnotation.Position int position3) {
if (position1 != RubySpan.POSITION_UNKNOWN) { if (position1 != TextAnnotation.POSITION_UNKNOWN) {
return position1; return position1;
} }
if (position2 != RubySpan.POSITION_UNKNOWN) { if (position2 != TextAnnotation.POSITION_UNKNOWN) {
return position2; return position2;
} }
if (position3 != RubySpan.POSITION_UNKNOWN) { if (position3 != TextAnnotation.POSITION_UNKNOWN) {
return position3; return position3;
} }
throw new IllegalArgumentException(); throw new IllegalArgumentException();
......
...@@ -1488,6 +1488,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -1488,6 +1488,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
maxPixels = width * height; maxPixels = width * height;
minCompressionRatio = 2; minCompressionRatio = 2;
break; break;
case MimeTypes.VIDEO_DOLBY_VISION:
// Dolby vision can be a wrapper around H264 or H265. We assume H264 here because the
// minimum compression ratio is lower, meaning we overestimate the maximum input size.
case MimeTypes.VIDEO_H264: case MimeTypes.VIDEO_H264:
if ("BRAVIA 4K 2015".equals(Util.MODEL) // Sony Bravia 4K if ("BRAVIA 4K 2015".equals(Util.MODEL) // Sony Bravia 4K
|| ("Amazon".equals(Util.MANUFACTURER) || ("Amazon".equals(Util.MANUFACTURER)
...@@ -1603,6 +1606,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { ...@@ -1603,6 +1606,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
case "dangalFHD": case "dangalFHD":
case "magnolia": case "magnolia":
case "machuca": case "machuca":
case "once":
case "oneday": case "oneday":
return true; return true;
default: default:
......
...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2; ...@@ -17,6 +17,7 @@ package com.google.android.exoplayer2;
import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runMainLooperUntil; import static com.google.android.exoplayer2.robolectric.RobolectricUtil.runMainLooperUntil;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.playUntilStartOfWindow; import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.playUntilStartOfWindow;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPlaybackState; import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPlaybackState;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilReceiveOffloadSchedulingEnabledNewState; import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilReceiveOffloadSchedulingEnabledNewState;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilSleepingForOffload; import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilSleepingForOffload;
...@@ -1649,55 +1650,44 @@ public final class ExoPlayerTest { ...@@ -1649,55 +1650,44 @@ public final class ExoPlayerTest {
} }
@Test @Test
public void seekAndReprepareAfterPlaybackError() throws Exception { public void seekAndReprepareAfterPlaybackError_keepsSeekPositionAndTimeline() throws Exception {
Timeline timeline = new FakeTimeline(); SimpleExoPlayer player = new TestExoPlayerBuilder(context).build();
final long[] positionHolder = new long[2]; Player.EventListener mockListener = mock(Player.EventListener.class);
ActionSchedule actionSchedule = player.addListener(mockListener);
new ActionSchedule.Builder(TAG) FakeMediaSource fakeMediaSource = new FakeMediaSource();
.pause() player.setMediaSource(fakeMediaSource);
.waitForPlaybackState(Player.STATE_READY)
.throwPlaybackException(ExoPlaybackException.createForSource(new IOException()))
.waitForPlaybackState(Player.STATE_IDLE)
.seek(/* positionMs= */ 50)
.waitForPendingPlayerCommands()
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
positionHolder[0] = player.getCurrentPosition();
}
})
.prepare()
.waitForPlaybackState(Player.STATE_READY)
.executeRunnable(
new PlayerRunnable() {
@Override
public void run(SimpleExoPlayer player) {
positionHolder[1] = player.getCurrentPosition();
}
})
.play()
.build();
ExoPlayerTestRunner testRunner =
new ExoPlayerTestRunner.Builder(context)
.setTimeline(timeline)
.setActionSchedule(actionSchedule)
.build();
assertThrows( player.prepare();
ExoPlaybackException.class, runUntilPlaybackState(player, Player.STATE_READY);
() -> player
testRunner .createMessage(
.start() (type, payload) -> {
.blockUntilActionScheduleFinished(TIMEOUT_MS) throw ExoPlaybackException.createForSource(new IOException());
.blockUntilEnded(TIMEOUT_MS)); })
testRunner.assertTimelinesSame(placeholderTimeline, timeline); .send();
testRunner.assertTimelineChangeReasonsEqual( runUntilPlaybackState(player, Player.STATE_IDLE);
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED, player.seekTo(/* positionMs= */ 50);
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE); runUntilPendingCommandsAreFullyHandled(player);
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK); long positionAfterSeekHandled = player.getCurrentPosition();
assertThat(positionHolder[0]).isEqualTo(50); // Delay re-preparation to force player to use its masking mechanisms.
assertThat(positionHolder[1]).isEqualTo(50); fakeMediaSource.setAllowPreparation(false);
player.prepare();
runUntilPendingCommandsAreFullyHandled(player);
long positionAfterReprepareHandled = player.getCurrentPosition();
fakeMediaSource.setAllowPreparation(true);
runUntilPlaybackState(player, Player.STATE_READY);
long positionWhenFullyReadyAfterReprepare = player.getCurrentPosition();
player.release();
// Ensure we don't receive further timeline updates when repreparing.
verify(mockListener)
.onTimelineChanged(any(), eq(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED));
verify(mockListener).onTimelineChanged(any(), eq(Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE));
verify(mockListener, times(2)).onTimelineChanged(any(), anyInt());
assertThat(positionAfterSeekHandled).isEqualTo(50);
assertThat(positionAfterReprepareHandled).isEqualTo(50);
assertThat(positionWhenFullyReadyAfterReprepare).isEqualTo(50);
} }
@Test @Test
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.analytics; package com.google.android.exoplayer2.analytics;
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
...@@ -42,6 +43,7 @@ import org.junit.Before; ...@@ -42,6 +43,7 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.robolectric.shadows.ShadowLooper;
/** Unit test for {@link PlaybackStatsListener}. */ /** Unit test for {@link PlaybackStatsListener}. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
...@@ -152,6 +154,35 @@ public final class PlaybackStatsListenerTest { ...@@ -152,6 +154,35 @@ public final class PlaybackStatsListenerTest {
} }
@Test @Test
public void playlistClear_callsAllPendingCallbacks() throws Exception {
PlaybackStatsListener.Callback callback = mock(PlaybackStatsListener.Callback.class);
PlaybackStatsListener playbackStatsListener =
new PlaybackStatsListener(/* keepHistory= */ true, callback);
player.addAnalyticsListener(playbackStatsListener);
MediaSource mediaSource = new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1));
player.setMediaSources(ImmutableList.of(mediaSource, mediaSource));
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
// Play close to the end of the first item to ensure the second session is already created, but
// the first one isn't finished yet.
TestPlayerRunHelper.playUntilPosition(
player, /* windowIndex= */ 0, /* positionMs= */ player.getDuration());
runUntilPendingCommandsAreFullyHandled(player);
player.clearMediaItems();
ShadowLooper.idleMainLooper();
ArgumentCaptor<AnalyticsListener.EventTime> eventTimeCaptor =
ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
verify(callback, times(2)).onPlaybackStatsReady(eventTimeCaptor.capture(), any());
assertThat(
eventTimeCaptor.getAllValues().stream()
.map(eventTime -> eventTime.windowIndex)
.collect(Collectors.toList()))
.containsExactly(0, 1);
}
@Test
public void playerRelease_callsAllPendingCallbacks() throws Exception { public void playerRelease_callsAllPendingCallbacks() throws Exception {
PlaybackStatsListener.Callback callback = mock(PlaybackStatsListener.Callback.class); PlaybackStatsListener.Callback callback = mock(PlaybackStatsListener.Callback.class);
PlaybackStatsListener playbackStatsListener = PlaybackStatsListener playbackStatsListener =
......
...@@ -45,6 +45,7 @@ public class CueTest { ...@@ -45,6 +45,7 @@ public class CueTest {
.setSize(0.8f) .setSize(0.8f)
.setWindowColor(Color.CYAN) .setWindowColor(Color.CYAN)
.setVerticalType(Cue.VERTICAL_TYPE_RL) .setVerticalType(Cue.VERTICAL_TYPE_RL)
.setShearDegrees(-15f)
.build(); .build();
Cue modifiedCue = cue.buildUpon().build(); Cue modifiedCue = cue.buildUpon().build();
...@@ -61,6 +62,7 @@ public class CueTest { ...@@ -61,6 +62,7 @@ public class CueTest {
assertThat(cue.windowColor).isEqualTo(Color.CYAN); assertThat(cue.windowColor).isEqualTo(Color.CYAN);
assertThat(cue.windowColorSet).isTrue(); assertThat(cue.windowColorSet).isTrue();
assertThat(cue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_RL); assertThat(cue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_RL);
assertThat(cue.shearDegrees).isEqualTo(-15f);
assertThat(modifiedCue.text).isSameInstanceAs(cue.text); assertThat(modifiedCue.text).isSameInstanceAs(cue.text);
assertThat(modifiedCue.textAlignment).isEqualTo(cue.textAlignment); assertThat(modifiedCue.textAlignment).isEqualTo(cue.textAlignment);
...@@ -74,6 +76,7 @@ public class CueTest { ...@@ -74,6 +76,7 @@ public class CueTest {
assertThat(modifiedCue.windowColor).isEqualTo(cue.windowColor); assertThat(modifiedCue.windowColor).isEqualTo(cue.windowColor);
assertThat(modifiedCue.windowColorSet).isEqualTo(cue.windowColorSet); assertThat(modifiedCue.windowColorSet).isEqualTo(cue.windowColorSet);
assertThat(modifiedCue.verticalType).isEqualTo(cue.verticalType); assertThat(modifiedCue.verticalType).isEqualTo(cue.verticalType);
assertThat(modifiedCue.shearDegrees).isEqualTo(cue.shearDegrees);
} }
@Test @Test
......
...@@ -49,6 +49,7 @@ public final class SsaDecoderTest { ...@@ -49,6 +49,7 @@ public final class SsaDecoderTest {
private static final String POSITIONS_WITHOUT_PLAYRES = "media/ssa/positioning_without_playres"; private static final String POSITIONS_WITHOUT_PLAYRES = "media/ssa/positioning_without_playres";
private static final String STYLE_COLORS = "media/ssa/style_colors"; private static final String STYLE_COLORS = "media/ssa/style_colors";
private static final String STYLE_FONT_SIZE = "media/ssa/style_font_size"; private static final String STYLE_FONT_SIZE = "media/ssa/style_font_size";
private static final String STYLE_BOLD_ITALIC = "media/ssa/style_bold_italic";
@Test @Test
public void decodeEmpty() throws IOException { public void decodeEmpty() throws IOException {
...@@ -336,6 +337,25 @@ public final class SsaDecoderTest { ...@@ -336,6 +337,25 @@ public final class SsaDecoderTest {
assertThat(secondCue.textSizeType).isEqualTo(Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING); assertThat(secondCue.textSizeType).isEqualTo(Cue.TEXT_SIZE_TYPE_FRACTIONAL_IGNORE_PADDING);
} }
@Test
public void decodeBoldItalic() throws IOException {
SsaDecoder decoder = new SsaDecoder();
byte[] bytes =
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), STYLE_BOLD_ITALIC);
Subtitle subtitle = decoder.decode(bytes, bytes.length, false);
assertThat(subtitle.getEventTimeCount()).isEqualTo(6);
Spanned firstCueText =
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(0))).text;
SpannedSubject.assertThat(firstCueText).hasBoldSpanBetween(0, firstCueText.length());
Spanned secondCueText =
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))).text;
SpannedSubject.assertThat(secondCueText).hasItalicSpanBetween(0, secondCueText.length());
Spanned thirdCueText =
(Spanned) Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))).text;
SpannedSubject.assertThat(thirdCueText).hasBoldItalicSpanBetween(0, thirdCueText.length());
}
private static void assertTypicalCue1(Subtitle subtitle, int eventIndex) { private static void assertTypicalCue1(Subtitle subtitle, int eventIndex) {
assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0); assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0);
assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString())
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.text.ttml; package com.google.android.exoplayer2.text.ttml;
import static android.graphics.Color.BLACK; import static android.graphics.Color.BLACK;
import static com.google.android.exoplayer2.text.span.TextAnnotation.POSITION_BEFORE;
import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD; import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD;
import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD_ITALIC; import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_BOLD_ITALIC;
import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_ITALIC; import static com.google.android.exoplayer2.text.ttml.TtmlStyle.STYLE_ITALIC;
...@@ -28,7 +29,8 @@ import android.graphics.Color; ...@@ -28,7 +29,8 @@ import android.graphics.Color;
import android.text.Layout; import android.text.Layout;
import androidx.annotation.ColorInt; import androidx.annotation.ColorInt;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.text.span.RubySpan; import com.google.android.exoplayer2.text.span.TextAnnotation;
import com.google.android.exoplayer2.text.span.TextEmphasisSpan;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
...@@ -43,9 +45,11 @@ public final class TtmlStyleTest { ...@@ -43,9 +45,11 @@ public final class TtmlStyleTest {
@TtmlStyle.FontSizeUnit private static final int FONT_SIZE_UNIT = TtmlStyle.FONT_SIZE_UNIT_EM; @TtmlStyle.FontSizeUnit private static final int FONT_SIZE_UNIT = TtmlStyle.FONT_SIZE_UNIT_EM;
@ColorInt private static final int BACKGROUND_COLOR = Color.BLACK; @ColorInt private static final int BACKGROUND_COLOR = Color.BLACK;
private static final int RUBY_TYPE = TtmlStyle.RUBY_TYPE_TEXT; private static final int RUBY_TYPE = TtmlStyle.RUBY_TYPE_TEXT;
private static final int RUBY_POSITION = RubySpan.POSITION_UNDER; private static final int RUBY_POSITION = TextAnnotation.POSITION_AFTER;
private static final Layout.Alignment TEXT_ALIGN = Layout.Alignment.ALIGN_CENTER; private static final Layout.Alignment TEXT_ALIGN = Layout.Alignment.ALIGN_CENTER;
private static final boolean TEXT_COMBINE = true; private static final boolean TEXT_COMBINE = true;
public static final String TEXT_EMPHASIS_STYLE = "dot before";
public static final float SHEAR_PERCENTAGE = 16f;
private final TtmlStyle populatedStyle = private final TtmlStyle populatedStyle =
new TtmlStyle() new TtmlStyle()
...@@ -62,7 +66,9 @@ public final class TtmlStyleTest { ...@@ -62,7 +66,9 @@ public final class TtmlStyleTest {
.setRubyType(RUBY_TYPE) .setRubyType(RUBY_TYPE)
.setRubyPosition(RUBY_POSITION) .setRubyPosition(RUBY_POSITION)
.setTextAlign(TEXT_ALIGN) .setTextAlign(TEXT_ALIGN)
.setTextCombine(TEXT_COMBINE); .setTextCombine(TEXT_COMBINE)
.setTextEmphasis(TextEmphasis.parse(TEXT_EMPHASIS_STYLE))
.setShearPercentage(SHEAR_PERCENTAGE);
@Test @Test
public void inheritStyle() { public void inheritStyle() {
...@@ -86,6 +92,11 @@ public final class TtmlStyleTest { ...@@ -86,6 +92,11 @@ public final class TtmlStyleTest {
assertWithMessage("backgroundColor should not be inherited") assertWithMessage("backgroundColor should not be inherited")
.that(style.hasBackgroundColor()) .that(style.hasBackgroundColor())
.isFalse(); .isFalse();
assertThat(style.getTextEmphasis()).isNotNull();
assertThat(style.getTextEmphasis().markShape).isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT);
assertThat(style.getTextEmphasis().markFill).isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
assertThat(style.getTextEmphasis().position).isEqualTo(POSITION_BEFORE);
assertThat(style.getShearPercentage()).isEqualTo(SHEAR_PERCENTAGE);
} }
@Test @Test
...@@ -109,6 +120,11 @@ public final class TtmlStyleTest { ...@@ -109,6 +120,11 @@ public final class TtmlStyleTest {
.that(style.getBackgroundColor()) .that(style.getBackgroundColor())
.isEqualTo(BACKGROUND_COLOR); .isEqualTo(BACKGROUND_COLOR);
assertWithMessage("rubyType should be chained").that(style.getRubyType()).isEqualTo(RUBY_TYPE); assertWithMessage("rubyType should be chained").that(style.getRubyType()).isEqualTo(RUBY_TYPE);
assertThat(style.getTextEmphasis()).isNotNull();
assertThat(style.getTextEmphasis().markShape).isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT);
assertThat(style.getTextEmphasis().markFill).isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
assertThat(style.getTextEmphasis().position).isEqualTo(POSITION_BEFORE);
assertThat(style.getShearPercentage()).isEqualTo(SHEAR_PERCENTAGE);
} }
@Test @Test
...@@ -221,9 +237,9 @@ public final class TtmlStyleTest { ...@@ -221,9 +237,9 @@ public final class TtmlStyleTest {
public void rubyPosition() { public void rubyPosition() {
TtmlStyle style = new TtmlStyle(); TtmlStyle style = new TtmlStyle();
assertThat(style.getRubyPosition()).isEqualTo(RubySpan.POSITION_UNKNOWN); assertThat(style.getRubyPosition()).isEqualTo(TextAnnotation.POSITION_UNKNOWN);
style.setRubyPosition(RubySpan.POSITION_OVER); style.setRubyPosition(POSITION_BEFORE);
assertThat(style.getRubyPosition()).isEqualTo(RubySpan.POSITION_OVER); assertThat(style.getRubyPosition()).isEqualTo(POSITION_BEFORE);
} }
@Test @Test
...@@ -245,4 +261,26 @@ public final class TtmlStyleTest { ...@@ -245,4 +261,26 @@ public final class TtmlStyleTest {
style.setTextCombine(true); style.setTextCombine(true);
assertThat(style.getTextCombine()).isTrue(); assertThat(style.getTextCombine()).isTrue();
} }
@Test
public void textEmphasis() {
TtmlStyle style = new TtmlStyle();
assertThat(style.getTextEmphasis()).isNull();
style.setTextEmphasis(TextEmphasis.parse("open sesame after"));
assertThat(style.getTextEmphasis().markShape).isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME);
assertThat(style.getTextEmphasis().markFill).isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN);
assertThat(style.getTextEmphasis().position).isEqualTo(TextAnnotation.POSITION_AFTER);
}
@Test
public void shear() {
TtmlStyle style = new TtmlStyle();
assertThat(style.getShearPercentage()).isEqualTo(TtmlStyle.UNSPECIFIED_SHEAR);
style.setShearPercentage(101f);
assertThat(style.getShearPercentage()).isEqualTo(101f);
style.setShearPercentage(-200f);
assertThat(style.getShearPercentage()).isEqualTo(-200f);
style.setShearPercentage(0.1f);
assertThat(style.getShearPercentage()).isEqualTo(0.1f);
}
} }
...@@ -26,7 +26,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; ...@@ -26,7 +26,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.testutil.TestUtil; import com.google.android.exoplayer2.testutil.TestUtil;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.SubtitleDecoderException; import com.google.android.exoplayer2.text.SubtitleDecoderException;
import com.google.android.exoplayer2.text.span.RubySpan; import com.google.android.exoplayer2.text.span.TextAnnotation;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ColorParser; import com.google.android.exoplayer2.util.ColorParser;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
...@@ -349,7 +349,7 @@ public class WebvttDecoderTest { ...@@ -349,7 +349,7 @@ public class WebvttDecoderTest {
assertThat(firstCue.text.toString()).isEqualTo("Some text with over-ruby."); assertThat(firstCue.text.toString()).isEqualTo("Some text with over-ruby.");
assertThat((Spanned) firstCue.text) assertThat((Spanned) firstCue.text)
.hasRubySpanBetween("Some ".length(), "Some text with over-ruby".length()) .hasRubySpanBetween("Some ".length(), "Some text with over-ruby".length())
.withTextAndPosition("over", RubySpan.POSITION_OVER); .withTextAndPosition("over", TextAnnotation.POSITION_BEFORE);
// Check that `under` is read from CSS and unspecified defaults to `over`. // Check that `under` is read from CSS and unspecified defaults to `over`.
Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2))); Cue secondCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(2)));
...@@ -357,25 +357,25 @@ public class WebvttDecoderTest { ...@@ -357,25 +357,25 @@ public class WebvttDecoderTest {
.isEqualTo("Some text with under-ruby and over-ruby (default)."); .isEqualTo("Some text with under-ruby and over-ruby (default).");
assertThat((Spanned) secondCue.text) assertThat((Spanned) secondCue.text)
.hasRubySpanBetween("Some ".length(), "Some text with under-ruby".length()) .hasRubySpanBetween("Some ".length(), "Some text with under-ruby".length())
.withTextAndPosition("under", RubySpan.POSITION_UNDER); .withTextAndPosition("under", TextAnnotation.POSITION_AFTER);
assertThat((Spanned) secondCue.text) assertThat((Spanned) secondCue.text)
.hasRubySpanBetween( .hasRubySpanBetween(
"Some text with under-ruby and ".length(), "Some text with under-ruby and ".length(),
"Some text with under-ruby and over-ruby (default)".length()) "Some text with under-ruby and over-ruby (default)".length())
.withTextAndPosition("over", RubySpan.POSITION_OVER); .withTextAndPosition("over", TextAnnotation.POSITION_BEFORE);
// Check many <rt> tags with different positions nested in a single <ruby> span. // Check many <rt> tags with different positions nested in a single <ruby> span.
Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4))); Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4)));
assertThat(thirdCue.text.toString()).isEqualTo("base1base2base3."); assertThat(thirdCue.text.toString()).isEqualTo("base1base2base3.");
assertThat((Spanned) thirdCue.text) assertThat((Spanned) thirdCue.text)
.hasRubySpanBetween(/* start= */ 0, "base1".length()) .hasRubySpanBetween(/* start= */ 0, "base1".length())
.withTextAndPosition("over1", RubySpan.POSITION_OVER); .withTextAndPosition("over1", TextAnnotation.POSITION_BEFORE);
assertThat((Spanned) thirdCue.text) assertThat((Spanned) thirdCue.text)
.hasRubySpanBetween("base1".length(), "base1base2".length()) .hasRubySpanBetween("base1".length(), "base1base2".length())
.withTextAndPosition("under2", RubySpan.POSITION_UNDER); .withTextAndPosition("under2", TextAnnotation.POSITION_AFTER);
assertThat((Spanned) thirdCue.text) assertThat((Spanned) thirdCue.text)
.hasRubySpanBetween("base1base2".length(), "base1base2base3".length()) .hasRubySpanBetween("base1base2".length(), "base1base2base3".length())
.withTextAndPosition("under3", RubySpan.POSITION_UNDER); .withTextAndPosition("under3", TextAnnotation.POSITION_AFTER);
// Check a <ruby> span with no <rt> tags. // Check a <ruby> span with no <rt> tags.
Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6))); Cue fourthCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(6)));
......
...@@ -39,6 +39,7 @@ import com.google.android.exoplayer2.util.MimeTypes; ...@@ -39,6 +39,7 @@ import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.UriUtil; import com.google.android.exoplayer2.util.UriUtil;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.util.XmlPullParserUtil; import com.google.android.exoplayer2.util.XmlPullParserUtil;
import com.google.common.base.Ascii;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
...@@ -1390,15 +1391,31 @@ public class DashManifestParser extends DefaultHandler ...@@ -1390,15 +1391,31 @@ public class DashManifestParser extends DefaultHandler
// Selection flag parsing. // Selection flag parsing.
@C.SelectionFlags
protected int parseSelectionFlagsFromRoleDescriptors(List<Descriptor> roleDescriptors) { protected int parseSelectionFlagsFromRoleDescriptors(List<Descriptor> roleDescriptors) {
@C.SelectionFlags int result = 0;
for (int i = 0; i < roleDescriptors.size(); i++) { for (int i = 0; i < roleDescriptors.size(); i++) {
Descriptor descriptor = roleDescriptors.get(i); Descriptor descriptor = roleDescriptors.get(i);
if ("urn:mpeg:dash:role:2011".equalsIgnoreCase(descriptor.schemeIdUri) if (Ascii.equalsIgnoreCase("urn:mpeg:dash:role:2011", descriptor.schemeIdUri)) {
&& "main".equals(descriptor.value)) { result |= parseSelectionFlagsFromDashRoleScheme(descriptor.value);
return C.SELECTION_FLAG_DEFAULT;
} }
} }
return 0; return result;
}
@C.SelectionFlags
protected int parseSelectionFlagsFromDashRoleScheme(@Nullable String value) {
if (value == null) {
return 0;
}
switch (value) {
case "main":
return C.SELECTION_FLAG_DEFAULT;
case "forced_subtitle":
return C.SELECTION_FLAG_FORCED;
default:
return 0;
}
} }
// Role and Accessibility parsing. // Role and Accessibility parsing.
...@@ -1408,8 +1425,8 @@ public class DashManifestParser extends DefaultHandler ...@@ -1408,8 +1425,8 @@ public class DashManifestParser extends DefaultHandler
@C.RoleFlags int result = 0; @C.RoleFlags int result = 0;
for (int i = 0; i < roleDescriptors.size(); i++) { for (int i = 0; i < roleDescriptors.size(); i++) {
Descriptor descriptor = roleDescriptors.get(i); Descriptor descriptor = roleDescriptors.get(i);
if ("urn:mpeg:dash:role:2011".equalsIgnoreCase(descriptor.schemeIdUri)) { if (Ascii.equalsIgnoreCase("urn:mpeg:dash:role:2011", descriptor.schemeIdUri)) {
result |= parseDashRoleSchemeValue(descriptor.value); result |= parseRoleFlagsFromDashRoleScheme(descriptor.value);
} }
} }
return result; return result;
...@@ -1421,10 +1438,10 @@ public class DashManifestParser extends DefaultHandler ...@@ -1421,10 +1438,10 @@ public class DashManifestParser extends DefaultHandler
@C.RoleFlags int result = 0; @C.RoleFlags int result = 0;
for (int i = 0; i < accessibilityDescriptors.size(); i++) { for (int i = 0; i < accessibilityDescriptors.size(); i++) {
Descriptor descriptor = accessibilityDescriptors.get(i); Descriptor descriptor = accessibilityDescriptors.get(i);
if ("urn:mpeg:dash:role:2011".equalsIgnoreCase(descriptor.schemeIdUri)) { if (Ascii.equalsIgnoreCase("urn:mpeg:dash:role:2011", descriptor.schemeIdUri)) {
result |= parseDashRoleSchemeValue(descriptor.value); result |= parseRoleFlagsFromDashRoleScheme(descriptor.value);
} else if ("urn:tva:metadata:cs:AudioPurposeCS:2007" } else if (Ascii.equalsIgnoreCase(
.equalsIgnoreCase(descriptor.schemeIdUri)) { "urn:tva:metadata:cs:AudioPurposeCS:2007", descriptor.schemeIdUri)) {
result |= parseTvaAudioPurposeCsValue(descriptor.value); result |= parseTvaAudioPurposeCsValue(descriptor.value);
} }
} }
...@@ -1436,7 +1453,8 @@ public class DashManifestParser extends DefaultHandler ...@@ -1436,7 +1453,8 @@ public class DashManifestParser extends DefaultHandler
@C.RoleFlags int result = 0; @C.RoleFlags int result = 0;
for (int i = 0; i < accessibilityDescriptors.size(); i++) { for (int i = 0; i < accessibilityDescriptors.size(); i++) {
Descriptor descriptor = accessibilityDescriptors.get(i); Descriptor descriptor = accessibilityDescriptors.get(i);
if ("http://dashif.org/guidelines/trickmode".equalsIgnoreCase(descriptor.schemeIdUri)) { if (Ascii.equalsIgnoreCase(
"http://dashif.org/guidelines/trickmode", descriptor.schemeIdUri)) {
result |= C.ROLE_FLAG_TRICK_PLAY; result |= C.ROLE_FLAG_TRICK_PLAY;
} }
} }
...@@ -1444,7 +1462,7 @@ public class DashManifestParser extends DefaultHandler ...@@ -1444,7 +1462,7 @@ public class DashManifestParser extends DefaultHandler
} }
@C.RoleFlags @C.RoleFlags
protected int parseDashRoleSchemeValue(@Nullable String value) { protected int parseRoleFlagsFromDashRoleScheme(@Nullable String value) {
if (value == null) { if (value == null) {
return 0; return 0;
} }
...@@ -1463,6 +1481,7 @@ public class DashManifestParser extends DefaultHandler ...@@ -1463,6 +1481,7 @@ public class DashManifestParser extends DefaultHandler
return C.ROLE_FLAG_EMERGENCY; return C.ROLE_FLAG_EMERGENCY;
case "caption": case "caption":
return C.ROLE_FLAG_CAPTION; return C.ROLE_FLAG_CAPTION;
case "forced_subtitle":
case "subtitle": case "subtitle":
return C.ROLE_FLAG_SUBTITLE; return C.ROLE_FLAG_SUBTITLE;
case "sign": case "sign":
...@@ -1801,8 +1820,8 @@ public class DashManifestParser extends DefaultHandler ...@@ -1801,8 +1820,8 @@ public class DashManifestParser extends DefaultHandler
List<Descriptor> supplementalProperties) { List<Descriptor> supplementalProperties) {
for (int i = 0; i < supplementalProperties.size(); i++) { for (int i = 0; i < supplementalProperties.size(); i++) {
Descriptor descriptor = supplementalProperties.get(i); Descriptor descriptor = supplementalProperties.get(i);
if ("http://dashif.org/guidelines/last-segment-number" if (Ascii.equalsIgnoreCase(
.equalsIgnoreCase(descriptor.schemeIdUri)) { "http://dashif.org/guidelines/last-segment-number", descriptor.schemeIdUri)) {
return Long.parseLong(descriptor.value); return Long.parseLong(descriptor.value);
} }
} }
......
...@@ -220,18 +220,22 @@ public class DashManifestParserTest { ...@@ -220,18 +220,22 @@ public class DashManifestParserTest {
assertThat(format.containerMimeType).isEqualTo(MimeTypes.APPLICATION_RAWCC); assertThat(format.containerMimeType).isEqualTo(MimeTypes.APPLICATION_RAWCC);
assertThat(format.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_CEA608); assertThat(format.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_CEA608);
assertThat(format.codecs).isEqualTo("cea608"); assertThat(format.codecs).isEqualTo("cea608");
assertThat(format.roleFlags).isEqualTo(C.ROLE_FLAG_SUBTITLE);
assertThat(adaptationSets.get(0).type).isEqualTo(C.TRACK_TYPE_TEXT); assertThat(adaptationSets.get(0).type).isEqualTo(C.TRACK_TYPE_TEXT);
format = adaptationSets.get(1).representations.get(0).format; format = adaptationSets.get(1).representations.get(0).format;
assertThat(format.containerMimeType).isEqualTo(MimeTypes.APPLICATION_MP4); assertThat(format.containerMimeType).isEqualTo(MimeTypes.APPLICATION_MP4);
assertThat(format.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_TTML); assertThat(format.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_TTML);
assertThat(format.codecs).isEqualTo("stpp.ttml.im1t"); assertThat(format.codecs).isEqualTo("stpp.ttml.im1t");
assertThat(format.roleFlags).isEqualTo(C.ROLE_FLAG_SUBTITLE);
assertThat(format.selectionFlags).isEqualTo(C.SELECTION_FLAG_FORCED);
assertThat(adaptationSets.get(1).type).isEqualTo(C.TRACK_TYPE_TEXT); assertThat(adaptationSets.get(1).type).isEqualTo(C.TRACK_TYPE_TEXT);
format = adaptationSets.get(2).representations.get(0).format; format = adaptationSets.get(2).representations.get(0).format;
assertThat(format.containerMimeType).isEqualTo(MimeTypes.APPLICATION_TTML); assertThat(format.containerMimeType).isEqualTo(MimeTypes.APPLICATION_TTML);
assertThat(format.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_TTML); assertThat(format.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_TTML);
assertThat(format.codecs).isNull(); assertThat(format.codecs).isNull();
assertThat(format.roleFlags).isEqualTo(0);
assertThat(adaptationSets.get(2).type).isEqualTo(C.TRACK_TYPE_TEXT); assertThat(adaptationSets.get(2).type).isEqualTo(C.TRACK_TYPE_TEXT);
} }
......
...@@ -60,10 +60,11 @@ public final class JpegExtractor implements Extractor { ...@@ -60,10 +60,11 @@ public final class JpegExtractor implements Extractor {
private static final int STATE_READING_MOTION_PHOTO_VIDEO = 5; private static final int STATE_READING_MOTION_PHOTO_VIDEO = 5;
private static final int STATE_ENDED = 6; private static final int STATE_ENDED = 6;
private static final int JPEG_EXIF_HEADER_LENGTH = 12; private static final int EXIF_ID_CODE_LENGTH = 6;
private static final long EXIF_HEADER = 0x45786966; // Exif private static final long EXIF_HEADER = 0x45786966; // Exif
private static final int MARKER_SOI = 0xFFD8; // Start of image marker private static final int MARKER_SOI = 0xFFD8; // Start of image marker
private static final int MARKER_SOS = 0xFFDA; // Start of scan (image data) marker private static final int MARKER_SOS = 0xFFDA; // Start of scan (image data) marker
private static final int MARKER_APP0 = 0xFFE0; // Application data 0 marker
private static final int MARKER_APP1 = 0xFFE1; // Application data 1 marker private static final int MARKER_APP1 = 0xFFE1; // Application data 1 marker
private static final String HEADER_XMP_APP1 = "http://ns.adobe.com/xap/1.0/"; private static final String HEADER_XMP_APP1 = "http://ns.adobe.com/xap/1.0/";
...@@ -85,21 +86,33 @@ public final class JpegExtractor implements Extractor { ...@@ -85,21 +86,33 @@ public final class JpegExtractor implements Extractor {
@Nullable private MotionPhotoMetadata motionPhotoMetadata; @Nullable private MotionPhotoMetadata motionPhotoMetadata;
private @MonotonicNonNull ExtractorInput lastExtractorInput; private @MonotonicNonNull ExtractorInput lastExtractorInput;
private @MonotonicNonNull StartOffsetExtractorInput mp4ExtractorStartOffsetExtractorInput; private @MonotonicNonNull StartOffsetExtractorInput mp4ExtractorStartOffsetExtractorInput;
private @MonotonicNonNull Mp4Extractor mp4Extractor; @Nullable private Mp4Extractor mp4Extractor;
public JpegExtractor() { public JpegExtractor() {
scratch = new ParsableByteArray(JPEG_EXIF_HEADER_LENGTH); scratch = new ParsableByteArray(EXIF_ID_CODE_LENGTH);
mp4StartPosition = C.POSITION_UNSET; mp4StartPosition = C.POSITION_UNSET;
} }
@Override @Override
public boolean sniff(ExtractorInput input) throws IOException { public boolean sniff(ExtractorInput input) throws IOException {
// See ITU-T.81 (1992) subsection B.1.1.3 and Exif version 2.2 (2002) subsection 4.5.4. // See ITU-T.81 (1992) subsection B.1.1.3 and Exif version 2.2 (2002) subsection 4.5.4.
input.peekFully(scratch.getData(), /* offset= */ 0, JPEG_EXIF_HEADER_LENGTH); if (peekMarker(input) != MARKER_SOI) {
if (scratch.readUnsignedShort() != MARKER_SOI || scratch.readUnsignedShort() != MARKER_APP1) {
return false; return false;
} }
scratch.skipBytes(2); // Unused segment length marker = peekMarker(input);
// Even though JFIF and Exif standards are incompatible in theory, Exif files often contain a
// JFIF APP0 marker segment preceding the Exif APP1 marker segment. Skip the JFIF segment if
// present.
if (marker == MARKER_APP0) {
advancePeekPositionToNextSegment(input);
marker = peekMarker(input);
}
if (marker != MARKER_APP1) {
return false;
}
input.advancePeekPosition(2); // Unused segment length
scratch.reset(/* limit= */ EXIF_ID_CODE_LENGTH);
input.peekFully(scratch.getData(), /* offset= */ 0, EXIF_ID_CODE_LENGTH);
return scratch.readUnsignedInt() == EXIF_HEADER && scratch.readUnsignedShort() == 0; // Exif\0\0 return scratch.readUnsignedInt() == EXIF_HEADER && scratch.readUnsignedShort() == 0; // Exif\0\0
} }
...@@ -152,6 +165,7 @@ public final class JpegExtractor implements Extractor { ...@@ -152,6 +165,7 @@ public final class JpegExtractor implements Extractor {
public void seek(long position, long timeUs) { public void seek(long position, long timeUs) {
if (position == 0) { if (position == 0) {
state = STATE_READING_MARKER; state = STATE_READING_MARKER;
mp4Extractor = null;
} else if (state == STATE_READING_MOTION_PHOTO_VIDEO) { } else if (state == STATE_READING_MOTION_PHOTO_VIDEO) {
checkNotNull(mp4Extractor).seek(position, timeUs); checkNotNull(mp4Extractor).seek(position, timeUs);
} }
...@@ -164,6 +178,19 @@ public final class JpegExtractor implements Extractor { ...@@ -164,6 +178,19 @@ public final class JpegExtractor implements Extractor {
} }
} }
private int peekMarker(ExtractorInput input) throws IOException {
scratch.reset(/* limit= */ 2);
input.peekFully(scratch.getData(), /* offset= */ 0, /* length= */ 2);
return scratch.readUnsignedShort();
}
private void advancePeekPositionToNextSegment(ExtractorInput input) throws IOException {
scratch.reset(/* limit= */ 2);
input.peekFully(scratch.getData(), /* offset= */ 0, /* length= */ 2);
int segmentLength = scratch.readUnsignedShort() - 2;
input.advancePeekPosition(segmentLength);
}
private void readMarker(ExtractorInput input) throws IOException { private void readMarker(ExtractorInput input) throws IOException {
scratch.reset(/* limit= */ 2); scratch.reset(/* limit= */ 2);
input.readFully(scratch.getData(), /* offset= */ 0, /* length= */ 2); input.readFully(scratch.getData(), /* offset= */ 0, /* length= */ 2);
......
...@@ -100,7 +100,9 @@ import org.xmlpull.v1.XmlPullParserFactory; ...@@ -100,7 +100,9 @@ import org.xmlpull.v1.XmlPullParserFactory;
parseMotionPhotoPresentationTimestampUsFromDescription(xpp); parseMotionPhotoPresentationTimestampUsFromDescription(xpp);
containerItems = parseMicroVideoOffsetFromDescription(xpp); containerItems = parseMicroVideoOffsetFromDescription(xpp);
} else if (XmlPullParserUtil.isStartTag(xpp, "Container:Directory")) { } else if (XmlPullParserUtil.isStartTag(xpp, "Container:Directory")) {
containerItems = parseMotionPhotoV1Directory(xpp); containerItems = parseMotionPhotoV1Directory(xpp, "Container", "Item");
} else if (XmlPullParserUtil.isStartTag(xpp, "GContainer:Directory")) {
containerItems = parseMotionPhotoV1Directory(xpp, "GContainer", "GContainerItem");
} }
} while (!XmlPullParserUtil.isEndTag(xpp, "x:xmpmeta")); } while (!XmlPullParserUtil.isEndTag(xpp, "x:xmpmeta"));
if (containerItems.isEmpty()) { if (containerItems.isEmpty()) {
...@@ -154,16 +156,23 @@ import org.xmlpull.v1.XmlPullParserFactory; ...@@ -154,16 +156,23 @@ import org.xmlpull.v1.XmlPullParserFactory;
} }
private static ImmutableList<MotionPhotoDescription.ContainerItem> parseMotionPhotoV1Directory( private static ImmutableList<MotionPhotoDescription.ContainerItem> parseMotionPhotoV1Directory(
XmlPullParser xpp) throws XmlPullParserException, IOException { XmlPullParser xpp, String containerNamespacePrefix, String itemNamespacePrefix)
throws XmlPullParserException, IOException {
ImmutableList.Builder<MotionPhotoDescription.ContainerItem> containerItems = ImmutableList.Builder<MotionPhotoDescription.ContainerItem> containerItems =
ImmutableList.builder(); ImmutableList.builder();
String itemTagName = containerNamespacePrefix + ":Item";
String directoryTagName = containerNamespacePrefix + ":Directory";
do { do {
xpp.next(); xpp.next();
if (XmlPullParserUtil.isStartTag(xpp, "Container:Item")) { if (XmlPullParserUtil.isStartTag(xpp, itemTagName)) {
@Nullable String mime = XmlPullParserUtil.getAttributeValue(xpp, "Item:Mime"); String mimeAttributeName = itemNamespacePrefix + ":Mime";
@Nullable String semantic = XmlPullParserUtil.getAttributeValue(xpp, "Item:Semantic"); String semanticAttributeName = itemNamespacePrefix + ":Semantic";
@Nullable String length = XmlPullParserUtil.getAttributeValue(xpp, "Item:Length"); String lengthAttributeName = itemNamespacePrefix + ":Length";
@Nullable String padding = XmlPullParserUtil.getAttributeValue(xpp, "Item:Padding"); String paddinghAttributeName = itemNamespacePrefix + ":Padding";
@Nullable String mime = XmlPullParserUtil.getAttributeValue(xpp, mimeAttributeName);
@Nullable String semantic = XmlPullParserUtil.getAttributeValue(xpp, semanticAttributeName);
@Nullable String length = XmlPullParserUtil.getAttributeValue(xpp, lengthAttributeName);
@Nullable String padding = XmlPullParserUtil.getAttributeValue(xpp, paddinghAttributeName);
if (mime == null || semantic == null) { if (mime == null || semantic == null) {
// Required values are missing. // Required values are missing.
return ImmutableList.of(); return ImmutableList.of();
...@@ -175,7 +184,7 @@ import org.xmlpull.v1.XmlPullParserFactory; ...@@ -175,7 +184,7 @@ import org.xmlpull.v1.XmlPullParserFactory;
length != null ? Long.parseLong(length) : 0, length != null ? Long.parseLong(length) : 0,
padding != null ? Long.parseLong(padding) : 0)); padding != null ? Long.parseLong(padding) : 0));
} }
} while (!XmlPullParserUtil.isEndTag(xpp, "Container:Directory")); } while (!XmlPullParserUtil.isEndTag(xpp, directoryTagName));
return containerItems.build(); return containerItems.build();
} }
......
...@@ -119,6 +119,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -119,6 +119,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
case STATE_READ_PAYLOAD: case STATE_READ_PAYLOAD:
castNonNull(oggSeeker); castNonNull(oggSeeker);
return readPayload(input, seekPosition); return readPayload(input, seekPosition);
case STATE_END_OF_INPUT:
return C.RESULT_END_OF_INPUT;
default: default:
// Never happens. // Never happens.
throw new IllegalStateException(); throw new IllegalStateException();
......
...@@ -144,8 +144,7 @@ public final class PsExtractor implements Extractor { ...@@ -144,8 +144,7 @@ public final class PsExtractor implements Extractor {
// we have to set the first sample timestamp manually. // we have to set the first sample timestamp manually.
// - If the timestamp adjuster has its timestamp set manually before, and now we seek to a // - If the timestamp adjuster has its timestamp set manually before, and now we seek to a
// different position, we need to set the first sample timestamp manually again. // different position, we need to set the first sample timestamp manually again.
timestampAdjuster.reset(); timestampAdjuster.reset(timeUs);
timestampAdjuster.setFirstSampleTimestampUs(timeUs);
} }
if (psBinarySearchSeeker != null) { if (psBinarySearchSeeker != null) {
......
...@@ -268,8 +268,7 @@ public final class TsExtractor implements Extractor { ...@@ -268,8 +268,7 @@ public final class TsExtractor implements Extractor {
// sample timestamp for that track manually. // sample timestamp for that track manually.
// - If the timestamp adjuster has its timestamp set manually before, and now we seek to a // - If the timestamp adjuster has its timestamp set manually before, and now we seek to a
// different position, we need to set the first sample timestamp manually again. // different position, we need to set the first sample timestamp manually again.
timestampAdjuster.reset(); timestampAdjuster.reset(timeUs);
timestampAdjuster.setFirstSampleTimestampUs(timeUs);
} }
} }
if (timeUs != 0 && tsBinarySearchSeeker != null) { if (timeUs != 0 && tsBinarySearchSeeker != null) {
......
...@@ -46,6 +46,14 @@ public final class JpegExtractorTest { ...@@ -46,6 +46,14 @@ public final class JpegExtractorTest {
} }
@Test @Test
public void samplePixelMotionPhotoJfifSegmentShortened() throws Exception {
ExtractorAsserts.assertBehavior(
JpegExtractor::new,
"media/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg",
simulationConfig);
}
@Test
public void samplePixelMotionPhotoVideoRemovedShortened() throws Exception { public void samplePixelMotionPhotoVideoRemovedShortened() throws Exception {
ExtractorAsserts.assertBehavior( ExtractorAsserts.assertBehavior(
JpegExtractor::new, JpegExtractor::new,
......
...@@ -16,11 +16,15 @@ ...@@ -16,11 +16,15 @@
package com.google.android.exoplayer2.extractor.ogg; package com.google.android.exoplayer2.extractor.ogg;
import static com.google.android.exoplayer2.testutil.TestUtil.getByteArray; import static com.google.android.exoplayer2.testutil.TestUtil.getByteArray;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.PositionHolder;
import com.google.android.exoplayer2.testutil.ExtractorAsserts; import com.google.android.exoplayer2.testutil.ExtractorAsserts;
import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput;
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
import java.io.IOException; import java.io.IOException;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
...@@ -35,6 +39,19 @@ import org.junit.runner.RunWith; ...@@ -35,6 +39,19 @@ import org.junit.runner.RunWith;
public final class OggExtractorNonParameterizedTest { public final class OggExtractorNonParameterizedTest {
@Test @Test
public void read_afterEndOfInput_doesNotThrowIllegalState() throws Exception {
byte[] data =
getByteArray(ApplicationProvider.getApplicationContext(), "media/ogg/bear_flac.ogg");
FakeExtractorInput input = new FakeExtractorInput.Builder().setData(data).build();
OggExtractor oggExtractor = new OggExtractor();
oggExtractor.init(new FakeExtractorOutput());
// We feed data to the extractor until the end of input is reached.
while (oggExtractor.read(input, new PositionHolder()) != C.RESULT_END_OF_INPUT) {}
// We call read again to check that it does not throw an IllegalStateException.
assertThat(oggExtractor.read(input, new PositionHolder())).isEqualTo(C.RESULT_END_OF_INPUT);
}
@Test
public void sniffVorbis() throws Exception { public void sniffVorbis() throws Exception {
byte[] data = byte[] data =
getByteArray(ApplicationProvider.getApplicationContext(), "media/ogg/vorbis_header"); getByteArray(ApplicationProvider.getApplicationContext(), "media/ogg/vorbis_header");
......
...@@ -35,6 +35,7 @@ import com.google.android.exoplayer2.metadata.Metadata; ...@@ -35,6 +35,7 @@ import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.util.FileTypes; import com.google.android.exoplayer2.util.FileTypes;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.TimestampAdjuster;
import com.google.common.primitives.Ints;
import java.io.EOFException; import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
...@@ -107,11 +108,11 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { ...@@ -107,11 +108,11 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
// Defines the order in which to try the extractors. // Defines the order in which to try the extractors.
List<Integer> fileTypeOrder = List<Integer> fileTypeOrder =
new ArrayList<>(/* initialCapacity= */ DEFAULT_EXTRACTOR_ORDER.length); new ArrayList<>(/* initialCapacity= */ DEFAULT_EXTRACTOR_ORDER.length);
addFileTypeIfNotPresent(formatInferredFileType, fileTypeOrder); addFileTypeIfValidAndNotPresent(formatInferredFileType, fileTypeOrder);
addFileTypeIfNotPresent(responseHeadersInferredFileType, fileTypeOrder); addFileTypeIfValidAndNotPresent(responseHeadersInferredFileType, fileTypeOrder);
addFileTypeIfNotPresent(uriInferredFileType, fileTypeOrder); addFileTypeIfValidAndNotPresent(uriInferredFileType, fileTypeOrder);
for (int fileType : DEFAULT_EXTRACTOR_ORDER) { for (int fileType : DEFAULT_EXTRACTOR_ORDER) {
addFileTypeIfNotPresent(fileType, fileTypeOrder); addFileTypeIfValidAndNotPresent(fileType, fileTypeOrder);
} }
// Extractor to be used if the type is not recognized. // Extractor to be used if the type is not recognized.
...@@ -125,9 +126,13 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { ...@@ -125,9 +126,13 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
if (sniffQuietly(extractor, extractorInput)) { if (sniffQuietly(extractor, extractorInput)) {
return new BundledHlsMediaChunkExtractor(extractor, format, timestampAdjuster); return new BundledHlsMediaChunkExtractor(extractor, format, timestampAdjuster);
} }
if (fileType == FileTypes.TS) { if (fallBackExtractor == null
// Fall back on TsExtractor to handle TS streams with an EXT-X-MAP tag. See && (fileType == formatInferredFileType
// https://github.com/google/ExoPlayer/issues/8219. || fileType == responseHeadersInferredFileType
|| fileType == uriInferredFileType
|| fileType == FileTypes.TS)) {
// If sniffing fails, fallback to the file types inferred from context. If all else fails,
// fallback to Transport Stream. See https://github.com/google/ExoPlayer/issues/8219.
fallBackExtractor = extractor; fallBackExtractor = extractor;
} }
} }
...@@ -136,9 +141,9 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory { ...@@ -136,9 +141,9 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
checkNotNull(fallBackExtractor), format, timestampAdjuster); checkNotNull(fallBackExtractor), format, timestampAdjuster);
} }
private static void addFileTypeIfNotPresent( private static void addFileTypeIfValidAndNotPresent(
@FileTypes.Type int fileType, List<Integer> fileTypes) { @FileTypes.Type int fileType, List<Integer> fileTypes) {
if (fileType == FileTypes.UNKNOWN || fileTypes.contains(fileType)) { if (Ints.indexOf(DEFAULT_EXTRACTOR_ORDER, fileType) == -1 || fileTypes.contains(fileType)) {
return; return;
} }
fileTypes.add(fileType); fileTypes.add(fileType);
......
...@@ -391,15 +391,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -391,15 +391,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@RequiresNonNull("output") @RequiresNonNull("output")
private void loadMedia() throws IOException { private void loadMedia() throws IOException {
if (!isMasterTimestampSource) { try {
try { timestampAdjuster.sharedInitializeOrWait(isMasterTimestampSource, startTimeUs);
timestampAdjuster.waitUntilInitialized(); } catch (InterruptedException e) {
} catch (InterruptedException e) { throw new InterruptedIOException();
throw new InterruptedIOException();
}
} else if (timestampAdjuster.getFirstSampleTimestampUs() == TimestampAdjuster.DO_NOT_OFFSET) {
// We're the master and we haven't set the desired first sample timestamp yet.
timestampAdjuster.setFirstSampleTimestampUs(startTimeUs);
} }
feedDataToExtractor(dataSource, dataSpec, mediaSegmentEncrypted); feedDataToExtractor(dataSource, dataSpec, mediaSegmentEncrypted);
} }
......
...@@ -88,6 +88,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper ...@@ -88,6 +88,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
private HlsSampleStreamWrapper[] enabledSampleStreamWrappers; private HlsSampleStreamWrapper[] enabledSampleStreamWrappers;
// Maps sample stream wrappers to variant/rendition index by matching array positions. // Maps sample stream wrappers to variant/rendition index by matching array positions.
private int[][] manifestUrlIndicesPerWrapper; private int[][] manifestUrlIndicesPerWrapper;
private int audioVideoSampleStreamWrapperCount;
private SequenceableLoader compositeSequenceableLoader; private SequenceableLoader compositeSequenceableLoader;
/** /**
...@@ -315,8 +316,9 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper ...@@ -315,8 +316,9 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
if (wrapperEnabled) { if (wrapperEnabled) {
newEnabledSampleStreamWrappers[newEnabledSampleStreamWrapperCount] = sampleStreamWrapper; newEnabledSampleStreamWrappers[newEnabledSampleStreamWrapperCount] = sampleStreamWrapper;
if (newEnabledSampleStreamWrapperCount++ == 0) { if (newEnabledSampleStreamWrapperCount++ == 0) {
// The first enabled wrapper is responsible for initializing timestamp adjusters. This // The first enabled wrapper is always allowed to initialize timestamp adjusters. Note
// way, if enabled, variants are responsible. Else audio renditions. Else text renditions. // that the first wrapper will correspond to a variant, or else an audio rendition, or
// else a text rendition, in that order.
sampleStreamWrapper.setIsTimestampMaster(true); sampleStreamWrapper.setIsTimestampMaster(true);
if (wasReset || enabledSampleStreamWrappers.length == 0 if (wasReset || enabledSampleStreamWrappers.length == 0
|| sampleStreamWrapper != enabledSampleStreamWrappers[0]) { || sampleStreamWrapper != enabledSampleStreamWrappers[0]) {
...@@ -326,7 +328,11 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper ...@@ -326,7 +328,11 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
forceReset = true; forceReset = true;
} }
} else { } else {
sampleStreamWrapper.setIsTimestampMaster(false); // Additional wrappers are also allowed to initialize timestamp adjusters if they contain
// audio or video, since they are expected to contain dense samples. Text wrappers are not
// permitted except in the case above in which no variant or audio rendition wrappers are
// enabled.
sampleStreamWrapper.setIsTimestampMaster(i < audioVideoSampleStreamWrapperCount);
} }
} }
} }
...@@ -496,6 +502,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper ...@@ -496,6 +502,8 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
manifestUrlIndicesPerWrapper, manifestUrlIndicesPerWrapper,
overridingDrmInitData); overridingDrmInitData);
audioVideoSampleStreamWrapperCount = sampleStreamWrappers.size();
// Subtitle stream wrappers. We can always use master playlist information to prepare these. // Subtitle stream wrappers. We can always use master playlist information to prepare these.
for (int i = 0; i < subtitleRenditions.size(); i++) { for (int i = 0; i < subtitleRenditions.size(); i++) {
Rendition subtitleRendition = subtitleRenditions.get(i); Rendition subtitleRendition = subtitleRenditions.get(i);
......
...@@ -616,7 +616,9 @@ public final class HlsMediaSource extends BaseMediaSource ...@@ -616,7 +616,9 @@ public final class HlsMediaSource extends BaseMediaSource
HlsMediaPlaylist.ServerControl serverControl = playlist.serverControl; HlsMediaPlaylist.ServerControl serverControl = playlist.serverControl;
// Select part hold back only if the playlist has a part target duration. // Select part hold back only if the playlist has a part target duration.
long offsetToEndOfPlaylistUs; long offsetToEndOfPlaylistUs;
if (serverControl.partHoldBackUs != C.TIME_UNSET if (playlist.startOffsetUs != C.TIME_UNSET) {
offsetToEndOfPlaylistUs = playlist.durationUs - playlist.startOffsetUs;
} else if (serverControl.partHoldBackUs != C.TIME_UNSET
&& playlist.partTargetDurationUs != C.TIME_UNSET) { && playlist.partTargetDurationUs != C.TIME_UNSET) {
offsetToEndOfPlaylistUs = serverControl.partHoldBackUs; offsetToEndOfPlaylistUs = serverControl.partHoldBackUs;
} else if (serverControl.holdBackUs != C.TIME_UNSET) { } else if (serverControl.holdBackUs != C.TIME_UNSET) {
......
...@@ -1070,6 +1070,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; ...@@ -1070,6 +1070,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
drmSessionManager, drmSessionManager,
drmEventDispatcher, drmEventDispatcher,
overridingDrmInitData); overridingDrmInitData);
sampleQueue.setStartTimeUs(lastSeekPositionUs);
if (isAudioVideo) { if (isAudioVideo) {
sampleQueue.setDrmInitData(drmInitData); sampleQueue.setDrmInitData(drmInitData);
} }
......
...@@ -702,6 +702,10 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -702,6 +702,10 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
segmentByteRangeOffset = Long.parseLong(splitByteRange[1]); segmentByteRangeOffset = Long.parseLong(splitByteRange[1]);
} }
} }
if (segmentByteRangeLength == C.LENGTH_UNSET) {
// The segment has no byte range defined.
segmentByteRangeOffset = 0;
}
if (fullSegmentEncryptionKeyUri != null && fullSegmentEncryptionIV == null) { if (fullSegmentEncryptionKeyUri != null && fullSegmentEncryptionIV == null) {
// See RFC 8216, Section 4.3.2.5. // See RFC 8216, Section 4.3.2.5.
throw new ParserException( throw new ParserException(
...@@ -715,7 +719,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -715,7 +719,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
segmentByteRangeLength, segmentByteRangeLength,
fullSegmentEncryptionKeyUri, fullSegmentEncryptionKeyUri,
fullSegmentEncryptionIV); fullSegmentEncryptionIV);
segmentByteRangeOffset = 0; if (segmentByteRangeLength != C.LENGTH_UNSET) {
segmentByteRangeOffset += segmentByteRangeLength;
}
segmentByteRangeLength = C.LENGTH_UNSET; segmentByteRangeLength = C.LENGTH_UNSET;
} else if (line.startsWith(TAG_TARGET_DURATION)) { } else if (line.startsWith(TAG_TARGET_DURATION)) {
targetDurationUs = parseIntAttr(line, REGEX_TARGET_DURATION) * C.MICROS_PER_SECOND; targetDurationUs = parseIntAttr(line, REGEX_TARGET_DURATION) * C.MICROS_PER_SECOND;
...@@ -948,7 +954,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli ...@@ -948,7 +954,7 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
String segmentUri = replaceVariableReferences(line, variableDefinitions); String segmentUri = replaceVariableReferences(line, variableDefinitions);
@Nullable Segment inferredInitSegment = urlToInferredInitSegment.get(segmentUri); @Nullable Segment inferredInitSegment = urlToInferredInitSegment.get(segmentUri);
if (segmentByteRangeLength == C.LENGTH_UNSET) { if (segmentByteRangeLength == C.LENGTH_UNSET) {
// The segment is not byte range defined. // The segment has no byte range defined.
segmentByteRangeOffset = 0; segmentByteRangeOffset = 0;
} else if (isIFrameOnly && initializationSegment == null && inferredInitSegment == null) { } else if (isIFrameOnly && initializationSegment == null && inferredInitSegment == null) {
// The segment is a resource byte range without an initialization segment. // The segment is a resource byte range without an initialization segment.
......
...@@ -24,12 +24,15 @@ import com.google.android.exoplayer2.Format; ...@@ -24,12 +24,15 @@ import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.ExtractorInput; import com.google.android.exoplayer2.extractor.ExtractorInput;
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor; import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor;
import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor;
import com.google.android.exoplayer2.extractor.ts.Ac3Extractor; import com.google.android.exoplayer2.extractor.ts.Ac3Extractor;
import com.google.android.exoplayer2.extractor.ts.TsExtractor; import com.google.android.exoplayer2.extractor.ts.TsExtractor;
import com.google.android.exoplayer2.testutil.FakeExtractorInput; import com.google.android.exoplayer2.testutil.FakeExtractorInput;
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;
import com.google.android.exoplayer2.util.TimestampAdjuster; import com.google.android.exoplayer2.util.TimestampAdjuster;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
...@@ -42,14 +45,16 @@ import org.junit.runner.RunWith; ...@@ -42,14 +45,16 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class DefaultHlsExtractorFactoryTest { public class DefaultHlsExtractorFactoryTest {
private Uri tsUri; private static final Uri URI_WITH_JPEG_EXTENSION = Uri.parse("http://path/filename.jpg");
private static final Uri URI_WITH_MP4_EXTENSION = Uri.parse("http://path/filename.mp4");
private static final Uri URI_WITH_TS_EXTENSION = Uri.parse("http://path/filename.ts");
private Format webVttFormat; private Format webVttFormat;
private TimestampAdjuster timestampAdjuster; private TimestampAdjuster timestampAdjuster;
private Map<String, List<String>> ac3ResponseHeaders; private Map<String, List<String>> ac3ResponseHeaders;
@Before @Before
public void setUp() { public void setUp() {
tsUri = Uri.parse("http://path/filename.ts");
webVttFormat = new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build(); webVttFormat = new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build();
timestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0); timestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0);
ac3ResponseHeaders = new HashMap<>(); ac3ResponseHeaders = new HashMap<>();
...@@ -69,7 +74,7 @@ public class DefaultHlsExtractorFactoryTest { ...@@ -69,7 +74,7 @@ public class DefaultHlsExtractorFactoryTest {
BundledHlsMediaChunkExtractor result = BundledHlsMediaChunkExtractor result =
new DefaultHlsExtractorFactory() new DefaultHlsExtractorFactory()
.createExtractor( .createExtractor(
tsUri, URI_WITH_TS_EXTENSION,
webVttFormat, webVttFormat,
/* muxedCaptionFormats= */ null, /* muxedCaptionFormats= */ null,
timestampAdjuster, timestampAdjuster,
...@@ -93,7 +98,7 @@ public class DefaultHlsExtractorFactoryTest { ...@@ -93,7 +98,7 @@ public class DefaultHlsExtractorFactoryTest {
BundledHlsMediaChunkExtractor result = BundledHlsMediaChunkExtractor result =
new DefaultHlsExtractorFactory() new DefaultHlsExtractorFactory()
.createExtractor( .createExtractor(
tsUri, URI_WITH_TS_EXTENSION,
webVttFormat, webVttFormat,
/* muxedCaptionFormats= */ null, /* muxedCaptionFormats= */ null,
timestampAdjuster, timestampAdjuster,
...@@ -115,7 +120,7 @@ public class DefaultHlsExtractorFactoryTest { ...@@ -115,7 +120,7 @@ public class DefaultHlsExtractorFactoryTest {
BundledHlsMediaChunkExtractor result = BundledHlsMediaChunkExtractor result =
new DefaultHlsExtractorFactory() new DefaultHlsExtractorFactory()
.createExtractor( .createExtractor(
tsUri, URI_WITH_TS_EXTENSION,
webVttFormat, webVttFormat,
/* muxedCaptionFormats= */ null, /* muxedCaptionFormats= */ null,
timestampAdjuster, timestampAdjuster,
...@@ -138,7 +143,7 @@ public class DefaultHlsExtractorFactoryTest { ...@@ -138,7 +143,7 @@ public class DefaultHlsExtractorFactoryTest {
BundledHlsMediaChunkExtractor result = BundledHlsMediaChunkExtractor result =
new DefaultHlsExtractorFactory() new DefaultHlsExtractorFactory()
.createExtractor( .createExtractor(
tsUri, URI_WITH_TS_EXTENSION,
webVttFormat, webVttFormat,
/* muxedCaptionFormats= */ null, /* muxedCaptionFormats= */ null,
timestampAdjuster, timestampAdjuster,
...@@ -149,19 +154,97 @@ public class DefaultHlsExtractorFactoryTest { ...@@ -149,19 +154,97 @@ public class DefaultHlsExtractorFactoryTest {
} }
@Test @Test
public void createExtractor_withNoMatchingExtractor_fallsBackOnTsExtractor() throws Exception { public void createExtractor_withInvalidFileTypeInUri_returnsSniffedType() throws Exception {
ExtractorInput tsExtractorInput =
new FakeExtractorInput.Builder()
.setData(
TestUtil.getByteArray(
ApplicationProvider.getApplicationContext(), "media/ts/sample_ac3.ts"))
.build();
BundledHlsMediaChunkExtractor result =
new DefaultHlsExtractorFactory()
.createExtractor(
URI_WITH_JPEG_EXTENSION,
webVttFormat,
/* muxedCaptionFormats= */ null,
timestampAdjuster,
ImmutableMap.of("Content-Type", ImmutableList.of(MimeTypes.IMAGE_JPEG)),
tsExtractorInput);
assertThat(result.extractor.getClass()).isEqualTo(TsExtractor.class);
}
@Test
public void createExtractor_onFailedSniff_fallsBackOnFormatInferred() throws Exception {
ExtractorInput emptyExtractorInput = new FakeExtractorInput.Builder().build(); ExtractorInput emptyExtractorInput = new FakeExtractorInput.Builder().build();
BundledHlsMediaChunkExtractor result = BundledHlsMediaChunkExtractor result =
new DefaultHlsExtractorFactory() new DefaultHlsExtractorFactory()
.createExtractor( .createExtractor(
tsUri, URI_WITH_MP4_EXTENSION,
webVttFormat, webVttFormat,
/* muxedCaptionFormats= */ null, /* muxedCaptionFormats= */ null,
timestampAdjuster, timestampAdjuster,
ac3ResponseHeaders, ac3ResponseHeaders,
emptyExtractorInput); emptyExtractorInput);
// The format indicates WebVTT so we expect a WebVTT extractor.
assertThat(result.extractor.getClass()).isEqualTo(WebvttExtractor.class);
}
@Test
public void createExtractor_onFailedSniff_fallsBackOnHttpContentType() throws Exception {
ExtractorInput emptyExtractorInput = new FakeExtractorInput.Builder().build();
BundledHlsMediaChunkExtractor result =
new DefaultHlsExtractorFactory()
.createExtractor(
URI_WITH_MP4_EXTENSION,
new Format.Builder().build(),
/* muxedCaptionFormats= */ null,
timestampAdjuster,
ac3ResponseHeaders,
emptyExtractorInput);
// No format info, so we expect an AC-3 Extractor, as per HTTP Content-Type header.
assertThat(result.extractor.getClass()).isEqualTo(Ac3Extractor.class);
}
@Test
public void createExtractor_onFailedSniff_fallsBackOnFileExtension() throws Exception {
ExtractorInput emptyExtractorInput = new FakeExtractorInput.Builder().build();
BundledHlsMediaChunkExtractor result =
new DefaultHlsExtractorFactory()
.createExtractor(
URI_WITH_MP4_EXTENSION,
new Format.Builder().build(),
/* muxedCaptionFormats= */ null,
timestampAdjuster,
/* responseHeaders= */ ImmutableMap.of(),
emptyExtractorInput);
// No format info, and no HTTP headers, so we expect an fMP4 extractor, as per file extension.
assertThat(result.extractor.getClass()).isEqualTo(FragmentedMp4Extractor.class);
}
@Test
public void createExtractor_onFailedSniff_fallsBackOnTsExtractor() throws Exception {
ExtractorInput emptyExtractorInput = new FakeExtractorInput.Builder().build();
BundledHlsMediaChunkExtractor result =
new DefaultHlsExtractorFactory()
.createExtractor(
Uri.parse("http://path/no_extension"),
new Format.Builder().build(),
/* muxedCaptionFormats= */ null,
timestampAdjuster,
/* responseHeaders= */ ImmutableMap.of(),
emptyExtractorInput);
// There's no information for inferring the file type, we expect the factory to fall back on
// Transport Stream.
assertThat(result.extractor.getClass()).isEqualTo(TsExtractor.class); assertThat(result.extractor.getClass()).isEqualTo(TsExtractor.class);
} }
} }
...@@ -293,6 +293,44 @@ public class HlsMediaSourceTest { ...@@ -293,6 +293,44 @@ public class HlsMediaSourceTest {
} }
@Test @Test
public void loadPlaylist_withPlaylistStartTime_targetLiveOffsetFromStartTime()
throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
// The playlist has a duration of 16 seconds, and part hold back, hold back and start time
// defined.
String playlist =
"#EXTM3U\n"
+ "#EXT-X-PROGRAM-DATE-TIME:2020-01-01T00:00:00.0+00:00\n"
+ "#EXT-X-TARGETDURATION:4\n"
+ "#EXT-X-VERSION:3\n"
+ "#EXT-X-START:TIME-OFFSET=-15"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence0.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence1.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence2.ts\n"
+ "#EXTINF:4.00000,\n"
+ "fileSequence3.ts\n"
+ "#EXT-X-PART-INF:PART-TARGET=0.5\n"
+ "#EXT-X-SERVER-CONTROL:HOLD-BACK=12,PART-HOLD-BACK=3";
// The playlist finishes 1 second before the current time.
SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:17.0+00:00"));
HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist);
MediaItem mediaItem = MediaItem.fromUri(playlistUri);
HlsMediaSource mediaSource = factory.createMediaSource(mediaItem);
Timeline timeline = prepareAndWaitForTimeline(mediaSource);
Timeline.Window window = timeline.getWindow(0, new Timeline.Window());
// The target live offset is picked from start time and then expressed in relation to the live
// edge (+1 seconds).
assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(16000);
assertThat(window.defaultPositionUs).isEqualTo(0);
}
@Test
public void loadPlaylist_targetLiveOffsetInMediaItem_targetLiveOffsetPickedFromMediaItem() public void loadPlaylist_targetLiveOffsetInMediaItem_targetLiveOffsetPickedFromMediaItem()
throws TimeoutException, ParserException { throws TimeoutException, ParserException {
String playlistUri = "fake://foo.bar/media0/playlist.m3u8"; String playlistUri = "fake://foo.bar/media0/playlist.m3u8";
......
...@@ -155,6 +155,52 @@ public class HlsMediaPlaylistParserTest { ...@@ -155,6 +155,52 @@ public class HlsMediaPlaylistParserTest {
} }
@Test @Test
public void parseMediaPlaylist_withByteRanges() throws Exception {
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
String playlistString =
"#EXTM3U\n"
+ "#EXT-X-VERSION:3\n"
+ "#EXT-X-TARGETDURATION:5\n"
+ "\n"
+ "#EXT-X-BYTERANGE:200@100\n"
+ "#EXT-X-MAP:URI=\"stream.mp4\"\n"
+ "#EXTINF:5,\n"
+ "#EXT-X-BYTERANGE:400\n"
+ "stream.mp4\n"
+ "#EXTINF:5,\n"
+ "#EXT-X-BYTERANGE:500\n"
+ "stream.mp4\n"
+ "#EXT-X-DISCONTINUITY\n"
+ "#EXT-X-MAP:URI=\"init.mp4\"\n"
+ "#EXTINF:5,\n"
+ "segment.mp4\n";
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(playlistString));
HlsPlaylist playlist = new HlsPlaylistParser().parse(playlistUri, inputStream);
HlsMediaPlaylist mediaPlaylist = (HlsMediaPlaylist) playlist;
List<Segment> segments = mediaPlaylist.segments;
assertThat(segments).isNotNull();
assertThat(segments).hasSize(3);
Segment segment = segments.get(0);
assertThat(segment.initializationSegment.byteRangeOffset).isEqualTo(100);
assertThat(segment.initializationSegment.byteRangeLength).isEqualTo(200);
assertThat(segment.byteRangeOffset).isEqualTo(300);
assertThat(segment.byteRangeLength).isEqualTo(400);
segment = segments.get(1);
assertThat(segment.byteRangeOffset).isEqualTo(700);
assertThat(segment.byteRangeLength).isEqualTo(500);
segment = segments.get(2);
assertThat(segment.initializationSegment.byteRangeOffset).isEqualTo(0);
assertThat(segment.initializationSegment.byteRangeLength).isEqualTo(C.LENGTH_UNSET);
assertThat(segment.byteRangeOffset).isEqualTo(0);
assertThat(segment.byteRangeLength).isEqualTo(C.LENGTH_UNSET);
}
@Test
public void parseSampleAesMethod() throws Exception { public void parseSampleAesMethod() throws Exception {
Uri playlistUri = Uri.parse("https://example.com/test.m3u8"); Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
String playlistString = String playlistString =
......
...@@ -31,6 +31,8 @@ import android.util.SparseArray; ...@@ -31,6 +31,8 @@ import android.util.SparseArray;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
import com.google.android.exoplayer2.text.span.RubySpan; import com.google.android.exoplayer2.text.span.RubySpan;
import com.google.android.exoplayer2.text.span.TextAnnotation;
import com.google.android.exoplayer2.text.span.TextEmphasisSpan;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
...@@ -186,17 +188,26 @@ import java.util.regex.Pattern; ...@@ -186,17 +188,26 @@ import java.util.regex.Pattern;
} else if (span instanceof RubySpan) { } else if (span instanceof RubySpan) {
RubySpan rubySpan = (RubySpan) span; RubySpan rubySpan = (RubySpan) span;
switch (rubySpan.position) { switch (rubySpan.position) {
case RubySpan.POSITION_OVER: case TextAnnotation.POSITION_BEFORE:
return "<ruby style='ruby-position:over;'>"; return "<ruby style='ruby-position:over;'>";
case RubySpan.POSITION_UNDER: case TextAnnotation.POSITION_AFTER:
return "<ruby style='ruby-position:under;'>"; return "<ruby style='ruby-position:under;'>";
case RubySpan.POSITION_UNKNOWN: case TextAnnotation.POSITION_UNKNOWN:
return "<ruby style='ruby-position:unset;'>"; return "<ruby style='ruby-position:unset;'>";
default: default:
return null; return null;
} }
} else if (span instanceof UnderlineSpan) { } else if (span instanceof UnderlineSpan) {
return "<u>"; return "<u>";
} else if (span instanceof TextEmphasisSpan) {
TextEmphasisSpan textEmphasisSpan = (TextEmphasisSpan) span;
String style = getTextEmphasisStyle(textEmphasisSpan.markShape, textEmphasisSpan.markFill);
String position = getTextEmphasisPosition(textEmphasisSpan.position);
return Util.formatInvariant(
"<span style='-webkit-text-emphasis-style:%1$s;text-emphasis-style:%1$s;"
+ "-webkit-text-emphasis-position:%2$s;text-emphasis-position:%2$s;"
+ "display:inline-block;'>",
style, position);
} else { } else {
return null; return null;
} }
...@@ -209,7 +220,8 @@ import java.util.regex.Pattern; ...@@ -209,7 +220,8 @@ import java.util.regex.Pattern;
|| span instanceof BackgroundColorSpan || span instanceof BackgroundColorSpan
|| span instanceof HorizontalTextInVerticalContextSpan || span instanceof HorizontalTextInVerticalContextSpan
|| span instanceof AbsoluteSizeSpan || span instanceof AbsoluteSizeSpan
|| span instanceof RelativeSizeSpan) { || span instanceof RelativeSizeSpan
|| span instanceof TextEmphasisSpan) {
return "</span>"; return "</span>";
} else if (span instanceof TypefaceSpan) { } else if (span instanceof TypefaceSpan) {
@Nullable String fontFamily = ((TypefaceSpan) span).getFamily(); @Nullable String fontFamily = ((TypefaceSpan) span).getFamily();
...@@ -232,6 +244,52 @@ import java.util.regex.Pattern; ...@@ -232,6 +244,52 @@ import java.util.regex.Pattern;
return null; return null;
} }
private static String getTextEmphasisStyle(
@TextEmphasisSpan.MarkShape int shape, @TextEmphasisSpan.MarkFill int fill) {
StringBuilder builder = new StringBuilder();
switch (fill) {
case TextEmphasisSpan.MARK_FILL_FILLED:
builder.append("filled ");
break;
case TextEmphasisSpan.MARK_FILL_OPEN:
builder.append("open ");
break;
case TextEmphasisSpan.MARK_FILL_UNKNOWN:
default:
break;
}
switch (shape) {
case TextEmphasisSpan.MARK_SHAPE_CIRCLE:
builder.append("circle");
break;
case TextEmphasisSpan.MARK_SHAPE_DOT:
builder.append("dot");
break;
case TextEmphasisSpan.MARK_SHAPE_SESAME:
builder.append("sesame");
break;
case TextEmphasisSpan.MARK_SHAPE_NONE:
builder.append("none");
break;
default:
builder.append("unset");
break;
}
return builder.toString();
}
private static String getTextEmphasisPosition(@TextAnnotation.Position int position) {
switch (position) {
case TextAnnotation.POSITION_AFTER:
return "under left";
case TextAnnotation.POSITION_UNKNOWN:
case TextAnnotation.POSITION_BEFORE:
default:
return "over right";
}
}
private static Transition getOrCreate(SparseArray<Transition> transitions, int key) { private static Transition getOrCreate(SparseArray<Transition> transitions, int key) {
@Nullable Transition transition = transitions.get(key); @Nullable Transition transition = transitions.get(key);
if (transition == null) { if (transition == null) {
......
...@@ -50,6 +50,7 @@ import java.util.List; ...@@ -50,6 +50,7 @@ import java.util.List;
private final StyledPlayerControlView styledPlayerControlView; private final StyledPlayerControlView styledPlayerControlView;
@Nullable private final View controlsBackground;
@Nullable private final ViewGroup centerControls; @Nullable private final ViewGroup centerControls;
@Nullable private final ViewGroup bottomBar; @Nullable private final ViewGroup bottomBar;
@Nullable private final ViewGroup minimalControls; @Nullable private final ViewGroup minimalControls;
...@@ -99,7 +100,7 @@ import java.util.List; ...@@ -99,7 +100,7 @@ import java.util.List;
shownButtons = new ArrayList<>(); shownButtons = new ArrayList<>();
// Relating to Center View // Relating to Center View
View controlsBackground = styledPlayerControlView.findViewById(R.id.exo_controls_background); controlsBackground = styledPlayerControlView.findViewById(R.id.exo_controls_background);
centerControls = styledPlayerControlView.findViewById(R.id.exo_center_controls); centerControls = styledPlayerControlView.findViewById(R.id.exo_center_controls);
// Relating to Minimal Layout // Relating to Minimal Layout
...@@ -464,6 +465,15 @@ import java.util.List; ...@@ -464,6 +465,15 @@ import java.util.List;
} }
} }
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (controlsBackground != null) {
// The background view should occupy the entirety of the parent. This is done in code rather
// than in layout XML to stop the background view from influencing the size of the parent if
// it uses "wrap_content". See: https://github.com/google/ExoPlayer/issues/8726.
controlsBackground.layout(0, 0, right - left, bottom - top);
}
}
private void onLayoutChange( private void onLayoutChange(
View v, View v,
int left, int left,
...@@ -577,13 +587,17 @@ import java.util.List; ...@@ -577,13 +587,17 @@ import java.util.List;
- (centerControls != null - (centerControls != null
? (centerControls.getPaddingLeft() + centerControls.getPaddingRight()) ? (centerControls.getPaddingLeft() + centerControls.getPaddingRight())
: 0); : 0);
int centerControlHeight =
getHeightWithMargins(centerControls)
- (centerControls != null
? (centerControls.getPaddingTop() + centerControls.getPaddingBottom())
: 0);
int defaultModeMinimumWidth = int defaultModeMinimumWidth =
Math.max( Math.max(
centerControlWidth, centerControlWidth,
getWidthWithMargins(timeView) + getWidthWithMargins(overflowShowButton)); getWidthWithMargins(timeView) + getWidthWithMargins(overflowShowButton));
int defaultModeMinimumHeight = int defaultModeMinimumHeight = centerControlHeight + (2 * getHeightWithMargins(bottomBar));
getHeightWithMargins(centerControls) + 2 * getHeightWithMargins(bottomBar);
return width <= defaultModeMinimumWidth || height <= defaultModeMinimumHeight; return width <= defaultModeMinimumWidth || height <= defaultModeMinimumHeight;
} }
...@@ -607,7 +621,7 @@ import java.util.List; ...@@ -607,7 +621,7 @@ import java.util.List;
defaultTimeBar.hideScrubber(/* disableScrubberPadding= */ true); defaultTimeBar.hideScrubber(/* disableScrubberPadding= */ true);
} else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) { } else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) {
defaultTimeBar.hideScrubber(/* disableScrubberPadding= */ false); defaultTimeBar.hideScrubber(/* disableScrubberPadding= */ false);
} else if (uxState != UX_STATE_ANIMATING_HIDE && uxState != UX_STATE_ANIMATING_SHOW) { } else if (uxState != UX_STATE_ANIMATING_HIDE) {
defaultTimeBar.showScrubber(); defaultTimeBar.showScrubber();
} }
} }
......
...@@ -285,7 +285,8 @@ import java.util.Map; ...@@ -285,7 +285,8 @@ import java.util.Map;
+ "writing-mode:%s;" + "writing-mode:%s;"
+ "font-size:%s;" + "font-size:%s;"
+ "background-color:%s;" + "background-color:%s;"
+ "transform:translate(%s%%,%s%%);" + "transform:translate(%s%%,%s%%)"
+ "%s;"
+ "'>", + "'>",
positionProperty, positionProperty,
positionPercent, positionPercent,
...@@ -298,7 +299,8 @@ import java.util.Map; ...@@ -298,7 +299,8 @@ import java.util.Map;
cueTextSizeCssPx, cueTextSizeCssPx,
windowCssColor, windowCssColor,
horizontalTranslatePercent, horizontalTranslatePercent,
verticalTranslatePercent)) verticalTranslatePercent,
getBlockShearTransformFunction(cue)))
.append(Util.formatInvariant("<span class='%s'>", DEFAULT_BACKGROUND_CSS_CLASS)) .append(Util.formatInvariant("<span class='%s'>", DEFAULT_BACKGROUND_CSS_CLASS))
.append(htmlAndCss.html) .append(htmlAndCss.html)
.append("</span>") .append("</span>")
...@@ -320,6 +322,17 @@ import java.util.Map; ...@@ -320,6 +322,17 @@ import java.util.Map;
"base64"); "base64");
} }
private static String getBlockShearTransformFunction(Cue cue) {
if (cue.shearDegrees != 0.0f) {
String direction =
(cue.verticalType == Cue.VERTICAL_TYPE_LR || cue.verticalType == Cue.VERTICAL_TYPE_RL)
? "skewY"
: "skewX";
return Util.formatInvariant("%s(%.2fdeg)", direction, cue.shearDegrees);
}
return "";
}
/** /**
* Converts a text size to a CSS px value. * Converts a text size to a CSS px value.
* *
......
...@@ -15,10 +15,14 @@ ...@@ -15,10 +15,14 @@
--> -->
<merge xmlns:android="http://schemas.android.com/apk/res/android"> <merge xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 0dp dimensions are used to prevent this view from influencing the size of
the parent view if it uses "wrap_content". It is expanded to occupy the
entirety of the parent in code, after the parent's size has been
determined. See: https://github.com/google/ExoPlayer/issues/8726.
-->
<View android:id="@id/exo_controls_background" <View android:id="@id/exo_controls_background"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="0dp"
android:layout_gravity="center"
android:background="@color/exo_black_opacity_60"/> android:background="@color/exo_black_opacity_60"/>
<FrameLayout android:id="@id/exo_bottom_bar" <FrameLayout android:id="@id/exo_bottom_bar"
...@@ -126,7 +130,8 @@ ...@@ -126,7 +130,8 @@
android:layout_gravity="center" android:layout_gravity="center"
android:background="@android:color/transparent" android:background="@android:color/transparent"
android:gravity="center" android:gravity="center"
android:padding="@dimen/exo_styled_controls_padding"> android:padding="@dimen/exo_styled_controls_padding"
android:clipToPadding="false">
<ImageButton android:id="@id/exo_prev" <ImageButton android:id="@id/exo_prev"
style="@style/ExoStyledControls.Button.Center.Previous"/> style="@style/ExoStyledControls.Button.Center.Previous"/>
......
...@@ -43,6 +43,8 @@ ...@@ -43,6 +43,8 @@
<item name="exo_vr" type="id"/> <item name="exo_vr" type="id"/>
<item name="exo_subtitle" type="id"/> <item name="exo_subtitle" type="id"/>
<item name="exo_fullscreen" type="id"/> <item name="exo_fullscreen" type="id"/>
<item name="exo_playback_speed" type="id"/>
<item name="exo_audio_track" type="id"/>
<item name="exo_settings" type="id"/> <item name="exo_settings" type="id"/>
<item name="exo_controls_background" type="id"/> <item name="exo_controls_background" type="id"/>
<item name="exo_basic_controls" type="id"/> <item name="exo_basic_controls" type="id"/>
......
...@@ -192,6 +192,16 @@ ...@@ -192,6 +192,16 @@
<item name="android:contentDescription">@string/exo_controls_settings_description</item> <item name="android:contentDescription">@string/exo_controls_settings_description</item>
</style> </style>
<style name="ExoStyledControls.Button.Bottom.PlaybackSpeed">
<item name="android:src">@drawable/exo_styled_controls_speed</item>
<item name="android:contentDescription">@string/exo_controls_playback_speed</item>
</style>
<style name="ExoStyledControls.Button.Bottom.AudioTrack">
<item name="android:src">@drawable/exo_styled_controls_audiotrack</item>
<item name="android:contentDescription">@string/exo_track_selection_title_audio</item>
</style>
<style name="ExoStyledControls.TimeBar"> <style name="ExoStyledControls.TimeBar">
<item name="bar_height">@dimen/exo_styled_progress_bar_height</item> <item name="bar_height">@dimen/exo_styled_progress_bar_height</item>
<item name="bar_gravity">bottom</item> <item name="bar_gravity">bottom</item>
......
...@@ -34,6 +34,8 @@ import androidx.test.core.app.ApplicationProvider; ...@@ -34,6 +34,8 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan; import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
import com.google.android.exoplayer2.text.span.RubySpan; import com.google.android.exoplayer2.text.span.RubySpan;
import com.google.android.exoplayer2.text.span.TextAnnotation;
import com.google.android.exoplayer2.text.span.TextEmphasisSpan;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
...@@ -250,12 +252,12 @@ public class SpannedToHtmlConverterTest { ...@@ -250,12 +252,12 @@ public class SpannedToHtmlConverterTest {
SpannableString spanned = SpannableString spanned =
new SpannableString("String with over-annotated and under-annotated section"); new SpannableString("String with over-annotated and under-annotated section");
spanned.setSpan( spanned.setSpan(
new RubySpan("ruby-text", RubySpan.POSITION_OVER), new RubySpan("ruby-text", TextAnnotation.POSITION_BEFORE),
"String with ".length(), "String with ".length(),
"String with over-annotated".length(), "String with over-annotated".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spanned.setSpan( spanned.setSpan(
new RubySpan("non-àscìì-text", RubySpan.POSITION_UNDER), new RubySpan("non-àscìì-text", TextAnnotation.POSITION_AFTER),
"String with over-annotated and ".length(), "String with over-annotated and ".length(),
"String with over-annotated and under-annotated".length(), "String with over-annotated and under-annotated".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
...@@ -280,6 +282,42 @@ public class SpannedToHtmlConverterTest { ...@@ -280,6 +282,42 @@ public class SpannedToHtmlConverterTest {
} }
@Test @Test
public void convert_supportsTextEmphasisSpan() {
SpannableString spanned = new SpannableString("Text emphasis おはよ ございます");
spanned.setSpan(
new TextEmphasisSpan(
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
TextEmphasisSpan.MARK_FILL_FILLED,
TextAnnotation.POSITION_BEFORE),
"Text emphasis ".length(),
"Text emphasis おはよ".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spanned.setSpan(
new TextEmphasisSpan(
TextEmphasisSpan.MARK_SHAPE_SESAME,
TextEmphasisSpan.MARK_FILL_OPEN,
TextAnnotation.POSITION_AFTER),
"Text emphasis おはよ ".length(),
"Text emphasis おはよ ございます".length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannedToHtmlConverter.HtmlAndCss htmlAndCss =
SpannedToHtmlConverter.convert(spanned, displayDensity);
assertThat(htmlAndCss.cssRuleSets).isEmpty();
assertThat(htmlAndCss.html)
.isEqualTo(
"Text emphasis <span style='"
+ "-webkit-text-emphasis-style:filled circle;text-emphasis-style:filled circle;"
+ "-webkit-text-emphasis-position:over right;text-emphasis-position:over right;"
+ "display:inline-block;'>&#12362;&#12399;&#12424;</span> <span style='"
+ "-webkit-text-emphasis-style:open sesame;text-emphasis-style:open sesame;"
+ "-webkit-text-emphasis-position:under left;text-emphasis-position:under left;"
+ "display:inline-block;'>&#12372;&#12374;&#12356;&#12414;&#12377;</span>");
}
@Test
public void convert_supportsUnderlineSpan() { public void convert_supportsUnderlineSpan() {
SpannableString spanned = new SpannableString("String with underlined section."); SpannableString spanned = new SpannableString("String with underlined section.");
spanned.setSpan( spanned.setSpan(
......
...@@ -12,103 +12,46 @@ ...@@ -12,103 +12,46 @@
// 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.
if (project.ext.has("exoplayerPublishEnabled") apply plugin: 'maven-publish'
&& project.ext.exoplayerPublishEnabled) { afterEvaluate {
// For publishing to Bintray. publishing {
apply plugin: 'bintray-release' repositories {
publish { maven {
artifactId = releaseArtifact url = findProperty('mavenRepo') ?: "${buildDir}/repo"
desc = releaseDescription
publishVersion = releaseVersion
repoName = getBintrayRepo()
userOrg = 'google'
groupId = 'com.google.android.exoplayer'
website = 'https://github.com/google/ExoPlayer'
}
gradle.taskGraph.whenReady { taskGraph ->
project.tasks
.findAll { task -> task.name.contains("generatePomFileFor") }
.forEach { task ->
task.doLast {
task.outputs.files
.filter { File file ->
file.path.contains("publications") \
&& file.name.matches("^pom-.+\\.xml\$")
}
.forEach { File file -> addLicense(file) }
}
}
}
} else {
// For publishing to a Maven repository.
apply plugin: 'maven-publish'
afterEvaluate {
publishing {
repositories {
maven {
url = findProperty('mavenRepo') ?: "${buildDir}/repo"
}
} }
publications { }
release(MavenPublication) { publications {
from components.release release(MavenPublication) {
artifact androidSourcesJar from components.release
groupId = 'com.google.android.exoplayer' artifact androidSourcesJar
artifactId = releaseArtifact groupId = 'com.google.android.exoplayer'
version releaseVersion artifactId = releaseArtifact
pom { version releaseVersion
name = releaseArtifact pom {
description = releaseDescription name = releaseArtifact
licenses { description = releaseDescription
license { licenses {
name = 'The Apache Software License, Version 2.0' license {
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' name = 'The Apache Software License, Version 2.0'
distribution = 'repo' url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
} distribution = 'repo'
}
developers {
developer {
name = 'The Android Open Source Project'
}
} }
scm { }
connection = 'scm:git:https://github.com/google/ExoPlayer.git' developers {
url = 'https://github.com/google/ExoPlayer' developer {
name = 'The Android Open Source Project'
} }
} }
scm {
connection = 'scm:git:https://github.com/google/ExoPlayer.git'
url = 'https://github.com/google/ExoPlayer'
}
} }
} }
} }
} }
} }
tasks.withType(PublishToMavenRepository) { it.dependsOn lint, test }
def getBintrayRepo() {
boolean publicRepo = hasProperty('publicRepo') &&
property('publicRepo').toBoolean()
return publicRepo ? 'exoplayer' : 'exoplayer-test'
}
static void addLicense(File pom) {
def licenseNode = new Node(null, "license")
licenseNode.append(
new Node(null, "name", "The Apache Software License, Version 2.0"))
licenseNode.append(
new Node(null, "url", "http://www.apache.org/licenses/LICENSE-2.0.txt"))
licenseNode.append(new Node(null, "distribution", "repo"))
def licensesNode = new Node(null, "licenses")
licensesNode.append(licenseNode)
def xml = new XmlParser().parse(pom)
xml.append(licensesNode)
def writer = new PrintWriter(new FileWriter(pom))
writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
def printer = new XmlNodePrinter(writer)
printer.preserveWhitespace = true
printer.print(xml)
writer.close()
}
task androidSourcesJar(type: Jar) { task androidSourcesJar(type: Jar) {
archiveClassifier.set('sources') archiveClassifier.set('sources')
......
...@@ -19,6 +19,18 @@ import android.graphics.Bitmap; ...@@ -19,6 +19,18 @@ import android.graphics.Bitmap;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.Metadata;
import com.google.android.exoplayer2.metadata.dvbsi.AppInfoTable;
import com.google.android.exoplayer2.metadata.emsg.EventMessage;
import com.google.android.exoplayer2.metadata.flac.PictureFrame;
import com.google.android.exoplayer2.metadata.flac.VorbisComment;
import com.google.android.exoplayer2.metadata.icy.IcyHeaders;
import com.google.android.exoplayer2.metadata.icy.IcyInfo;
import com.google.android.exoplayer2.metadata.id3.Id3Frame;
import com.google.android.exoplayer2.metadata.mp4.MdtaMetadataEntry;
import com.google.android.exoplayer2.metadata.mp4.MotionPhotoMetadata;
import com.google.android.exoplayer2.metadata.mp4.SlowMotionData;
import com.google.android.exoplayer2.metadata.mp4.SmtaMetadataEntry;
import com.google.android.exoplayer2.metadata.scte35.SpliceCommand;
import com.google.android.exoplayer2.testutil.CapturingRenderersFactory; import com.google.android.exoplayer2.testutil.CapturingRenderersFactory;
import com.google.android.exoplayer2.testutil.Dumper; import com.google.android.exoplayer2.testutil.Dumper;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
...@@ -89,13 +101,38 @@ public final class PlaybackOutput implements Dumper.Dumpable { ...@@ -89,13 +101,38 @@ public final class PlaybackOutput implements Dumper.Dumpable {
dumper.startBlock("Metadata[" + i + "]"); dumper.startBlock("Metadata[" + i + "]");
Metadata metadata = metadatas.get(i); Metadata metadata = metadatas.get(i);
for (int j = 0; j < metadata.length(); j++) { for (int j = 0; j < metadata.length(); j++) {
dumper.add("entry[" + j + "]", metadata.get(j).getClass().getSimpleName()); dumper.add("entry[" + j + "]", getEntryAsString(metadata.get(j)));
} }
dumper.endBlock(); dumper.endBlock();
} }
dumper.endBlock(); dumper.endBlock();
} }
/**
* Returns {@code entry.toString()} if we know the implementation overrides it, otherwise returns
* the simple class name.
*/
private static String getEntryAsString(Metadata.Entry entry) {
if (entry instanceof EventMessage
|| entry instanceof PictureFrame
|| entry instanceof VorbisComment
|| entry instanceof Id3Frame
|| entry instanceof MdtaMetadataEntry
|| entry instanceof MotionPhotoMetadata
|| entry instanceof SlowMotionData
|| entry instanceof SmtaMetadataEntry
|| entry instanceof AppInfoTable
|| entry instanceof IcyHeaders
|| entry instanceof IcyInfo
|| entry instanceof SpliceCommand
|| "com.google.android.exoplayer2.hls.HlsTrackMetadataEntry"
.equals(entry.getClass().getCanonicalName())) {
return entry.toString();
} else {
return entry.getClass().getSimpleName();
}
}
private void dumpSubtitles(Dumper dumper) { private void dumpSubtitles(Dumper dumper) {
if (subtitles.isEmpty()) { if (subtitles.isEmpty()) {
return; return;
......
seekMap:
isSeekable = true
duration = 867000
getPosition(0) = [[timeUs=0, position=6425]]
getPosition(1) = [[timeUs=0, position=6425]]
getPosition(433500) = [[timeUs=0, position=6425]]
getPosition(867000) = [[timeUs=0, position=6425]]
numberOfTracks = 2
track 0:
total output bytes = 3865
sample count = 1
format 0:
id = 1
sampleMimeType = video/avc
codecs = avc1.64000A
maxInputSize = 3895
width = 180
height = 120
pixelWidthHeightRatio = 0.5
initializationData:
data = length 32, hash 1F3D6E87
data = length 10, hash 7A0D0F2B
sample 0:
time = 0
flags = 536870913
data = length 3865, hash 5B0DEEC7
track 1024:
total output bytes = 0
sample count = 0
format 0:
metadata = entries=[Motion photo metadata: photoStartPosition=0, photoSize=6377, photoPresentationTimestampUs=1232840, videoStartPosition=6377, videoSize=4686]
tracksEnded = true
seekMap:
isSeekable = true
duration = 867000
getPosition(0) = [[timeUs=0, position=6425]]
getPosition(1) = [[timeUs=0, position=6425]]
getPosition(433500) = [[timeUs=0, position=6425]]
getPosition(867000) = [[timeUs=0, position=6425]]
numberOfTracks = 2
track 0:
total output bytes = 3865
sample count = 1
format 0:
id = 1
sampleMimeType = video/avc
codecs = avc1.64000A
maxInputSize = 3895
width = 180
height = 120
pixelWidthHeightRatio = 0.5
initializationData:
data = length 32, hash 1F3D6E87
data = length 10, hash 7A0D0F2B
sample 0:
time = 0
flags = 536870913
data = length 3865, hash 5B0DEEC7
track 1024:
total output bytes = 0
sample count = 0
format 0:
metadata = entries=[Motion photo metadata: photoStartPosition=0, photoSize=6377, photoPresentationTimestampUs=1232840, videoStartPosition=6377, videoSize=4686]
tracksEnded = true
seekMap:
isSeekable = true
duration = 867000
getPosition(0) = [[timeUs=0, position=6425]]
getPosition(1) = [[timeUs=0, position=6425]]
getPosition(433500) = [[timeUs=0, position=6425]]
getPosition(867000) = [[timeUs=0, position=6425]]
numberOfTracks = 2
track 0:
total output bytes = 3865
sample count = 1
format 0:
id = 1
sampleMimeType = video/avc
codecs = avc1.64000A
maxInputSize = 3895
width = 180
height = 120
pixelWidthHeightRatio = 0.5
initializationData:
data = length 32, hash 1F3D6E87
data = length 10, hash 7A0D0F2B
sample 0:
time = 0
flags = 536870913
data = length 3865, hash 5B0DEEC7
track 1024:
total output bytes = 0
sample count = 0
format 0:
metadata = entries=[Motion photo metadata: photoStartPosition=0, photoSize=6377, photoPresentationTimestampUs=1232840, videoStartPosition=6377, videoSize=4686]
tracksEnded = true
seekMap:
isSeekable = true
duration = 867000
getPosition(0) = [[timeUs=0, position=6425]]
getPosition(1) = [[timeUs=0, position=6425]]
getPosition(433500) = [[timeUs=0, position=6425]]
getPosition(867000) = [[timeUs=0, position=6425]]
numberOfTracks = 2
track 0:
total output bytes = 3865
sample count = 1
format 0:
id = 1
sampleMimeType = video/avc
codecs = avc1.64000A
maxInputSize = 3895
width = 180
height = 120
pixelWidthHeightRatio = 0.5
initializationData:
data = length 32, hash 1F3D6E87
data = length 10, hash 7A0D0F2B
sample 0:
time = 0
flags = 536870913
data = length 3865, hash 5B0DEEC7
track 1024:
total output bytes = 0
sample count = 0
format 0:
metadata = entries=[Motion photo metadata: photoStartPosition=0, photoSize=6377, photoPresentationTimestampUs=1232840, videoStartPosition=6377, videoSize=4686]
tracksEnded = true
seekMap:
isSeekable = false
duration = UNSET TIME
getPosition(0) = [[timeUs=0, position=0]]
numberOfTracks = 1
track 1024:
total output bytes = 0
sample count = 0
format 0:
metadata = entries=[]
tracksEnded = true
...@@ -7,11 +7,13 @@ ...@@ -7,11 +7,13 @@
</SegmentTimeline> </SegmentTimeline>
</SegmentTemplate> </SegmentTemplate>
<AdaptationSet id="0" mimeType="application/x-rawcc" subsegmentAlignment="true"> <AdaptationSet id="0" mimeType="application/x-rawcc" subsegmentAlignment="true">
<Role schemeIdUri="urn:mpeg:DASH:role:2011" value="subtitle"/>
<Representation id="0" codecs="cea608" bandwidth="16"> <Representation id="0" codecs="cea608" bandwidth="16">
<BaseURL>https://test.com/0</BaseURL> <BaseURL>https://test.com/0</BaseURL>
</Representation> </Representation>
</AdaptationSet> </AdaptationSet>
<AdaptationSet id="0" mimeType="application/mp4" subsegmentAlignment="true"> <AdaptationSet id="0" mimeType="application/mp4" subsegmentAlignment="true">
<Role schemeIdUri="urn:mpeg:DASH:role:2011" value="forced_subtitle"/>
<Representation id="0" codecs="stpp.ttml.im1t" bandwidth="16"> <Representation id="0" codecs="stpp.ttml.im1t" bandwidth="16">
<BaseURL>https://test.com/0</BaseURL> <BaseURL>https://test.com/0</BaseURL>
</Representation> </Representation>
......
[Script Info]
Title: SSA/ASS Test
Original Script: Abel
Script Type: V4.00+
PlayResX: 1280
PlayResY: 720
[V4+ Styles]
Format: Name ,Bold,Italic
Style: FontBold ,-1 ,0
Style: FontItalic ,0 ,-1
Style: FontBoldItalic ,1 ,1
[Events]
Format: Start ,End ,Style ,Text
Dialogue: 0:00:01.00,0:00:03.00,FontBold ,First line with Bold.
Dialogue: 0:00:05.00,0:00:07.00,FontItalic ,Second line with Italic.
Dialogue: 0:00:09.00,0:00:11.00,FontBoldItalic,Third line with Bold Italic.
...@@ -5,22 +5,22 @@ PlayResX: 1280 ...@@ -5,22 +5,22 @@ PlayResX: 1280
PlayResY: 720 PlayResY: 720
[V4+ Styles] [V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding Format: Name ,PrimaryColour
Style: PrimaryColourStyleHexRed ,Roboto,50,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1 Style: PrimaryColourStyleHexRed ,&H000000FF
Style: PrimaryColourStyleHexYellow ,Roboto,50,&H0000FFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1 Style: PrimaryColourStyleHexYellow ,&H0000FFFF
Style: PrimaryColourStyleHexGreen ,Roboto,50,&HFF00 ,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1 Style: PrimaryColourStyleHexGreen ,&HFF00
Style: PrimaryColourStyleHexAlpha ,Roboto,50,&HA00000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1 Style: PrimaryColourStyleHexAlpha ,&HA00000FF
Style: PrimaryColourStyleDecimal ,Roboto,50,16711680 ,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1 Style: PrimaryColourStyleDecimal ,16711680
Style: PrimaryColourStyleDecimalAlpha ,Roboto,50,2164195328,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1 Style: PrimaryColourStyleDecimalAlpha,2164195328
Style: PrimaryColourStyleInvalid ,Roboto,50,blue ,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1 Style: PrimaryColourStyleInvalid ,blue
[Events] [Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text Format: Start ,End ,Style ,Text
Dialogue: 0,0:00:01.00,0:00:02.00,PrimaryColourStyleHexRed ,Arnold,0,0,0,,First line in RED (&H000000FF). Dialogue: 0:00:01.00,0:00:02.00,PrimaryColourStyleHexRed ,First line in RED (&H000000FF).
Dialogue: 0,0:00:03.00,0:00:04.00,PrimaryColourStyleHexYellow ,Arnold,0,0,0,,Second line in YELLOW (&H0000FFFF). Dialogue: 0:00:03.00,0:00:04.00,PrimaryColourStyleHexYellow ,Second line in YELLOW (&H0000FFFF).
Dialogue: 0,0:00:05.00,0:00:06.00,PrimaryColourStyleHexGreen ,Arnold,0,0,0,,Third line in GREEN (leading zeros &HFF00). Dialogue: 0:00:05.00,0:00:06.00,PrimaryColourStyleHexGreen ,Third line in GREEN (leading zeros &HFF00).
Dialogue: 0,0:00:07.00,0:00:08.00,PrimaryColourStyleHexAlpha ,Arnold,0,0,0,,Fourth line in RED with alpha (&H400000FF). Dialogue: 0:00:07.00,0:00:08.00,PrimaryColourStyleHexAlpha ,Fourth line in RED with alpha (&H400000FF).
Dialogue: 0,0:00:09.00,0:00:10.00,PrimaryColourStyleDecimal ,Arnold,0,0,0,,Fifth line in BLUE (16711680). Dialogue: 0:00:09.00,0:00:10.00,PrimaryColourStyleDecimal ,Fifth line in BLUE (16711680).
Dialogue: 0,0:00:11.00,0:00:12.00,PrimaryColourStyleDecimalAlpha ,Arnold,0,0,0,,Sixth line in BLUE with alpha (2164195328). Dialogue: 0:00:11.00,0:00:12.00,PrimaryColourStyleDecimalAlpha,Sixth line in BLUE with alpha (2164195328).
Dialogue: 0,0:00:13.00,0:00:14.00,PrimaryColourInvalid ,Arnold,0,0,0,,Seventh line with invalid color . Dialogue: 0:00:13.00,0:00:14.00,PrimaryColourInvalid ,Seventh line with invalid color.
...@@ -6,13 +6,13 @@ PlayResX: 1280 ...@@ -6,13 +6,13 @@ PlayResX: 1280
PlayResY: 720 PlayResY: 720
[V4+ Styles] [V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding Format: Name ,Fontsize
Style: FontSizeSmall ,Roboto,30, &H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1 Style: FontSizeSmall,30
Style: FontSizeBig ,Roboto,72.2,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1 Style: FontSizeBig ,72.2
[Events] [Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text Format: Start ,End ,Style ,Text
Dialogue: 0,0:00:00.95,0:00:03.11,FontSizeSmall ,Arnold,0,0,0,,First line with font size 30. Dialogue: 0:00:00.95,0:00:03.11,FontSizeSmall,First line with font size 30.
Dialogue: 0,0:00:08.50,0:00:11.50,FontSizeBig ,Arnold,0,0,0,,Second line with font size 72.2. Dialogue: 0:00:08.50,0:00:11.50,FontSizeBig ,Second line with font size 72.2.
<tt xmlns:ttm="http://www.w3.org/2006/10/ttaf1#metadata"
xmlns:ttp="http://www.w3.org/2006/10/ttaf1#parameter"
xmlns:tts="http://www.w3.org/2006/10/ttaf1#style"
xmlns="http://www.w3.org/ns/ttml"
xmlns="http://www.w3.org/2006/10/ttaf1">
<body>
<div>
<p begin="10s" end="18s" tts:shear="0%">0%</p>
</div>
<div>
<p begin="20s" end="28s" tts:shear="16.67%">16.67%</p>
</div>
<div>
<p begin="30s" end="38s" tts:shear="-16.67%">-16.67%</p>
</div>
<div>
<p begin="40s" end="48s" tts:shear="+16.67%">+16.67%</p>
</div>
<div>
<p begin="50s" end="58s" tts:shear="+25%">+25%</p>
</div>
<div>
<p begin="60s" end="68s" tts:shear="Invalid">Invalid</p>
</div>
<div>
<p begin="70s" end="78s" tts:shear="101.01%">100.01%</p>
</div>
<div>
<p begin="80s" end="88s" tts:shear="-101.1%">-101.1%</p>
</div>
</body>
</tt>
<tt xmlns:ttm="http://www.w3.org/2006/10/ttaf1#metadata"
xmlns:ttp="http://www.w3.org/2006/10/ttaf1#parameter"
xmlns:tts="http://www.w3.org/2006/10/ttaf1#style"
xmlns="http://www.w3.org/ns/ttml">
<head>
<region xml:id="region_tbrl" tts:extent="80.000% 80.000%" tts:origin="10.000% 10.000%" tts:writingMode="tbrl"/>
<region xml:id="region_tblr" tts:extent="80.000% 80.000%" tts:origin="10.000% 10.000%" tts:writingMode="tblr"/>
<region xml:id="region_tb" tts:extent="80.000% 80.000%" tts:origin="10.000% 10.000%" tts:writingMode="tb"/>
<region xml:id="region_lr" tts:extent="80.000% 80.000%" tts:origin="10.000% 10.000%" tts:writingMode="lr"/>
</head>
<body>
<div>
<p begin="10s" end="18s">None <span tts:textEmphasis="none">おはよ</span></p>
</div>
<div>
<p begin="20s" end="28s">Auto <span tts:textEmphasis="auto">ございます</span></p>
</div>
<div>
<p begin="30s" end="38s">Filled circle <span tts:textEmphasis="filled circle">こんばんは</span></p>
</div>
<div>
<p begin="40s" end="48s">Filled dot <span tts:textEmphasis="filled dot">ございます</span></p>
</div>
<div>
<p begin="50s" end="58s">Filled sesame <span tts:textEmphasis="filled sesame">おはよ</span></p>
</div>
<div>
<p begin="60s" end="68s">Open circle before <span tts:textEmphasis="open circle before">ございます</span></p>
</div>
<div>
<p begin="70s" end="78s">Open dot after <span tts:textEmphasis="open dot after">おはよ</span></p>
</div>
<div>
<p begin="80s" end="88s">Open sesame outside <span tts:textEmphasis="open sesame outside">ございます</span></p>
</div>
<div>
<p begin="90s" end="98s">Auto outside <span tts:textEmphasis="auto outside">おはよ</span></p>
</div>
<div>
<p begin="100s" end="108s">Circle before <span tts:textEmphasis="circle before">ございます</span></p>
</div>
<div>
<p begin="110s" end="118s">Sesame after <span tts:textEmphasis="sesame after">おはよ</span></p>
</div>
<div>
<p begin="120s" end="128s">Dot outside <span tts:textEmphasis="dot outside">ございます</span></p>
</div>
<div>
<p begin="130s" end="138s">No textEmphasis property <span>おはよ</span></p>
</div>
<div>
<p begin="140s" end="148s" region="region_tbrl">Auto (TBLR) <span tts:textEmphasis="auto">ございます</span></p>
</div>
<div>
<p begin="150s" end="158s" region="region_tblr">Auto (TBRL) <span tts:textEmphasis="auto">おはよ</span></p>
</div>
<div>
<p begin="160s" end="168s" region="region_tb">Auto (TB) <span tts:textEmphasis="auto">ございます</span></p>
</div>
<div>
<p begin="170s" end="178s" region="region_lr">Auto (LR) <span tts:textEmphasis="auto">おはよ</span></p>
</div>
</body>
</tt>
...@@ -22,11 +22,11 @@ MediaCodecAdapter (exotest.audio.eac3): ...@@ -22,11 +22,11 @@ MediaCodecAdapter (exotest.audio.eac3):
buffers[19] = length 0, hash 1 buffers[19] = length 0, hash 1
MetadataOutput: MetadataOutput:
Metadata[0]: Metadata[0]:
entry[0] = AppInfoTable entry[0] = Ait(controlCode=1,url=http://static-cdn.arte.tv/redbutton/index_fr.html)
entry[1] = AppInfoTable entry[1] = Ait(controlCode=2,url=http://www.arte.tv/hbbtvv2/index.html?lang=fr_FR&page=PLUS7)
Metadata[1]: Metadata[1]:
entry[0] = AppInfoTable entry[0] = Ait(controlCode=1,url=http://static-cdn.arte.tv/redbutton/index_fr.html)
entry[1] = AppInfoTable entry[1] = Ait(controlCode=2,url=http://www.arte.tv/hbbtvv2/index.html?lang=fr_FR&page=PLUS7)
Metadata[2]: Metadata[2]:
entry[0] = AppInfoTable entry[0] = Ait(controlCode=1,url=http://static-cdn.arte.tv/redbutton/index_fr.html)
entry[1] = AppInfoTable entry[1] = Ait(controlCode=2,url=http://www.arte.tv/hbbtvv2/index.html?lang=fr_FR&page=PLUS7)
...@@ -12,8 +12,8 @@ MediaCodecAdapter (exotest.video.mpeg2): ...@@ -12,8 +12,8 @@ MediaCodecAdapter (exotest.video.mpeg2):
buffers[2] = length 0, hash 1 buffers[2] = length 0, hash 1
MetadataOutput: MetadataOutput:
Metadata[0]: Metadata[0]:
entry[0] = SpliceInsertCommand entry[0] = SCTE-35 splice command: type=SpliceInsertCommand
Metadata[1]: Metadata[1]:
entry[0] = SpliceInsertCommand entry[0] = SCTE-35 splice command: type=SpliceInsertCommand
Metadata[2]: Metadata[2]:
entry[0] = SpliceInsertCommand entry[0] = SCTE-35 splice command: type=SpliceInsertCommand
...@@ -147,7 +147,7 @@ MediaCodecAdapter (exotest.audio.aac): ...@@ -147,7 +147,7 @@ MediaCodecAdapter (exotest.audio.aac):
buffers[144] = length 0, hash 1 buffers[144] = length 0, hash 1
MetadataOutput: MetadataOutput:
Metadata[0]: Metadata[0]:
entry[0] = ApicFrame entry[0] = APIC: mimeType=image/jpeg, description=Hello World
Metadata[1]: Metadata[1]:
entry[0] = CommentFrame entry[0] = COMM: language=eng, description=description
entry[1] = ApicFrame entry[1] = APIC: mimeType=image/jpeg, description=Hello World
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.testutil; package com.google.android.exoplayer2.testutil;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static com.google.android.exoplayer2.util.Util.castNonNull; import static com.google.android.exoplayer2.util.Util.castNonNull;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
...@@ -86,6 +87,7 @@ public class FakeMediaSource extends BaseMediaSource { ...@@ -86,6 +87,7 @@ public class FakeMediaSource extends BaseMediaSource {
private final ArrayList<MediaPeriodId> createdMediaPeriods; private final ArrayList<MediaPeriodId> createdMediaPeriods;
private final DrmSessionManager drmSessionManager; private final DrmSessionManager drmSessionManager;
private boolean preparationAllowed;
private @MonotonicNonNull Timeline timeline; private @MonotonicNonNull Timeline timeline;
private boolean preparedSource; private boolean preparedSource;
private boolean releasedSource; private boolean releasedSource;
...@@ -154,6 +156,22 @@ public class FakeMediaSource extends BaseMediaSource { ...@@ -154,6 +156,22 @@ public class FakeMediaSource extends BaseMediaSource {
this.createdMediaPeriods = new ArrayList<>(); this.createdMediaPeriods = new ArrayList<>();
this.drmSessionManager = drmSessionManager; this.drmSessionManager = drmSessionManager;
this.trackDataFactory = trackDataFactory; this.trackDataFactory = trackDataFactory;
preparationAllowed = true;
}
/**
* Sets whether the next call to {@link #prepareSource} is allowed to finish. If not allowed, a
* later call to this method with {@code allowPreparation} set to true will finish the
* preparation.
*
* @param allowPreparation Whether preparation is allowed to finish.
*/
public synchronized void setAllowPreparation(boolean allowPreparation) {
preparationAllowed = allowPreparation;
if (allowPreparation && sourceInfoRefreshHandler != null) {
sourceInfoRefreshHandler.post(
() -> finishSourcePreparation(/* sendManifestLoadEvents= */ true));
}
} }
@Nullable @Nullable
...@@ -186,14 +204,14 @@ public class FakeMediaSource extends BaseMediaSource { ...@@ -186,14 +204,14 @@ public class FakeMediaSource extends BaseMediaSource {
@Override @Override
@Nullable @Nullable
public Timeline getInitialTimeline() { public Timeline getInitialTimeline() {
return timeline == null || timeline == Timeline.EMPTY || timeline.getWindowCount() == 1 return timeline == null || timeline.isEmpty() || timeline.getWindowCount() == 1
? null ? null
: new InitialTimeline(timeline); : new InitialTimeline(timeline);
} }
@Override @Override
public boolean isSingleWindow() { public boolean isSingleWindow() {
return timeline == null || timeline == Timeline.EMPTY || timeline.getWindowCount() == 1; return timeline == null || timeline.isEmpty() || timeline.getWindowCount() == 1;
} }
@Override @Override
...@@ -204,7 +222,7 @@ public class FakeMediaSource extends BaseMediaSource { ...@@ -204,7 +222,7 @@ public class FakeMediaSource extends BaseMediaSource {
preparedSource = true; preparedSource = true;
releasedSource = false; releasedSource = false;
sourceInfoRefreshHandler = Util.createHandlerForCurrentLooper(); sourceInfoRefreshHandler = Util.createHandlerForCurrentLooper();
if (timeline != null) { if (preparationAllowed && timeline != null) {
finishSourcePreparation(/* sendManifestLoadEvents= */ true); finishSourcePreparation(/* sendManifestLoadEvents= */ true);
} }
} }
...@@ -273,11 +291,14 @@ public class FakeMediaSource extends BaseMediaSource { ...@@ -273,11 +291,14 @@ public class FakeMediaSource extends BaseMediaSource {
* Sets a new timeline. If the source is already prepared, this triggers a source info refresh * Sets a new timeline. If the source is already prepared, this triggers a source info refresh
* message being sent to the listener. * message being sent to the listener.
* *
* <p>Must only be called if preparation is {@link #setAllowPreparation(boolean) allowed}.
*
* @param newTimeline The new {@link Timeline}. * @param newTimeline The new {@link Timeline}.
* @param sendManifestLoadEvents Whether to treat this as a manifest refresh and send manifest * @param sendManifestLoadEvents Whether to treat this as a manifest refresh and send manifest
* load events to listeners. * load events to listeners.
*/ */
public synchronized void setNewSourceInfo(Timeline newTimeline, boolean sendManifestLoadEvents) { public synchronized void setNewSourceInfo(Timeline newTimeline, boolean sendManifestLoadEvents) {
checkState(preparationAllowed);
if (sourceInfoRefreshHandler != null) { if (sourceInfoRefreshHandler != null) {
sourceInfoRefreshHandler.post( sourceInfoRefreshHandler.post(
() -> { () -> {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment