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

r2.13.3
This commit is contained in:
Marc Baechinger 2021-04-14 17:01:14 +01:00 committed by GitHub
commit 0ba317b133
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
102 changed files with 3581 additions and 620 deletions

View file

@ -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

View file

@ -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:

View file

@ -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)

View file

@ -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.

View file

@ -38,6 +38,7 @@ android {
"proguard-rules.txt",
getDefaultProguardFile('proguard-android.txt')
]
signingConfig signingConfigs.debug
}
debug {
jniDebuggable = true

View file

@ -34,6 +34,7 @@ android {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt')
signingConfig signingConfigs.debug
}
}

View file

@ -38,6 +38,7 @@ android {
"proguard-rules.txt",
getDefaultProguardFile('proguard-android.txt')
]
signingConfig signingConfigs.debug
}
debug {
jniDebuggable = true

View file

@ -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",

View file

@ -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(

View file

@ -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();

View file

@ -34,6 +34,7 @@ android {
shrinkResources true
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt')
signingConfig signingConfigs.debug
}
}

View file

@ -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);

View file

@ -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:

View file

@ -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);

View file

@ -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'

View file

@ -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();
}

View file

@ -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.

View file

@ -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();

View file

@ -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);
}
}
}

View file

@ -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.
*

View file

@ -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);
}
}
}

View file

@ -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();
}

View file

@ -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.

View file

@ -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.

View file

@ -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);
}
}

View file

@ -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));
}
}

View file

@ -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;

View file

@ -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

View file

@ -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);

View file

@ -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.
*/

View file

@ -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));
}
}
}

View file

@ -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();
}

View file

@ -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;
/**

View file

@ -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;
}

View file

@ -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() {}
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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.

View file

@ -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());
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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));

View file

@ -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;
}

View file

@ -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();

View file

@ -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:

View file

@ -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

View file

@ -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);

View file

@ -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

View file

@ -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())

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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)));

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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);

View file

@ -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();
}

View file

@ -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();

View file

@ -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) {

View file

@ -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) {

View file

@ -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(

View file

@ -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 =

View file

@ -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);

View file

@ -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);
}

View file

@ -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);

View file

@ -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) {

View file

@ -1070,6 +1070,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
drmSessionManager,
drmEventDispatcher,
overridingDrmInitData);
sampleQueue.setStartTimeUs(lastSeekPositionUs);
if (isAudioVideo) {
sampleQueue.setDrmInitData(drmInitData);
}

View file

@ -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.

View file

@ -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);
}
}

View file

@ -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 {

View file

@ -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");

View file

@ -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) {

View file

@ -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);

View file

@ -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();
}
}

View file

@ -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.
*

View file

@ -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"/>

View file

@ -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"/>

View file

@ -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>

View file

@ -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;'>&#12362;&#12399;&#12424;</span> <span style='"
+ "-webkit-text-emphasis-style:open sesame;text-emphasis-style:open sesame;"
+ "-webkit-text-emphasis-position:under left;text-emphasis-position:under left;"
+ "display:inline-block;'>&#12372;&#12374;&#12356;&#12414;&#12377;</span>");
}
@Test
public void convert_supportsUnderlineSpan() {
SpannableString spanned = new SpannableString("String with underlined section.");

View file

@ -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')

View file

@ -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;

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -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>

View 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.

View file

@ -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.

View file

@ -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.

View 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>

View 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>

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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