mirror of
https://github.com/samsonjs/media.git
synced 2026-04-19 13:35:47 +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 ##
|
||||
|
||||
ExoPlayer modules can be obtained from JCenter. It's also possible to clone the
|
||||
repository and depend on the modules locally.
|
||||
ExoPlayer modules can be obtained from [the Google Maven repository][]. It's
|
||||
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
|
||||
dependency. You need to make sure you have the Google and JCenter repositories
|
||||
included in the `build.gradle` file in the root of your project:
|
||||
|
||||
```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:
|
||||
dependency in the `build.gradle` file of your app module. The following will add
|
||||
a dependency to the full library:
|
||||
|
||||
```gradle
|
||||
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.
|
||||
|
||||
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
|
||||
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
|
||||
|
|
@ -72,18 +63,19 @@ individually.
|
|||
* `exoplayer-smoothstreaming`: Support for SmoothStreaming content.
|
||||
* `exoplayer-ui`: UI components and resources for use with ExoPlayer.
|
||||
|
||||
In addition to library modules, ExoPlayer has multiple extension modules that
|
||||
depend on external libraries to provide additional functionality. Some
|
||||
extensions are available from JCenter, whereas others must be built manually.
|
||||
In addition to library modules, ExoPlayer has extension modules that depend on
|
||||
external libraries to provide additional functionality. Some extensions are
|
||||
available from the Maven repository, whereas others must be built manually.
|
||||
Browse the [extensions directory][] and their individual READMEs for details.
|
||||
|
||||
More information on the library and extension modules that are available from
|
||||
JCenter can be found on [Bintray][].
|
||||
More information on the library and extension modules that are available can be
|
||||
found on the [Google Maven ExoPlayer page][].
|
||||
|
||||
[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
|
||||
`build.gradle` files depending on ExoPlayer, by adding the following to the
|
||||
|
|
|
|||
|
|
@ -1,5 +1,79 @@
|
|||
# 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)
|
||||
|
||||
* Extractors:
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ buildscript {
|
|||
}
|
||||
dependencies {
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
|
@ -27,9 +26,6 @@ allprojects {
|
|||
google()
|
||||
jcenter()
|
||||
}
|
||||
project.ext {
|
||||
exoplayerPublishEnabled = false
|
||||
}
|
||||
if (it.hasProperty('externalBuildDir')) {
|
||||
if (!new File(externalBuildDir).isAbsolute()) {
|
||||
externalBuildDir = new File(rootDir, externalBuildDir)
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@
|
|||
// limitations under the License.
|
||||
project.ext {
|
||||
// ExoPlayer version and version code.
|
||||
releaseVersion = '2.13.2'
|
||||
releaseVersionCode = 2013002
|
||||
releaseVersion = '2.13.3'
|
||||
releaseVersionCode = 2013003
|
||||
minSdkVersion = 16
|
||||
appTargetSdkVersion = 29
|
||||
targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest.
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ android {
|
|||
"proguard-rules.txt",
|
||||
getDefaultProguardFile('proguard-android.txt')
|
||||
]
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
debug {
|
||||
jniDebuggable = true
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ android {
|
|||
shrinkResources true
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ android {
|
|||
"proguard-rules.txt",
|
||||
getDefaultProguardFile('proguard-android.txt')
|
||||
]
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
debug {
|
||||
jniDebuggable = true
|
||||
|
|
|
|||
|
|
@ -485,6 +485,13 @@
|
|||
"subtitle_mime_type": "application/ttml+xml",
|
||||
"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",
|
||||
"uri": "https://html5demos.com/assets/dizzy.mp4",
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import android.net.Uri;
|
|||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
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.Util;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
|
@ -42,12 +43,12 @@ public class IntentUtil {
|
|||
"com.google.android.exoplayer.demo.action.VIEW_LIST";
|
||||
|
||||
// Activity extras.
|
||||
|
||||
public static final String PREFER_EXTENSION_DECODERS_EXTRA = "prefer_extension_decoders";
|
||||
|
||||
// Media item configuration extras.
|
||||
|
||||
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 CLIP_START_POSITION_MS_EXTRA = "clip_start_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.PlaybackProperties playbackProperties = checkNotNull(mediaItem.playbackProperties);
|
||||
intent.setAction(ACTION_VIEW).setData(mediaItem.playbackProperties.uri);
|
||||
if (mediaItem.mediaMetadata.title != null) {
|
||||
intent.putExtra(TITLE_EXTRA, mediaItem.mediaMetadata.title);
|
||||
}
|
||||
addPlaybackPropertiesToIntent(playbackProperties, intent, /* extrasKeySuffix= */ "");
|
||||
addClippingPropertiesToIntent(
|
||||
mediaItem.clippingProperties, intent, /* extrasKeySuffix= */ "");
|
||||
|
|
@ -102,6 +106,9 @@ public class IntentUtil {
|
|||
addPlaybackPropertiesToIntent(playbackProperties, intent, /* extrasKeySuffix= */ "_" + i);
|
||||
addClippingPropertiesToIntent(
|
||||
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(
|
||||
Uri uri, Intent intent, String extrasKeySuffix) {
|
||||
@Nullable String mimeType = intent.getStringExtra(MIME_TYPE_EXTRA + extrasKeySuffix);
|
||||
@Nullable String title = intent.getStringExtra(TITLE_EXTRA + extrasKeySuffix);
|
||||
MediaItem.Builder builder =
|
||||
new MediaItem.Builder()
|
||||
.setUri(uri)
|
||||
.setMimeType(mimeType)
|
||||
.setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build())
|
||||
.setAdTagUri(intent.getStringExtra(AD_TAG_URI_EXTRA + extrasKeySuffix))
|
||||
.setSubtitles(createSubtitlesFromIntent(intent, extrasKeySuffix))
|
||||
.setClipStartPositionMs(
|
||||
|
|
|
|||
|
|
@ -440,8 +440,8 @@ public class PlayerActivity extends AppCompatActivity
|
|||
@Override
|
||||
public void onPlayerError(@NonNull ExoPlaybackException e) {
|
||||
if (isBehindLiveWindow(e)) {
|
||||
clearStartPosition();
|
||||
initializePlayer();
|
||||
player.seekToDefaultPosition();
|
||||
player.prepare();
|
||||
} else {
|
||||
updateButtonVisibility();
|
||||
showControls();
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ android {
|
|||
shrinkResources true
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -653,15 +653,7 @@ public final class CastPlayer extends BasePlayer {
|
|||
updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null);
|
||||
updateTimelineAndNotifyIfChanged();
|
||||
|
||||
int currentWindowIndex = C.INDEX_UNSET;
|
||||
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;
|
||||
}
|
||||
int currentWindowIndex = fetchCurrentWindowIndex(remoteMediaClient, currentTimeline);
|
||||
if (this.currentWindowIndex != currentWindowIndex && pendingSeekCount == 0) {
|
||||
this.currentWindowIndex = currentWindowIndex;
|
||||
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() {
|
||||
CastTimeline oldTimeline = currentTimeline;
|
||||
|
|
@ -730,7 +724,11 @@ public final class CastPlayer extends BasePlayer {
|
|||
status != null
|
||||
? timelineTracker.getCastTimeline(remoteMediaClient)
|
||||
: 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. */
|
||||
|
|
@ -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) {
|
||||
for (long activeTrackId : activeTrackIds) {
|
||||
if (activeTrackId == id) {
|
||||
|
|
@ -1078,6 +1094,7 @@ public final class CastPlayer extends BasePlayer {
|
|||
+ CastUtils.getLogString(statusCode));
|
||||
}
|
||||
if (--pendingSeekCount == 0) {
|
||||
currentWindowIndex = pendingSeekWindowIndex;
|
||||
pendingSeekWindowIndex = C.INDEX_UNSET;
|
||||
pendingSeekPositionMs = C.TIME_UNSET;
|
||||
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
|
||||
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][]
|
||||
for more information).
|
||||
[top level README][]. The extension is not provided via Google's Maven
|
||||
repository (see [#2781][] for more information).
|
||||
|
||||
In addition, it's necessary to manually build the FFmpeg library, so that gradle
|
||||
can bundle the FFmpeg binaries in the APK:
|
||||
|
|
|
|||
|
|
@ -110,14 +110,18 @@ import java.util.List;
|
|||
int inputSize = inputData.limit();
|
||||
ByteBuffer outputData = outputBuffer.init(inputBuffer.timeUs, 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
|
||||
// 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.
|
||||
outputBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY);
|
||||
return null;
|
||||
} else if (result == AUDIO_DECODER_ERROR_OTHER) {
|
||||
return new FfmpegDecoderException("Error decoding (see logcat).");
|
||||
} else if (result == 0) {
|
||||
// There's no need to output empty buffers.
|
||||
outputBuffer.setFlags(C.BUFFER_FLAG_DECODE_ONLY);
|
||||
return null;
|
||||
}
|
||||
if (!hasOutputFormat) {
|
||||
channelCount = ffmpegGetChannelCount(nativeContext);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ android {
|
|||
}
|
||||
|
||||
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 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0'
|
||||
|
|
|
|||
|
|
@ -410,7 +410,10 @@ import java.util.Map;
|
|||
stopUpdatingAdProgress();
|
||||
imaAdInfo = 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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,11 +30,11 @@ public final class ExoPlayerLibraryInfo {
|
|||
|
||||
/** The version of the library expressed as a string, for example "1.2.3". */
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
||||
public static final String VERSION = "2.13.2";
|
||||
public static final String VERSION = "2.13.3";
|
||||
|
||||
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.13.2";
|
||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.13.3";
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
// 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.
|
||||
|
|
|
|||
|
|
@ -1133,6 +1133,7 @@ public interface Player {
|
|||
* Returns the current {@link State playback state} of the player.
|
||||
*
|
||||
* @return The current {@link State playback state}.
|
||||
* @see EventListener#onPlaybackStateChanged(int)
|
||||
*/
|
||||
@State
|
||||
int getPlaybackState();
|
||||
|
|
@ -1142,6 +1143,7 @@ public interface Player {
|
|||
* true}, or {@link #PLAYBACK_SUPPRESSION_REASON_NONE} if playback is not suppressed.
|
||||
*
|
||||
* @return The current {@link PlaybackSuppressionReason playback suppression reason}.
|
||||
* @see EventListener#onPlaybackSuppressionReasonChanged(int)
|
||||
*/
|
||||
@PlaybackSuppressionReason
|
||||
int getPlaybackSuppressionReason();
|
||||
|
|
@ -1158,6 +1160,7 @@ public interface Player {
|
|||
* </ul>
|
||||
*
|
||||
* @return Whether the player is playing.
|
||||
* @see EventListener#onIsPlayingChanged(boolean)
|
||||
*/
|
||||
boolean isPlaying();
|
||||
|
||||
|
|
@ -1170,6 +1173,7 @@ public interface Player {
|
|||
* {@link #STATE_IDLE}.
|
||||
*
|
||||
* @return The error, or {@code null}.
|
||||
* @see EventListener#onPlayerError(ExoPlaybackException)
|
||||
*/
|
||||
@Nullable
|
||||
ExoPlaybackException getPlayerError();
|
||||
|
|
@ -1201,6 +1205,7 @@ public interface Player {
|
|||
* Whether playback will proceed when {@link #getPlaybackState()} == {@link #STATE_READY}.
|
||||
*
|
||||
* @return Whether playback will proceed when ready.
|
||||
* @see EventListener#onPlayWhenReadyChanged(boolean, int)
|
||||
*/
|
||||
boolean getPlayWhenReady();
|
||||
|
||||
|
|
@ -1215,6 +1220,7 @@ public interface Player {
|
|||
* Returns the current {@link RepeatMode} used for playback.
|
||||
*
|
||||
* @return The current repeat mode.
|
||||
* @see EventListener#onRepeatModeChanged(int)
|
||||
*/
|
||||
@RepeatMode
|
||||
int getRepeatMode();
|
||||
|
|
@ -1226,13 +1232,18 @@ public interface Player {
|
|||
*/
|
||||
void setShuffleModeEnabled(boolean shuffleModeEnabled);
|
||||
|
||||
/** Returns whether shuffling of windows is enabled. */
|
||||
/**
|
||||
* Returns whether shuffling of windows is enabled.
|
||||
*
|
||||
* @see EventListener#onShuffleModeEnabledChanged(boolean)
|
||||
*/
|
||||
boolean getShuffleModeEnabled();
|
||||
|
||||
/**
|
||||
* Whether the player is currently loading the source.
|
||||
*
|
||||
* @return Whether the player is currently loading the source.
|
||||
* @see EventListener#onIsLoadingChanged(boolean)
|
||||
*/
|
||||
boolean isLoading();
|
||||
|
||||
|
|
@ -1375,10 +1386,20 @@ public interface Player {
|
|||
*/
|
||||
int getRendererType(int index);
|
||||
|
||||
/** Returns the available track groups. */
|
||||
/**
|
||||
* Returns the available track groups.
|
||||
*
|
||||
* @see EventListener#onTracksChanged(TrackGroupArray, TrackSelectionArray)
|
||||
*/
|
||||
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();
|
||||
|
||||
/**
|
||||
|
|
@ -1391,6 +1412,8 @@ public interface Player {
|
|||
*
|
||||
* <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.
|
||||
*
|
||||
* @see EventListener#onStaticMetadataChanged(List)
|
||||
*/
|
||||
List<Metadata> getCurrentStaticMetadata();
|
||||
|
||||
|
|
@ -1400,7 +1423,11 @@ public interface Player {
|
|||
@Nullable
|
||||
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();
|
||||
|
||||
/** 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
|
||||
* empty.
|
||||
*
|
||||
* @see EventListener#onMediaItemTransition(MediaItem, int)
|
||||
*/
|
||||
@Nullable
|
||||
MediaItem getCurrentMediaItem();
|
||||
|
|
|
|||
|
|
@ -269,6 +269,12 @@ public final class Cue {
|
|||
*/
|
||||
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
|
||||
* {@link #TYPE_UNSET} and whose dimension parameters are set to {@link #DIMEN_UNSET}.
|
||||
|
|
@ -370,7 +376,8 @@ public final class Cue {
|
|||
/* bitmapHeight= */ DIMEN_UNSET,
|
||||
/* windowColorSet= */ false,
|
||||
/* windowColor= */ Color.BLACK,
|
||||
/* verticalType= */ TYPE_UNSET);
|
||||
/* verticalType= */ TYPE_UNSET,
|
||||
/* shearDegrees= */ 0f);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -415,7 +422,8 @@ public final class Cue {
|
|||
/* bitmapHeight= */ DIMEN_UNSET,
|
||||
windowColorSet,
|
||||
windowColor,
|
||||
/* verticalType= */ TYPE_UNSET);
|
||||
/* verticalType= */ TYPE_UNSET,
|
||||
/* shearDegrees= */ 0f);
|
||||
}
|
||||
|
||||
private Cue(
|
||||
|
|
@ -433,7 +441,8 @@ public final class Cue {
|
|||
float bitmapHeight,
|
||||
boolean windowColorSet,
|
||||
int windowColor,
|
||||
@VerticalType int verticalType) {
|
||||
@VerticalType int verticalType,
|
||||
float shearDegrees) {
|
||||
// Exactly one of text or bitmap should be set.
|
||||
if (text == null) {
|
||||
Assertions.checkNotNull(bitmap);
|
||||
|
|
@ -455,6 +464,7 @@ public final class Cue {
|
|||
this.textSizeType = textSizeType;
|
||||
this.textSize = textSize;
|
||||
this.verticalType = verticalType;
|
||||
this.shearDegrees = shearDegrees;
|
||||
}
|
||||
|
||||
/** 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;
|
||||
@ColorInt private int windowColor;
|
||||
@VerticalType private int verticalType;
|
||||
private float shearDegrees;
|
||||
|
||||
public Builder() {
|
||||
text = null;
|
||||
|
|
@ -514,6 +525,7 @@ public final class Cue {
|
|||
windowColorSet = cue.windowColorSet;
|
||||
windowColor = cue.windowColor;
|
||||
verticalType = cue.verticalType;
|
||||
shearDegrees = cue.shearDegrees;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -794,6 +806,12 @@ public final class Cue {
|
|||
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.
|
||||
*
|
||||
|
|
@ -821,7 +839,8 @@ public final class Cue {
|
|||
bitmapHeight,
|
||||
windowColorSet,
|
||||
windowColor,
|
||||
verticalType);
|
||||
verticalType,
|
||||
shearDegrees);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.util;
|
||||
|
||||
import androidx.annotation.GuardedBy;
|
||||
import com.google.android.exoplayer2.C;
|
||||
|
||||
/**
|
||||
|
|
@ -35,34 +36,73 @@ public final class TimestampAdjuster {
|
|||
*/
|
||||
private static final long MAX_PTS_PLUS_ONE = 0x200000000L;
|
||||
|
||||
@GuardedBy("this")
|
||||
private boolean sharedInitializationStarted;
|
||||
|
||||
@GuardedBy("this")
|
||||
private long firstSampleTimestampUs;
|
||||
|
||||
@GuardedBy("this")
|
||||
private long timestampOffsetUs;
|
||||
|
||||
// Volatile to allow isInitialized to be called on a different thread to adjustSampleTimestamp.
|
||||
private volatile long lastSampleTimestampUs;
|
||||
@GuardedBy("this")
|
||||
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) {
|
||||
this.firstSampleTimestampUs = firstSampleTimestampUs;
|
||||
lastSampleTimestampUs = C.TIME_UNSET;
|
||||
setFirstSampleTimestampUs(firstSampleTimestampUs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the desired result of the first call to {@link #adjustSampleTimestamp(long)}. Can only be
|
||||
* called before any timestamps have been adjusted.
|
||||
* For shared timestamp adjusters, performs necessary initialization actions for a caller.
|
||||
*
|
||||
* @param firstSampleTimestampUs The first adjusted sample timestamp in microseconds, or
|
||||
* {@link #DO_NOT_OFFSET} if presentation timestamps should not be offset.
|
||||
* <ul>
|
||||
* <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) {
|
||||
Assertions.checkState(lastSampleTimestampUs == C.TIME_UNSET);
|
||||
this.firstSampleTimestampUs = firstSampleTimestampUs;
|
||||
public synchronized void sharedInitializeOrWait(boolean canInitialize, long startTimeUs)
|
||||
throws InterruptedException {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -72,22 +112,22 @@ public final class TimestampAdjuster {
|
|||
* #getFirstSampleTimestampUs()}. If this value is {@link #DO_NOT_OFFSET}, returns {@link
|
||||
* C#TIME_UNSET}.
|
||||
*/
|
||||
public long getLastAdjustedTimestampUs() {
|
||||
public synchronized long getLastAdjustedTimestampUs() {
|
||||
return lastSampleTimestampUs != C.TIME_UNSET
|
||||
? (lastSampleTimestampUs + timestampOffsetUs)
|
||||
: firstSampleTimestampUs != DO_NOT_OFFSET ? firstSampleTimestampUs : C.TIME_UNSET;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the offset between the input of {@link #adjustSampleTimestamp(long)} and its output.
|
||||
* If {@link #DO_NOT_OFFSET} was provided to the constructor, 0 is returned. If the timestamp
|
||||
* Returns the offset between the input of {@link #adjustSampleTimestamp(long)} and its output. If
|
||||
* {@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.
|
||||
*
|
||||
* @return The offset between {@link #adjustSampleTimestamp(long)}'s input and output.
|
||||
* {@link C#TIME_UNSET} if the adjuster is not yet initialized and 0 if timestamps should not
|
||||
* be offset.
|
||||
* @return The offset between {@link #adjustSampleTimestamp(long)}'s input and output. {@link
|
||||
* C#TIME_UNSET} if the adjuster is not yet initialized and 0 if timestamps should not be
|
||||
* offset.
|
||||
*/
|
||||
public long getTimestampOffsetUs() {
|
||||
public synchronized long getTimestampOffsetUs() {
|
||||
return firstSampleTimestampUs == DO_NOT_OFFSET
|
||||
? 0
|
||||
: lastSampleTimestampUs == C.TIME_UNSET ? C.TIME_UNSET : timestampOffsetUs;
|
||||
|
|
@ -95,9 +135,14 @@ public final class TimestampAdjuster {
|
|||
|
||||
/**
|
||||
* 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;
|
||||
sharedInitializationStarted = false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -106,7 +151,7 @@ public final class TimestampAdjuster {
|
|||
* @param pts90Khz A 90 kHz clock MPEG-2 TS presentation timestamp.
|
||||
* @return The adjusted timestamp in microseconds.
|
||||
*/
|
||||
public long adjustTsTimestamp(long pts90Khz) {
|
||||
public synchronized long adjustTsTimestamp(long pts90Khz) {
|
||||
if (pts90Khz == C.TIME_UNSET) {
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
|
|
@ -131,7 +176,7 @@ public final class TimestampAdjuster {
|
|||
* @param timeUs The timestamp to adjust in microseconds.
|
||||
* @return The adjusted timestamp in microseconds.
|
||||
*/
|
||||
public long adjustSampleTimestamp(long timeUs) {
|
||||
public synchronized long adjustSampleTimestamp(long timeUs) {
|
||||
if (timeUs == C.TIME_UNSET) {
|
||||
return C.TIME_UNSET;
|
||||
}
|
||||
|
|
@ -143,26 +188,13 @@ public final class TimestampAdjuster {
|
|||
// Calculate the timestamp offset.
|
||||
timestampOffsetUs = firstSampleTimestampUs - timeUs;
|
||||
}
|
||||
synchronized (this) {
|
||||
lastSampleTimestampUs = timeUs;
|
||||
// Notify threads waiting for this adjuster to be initialized.
|
||||
notifyAll();
|
||||
}
|
||||
lastSampleTimestampUs = timeUs;
|
||||
// Notify threads waiting for this adjuster to be initialized.
|
||||
notifyAll();
|
||||
}
|
||||
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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
* @see #experimentalSetOffloadSchedulingEnabled(boolean)
|
||||
* @see EventListener#onExperimentalSleepingForOffloadChanged(boolean)
|
||||
*/
|
||||
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.ShuffleOrder;
|
||||
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.TrackSelector;
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
|
|
@ -1829,7 +1830,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
MediaPeriodId oldPeriodId,
|
||||
long positionForTargetOffsetOverrideUs) {
|
||||
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;
|
||||
}
|
||||
int windowIndex = newTimeline.getPeriodByUid(newPeriodId.periodUid, period).windowIndex;
|
||||
|
|
@ -1937,7 +1941,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
if (sampleStream != null
|
||||
&& renderer.getStream() == sampleStream
|
||||
&& 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) {
|
||||
// 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.
|
||||
setAllRendererStreamsFinal();
|
||||
setAllRendererStreamsFinal(
|
||||
/* streamEndPositionUs= */ readingPeriodHolder.getStartPositionRendererTime());
|
||||
return;
|
||||
}
|
||||
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
|
||||
// starting to play the next period. Mark the SampleStream as final to play out any
|
||||
// remaining data.
|
||||
renderers[i].setCurrentStreamFinal();
|
||||
setCurrentStreamFinal(
|
||||
renderers[i],
|
||||
/* streamEndPositionUs= */ readingPeriodHolder.getStartPositionRendererTime());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2103,14 +2115,21 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||
return true;
|
||||
}
|
||||
|
||||
private void setAllRendererStreamsFinal() {
|
||||
private void setAllRendererStreamsFinal(long streamEndPositionUs) {
|
||||
for (Renderer renderer : renderers) {
|
||||
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 {
|
||||
if (!queue.isLoading(mediaPeriod)) {
|
||||
// Stale event.
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ public class SimpleExoPlayer extends BasePlayer
|
|||
* <li>{@link PriorityTaskManager}: {@code null} (not used)
|
||||
* <li>{@link AudioAttributes}: {@link AudioAttributes#DEFAULT}, not handling audio focus
|
||||
* <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>{@link C.VideoScalingMode}: {@link C#VIDEO_SCALING_MODE_DEFAULT}
|
||||
* <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
|
||||
* 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
|
||||
* 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
|
||||
* 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}
|
||||
* to ensure the device stays awake for playback, even when the screen is off.
|
||||
* @deprecated Use {@link #setWakeMode(int)} instead.
|
||||
|
|
|
|||
|
|
@ -293,18 +293,16 @@ public final class PlaybackStatsListener
|
|||
}
|
||||
|
||||
private void maybeAddSessions(Player player, Events events) {
|
||||
if (player.getCurrentTimeline().isEmpty() && player.getPlaybackState() == Player.STATE_IDLE) {
|
||||
// Player is completely idle. Don't add new sessions.
|
||||
return;
|
||||
}
|
||||
boolean isCompletelyIdle =
|
||||
player.getCurrentTimeline().isEmpty() && player.getPlaybackState() == Player.STATE_IDLE;
|
||||
for (int i = 0; i < events.size(); i++) {
|
||||
@EventFlags int event = events.get(i);
|
||||
EventTime eventTime = events.getEventTime(event);
|
||||
if (event == EVENT_TIMELINE_CHANGED) {
|
||||
sessionManager.updateSessionsWithTimelineChange(eventTime);
|
||||
} else if (event == EVENT_POSITION_DISCONTINUITY) {
|
||||
} else if (!isCompletelyIdle && event == EVENT_POSITION_DISCONTINUITY) {
|
||||
sessionManager.updateSessionsWithDiscontinuity(eventTime, discontinuityReason);
|
||||
} else {
|
||||
} else if (!isCompletelyIdle) {
|
||||
sessionManager.updateSessions(eventTime);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ public final class AudioCapabilities {
|
|||
}
|
||||
|
||||
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)) {
|
||||
// E-AC3 receivers support E-AC3 JOC streams (but decode only the base layer).
|
||||
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)) {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
|
|||
private int pendingMetadataCount;
|
||||
@Nullable private MetadataDecoder decoder;
|
||||
private boolean inputStreamEnded;
|
||||
private boolean outputStreamEnded;
|
||||
private long subsampleOffsetUs;
|
||||
|
||||
/**
|
||||
|
|
@ -118,6 +119,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
|
|||
protected void onPositionReset(long positionUs, boolean joining) {
|
||||
flushPendingMetadata();
|
||||
inputStreamEnded = false;
|
||||
outputStreamEnded = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -158,6 +160,9 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
|
|||
pendingMetadataIndex = (pendingMetadataIndex + 1) % MAX_PENDING_METADATA_COUNT;
|
||||
pendingMetadataCount--;
|
||||
}
|
||||
if (inputStreamEnded && pendingMetadataCount == 0) {
|
||||
outputStreamEnded = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -198,7 +203,7 @@ public final class MetadataRenderer extends BaseRenderer implements Callback {
|
|||
|
||||
@Override
|
||||
public boolean isEnded() {
|
||||
return inputStreamEnded;
|
||||
return outputStreamEnded;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -178,13 +178,17 @@ public final class MaskingMediaSource extends CompositeMediaSource<Void> {
|
|||
// anyway.
|
||||
newTimeline.getWindow(/* windowIndex= */ 0, window);
|
||||
long windowStartPositionUs = window.getDefaultPositionUs();
|
||||
Object windowUid = window.uid;
|
||||
if (unpreparedMaskingMediaPeriod != null) {
|
||||
long periodPreparePositionUs = unpreparedMaskingMediaPeriod.getPreparePositionUs();
|
||||
if (periodPreparePositionUs != 0) {
|
||||
windowStartPositionUs = periodPreparePositionUs;
|
||||
timeline.getPeriodByUid(unpreparedMaskingMediaPeriod.id.periodUid, period);
|
||||
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 =
|
||||
newTimeline.getPeriodPosition(
|
||||
window, period, /* windowIndex= */ 0, windowStartPositionUs);
|
||||
|
|
|
|||
|
|
@ -56,7 +56,8 @@ public interface AdsLoader {
|
|||
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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer2.source.ads;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
|
|
@ -290,6 +291,8 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||
if (this.adPlaybackState == null) {
|
||||
adMediaSourceHolders = new AdMediaSourceHolder[adPlaybackState.adGroupCount][];
|
||||
Arrays.fill(adMediaSourceHolders, new AdMediaSourceHolder[0]);
|
||||
} else {
|
||||
checkState(adPlaybackState.adGroupCount == this.adPlaybackState.adGroupCount);
|
||||
}
|
||||
this.adPlaybackState = adPlaybackState;
|
||||
maybeUpdateAdMediaSources();
|
||||
|
|
@ -350,12 +353,12 @@ public final class AdsMediaSource extends CompositeMediaSource<MediaPeriodId> {
|
|||
private void maybeUpdateSourceInfo() {
|
||||
@Nullable Timeline contentTimeline = this.contentTimeline;
|
||||
if (adPlaybackState != null && contentTimeline != null) {
|
||||
adPlaybackState = adPlaybackState.withAdDurationsUs(getAdDurationsUs());
|
||||
Timeline timeline =
|
||||
adPlaybackState.adGroupCount == 0
|
||||
? contentTimeline
|
||||
: new SinglePeriodAdTimeline(contentTimeline, adPlaybackState);
|
||||
refreshSourceInfo(timeline);
|
||||
if (adPlaybackState.adGroupCount == 0) {
|
||||
refreshSourceInfo(contentTimeline);
|
||||
} else {
|
||||
adPlaybackState = adPlaybackState.withAdDurationsUs(getAdDurationsUs());
|
||||
refreshSourceInfo(new SinglePeriodAdTimeline(contentTimeline, adPlaybackState));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer2.text;
|
||||
|
||||
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.Callback;
|
||||
|
|
@ -91,6 +92,7 @@ public final class TextRenderer extends BaseRenderer implements Callback {
|
|||
@Nullable private SubtitleOutputBuffer subtitle;
|
||||
@Nullable private SubtitleOutputBuffer nextSubtitle;
|
||||
private int nextSubtitleEventIndex;
|
||||
private long finalStreamEndPositionUs;
|
||||
|
||||
/**
|
||||
* @param output The output.
|
||||
|
|
@ -121,6 +123,7 @@ public final class TextRenderer extends BaseRenderer implements Callback {
|
|||
outputLooper == null ? null : Util.createHandler(outputLooper, /* callback= */ this);
|
||||
this.decoderFactory = decoderFactory;
|
||||
formatHolder = new FormatHolder();
|
||||
finalStreamEndPositionUs = C.TIME_UNSET;
|
||||
}
|
||||
|
||||
@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
|
||||
protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs) {
|
||||
streamFormat = formats[0];
|
||||
|
|
@ -156,6 +174,7 @@ public final class TextRenderer extends BaseRenderer implements Callback {
|
|||
clearOutput();
|
||||
inputStreamEnded = false;
|
||||
outputStreamEnded = false;
|
||||
finalStreamEndPositionUs = C.TIME_UNSET;
|
||||
if (decoderReplacementState != REPLACEMENT_STATE_NONE) {
|
||||
replaceDecoder();
|
||||
} else {
|
||||
|
|
@ -166,6 +185,13 @@ public final class TextRenderer extends BaseRenderer implements Callback {
|
|||
|
||||
@Override
|
||||
public void render(long positionUs, long elapsedRealtimeUs) {
|
||||
if (isCurrentStreamFinal()
|
||||
&& finalStreamEndPositionUs != C.TIME_UNSET
|
||||
&& positionUs >= finalStreamEndPositionUs) {
|
||||
releaseBuffers();
|
||||
outputStreamEnded = true;
|
||||
}
|
||||
|
||||
if (outputStreamEnded) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -278,6 +304,7 @@ public final class TextRenderer extends BaseRenderer implements Callback {
|
|||
@Override
|
||||
protected void onDisabled() {
|
||||
streamFormat = null;
|
||||
finalStreamEndPositionUs = C.TIME_UNSET;
|
||||
clearOutput();
|
||||
releaseDecoder();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import com.google.android.exoplayer2.util.ParsableByteArray;
|
|||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
|
|
@ -798,9 +799,7 @@ public final class Cea708Decoder extends CeaDecoder {
|
|||
}
|
||||
}
|
||||
}
|
||||
Collections.sort(
|
||||
displayCueInfos,
|
||||
(thisInfo, thatInfo) -> Integer.compare(thisInfo.priority, thatInfo.priority));
|
||||
Collections.sort(displayCueInfos, Cea708CueInfo.LEAST_IMPORTANT_FIRST);
|
||||
List<Cue> displayCues = new ArrayList<>(displayCueInfos.size());
|
||||
for (int i = 0; i < displayCueInfos.size(); i++) {
|
||||
displayCues.add(displayCueInfos.get(i).cue);
|
||||
|
|
@ -1321,9 +1320,22 @@ public final class Cea708Decoder extends CeaDecoder {
|
|||
/** A {@link Cue} for CEA-708. */
|
||||
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;
|
||||
|
||||
/** 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;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -16,12 +16,6 @@
|
|||
*/
|
||||
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.
|
||||
*
|
||||
|
|
@ -38,48 +32,13 @@ import java.lang.annotation.Retention;
|
|||
// rubies (e.g. HTML <rp> tag).
|
||||
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. */
|
||||
public final String rubyText;
|
||||
|
||||
/** 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.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.util.Util.castNonNull;
|
||||
|
||||
import android.graphics.Typeface;
|
||||
import android.text.Layout;
|
||||
import android.text.SpannableString;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
|
|
@ -318,6 +320,25 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
|
|||
cue.setTextSize(
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -76,7 +76,9 @@ import com.google.android.exoplayer2.util.Util;
|
|||
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)
|
||||
: null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,16 +92,22 @@ import java.util.regex.Pattern;
|
|||
@SsaAlignment public final int alignment;
|
||||
@Nullable @ColorInt public final Integer primaryColor;
|
||||
public final float fontSize;
|
||||
public final boolean bold;
|
||||
public final boolean italic;
|
||||
|
||||
private SsaStyle(
|
||||
String name,
|
||||
@SsaAlignment int alignment,
|
||||
@Nullable @ColorInt Integer primaryColor,
|
||||
float fontSize) {
|
||||
float fontSize,
|
||||
boolean bold,
|
||||
boolean italic) {
|
||||
this.name = name;
|
||||
this.alignment = alignment;
|
||||
this.primaryColor = primaryColor;
|
||||
this.fontSize = fontSize;
|
||||
this.bold = bold;
|
||||
this.italic = italic;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
|
@ -119,9 +125,21 @@ import java.util.regex.Pattern;
|
|||
try {
|
||||
return new SsaStyle(
|
||||
styleValues[format.nameIndex].trim(),
|
||||
parseAlignment(styleValues[format.alignmentIndex].trim()),
|
||||
parseColor(styleValues[format.primaryColorIndex].trim()),
|
||||
parseFontSize(styleValues[format.fontSizeIndex].trim()));
|
||||
format.alignmentIndex != C.INDEX_UNSET
|
||||
? parseAlignment(styleValues[format.alignmentIndex].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) {
|
||||
Log.w(TAG, "Skipping malformed 'Style:' line: '" + styleLine + "'", e);
|
||||
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
|
||||
*
|
||||
|
|
@ -219,14 +247,24 @@ import java.util.regex.Pattern;
|
|||
public final int alignmentIndex;
|
||||
public final int primaryColorIndex;
|
||||
public final int fontSizeIndex;
|
||||
public final int boldIndex;
|
||||
public final int italicIndex;
|
||||
public final int length;
|
||||
|
||||
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.alignmentIndex = alignmentIndex;
|
||||
this.primaryColorIndex = primaryColorIndex;
|
||||
this.fontSizeIndex = fontSizeIndex;
|
||||
this.boldIndex = boldIndex;
|
||||
this.italicIndex = italicIndex;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
|
|
@ -241,6 +279,8 @@ import java.util.regex.Pattern;
|
|||
int alignmentIndex = C.INDEX_UNSET;
|
||||
int primaryColorIndex = C.INDEX_UNSET;
|
||||
int fontSizeIndex = C.INDEX_UNSET;
|
||||
int boldIndex = C.INDEX_UNSET;
|
||||
int italicIndex = C.INDEX_UNSET;
|
||||
String[] keys =
|
||||
TextUtils.split(styleFormatLine.substring(SsaDecoder.FORMAT_LINE_PREFIX.length()), ",");
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
|
|
@ -257,10 +297,23 @@ import java.util.regex.Pattern;
|
|||
case "fontsize":
|
||||
fontSizeIndex = i;
|
||||
break;
|
||||
case "bold":
|
||||
boldIndex = i;
|
||||
break;
|
||||
case "italic":
|
||||
italicIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return nameIndex != C.INDEX_UNSET
|
||||
? new Format(nameIndex, alignmentIndex, primaryColorIndex, fontSizeIndex, keys.length)
|
||||
? new Format(
|
||||
nameIndex,
|
||||
alignmentIndex,
|
||||
primaryColorIndex,
|
||||
fontSizeIndex,
|
||||
boldIndex,
|
||||
italicIndex,
|
||||
keys.length)
|
||||
: 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;
|
||||
|
||||
import static java.lang.Math.max;
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import android.text.Layout;
|
||||
import androidx.annotation.Nullable;
|
||||
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.Subtitle;
|
||||
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.ColorParser;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
|
|
@ -81,7 +84,8 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
|
|||
private static final Pattern OFFSET_TIME =
|
||||
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 PERCENTAGE_COORDINATES =
|
||||
static final Pattern SIGNED_PERCENTAGE = Pattern.compile("^([-+]?\\d+\\.?\\d*?)%$");
|
||||
static final Pattern PERCENTAGE_COORDINATES =
|
||||
Pattern.compile("^(\\d+\\.?\\d*?)% (\\d+\\.?\\d*?)%$");
|
||||
private static final Pattern PIXEL_COORDINATES =
|
||||
Pattern.compile("^(\\d+\\.?\\d*?)px (\\d+\\.?\\d*?)px$");
|
||||
|
|
@ -582,11 +586,11 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
|
|||
break;
|
||||
case TtmlNode.ATTR_TTS_RUBY_POSITION:
|
||||
switch (Util.toLowerInvariant(attributeValue)) {
|
||||
case TtmlNode.RUBY_BEFORE:
|
||||
style = createIfNull(style).setRubyPosition(RubySpan.POSITION_OVER);
|
||||
case TtmlNode.ANNOTATION_POSITION_BEFORE:
|
||||
style = createIfNull(style).setRubyPosition(TextAnnotation.POSITION_BEFORE);
|
||||
break;
|
||||
case TtmlNode.RUBY_AFTER:
|
||||
style = createIfNull(style).setRubyPosition(RubySpan.POSITION_UNDER);
|
||||
case TtmlNode.ANNOTATION_POSITION_AFTER:
|
||||
style = createIfNull(style).setRubyPosition(TextAnnotation.POSITION_AFTER);
|
||||
break;
|
||||
default:
|
||||
// ignore
|
||||
|
|
@ -609,6 +613,14 @@ public final class TtmlDecoder extends SimpleSubtitleDecoder {
|
|||
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:
|
||||
// ignore
|
||||
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.
|
||||
* <p>
|
||||
* For the format of a time expression, see:
|
||||
* <a href="http://www.w3.org/TR/ttaf1-dfxp/#timing-value-timeExpression">timeExpression</a>
|
||||
*
|
||||
* <p>For the format of a time expression, see: <a
|
||||
* href="http://www.w3.org/TR/ttaf1-dfxp/#timing-value-timeExpression">timeExpression</a>
|
||||
*
|
||||
* @param time A string that includes the time expression.
|
||||
* @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_ALIGN = "textAlign";
|
||||
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_SHEAR = "shear";
|
||||
|
||||
// Values for ruby
|
||||
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_DELIMITER = "delimiter";
|
||||
|
||||
// Values for rubyPosition
|
||||
public static final String RUBY_BEFORE = "before";
|
||||
public static final String RUBY_AFTER = "after";
|
||||
// Values for text annotation (i.e. ruby, text emphasis) position
|
||||
public static final String ANNOTATION_POSITION_BEFORE = "before";
|
||||
public static final String ANNOTATION_POSITION_AFTER = "after";
|
||||
public static final String ANNOTATION_POSITION_OUTSIDE = "outside";
|
||||
|
||||
// Values for textDecoration
|
||||
public static final String LINETHROUGH = "linethrough";
|
||||
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_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 text;
|
||||
public final boolean isTextNode;
|
||||
|
|
@ -243,7 +256,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
|
||||
TreeMap<String, Cue.Builder> regionTextOutputs = new TreeMap<>();
|
||||
traverseForText(timeUs, false, regionId, regionTextOutputs);
|
||||
traverseForStyle(timeUs, globalStyles, regionTextOutputs);
|
||||
traverseForStyle(timeUs, globalStyles, regionMap, regionId, regionTextOutputs);
|
||||
|
||||
List<Cue> cues = new ArrayList<>();
|
||||
|
||||
|
|
@ -354,26 +367,39 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
}
|
||||
|
||||
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)) {
|
||||
return;
|
||||
}
|
||||
String resolvedRegionId = ANONYMOUS_REGION_ID.equals(regionId) ? inheritedRegion : regionId;
|
||||
|
||||
for (Map.Entry<String, Integer> entry : nodeEndsByRegion.entrySet()) {
|
||||
String regionId = entry.getKey();
|
||||
int start = nodeStartsByRegion.containsKey(regionId) ? nodeStartsByRegion.get(regionId) : 0;
|
||||
int end = entry.getValue();
|
||||
if (start != end) {
|
||||
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) {
|
||||
getChild(i).traverseForStyle(timeUs, globalStyles, regionOutputs);
|
||||
getChild(i)
|
||||
.traverseForStyle(timeUs, globalStyles, regionMaps, resolvedRegionId, regionOutputs);
|
||||
}
|
||||
}
|
||||
|
||||
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 SpannableStringBuilder text = (SpannableStringBuilder) regionOutput.getText();
|
||||
if (text == null) {
|
||||
|
|
@ -381,7 +407,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||
regionOutput.setText(text);
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
*/
|
||||
package com.google.android.exoplayer2.text.ttml;
|
||||
|
||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
|
|
@ -27,9 +29,12 @@ import android.text.style.StyleSpan;
|
|||
import android.text.style.TypefaceSpan;
|
||||
import android.text.style.UnderlineSpan;
|
||||
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.RubySpan;
|
||||
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.Util;
|
||||
import java.util.ArrayDeque;
|
||||
|
|
@ -83,7 +88,8 @@ import java.util.Map;
|
|||
int end,
|
||||
TtmlStyle style,
|
||||
@Nullable TtmlNode parent,
|
||||
Map<String, TtmlStyle> globalStyles) {
|
||||
Map<String, TtmlStyle> globalStyles,
|
||||
@Cue.VerticalType int verticalType) {
|
||||
|
||||
if (style.getStyle() != TtmlStyle.UNSPECIFIED) {
|
||||
builder.setSpan(new StyleSpan(style.getStyle()), start, end,
|
||||
|
|
@ -119,6 +125,40 @@ import java.util.Map;
|
|||
end,
|
||||
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()) {
|
||||
case TtmlStyle.RUBY_TYPE_BASE:
|
||||
// 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.
|
||||
@RubySpan.Position
|
||||
@TextAnnotation.Position
|
||||
int rubyPosition =
|
||||
containerNode.style != null
|
||||
? containerNode.style.getRubyPosition()
|
||||
: RubySpan.POSITION_UNKNOWN;
|
||||
: TextAnnotation.POSITION_UNKNOWN;
|
||||
builder.setSpan(
|
||||
new RubySpan(rubyText, rubyPosition), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import android.graphics.Typeface;
|
|||
import android.text.Layout;
|
||||
import androidx.annotation.IntDef;
|
||||
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.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
|
@ -30,6 +30,7 @@ import java.lang.annotation.RetentionPolicy;
|
|||
/* package */ final class TtmlStyle {
|
||||
|
||||
public static final int UNSPECIFIED = -1;
|
||||
public static final float UNSPECIFIED_SHEAR = Float.MAX_VALUE;
|
||||
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
|
|
@ -83,9 +84,11 @@ import java.lang.annotation.RetentionPolicy;
|
|||
private float fontSize;
|
||||
@Nullable private String id;
|
||||
@RubyType private int rubyType;
|
||||
@RubySpan.Position private int rubyPosition;
|
||||
@TextAnnotation.Position private int rubyPosition;
|
||||
@Nullable private Layout.Alignment textAlign;
|
||||
@OptionalBoolean private int textCombine;
|
||||
@Nullable private TextEmphasis textEmphasis;
|
||||
private float shearPercentage;
|
||||
|
||||
public TtmlStyle() {
|
||||
linethrough = UNSPECIFIED;
|
||||
|
|
@ -94,8 +97,9 @@ import java.lang.annotation.RetentionPolicy;
|
|||
italic = UNSPECIFIED;
|
||||
fontSizeUnit = UNSPECIFIED;
|
||||
rubyType = UNSPECIFIED;
|
||||
rubyPosition = RubySpan.POSITION_UNKNOWN;
|
||||
rubyPosition = TextAnnotation.POSITION_UNKNOWN;
|
||||
textCombine = UNSPECIFIED;
|
||||
shearPercentage = UNSPECIFIED_SHEAR;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -184,6 +188,15 @@ import java.lang.annotation.RetentionPolicy;
|
|||
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
|
||||
* overridden.
|
||||
|
|
@ -225,7 +238,7 @@ import java.lang.annotation.RetentionPolicy;
|
|||
if (underline == UNSPECIFIED) {
|
||||
underline = ancestor.underline;
|
||||
}
|
||||
if (rubyPosition == RubySpan.POSITION_UNKNOWN) {
|
||||
if (rubyPosition == TextAnnotation.POSITION_UNKNOWN) {
|
||||
rubyPosition = ancestor.rubyPosition;
|
||||
}
|
||||
if (textAlign == null && ancestor.textAlign != null) {
|
||||
|
|
@ -238,6 +251,12 @@ import java.lang.annotation.RetentionPolicy;
|
|||
fontSizeUnit = ancestor.fontSizeUnit;
|
||||
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/
|
||||
if (chaining && !hasBackgroundColor && ancestor.hasBackgroundColor) {
|
||||
setBackgroundColor(ancestor.backgroundColor);
|
||||
|
|
@ -269,12 +288,12 @@ import java.lang.annotation.RetentionPolicy;
|
|||
return rubyType;
|
||||
}
|
||||
|
||||
public TtmlStyle setRubyPosition(@RubySpan.Position int position) {
|
||||
public TtmlStyle setRubyPosition(@TextAnnotation.Position int position) {
|
||||
this.rubyPosition = position;
|
||||
return this;
|
||||
}
|
||||
|
||||
@RubySpan.Position
|
||||
@TextAnnotation.Position
|
||||
public int getRubyPosition() {
|
||||
return rubyPosition;
|
||||
}
|
||||
|
|
@ -299,6 +318,16 @@ import java.lang.annotation.RetentionPolicy;
|
|||
return this;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public TextEmphasis getTextEmphasis() {
|
||||
return textEmphasis;
|
||||
}
|
||||
|
||||
public TtmlStyle setTextEmphasis(@Nullable TextEmphasis textEmphasis) {
|
||||
this.textEmphasis = textEmphasis;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TtmlStyle setFontSize(float fontSize) {
|
||||
this.fontSize = fontSize;
|
||||
return this;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ package com.google.android.exoplayer2.text.webvtt;
|
|||
|
||||
import android.text.TextUtils;
|
||||
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.ColorParser;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
|
|
@ -195,9 +195,9 @@ import java.util.regex.Pattern;
|
|||
style.setBackgroundColor(ColorParser.parseCssColor(value));
|
||||
} else if (PROPERTY_RUBY_POSITION.equals(property)) {
|
||||
if (VALUE_OVER.equals(value)) {
|
||||
style.setRubyPosition(RubySpan.POSITION_OVER);
|
||||
style.setRubyPosition(TextAnnotation.POSITION_BEFORE);
|
||||
} else if (VALUE_UNDER.equals(value)) {
|
||||
style.setRubyPosition(RubySpan.POSITION_UNDER);
|
||||
style.setRubyPosition(TextAnnotation.POSITION_AFTER);
|
||||
}
|
||||
} else if (PROPERTY_TEXT_COMBINE_UPRIGHT.equals(property)) {
|
||||
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.IntDef;
|
||||
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 java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
|
|
@ -95,7 +95,7 @@ public final class WebvttCssStyle {
|
|||
@OptionalBoolean private int italic;
|
||||
@FontSizeUnit private int fontSizeUnit;
|
||||
private float fontSize;
|
||||
@RubySpan.Position private int rubyPosition;
|
||||
@TextAnnotation.Position private int rubyPosition;
|
||||
private boolean combineUpright;
|
||||
|
||||
public WebvttCssStyle() {
|
||||
|
|
@ -111,7 +111,7 @@ public final class WebvttCssStyle {
|
|||
bold = UNSPECIFIED;
|
||||
italic = UNSPECIFIED;
|
||||
fontSizeUnit = UNSPECIFIED;
|
||||
rubyPosition = RubySpan.POSITION_UNKNOWN;
|
||||
rubyPosition = TextAnnotation.POSITION_UNKNOWN;
|
||||
combineUpright = false;
|
||||
}
|
||||
|
||||
|
|
@ -272,12 +272,12 @@ public final class WebvttCssStyle {
|
|||
return fontSize;
|
||||
}
|
||||
|
||||
public WebvttCssStyle setRubyPosition(@RubySpan.Position int rubyPosition) {
|
||||
public WebvttCssStyle setRubyPosition(@TextAnnotation.Position int rubyPosition) {
|
||||
this.rubyPosition = rubyPosition;
|
||||
return this;
|
||||
}
|
||||
|
||||
@RubySpan.Position
|
||||
@TextAnnotation.Position
|
||||
public int getRubyPosition() {
|
||||
return rubyPosition;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ 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.RubySpan;
|
||||
import com.google.android.exoplayer2.text.span.TextAnnotation;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.ParsableByteArray;
|
||||
|
|
@ -572,7 +573,7 @@ public final class WebvttCueParser {
|
|||
StartTag startTag,
|
||||
List<Element> nestedElements,
|
||||
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());
|
||||
sortedNestedElements.addAll(nestedElements);
|
||||
Collections.sort(sortedNestedElements, Element.BY_START_POSITION_ASC);
|
||||
|
|
@ -585,12 +586,12 @@ public final class WebvttCueParser {
|
|||
Element rubyTextElement = sortedNestedElements.get(i);
|
||||
// Use the <rt> element's ruby-position if set, otherwise the <ruby> element's and otherwise
|
||||
// default to OVER.
|
||||
@RubySpan.Position
|
||||
@TextAnnotation.Position
|
||||
int rubyPosition =
|
||||
firstKnownRubyPosition(
|
||||
getRubyPosition(styles, cueId, rubyTextElement.startTag),
|
||||
rubyTagPosition,
|
||||
RubySpan.POSITION_OVER);
|
||||
TextAnnotation.POSITION_BEFORE);
|
||||
// Move the rubyText from spannedText into the RubySpan.
|
||||
int adjustedRubyTextStart = rubyTextElement.startTag.position - deletedCharCount;
|
||||
int adjustedRubyTextEnd = rubyTextElement.endPosition - deletedCharCount;
|
||||
|
|
@ -607,31 +608,31 @@ public final class WebvttCueParser {
|
|||
}
|
||||
}
|
||||
|
||||
@RubySpan.Position
|
||||
@TextAnnotation.Position
|
||||
private static int getRubyPosition(
|
||||
List<WebvttCssStyle> styles, @Nullable String cueId, StartTag startTag) {
|
||||
List<StyleMatch> styleMatches = getApplicableStyles(styles, cueId, startTag);
|
||||
for (int i = 0; i < styleMatches.size(); i++) {
|
||||
WebvttCssStyle style = styleMatches.get(i).style;
|
||||
if (style.getRubyPosition() != RubySpan.POSITION_UNKNOWN) {
|
||||
if (style.getRubyPosition() != TextAnnotation.POSITION_UNKNOWN) {
|
||||
return style.getRubyPosition();
|
||||
}
|
||||
}
|
||||
return RubySpan.POSITION_UNKNOWN;
|
||||
return TextAnnotation.POSITION_UNKNOWN;
|
||||
}
|
||||
|
||||
@RubySpan.Position
|
||||
@TextAnnotation.Position
|
||||
private static int firstKnownRubyPosition(
|
||||
@RubySpan.Position int position1,
|
||||
@RubySpan.Position int position2,
|
||||
@RubySpan.Position int position3) {
|
||||
if (position1 != RubySpan.POSITION_UNKNOWN) {
|
||||
@TextAnnotation.Position int position1,
|
||||
@TextAnnotation.Position int position2,
|
||||
@TextAnnotation.Position int position3) {
|
||||
if (position1 != TextAnnotation.POSITION_UNKNOWN) {
|
||||
return position1;
|
||||
}
|
||||
if (position2 != RubySpan.POSITION_UNKNOWN) {
|
||||
if (position2 != TextAnnotation.POSITION_UNKNOWN) {
|
||||
return position2;
|
||||
}
|
||||
if (position3 != RubySpan.POSITION_UNKNOWN) {
|
||||
if (position3 != TextAnnotation.POSITION_UNKNOWN) {
|
||||
return position3;
|
||||
}
|
||||
throw new IllegalArgumentException();
|
||||
|
|
|
|||
|
|
@ -1488,6 +1488,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||
maxPixels = width * height;
|
||||
minCompressionRatio = 2;
|
||||
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:
|
||||
if ("BRAVIA 4K 2015".equals(Util.MODEL) // Sony Bravia 4K
|
||||
|| ("Amazon".equals(Util.MANUFACTURER)
|
||||
|
|
@ -1603,6 +1606,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
|
|||
case "dangalFHD":
|
||||
case "magnolia":
|
||||
case "machuca":
|
||||
case "once":
|
||||
case "oneday":
|
||||
return true;
|
||||
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.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.runUntilReceiveOffloadSchedulingEnabledNewState;
|
||||
import static com.google.android.exoplayer2.robolectric.TestPlayerRunHelper.runUntilSleepingForOffload;
|
||||
|
|
@ -1649,55 +1650,44 @@ public final class ExoPlayerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void seekAndReprepareAfterPlaybackError() throws Exception {
|
||||
Timeline timeline = new FakeTimeline();
|
||||
final long[] positionHolder = new long[2];
|
||||
ActionSchedule actionSchedule =
|
||||
new ActionSchedule.Builder(TAG)
|
||||
.pause()
|
||||
.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();
|
||||
public void seekAndReprepareAfterPlaybackError_keepsSeekPositionAndTimeline() throws Exception {
|
||||
SimpleExoPlayer player = new TestExoPlayerBuilder(context).build();
|
||||
Player.EventListener mockListener = mock(Player.EventListener.class);
|
||||
player.addListener(mockListener);
|
||||
FakeMediaSource fakeMediaSource = new FakeMediaSource();
|
||||
player.setMediaSource(fakeMediaSource);
|
||||
|
||||
assertThrows(
|
||||
ExoPlaybackException.class,
|
||||
() ->
|
||||
testRunner
|
||||
.start()
|
||||
.blockUntilActionScheduleFinished(TIMEOUT_MS)
|
||||
.blockUntilEnded(TIMEOUT_MS));
|
||||
testRunner.assertTimelinesSame(placeholderTimeline, timeline);
|
||||
testRunner.assertTimelineChangeReasonsEqual(
|
||||
Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
|
||||
Player.TIMELINE_CHANGE_REASON_SOURCE_UPDATE);
|
||||
testRunner.assertPositionDiscontinuityReasonsEqual(Player.DISCONTINUITY_REASON_SEEK);
|
||||
assertThat(positionHolder[0]).isEqualTo(50);
|
||||
assertThat(positionHolder[1]).isEqualTo(50);
|
||||
player.prepare();
|
||||
runUntilPlaybackState(player, Player.STATE_READY);
|
||||
player
|
||||
.createMessage(
|
||||
(type, payload) -> {
|
||||
throw ExoPlaybackException.createForSource(new IOException());
|
||||
})
|
||||
.send();
|
||||
runUntilPlaybackState(player, Player.STATE_IDLE);
|
||||
player.seekTo(/* positionMs= */ 50);
|
||||
runUntilPendingCommandsAreFullyHandled(player);
|
||||
long positionAfterSeekHandled = player.getCurrentPosition();
|
||||
// Delay re-preparation to force player to use its masking mechanisms.
|
||||
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
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
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 org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
|
@ -42,6 +43,7 @@ import org.junit.Before;
|
|||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.robolectric.shadows.ShadowLooper;
|
||||
|
||||
/** Unit test for {@link PlaybackStatsListener}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
|
|
@ -151,6 +153,35 @@ public final class PlaybackStatsListenerTest {
|
|||
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
|
||||
public void playerRelease_callsAllPendingCallbacks() throws Exception {
|
||||
PlaybackStatsListener.Callback callback = mock(PlaybackStatsListener.Callback.class);
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ public class CueTest {
|
|||
.setSize(0.8f)
|
||||
.setWindowColor(Color.CYAN)
|
||||
.setVerticalType(Cue.VERTICAL_TYPE_RL)
|
||||
.setShearDegrees(-15f)
|
||||
.build();
|
||||
|
||||
Cue modifiedCue = cue.buildUpon().build();
|
||||
|
|
@ -61,6 +62,7 @@ public class CueTest {
|
|||
assertThat(cue.windowColor).isEqualTo(Color.CYAN);
|
||||
assertThat(cue.windowColorSet).isTrue();
|
||||
assertThat(cue.verticalType).isEqualTo(Cue.VERTICAL_TYPE_RL);
|
||||
assertThat(cue.shearDegrees).isEqualTo(-15f);
|
||||
|
||||
assertThat(modifiedCue.text).isSameInstanceAs(cue.text);
|
||||
assertThat(modifiedCue.textAlignment).isEqualTo(cue.textAlignment);
|
||||
|
|
@ -74,6 +76,7 @@ public class CueTest {
|
|||
assertThat(modifiedCue.windowColor).isEqualTo(cue.windowColor);
|
||||
assertThat(modifiedCue.windowColorSet).isEqualTo(cue.windowColorSet);
|
||||
assertThat(modifiedCue.verticalType).isEqualTo(cue.verticalType);
|
||||
assertThat(modifiedCue.shearDegrees).isEqualTo(cue.shearDegrees);
|
||||
}
|
||||
|
||||
@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 STYLE_COLORS = "media/ssa/style_colors";
|
||||
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
|
||||
public void decodeEmpty() throws IOException {
|
||||
|
|
@ -336,6 +337,25 @@ public final class SsaDecoderTest {
|
|||
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) {
|
||||
assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0);
|
||||
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.Subtitle;
|
||||
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.ColorParser;
|
||||
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 TEXT_COMBINE_FILE = "media/ttml/text_combine.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
|
||||
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
|
||||
* </code>.
|
||||
*
|
||||
* @throws IOException thrown if reading subtitle file fails.
|
||||
* @see <a
|
||||
* href="https://github.com/android/platform_frameworks_base/blob/jb-mr2-release/graphics/java/android/graphics/Color.java#L414">
|
||||
* JellyBean Color</a> <a
|
||||
* href="https://github.com/android/platform_frameworks_base/blob/kitkat-mr2.2-release/graphics/java/android/graphics/Color.java#L414">
|
||||
* Kitkat Color</a>
|
||||
* @throws IOException thrown if reading subtitle file fails.
|
||||
*/
|
||||
@Test
|
||||
public void lime() throws IOException, SubtitleDecoderException {
|
||||
|
|
@ -646,16 +649,16 @@ public final class TtmlDecoderTest {
|
|||
assertThat(firstCue.toString()).isEqualTo("Cue with annotated text.");
|
||||
assertThat(firstCue)
|
||||
.hasRubySpanBetween("Cue with ".length(), "Cue with annotated".length())
|
||||
.withTextAndPosition("1st rubies", RubySpan.POSITION_OVER);
|
||||
.withTextAndPosition("1st rubies", TextAnnotation.POSITION_BEFORE);
|
||||
assertThat(firstCue)
|
||||
.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);
|
||||
assertThat(secondCue.toString()).isEqualTo("Cue with annotated text.");
|
||||
assertThat(secondCue)
|
||||
.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);
|
||||
assertThat(thirdCue.toString()).isEqualTo("Cue with annotated text.");
|
||||
|
|
@ -674,6 +677,175 @@ public final class TtmlDecoderTest {
|
|||
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) {
|
||||
Cue cue = getOnlyCueAtTimeUs(subtitle, timeUs);
|
||||
assertThat(cue.text).isInstanceOf(Spanned.class);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
package com.google.android.exoplayer2.text.ttml;
|
||||
|
||||
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_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 androidx.annotation.ColorInt;
|
||||
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.runner.RunWith;
|
||||
|
||||
|
|
@ -43,9 +45,11 @@ public final class TtmlStyleTest {
|
|||
@TtmlStyle.FontSizeUnit private static final int FONT_SIZE_UNIT = TtmlStyle.FONT_SIZE_UNIT_EM;
|
||||
@ColorInt private static final int BACKGROUND_COLOR = Color.BLACK;
|
||||
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 boolean TEXT_COMBINE = true;
|
||||
public static final String TEXT_EMPHASIS_STYLE = "dot before";
|
||||
public static final float SHEAR_PERCENTAGE = 16f;
|
||||
|
||||
private final TtmlStyle populatedStyle =
|
||||
new TtmlStyle()
|
||||
|
|
@ -62,7 +66,9 @@ public final class TtmlStyleTest {
|
|||
.setRubyType(RUBY_TYPE)
|
||||
.setRubyPosition(RUBY_POSITION)
|
||||
.setTextAlign(TEXT_ALIGN)
|
||||
.setTextCombine(TEXT_COMBINE);
|
||||
.setTextCombine(TEXT_COMBINE)
|
||||
.setTextEmphasis(TextEmphasis.parse(TEXT_EMPHASIS_STYLE))
|
||||
.setShearPercentage(SHEAR_PERCENTAGE);
|
||||
|
||||
@Test
|
||||
public void inheritStyle() {
|
||||
|
|
@ -86,6 +92,11 @@ public final class TtmlStyleTest {
|
|||
assertWithMessage("backgroundColor should not be inherited")
|
||||
.that(style.hasBackgroundColor())
|
||||
.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
|
||||
|
|
@ -109,6 +120,11 @@ public final class TtmlStyleTest {
|
|||
.that(style.getBackgroundColor())
|
||||
.isEqualTo(BACKGROUND_COLOR);
|
||||
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
|
||||
|
|
@ -221,9 +237,9 @@ public final class TtmlStyleTest {
|
|||
public void rubyPosition() {
|
||||
TtmlStyle style = new TtmlStyle();
|
||||
|
||||
assertThat(style.getRubyPosition()).isEqualTo(RubySpan.POSITION_UNKNOWN);
|
||||
style.setRubyPosition(RubySpan.POSITION_OVER);
|
||||
assertThat(style.getRubyPosition()).isEqualTo(RubySpan.POSITION_OVER);
|
||||
assertThat(style.getRubyPosition()).isEqualTo(TextAnnotation.POSITION_UNKNOWN);
|
||||
style.setRubyPosition(POSITION_BEFORE);
|
||||
assertThat(style.getRubyPosition()).isEqualTo(POSITION_BEFORE);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -245,4 +261,26 @@ public final class TtmlStyleTest {
|
|||
style.setTextCombine(true);
|
||||
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.text.Cue;
|
||||
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.ColorParser;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
|
@ -349,7 +349,7 @@ public class WebvttDecoderTest {
|
|||
assertThat(firstCue.text.toString()).isEqualTo("Some text with over-ruby.");
|
||||
assertThat((Spanned) firstCue.text)
|
||||
.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`.
|
||||
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).");
|
||||
assertThat((Spanned) secondCue.text)
|
||||
.hasRubySpanBetween("Some ".length(), "Some text with under-ruby".length())
|
||||
.withTextAndPosition("under", RubySpan.POSITION_UNDER);
|
||||
.withTextAndPosition("under", TextAnnotation.POSITION_AFTER);
|
||||
assertThat((Spanned) secondCue.text)
|
||||
.hasRubySpanBetween(
|
||||
"Some text with under-ruby and ".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.
|
||||
Cue thirdCue = Iterables.getOnlyElement(subtitle.getCues(subtitle.getEventTime(4)));
|
||||
assertThat(thirdCue.text.toString()).isEqualTo("base1base2base3.");
|
||||
assertThat((Spanned) thirdCue.text)
|
||||
.hasRubySpanBetween(/* start= */ 0, "base1".length())
|
||||
.withTextAndPosition("over1", RubySpan.POSITION_OVER);
|
||||
.withTextAndPosition("over1", TextAnnotation.POSITION_BEFORE);
|
||||
assertThat((Spanned) thirdCue.text)
|
||||
.hasRubySpanBetween("base1".length(), "base1base2".length())
|
||||
.withTextAndPosition("under2", RubySpan.POSITION_UNDER);
|
||||
.withTextAndPosition("under2", TextAnnotation.POSITION_AFTER);
|
||||
assertThat((Spanned) thirdCue.text)
|
||||
.hasRubySpanBetween("base1base2".length(), "base1base2base3".length())
|
||||
.withTextAndPosition("under3", RubySpan.POSITION_UNDER);
|
||||
.withTextAndPosition("under3", TextAnnotation.POSITION_AFTER);
|
||||
|
||||
// Check a <ruby> span with no <rt> tags.
|
||||
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.Util;
|
||||
import com.google.android.exoplayer2.util.XmlPullParserUtil;
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
|
@ -1390,15 +1391,31 @@ public class DashManifestParser extends DefaultHandler
|
|||
|
||||
// Selection flag parsing.
|
||||
|
||||
@C.SelectionFlags
|
||||
protected int parseSelectionFlagsFromRoleDescriptors(List<Descriptor> roleDescriptors) {
|
||||
@C.SelectionFlags int result = 0;
|
||||
for (int i = 0; i < roleDescriptors.size(); i++) {
|
||||
Descriptor descriptor = roleDescriptors.get(i);
|
||||
if ("urn:mpeg:dash:role:2011".equalsIgnoreCase(descriptor.schemeIdUri)
|
||||
&& "main".equals(descriptor.value)) {
|
||||
return C.SELECTION_FLAG_DEFAULT;
|
||||
if (Ascii.equalsIgnoreCase("urn:mpeg:dash:role:2011", descriptor.schemeIdUri)) {
|
||||
result |= parseSelectionFlagsFromDashRoleScheme(descriptor.value);
|
||||
}
|
||||
}
|
||||
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.
|
||||
|
|
@ -1408,8 +1425,8 @@ public class DashManifestParser extends DefaultHandler
|
|||
@C.RoleFlags int result = 0;
|
||||
for (int i = 0; i < roleDescriptors.size(); i++) {
|
||||
Descriptor descriptor = roleDescriptors.get(i);
|
||||
if ("urn:mpeg:dash:role:2011".equalsIgnoreCase(descriptor.schemeIdUri)) {
|
||||
result |= parseDashRoleSchemeValue(descriptor.value);
|
||||
if (Ascii.equalsIgnoreCase("urn:mpeg:dash:role:2011", descriptor.schemeIdUri)) {
|
||||
result |= parseRoleFlagsFromDashRoleScheme(descriptor.value);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
|
@ -1421,10 +1438,10 @@ public class DashManifestParser extends DefaultHandler
|
|||
@C.RoleFlags int result = 0;
|
||||
for (int i = 0; i < accessibilityDescriptors.size(); i++) {
|
||||
Descriptor descriptor = accessibilityDescriptors.get(i);
|
||||
if ("urn:mpeg:dash:role:2011".equalsIgnoreCase(descriptor.schemeIdUri)) {
|
||||
result |= parseDashRoleSchemeValue(descriptor.value);
|
||||
} else if ("urn:tva:metadata:cs:AudioPurposeCS:2007"
|
||||
.equalsIgnoreCase(descriptor.schemeIdUri)) {
|
||||
if (Ascii.equalsIgnoreCase("urn:mpeg:dash:role:2011", descriptor.schemeIdUri)) {
|
||||
result |= parseRoleFlagsFromDashRoleScheme(descriptor.value);
|
||||
} else if (Ascii.equalsIgnoreCase(
|
||||
"urn:tva:metadata:cs:AudioPurposeCS:2007", descriptor.schemeIdUri)) {
|
||||
result |= parseTvaAudioPurposeCsValue(descriptor.value);
|
||||
}
|
||||
}
|
||||
|
|
@ -1436,7 +1453,8 @@ public class DashManifestParser extends DefaultHandler
|
|||
@C.RoleFlags int result = 0;
|
||||
for (int i = 0; i < accessibilityDescriptors.size(); 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1444,7 +1462,7 @@ public class DashManifestParser extends DefaultHandler
|
|||
}
|
||||
|
||||
@C.RoleFlags
|
||||
protected int parseDashRoleSchemeValue(@Nullable String value) {
|
||||
protected int parseRoleFlagsFromDashRoleScheme(@Nullable String value) {
|
||||
if (value == null) {
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1463,6 +1481,7 @@ public class DashManifestParser extends DefaultHandler
|
|||
return C.ROLE_FLAG_EMERGENCY;
|
||||
case "caption":
|
||||
return C.ROLE_FLAG_CAPTION;
|
||||
case "forced_subtitle":
|
||||
case "subtitle":
|
||||
return C.ROLE_FLAG_SUBTITLE;
|
||||
case "sign":
|
||||
|
|
@ -1801,8 +1820,8 @@ public class DashManifestParser extends DefaultHandler
|
|||
List<Descriptor> supplementalProperties) {
|
||||
for (int i = 0; i < supplementalProperties.size(); i++) {
|
||||
Descriptor descriptor = supplementalProperties.get(i);
|
||||
if ("http://dashif.org/guidelines/last-segment-number"
|
||||
.equalsIgnoreCase(descriptor.schemeIdUri)) {
|
||||
if (Ascii.equalsIgnoreCase(
|
||||
"http://dashif.org/guidelines/last-segment-number", descriptor.schemeIdUri)) {
|
||||
return Long.parseLong(descriptor.value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -220,18 +220,22 @@ public class DashManifestParserTest {
|
|||
assertThat(format.containerMimeType).isEqualTo(MimeTypes.APPLICATION_RAWCC);
|
||||
assertThat(format.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_CEA608);
|
||||
assertThat(format.codecs).isEqualTo("cea608");
|
||||
assertThat(format.roleFlags).isEqualTo(C.ROLE_FLAG_SUBTITLE);
|
||||
assertThat(adaptationSets.get(0).type).isEqualTo(C.TRACK_TYPE_TEXT);
|
||||
|
||||
format = adaptationSets.get(1).representations.get(0).format;
|
||||
assertThat(format.containerMimeType).isEqualTo(MimeTypes.APPLICATION_MP4);
|
||||
assertThat(format.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_TTML);
|
||||
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);
|
||||
|
||||
format = adaptationSets.get(2).representations.get(0).format;
|
||||
assertThat(format.containerMimeType).isEqualTo(MimeTypes.APPLICATION_TTML);
|
||||
assertThat(format.sampleMimeType).isEqualTo(MimeTypes.APPLICATION_TTML);
|
||||
assertThat(format.codecs).isNull();
|
||||
assertThat(format.roleFlags).isEqualTo(0);
|
||||
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_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 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_APP0 = 0xFFE0; // Application data 0 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/";
|
||||
|
||||
|
|
@ -85,21 +86,33 @@ public final class JpegExtractor implements Extractor {
|
|||
@Nullable private MotionPhotoMetadata motionPhotoMetadata;
|
||||
private @MonotonicNonNull ExtractorInput lastExtractorInput;
|
||||
private @MonotonicNonNull StartOffsetExtractorInput mp4ExtractorStartOffsetExtractorInput;
|
||||
private @MonotonicNonNull Mp4Extractor mp4Extractor;
|
||||
@Nullable private Mp4Extractor mp4Extractor;
|
||||
|
||||
public JpegExtractor() {
|
||||
scratch = new ParsableByteArray(JPEG_EXIF_HEADER_LENGTH);
|
||||
scratch = new ParsableByteArray(EXIF_ID_CODE_LENGTH);
|
||||
mp4StartPosition = C.POSITION_UNSET;
|
||||
}
|
||||
|
||||
@Override
|
||||
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.
|
||||
input.peekFully(scratch.getData(), /* offset= */ 0, JPEG_EXIF_HEADER_LENGTH);
|
||||
if (scratch.readUnsignedShort() != MARKER_SOI || scratch.readUnsignedShort() != MARKER_APP1) {
|
||||
if (peekMarker(input) != MARKER_SOI) {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -152,6 +165,7 @@ public final class JpegExtractor implements Extractor {
|
|||
public void seek(long position, long timeUs) {
|
||||
if (position == 0) {
|
||||
state = STATE_READING_MARKER;
|
||||
mp4Extractor = null;
|
||||
} else if (state == STATE_READING_MOTION_PHOTO_VIDEO) {
|
||||
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 {
|
||||
scratch.reset(/* limit= */ 2);
|
||||
input.readFully(scratch.getData(), /* offset= */ 0, /* length= */ 2);
|
||||
|
|
|
|||
|
|
@ -100,7 +100,9 @@ import org.xmlpull.v1.XmlPullParserFactory;
|
|||
parseMotionPhotoPresentationTimestampUsFromDescription(xpp);
|
||||
containerItems = parseMicroVideoOffsetFromDescription(xpp);
|
||||
} 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"));
|
||||
if (containerItems.isEmpty()) {
|
||||
|
|
@ -154,16 +156,23 @@ import org.xmlpull.v1.XmlPullParserFactory;
|
|||
}
|
||||
|
||||
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();
|
||||
String itemTagName = containerNamespacePrefix + ":Item";
|
||||
String directoryTagName = containerNamespacePrefix + ":Directory";
|
||||
do {
|
||||
xpp.next();
|
||||
if (XmlPullParserUtil.isStartTag(xpp, "Container:Item")) {
|
||||
@Nullable String mime = XmlPullParserUtil.getAttributeValue(xpp, "Item:Mime");
|
||||
@Nullable String semantic = XmlPullParserUtil.getAttributeValue(xpp, "Item:Semantic");
|
||||
@Nullable String length = XmlPullParserUtil.getAttributeValue(xpp, "Item:Length");
|
||||
@Nullable String padding = XmlPullParserUtil.getAttributeValue(xpp, "Item:Padding");
|
||||
if (XmlPullParserUtil.isStartTag(xpp, itemTagName)) {
|
||||
String mimeAttributeName = itemNamespacePrefix + ":Mime";
|
||||
String semanticAttributeName = itemNamespacePrefix + ":Semantic";
|
||||
String lengthAttributeName = itemNamespacePrefix + ":Length";
|
||||
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) {
|
||||
// Required values are missing.
|
||||
return ImmutableList.of();
|
||||
|
|
@ -175,7 +184,7 @@ import org.xmlpull.v1.XmlPullParserFactory;
|
|||
length != null ? Long.parseLong(length) : 0,
|
||||
padding != null ? Long.parseLong(padding) : 0));
|
||||
}
|
||||
} while (!XmlPullParserUtil.isEndTag(xpp, "Container:Directory"));
|
||||
} while (!XmlPullParserUtil.isEndTag(xpp, directoryTagName));
|
||||
return containerItems.build();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -119,6 +119,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||
case STATE_READ_PAYLOAD:
|
||||
castNonNull(oggSeeker);
|
||||
return readPayload(input, seekPosition);
|
||||
case STATE_END_OF_INPUT:
|
||||
return C.RESULT_END_OF_INPUT;
|
||||
default:
|
||||
// Never happens.
|
||||
throw new IllegalStateException();
|
||||
|
|
|
|||
|
|
@ -144,8 +144,7 @@ public final class PsExtractor implements Extractor {
|
|||
// 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
|
||||
// different position, we need to set the first sample timestamp manually again.
|
||||
timestampAdjuster.reset();
|
||||
timestampAdjuster.setFirstSampleTimestampUs(timeUs);
|
||||
timestampAdjuster.reset(timeUs);
|
||||
}
|
||||
|
||||
if (psBinarySearchSeeker != null) {
|
||||
|
|
|
|||
|
|
@ -268,8 +268,7 @@ public final class TsExtractor implements Extractor {
|
|||
// sample timestamp for that track manually.
|
||||
// - 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.
|
||||
timestampAdjuster.reset();
|
||||
timestampAdjuster.setFirstSampleTimestampUs(timeUs);
|
||||
timestampAdjuster.reset(timeUs);
|
||||
}
|
||||
}
|
||||
if (timeUs != 0 && tsBinarySearchSeeker != null) {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,14 @@ public final class JpegExtractorTest {
|
|||
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
|
||||
public void samplePixelMotionPhotoVideoRemovedShortened() throws Exception {
|
||||
ExtractorAsserts.assertBehavior(
|
||||
|
|
|
|||
|
|
@ -16,11 +16,15 @@
|
|||
package com.google.android.exoplayer2.extractor.ogg;
|
||||
|
||||
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.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.FakeExtractorInput;
|
||||
import com.google.android.exoplayer2.testutil.FakeExtractorOutput;
|
||||
import java.io.IOException;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
|
@ -34,6 +38,19 @@ import org.junit.runner.RunWith;
|
|||
@RunWith(AndroidJUnit4.class)
|
||||
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
|
||||
public void sniffVorbis() throws Exception {
|
||||
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.MimeTypes;
|
||||
import com.google.android.exoplayer2.util.TimestampAdjuster;
|
||||
import com.google.common.primitives.Ints;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -107,11 +108,11 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
|
|||
// Defines the order in which to try the extractors.
|
||||
List<Integer> fileTypeOrder =
|
||||
new ArrayList<>(/* initialCapacity= */ DEFAULT_EXTRACTOR_ORDER.length);
|
||||
addFileTypeIfNotPresent(formatInferredFileType, fileTypeOrder);
|
||||
addFileTypeIfNotPresent(responseHeadersInferredFileType, fileTypeOrder);
|
||||
addFileTypeIfNotPresent(uriInferredFileType, fileTypeOrder);
|
||||
addFileTypeIfValidAndNotPresent(formatInferredFileType, fileTypeOrder);
|
||||
addFileTypeIfValidAndNotPresent(responseHeadersInferredFileType, fileTypeOrder);
|
||||
addFileTypeIfValidAndNotPresent(uriInferredFileType, fileTypeOrder);
|
||||
for (int fileType : DEFAULT_EXTRACTOR_ORDER) {
|
||||
addFileTypeIfNotPresent(fileType, fileTypeOrder);
|
||||
addFileTypeIfValidAndNotPresent(fileType, fileTypeOrder);
|
||||
}
|
||||
|
||||
// Extractor to be used if the type is not recognized.
|
||||
|
|
@ -125,9 +126,13 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
|
|||
if (sniffQuietly(extractor, extractorInput)) {
|
||||
return new BundledHlsMediaChunkExtractor(extractor, format, timestampAdjuster);
|
||||
}
|
||||
if (fileType == FileTypes.TS) {
|
||||
// Fall back on TsExtractor to handle TS streams with an EXT-X-MAP tag. See
|
||||
// https://github.com/google/ExoPlayer/issues/8219.
|
||||
if (fallBackExtractor == null
|
||||
&& (fileType == formatInferredFileType
|
||||
|| 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -136,9 +141,9 @@ public final class DefaultHlsExtractorFactory implements HlsExtractorFactory {
|
|||
checkNotNull(fallBackExtractor), format, timestampAdjuster);
|
||||
}
|
||||
|
||||
private static void addFileTypeIfNotPresent(
|
||||
private static void addFileTypeIfValidAndNotPresent(
|
||||
@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;
|
||||
}
|
||||
fileTypes.add(fileType);
|
||||
|
|
|
|||
|
|
@ -391,15 +391,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||
|
||||
@RequiresNonNull("output")
|
||||
private void loadMedia() throws IOException {
|
||||
if (!isMasterTimestampSource) {
|
||||
try {
|
||||
timestampAdjuster.waitUntilInitialized();
|
||||
} catch (InterruptedException e) {
|
||||
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);
|
||||
try {
|
||||
timestampAdjuster.sharedInitializeOrWait(isMasterTimestampSource, startTimeUs);
|
||||
} catch (InterruptedException e) {
|
||||
throw new InterruptedIOException();
|
||||
}
|
||||
feedDataToExtractor(dataSource, dataSpec, mediaSegmentEncrypted);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
|||
private HlsSampleStreamWrapper[] enabledSampleStreamWrappers;
|
||||
// Maps sample stream wrappers to variant/rendition index by matching array positions.
|
||||
private int[][] manifestUrlIndicesPerWrapper;
|
||||
private int audioVideoSampleStreamWrapperCount;
|
||||
private SequenceableLoader compositeSequenceableLoader;
|
||||
|
||||
/**
|
||||
|
|
@ -315,8 +316,9 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
|||
if (wrapperEnabled) {
|
||||
newEnabledSampleStreamWrappers[newEnabledSampleStreamWrapperCount] = sampleStreamWrapper;
|
||||
if (newEnabledSampleStreamWrapperCount++ == 0) {
|
||||
// The first enabled wrapper is responsible for initializing timestamp adjusters. This
|
||||
// way, if enabled, variants are responsible. Else audio renditions. Else text renditions.
|
||||
// The first enabled wrapper is always allowed to initialize timestamp adjusters. Note
|
||||
// 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);
|
||||
if (wasReset || enabledSampleStreamWrappers.length == 0
|
||||
|| sampleStreamWrapper != enabledSampleStreamWrappers[0]) {
|
||||
|
|
@ -326,7 +328,11 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
|||
forceReset = true;
|
||||
}
|
||||
} 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,
|
||||
overridingDrmInitData);
|
||||
|
||||
audioVideoSampleStreamWrapperCount = sampleStreamWrappers.size();
|
||||
|
||||
// Subtitle stream wrappers. We can always use master playlist information to prepare these.
|
||||
for (int i = 0; i < subtitleRenditions.size(); i++) {
|
||||
Rendition subtitleRendition = subtitleRenditions.get(i);
|
||||
|
|
|
|||
|
|
@ -616,7 +616,9 @@ public final class HlsMediaSource extends BaseMediaSource
|
|||
HlsMediaPlaylist.ServerControl serverControl = playlist.serverControl;
|
||||
// Select part hold back only if the playlist has a part target duration.
|
||||
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) {
|
||||
offsetToEndOfPlaylistUs = serverControl.partHoldBackUs;
|
||||
} else if (serverControl.holdBackUs != C.TIME_UNSET) {
|
||||
|
|
|
|||
|
|
@ -1070,6 +1070,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
|||
drmSessionManager,
|
||||
drmEventDispatcher,
|
||||
overridingDrmInitData);
|
||||
sampleQueue.setStartTimeUs(lastSeekPositionUs);
|
||||
if (isAudioVideo) {
|
||||
sampleQueue.setDrmInitData(drmInitData);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -702,6 +702,10 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
|||
segmentByteRangeOffset = Long.parseLong(splitByteRange[1]);
|
||||
}
|
||||
}
|
||||
if (segmentByteRangeLength == C.LENGTH_UNSET) {
|
||||
// The segment has no byte range defined.
|
||||
segmentByteRangeOffset = 0;
|
||||
}
|
||||
if (fullSegmentEncryptionKeyUri != null && fullSegmentEncryptionIV == null) {
|
||||
// See RFC 8216, Section 4.3.2.5.
|
||||
throw new ParserException(
|
||||
|
|
@ -715,7 +719,9 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
|
|||
segmentByteRangeLength,
|
||||
fullSegmentEncryptionKeyUri,
|
||||
fullSegmentEncryptionIV);
|
||||
segmentByteRangeOffset = 0;
|
||||
if (segmentByteRangeLength != C.LENGTH_UNSET) {
|
||||
segmentByteRangeOffset += segmentByteRangeLength;
|
||||
}
|
||||
segmentByteRangeLength = C.LENGTH_UNSET;
|
||||
} else if (line.startsWith(TAG_TARGET_DURATION)) {
|
||||
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);
|
||||
@Nullable Segment inferredInitSegment = urlToInferredInitSegment.get(segmentUri);
|
||||
if (segmentByteRangeLength == C.LENGTH_UNSET) {
|
||||
// The segment is not byte range defined.
|
||||
// The segment has no byte range defined.
|
||||
segmentByteRangeOffset = 0;
|
||||
} else if (isIFrameOnly && initializationSegment == null && inferredInitSegment == null) {
|
||||
// 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.ExtractorInput;
|
||||
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.TsExtractor;
|
||||
import com.google.android.exoplayer2.testutil.FakeExtractorInput;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
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.HashMap;
|
||||
import java.util.List;
|
||||
|
|
@ -42,14 +45,16 @@ import org.junit.runner.RunWith;
|
|||
@RunWith(AndroidJUnit4.class)
|
||||
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 TimestampAdjuster timestampAdjuster;
|
||||
private Map<String, List<String>> ac3ResponseHeaders;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
tsUri = Uri.parse("http://path/filename.ts");
|
||||
webVttFormat = new Format.Builder().setSampleMimeType(MimeTypes.TEXT_VTT).build();
|
||||
timestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0);
|
||||
ac3ResponseHeaders = new HashMap<>();
|
||||
|
|
@ -69,7 +74,7 @@ public class DefaultHlsExtractorFactoryTest {
|
|||
BundledHlsMediaChunkExtractor result =
|
||||
new DefaultHlsExtractorFactory()
|
||||
.createExtractor(
|
||||
tsUri,
|
||||
URI_WITH_TS_EXTENSION,
|
||||
webVttFormat,
|
||||
/* muxedCaptionFormats= */ null,
|
||||
timestampAdjuster,
|
||||
|
|
@ -93,7 +98,7 @@ public class DefaultHlsExtractorFactoryTest {
|
|||
BundledHlsMediaChunkExtractor result =
|
||||
new DefaultHlsExtractorFactory()
|
||||
.createExtractor(
|
||||
tsUri,
|
||||
URI_WITH_TS_EXTENSION,
|
||||
webVttFormat,
|
||||
/* muxedCaptionFormats= */ null,
|
||||
timestampAdjuster,
|
||||
|
|
@ -115,7 +120,7 @@ public class DefaultHlsExtractorFactoryTest {
|
|||
BundledHlsMediaChunkExtractor result =
|
||||
new DefaultHlsExtractorFactory()
|
||||
.createExtractor(
|
||||
tsUri,
|
||||
URI_WITH_TS_EXTENSION,
|
||||
webVttFormat,
|
||||
/* muxedCaptionFormats= */ null,
|
||||
timestampAdjuster,
|
||||
|
|
@ -138,7 +143,7 @@ public class DefaultHlsExtractorFactoryTest {
|
|||
BundledHlsMediaChunkExtractor result =
|
||||
new DefaultHlsExtractorFactory()
|
||||
.createExtractor(
|
||||
tsUri,
|
||||
URI_WITH_TS_EXTENSION,
|
||||
webVttFormat,
|
||||
/* muxedCaptionFormats= */ null,
|
||||
timestampAdjuster,
|
||||
|
|
@ -149,19 +154,97 @@ public class DefaultHlsExtractorFactoryTest {
|
|||
}
|
||||
|
||||
@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();
|
||||
|
||||
BundledHlsMediaChunkExtractor result =
|
||||
new DefaultHlsExtractorFactory()
|
||||
.createExtractor(
|
||||
tsUri,
|
||||
URI_WITH_MP4_EXTENSION,
|
||||
webVttFormat,
|
||||
/* muxedCaptionFormats= */ null,
|
||||
timestampAdjuster,
|
||||
ac3ResponseHeaders,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -292,6 +292,44 @@ public class HlsMediaSourceTest {
|
|||
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
|
||||
public void loadPlaylist_targetLiveOffsetInMediaItem_targetLiveOffsetPickedFromMediaItem()
|
||||
throws TimeoutException, ParserException {
|
||||
|
|
|
|||
|
|
@ -154,6 +154,52 @@ public class HlsMediaPlaylistParserTest {
|
|||
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
|
||||
public void parseSampleAesMethod() throws Exception {
|
||||
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ import android.util.SparseArray;
|
|||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
|
||||
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.Util;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
|
@ -186,17 +188,26 @@ import java.util.regex.Pattern;
|
|||
} else if (span instanceof RubySpan) {
|
||||
RubySpan rubySpan = (RubySpan) span;
|
||||
switch (rubySpan.position) {
|
||||
case RubySpan.POSITION_OVER:
|
||||
case TextAnnotation.POSITION_BEFORE:
|
||||
return "<ruby style='ruby-position:over;'>";
|
||||
case RubySpan.POSITION_UNDER:
|
||||
case TextAnnotation.POSITION_AFTER:
|
||||
return "<ruby style='ruby-position:under;'>";
|
||||
case RubySpan.POSITION_UNKNOWN:
|
||||
case TextAnnotation.POSITION_UNKNOWN:
|
||||
return "<ruby style='ruby-position:unset;'>";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
} else if (span instanceof UnderlineSpan) {
|
||||
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 {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -209,7 +220,8 @@ import java.util.regex.Pattern;
|
|||
|| span instanceof BackgroundColorSpan
|
||||
|| span instanceof HorizontalTextInVerticalContextSpan
|
||||
|| span instanceof AbsoluteSizeSpan
|
||||
|| span instanceof RelativeSizeSpan) {
|
||||
|| span instanceof RelativeSizeSpan
|
||||
|| span instanceof TextEmphasisSpan) {
|
||||
return "</span>";
|
||||
} else if (span instanceof TypefaceSpan) {
|
||||
@Nullable String fontFamily = ((TypefaceSpan) span).getFamily();
|
||||
|
|
@ -232,6 +244,52 @@ import java.util.regex.Pattern;
|
|||
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) {
|
||||
@Nullable Transition transition = transitions.get(key);
|
||||
if (transition == null) {
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
|||
* <li>Corresponding method: {@link #setShowShuffleButton(boolean)}
|
||||
* <li>Default: false
|
||||
* </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>
|
||||
* <li>Corresponding method: {@link #setShowSubtitleButton(boolean)}
|
||||
* <li>Default: false
|
||||
|
|
@ -436,14 +436,10 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
private StyledPlayerControlViewLayoutManager controlViewLayoutManager;
|
||||
private Resources resources;
|
||||
|
||||
private int selectedMainSettingsPosition;
|
||||
private RecyclerView settingsView;
|
||||
private SettingsAdapter settingsAdapter;
|
||||
private SubSettingsAdapter subSettingsAdapter;
|
||||
private PlaybackSpeedAdapter playbackSpeedAdapter;
|
||||
private PopupWindow settingsWindow;
|
||||
private String[] playbackSpeedTexts;
|
||||
private int[] playbackSpeedsMultBy100;
|
||||
private int selectedPlaybackSpeedIndex;
|
||||
private boolean needToHideBars;
|
||||
private int settingsWindowMargin;
|
||||
|
||||
|
|
@ -457,6 +453,8 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
@Nullable private ImageView fullScreenButton;
|
||||
@Nullable private ImageView minimalFullScreenButton;
|
||||
@Nullable private View settingsButton;
|
||||
@Nullable private View playbackSpeedButton;
|
||||
@Nullable private View audioTrackButton;
|
||||
|
||||
public StyledPlayerControlView(Context context) {
|
||||
this(context, /* attrs= */ null);
|
||||
|
|
@ -575,6 +573,16 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
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);
|
||||
View timeBarPlaceholder = findViewById(R.id.exo_progress_placeholder);
|
||||
if (customTimeBar != null) {
|
||||
|
|
@ -663,12 +671,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
settingIcons[SETTINGS_AUDIO_TRACK_SELECTION_POSITION] =
|
||||
resources.getDrawable(R.drawable.exo_styled_controls_audiotrack);
|
||||
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);
|
||||
|
||||
subSettingsAdapter = new SubSettingsAdapter();
|
||||
settingsView =
|
||||
(RecyclerView)
|
||||
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);
|
||||
textTrackSelectionAdapter = new TextTrackSelectionAdapter();
|
||||
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);
|
||||
fullScreenEnterDrawable =
|
||||
|
|
@ -770,7 +777,6 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
this.trackSelector = null;
|
||||
}
|
||||
updateAll();
|
||||
updateSettingsPlaybackSpeedLists();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1102,6 +1108,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
updateRepeatModeButton();
|
||||
updateShuffleButton();
|
||||
updateTrackLists();
|
||||
updatePlaybackSpeedList();
|
||||
updateTimeline();
|
||||
}
|
||||
|
||||
|
|
@ -1437,24 +1444,13 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
}
|
||||
}
|
||||
|
||||
private void updateSettingsPlaybackSpeedLists() {
|
||||
private void updatePlaybackSpeedList() {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
float speed = 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;
|
||||
playbackSpeedAdapter.updateSelectedIndex(player.getPlaybackParameters().speed);
|
||||
settingsAdapter.setSubTextAtPosition(
|
||||
SETTINGS_PLAYBACK_SPEED_POSITION, playbackSpeedTexts[closestMatchIndex]);
|
||||
SETTINGS_PLAYBACK_SPEED_POSITION, playbackSpeedAdapter.getSelectedText());
|
||||
}
|
||||
|
||||
private void updateSettingsWindowSize() {
|
||||
|
|
@ -1570,50 +1566,14 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
|
||||
private void onSettingViewClicked(int position) {
|
||||
if (position == SETTINGS_PLAYBACK_SPEED_POSITION) {
|
||||
subSettingsAdapter.init(playbackSpeedTexts, selectedPlaybackSpeedIndex);
|
||||
selectedMainSettingsPosition = SETTINGS_PLAYBACK_SPEED_POSITION;
|
||||
displaySettingsWindow(subSettingsAdapter);
|
||||
displaySettingsWindow(playbackSpeedAdapter);
|
||||
} else if (position == SETTINGS_AUDIO_TRACK_SELECTION_POSITION) {
|
||||
selectedMainSettingsPosition = SETTINGS_AUDIO_TRACK_SELECTION_POSITION;
|
||||
displaySettingsWindow(audioTrackSelectionAdapter);
|
||||
} else {
|
||||
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
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
|
@ -1685,6 +1645,35 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
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() {
|
||||
return player != null
|
||||
&& player.getPlaybackState() != Player.STATE_ENDED
|
||||
|
|
@ -1835,7 +1824,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
updateTimeline();
|
||||
}
|
||||
if (events.contains(EVENT_PLAYBACK_PARAMETERS_CHANGED)) {
|
||||
updateSettingsPlaybackSpeedLists();
|
||||
updatePlaybackSpeedList();
|
||||
}
|
||||
if (events.contains(EVENT_TRACKS_CHANGED)) {
|
||||
updateTrackLists();
|
||||
|
|
@ -1876,6 +1865,12 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
} else if (settingsButton == view) {
|
||||
controlViewLayoutManager.removeHideCallbacks();
|
||||
displaySettingsWindow(settingsAdapter);
|
||||
} else if (playbackSpeedButton == view) {
|
||||
controlViewLayoutManager.removeHideCallbacks();
|
||||
displaySettingsWindow(playbackSpeedAdapter);
|
||||
} else if (audioTrackButton == view) {
|
||||
controlViewLayoutManager.removeHideCallbacks();
|
||||
displaySettingsWindow(audioTrackSelectionAdapter);
|
||||
} else if (subtitleButton == view) {
|
||||
controlViewLayoutManager.removeHideCallbacks();
|
||||
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;
|
||||
|
||||
public SubSettingsAdapter() {
|
||||
texts = new String[0];
|
||||
public PlaybackSpeedAdapter(String[] playbackSpeedTexts, int[] playbackSpeedsMultBy100) {
|
||||
this.playbackSpeedTexts = playbackSpeedTexts;
|
||||
this.playbackSpeedsMultBy100 = playbackSpeedsMultBy100;
|
||||
}
|
||||
|
||||
public void init(String[] texts, int selectedIndex) {
|
||||
this.texts = texts;
|
||||
this.selectedIndex = selectedIndex;
|
||||
public void updateSelectedIndex(float playbackSpeed) {
|
||||
int currentSpeedMultBy100 = Math.round(playbackSpeed * 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;
|
||||
}
|
||||
}
|
||||
selectedIndex = closestMatchIndex;
|
||||
}
|
||||
|
||||
public String getSelectedText() {
|
||||
return playbackSpeedTexts[selectedIndex];
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -1973,27 +1983,23 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
|
||||
@Override
|
||||
public void onBindViewHolder(SubSettingViewHolder holder, int position) {
|
||||
if (position < texts.length) {
|
||||
holder.textView.setText(texts[position]);
|
||||
if (position < playbackSpeedTexts.length) {
|
||||
holder.textView.setText(playbackSpeedTexts[position]);
|
||||
}
|
||||
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
|
||||
public int getItemCount() {
|
||||
return texts.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()));
|
||||
return playbackSpeedTexts.length;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2041,7 +2047,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolderAtZeroPosition(TrackSelectionViewHolder holder) {
|
||||
public void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder) {
|
||||
// CC options include "Off" at the first position, which disables text rendering.
|
||||
holder.textView.setText(R.string.exo_track_selection_none);
|
||||
boolean isTrackSelectionOff = true;
|
||||
|
|
@ -2070,7 +2076,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(TrackSelectionViewHolder holder, int position) {
|
||||
public void onBindViewHolder(SubSettingViewHolder holder, int position) {
|
||||
super.onBindViewHolder(holder, position);
|
||||
if (position > 0) {
|
||||
TrackInfo track = tracks.get(position - 1);
|
||||
|
|
@ -2087,7 +2093,7 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
private final class AudioTrackSelectionAdapter extends TrackSelectionAdapter {
|
||||
|
||||
@Override
|
||||
public void onBindViewHolderAtZeroPosition(TrackSelectionViewHolder holder) {
|
||||
public void onBindViewHolderAtZeroPosition(SubSettingViewHolder holder) {
|
||||
// Audio track selection option includes "Auto" at the top.
|
||||
holder.textView.setText(R.string.exo_track_selection_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
|
||||
extends RecyclerView.Adapter<TrackSelectionViewHolder> {
|
||||
private abstract class TrackSelectionAdapter extends RecyclerView.Adapter<SubSettingViewHolder> {
|
||||
|
||||
protected List<Integer> rendererIndices;
|
||||
protected List<TrackInfo> tracks;
|
||||
|
|
@ -2183,19 +2188,19 @@ public class StyledPlayerControlView extends FrameLayout {
|
|||
List<Integer> rendererIndices, List<TrackInfo> trackInfos, MappedTrackInfo mappedTrackInfo);
|
||||
|
||||
@Override
|
||||
public TrackSelectionViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
public SubSettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View v =
|
||||
LayoutInflater.from(getContext())
|
||||
.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);
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(TrackSelectionViewHolder holder, int position) {
|
||||
public void onBindViewHolder(SubSettingViewHolder holder, int position) {
|
||||
if (trackSelector == null || mappedTrackInfo == null) {
|
||||
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 View checkView;
|
||||
|
||||
public TrackSelectionViewHolder(View itemView) {
|
||||
public SubSettingViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
textView = itemView.findViewById(R.id.exo_text);
|
||||
checkView = itemView.findViewById(R.id.exo_check);
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import java.util.List;
|
|||
|
||||
private final StyledPlayerControlView styledPlayerControlView;
|
||||
|
||||
@Nullable private final View controlsBackground;
|
||||
@Nullable private final ViewGroup centerControls;
|
||||
@Nullable private final ViewGroup bottomBar;
|
||||
@Nullable private final ViewGroup minimalControls;
|
||||
|
|
@ -99,7 +100,7 @@ import java.util.List;
|
|||
shownButtons = new ArrayList<>();
|
||||
|
||||
// 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);
|
||||
|
||||
// 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(
|
||||
View v,
|
||||
int left,
|
||||
|
|
@ -577,13 +587,17 @@ import java.util.List;
|
|||
- (centerControls != null
|
||||
? (centerControls.getPaddingLeft() + centerControls.getPaddingRight())
|
||||
: 0);
|
||||
int centerControlHeight =
|
||||
getHeightWithMargins(centerControls)
|
||||
- (centerControls != null
|
||||
? (centerControls.getPaddingTop() + centerControls.getPaddingBottom())
|
||||
: 0);
|
||||
|
||||
int defaultModeMinimumWidth =
|
||||
Math.max(
|
||||
centerControlWidth,
|
||||
getWidthWithMargins(timeView) + getWidthWithMargins(overflowShowButton));
|
||||
int defaultModeMinimumHeight =
|
||||
getHeightWithMargins(centerControls) + 2 * getHeightWithMargins(bottomBar);
|
||||
int defaultModeMinimumHeight = centerControlHeight + (2 * getHeightWithMargins(bottomBar));
|
||||
|
||||
return width <= defaultModeMinimumWidth || height <= defaultModeMinimumHeight;
|
||||
}
|
||||
|
|
@ -607,7 +621,7 @@ import java.util.List;
|
|||
defaultTimeBar.hideScrubber(/* disableScrubberPadding= */ true);
|
||||
} else if (uxState == UX_STATE_ONLY_PROGRESS_VISIBLE) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -285,7 +285,8 @@ import java.util.Map;
|
|||
+ "writing-mode:%s;"
|
||||
+ "font-size:%s;"
|
||||
+ "background-color:%s;"
|
||||
+ "transform:translate(%s%%,%s%%);"
|
||||
+ "transform:translate(%s%%,%s%%)"
|
||||
+ "%s;"
|
||||
+ "'>",
|
||||
positionProperty,
|
||||
positionPercent,
|
||||
|
|
@ -298,7 +299,8 @@ import java.util.Map;
|
|||
cueTextSizeCssPx,
|
||||
windowCssColor,
|
||||
horizontalTranslatePercent,
|
||||
verticalTranslatePercent))
|
||||
verticalTranslatePercent,
|
||||
getBlockShearTransformFunction(cue)))
|
||||
.append(Util.formatInvariant("<span class='%s'>", DEFAULT_BACKGROUND_CSS_CLASS))
|
||||
.append(htmlAndCss.html)
|
||||
.append("</span>")
|
||||
|
|
@ -320,6 +322,17 @@ import java.util.Map;
|
|||
"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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -15,10 +15,14 @@
|
|||
-->
|
||||
<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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@color/exo_black_opacity_60"/>
|
||||
|
||||
<FrameLayout android:id="@id/exo_bottom_bar"
|
||||
|
|
@ -126,7 +130,8 @@
|
|||
android:layout_gravity="center"
|
||||
android:background="@android:color/transparent"
|
||||
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"
|
||||
style="@style/ExoStyledControls.Button.Center.Previous"/>
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@
|
|||
<item name="exo_vr" type="id"/>
|
||||
<item name="exo_subtitle" 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_controls_background" type="id"/>
|
||||
<item name="exo_basic_controls" type="id"/>
|
||||
|
|
|
|||
|
|
@ -192,6 +192,16 @@
|
|||
<item name="android:contentDescription">@string/exo_controls_settings_description</item>
|
||||
</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">
|
||||
<item name="bar_height">@dimen/exo_styled_progress_bar_height</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 com.google.android.exoplayer2.text.span.HorizontalTextInVerticalContextSpan;
|
||||
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.runner.RunWith;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
|
@ -250,12 +252,12 @@ public class SpannedToHtmlConverterTest {
|
|||
SpannableString spanned =
|
||||
new SpannableString("String with over-annotated and under-annotated section");
|
||||
spanned.setSpan(
|
||||
new RubySpan("ruby-text", RubySpan.POSITION_OVER),
|
||||
new RubySpan("ruby-text", TextAnnotation.POSITION_BEFORE),
|
||||
"String with ".length(),
|
||||
"String with over-annotated".length(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
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 under-annotated".length(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
|
@ -279,6 +281,42 @@ public class SpannedToHtmlConverterTest {
|
|||
+ "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
|
||||
public void convert_supportsUnderlineSpan() {
|
||||
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
|
||||
// limitations under the License.
|
||||
|
||||
if (project.ext.has("exoplayerPublishEnabled")
|
||||
&& project.ext.exoplayerPublishEnabled) {
|
||||
// For publishing to Bintray.
|
||||
apply plugin: 'bintray-release'
|
||||
publish {
|
||||
artifactId = releaseArtifact
|
||||
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"
|
||||
}
|
||||
apply plugin: 'maven-publish'
|
||||
afterEvaluate {
|
||||
publishing {
|
||||
repositories {
|
||||
maven {
|
||||
url = findProperty('mavenRepo') ?: "${buildDir}/repo"
|
||||
}
|
||||
publications {
|
||||
release(MavenPublication) {
|
||||
from components.release
|
||||
artifact androidSourcesJar
|
||||
groupId = 'com.google.android.exoplayer'
|
||||
artifactId = releaseArtifact
|
||||
version releaseVersion
|
||||
pom {
|
||||
name = releaseArtifact
|
||||
description = releaseDescription
|
||||
licenses {
|
||||
license {
|
||||
name = 'The Apache Software License, Version 2.0'
|
||||
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
|
||||
distribution = 'repo'
|
||||
}
|
||||
}
|
||||
publications {
|
||||
release(MavenPublication) {
|
||||
from components.release
|
||||
artifact androidSourcesJar
|
||||
groupId = 'com.google.android.exoplayer'
|
||||
artifactId = releaseArtifact
|
||||
version releaseVersion
|
||||
pom {
|
||||
name = releaseArtifact
|
||||
description = releaseDescription
|
||||
licenses {
|
||||
license {
|
||||
name = 'The Apache Software License, Version 2.0'
|
||||
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
|
||||
distribution = 'repo'
|
||||
}
|
||||
developers {
|
||||
developer {
|
||||
name = 'The Android Open Source Project'
|
||||
}
|
||||
}
|
||||
scm {
|
||||
connection = 'scm:git:https://github.com/google/ExoPlayer.git'
|
||||
url = 'https://github.com/google/ExoPlayer'
|
||||
}
|
||||
developers {
|
||||
developer {
|
||||
name = 'The Android Open Source Project'
|
||||
}
|
||||
}
|
||||
scm {
|
||||
connection = 'scm:git:https://github.com/google/ExoPlayer.git'
|
||||
url = 'https://github.com/google/ExoPlayer'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
tasks.withType(PublishToMavenRepository) { it.dependsOn lint, test }
|
||||
|
||||
task androidSourcesJar(type: Jar) {
|
||||
archiveClassifier.set('sources')
|
||||
|
|
|
|||
|
|
@ -19,6 +19,18 @@ import android.graphics.Bitmap;
|
|||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
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.Dumper;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
|
|
@ -89,13 +101,38 @@ public final class PlaybackOutput implements Dumper.Dumpable {
|
|||
dumper.startBlock("Metadata[" + i + "]");
|
||||
Metadata metadata = metadatas.get(i);
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (subtitles.isEmpty()) {
|
||||
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>
|
||||
</SegmentTemplate>
|
||||
<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">
|
||||
<BaseURL>https://test.com/0</BaseURL>
|
||||
</Representation>
|
||||
</AdaptationSet>
|
||||
<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">
|
||||
<BaseURL>https://test.com/0</BaseURL>
|
||||
</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
|
||||
|
||||
[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
|
||||
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: PrimaryColourStyleHexYellow ,Roboto,50,&H0000FFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
|
||||
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: PrimaryColourStyleHexAlpha ,Roboto,50,&HA00000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
|
||||
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: PrimaryColourStyleDecimalAlpha ,Roboto,50,2164195328,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
|
||||
Style: PrimaryColourStyleInvalid ,Roboto,50,blue ,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
|
||||
Format: Name ,PrimaryColour
|
||||
Style: PrimaryColourStyleHexRed ,&H000000FF
|
||||
Style: PrimaryColourStyleHexYellow ,&H0000FFFF
|
||||
Style: PrimaryColourStyleHexGreen ,&HFF00
|
||||
Style: PrimaryColourStyleHexAlpha ,&HA00000FF
|
||||
Style: PrimaryColourStyleDecimal ,16711680
|
||||
Style: PrimaryColourStyleDecimalAlpha,2164195328
|
||||
Style: PrimaryColourStyleInvalid ,blue
|
||||
|
||||
|
||||
[Events]
|
||||
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
||||
Dialogue: 0,0:00:01.00,0:00:02.00,PrimaryColourStyleHexRed ,Arnold,0,0,0,,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,0:00:05.00,0:00:06.00,PrimaryColourStyleHexGreen ,Arnold,0,0,0,,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,0:00:09.00,0:00:10.00,PrimaryColourStyleDecimal ,Arnold,0,0,0,,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,0:00:13.00,0:00:14.00,PrimaryColourInvalid ,Arnold,0,0,0,,Seventh line with invalid color .
|
||||
Format: Start ,End ,Style ,Text
|
||||
Dialogue: 0:00:01.00,0:00:02.00,PrimaryColourStyleHexRed ,First line in RED (&H000000FF).
|
||||
Dialogue: 0:00:03.00,0:00:04.00,PrimaryColourStyleHexYellow ,Second line in YELLOW (&H0000FFFF).
|
||||
Dialogue: 0:00:05.00,0:00:06.00,PrimaryColourStyleHexGreen ,Third line in GREEN (leading zeros &HFF00).
|
||||
Dialogue: 0:00:07.00,0:00:08.00,PrimaryColourStyleHexAlpha ,Fourth line in RED with alpha (&H400000FF).
|
||||
Dialogue: 0:00:09.00,0:00:10.00,PrimaryColourStyleDecimal ,Fifth line in BLUE (16711680).
|
||||
Dialogue: 0:00:11.00,0:00:12.00,PrimaryColourStyleDecimalAlpha,Sixth line in BLUE with alpha (2164195328).
|
||||
Dialogue: 0:00:13.00,0:00:14.00,PrimaryColourInvalid ,Seventh line with invalid color.
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ PlayResX: 1280
|
|||
PlayResY: 720
|
||||
|
||||
[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
|
||||
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: FontSizeBig ,Roboto,72.2,&H000000FF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,50,50,70,1
|
||||
Format: Name ,Fontsize
|
||||
Style: FontSizeSmall,30
|
||||
Style: FontSizeBig ,72.2
|
||||
|
||||
|
||||
|
||||
[Events]
|
||||
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
||||
Dialogue: 0,0:00:00.95,0:00:03.11,FontSizeSmall ,Arnold,0,0,0,,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.
|
||||
Format: Start ,End ,Style ,Text
|
||||
Dialogue: 0:00:00.95,0:00:03.11,FontSizeSmall,First line with font size 30.
|
||||
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
|
||||
MetadataOutput:
|
||||
Metadata[0]:
|
||||
entry[0] = AppInfoTable
|
||||
entry[1] = AppInfoTable
|
||||
entry[0] = Ait(controlCode=1,url=http://static-cdn.arte.tv/redbutton/index_fr.html)
|
||||
entry[1] = Ait(controlCode=2,url=http://www.arte.tv/hbbtvv2/index.html?lang=fr_FR&page=PLUS7)
|
||||
Metadata[1]:
|
||||
entry[0] = AppInfoTable
|
||||
entry[1] = AppInfoTable
|
||||
entry[0] = Ait(controlCode=1,url=http://static-cdn.arte.tv/redbutton/index_fr.html)
|
||||
entry[1] = Ait(controlCode=2,url=http://www.arte.tv/hbbtvv2/index.html?lang=fr_FR&page=PLUS7)
|
||||
Metadata[2]:
|
||||
entry[0] = AppInfoTable
|
||||
entry[1] = AppInfoTable
|
||||
entry[0] = Ait(controlCode=1,url=http://static-cdn.arte.tv/redbutton/index_fr.html)
|
||||
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
|
||||
MetadataOutput:
|
||||
Metadata[0]:
|
||||
entry[0] = SpliceInsertCommand
|
||||
entry[0] = SCTE-35 splice command: type=SpliceInsertCommand
|
||||
Metadata[1]:
|
||||
entry[0] = SpliceInsertCommand
|
||||
entry[0] = SCTE-35 splice command: type=SpliceInsertCommand
|
||||
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
|
||||
MetadataOutput:
|
||||
Metadata[0]:
|
||||
entry[0] = ApicFrame
|
||||
entry[0] = APIC: mimeType=image/jpeg, description=Hello World
|
||||
Metadata[1]:
|
||||
entry[0] = CommentFrame
|
||||
entry[1] = ApicFrame
|
||||
entry[0] = COMM: language=eng, description=description
|
||||
entry[1] = APIC: mimeType=image/jpeg, description=Hello World
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
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.common.truth.Truth.assertThat;
|
||||
|
||||
|
|
@ -86,6 +87,7 @@ public class FakeMediaSource extends BaseMediaSource {
|
|||
private final ArrayList<MediaPeriodId> createdMediaPeriods;
|
||||
private final DrmSessionManager drmSessionManager;
|
||||
|
||||
private boolean preparationAllowed;
|
||||
private @MonotonicNonNull Timeline timeline;
|
||||
private boolean preparedSource;
|
||||
private boolean releasedSource;
|
||||
|
|
@ -154,6 +156,22 @@ public class FakeMediaSource extends BaseMediaSource {
|
|||
this.createdMediaPeriods = new ArrayList<>();
|
||||
this.drmSessionManager = drmSessionManager;
|
||||
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
|
||||
|
|
@ -186,14 +204,14 @@ public class FakeMediaSource extends BaseMediaSource {
|
|||
@Override
|
||||
@Nullable
|
||||
public Timeline getInitialTimeline() {
|
||||
return timeline == null || timeline == Timeline.EMPTY || timeline.getWindowCount() == 1
|
||||
return timeline == null || timeline.isEmpty() || timeline.getWindowCount() == 1
|
||||
? null
|
||||
: new InitialTimeline(timeline);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSingleWindow() {
|
||||
return timeline == null || timeline == Timeline.EMPTY || timeline.getWindowCount() == 1;
|
||||
return timeline == null || timeline.isEmpty() || timeline.getWindowCount() == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -204,7 +222,7 @@ public class FakeMediaSource extends BaseMediaSource {
|
|||
preparedSource = true;
|
||||
releasedSource = false;
|
||||
sourceInfoRefreshHandler = Util.createHandlerForCurrentLooper();
|
||||
if (timeline != null) {
|
||||
if (preparationAllowed && timeline != null) {
|
||||
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
|
||||
* 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 sendManifestLoadEvents Whether to treat this as a manifest refresh and send manifest
|
||||
* load events to listeners.
|
||||
*/
|
||||
public synchronized void setNewSourceInfo(Timeline newTimeline, boolean sendManifestLoadEvents) {
|
||||
checkState(preparationAllowed);
|
||||
if (sourceInfoRefreshHandler != null) {
|
||||
sourceInfoRefreshHandler.post(
|
||||
() -> {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue