Commit 0ba317b1 by Marc Baechinger Committed by GitHub

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

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