mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
commit
0ba317b133
102 changed files with 3581 additions and 620 deletions
42
README.md
42
README.md
|
|
@ -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'
|
||||||
|
|
||||||
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.
|
||||||
* `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
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,79 @@
|
||||||
# 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 {
|
||||||
}
|
}
|
||||||
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 {
|
||||||
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 @@
|
||||||
// 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 {
|
||||||
"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 {
|
||||||
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 {
|
||||||
"proguard-rules.txt",
|
"proguard-rules.txt",
|
||||||
getDefaultProguardFile('proguard-android.txt')
|
getDefaultProguardFile('proguard-android.txt')
|
||||||
]
|
]
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
jniDebuggable = true
|
jniDebuggable = true
|
||||||
|
|
|
||||||
|
|
@ -485,6 +485,13 @@
|
||||||
"subtitle_mime_type": "application/ttml+xml",
|
"subtitle_mime_type": "application/ttml+xml",
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
"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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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
|
||||||
@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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
+ 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.
|
||||||
|
|
||||||
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;
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
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 {
|
||||||
|
|
||||||
/** 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 {
|
||||||
* 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 {
|
||||||
* 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 {
|
||||||
* 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 {
|
||||||
* </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 {
|
||||||
* {@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 {
|
||||||
* 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 {
|
||||||
* 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 {
|
||||||
*/
|
*/
|
||||||
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 {
|
||||||
*/
|
*/
|
||||||
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 {
|
||||||
*
|
*
|
||||||
* <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 {
|
||||||
@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 {
|
||||||
/**
|
/**
|
||||||
* 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();
|
||||||
|
|
|
||||||
|
|
@ -269,6 +269,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 {
|
||||||
/* 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 {
|
||||||
/* 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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
bitmapHeight,
|
bitmapHeight,
|
||||||
windowColorSet,
|
windowColorSet,
|
||||||
windowColor,
|
windowColor,
|
||||||
verticalType);
|
verticalType,
|
||||||
|
shearDegrees);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
*/
|
*/
|
||||||
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 {
|
||||||
* #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 {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 {
|
||||||
* @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 {
|
||||||
* @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,26 +188,13 @@ 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.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
/*
|
||||||
|
* 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 {
|
||||||
* 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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
&& 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;
|
||||||
// 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;
|
||||||
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
|
||||||
* <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
|
||||||
* 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
|
||||||
* 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
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
&& !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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
|
|
||||||
@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> {
|
||||||
// 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 {
|
||||||
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 @@
|
||||||
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> {
|
||||||
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> {
|
||||||
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 @@
|
||||||
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 {
|
||||||
@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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
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 {
|
||||||
|
|
||||||
@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 {
|
||||||
@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;
|
||||||
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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 {
|
||||||
/** 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 @@
|
||||||
*/
|
*/
|
||||||
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;
|
||||||
// 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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
* 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() {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
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 {
|
||||||
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;
|
||||||
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;
|
||||||
@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;
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
/*
|
||||||
|
* 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 @@
|
||||||
*/
|
*/
|
||||||
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;
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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;
|
||||||
|
|
@ -750,11 +762,36 @@ 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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
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 @@
|
||||||
*/
|
*/
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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;
|
||||||
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;
|
||||||
/* 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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
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;
|
||||||
|
|
||||||
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;
|
||||||
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;
|
||||||
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 {
|
||||||
@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 {
|
||||||
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 {
|
||||||
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;
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 {
|
||||||
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 {
|
||||||
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;
|
||||||
|
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 @@
|
||||||
*/
|
*/
|
||||||
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;
|
||||||
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)
|
||||||
|
|
@ -151,6 +153,35 @@ public final class PlaybackStatsListenerTest {
|
||||||
verify(callback).onPlaybackStatsReady(any(), any());
|
verify(callback).onPlaybackStatsReady(any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
@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);
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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())
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,729 @@
|
||||||
|
/*
|
||||||
|
* 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 com.google.android.exoplayer2.text.ttml.TextEmphasis.MARK_SHAPE_AUTO;
|
||||||
|
import static com.google.android.exoplayer2.text.ttml.TextEmphasis.POSITION_OUTSIDE;
|
||||||
|
import static com.google.android.exoplayer2.text.ttml.TextEmphasis.parse;
|
||||||
|
import static com.google.common.truth.Truth.assertWithMessage;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import com.google.android.exoplayer2.text.span.TextAnnotation;
|
||||||
|
import com.google.android.exoplayer2.text.span.TextEmphasisSpan;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** Unit test for {@link TextEmphasis}. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class TextEmphasisTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNull() {
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(null);
|
||||||
|
assertWithMessage("Text Emphasis must be null").that(textEmphasis).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmpty() {
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse("");
|
||||||
|
assertWithMessage("Text Emphasis must be null").that(textEmphasis).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEmptyWithWhitespace() {
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(" ");
|
||||||
|
assertWithMessage("Text Emphasis must be null").that(textEmphasis).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNone() {
|
||||||
|
String value = "none";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_NONE);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextEmphasis.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAuto() {
|
||||||
|
String value = "auto";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape").that(textEmphasis.markShape).isEqualTo(MARK_SHAPE_AUTO);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_UNKNOWN);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextEmphasis.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalid() {
|
||||||
|
String value = "invalid";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape").that(textEmphasis.markShape).isEqualTo(MARK_SHAPE_AUTO);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_UNKNOWN);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextEmphasis.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAutoOutside() {
|
||||||
|
String value = "auto outside";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape").that(textEmphasis.markShape).isEqualTo(MARK_SHAPE_AUTO);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_UNKNOWN);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextEmphasis.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAutoAfter() {
|
||||||
|
String value = "auto after";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape").that(textEmphasis.markShape).isEqualTo(MARK_SHAPE_AUTO);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_UNKNOWN);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_AFTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If only filled or open is specified, then it is equivalent to filled circle and open circle,
|
||||||
|
* respectively.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testFilled() {
|
||||||
|
String value = "filled";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextEmphasis.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpen() {
|
||||||
|
String value = "open";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextEmphasis.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenAfter() {
|
||||||
|
String value = "open after";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_AFTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If only circle, dot, or sesame is specified, then it is equivalent to filled circle, filled
|
||||||
|
* dot, and filled sesame, respectively.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void testDotBefore() {
|
||||||
|
String value = "dot before";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCircleBefore() {
|
||||||
|
String value = "circle before";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSesameBefore() {
|
||||||
|
String value = "sesame before";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDotAfter() {
|
||||||
|
String value = "dot after";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_AFTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCircleAfter() {
|
||||||
|
String value = "circle after";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_AFTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSesameAfter() {
|
||||||
|
String value = "sesame after";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_AFTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDotOutside() {
|
||||||
|
String value = "dot outside";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextEmphasis.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCircleOutside() {
|
||||||
|
String value = "circle outside";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextEmphasis.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSesameOutside() {
|
||||||
|
String value = "sesame outside";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextEmphasis.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenDotAfter() {
|
||||||
|
String value = "open dot after";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_AFTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenCircleAfter() {
|
||||||
|
String value = "open circle after";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_AFTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenSesameAfter() {
|
||||||
|
String value = "open sesame after";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_AFTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenDotBefore() {
|
||||||
|
String value = "open dot before";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenCircleBefore() {
|
||||||
|
String value = "open circle before";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenSesameBefore() {
|
||||||
|
String value = "open sesame before";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenDotOutside() {
|
||||||
|
String value = "open dot Outside";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextEmphasis.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenCircleOutside() {
|
||||||
|
String value = "open circle outside";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextEmphasis.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOpenSesameOutside() {
|
||||||
|
String value = "open sesame outside";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextEmphasis.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFilledDotOutside() {
|
||||||
|
String value = "filled dot outside";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextEmphasis.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFilledCircleOutside() {
|
||||||
|
String value = "filled circle outside";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextEmphasis.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFilledSesameOutside() {
|
||||||
|
String value = "filled sesame outside";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextEmphasis.POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFilledDotAfter() {
|
||||||
|
String value = "filled dot after";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_AFTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFilledCircleAfter() {
|
||||||
|
String value = "filled circle after";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_AFTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFilledSesameAfter() {
|
||||||
|
String value = "filled sesame after";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_AFTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFilledDotBefore() {
|
||||||
|
String value = "filled dot before";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_DOT);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFilledCircleBefore() {
|
||||||
|
String value = "filled circle before";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFilledSesameBefore() {
|
||||||
|
String value = "filled sesame before";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBeforeFilledSesame() {
|
||||||
|
String value = "before filled sesame";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBeforeSesameFilled() {
|
||||||
|
String value = "before sesame filled";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidMarkShape() {
|
||||||
|
String value = "before sesamee filled";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_CIRCLE);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidMarkFill() {
|
||||||
|
String value = "before sesame filed";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidPosition() {
|
||||||
|
String value = "befour sesame filled";
|
||||||
|
@Nullable TextEmphasis textEmphasis = parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_FILLED);
|
||||||
|
assertWithMessage("position").that(textEmphasis.position).isEqualTo(POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidMixedWithInvalidDescription() {
|
||||||
|
String value = "blue open sesame foo bar after";
|
||||||
|
@Nullable TextEmphasis textEmphasis = TextEmphasis.parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape")
|
||||||
|
.that(textEmphasis.markShape)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_SHAPE_SESAME);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_OPEN);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_AFTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testColorDescriptionNotSupported() {
|
||||||
|
String value = "blue";
|
||||||
|
@Nullable TextEmphasis textEmphasis = TextEmphasis.parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape").that(textEmphasis.markShape).isEqualTo(MARK_SHAPE_AUTO);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_UNKNOWN);
|
||||||
|
assertWithMessage("position").that(textEmphasis.position).isEqualTo(POSITION_OUTSIDE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testQuotedStringStyleNotSupported() {
|
||||||
|
String value = "\"x\" after";
|
||||||
|
@Nullable TextEmphasis textEmphasis = TextEmphasis.parse(value);
|
||||||
|
assertWithMessage("Text Emphasis must exist").that(textEmphasis).isNotNull();
|
||||||
|
assertWithMessage("markShape").that(textEmphasis.markShape).isEqualTo(MARK_SHAPE_AUTO);
|
||||||
|
assertWithMessage("markFill")
|
||||||
|
.that(textEmphasis.markFill)
|
||||||
|
.isEqualTo(TextEmphasisSpan.MARK_FILL_UNKNOWN);
|
||||||
|
assertWithMessage("position")
|
||||||
|
.that(textEmphasis.position)
|
||||||
|
.isEqualTo(TextAnnotation.POSITION_AFTER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,7 +27,8 @@ 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.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.text.span.TextEmphasisSpan;
|
||||||
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 java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
@ -67,6 +68,8 @@ public final class TtmlDecoderTest {
|
||||||
private static final String VERTICAL_TEXT_FILE = "media/ttml/vertical_text.xml";
|
private static final String VERTICAL_TEXT_FILE = "media/ttml/vertical_text.xml";
|
||||||
private static final String TEXT_COMBINE_FILE = "media/ttml/text_combine.xml";
|
private static final String TEXT_COMBINE_FILE = "media/ttml/text_combine.xml";
|
||||||
private static final String RUBIES_FILE = "media/ttml/rubies.xml";
|
private static final String RUBIES_FILE = "media/ttml/rubies.xml";
|
||||||
|
private static final String TEXT_EMPHASIS_FILE = "media/ttml/text_emphasis.xml";
|
||||||
|
private static final String SHEAR_FILE = "media/ttml/shear.xml";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void inlineAttributes() throws IOException, SubtitleDecoderException {
|
public void inlineAttributes() throws IOException, SubtitleDecoderException {
|
||||||
|
|
@ -109,12 +112,12 @@ public final class TtmlDecoderTest {
|
||||||
* framework level. Tests that <i>lime</i> resolves to <code>#FF00FF00</code> not <code>#00FF00
|
* framework level. Tests that <i>lime</i> resolves to <code>#FF00FF00</code> not <code>#00FF00
|
||||||
* </code>.
|
* </code>.
|
||||||
*
|
*
|
||||||
|
* @throws IOException thrown if reading subtitle file fails.
|
||||||
* @see <a
|
* @see <a
|
||||||
* href="https://github.com/android/platform_frameworks_base/blob/jb-mr2-release/graphics/java/android/graphics/Color.java#L414">
|
* href="https://github.com/android/platform_frameworks_base/blob/jb-mr2-release/graphics/java/android/graphics/Color.java#L414">
|
||||||
* JellyBean Color</a> <a
|
* JellyBean Color</a> <a
|
||||||
* href="https://github.com/android/platform_frameworks_base/blob/kitkat-mr2.2-release/graphics/java/android/graphics/Color.java#L414">
|
* href="https://github.com/android/platform_frameworks_base/blob/kitkat-mr2.2-release/graphics/java/android/graphics/Color.java#L414">
|
||||||
* Kitkat Color</a>
|
* Kitkat Color</a>
|
||||||
* @throws IOException thrown if reading subtitle file fails.
|
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void lime() throws IOException, SubtitleDecoderException {
|
public void lime() throws IOException, SubtitleDecoderException {
|
||||||
|
|
@ -646,16 +649,16 @@ public final class TtmlDecoderTest {
|
||||||
assertThat(firstCue.toString()).isEqualTo("Cue with annotated text.");
|
assertThat(firstCue.toString()).isEqualTo("Cue with annotated text.");
|
||||||
assertThat(firstCue)
|
assertThat(firstCue)
|
||||||
.hasRubySpanBetween("Cue with ".length(), "Cue with annotated".length())
|
.hasRubySpanBetween("Cue with ".length(), "Cue with annotated".length())
|
||||||
.withTextAndPosition("1st rubies", RubySpan.POSITION_OVER);
|
.withTextAndPosition("1st rubies", TextAnnotation.POSITION_BEFORE);
|
||||||
assertThat(firstCue)
|
assertThat(firstCue)
|
||||||
.hasRubySpanBetween("Cue with annotated ".length(), "Cue with annotated text".length())
|
.hasRubySpanBetween("Cue with annotated ".length(), "Cue with annotated text".length())
|
||||||
.withTextAndPosition("2nd rubies", RubySpan.POSITION_UNKNOWN);
|
.withTextAndPosition("2nd rubies", TextAnnotation.POSITION_UNKNOWN);
|
||||||
|
|
||||||
Spanned secondCue = getOnlyCueTextAtTimeUs(subtitle, 20_000_000);
|
Spanned secondCue = getOnlyCueTextAtTimeUs(subtitle, 20_000_000);
|
||||||
assertThat(secondCue.toString()).isEqualTo("Cue with annotated text.");
|
assertThat(secondCue.toString()).isEqualTo("Cue with annotated text.");
|
||||||
assertThat(secondCue)
|
assertThat(secondCue)
|
||||||
.hasRubySpanBetween("Cue with ".length(), "Cue with annotated".length())
|
.hasRubySpanBetween("Cue with ".length(), "Cue with annotated".length())
|
||||||
.withTextAndPosition("rubies", RubySpan.POSITION_UNKNOWN);
|
.withTextAndPosition("rubies", TextAnnotation.POSITION_UNKNOWN);
|
||||||
|
|
||||||
Spanned thirdCue = getOnlyCueTextAtTimeUs(subtitle, 30_000_000);
|
Spanned thirdCue = getOnlyCueTextAtTimeUs(subtitle, 30_000_000);
|
||||||
assertThat(thirdCue.toString()).isEqualTo("Cue with annotated text.");
|
assertThat(thirdCue.toString()).isEqualTo("Cue with annotated text.");
|
||||||
|
|
@ -674,6 +677,175 @@ public final class TtmlDecoderTest {
|
||||||
assertThat(sixthCue).hasNoRubySpanBetween(0, sixthCue.length());
|
assertThat(sixthCue).hasNoRubySpanBetween(0, sixthCue.length());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void textEmphasis() throws IOException, SubtitleDecoderException {
|
||||||
|
TtmlSubtitle subtitle = getSubtitle(TEXT_EMPHASIS_FILE);
|
||||||
|
|
||||||
|
Spanned firstCue = getOnlyCueTextAtTimeUs(subtitle, 10_000_000);
|
||||||
|
assertThat(firstCue)
|
||||||
|
.hasTextEmphasisSpanBetween("None ".length(), "None おはよ".length())
|
||||||
|
.withMarkAndPosition(
|
||||||
|
TextEmphasisSpan.MARK_SHAPE_NONE,
|
||||||
|
TextEmphasisSpan.MARK_FILL_UNKNOWN,
|
||||||
|
TextAnnotation.POSITION_BEFORE);
|
||||||
|
|
||||||
|
Spanned secondCue = getOnlyCueTextAtTimeUs(subtitle, 20_000_000);
|
||||||
|
assertThat(secondCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Auto ".length(), "Auto ございます".length())
|
||||||
|
.withMarkAndPosition(
|
||||||
|
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
|
||||||
|
TextEmphasisSpan.MARK_FILL_FILLED,
|
||||||
|
TextAnnotation.POSITION_BEFORE);
|
||||||
|
|
||||||
|
Spanned thirdCue = getOnlyCueTextAtTimeUs(subtitle, 30_000_000);
|
||||||
|
assertThat(thirdCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Filled circle ".length(), "Filled circle こんばんは".length())
|
||||||
|
.withMarkAndPosition(
|
||||||
|
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
|
||||||
|
TextEmphasisSpan.MARK_FILL_FILLED,
|
||||||
|
TextAnnotation.POSITION_BEFORE);
|
||||||
|
|
||||||
|
Spanned fourthCue = getOnlyCueTextAtTimeUs(subtitle, 40_000_000);
|
||||||
|
assertThat(fourthCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Filled dot ".length(), "Filled dot ございます".length())
|
||||||
|
.withMarkAndPosition(
|
||||||
|
TextEmphasisSpan.MARK_SHAPE_DOT,
|
||||||
|
TextEmphasisSpan.MARK_FILL_FILLED,
|
||||||
|
TextAnnotation.POSITION_BEFORE);
|
||||||
|
|
||||||
|
Spanned fifthCue = getOnlyCueTextAtTimeUs(subtitle, 50_000_000);
|
||||||
|
assertThat(fifthCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Filled sesame ".length(), "Filled sesame おはよ".length())
|
||||||
|
.withMarkAndPosition(
|
||||||
|
TextEmphasisSpan.MARK_SHAPE_SESAME,
|
||||||
|
TextEmphasisSpan.MARK_FILL_FILLED,
|
||||||
|
TextAnnotation.POSITION_BEFORE);
|
||||||
|
|
||||||
|
Spanned sixthCue = getOnlyCueTextAtTimeUs(subtitle, 60_000_000);
|
||||||
|
assertThat(sixthCue)
|
||||||
|
.hasTextEmphasisSpanBetween(
|
||||||
|
"Open circle before ".length(), "Open circle before ございます".length())
|
||||||
|
.withMarkAndPosition(
|
||||||
|
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
|
||||||
|
TextEmphasisSpan.MARK_FILL_OPEN,
|
||||||
|
TextAnnotation.POSITION_BEFORE);
|
||||||
|
|
||||||
|
Spanned seventhCue = getOnlyCueTextAtTimeUs(subtitle, 70_000_000);
|
||||||
|
assertThat(seventhCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Open dot after ".length(), "Open dot after おはよ".length())
|
||||||
|
.withMarkAndPosition(
|
||||||
|
TextEmphasisSpan.MARK_SHAPE_DOT,
|
||||||
|
TextEmphasisSpan.MARK_FILL_OPEN,
|
||||||
|
TextAnnotation.POSITION_AFTER);
|
||||||
|
|
||||||
|
Spanned eighthCue = getOnlyCueTextAtTimeUs(subtitle, 80_000_000);
|
||||||
|
assertThat(eighthCue)
|
||||||
|
.hasTextEmphasisSpanBetween(
|
||||||
|
"Open sesame outside ".length(), "Open sesame outside ございます".length())
|
||||||
|
.withMarkAndPosition(
|
||||||
|
TextEmphasisSpan.MARK_SHAPE_SESAME,
|
||||||
|
TextEmphasisSpan.MARK_FILL_OPEN,
|
||||||
|
TextAnnotation.POSITION_BEFORE);
|
||||||
|
|
||||||
|
Spanned ninthCue = getOnlyCueTextAtTimeUs(subtitle, 90_000_000);
|
||||||
|
assertThat(ninthCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Auto outside ".length(), "Auto outside おはよ".length())
|
||||||
|
.withMarkAndPosition(
|
||||||
|
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
|
||||||
|
TextEmphasisSpan.MARK_FILL_FILLED,
|
||||||
|
TextAnnotation.POSITION_BEFORE);
|
||||||
|
|
||||||
|
Spanned tenthCue = getOnlyCueTextAtTimeUs(subtitle, 100_000_000);
|
||||||
|
assertThat(tenthCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Circle before ".length(), "Circle before ございます".length())
|
||||||
|
.withMarkAndPosition(
|
||||||
|
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
|
||||||
|
TextEmphasisSpan.MARK_FILL_FILLED,
|
||||||
|
TextAnnotation.POSITION_BEFORE);
|
||||||
|
|
||||||
|
Spanned eleventhCue = getOnlyCueTextAtTimeUs(subtitle, 110_000_000);
|
||||||
|
assertThat(eleventhCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Sesame after ".length(), "Sesame after おはよ".length())
|
||||||
|
.withMarkAndPosition(
|
||||||
|
TextEmphasisSpan.MARK_SHAPE_SESAME,
|
||||||
|
TextEmphasisSpan.MARK_FILL_FILLED,
|
||||||
|
TextAnnotation.POSITION_AFTER);
|
||||||
|
|
||||||
|
Spanned twelfthCue = getOnlyCueTextAtTimeUs(subtitle, 120_000_000);
|
||||||
|
assertThat(twelfthCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Dot outside ".length(), "Dot outside ございます".length())
|
||||||
|
.withMarkAndPosition(
|
||||||
|
TextEmphasisSpan.MARK_SHAPE_DOT,
|
||||||
|
TextEmphasisSpan.MARK_FILL_FILLED,
|
||||||
|
TextAnnotation.POSITION_BEFORE);
|
||||||
|
|
||||||
|
Spanned thirteenthCue = getOnlyCueTextAtTimeUs(subtitle, 130_000_000);
|
||||||
|
assertThat(thirteenthCue)
|
||||||
|
.hasNoTextEmphasisSpanBetween(
|
||||||
|
"No textEmphasis property ".length(), "No textEmphasis property おはよ".length());
|
||||||
|
|
||||||
|
Spanned fourteenthCue = getOnlyCueTextAtTimeUs(subtitle, 140_000_000);
|
||||||
|
assertThat(fourteenthCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Auto (TBLR) ".length(), "Auto (TBLR) ございます".length())
|
||||||
|
.withMarkAndPosition(
|
||||||
|
TextEmphasisSpan.MARK_SHAPE_SESAME,
|
||||||
|
TextEmphasisSpan.MARK_FILL_FILLED,
|
||||||
|
TextAnnotation.POSITION_BEFORE);
|
||||||
|
|
||||||
|
Spanned fifteenthCue = getOnlyCueTextAtTimeUs(subtitle, 150_000_000);
|
||||||
|
assertThat(fifteenthCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Auto (TBRL) ".length(), "Auto (TBRL) おはよ".length())
|
||||||
|
.withMarkAndPosition(
|
||||||
|
TextEmphasisSpan.MARK_SHAPE_SESAME,
|
||||||
|
TextEmphasisSpan.MARK_FILL_FILLED,
|
||||||
|
TextAnnotation.POSITION_BEFORE);
|
||||||
|
|
||||||
|
Spanned sixteenthCue = getOnlyCueTextAtTimeUs(subtitle, 160_000_000);
|
||||||
|
assertThat(sixteenthCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Auto (TB) ".length(), "Auto (TB) ございます".length())
|
||||||
|
.withMarkAndPosition(
|
||||||
|
TextEmphasisSpan.MARK_SHAPE_SESAME,
|
||||||
|
TextEmphasisSpan.MARK_FILL_FILLED,
|
||||||
|
TextAnnotation.POSITION_BEFORE);
|
||||||
|
|
||||||
|
Spanned seventeenthCue = getOnlyCueTextAtTimeUs(subtitle, 170_000_000);
|
||||||
|
assertThat(seventeenthCue)
|
||||||
|
.hasTextEmphasisSpanBetween("Auto (LR) ".length(), "Auto (LR) おはよ".length())
|
||||||
|
.withMarkAndPosition(
|
||||||
|
TextEmphasisSpan.MARK_SHAPE_CIRCLE,
|
||||||
|
TextEmphasisSpan.MARK_FILL_FILLED,
|
||||||
|
TextAnnotation.POSITION_BEFORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shear() throws IOException, SubtitleDecoderException {
|
||||||
|
TtmlSubtitle subtitle = getSubtitle(SHEAR_FILE);
|
||||||
|
|
||||||
|
Cue firstCue = getOnlyCueAtTimeUs(subtitle, 10_000_000);
|
||||||
|
assertThat(firstCue.shearDegrees).isZero();
|
||||||
|
|
||||||
|
Cue secondCue = getOnlyCueAtTimeUs(subtitle, 20_000_000);
|
||||||
|
assertThat(secondCue.shearDegrees).isWithin(0.01f).of(-15f);
|
||||||
|
|
||||||
|
Cue thirdCue = getOnlyCueAtTimeUs(subtitle, 30_000_000);
|
||||||
|
assertThat(thirdCue.shearDegrees).isWithin(0.01f).of(15f);
|
||||||
|
|
||||||
|
Cue fourthCue = getOnlyCueAtTimeUs(subtitle, 40_000_000);
|
||||||
|
assertThat(fourthCue.shearDegrees).isWithin(0.01f).of(-15f);
|
||||||
|
|
||||||
|
Cue fifthCue = getOnlyCueAtTimeUs(subtitle, 50_000_000);
|
||||||
|
assertThat(fifthCue.shearDegrees).isWithin(0.01f).of(-22.5f);
|
||||||
|
|
||||||
|
Cue sixthCue = getOnlyCueAtTimeUs(subtitle, 60_000_000);
|
||||||
|
assertThat(sixthCue.shearDegrees).isWithin(0.01f).of(0f);
|
||||||
|
|
||||||
|
Cue seventhCue = getOnlyCueAtTimeUs(subtitle, 70_000_000);
|
||||||
|
assertThat(seventhCue.shearDegrees).isWithin(0.01f).of(-90f);
|
||||||
|
|
||||||
|
Cue eighthCue = getOnlyCueAtTimeUs(subtitle, 80_000_000);
|
||||||
|
assertThat(eighthCue.shearDegrees).isWithin(0.01f).of(90f);
|
||||||
|
}
|
||||||
|
|
||||||
private static Spanned getOnlyCueTextAtTimeUs(Subtitle subtitle, long timeUs) {
|
private static Spanned getOnlyCueTextAtTimeUs(Subtitle subtitle, long timeUs) {
|
||||||
Cue cue = getOnlyCueAtTimeUs(subtitle, timeUs);
|
Cue cue = getOnlyCueAtTimeUs(subtitle, timeUs);
|
||||||
assertThat(cue.text).isInstanceOf(Spanned.class);
|
assertThat(cue.text).isInstanceOf(Spanned.class);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
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 {
|
||||||
@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 {
|
||||||
.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 {
|
||||||
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 {
|
||||||
.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 {
|
||||||
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 {
|
||||||
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;
|
||||||
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 {
|
||||||
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 {
|
||||||
.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;
|
||||||
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
|
||||||
|
|
||||||
// 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
|
||||||
@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
|
||||||
@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
|
||||||
@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
|
||||||
}
|
}
|
||||||
|
|
||||||
@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
|
||||||
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
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
@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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
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;
|
||||||
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 {
|
||||||
// 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 {
|
||||||
// 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) {
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,14 @@ public final class JpegExtractorTest {
|
||||||
JpegExtractor::new, "media/jpeg/pixel-motion-photo-shortened.jpg", simulationConfig);
|
JpegExtractor::new, "media/jpeg/pixel-motion-photo-shortened.jpg", simulationConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void samplePixelMotionPhotoJfifSegmentShortened() throws Exception {
|
||||||
|
ExtractorAsserts.assertBehavior(
|
||||||
|
JpegExtractor::new,
|
||||||
|
"media/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg",
|
||||||
|
simulationConfig);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void samplePixelMotionPhotoVideoRemovedShortened() throws Exception {
|
public void samplePixelMotionPhotoVideoRemovedShortened() throws Exception {
|
||||||
ExtractorAsserts.assertBehavior(
|
ExtractorAsserts.assertBehavior(
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -34,6 +38,19 @@ import org.junit.runner.RunWith;
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public final class OggExtractorNonParameterizedTest {
|
public final class OggExtractorNonParameterizedTest {
|
||||||
|
|
||||||
|
@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
|
@Test
|
||||||
public void sniffVorbis() throws Exception {
|
public void sniffVorbis() throws Exception {
|
||||||
byte[] data =
|
byte[] data =
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
// 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 {
|
||||||
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 {
|
||||||
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;
|
||||||
|
|
||||||
@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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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;
|
||||||
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
|
||||||
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
|
||||||
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
|
||||||
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;
|
||||||
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;
|
||||||
@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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
@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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -292,6 +292,44 @@ public class HlsMediaSourceTest {
|
||||||
assertThat(window.defaultPositionUs).isEqualTo(0);
|
assertThat(window.defaultPositionUs).isEqualTo(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
@Test
|
||||||
public void loadPlaylist_targetLiveOffsetInMediaItem_targetLiveOffsetPickedFromMediaItem()
|
public void loadPlaylist_targetLiveOffsetInMediaItem_targetLiveOffsetPickedFromMediaItem()
|
||||||
throws TimeoutException, ParserException {
|
throws TimeoutException, ParserException {
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,52 @@ public class HlsMediaPlaylistParserTest {
|
||||||
assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2683.ts");
|
assertThat(segment.url).isEqualTo("https://priv.example.com/fileSequence2683.ts");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
@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");
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
} 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;
|
||||||
|| 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;
|
||||||
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) {
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
* <li>Corresponding method: {@link #setShowShuffleButton(boolean)}
|
* <li>Corresponding method: {@link #setShowShuffleButton(boolean)}
|
||||||
* <li>Default: false
|
* <li>Default: false
|
||||||
* </ul>
|
* </ul>
|
||||||
* <li><b>{@code show_subtitle_button}</b> - Whether the shuffle button is shown.
|
* <li><b>{@code show_subtitle_button}</b> - Whether the subtitle button is shown.
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Corresponding method: {@link #setShowSubtitleButton(boolean)}
|
* <li>Corresponding method: {@link #setShowSubtitleButton(boolean)}
|
||||||
* <li>Default: false
|
* <li>Default: false
|
||||||
|
|
@ -436,14 +436,10 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||||
private StyledPlayerControlViewLayoutManager controlViewLayoutManager;
|
private StyledPlayerControlViewLayoutManager controlViewLayoutManager;
|
||||||
private Resources resources;
|
private Resources resources;
|
||||||
|
|
||||||
private int selectedMainSettingsPosition;
|
|
||||||
private RecyclerView settingsView;
|
private RecyclerView settingsView;
|
||||||
private SettingsAdapter settingsAdapter;
|
private SettingsAdapter settingsAdapter;
|
||||||
private SubSettingsAdapter subSettingsAdapter;
|
private PlaybackSpeedAdapter playbackSpeedAdapter;
|
||||||
private PopupWindow settingsWindow;
|
private PopupWindow settingsWindow;
|
||||||
private String[] playbackSpeedTexts;
|
|
||||||
private int[] playbackSpeedsMultBy100;
|
|
||||||
private int selectedPlaybackSpeedIndex;
|
|
||||||
private boolean needToHideBars;
|
private boolean needToHideBars;
|
||||||
private int settingsWindowMargin;
|
private int settingsWindowMargin;
|
||||||
|
|
||||||
|
|
@ -457,6 +453,8 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||||
@Nullable private ImageView fullScreenButton;
|
@Nullable private ImageView fullScreenButton;
|
||||||
@Nullable private ImageView minimalFullScreenButton;
|
@Nullable private ImageView minimalFullScreenButton;
|
||||||
@Nullable private View settingsButton;
|
@Nullable private View settingsButton;
|
||||||
|
@Nullable private View playbackSpeedButton;
|
||||||
|
@Nullable private View audioTrackButton;
|
||||||
|
|
||||||
public StyledPlayerControlView(Context context) {
|
public StyledPlayerControlView(Context context) {
|
||||||
this(context, /* attrs= */ null);
|
this(context, /* attrs= */ null);
|
||||||
|
|
@ -575,6 +573,16 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||||
settingsButton.setOnClickListener(componentListener);
|
settingsButton.setOnClickListener(componentListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playbackSpeedButton = findViewById(R.id.exo_playback_speed);
|
||||||
|
if (playbackSpeedButton != null) {
|
||||||
|
playbackSpeedButton.setOnClickListener(componentListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
audioTrackButton = findViewById(R.id.exo_audio_track);
|
||||||
|
if (audioTrackButton != null) {
|
||||||
|
audioTrackButton.setOnClickListener(componentListener);
|
||||||
|
}
|
||||||
|
|
||||||
TimeBar customTimeBar = findViewById(R.id.exo_progress);
|
TimeBar customTimeBar = findViewById(R.id.exo_progress);
|
||||||
View timeBarPlaceholder = findViewById(R.id.exo_progress_placeholder);
|
View timeBarPlaceholder = findViewById(R.id.exo_progress_placeholder);
|
||||||
if (customTimeBar != null) {
|
if (customTimeBar != null) {
|
||||||
|
|
@ -663,12 +671,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||||
settingIcons[SETTINGS_AUDIO_TRACK_SELECTION_POSITION] =
|
settingIcons[SETTINGS_AUDIO_TRACK_SELECTION_POSITION] =
|
||||||
resources.getDrawable(R.drawable.exo_styled_controls_audiotrack);
|
resources.getDrawable(R.drawable.exo_styled_controls_audiotrack);
|
||||||
settingsAdapter = new SettingsAdapter(settingTexts, settingIcons);
|
settingsAdapter = new SettingsAdapter(settingTexts, settingIcons);
|
||||||
|
|
||||||
playbackSpeedTexts = resources.getStringArray(R.array.exo_playback_speeds);
|
|
||||||
playbackSpeedsMultBy100 = resources.getIntArray(R.array.exo_speed_multiplied_by_100);
|
|
||||||
settingsWindowMargin = resources.getDimensionPixelSize(R.dimen.exo_settings_offset);
|
settingsWindowMargin = resources.getDimensionPixelSize(R.dimen.exo_settings_offset);
|
||||||
|
|
||||||
subSettingsAdapter = new SubSettingsAdapter();
|
|
||||||
settingsView =
|
settingsView =
|
||||||
(RecyclerView)
|
(RecyclerView)
|
||||||
LayoutInflater.from(context).inflate(R.layout.exo_styled_settings_list, null);
|
LayoutInflater.from(context).inflate(R.layout.exo_styled_settings_list, null);
|
||||||
|
|
@ -693,6 +696,10 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||||
resources.getString(R.string.exo_controls_cc_disabled_description);
|
resources.getString(R.string.exo_controls_cc_disabled_description);
|
||||||
textTrackSelectionAdapter = new TextTrackSelectionAdapter();
|
textTrackSelectionAdapter = new TextTrackSelectionAdapter();
|
||||||
audioTrackSelectionAdapter = new AudioTrackSelectionAdapter();
|
audioTrackSelectionAdapter = new AudioTrackSelectionAdapter();
|
||||||
|
playbackSpeedAdapter =
|
||||||
|
new PlaybackSpeedAdapter(
|
||||||
|
resources.getStringArray(R.array.exo_playback_speeds),
|
||||||
|
resources.getIntArray(R.array.exo_speed_multiplied_by_100));
|
||||||
|
|
||||||
fullScreenExitDrawable = resources.getDrawable(R.drawable.exo_styled_controls_fullscreen_exit);
|
fullScreenExitDrawable = resources.getDrawable(R.drawable.exo_styled_controls_fullscreen_exit);
|
||||||
fullScreenEnterDrawable =
|
fullScreenEnterDrawable =
|
||||||
|
|
@ -770,7 +777,6 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||||
this.trackSelector = null;
|
this.trackSelector = null;
|
||||||
}
|
}
|
||||||
updateAll();
|
updateAll();
|
||||||
updateSettingsPlaybackSpeedLists();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1102,6 +1108,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||||
updateRepeatModeButton();
|
updateRepeatModeButton();
|
||||||
updateShuffleButton();
|
updateShuffleButton();
|
||||||
updateTrackLists();
|
updateTrackLists();
|
||||||
|
updatePlaybackSpeedList();
|
||||||
updateTimeline();
|
updateTimeline();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1437,24 +1444,13 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSettingsPlaybackSpeedLists() {
|
private void updatePlaybackSpeedList() {
|
||||||
if (player == null) {
|
if (player == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
float speed = player.getPlaybackParameters().speed;
|
playbackSpeedAdapter.updateSelectedIndex(player.getPlaybackParameters().speed);
|
||||||
int currentSpeedMultBy100 = Math.round(speed * 100);
|
|
||||||
int closestMatchIndex = 0;
|
|
||||||
int closestMatchDifference = Integer.MAX_VALUE;
|
|
||||||
for (int i = 0; i < playbackSpeedsMultBy100.length; i++) {
|
|
||||||
int difference = Math.abs(currentSpeedMultBy100 - playbackSpeedsMultBy100[i]);
|
|
||||||
if (difference < closestMatchDifference) {
|
|
||||||
closestMatchIndex = i;
|
|
||||||
closestMatchDifference = difference;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
selectedPlaybackSpeedIndex = closestMatchIndex;
|
|
||||||
settingsAdapter.setSubTextAtPosition(
|
settingsAdapter.setSubTextAtPosition(
|
||||||
SETTINGS_PLAYBACK_SPEED_POSITION, playbackSpeedTexts[closestMatchIndex]);
|
SETTINGS_PLAYBACK_SPEED_POSITION, playbackSpeedAdapter.getSelectedText());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSettingsWindowSize() {
|
private void updateSettingsWindowSize() {
|
||||||
|
|
@ -1570,50 +1566,14 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||||
|
|
||||||
private void onSettingViewClicked(int position) {
|
private void onSettingViewClicked(int position) {
|
||||||
if (position == SETTINGS_PLAYBACK_SPEED_POSITION) {
|
if (position == SETTINGS_PLAYBACK_SPEED_POSITION) {
|
||||||
subSettingsAdapter.init(playbackSpeedTexts, selectedPlaybackSpeedIndex);
|
displaySettingsWindow(playbackSpeedAdapter);
|
||||||
selectedMainSettingsPosition = SETTINGS_PLAYBACK_SPEED_POSITION;
|
|
||||||
displaySettingsWindow(subSettingsAdapter);
|
|
||||||
} else if (position == SETTINGS_AUDIO_TRACK_SELECTION_POSITION) {
|
} else if (position == SETTINGS_AUDIO_TRACK_SELECTION_POSITION) {
|
||||||
selectedMainSettingsPosition = SETTINGS_AUDIO_TRACK_SELECTION_POSITION;
|
|
||||||
displaySettingsWindow(audioTrackSelectionAdapter);
|
displaySettingsWindow(audioTrackSelectionAdapter);
|
||||||
} else {
|
} else {
|
||||||
settingsWindow.dismiss();
|
settingsWindow.dismiss();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onSubSettingViewClicked(int position) {
|
|
||||||
if (selectedMainSettingsPosition == SETTINGS_PLAYBACK_SPEED_POSITION) {
|
|
||||||
if (position != selectedPlaybackSpeedIndex) {
|
|
||||||
float speed = playbackSpeedsMultBy100[position] / 100.0f;
|
|
||||||
setPlaybackSpeed(speed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
settingsWindow.dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onLayoutChange(
|
|
||||||
View v,
|
|
||||||
int left,
|
|
||||||
int top,
|
|
||||||
int right,
|
|
||||||
int bottom,
|
|
||||||
int oldLeft,
|
|
||||||
int oldTop,
|
|
||||||
int oldRight,
|
|
||||||
int oldBottom) {
|
|
||||||
int width = right - left;
|
|
||||||
int height = bottom - top;
|
|
||||||
int oldWidth = oldRight - oldLeft;
|
|
||||||
int oldHeight = oldBottom - oldTop;
|
|
||||||
|
|
||||||
if ((width != oldWidth || height != oldHeight) && settingsWindow.isShowing()) {
|
|
||||||
updateSettingsWindowSize();
|
|
||||||
int xOffset = getWidth() - settingsWindow.getWidth() - settingsWindowMargin;
|
|
||||||
int yOffset = -settingsWindow.getHeight() - settingsWindowMargin;
|
|
||||||
settingsWindow.update(v, xOffset, yOffset, -1, -1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttachedToWindow() {
|
public void onAttachedToWindow() {
|
||||||
super.onAttachedToWindow();
|
super.onAttachedToWindow();
|
||||||
|
|
@ -1685,6 +1645,35 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||||
|
super.onLayout(changed, left, top, right, bottom);
|
||||||
|
controlViewLayoutManager.onLayout(changed, left, top, right, bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onLayoutChange(
|
||||||
|
View v,
|
||||||
|
int left,
|
||||||
|
int top,
|
||||||
|
int right,
|
||||||
|
int bottom,
|
||||||
|
int oldLeft,
|
||||||
|
int oldTop,
|
||||||
|
int oldRight,
|
||||||
|
int oldBottom) {
|
||||||
|
int width = right - left;
|
||||||
|
int height = bottom - top;
|
||||||
|
int oldWidth = oldRight - oldLeft;
|
||||||
|
int oldHeight = oldBottom - oldTop;
|
||||||
|
|
||||||
|
if ((width != oldWidth || height != oldHeight) && settingsWindow.isShowing()) {
|
||||||
|
updateSettingsWindowSize();
|
||||||
|
int xOffset = getWidth() - settingsWindow.getWidth() - settingsWindowMargin;
|
||||||
|
int yOffset = -settingsWindow.getHeight() - settingsWindowMargin;
|
||||||
|
settingsWindow.update(v, xOffset, yOffset, -1, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean shouldShowPauseButton() {
|
private boolean shouldShowPauseButton() {
|
||||||
return player != null
|
return player != null
|
||||||
&& player.getPlaybackState() != Player.STATE_ENDED
|
&& player.getPlaybackState() != Player.STATE_ENDED
|
||||||
|
|
@ -1835,7 +1824,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||||
updateTimeline();
|
updateTimeline();
|
||||||
}
|
}
|
||||||
if (events.contains(EVENT_PLAYBACK_PARAMETERS_CHANGED)) {
|
if (events.contains(EVENT_PLAYBACK_PARAMETERS_CHANGED)) {
|
||||||
updateSettingsPlaybackSpeedLists();
|
updatePlaybackSpeedList();
|
||||||
}
|
}
|
||||||
if (events.contains(EVENT_TRACKS_CHANGED)) {
|
if (events.contains(EVENT_TRACKS_CHANGED)) {
|
||||||
updateTrackLists();
|
updateTrackLists();
|
||||||
|
|
@ -1876,6 +1865,12 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||||
} else if (settingsButton == view) {
|
} else if (settingsButton == view) {
|
||||||
controlViewLayoutManager.removeHideCallbacks();
|
controlViewLayoutManager.removeHideCallbacks();
|
||||||
displaySettingsWindow(settingsAdapter);
|
displaySettingsWindow(settingsAdapter);
|
||||||
|
} else if (playbackSpeedButton == view) {
|
||||||
|
controlViewLayoutManager.removeHideCallbacks();
|
||||||
|
displaySettingsWindow(playbackSpeedAdapter);
|
||||||
|
} else if (audioTrackButton == view) {
|
||||||
|
controlViewLayoutManager.removeHideCallbacks();
|
||||||
|
displaySettingsWindow(audioTrackSelectionAdapter);
|
||||||
} else if (subtitleButton == view) {
|
} else if (subtitleButton == view) {
|
||||||
controlViewLayoutManager.removeHideCallbacks();
|
controlViewLayoutManager.removeHideCallbacks();
|
||||||
displaySettingsWindow(textTrackSelectionAdapter);
|
displaySettingsWindow(textTrackSelectionAdapter);
|
||||||
|
|
@ -1949,18 +1944,33 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class SubSettingsAdapter extends RecyclerView.Adapter<SubSettingViewHolder> {
|
private final class PlaybackSpeedAdapter extends RecyclerView.Adapter<SubSettingViewHolder> {
|
||||||
|
|
||||||
private String[] texts;
|
private final String[] playbackSpeedTexts;
|
||||||
|
private final int[] playbackSpeedsMultBy100;
|
||||||
private int selectedIndex;
|
private int selectedIndex;
|
||||||
|
|
||||||
public SubSettingsAdapter() {
|
public PlaybackSpeedAdapter(String[] playbackSpeedTexts, int[] playbackSpeedsMultBy100) {
|
||||||
texts = new String[0];
|
this.playbackSpeedTexts = playbackSpeedTexts;
|
||||||
|
this.playbackSpeedsMultBy100 = playbackSpeedsMultBy100;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void init(String[] texts, int selectedIndex) {
|
public void updateSelectedIndex(float playbackSpeed) {
|
||||||
this.texts = texts;
|
int currentSpeedMultBy100 = Math.round(playbackSpeed * 100);
|
||||||
this.selectedIndex = selectedIndex;
|
int closestMatchIndex = 0;
|
||||||
|
int closestMatchDifference = Integer.MAX_VALUE;
|
||||||
|
for (int i = 0; i < playbackSpeedsMultBy100.length; i++) {
|
||||||
|
int difference = Math.abs(currentSpeedMultBy100 - playbackSpeedsMultBy100[i]);
|
||||||
|
if (difference < closestMatchDifference) {
|
||||||
|
closestMatchIndex = i;
|
||||||
|
closestMatchDifference = difference;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedIndex = closestMatchIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSelectedText() {
|
||||||
|
return playbackSpeedTexts[selectedIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -1973,27 +1983,23 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(SubSettingViewHolder holder, int position) {
|
public void onBindViewHolder(SubSettingViewHolder holder, int position) {
|
||||||
if (position < texts.length) {
|
if (position < playbackSpeedTexts.length) {
|
||||||
holder.textView.setText(texts[position]);
|
holder.textView.setText(playbackSpeedTexts[position]);
|
||||||
}
|
}
|
||||||
holder.checkView.setVisibility(position == selectedIndex ? VISIBLE : INVISIBLE);
|
holder.checkView.setVisibility(position == selectedIndex ? VISIBLE : INVISIBLE);
|
||||||
|
holder.itemView.setOnClickListener(
|
||||||
|
v -> {
|
||||||
|
if (position != selectedIndex) {
|
||||||
|
float speed = playbackSpeedsMultBy100[position] / 100.0f;
|
||||||
|
setPlaybackSpeed(speed);
|
||||||
|
}
|
||||||
|
settingsWindow.dismiss();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemCount() {
|
public int getItemCount() {
|
||||||
return texts.length;
|
return playbackSpeedTexts.length;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class SubSettingViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
private final TextView textView;
|
|
||||||
private final View checkView;
|
|
||||||
|
|
||||||
public SubSettingViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
textView = itemView.findViewById(R.id.exo_text);
|
|
||||||
checkView = itemView.findViewById(R.id.exo_check);
|
|
||||||
itemView.setOnClickListener(v -> onSubSettingViewClicked(getAdapterPosition()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2041,7 +2047,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolderAtZeroPosition(TrackSelectionViewHolder holder) {
|
public void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder) {
|
||||||
// CC options include "Off" at the first position, which disables text rendering.
|
// CC options include "Off" at the first position, which disables text rendering.
|
||||||
holder.textView.setText(R.string.exo_track_selection_none);
|
holder.textView.setText(R.string.exo_track_selection_none);
|
||||||
boolean isTrackSelectionOff = true;
|
boolean isTrackSelectionOff = true;
|
||||||
|
|
@ -2070,7 +2076,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(TrackSelectionViewHolder holder, int position) {
|
public void onBindViewHolder(SubSettingViewHolder holder, int position) {
|
||||||
super.onBindViewHolder(holder, position);
|
super.onBindViewHolder(holder, position);
|
||||||
if (position > 0) {
|
if (position > 0) {
|
||||||
TrackInfo track = tracks.get(position - 1);
|
TrackInfo track = tracks.get(position - 1);
|
||||||
|
|
@ -2087,7 +2093,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||||
private final class AudioTrackSelectionAdapter extends TrackSelectionAdapter {
|
private final class AudioTrackSelectionAdapter extends TrackSelectionAdapter {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolderAtZeroPosition(TrackSelectionViewHolder holder) {
|
public void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder) {
|
||||||
// Audio track selection option includes "Auto" at the top.
|
// Audio track selection option includes "Auto" at the top.
|
||||||
holder.textView.setText(R.string.exo_track_selection_auto);
|
holder.textView.setText(R.string.exo_track_selection_auto);
|
||||||
// hasSelectionOverride is true means there is an explicit track selection, not "Auto".
|
// hasSelectionOverride is true means there is an explicit track selection, not "Auto".
|
||||||
|
|
@ -2166,8 +2172,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private abstract class TrackSelectionAdapter
|
private abstract class TrackSelectionAdapter extends RecyclerView.Adapter<SubSettingViewHolder> {
|
||||||
extends RecyclerView.Adapter<TrackSelectionViewHolder> {
|
|
||||||
|
|
||||||
protected List<Integer> rendererIndices;
|
protected List<Integer> rendererIndices;
|
||||||
protected List<TrackInfo> tracks;
|
protected List<TrackInfo> tracks;
|
||||||
|
|
@ -2183,19 +2188,19 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||||
List<Integer> rendererIndices, List<TrackInfo> trackInfos, MappedTrackInfo mappedTrackInfo);
|
List<Integer> rendererIndices, List<TrackInfo> trackInfos, MappedTrackInfo mappedTrackInfo);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TrackSelectionViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
public SubSettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||||
View v =
|
View v =
|
||||||
LayoutInflater.from(getContext())
|
LayoutInflater.from(getContext())
|
||||||
.inflate(R.layout.exo_styled_sub_settings_list_item, null);
|
.inflate(R.layout.exo_styled_sub_settings_list_item, null);
|
||||||
return new TrackSelectionViewHolder(v);
|
return new SubSettingViewHolder(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract void onBindViewHolderAtZeroPosition(TrackSelectionViewHolder holder);
|
public abstract void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder);
|
||||||
|
|
||||||
public abstract void onTrackSelection(String subtext);
|
public abstract void onTrackSelection(String subtext);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(TrackSelectionViewHolder holder, int position) {
|
public void onBindViewHolder(SubSettingViewHolder holder, int position) {
|
||||||
if (trackSelector == null || mappedTrackInfo == null) {
|
if (trackSelector == null || mappedTrackInfo == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -2251,12 +2256,12 @@ public class StyledPlayerControlView extends FrameLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class TrackSelectionViewHolder extends RecyclerView.ViewHolder {
|
private static class SubSettingViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
public final TextView textView;
|
public final TextView textView;
|
||||||
public final View checkView;
|
public final View checkView;
|
||||||
|
|
||||||
public TrackSelectionViewHolder(View itemView) {
|
public SubSettingViewHolder(View itemView) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
textView = itemView.findViewById(R.id.exo_text);
|
textView = itemView.findViewById(R.id.exo_text);
|
||||||
checkView = itemView.findViewById(R.id.exo_check);
|
checkView = itemView.findViewById(R.id.exo_check);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
- (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;
|
||||||
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;
|
||||||
+ "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;
|
||||||
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;
|
||||||
"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 @@
|
||||||
-->
|
-->
|
||||||
<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 @@
|
||||||
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 @@
|
||||||
<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 @@
|
||||||
<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;
|
||||||
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 {
|
||||||
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);
|
||||||
|
|
@ -279,6 +281,42 @@ public class SpannedToHtmlConverterTest {
|
||||||
+ "section");
|
+ "section");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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;'>おはよ</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;'>ございます</span>");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@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.");
|
||||||
|
|
|
||||||
119
publish.gradle
119
publish.gradle
|
|
@ -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 {
|
developers {
|
||||||
name = 'The Android Open Source Project'
|
developer {
|
||||||
}
|
name = 'The Android Open Source Project'
|
||||||
}
|
|
||||||
scm {
|
|
||||||
connection = 'scm:git:https://github.com/google/ExoPlayer.git'
|
|
||||||
url = 'https://github.com/google/ExoPlayer'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
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 {
|
||||||
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;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
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
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
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
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
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
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
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
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
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
|
||||||
BIN
testdata/src/test/assets/media/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg
vendored
Normal file
BIN
testdata/src/test/assets/media/jpeg/pixel-motion-photo-jfif-segment-shortened.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -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>
|
||||||
|
|
|
||||||
18
testdata/src/test/assets/media/ssa/style_bold_italic
vendored
Normal file
18
testdata/src/test/assets/media/ssa/style_bold_italic
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
[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.
|
||||||
32
testdata/src/test/assets/media/ssa/style_colors
vendored
32
testdata/src/test/assets/media/ssa/style_colors
vendored
|
|
@ -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
|
||||||
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.
|
||||||
|
|
|
||||||
32
testdata/src/test/assets/media/ttml/shear.xml
vendored
Normal file
32
testdata/src/test/assets/media/ttml/shear.xml
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<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>
|
||||||
65
testdata/src/test/assets/media/ttml/text_emphasis.xml
vendored
Normal file
65
testdata/src/test/assets/media/ttml/text_emphasis.xml
vendored
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<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):
|
||||||
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):
|
||||||
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):
|
||||||
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 @@
|
||||||
*/
|
*/
|
||||||
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 {
|
||||||
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 {
|
||||||
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 {
|
||||||
@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 {
|
||||||
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 {
|
||||||
* 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(
|
||||||
() -> {
|
() -> {
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue